Improved Constructors for Nonce
- Dominant language
- Assembly
- Stars
- 1.7k
- Forks
- 223
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 7
Description
### New API Proposal: Nonce from `HKDF` Key Derivation
#### Motivation:
It is quite common to derive a Nonce from the `HKDF` family of transformations, as outlined in [RFC8188](https://datatracker.ietf.org/doc/html/rfc8188#section-2.3) for instance. This came up in the [open source slack](https://swift-open-source.slack.com/archives/C04SH3Y3X27/p1732619191827849).
#### Importance:
Currently, this is possible by deriving a symmetric key, then accessing its unsafeBytes before converting the type to a Nonce. Implementing the above referenced RFC for an implementation of WebPush, this would ultimately look like:
```swift
let applicationServerECDHKey = P256.KeyAgreement.PrivateKey()
let payload = try JSONEncoder().encode(alert)
guard
let userAuthentication = URLEncodedBase64(webPushToken.keys.auth).decodedBytes,
let userAgentECDHPublicKey = URLEncodedBase64(webPushToken.keys.p256dh).decodedBytes,
let userAgentECDHKey = try? P256.KeyAgreement.PublicKey(x963Representation: userAgentECDHPublicKey),
let sharedSecret = try? applicationServerECDHKey.sharedSecretFromKeyAgreement(with: userAgentECDHKey)
else { throw BadSubscription() }
var salt: [UInt8] = Array(repeating: 0, count: 16)
for index in salt.indices { salt[index] = .random(in: .min ... .max) }
let paddedPayloadSize = max(payload.count, 3993)
let paddedPayload = payload + [0x02] + Array(repeating: 0, count: paddedPayloadSize - payload.count)
let recordSize = UInt32(paddedPayload.count + 16)
let keyID = applicationServerECDHKey.publicKey.x963Representation
let keyIDSize = UInt8(keyID.count)
let contentCodingHeader = salt + recordSize.bigEndianBytes + keyIDSize.bigEndianBytes + keyID
let keyInfo = "WebPush: info".utf8Bytes + [0x00] + userAgentECDHKey.x963Representation + applicationServerECDHKey.publicKey.x963Representation
let contentEncryptionKeyInfo = "Content-Encoding: aes128gcm".utf8Bytes + [0x00]
let nonceInfo = "Content-Encoding: nonce".utf8Bytes + [0x00]
let inputKeyMaterial = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: userAuthentication, sharedInfo: keyInfo, outputByteCount: 32)
let contentEncryptionKey = HKDF.deriveKey(inputKeyMaterial: inputKeyMaterial, salt: salt, info: contentEncryptionKeyInfo, outputByteCount: 16)
let nonce = HKDF.deriveKey(inputKeyMaterial: inputKeyMaterial, salt: salt, info: nonceInfo, outputByteCount: 12)
let encryptedRecord = try nonce.withUnsafeBytes { nonceBytes in
try AES.GCM.seal(paddedPayload, using: contentEncryptionKey, nonce: .init(data: nonceBytes))
}
let requestContent = contentCodingHeader + encryptedRecord.ciphertext + encryptedRecord.tag
```
However, this sticks out quite a bit against the semantically pleasant `deriveKey` APIs.
#### Proposed Improvements
We could either add conversions between SymmetricKey and Nonce, but this feels like the wrong approach. Alternatively, we could define `deriveNonce` alternatives to the above to directly return a Nonce. Either way, a non-unsafe way to get a nonce suitable for sealing a payload with a given algorithm would be welcome (even better if we could use the new generic integer parameters to correctly determine things like sizing 😍).
Contributor guide
Research direction
Start by reviewing the existing HKDF deriveKey APIs and the Nonce initializers referenced in the proposal. Determine which API direction is accepted—direct nonce derivation or safe SymmetricKey conversion—and define completion around deriving a nonce suitable for the requested sealing algorithm without unsafe byte access.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- cryptography
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 30/100