-
Notifications
You must be signed in to change notification settings - Fork 198
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 for instance. This came up in the open source slack.
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:
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<SHA256>.deriveKey(inputKeyMaterial: inputKeyMaterial, salt: salt, info: contentEncryptionKeyInfo, outputByteCount: 16)
let nonce = HKDF<SHA256>.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.tagHowever, 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 😍).