From d11194ad267d645890f6e9cbcc7911c871eab544 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 14 Oct 2022 09:50:36 +0100 Subject: [PATCH] Provide AES-GCM-SIV in CryptoExtras (#133) # Motivation BoringSSL exposes AES-GCM-SIV algorithms which are in general useful to have and provide a nonce-misuse resistant mode of AES-GCM. Since, `CryptoKit` is not exposing AES-GCM-SIV we need to add this to `_CryptoExtras` # Modification Exposes `AES-GCM-SIV` through `_CryptoExtras`. # Result We can now use `AES-GCM-SIV` through `_CryptoExtras`. --- Package.swift | 18 +- .../include/CCryptoBoringSSLShims.h | 7 + Sources/CCryptoBoringSSLShims/shims.c | 7 + Sources/CMakeLists.txt | 1 + .../AES/GCM/BoringSSL/AES-GCM_boring.swift | 1 + .../BoringSSL/ChaChaPoly_boring.swift | 227 +- Sources/Crypto/CMakeLists.txt | 6 +- .../AEAD/BoringSSLAEAD.swift | 271 +++ Sources/CryptoBoringWrapper/CMakeLists.txt | 30 + .../CryptoKitErrors_boring.swift | 27 + Sources/_CryptoExtras/AES/AES_GCM_SIV.swift | 160 ++ .../AES/BoringSSL/AES_GCM_SIV_boring.swift | 87 + .../Util/CryptoKitErrors_boring.swift | 3 - Sources/_CryptoExtras/Util/RandomBytes.swift | 41 + .../AES-GCM-SIV-Runner.swift | 232 ++ .../_CryptoExtrasTests/Utils/SplitData.swift | 37 + .../aes_gcm_siv_test.json | 1912 +++++++++++++++++ scripts/soundness.sh | 2 +- 18 files changed, 2856 insertions(+), 213 deletions(-) create mode 100644 Sources/CryptoBoringWrapper/AEAD/BoringSSLAEAD.swift create mode 100644 Sources/CryptoBoringWrapper/CMakeLists.txt create mode 100644 Sources/CryptoBoringWrapper/CryptoKitErrors_boring.swift create mode 100644 Sources/_CryptoExtras/AES/AES_GCM_SIV.swift create mode 100644 Sources/_CryptoExtras/AES/BoringSSL/AES_GCM_SIV_boring.swift create mode 100644 Sources/_CryptoExtras/Util/RandomBytes.swift create mode 100644 Tests/_CryptoExtrasTests/AES-GCM-SIV-Runner.swift create mode 100644 Tests/_CryptoExtrasTests/Utils/SplitData.swift create mode 100644 Tests/_CryptoExtrasVectors/aes_gcm_siv_test.json diff --git a/Package.swift b/Package.swift index 975eded..70a10b1 100644 --- a/Package.swift +++ b/Package.swift @@ -36,7 +36,8 @@ if development { ] dependencies = [ "CCryptoBoringSSL", - "CCryptoBoringSSLShims" + "CCryptoBoringSSLShims", + "CryptoBoringWrapper" ] } else { swiftSettings = [ @@ -49,7 +50,8 @@ if development { ] dependencies = [ .target(name: "CCryptoBoringSSL", condition: .when(platforms: platforms)), - .target(name: "CCryptoBoringSSLShims", condition: .when(platforms: platforms)) + .target(name: "CCryptoBoringSSLShims", condition: .when(platforms: platforms)), + .target(name: "CryptoBoringWrapper", condition: .when(platforms: platforms)) ] } @@ -105,7 +107,17 @@ let package = Package( ], swiftSettings: swiftSettings ), - .target(name: "_CryptoExtras", dependencies: ["CCryptoBoringSSL", "CCryptoBoringSSLShims", "Crypto"]), + .target(name: "_CryptoExtras", dependencies: ["CCryptoBoringSSL", "CCryptoBoringSSLShims", "CryptoBoringWrapper", "Crypto"]), + .target( + name: "CryptoBoringWrapper", + dependencies: [ + "CCryptoBoringSSL", + "CCryptoBoringSSLShims" + ], + exclude: [ + "CMakeLists.txt", + ] + ), .executableTarget(name: "crypto-shasum", dependencies: ["Crypto"]), .testTarget(name: "CryptoTests", dependencies: ["Crypto"], swiftSettings: swiftSettings), .testTarget(name: "_CryptoExtrasTests", dependencies: ["_CryptoExtras"]), diff --git a/Sources/CCryptoBoringSSLShims/include/CCryptoBoringSSLShims.h b/Sources/CCryptoBoringSSLShims/include/CCryptoBoringSSLShims.h index a665f1c..f427165 100644 --- a/Sources/CCryptoBoringSSLShims/include/CCryptoBoringSSLShims.h +++ b/Sources/CCryptoBoringSSLShims/include/CCryptoBoringSSLShims.h @@ -58,6 +58,13 @@ int CCryptoBoringSSLShims_EVP_AEAD_CTX_open_gather(const EVP_AEAD_CTX *ctx, void const void *in_tag, size_t in_tag_len, const void *ad, size_t ad_len); + +int CCryptoBoringSSLShims_EVP_AEAD_CTX_open(const EVP_AEAD_CTX *ctx, void *out, size_t *out_len, size_t max_out_len, + const void *nonce, size_t nonce_len, + const void *in, size_t in_len, + const void *ad, size_t ad_len); + + void CCryptoBoringSSLShims_ED25519_keypair(void *out_public_key, void *out_private_key); void CCryptoBoringSSLShims_ED25519_keypair_from_seed(void *out_public_key, diff --git a/Sources/CCryptoBoringSSLShims/shims.c b/Sources/CCryptoBoringSSLShims/shims.c index dcefc8b..c1c29cc 100644 --- a/Sources/CCryptoBoringSSLShims/shims.c +++ b/Sources/CCryptoBoringSSLShims/shims.c @@ -55,6 +55,13 @@ int CCryptoBoringSSLShims_EVP_AEAD_CTX_open_gather(const EVP_AEAD_CTX *ctx, void return CCryptoBoringSSL_EVP_AEAD_CTX_open_gather(ctx, out, nonce, nonce_len, in, in_len, in_tag, in_tag_len, ad, ad_len); } +int CCryptoBoringSSLShims_EVP_AEAD_CTX_open(const EVP_AEAD_CTX *ctx, void *out, size_t *out_len, size_t max_out_len, + const void *nonce, size_t nonce_len, + const void *in, size_t in_len, + const void *ad, size_t ad_len) { + return CCryptoBoringSSL_EVP_AEAD_CTX_open(ctx, out, out_len, max_out_len, nonce, nonce_len, in, in_len, ad, ad_len); +} + void CCryptoBoringSSLShims_ED25519_keypair(void *out_public_key, void *out_private_key) { CCryptoBoringSSL_ED25519_keypair(out_public_key, out_private_key); } diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index 7f1909f..35d3fd8 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -14,4 +14,5 @@ add_subdirectory(CCryptoBoringSSL) add_subdirectory(CCryptoBoringSSLShims) +add_subdirectory(CryptoBoringWrapper) add_subdirectory(Crypto) diff --git a/Sources/Crypto/AEADs/AES/GCM/BoringSSL/AES-GCM_boring.swift b/Sources/Crypto/AEADs/AES/GCM/BoringSSL/AES-GCM_boring.swift index 069be88..92fc020 100644 --- a/Sources/Crypto/AEADs/AES/GCM/BoringSSL/AES-GCM_boring.swift +++ b/Sources/Crypto/AEADs/AES/GCM/BoringSSL/AES-GCM_boring.swift @@ -15,6 +15,7 @@ @_exported import CryptoKit #else @_implementationOnly import CCryptoBoringSSL +@_implementationOnly import CryptoBoringWrapper import Foundation enum OpenSSLAESGCMImpl { diff --git a/Sources/Crypto/AEADs/ChachaPoly/BoringSSL/ChaChaPoly_boring.swift b/Sources/Crypto/AEADs/ChachaPoly/BoringSSL/ChaChaPoly_boring.swift index 1dda067..e7eea00 100644 --- a/Sources/Crypto/AEADs/ChachaPoly/BoringSSL/ChaChaPoly_boring.swift +++ b/Sources/Crypto/AEADs/ChachaPoly/BoringSSL/ChaChaPoly_boring.swift @@ -16,8 +16,31 @@ #else @_implementationOnly import CCryptoBoringSSL @_implementationOnly import CCryptoBoringSSLShims +@_implementationOnly import CryptoBoringWrapper import Foundation +extension BoringSSLAEAD { + /// Seal a given message. + func seal(message: Plaintext, key: SymmetricKey, nonce: Nonce, authenticatedData: AuthenticatedData) throws -> (ciphertext: Data, tag: Data) { + do { + let context = try AEADContext(cipher: self, key: key) + return try context.seal(message: message, nonce: nonce, authenticatedData: authenticatedData) + } catch CryptoBoringWrapperError.underlyingCoreCryptoError(let errorCode) { + throw CryptoKitError.underlyingCoreCryptoError(error: errorCode) + } + } + + /// Open a given message. + func open(ciphertext: Data, key: SymmetricKey, nonce: Nonce, tag: Data, authenticatedData: AuthenticatedData) throws -> Data { + do { + let context = try AEADContext(cipher: self, key: key) + return try context.open(ciphertext: ciphertext, nonce: nonce, tag: tag, authenticatedData: authenticatedData) + } catch CryptoBoringWrapperError.underlyingCoreCryptoError(let errorCode) { + throw CryptoKitError.underlyingCoreCryptoError(error: errorCode) + } + } +} + enum OpenSSLChaChaPolyImpl { static func encrypt(key: SymmetricKey, message: M, nonce: ChaChaPoly.Nonce?, authenticatedData: AD?) throws -> ChaChaPoly.SealedBox { guard key.bitCount == ChaChaPoly.keyBitsCount else { @@ -48,208 +71,4 @@ enum OpenSSLChaChaPolyImpl { } } } - -/// An abstraction over a BoringSSL AEAD -@usableFromInline -enum BoringSSLAEAD { - /// The supported AEAD ciphers for BoringSSL. - case aes128gcm - case aes192gcm - case aes256gcm - case chacha20 - - /// Seal a given message. - func seal(message: Plaintext, key: SymmetricKey, nonce: Nonce, authenticatedData: AuthenticatedData) throws -> (ciphertext: Data, tag: Data) { - let context = try AEADContext(cipher: self, key: key) - return try context.seal(message: message, nonce: nonce, authenticatedData: authenticatedData) - } - - /// Open a given message. - func open(ciphertext: Data, key: SymmetricKey, nonce: Nonce, tag: Data, authenticatedData: AuthenticatedData) throws -> Data { - let context = try AEADContext(cipher: self, key: key) - return try context.open(ciphertext: ciphertext, nonce: nonce, tag: tag, authenticatedData: authenticatedData) - } -} - -extension BoringSSLAEAD { - // Arguably this class is excessive, but it's probably better for this API to be as safe as possible - // rather than rely on defer statements for our cleanup. - class AEADContext { - private var context: EVP_AEAD_CTX - - @usableFromInline - init(cipher: BoringSSLAEAD, key: SymmetricKey) throws { - self.context = EVP_AEAD_CTX() - let rc: CInt = key.withUnsafeBytes { keyPointer in - withUnsafeMutablePointer(to: &self.context) { contextPointer in - // Create the AEAD context with a default tag length using the given key. - CCryptoBoringSSLShims_EVP_AEAD_CTX_init(contextPointer, cipher.boringSSLCipher, keyPointer.baseAddress, keyPointer.count, 0, nil) - } - } - guard rc == 1 else { - throw CryptoKitError.internalBoringSSLError() - } - } - - deinit { - withUnsafeMutablePointer(to: &self.context) { contextPointer in - CCryptoBoringSSL_EVP_AEAD_CTX_cleanup(contextPointer) - } - } - } -} - -// MARK: - Sealing - -extension BoringSSLAEAD.AEADContext { - /// The main entry point for sealing data. Covers the full gamut of types, including discontiguous data types. This must be inlinable. - @inlinable - func seal(message: Plaintext, nonce: Nonce, authenticatedData: AuthenticatedData) throws -> (ciphertext: Data, tag: Data) { - // Seal is a somewhat awkward function. As it returns a Data, we are going to need to initialize a Data large enough to write into. Data does not provide us an - // initializer that gives us access to its uninitialized memory, so the cost of creating this Data is the cost of allocating the data + the cost of initializing - // it. For smaller plaintexts this isn't too big a deal, but for larger ones the initialization cost can really get hairy. - // - // We can avoid this by using Data(bytesNoCopy:deallocator:), so that's what we do. In principle we can do slightly better in the case where we have a discontiguous Plaintext - // type, but it's honestly not worth it enough to justify the code complexity. - switch (message.regions.count, authenticatedData.regions.count) { - case (1, 1): - // We can use a nice fast-path here. - return try self._sealContiguous(message: message.regions.first!, nonce: nonce, authenticatedData: authenticatedData.regions.first!) - case (1, _): - let contiguousAD = Array(authenticatedData) - return try self._sealContiguous(message: message.regions.first!, nonce: nonce, authenticatedData: contiguousAD) - case (_, 1): - let contiguousMessage = Array(message) - return try self._sealContiguous(message: contiguousMessage, nonce: nonce, authenticatedData: authenticatedData.regions.first!) - case (_, _): - let contiguousMessage = Array(message) - let contiguousAD = Array(authenticatedData) - return try self._sealContiguous(message: contiguousMessage, nonce: nonce, authenticatedData: contiguousAD) - } - } - - /// A fast-path for sealing contiguous data. Also inlinable to gain specialization information. - @inlinable - func _sealContiguous(message: Plaintext, nonce: Nonce, authenticatedData: AuthenticatedData) throws -> (ciphertext: Data, tag: Data) { - return try message.withUnsafeBytes { messagePointer in - try nonce.withUnsafeBytes { noncePointer in - try authenticatedData.withUnsafeBytes { authenticatedDataPointer in - try self._sealContiguous(plaintext: messagePointer, noncePointer: noncePointer, authenticatedData: authenticatedDataPointer) - } - } - } - } - - /// The unsafe base call: not inlinable so that it can touch private variables. - @usableFromInline - func _sealContiguous(plaintext: UnsafeRawBufferPointer, noncePointer: UnsafeRawBufferPointer, authenticatedData: UnsafeRawBufferPointer) throws -> (ciphertext: Data, tag: Data) { - let tagByteCount = CCryptoBoringSSL_EVP_AEAD_max_overhead(self.context.aead) - - // We use malloc here because we are going to call free later. We force unwrap to trigger crashes if the allocation - // fails. - let outputBuffer = UnsafeMutableRawBufferPointer(start: malloc(plaintext.count)!, count: plaintext.count) - let tagBuffer = UnsafeMutableRawBufferPointer(start: malloc(tagByteCount)!, count: tagByteCount) - var actualTagSize = tagBuffer.count - - let rc = withUnsafeMutablePointer(to: &self.context) { contextPointer in - CCryptoBoringSSLShims_EVP_AEAD_CTX_seal_scatter(contextPointer, - outputBuffer.baseAddress, - tagBuffer.baseAddress, &actualTagSize, tagBuffer.count, - noncePointer.baseAddress, noncePointer.count, - plaintext.baseAddress, plaintext.count, - nil, 0, - authenticatedData.baseAddress, authenticatedData.count) - } - - guard rc == 1 else { - // Ooops, error. Free the memory we allocated before we throw. - free(outputBuffer.baseAddress) - free(tagBuffer.baseAddress) - throw CryptoKitError.internalBoringSSLError() - } - - let output = Data(bytesNoCopy: outputBuffer.baseAddress!, count: outputBuffer.count, deallocator: .free) - let tag = Data(bytesNoCopy: tagBuffer.baseAddress!, count: actualTagSize, deallocator: .free) - return (ciphertext: output, tag: tag) - } -} - -// MARK: - Opening - -extension BoringSSLAEAD.AEADContext { - /// The main entry point for opening data. Covers the full gamut of types, including discontiguous data types. This must be inlinable. - @inlinable - func open(ciphertext: Data, nonce: Nonce, tag: Data, authenticatedData: AuthenticatedData) throws -> Data { - // Open is a somewhat awkward function. As it returns a Data, we are going to need to initialize a Data large enough to write into. Data does not provide us an - // initializer that gives us access to its uninitialized memory, so the cost of creating this Data is the cost of allocating the data + the cost of initializing - // it. For smaller plaintexts this isn't too big a deal, but for larger ones the initialization cost can really get hairy. - // - // We can avoid this by using Data(bytesNoCopy:deallocator:), so that's what we do. In principle we can do slightly better in the case where we have a discontiguous Plaintext - // type, but it's honestly not worth it enough to justify the code complexity. - if authenticatedData.regions.count == 1 { - // We can use a nice fast-path here. - return try self._openContiguous(ciphertext: ciphertext, nonce: nonce, tag: tag, authenticatedData: authenticatedData.regions.first!) - } else { - let contiguousAD = Array(authenticatedData) - return try self._openContiguous(ciphertext: ciphertext, nonce: nonce, tag: tag, authenticatedData: contiguousAD) - } - } - - /// A fast-path for opening contiguous data. Also inlinable to gain specialization information. - @inlinable - func _openContiguous(ciphertext: Data, nonce: Nonce, tag: Data, authenticatedData: AuthenticatedData) throws -> Data { - try ciphertext.withUnsafeBytes { ciphertextPointer in - try nonce.withUnsafeBytes { nonceBytes in - try tag.withUnsafeBytes { tagBytes in - try authenticatedData.withUnsafeBytes { authenticatedDataBytes in - try self._openContiguous(ciphertext: ciphertextPointer, nonceBytes: nonceBytes, tagBytes: tagBytes, authenticatedData: authenticatedDataBytes) - } - } - } - } - } - - /// The unsafe base call: not inlinable so that it can touch private variables. - @usableFromInline - func _openContiguous(ciphertext: UnsafeRawBufferPointer, nonceBytes: UnsafeRawBufferPointer, tagBytes: UnsafeRawBufferPointer, authenticatedData: UnsafeRawBufferPointer) throws -> Data { - // We use malloc here because we are going to call free later. We force unwrap to trigger crashes if the allocation - // fails. - let outputBuffer = UnsafeMutableRawBufferPointer(start: malloc(ciphertext.count)!, count: ciphertext.count) - - let rc = withUnsafePointer(to: &self.context) { contextPointer in - CCryptoBoringSSLShims_EVP_AEAD_CTX_open_gather(contextPointer, - outputBuffer.baseAddress, - nonceBytes.baseAddress, nonceBytes.count, - ciphertext.baseAddress, ciphertext.count, - tagBytes.baseAddress, tagBytes.count, - authenticatedData.baseAddress, authenticatedData.count) - } - - guard rc == 1 else { - // Ooops, error. Free the memory we allocated before we throw. - free(outputBuffer.baseAddress) - throw CryptoKitError.internalBoringSSLError() - } - - let output = Data(bytesNoCopy: outputBuffer.baseAddress!, count: outputBuffer.count, deallocator: .free) - return output - } -} - -// MARK: - Supported ciphers - -extension BoringSSLAEAD { - var boringSSLCipher: OpaquePointer { - switch self { - case .aes128gcm: - return CCryptoBoringSSL_EVP_aead_aes_128_gcm() - case .aes192gcm: - return CCryptoBoringSSL_EVP_aead_aes_192_gcm() - case .aes256gcm: - return CCryptoBoringSSL_EVP_aead_aes_256_gcm() - case .chacha20: - return CCryptoBoringSSL_EVP_aead_chacha20_poly1305() - } - } -} #endif // (os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) && CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API diff --git a/Sources/Crypto/CMakeLists.txt b/Sources/Crypto/CMakeLists.txt index 0a5629b..6ebaa8b 100644 --- a/Sources/Crypto/CMakeLists.txt +++ b/Sources/Crypto/CMakeLists.txt @@ -85,12 +85,14 @@ target_compile_definitions(Crypto PRIVATE CRYPTO_IN_SWIFTPM_FORCE_BUILD_API) target_include_directories(Crypto PRIVATE $ - $) + $ + $) target_link_libraries(Crypto PUBLIC $<$>:dispatch> $<$>:Foundation> CCryptoBoringSSL - CCryptoBoringSSLShims) + CCryptoBoringSSLShims + CryptoBoringWrapper) set_target_properties(Crypto PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) diff --git a/Sources/CryptoBoringWrapper/AEAD/BoringSSLAEAD.swift b/Sources/CryptoBoringWrapper/AEAD/BoringSSLAEAD.swift new file mode 100644 index 0000000..4299e56 --- /dev/null +++ b/Sources/CryptoBoringWrapper/AEAD/BoringSSLAEAD.swift @@ -0,0 +1,271 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2022 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@_implementationOnly import CCryptoBoringSSL +@_implementationOnly import CCryptoBoringSSLShims +import Foundation + +/// An abstraction over a BoringSSL AEAD +public enum BoringSSLAEAD { + /// The supported AEAD ciphers for BoringSSL. + case aes128gcm + case aes192gcm + case aes256gcm + case aes128gcmsiv + case aes256gcmsiv + case chacha20 +} + +extension BoringSSLAEAD { + // Arguably this class is excessive, but it's probably better for this API to be as safe as possible + // rather than rely on defer statements for our cleanup. + public class AEADContext { + private var context: EVP_AEAD_CTX + + public init(cipher: BoringSSLAEAD, key: Key) throws { + self.context = EVP_AEAD_CTX() + + let rc: CInt = key.withUnsafeBytes { keyPointer in + withUnsafeMutablePointer(to: &self.context) { contextPointer in + // Create the AEAD context with a default tag length using the given key. + CCryptoBoringSSLShims_EVP_AEAD_CTX_init(contextPointer, cipher.boringSSLCipher, keyPointer.baseAddress, keyPointer.count, 0, nil) + } + } + + guard rc == 1 else { + throw CryptoBoringWrapperError.internalBoringSSLError() + } + } + + deinit { + withUnsafeMutablePointer(to: &self.context) { contextPointer in + CCryptoBoringSSL_EVP_AEAD_CTX_cleanup(contextPointer) + } + } + } +} + +// MARK: - Sealing + +extension BoringSSLAEAD.AEADContext { + /// The main entry point for sealing data. Covers the full gamut of types, including discontiguous data types. This must be inlinable. + public func seal(message: Plaintext, nonce: Nonce, authenticatedData: AuthenticatedData) throws -> (ciphertext: Data, tag: Data) { + // Seal is a somewhat awkward function. As it returns a Data, we are going to need to initialize a Data large enough to write into. Data does not provide us an + // initializer that gives us access to its uninitialized memory, so the cost of creating this Data is the cost of allocating the data + the cost of initializing + // it. For smaller plaintexts this isn't too big a deal, but for larger ones the initialization cost can really get hairy. + // + // We can avoid this by using Data(bytesNoCopy:deallocator:), so that's what we do. In principle we can do slightly better in the case where we have a discontiguous Plaintext + // type, but it's honestly not worth it enough to justify the code complexity. + switch (message.regions.count, authenticatedData.regions.count) { + case (1, 1): + // We can use a nice fast-path here. + return try self._sealContiguous(message: message.regions.first!, nonce: nonce, authenticatedData: authenticatedData.regions.first!) + case (1, _): + let contiguousAD = Array(authenticatedData) + return try self._sealContiguous(message: message.regions.first!, nonce: nonce, authenticatedData: contiguousAD) + case (_, 1): + let contiguousMessage = Array(message) + return try self._sealContiguous(message: contiguousMessage, nonce: nonce, authenticatedData: authenticatedData.regions.first!) + case (_, _): + let contiguousMessage = Array(message) + let contiguousAD = Array(authenticatedData) + return try self._sealContiguous(message: contiguousMessage, nonce: nonce, authenticatedData: contiguousAD) + } + } + + /// A fast-path for sealing contiguous data. Also inlinable to gain specialization information. + @inlinable + func _sealContiguous(message: Plaintext, nonce: Nonce, authenticatedData: AuthenticatedData) throws -> (ciphertext: Data, tag: Data) { + return try message.withUnsafeBytes { messagePointer in + try nonce.withUnsafeBytes { noncePointer in + try authenticatedData.withUnsafeBytes { authenticatedDataPointer in + try self._sealContiguous(plaintext: messagePointer, noncePointer: noncePointer, authenticatedData: authenticatedDataPointer) + } + } + } + } + + /// The unsafe base call: not inlinable so that it can touch private variables. + @usableFromInline + func _sealContiguous(plaintext: UnsafeRawBufferPointer, noncePointer: UnsafeRawBufferPointer, authenticatedData: UnsafeRawBufferPointer) throws -> (ciphertext: Data, tag: Data) { + let tagByteCount = CCryptoBoringSSL_EVP_AEAD_max_overhead(self.context.aead) + + // We use malloc here because we are going to call free later. We force unwrap to trigger crashes if the allocation + // fails. + let outputBuffer = UnsafeMutableRawBufferPointer(start: malloc(plaintext.count)!, count: plaintext.count) + let tagBuffer = UnsafeMutableRawBufferPointer(start: malloc(tagByteCount)!, count: tagByteCount) + var actualTagSize = tagBuffer.count + + let rc = withUnsafeMutablePointer(to: &self.context) { contextPointer in + CCryptoBoringSSLShims_EVP_AEAD_CTX_seal_scatter(contextPointer, + outputBuffer.baseAddress, + tagBuffer.baseAddress, &actualTagSize, tagBuffer.count, + noncePointer.baseAddress, noncePointer.count, + plaintext.baseAddress, plaintext.count, + nil, 0, + authenticatedData.baseAddress, authenticatedData.count) + } + + guard rc == 1 else { + // Ooops, error. Free the memory we allocated before we throw. + free(outputBuffer.baseAddress) + free(tagBuffer.baseAddress) + throw CryptoBoringWrapperError.internalBoringSSLError() + } + + let output = Data(bytesNoCopy: outputBuffer.baseAddress!, count: outputBuffer.count, deallocator: .free) + let tag = Data(bytesNoCopy: tagBuffer.baseAddress!, count: actualTagSize, deallocator: .free) + return (ciphertext: output, tag: tag) + } +} + +// MARK: - Opening + +extension BoringSSLAEAD.AEADContext { + /// The main entry point for opening data. Covers the full gamut of types, including discontiguous data types. This must be inlinable. + @inlinable + public func open(ciphertext: Data, nonce: Nonce, tag: Data, authenticatedData: AuthenticatedData) throws -> Data { + // Open is a somewhat awkward function. As it returns a Data, we are going to need to initialize a Data large enough to write into. Data does not provide us an + // initializer that gives us access to its uninitialized memory, so the cost of creating this Data is the cost of allocating the data + the cost of initializing + // it. For smaller plaintexts this isn't too big a deal, but for larger ones the initialization cost can really get hairy. + // + // We can avoid this by using Data(bytesNoCopy:deallocator:), so that's what we do. In principle we can do slightly better in the case where we have a discontiguous Plaintext + // type, but it's honestly not worth it enough to justify the code complexity. + if authenticatedData.regions.count == 1 { + // We can use a nice fast-path here. + return try self._openContiguous(ciphertext: ciphertext, nonce: nonce, tag: tag, authenticatedData: authenticatedData.regions.first!) + } else { + let contiguousAD = Array(authenticatedData) + return try self._openContiguous(ciphertext: ciphertext, nonce: nonce, tag: tag, authenticatedData: contiguousAD) + } + } + + /// A fast-path for opening contiguous data. Also inlinable to gain specialization information. + @inlinable + func _openContiguous(ciphertext: Data, nonce: Nonce, tag: Data, authenticatedData: AuthenticatedData) throws -> Data { + try ciphertext.withUnsafeBytes { ciphertextPointer in + try nonce.withUnsafeBytes { nonceBytes in + try tag.withUnsafeBytes { tagBytes in + try authenticatedData.withUnsafeBytes { authenticatedDataBytes in + try self._openContiguous(ciphertext: ciphertextPointer, nonceBytes: nonceBytes, tagBytes: tagBytes, authenticatedData: authenticatedDataBytes) + } + } + } + } + } + + /// The unsafe base call: not inlinable so that it can touch private variables. + @usableFromInline + func _openContiguous(ciphertext: UnsafeRawBufferPointer, nonceBytes: UnsafeRawBufferPointer, tagBytes: UnsafeRawBufferPointer, authenticatedData: UnsafeRawBufferPointer) throws -> Data { + // We use malloc here because we are going to call free later. We force unwrap to trigger crashes if the allocation + // fails. + let outputBuffer = UnsafeMutableRawBufferPointer(start: malloc(ciphertext.count)!, count: ciphertext.count) + + let rc = withUnsafePointer(to: &self.context) { contextPointer in + return CCryptoBoringSSLShims_EVP_AEAD_CTX_open_gather(contextPointer, + outputBuffer.baseAddress, + nonceBytes.baseAddress, nonceBytes.count, + ciphertext.baseAddress, ciphertext.count, + tagBytes.baseAddress, tagBytes.count, + authenticatedData.baseAddress, authenticatedData.count) + } + + guard rc == 1 else { + // Ooops, error. Free the memory we allocated before we throw. + free(outputBuffer.baseAddress) + throw CryptoBoringWrapperError.internalBoringSSLError() + } + + let output = Data(bytesNoCopy: outputBuffer.baseAddress!, count: outputBuffer.count, deallocator: .free) + return output + } + + /// An additional entry point for opening data where the ciphertext and the tag can be provided as one combined data . Covers the full gamut of types, including discontiguous data types. This must be inlinable. + @inlinable + public func open(combinedCiphertextAndTag: Data, nonce: Nonce, authenticatedData: AuthenticatedData) throws -> Data { + // Open is a somewhat awkward function. As it returns a Data, we are going to need to initialize a Data large enough to write into. Data does not provide us an + // initializer that gives us access to its uninitialized memory, so the cost of creating this Data is the cost of allocating the data + the cost of initializing + // it. For smaller plaintexts this isn't too big a deal, but for larger ones the initialization cost can really get hairy. + // + // We can avoid this by using Data(bytesNoCopy:deallocator:), so that's what we do. In principle we can do slightly better in the case where we have a discontiguous Plaintext + // type, but it's honestly not worth it enough to justify the code complexity. + if authenticatedData.regions.count == 1 { + // We can use a nice fast-path here. + return try self._openContiguous(combinedCiphertextAndTag: combinedCiphertextAndTag, nonce: nonce, authenticatedData: authenticatedData.regions.first!) + } else { + let contiguousAD = Array(authenticatedData) + return try self._openContiguous(combinedCiphertextAndTag: combinedCiphertextAndTag, nonce: nonce, authenticatedData: contiguousAD) + } + } + + /// A fast-path for opening contiguous data. Also inlinable to gain specialization information. + @inlinable + func _openContiguous(combinedCiphertextAndTag: Data, nonce: Nonce, authenticatedData: AuthenticatedData) throws -> Data { + try combinedCiphertextAndTag.withUnsafeBytes { combinedCiphertextAndTagPointer in + try nonce.withUnsafeBytes { nonceBytes in + try authenticatedData.withUnsafeBytes { authenticatedDataBytes in + try self._openContiguous(combinedCiphertextAndTag: combinedCiphertextAndTagPointer, nonceBytes: nonceBytes, authenticatedData: authenticatedDataBytes) + } + } + } + } + + /// The unsafe base call: not inlinable so that it can touch private variables. + @usableFromInline + func _openContiguous(combinedCiphertextAndTag: UnsafeRawBufferPointer, nonceBytes: UnsafeRawBufferPointer, authenticatedData: UnsafeRawBufferPointer) throws -> Data { + // We use malloc here because we are going to call free later. We force unwrap to trigger crashes if the allocation + // fails. + let outputBuffer = UnsafeMutableRawBufferPointer(start: malloc(combinedCiphertextAndTag.count)!, count: combinedCiphertextAndTag.count) + + var writtenBytes = 0 + let rc = withUnsafePointer(to: &self.context) { contextPointer in + return CCryptoBoringSSLShims_EVP_AEAD_CTX_open(contextPointer, + outputBuffer.baseAddress, &writtenBytes, outputBuffer.count, + nonceBytes.baseAddress, nonceBytes.count, + combinedCiphertextAndTag.baseAddress, combinedCiphertextAndTag.count, + authenticatedData.baseAddress, authenticatedData.count) + } + + guard rc == 1 else { + // Ooops, error. Free the memory we allocated before we throw. + free(outputBuffer.baseAddress) + throw CryptoBoringWrapperError.internalBoringSSLError() + } + + let output = Data(bytesNoCopy: outputBuffer.baseAddress!, count: outputBuffer.count, deallocator: .free).prefix(writtenBytes) + return output + } + +} + +// MARK: - Supported ciphers + +extension BoringSSLAEAD { + var boringSSLCipher: OpaquePointer { + switch self { + case .aes128gcm: + return CCryptoBoringSSL_EVP_aead_aes_128_gcm() + case .aes192gcm: + return CCryptoBoringSSL_EVP_aead_aes_192_gcm() + case .aes256gcm: + return CCryptoBoringSSL_EVP_aead_aes_256_gcm() + case .aes128gcmsiv: + return CCryptoBoringSSL_EVP_aead_aes_128_gcm_siv() + case .aes256gcmsiv: + return CCryptoBoringSSL_EVP_aead_aes_256_gcm_siv() + case .chacha20: + return CCryptoBoringSSL_EVP_aead_chacha20_poly1305() + } + } +} diff --git a/Sources/CryptoBoringWrapper/CMakeLists.txt b/Sources/CryptoBoringWrapper/CMakeLists.txt new file mode 100644 index 0000000..75bcc48 --- /dev/null +++ b/Sources/CryptoBoringWrapper/CMakeLists.txt @@ -0,0 +1,30 @@ +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftCrypto open source project +## +## Copyright (c) 2022 Apple Inc. and the SwiftCrypto project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.md for the list of SwiftCrypto project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +add_library(CryptoBoringWrapper + "AEAD/BoringSSLAEAD.swift" + "CryptoKitErrors_boring.swift") + +target_include_directories(CryptoBoringWrapper PUBLIC + $ + $) + +target_link_libraries(CryptoBoringWrapper PUBLIC + CCryptoBoringSSL + CCryptoBoringSSLShims) + +set_target_properties(CryptoBoringWrapper PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) + +set_property(GLOBAL APPEND PROPERTY SWIFT_CRYPTO_EXPORTS CryptoBoringWrapper) diff --git a/Sources/CryptoBoringWrapper/CryptoKitErrors_boring.swift b/Sources/CryptoBoringWrapper/CryptoKitErrors_boring.swift new file mode 100644 index 0000000..81f127b --- /dev/null +++ b/Sources/CryptoBoringWrapper/CryptoKitErrors_boring.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2021 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@_implementationOnly import CCryptoBoringSSL + +public enum CryptoBoringWrapperError: Error { + case underlyingCoreCryptoError(error: Int32) +} + +extension CryptoBoringWrapperError { + /// A helper function that packs the value of `ERR_get_error` into the internal error field. + @usableFromInline + static func internalBoringSSLError() -> CryptoBoringWrapperError { + return .underlyingCoreCryptoError(error: Int32(bitPattern: CCryptoBoringSSL_ERR_get_error())) + } +} diff --git a/Sources/_CryptoExtras/AES/AES_GCM_SIV.swift b/Sources/_CryptoExtras/AES/AES_GCM_SIV.swift new file mode 100644 index 0000000..71600ad --- /dev/null +++ b/Sources/_CryptoExtras/AES/AES_GCM_SIV.swift @@ -0,0 +1,160 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2022 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Crypto +@_implementationOnly import CCryptoBoringSSL +@_implementationOnly import CCryptoBoringSSLShims +@_implementationOnly import CryptoBoringWrapper +import Foundation + +/// Types associated with the AES GCM SIV algorithm +extension AES.GCM { + /// AES in GCM SIV mode with 128-bit tags. + public enum _SIV { + static let tagByteCount = 16 + static let nonceByteCount = 12 + + /// Encrypts and authenticates data using AES-GCM-SIV. + /// + /// - Parameters: + /// - message: The message to encrypt and authenticate + /// - key: An encryption key of 128 or 256 bits + /// - nonce: An Nonce for AES-GCM-SIV 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 + public static func seal + (_ message: Plaintext, using key: SymmetricKey, nonce: Nonce? = nil, authenticating authenticatedData: AuthenticatedData) throws -> SealedBox { + return try OpenSSLAESGCMSIVImpl.seal(key: key, message: message, nonce: nonce, authenticatedData: authenticatedData) + } + + /// Encrypts and authenticates data using AES-GCM-SIV. + /// + /// - Parameters: + /// - message: The message to encrypt and authenticate + /// - key: An encryption key of 128 or 256 bits + /// - nonce: An Nonce for AES-GCM-SIV 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 + public static func seal + (_ message: Plaintext, using key: SymmetricKey, nonce: Nonce? = nil) throws -> SealedBox { + return try OpenSSLAESGCMSIVImpl.seal(key: key, message: message, nonce: nonce, authenticatedData: Data?.none) + } + + /// Authenticates and decrypts data using AES-GCM-SIV. + /// + /// - Parameters: + /// - sealedBox: The sealed box to authenticate and decrypt + /// - key: An encryption key of 128 or 256 bits + /// - nonce: An Nonce for AES-GCM-SIV 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. + public static func open + (_ sealedBox: SealedBox, using key: SymmetricKey, authenticating authenticatedData: AuthenticatedData) throws -> Data { + return try OpenSSLAESGCMSIVImpl.open(key: key, sealedBox: sealedBox, authenticatedData: authenticatedData) + } + + /// Authenticates and decrypts data using AES-GCM-SIV. + /// + /// - Parameters: + /// - sealedBox: The sealed box to authenticate and decrypt + /// - key: An encryption key of 128 or 256 bits + /// - nonce: An Nonce for AES-GCM-SIV 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. + public static func open(_ sealedBox: SealedBox, using key: SymmetricKey) throws -> Data { + return try OpenSSLAESGCMSIVImpl.open(key: key, sealedBox: sealedBox, authenticatedData: Data?.none) + } + } +} + +extension AES.GCM._SIV { + public struct Nonce: ContiguousBytes, Sequence { + let bytes: Data + + /// Generates a fresh random Nonce. Unless required by a specification to provide a specific Nonce, this is the recommended initializer. + public init() { + var data = Data(repeating: 0, count: AES.GCM._SIV.nonceByteCount) + data.withUnsafeMutableBytes { + assert($0.count == AES.GCM._SIV.nonceByteCount) + $0.initializeWithRandomBytes(count: AES.GCM._SIV.nonceByteCount) + } + self.bytes = data + } + + public init(data: D) throws { + if data.count != AES.GCM._SIV.nonceByteCount { + throw CryptoKitError.incorrectParameterSize + } + + self.bytes = Data(data) + } + + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + return try self.bytes.withUnsafeBytes(body) + } + + public func makeIterator() -> Array.Iterator { + self.withUnsafeBytes({ (buffPtr) in + return Array(buffPtr).makeIterator() + }) + } + } +} + +extension AES.GCM._SIV { + public struct SealedBox { + /// The combined representation ( nonce || ciphertext || tag) + public let combined: Data + /// The authentication tag + public var tag: Data { + return combined.suffix(AES.GCM._SIV.tagByteCount) + } + /// The ciphertext + public var ciphertext: Data { + return combined.dropFirst(AES.GCM._SIV.nonceByteCount).dropLast(AES.GCM._SIV.tagByteCount) + } + /// The Nonce + public var nonce: AES.GCM._SIV.Nonce { + return try! AES.GCM._SIV.Nonce(data: combined.prefix(AES.GCM._SIV.nonceByteCount)) + } + + @inlinable + public init(combined: D) throws { + // AES minimum nonce (12 bytes) + AES tag (16 bytes) + // While we have these values in the internal APIs, we can't use it in inlinable code. + let aesGCMOverhead = 12 + 16 + + if combined.count < aesGCMOverhead { + throw CryptoKitError.incorrectParameterSize + } + + self.combined = Data(combined) + } + + public init(nonce: AES.GCM._SIV.Nonce, ciphertext: C, tag: T) throws { + guard tag.count == AES.GCM._SIV.tagByteCount else { + throw CryptoKitError.incorrectParameterSize + } + + self.combined = Data(nonce) + ciphertext + tag + } + + internal init(combined: Data, nonceByteCount: Int) { + assert(nonceByteCount == AES.GCM._SIV.nonceByteCount) + self.combined = combined + } + } +} diff --git a/Sources/_CryptoExtras/AES/BoringSSL/AES_GCM_SIV_boring.swift b/Sources/_CryptoExtras/AES/BoringSSL/AES_GCM_SIV_boring.swift new file mode 100644 index 0000000..c291ddd --- /dev/null +++ b/Sources/_CryptoExtras/AES/BoringSSL/AES_GCM_SIV_boring.swift @@ -0,0 +1,87 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2022 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// This is a copy ChaChaPoly_boring just with a different set aes algos + +@_implementationOnly import CCryptoBoringSSL +@_implementationOnly import CCryptoBoringSSLShims +import Crypto +@_implementationOnly import CryptoBoringWrapper +import Foundation + +extension BoringSSLAEAD { + /// Seal a given message. + func seal(message: Plaintext, key: SymmetricKey, nonce: Nonce, authenticatedData: AuthenticatedData) throws -> (ciphertext: Data, tag: Data) { + do { + let context = try AEADContext(cipher: self, key: key) + return try context.seal(message: message, nonce: nonce, authenticatedData: authenticatedData) + } catch CryptoBoringWrapperError.underlyingCoreCryptoError(let errorCode) { + throw CryptoKitError.underlyingCoreCryptoError(error: errorCode) + } + } + + /// Open a given message. + func open(combinedCiphertextAndTag: Data, key: SymmetricKey, nonce: Nonce, authenticatedData: AuthenticatedData) throws -> Data { + do { + let context = try AEADContext(cipher: self, key: key) + return try context.open(combinedCiphertextAndTag: combinedCiphertextAndTag, nonce: nonce, authenticatedData: authenticatedData) + } catch CryptoBoringWrapperError.underlyingCoreCryptoError(let errorCode) { + throw CryptoKitError.underlyingCoreCryptoError(error: errorCode) + } + } +} + +enum OpenSSLAESGCMSIVImpl { + @inlinable + static func seal + (key: SymmetricKey, message: Plaintext, nonce: AES.GCM._SIV.Nonce?, authenticatedData: AuthenticatedData? = nil) throws -> AES.GCM._SIV.SealedBox { + let nonce = nonce ?? AES.GCM._SIV.Nonce() + + let aead = try Self._backingAEAD(key: key) + + let ciphertext: Data + let tag: Data + if let ad = authenticatedData { + (ciphertext, tag) = try aead.seal(message: message, key: key, nonce: nonce, authenticatedData: ad) + } else { + (ciphertext, tag) = try aead.seal(message: message, key: key, nonce: nonce, authenticatedData: []) + } + + return try AES.GCM._SIV.SealedBox(nonce: nonce, ciphertext: ciphertext, tag: tag) + } + + @inlinable + static func open + (key: SymmetricKey, sealedBox: AES.GCM._SIV.SealedBox, authenticatedData: AuthenticatedData? = nil) throws -> Data { + let aead = try Self._backingAEAD(key: key) + + if let ad = authenticatedData { + return try aead.open(combinedCiphertextAndTag: sealedBox.combined.dropFirst(AES.GCM._SIV.nonceByteCount), key: key, nonce: sealedBox.nonce, authenticatedData: ad) + } else { + return try aead.open(combinedCiphertextAndTag: sealedBox.combined.dropFirst(AES.GCM._SIV.nonceByteCount), key: key, nonce: sealedBox.nonce, authenticatedData: []) + } + } + + @usableFromInline + static func _backingAEAD(key: SymmetricKey) throws -> BoringSSLAEAD { + switch key.bitCount { + case 128: + return .aes128gcmsiv + case 256: + return .aes256gcmsiv + default: + throw CryptoKitError.incorrectKeySize + } + } +} diff --git a/Sources/_CryptoExtras/Util/CryptoKitErrors_boring.swift b/Sources/_CryptoExtras/Util/CryptoKitErrors_boring.swift index f180885..55b3cb3 100644 --- a/Sources/_CryptoExtras/Util/CryptoKitErrors_boring.swift +++ b/Sources/_CryptoExtras/Util/CryptoKitErrors_boring.swift @@ -12,7 +12,6 @@ // //===----------------------------------------------------------------------===// -#if !canImport(Security) @_implementationOnly import CCryptoBoringSSL import Crypto @@ -23,5 +22,3 @@ extension CryptoKitError { return .underlyingCoreCryptoError(error: Int32(bitPattern: CCryptoBoringSSL_ERR_get_error())) } } - -#endif diff --git a/Sources/_CryptoExtras/Util/RandomBytes.swift b/Sources/_CryptoExtras/Util/RandomBytes.swift new file mode 100644 index 0000000..1c68465 --- /dev/null +++ b/Sources/_CryptoExtras/Util/RandomBytes.swift @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +extension UnsafeMutableRawBufferPointer { + @inlinable + func initializeWithRandomBytes(count: Int) { + guard count > 0 else { + return + } + + precondition(count <= self.count) + var rng = SystemRandomNumberGenerator() + + // We store bytes 64-bits at a time until we can't anymore. + var targetPtr = self + while targetPtr.count > 8 { + targetPtr.storeBytes(of: rng.next(), as: UInt64.self) + targetPtr = UnsafeMutableRawBufferPointer(rebasing: targetPtr[8...]) + } + + // Now we're down to having to store things an integer at a time. We do this by shifting and + // masking. + var remainingWord: UInt64 = rng.next() + while targetPtr.count > 0 { + targetPtr.storeBytes(of: UInt8(remainingWord & 0xFF), as: UInt8.self) + remainingWord >>= 8 + targetPtr = UnsafeMutableRawBufferPointer(rebasing: targetPtr[1...]) + } + } +} diff --git a/Tests/_CryptoExtrasTests/AES-GCM-SIV-Runner.swift b/Tests/_CryptoExtrasTests/AES-GCM-SIV-Runner.swift new file mode 100644 index 0000000..b133789 --- /dev/null +++ b/Tests/_CryptoExtrasTests/AES-GCM-SIV-Runner.swift @@ -0,0 +1,232 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2022 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import Foundation +import XCTest +import Crypto +import _CryptoExtras + +struct AEADTestGroup: Codable { + let ivSize: Int + let keySize: UInt16 + let tagSize: UInt16 + let type: String + let tests: [AESGCMTestVector] +} + +struct AESGCMTestVector: Codable { + let key: String + let iv: String + let aad: String + let msg: String + let ct: String + let tag: String + let result: String +} + +class AESGCMSIVTests: XCTestCase { + func testPropertiesStayTheSameAfterFailedOpening() throws { + let message = Data("this is a message".utf8) + let sealed = try AES.GCM._SIV.seal(message, using: SymmetricKey(size: .bits128)) + + // We copy the bytes of these fields out here to ensure they're saved. + let originalCiphertext = Array(sealed.ciphertext) + let originalNonce = Array(sealed.nonce) + let originalTag = Array(sealed.tag) + + XCTAssertThrowsError(try AES.GCM._SIV.open(sealed, using: SymmetricKey(size: .bits128))) + + // The fields must all be unchanged. + XCTAssertEqual(originalCiphertext, Array(sealed.ciphertext)) + XCTAssertEqual(originalNonce, Array(sealed.nonce)) + XCTAssertEqual(originalTag, Array(sealed.tag)) + } + + func testBadKeySize() { + let plaintext: Data = "Some Super Secret Message".data(using: String.Encoding.utf8)! + let key = SymmetricKey(size: .init(bitCount: 304)) + let nonce = AES.GCM._SIV.Nonce() + + XCTAssertThrowsError(try AES.GCM._SIV.seal(plaintext, using: key, nonce: nonce)) + } + + func testEncryptDecrypt() throws { + let plaintext: Data = "Some Super Secret Message".data(using: String.Encoding.utf8)! + + let key = SymmetricKey(size: .bits256) + let nonce = AES.GCM._SIV.Nonce() + + let ciphertext = try AES.GCM._SIV.seal(plaintext, using: key, nonce: nonce) + let recoveredPlaintext = try AES.GCM._SIV.open(ciphertext, using: key, authenticating: Data()) + let recoveredPlaintextWithoutAAD = try AES.GCM._SIV.open(ciphertext, using: key) + + XCTAssertEqual(recoveredPlaintext, plaintext) + XCTAssertEqual(recoveredPlaintextWithoutAAD, plaintext) + } + + func testExtractingBytesFromNonce() throws { + let nonce = AES.GCM._SIV.Nonce() + XCTAssertEqual(Array(nonce), nonce.withUnsafeBytes { Array($0) }) + + let testNonceBytes = Array(UInt8(0)..(message: Message, aad: AAD, file: StaticString = (#file), line: UInt = #line) throws { + let key = SymmetricKey(size: .bits256) + let nonce = AES.GCM._SIV.Nonce() + let ciphertext = try AES.GCM._SIV.seal(message, using: key, nonce: nonce, authenticating: aad) + let recoveredPlaintext = try AES.GCM._SIV.open(ciphertext, using: key, authenticating: aad) + + XCTAssertEqual(Array(recoveredPlaintext), Array(message), file: file, line: line) + } + + let message = Array("Hello, world, it's AES-GCM!".utf8) + let aad = Array("I heard you like Counter Mode, so I put a Galois on it".utf8) + let (contiguousMessage, discontiguousMessage) = message.asDataProtocols() + let (contiguousAad, discontiguousAad) = aad.asDataProtocols() + + _ = try roundTrip(message: contiguousMessage, aad: contiguousAad) + _ = try roundTrip(message: discontiguousMessage, aad: contiguousAad) + _ = try roundTrip(message: contiguousMessage, aad: discontiguousAad) + _ = try roundTrip(message: discontiguousMessage, aad: discontiguousAad) + } + + func testWycheproof() throws { + try wycheproofTest( + jsonName: "aes_gcm_siv_test", + testFunction: { (group: AEADTestGroup) in + _ = try testGroup(group: group) + }) + } + + func testGroup(group: AEADTestGroup) throws { + for testVector in group.tests { + var msg: Data = Data() + var aad: Data = Data() + var ct: [UInt8] = [] + var tag: [UInt8] = [] + + var nonce: AES.GCM._SIV.Nonce + + do { + nonce = try AES.GCM._SIV.Nonce(data: Array(hexString: testVector.iv)) + } catch { + XCTAssertEqual(testVector.result, "invalid") + return + } + + if testVector.ct.count > 0 { + ct = try Array(hexString: testVector.ct) + } + + if testVector.msg.count > 0 { + msg = try Data(hexString: testVector.msg) + } + + if testVector.aad.count > 0 { + aad = try Data(hexString: testVector.aad) + } + + if testVector.tag.count > 0 { + tag = try Array(hexString: testVector.tag) + } + + let key = try SymmetricKey(data: Array(hexString: testVector.key)) + XCTAssertNotNil(key) + + let sb = try AES.GCM._SIV.seal(msg, using: key, nonce: nonce, authenticating: aad) + + if testVector.result == "valid" { + XCTAssertEqual(Data(ct), sb.ciphertext) + XCTAssertEqual(Data(tag), sb.tag) + } + + do { + let recovered_pt = try AES.GCM._SIV.open(AES.GCM._SIV.SealedBox(nonce: nonce, ciphertext: ct, tag: tag), using: key, authenticating: aad) + + if testVector.result == "valid" || testVector.result == "acceptable" { + XCTAssertEqual(recovered_pt, msg) + } + } catch { + XCTAssertEqual(testVector.result, "invalid") + } + } + } +} diff --git a/Tests/_CryptoExtrasTests/Utils/SplitData.swift b/Tests/_CryptoExtrasTests/Utils/SplitData.swift new file mode 100644 index 0000000..b7aacf2 --- /dev/null +++ b/Tests/_CryptoExtrasTests/Utils/SplitData.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2022 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import Foundation +import Dispatch + +// A testing utility that creates one contiguous and one discontiguous representation of the given Data. +extension Array where Element == UInt8 { + func asDataProtocols() -> (contiguous: Data, discontiguous: DispatchData) { + guard self.count > 0 else { + // We can't really have discontiguous options here, so we just return empty versions + // of both. + return (Data(), DispatchData.empty) + } + + let contiguous = Data(self) + let discontiguous: DispatchData = self.withUnsafeBytes { bytesPointer in + let pivot = bytesPointer.count / 2 + var data = DispatchData.empty + data.append(DispatchData(bytes: UnsafeRawBufferPointer(rebasing: bytesPointer[.. Checking for unacceptable language... "