Skip to content

Improved Constructors for Nonce #307

@dimitribouniol

Description

@dimitribouniol

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.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 😍).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions