Skip to content

Commit

Permalink
Bring over the WWDC 2023 CryptoKit API (#181)
Browse files Browse the repository at this point in the history
Motivation

WWDC has arrived! 🎉 As part of the celebration, let's
bring Crypto up to speed with the new CryptoKit API surface.

Modifications

Substantial new docstrings
HPKE support

Result

WWDC 2023 support.
  • Loading branch information
Lukasa authored Jun 13, 2023
1 parent 940a631 commit c433cd3
Show file tree
Hide file tree
Showing 58 changed files with 3,989 additions and 502 deletions.
9 changes: 8 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,14 @@ let package = Package(
]
),
.executableTarget(name: "crypto-shasum", dependencies: ["Crypto"]),
.testTarget(name: "CryptoTests", dependencies: ["Crypto"], swiftSettings: swiftSettings),
.testTarget(
name: "CryptoTests",
dependencies: ["Crypto"],
resources: [
.copy("HPKE/hpke-test-vectors.json"),
],
swiftSettings: swiftSettings
),
.testTarget(name: "_CryptoExtrasTests", dependencies: ["_CryptoExtras"]),
],
cxxLanguageStandard: .cxx11
Expand Down
118 changes: 84 additions & 34 deletions Sources/Crypto/AEADs/AES/GCM/AES-GCM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,85 +24,117 @@ typealias AESGCMImpl = OpenSSLAESGCMImpl
import Foundation

extension AES {
/// AES in GCM mode with 128-bit tags.
/// The Advanced Encryption Standard (AES) Galois Counter Mode (GCM) cipher
/// suite.
public enum GCM: Cipher {
static let tagByteCount = 16
static let defaultNonceByteCount = 12

/// Encrypts and authenticates data using AES-GCM.
/// Secures the given plaintext message with encryption and an
/// authentication tag that covers both the encrypted data and
/// additional data.
///
/// - Parameters:
/// - message: The message to encrypt and authenticate
/// - key: An encryption key of 128, 192 or 256 bits
/// - nonce: An Nonce for AES-GCM encryption. The nonce must be unique for every use of the key to seal data. It can be safely generated with AES.GCM.Nonce()
/// - authenticatedData: Data to authenticate as part of the seal
/// - Returns: A sealed box returning the authentication tag (seal) and the ciphertext
/// - Throws: CipherError errors
/// - message: The plaintext data to seal.
/// - key: A cryptographic key used to seal the message.
/// - nonce: The nonce the sealing process requires. If you don't provide a nonce, the method generates a random one by invoking ``AES.GCM.Nonce()``.
/// - authenticatedData: Additional data to be authenticated.
///
/// - Returns: The sealed message.
public static func seal<Plaintext: DataProtocol, AuthenticatedData: DataProtocol>
(_ message: Plaintext, using key: SymmetricKey, nonce: Nonce? = nil, authenticating authenticatedData: AuthenticatedData) throws -> SealedBox {
return try AESGCMImpl.seal(key: key, message: message, nonce: nonce, authenticatedData: authenticatedData)
}

/// Encrypts and authenticates data using AES-GCM.
/// Secures the given plaintext message with encryption and an
/// authentication tag.
///
/// - Parameters:
/// - message: The message to encrypt and authenticate
/// - key: An encryption key of 128, 192 or 256 bits
/// - nonce: An Nonce for AES-GCM encryption. The nonce must be unique for every use of the key to seal data. It can be safely generated with AES.GCM.Nonce()
/// - Returns: A sealed box returning the authentication tag (seal) and the ciphertext
/// - Throws: CipherError errors
/// - message: The plaintext data to seal.
/// - key: A cryptographic key used to seal the message.
/// - nonce: The nonce the sealing process requires. If you don't provide a nonce, the method generates a random one by invoking ``AES.GCM.Nonce()``.
///
/// - Returns: The sealed message.
public static func seal<Plaintext: DataProtocol>
(_ message: Plaintext, using key: SymmetricKey, nonce: Nonce? = nil) throws -> SealedBox {
return try AESGCMImpl.seal(key: key, message: message, nonce: nonce, authenticatedData: Data?.none)
}

/// Authenticates and decrypts data using AES-GCM.
/// Decrypts the message and verifies the authenticity of both the
/// encrypted message and additional data.
///
/// - Parameters:
/// - sealedBox: The sealed box to authenticate and decrypt
/// - key: An encryption key of 128, 192 or 256 bits
/// - nonce: An Nonce for AES-GCM encryption. The nonce must be unique for every use of the key to seal data. It can be safely generated with AES.GCM.Nonce().
/// - authenticatedData: Data that was authenticated as part of the seal
/// - Returns: The ciphertext if opening was successful
/// - Throws: CipherError errors. If the authentication of the sealedbox failed, incorrectTag is thrown.
/// - sealedBox: The sealed box to open.
/// - key: The cryptographic key that was used to seal the message.
/// - authenticatedData: Additional data that was authenticated.
///
/// - Returns: The original plaintext message that was sealed in the
/// box, as long as the correct key is used and authentication succeeds.
/// The call throws an error if decryption or authentication fail.
public static func open<AuthenticatedData: DataProtocol>
(_ sealedBox: SealedBox, using key: SymmetricKey, authenticating authenticatedData: AuthenticatedData) throws -> Data {
return try AESGCMImpl.open(key: key, sealedBox: sealedBox, authenticatedData: authenticatedData)
}

/// Authenticates and decrypts data using AES-GCM.
/// Decrypts the message and verifies its authenticity.
///
/// - Parameters:
/// - sealedBox: The sealed box to authenticate and decrypt
/// - key: An encryption key of 128, 192 or 256 bits
/// - nonce: An Nonce for AES-GCM encryption. The nonce must be unique for every use of the key to seal data. It can be safely generated with AES.GCM.Nonce().
/// - Returns: The ciphertext if opening was successful
/// - Throws: CipherError errors. If the authentication of the sealedbox failed, incorrectTag is thrown.
/// - sealedBox: The sealed box to open.
/// - key: The cryptographic key that was used to seal the message.
///
/// - Returns: The original plaintext message that was sealed in the
/// box, as long as the correct key is used and authentication succeeds.
/// The call throws an error if decryption or authentication fail.
public static func open(_ sealedBox: SealedBox, using key: SymmetricKey) throws -> Data {
return try AESGCMImpl.open(key: key, sealedBox: sealedBox, authenticatedData: Data?.none)
}
}
}

extension AES.GCM {
/// A secure container for your data that you can access using a cipher.
///
/// Use a sealed box as a container for data that you want to transmit
/// securely. Seal data into a box with one of the cipher algorithms, like
/// ``seal(_:using:nonce:)``.
///
/// The box holds an encrypted version of the original data, an
/// authentication tag, and the nonce during encryption. The encryption
/// makes the data unintelligible to anyone without the key, while the
/// authentication tag makes it possible for the intended receiver to be
/// sure the data remains intact.
///
/// The receiver uses another instance of the same cipher, like the
/// ``open(_:using:)`` method, to open the box.
public struct SealedBox: AEADSealedBox {
private let combinedRepresentation: Data
private let nonceByteCount: Int

/// The authentication tag
/// An authentication tag.
///
/// The authentication tag has a length of 16 bytes.
public var tag: Data {
return combinedRepresentation.suffix(AES.GCM.tagByteCount)
}
/// The ciphertext
/// The encrypted data.
///
/// The length of the ciphertext of a sealed box is the same as the
/// length of the plaintext you encrypt.
public var ciphertext: Data {
return combinedRepresentation.dropFirst(nonceByteCount).dropLast(AES.GCM.tagByteCount)
}
/// The Nonce
/// The nonce used to encrypt the data.
public var nonce: AES.GCM.Nonce {
return try! AES.GCM.Nonce(data: combinedRepresentation.prefix(nonceByteCount))
}

/// The combined representation ( nonce || ciphertext || tag)
/// A combined element composed of the nonce, encrypted data, and
/// authentication tag.
///
/// The combined representation is only available when the
/// ``AES/GCM/Nonce`` size is the default size of 12 bytes. The data
/// layout of the combined representation is nonce, ciphertext, then
/// tag.
public var combined: Data? {
if nonceByteCount == AES.GCM.defaultNonceByteCount {
return self.combinedRepresentation
Expand All @@ -122,6 +154,12 @@ extension AES.GCM {
self.nonceByteCount = nonceByteCount
}

/// Creates a sealed box from the combined bytes of an authentication
/// tag, nonce, and encrypted data.
///
/// - Parameters:
/// - combined: The combined bytes of the nonce, encrypted data, and
/// authentication tag.
@inlinable
public init<D: DataProtocol>(combined: D) throws {
// AES minimum nonce (12 bytes) + AES tag (16 bytes)
Expand All @@ -135,13 +173,25 @@ extension AES.GCM {
self.init(combined: Data(combined))
}

/// Creates a sealed box from the given tag, nonce, and ciphertext.
///
/// - Parameters:
/// - nonce: The nonce.
/// - ciphertext: The encrypted data.
/// - tag: The authentication tag.
public init<C: DataProtocol, T: DataProtocol>(nonce: AES.GCM.Nonce, ciphertext: C, tag: T) throws {
guard tag.count == AES.GCM.tagByteCount else {
throw CryptoKitError.incorrectParameterSize
}

self.combinedRepresentation = nonce.bytes + ciphertext + tag
self.nonceByteCount = nonce.bytes.count

let nonceByteCount = nonce.bytes.count
var combinedRepresentation = Data()
combinedRepresentation.reserveCapacity(nonceByteCount + ciphertext.count + tag.count)
combinedRepresentation.append(contentsOf: nonce.bytes)
combinedRepresentation.append(contentsOf: ciphertext)
combinedRepresentation.append(contentsOf: tag)

self.init(combined: combinedRepresentation, nonceByteCount: nonceByteCount)
}

}
Expand Down
98 changes: 67 additions & 31 deletions Sources/Crypto/AEADs/ChachaPoly/ChaChaPoly.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,85 +23,115 @@ typealias ChaChaPolyImpl = OpenSSLChaChaPolyImpl

import Foundation

/// ChaCha20-Poly1305 as described in RFC 7539 with 96-bit nonces.
/// An implementation of the ChaCha20-Poly1305 cipher.
public enum ChaChaPoly: Cipher {
static let tagByteCount = 16
static let keyBitsCount = 256
static let nonceByteCount = 12

/// Encrypts and seals data using ChaCha20-Poly1305.
/// Secures the given plaintext message with encryption and an
/// authentication tag that covers both the encrypted data and additional
/// data.
///
/// - Parameters:
/// - message: The message to encrypt and authenticate
/// - key: A 256-bit encryption key
/// - nonce: A nonce for ChaChaPoly encryption. The nonce must be unique for every use of the key to seal data. It can be safely generated with ChaChaPoly.Nonce()
/// - authenticatedData: Data to authenticate as part of the seal
/// - Returns: A sealed box returning the authentication tag (seal) and the ciphertext
/// - Throws: CipherError errors
/// - message: The plaintext data to seal.
/// - key: A cryptographic key used to seal the message.
/// - nonce: The nonce the sealing process requires. If you don't provide a nonce, the method generates a random one by invoking ``ChaChaPoly.Nonce()``.
/// - authenticatedData: Additional data to be authenticated.
///
/// - Returns: The sealed message.
public static func seal<Plaintext: DataProtocol, AuthenticatedData: DataProtocol>
(_ message: Plaintext, using key: SymmetricKey, nonce: Nonce? = nil, authenticating authenticatedData: AuthenticatedData) throws -> SealedBox {
return try ChaChaPolyImpl.encrypt(key: key, message: message, nonce: nonce, authenticatedData: authenticatedData)
}

/// Encrypts and seals data using ChaCha20-Poly1305.
/// Secures the given plaintext message with encryption and an
/// authentication tag.
///
/// - Parameters:
/// - message: The message to encrypt and authenticate
/// - key: A 256-bit encryption key
/// - nonce: A nonce for ChaChaPoly encryption. The nonce must be unique for every use of the key to seal data. It can be safely generated with ChaChaPoly.Nonce()
/// - Returns: A sealed box returning the authentication tag (seal) and the ciphertext
/// - Throws: CipherError errors
/// - message: The plaintext data to seal.
/// - key: A cryptographic key used to seal the message.
/// - nonce: The nonce the sealing process requires. If you don't provide a nonce, the method generates a random one by invoking ``ChaChaPoly.Nonce()``.
///
/// - Returns: The sealed message.
public static func seal<Plaintext: DataProtocol>
(_ message: Plaintext, using key: SymmetricKey, nonce: Nonce? = nil) throws -> SealedBox {
return try ChaChaPolyImpl.encrypt(key: key, message: message, nonce: nonce, authenticatedData: Data?.none)
}

/// Authenticates and decrypts data using ChaCha20-Poly1305.
/// Decrypts the message and verifies the authenticity of both the encrypted
/// message and additional data.
///
/// - Parameters:
/// - sealedBox: The sealed box to authenticate and decrypt
/// - key: A 256-bit encryption key
/// - nonce: The nonce that was provided for encryption.
/// - authenticatedData: Data that was authenticated as part of the seal
/// - Returns: The ciphertext if opening was successful
/// - Throws: CipherError errors. If the authentication of the sealedbox failed, incorrectTag is thrown.
/// - sealedBox: The sealed box to open.
/// - key: The cryptographic key that was used to seal the message.
/// - authenticatedData: Additional data that was authenticated.
///
/// - Returns: The original plaintext message that was sealed in the box, as
/// long as the correct key is used and authentication succeeds. The call
/// throws an error if decryption or authentication fail.
public static func open<AuthenticatedData: DataProtocol>
(_ sealedBox: SealedBox, using key: SymmetricKey, authenticating authenticatedData: AuthenticatedData) throws -> Data {
return try ChaChaPolyImpl.decrypt(key: key, ciphertext: sealedBox, authenticatedData: authenticatedData)
}

/// Authenticates and decrypts data using ChaCha20-Poly1305.
/// Decrypts the message and verifies its authenticity.
///
/// - Parameters:
/// - sealedBox: The sealed box to authenticate and decrypt
/// - key: A 256-bit encryption key
/// - nonce: The nonce that was provided for encryption.
/// - Returns: The ciphertext if opening was successful
/// - Throws: CipherError errors. If the authentication of the sealedbox failed, incorrectTag is thrown.
/// - sealedBox: The sealed box to open.
/// - key: The cryptographic key that was used to seal the message.
///
/// - Returns: The original plaintext message that was sealed in the box, as
/// long as the correct key is used and authentication succeeds. The call
/// throws an error if decryption or authentication fail.
public static func open
(_ sealedBox: SealedBox, using key: SymmetricKey) throws -> Data {
return try ChaChaPolyImpl.decrypt(key: key, ciphertext: sealedBox, authenticatedData: Data?.none)
}
}

extension ChaChaPoly {
/// A secure container for your data that you access using a cipher.
///
/// Use a sealed box as a container for data that you want to transmit
/// securely. Seal data into a box with one of the cipher algorithms, like
/// ``seal(_:using:nonce:)``.
///
/// The box holds an encrypted version of the original data, an
/// authentication tag, and the nonce during encryption. The encryption
/// makes the data unintelligible to anyone without the key, while the
/// authentication tag makes it possible for the intended receiver to be
/// sure the data remains intact.
///
/// The receiver uses another instance of the same cipher, like the
/// ``open(_:using:)`` method, to open the box.
@frozen
public struct SealedBox: AEADSealedBox {
/// The combined representation ( nonce || ciphertext || tag)
/// A combined element composed of the tag, the nonce, and the
/// ciphertext.
///
/// The data layout of the combined representation is: nonce,
/// ciphertext, then tag.
public let combined: Data
/// The authentication tag
/// An authentication tag.
///
/// The authentication tag has a length of 16 bytes.
public var tag: Data {
return combined.suffix(ChaChaPoly.tagByteCount)
}
/// The ciphertext
/// The encrypted data.
public var ciphertext: Data {
return combined.dropFirst(ChaChaPoly.nonceByteCount).dropLast(ChaChaPoly.tagByteCount)
}
/// The Nonce
/// The nonce used to encrypt the data.
public var nonce: ChaChaPoly.Nonce {
return try! ChaChaPoly.Nonce(data: combined.prefix(ChaChaPoly.nonceByteCount))
}

/// Creates a sealed box from the given data.
///
/// - Parameters:
/// - combined: The combined bytes of the tag and ciphertext.
@inlinable
public init<D: DataProtocol>(combined: D) throws {
// ChachaPoly nonce (12 bytes) + ChachaPoly tag (16 bytes)
Expand All @@ -115,6 +145,12 @@ extension ChaChaPoly {
self.combined = Data(combined)
}

/// Creates a sealed box from the given tag, nonce, and ciphertext.
///
/// - Parameters:
/// - nonce: The nonce.
/// - ciphertext: The encrypted data.
/// - tag: An authentication tag.
public init<C: DataProtocol, T: DataProtocol>(nonce: ChaChaPoly.Nonce, ciphertext: C, tag: T) throws {
guard tag.count == ChaChaPoly.tagByteCount else {
throw CryptoKitError.incorrectParameterSize
Expand Down
Loading

0 comments on commit c433cd3

Please sign in to comment.