apple / apple/swift-crypto

Improved Constructors for Nonce

Open
#307 1 comment 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.