Skip to content

Commit 65f8c60

Browse files
authored
ChaCha20 CTR Encryption (#169)
* OpenSSLChaCha20CTR Implementation. Wraps CCryptoBoringSSL_CRYPTO_chacha_20. * Insecure extension implementing the ChaCha20 CTR encrypt method. * Added ChaCha20CTR Tests based on vectors provided in RFC9001 Appendix A.5 * Corrected year in header * Changed return type to Data. Removed redundant pointer castings. Removed unnecessary array allocations in favor of withUnsafeBytes. * Introduced a typed ChaCha20CTR Nonce and Counter struct in order to help enforce parameter constraints and type safety. * Formatting * Updated tests to use new Nonce and Counter structs. Added additional test checking for invalid parameters. * Switch to HexStrings for better readability. * Removed empty line at top of file * Fixed UInt32.max counter assertion. * Moved the bindMemory calls out of the function and copied a note from a similar situation elsewhere in the codebase. * Formatting * Implemented an _encryptContiguous function that prevents having to use the withContiguousStorageIfAvailable method. If our DataProtocol is contiguous we encrypt directly, otherwise we consolidate before encrypting. Removed inLen param from chacha20CTR call. * Replaced the chacha20CTR function with a direct call to CCryptoBoringSSL_CRYPTO_chacha_20. * Counter is now backed by a UInt32 instead of Data. Removed Sequence conformance. * Formatting * Formatting * Replaced unsafe code (unsafeBytes and load) with a more generic and safer UInt32 construction. * Replaced counterAsUInt32 definitions with integer literals to avoid symmetric bugs in the load. * Updated _CryptoExtras/CMakeList.txt
1 parent 1863cc9 commit 65f8c60

File tree

4 files changed

+287
-0
lines changed

4 files changed

+287
-0
lines changed

Sources/_CryptoExtras/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
##===----------------------------------------------------------------------===##
1414

1515
add_library(_CryptoExtras
16+
"ChaCha20CTR/BoringSSL/ChaCha20CTR_boring.swift"
17+
"ChaCha20CTR/ChaCha20CTR.swift"
1618
"RSA/RSA.swift"
1719
"RSA/RSA_boring.swift"
1820
"RSA/RSA_security.swift"
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftCrypto open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftCrypto project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.md for the list of SwiftCrypto project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
@_implementationOnly import CCryptoBoringSSL
16+
@_implementationOnly import CCryptoBoringSSLShims
17+
import Crypto
18+
@_implementationOnly import CryptoBoringWrapper
19+
import Foundation
20+
21+
enum OpenSSLChaCha20CTRImpl {
22+
static func encrypt<M: DataProtocol, N: ContiguousBytes>(key: SymmetricKey, message: M, counter: UInt32, nonce: N) throws -> Data {
23+
guard key.bitCount == Insecure.ChaCha20CTR.keyBitsCount else {
24+
throw CryptoKitError.incorrectKeySize
25+
}
26+
27+
// If our message, conforming to DataProtocol, happens to be allocated contiguously in memory, then we can grab the first, and only, contiguous region and operate on it
28+
if message.regions.count == 1 {
29+
return self._encryptContiguous(key: key, message: message.regions.first!, counter: counter, nonce: nonce)
30+
} else {
31+
// Otherwise we need to consolidate the noncontiguous bytes by instantiating an Array<UInt8>
32+
let contiguousMessage = Array(message)
33+
return self._encryptContiguous(key: key, message: contiguousMessage, counter: counter, nonce: nonce)
34+
}
35+
}
36+
37+
/// A fast-path for encrypting contiguous data. Also inlinable to gain specialization information.
38+
@inlinable
39+
static func _encryptContiguous<Plaintext: ContiguousBytes, Nonce: ContiguousBytes>(key: SymmetricKey, message: Plaintext, counter: UInt32, nonce: Nonce) -> Data {
40+
key.withUnsafeBytes { keyPtr in
41+
nonce.withUnsafeBytes { noncePtr in
42+
message.withUnsafeBytes { plaintextPtr in
43+
// We bind all three pointers here. These binds are not technically safe, but because we
44+
// know the pointers don't persist they can't violate the aliasing rules. We really
45+
// want a "with memory rebound" function but we don't have it yet.
46+
let keyBytes = keyPtr.bindMemory(to: UInt8.self)
47+
let nonceBytes = noncePtr.bindMemory(to: UInt8.self)
48+
let plaintext = plaintextPtr.bindMemory(to: UInt8.self)
49+
50+
var ciphertext = Data(repeating: 0, count: plaintext.count)
51+
52+
ciphertext.withUnsafeMutableBytes { ciphertext in
53+
CCryptoBoringSSL_CRYPTO_chacha_20(
54+
ciphertext,
55+
plaintext.baseAddress,
56+
plaintext.count,
57+
keyBytes.baseAddress,
58+
nonceBytes.baseAddress,
59+
counter
60+
)
61+
}
62+
63+
return ciphertext
64+
}
65+
}
66+
}
67+
}
68+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftCrypto open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftCrypto project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.md for the list of SwiftCrypto project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
@_implementationOnly import CCryptoBoringSSL
16+
@_implementationOnly import CCryptoBoringSSLShims
17+
import Crypto
18+
@_implementationOnly import CryptoBoringWrapper
19+
import Foundation
20+
21+
typealias ChaCha20CTRImpl = OpenSSLChaCha20CTRImpl
22+
23+
extension Insecure {
24+
/// ChaCha20-CTR with 96-bit nonces and a 32 bit counter.
25+
public enum ChaCha20CTR {
26+
static let keyBitsCount = 256
27+
static let nonceByteCount = 12
28+
static let counterByteCount = 4
29+
30+
/// Encrypts data using ChaCha20CTR
31+
///
32+
/// - Parameters:
33+
/// - message: The message to encrypt
34+
/// - key: A 256-bit encryption key
35+
/// - counter: A 4 byte counter (UInt32), defaults to 0
36+
/// - nonce: A 12 byte nonce for ChaCha20 encryption. The nonce must be unique for every use of the key to seal data.
37+
/// - Returns: The encrypted ciphertext
38+
/// - Throws: CipherError errors
39+
/// - Warning: You most likely want to use the ChaChaPoly implemention with AuthenticatedData available at `Crypto.ChaChaPoly`
40+
public static func encrypt<
41+
Plaintext: DataProtocol
42+
>(
43+
_ message: Plaintext,
44+
using key: SymmetricKey,
45+
counter: Insecure.ChaCha20CTR.Counter = Counter(),
46+
nonce: Insecure.ChaCha20CTR.Nonce
47+
) throws -> Data {
48+
return try ChaCha20CTRImpl.encrypt(key: key, message: message, counter: counter.counter, nonce: nonce.bytes)
49+
}
50+
}
51+
}
52+
53+
extension Insecure.ChaCha20CTR {
54+
public struct Nonce: ContiguousBytes, Sequence {
55+
let bytes: Data
56+
57+
/// Generates a fresh random Nonce. Unless required by a specification to provide a specific Nonce, this is the recommended initializer.
58+
public init() {
59+
var data = Data(repeating: 0, count: Insecure.ChaCha20CTR.nonceByteCount)
60+
data.withUnsafeMutableBytes {
61+
assert($0.count == Insecure.ChaCha20CTR.nonceByteCount)
62+
$0.initializeWithRandomBytes(count: Insecure.ChaCha20CTR.nonceByteCount)
63+
}
64+
self.bytes = data
65+
}
66+
67+
public init<D: DataProtocol>(data: D) throws {
68+
if data.count != Insecure.ChaCha20CTR.nonceByteCount {
69+
throw CryptoKitError.incorrectParameterSize
70+
}
71+
72+
self.bytes = Data(data)
73+
}
74+
75+
public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R {
76+
return try self.bytes.withUnsafeBytes(body)
77+
}
78+
79+
public func makeIterator() -> Array<UInt8>.Iterator {
80+
self.withUnsafeBytes({ buffPtr in
81+
Array(buffPtr).makeIterator()
82+
})
83+
}
84+
}
85+
86+
public struct Counter: ContiguousBytes {
87+
let counter: UInt32
88+
89+
/// Generates a fresh Counter set to 0. Unless required by a specification to provide a specific Counter, this is the recommended initializer.
90+
public init() {
91+
self.counter = 0
92+
}
93+
94+
/// Explicitly set the Counter's offset using a byte sequence
95+
public init<D: DataProtocol>(data: D) throws {
96+
if data.count != Insecure.ChaCha20CTR.counterByteCount {
97+
throw CryptoKitError.incorrectParameterSize
98+
}
99+
100+
let startIndex = data.startIndex
101+
self.counter = (
102+
(UInt32(data[data.index(startIndex, offsetBy: 0)]) << 0) |
103+
(UInt32(data[data.index(startIndex, offsetBy: 1)]) << 8) |
104+
(UInt32(data[data.index(startIndex, offsetBy: 2)]) << 16) |
105+
(UInt32(data[data.index(startIndex, offsetBy: 3)]) << 24)
106+
)
107+
}
108+
109+
/// Explicitly set the Counter's offset using a UInt32
110+
public init(offset: UInt32) throws {
111+
self.counter = offset
112+
}
113+
114+
public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R {
115+
return try Swift.withUnsafeBytes(of: self.counter, body)
116+
}
117+
}
118+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftCrypto open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftCrypto project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.md for the list of SwiftCrypto project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
import Foundation
15+
import XCTest
16+
import Crypto
17+
import _CryptoExtras
18+
19+
class ChaCha20CTRTests: XCTestCase {
20+
21+
/// Test Vector - https://datatracker.ietf.org/doc/html/rfc9001#name-chacha20-poly1305-short-hea
22+
func testChaCha20CTR_v1() throws {
23+
let hpKey = try Array(hexString: "25a282b9e82f06f21f488917a4fc8f1b73573685608597d0efcb076b0ab7a7a4")
24+
/// Sample = 0x5e5cd55c41f69080575d7999c25a5bfb
25+
let counterAsData = try Array(hexString: "5e5cd55c")
26+
let counterAsUInt32 = UInt32(bigEndian: 0x5e5cd55c)
27+
let iv = try Array(hexString: "41f69080575d7999c25a5bfb")
28+
29+
let mask: Data = try Insecure.ChaCha20CTR.encrypt(Array<UInt8>(repeating: 0, count: 5), using: SymmetricKey(data: hpKey), counter: Insecure.ChaCha20CTR.Counter(data: counterAsData), nonce: Insecure.ChaCha20CTR.Nonce(data: iv))
30+
let mask2: Data = try Insecure.ChaCha20CTR.encrypt(Array<UInt8>(repeating: 0, count: 5), using: SymmetricKey(data: hpKey), counter: Insecure.ChaCha20CTR.Counter(offset: counterAsUInt32), nonce: Insecure.ChaCha20CTR.Nonce(data: iv))
31+
32+
XCTAssertEqual(mask, try Data(hexString: "aefefe7d03"))
33+
XCTAssertEqual(mask, mask2)
34+
}
35+
36+
/// Test Vector - https://www.ietf.org/archive/id/draft-ietf-quic-v2-10.html#name-chacha20-poly1305-short-head
37+
func testChaCha20CTR_v2() throws {
38+
let hpKey = try Array(hexString: "d659760d2ba434a226fd37b35c69e2da8211d10c4f12538787d65645d5d1b8e2")
39+
/// Sample = 0xe7b6b932bc27d786f4bc2bb20f2162ba
40+
let counterAsData = try Array(hexString: "e7b6b932")
41+
let counterAsUInt32 = UInt32(bigEndian: 0xe7b6b932)
42+
let iv = try Array(hexString: "bc27d786f4bc2bb20f2162ba")
43+
44+
let mask: Data = try Insecure.ChaCha20CTR.encrypt(Array<UInt8>(repeating: 0, count: 5), using: SymmetricKey(data: hpKey), counter: Insecure.ChaCha20CTR.Counter(data: counterAsData), nonce: Insecure.ChaCha20CTR.Nonce(data: iv))
45+
let mask2: Data = try Insecure.ChaCha20CTR.encrypt(Array<UInt8>(repeating: 0, count: 5), using: SymmetricKey(data: hpKey), counter: Insecure.ChaCha20CTR.Counter(offset: counterAsUInt32), nonce: Insecure.ChaCha20CTR.Nonce(data: iv))
46+
47+
XCTAssertEqual(mask, try Data(hexString: "97580e32bf"))
48+
XCTAssertEqual(mask, mask2)
49+
}
50+
51+
func testChaCha20CTR_InvalidParameters() throws {
52+
let keyTooLong: SymmetricKey = SymmetricKey(data: [214, 89, 118, 13, 43, 164, 52, 162, 38, 253, 55, 179, 92, 105, 226, 218, 130, 17, 209, 12, 79, 18, 83, 135, 135, 214, 86, 69, 213, 209, 184, 226, 22])
53+
XCTAssertThrowsError(try Insecure.ChaCha20CTR.encrypt(Array<UInt8>(repeating: 0, count: 5), using: keyTooLong, nonce: Insecure.ChaCha20CTR.Nonce())) { error in
54+
guard case CryptoKitError.incorrectKeySize = error else { return XCTFail("Error thrown was of unexpected type: \(error)") }
55+
}
56+
57+
let keyTooShort: SymmetricKey = SymmetricKey(data: [214, 89, 118, 13, 43, 164, 52, 162, 38, 253, 55, 179, 92, 105, 226, 218, 130, 17, 209, 12, 79, 18, 83, 135, 135, 214, 86, 69, 213, 209, 184])
58+
XCTAssertThrowsError(try Insecure.ChaCha20CTR.encrypt(Array<UInt8>(repeating: 0, count: 5), using: keyTooShort, nonce: Insecure.ChaCha20CTR.Nonce())) { error in
59+
guard case CryptoKitError.incorrectKeySize = error else { return XCTFail("Error thrown was of unexpected type: \(error)") }
60+
}
61+
62+
let nonceTooLong: [UInt8] = [188, 39, 215, 134, 244, 188, 43, 178, 15, 33, 98, 186, 14]
63+
XCTAssertThrowsError(try Insecure.ChaCha20CTR.Nonce(data: nonceTooLong)) { error in
64+
guard case CryptoKitError.incorrectParameterSize = error else { return XCTFail("Error thrown was of unexpected type: \(error)") }
65+
}
66+
67+
let nonceTooShort: [UInt8] = [188, 39, 215, 134, 244, 188, 43, 178, 15, 33, 98]
68+
XCTAssertThrowsError(try Insecure.ChaCha20CTR.Nonce(data: nonceTooShort)) { error in
69+
guard case CryptoKitError.incorrectParameterSize = error else { return XCTFail("Error thrown was of unexpected type: \(error)") }
70+
}
71+
72+
let counterTooLong: [UInt8] = [231, 182, 185, 50, 82]
73+
XCTAssertThrowsError(try Insecure.ChaCha20CTR.Counter(data: counterTooLong)) { error in
74+
guard case CryptoKitError.incorrectParameterSize = error else { return XCTFail("Error thrown was of unexpected type: \(error)") }
75+
}
76+
77+
let counterTooShort: [UInt8] = [231, 182, 185]
78+
XCTAssertThrowsError(try Insecure.ChaCha20CTR.Counter(data: counterTooShort)) { error in
79+
guard case CryptoKitError.incorrectParameterSize = error else { return XCTFail("Error thrown was of unexpected type: \(error)") }
80+
}
81+
82+
let key: SymmetricKey = SymmetricKey(data: [214, 89, 118, 13, 43, 164, 52, 162, 38, 253, 55, 179, 92, 105, 226, 218, 130, 17, 209, 12, 79, 18, 83, 135, 135, 214, 86, 69, 213, 209, 184, 226])
83+
84+
// Ensure UInt32.max Counter Supported
85+
XCTAssertNoThrow(try Insecure.ChaCha20CTR.encrypt(Array<UInt8>(repeating: 0, count: 5), using: key, counter: Insecure.ChaCha20CTR.Counter(offset: UInt32.max), nonce: Insecure.ChaCha20CTR.Nonce()))
86+
87+
// Assert that two calls with the same Counter + Nonce params results in the same output
88+
let nonce = Insecure.ChaCha20CTR.Nonce()
89+
let counter = Insecure.ChaCha20CTR.Counter()
90+
let ciphertext1 = try Insecure.ChaCha20CTR.encrypt(Array<UInt8>(repeating: 0, count: 5), using: key, counter: counter, nonce: nonce)
91+
let ciphertext2 = try Insecure.ChaCha20CTR.encrypt(Array<UInt8>(repeating: 0, count: 5), using: key, counter: counter, nonce: nonce)
92+
XCTAssertEqual(ciphertext1, ciphertext2)
93+
94+
// Assert that two calls with different Nonce params results in different output
95+
let ciphertext3 = try Insecure.ChaCha20CTR.encrypt(Array<UInt8>(repeating: 0, count: 5), using: key, counter: counter, nonce: Insecure.ChaCha20CTR.Nonce())
96+
let ciphertext4 = try Insecure.ChaCha20CTR.encrypt(Array<UInt8>(repeating: 0, count: 5), using: key, counter: counter, nonce: Insecure.ChaCha20CTR.Nonce())
97+
XCTAssertNotEqual(ciphertext3, ciphertext4)
98+
}
99+
}

0 commit comments

Comments
 (0)