SecRandomCopyBytes on macOS: Fail-Closed Random Bytes in Swift
Use SecRandomCopyBytes safely from Swift: check OSStatus, discard failed output, encode bytes deliberately, and choose CryptoKit when the value is a key.
Random bytes are useful for protocol nonces, opaque bearer tokens, and inputs to cryptographic constructions. The difficult part is not making bytes appear random; it is choosing an API that matches the value, treating failure as failure, and keeping binary data out of text-oriented code. On Apple platforms, Security.framework exposes SecRandomCopyBytes for callers that need a raw buffer. If the value is a cryptographic key or a random integer, a higher-level API is usually a better fit.
This guide covers the raw-byte path in Swift. It does not make a generated token an authorization policy, explain key storage, or replace a protocol’s requirements for token length and lifetime.
Choose the representation before the generator
Use the abstraction that expresses what the application needs:
- For a symmetric key managed by CryptoKit, construct a
SymmetricKeywith the required algorithm-specific size. CryptoKit generates the key material; application code does not need to pass a raw key buffer around just to initialize the primitive. - For a random integer or an element selected uniformly from a range, use Swift’s
SystemRandomNumberGeneratorthrough APIs such asInt.random(in:). Do not fill bytes and reduce the result with% upperBound: unless the source range divides evenly, some values occur more often than others. - For a raw byte sequence required by a C API, wire format, or token format, call
SecRandomCopyBytesand retain the result asDataor another byte container. Random bytes are not guaranteed to be valid UTF-8.
These choices solve different problems. A random identifier is not automatically a secret, a hash is not a random generator, and a byte array is not automatically a correctly sized key. Use the consuming protocol or cryptographic primitive’s documented size and encoding rules.
Request bytes and propagate the status
SecRandomCopyBytes accepts a random source, a byte count, and a writable pointer. Passing kSecRandomDefault selects Apple’s default source. The function reports success with errSecSuccess; any other OSStatus means the caller did not receive a usable result. Do not ignore the status or return a buffer that may have been only partly filled.
The helper below returns exactly the requested number of bytes only on success. A zero-length request is rejected at the application boundary so that the pointer is always expected to refer to a nonempty buffer.
import Foundation
import Security
enum SecureRandomError: Error {
case invalidCount
case osStatus(OSStatus)
}
func secureRandomBytes(count: Int = 32) throws -> Data {
guard count > 0 else {
throw SecureRandomError.invalidCount
}
var bytes = Data(count: count)
let status: OSStatus = bytes.withUnsafeMutableBytes { buffer in
guard let address = buffer.baseAddress else {
return errSecParam
}
return SecRandomCopyBytes(kSecRandomDefault, buffer.count, address)
}
guard status == errSecSuccess else {
// Preserve the diagnostic status, but never return this buffer.
throw SecureRandomError.osStatus(status)
}
return bytes
}
The sample’s default is 32 bytes, or 256 bits, because that is a convenient example size for an opaque token. It is not a universal key-size recommendation. Select a token length according to the protocol’s entropy and transport constraints; select a key size according to the algorithm. Avoid arbitrarily large counts: allocation can fail before the random API is called, so production code that accepts a caller-controlled count should impose a documented upper bound.
Check for exact equality with errSecSuccess. Do not assume that a positive status means success, and do not silently retry forever. If failure matters to the caller, propagate it as above; if the application can continue without the value, take an explicit alternate path that does not weaken authentication or uniqueness.
Keep bytes binary and secret
Do not use String(decoding: bytes, as: UTF8.self) as a display conversion. It replaces invalid sequences and loses the one-to-one relationship between the original bytes and the text. If a protocol requires text, use the encoding it specifies. Standard Base64 is suitable for many transport formats, but it is not automatically Base64URL: URL-safe alphabets and padding rules must match the receiving protocol. Hexadecimal is readable for diagnostics but doubles the output length and should not be used as a reason to log a secret.
Keep a bearer token out of logs, crash annotations, analytics, screenshots, and error messages. Return only a redacted identifier or a short status code to diagnostics. If the bytes are key material, prefer a key type and a storage mechanism designed for keys rather than persisting a Data value in a general preferences file. Secure generation cannot compensate for an unsafe lifetime or storage location.
For values with structure, avoid ad hoc byte arithmetic. To choose a bounded integer, use the range-based Swift API, which handles uniform selection for the range. If an external protocol requires a byte-level construction such as a nonce, follow that protocol’s exact width, uniqueness scope, and reuse rule. A cryptographically strong generator does not by itself prevent accidental reuse after the application copies, caches, or restores old state.
Verify the call without printing the result
A basic test should assert that a successful helper returns the requested length and that a failure path never returns bytes. It should not print the generated value. To test failure deterministically, put the system call behind a small injected closure in the production wrapper, then inject a closure that returns a chosen non-success OSStatus; verify the wrapper throws and that downstream code receives no token. Keep that test seam out of the API callers use to request production tokens.
Do not treat a handful of generated values, a frequency chart, or a test that “looks random” as evidence that the source is cryptographically secure. Such tests can catch wiring mistakes, but they cannot validate the operating system’s entropy source. Test the contract you own: count, status propagation, encoding, and the rule that failed output is discarded.
The code uses Apple’s Security framework and Swift’s Data API. Compile it against the SDK and deployment target your application actually supports; check availability when a higher-level CryptoKit API is not present on every target. SystemRandomNumberGenerator is the standard library option for Swift scalar randomness; raw-byte callers should still use the documented Security API rather than inventing a seed or maintaining a home-grown generator.
Related:
- Secure Enclave Keys on macOS: Keychain Access Control Without Exportable Private Material
- Passkeys on macOS: AuthenticationServices Requests and Server Verification
Sources: