Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ChaCha20 CTR Encryption #169

Merged
merged 22 commits into from
Apr 18, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d3f7a0a
OpenSSLChaCha20CTR Implementation. Wraps CCryptoBoringSSL_CRYPTO_chac…
btoms20 Apr 11, 2023
2d98e4b
Insecure extension implementing the ChaCha20 CTR encrypt method.
btoms20 Apr 11, 2023
70092b9
Added ChaCha20CTR Tests based on vectors provided in RFC9001 Appendix…
btoms20 Apr 11, 2023
bcb5992
Corrected year in header
btoms20 Apr 12, 2023
1c7dcfc
Changed return type to Data. Removed redundant pointer castings. Remo…
btoms20 Apr 12, 2023
56aff30
Introduced a typed ChaCha20CTR Nonce and Counter struct in order to h…
btoms20 Apr 12, 2023
ab456f5
Formatting
btoms20 Apr 12, 2023
9c39e5f
Updated tests to use new Nonce and Counter structs. Added additional …
btoms20 Apr 12, 2023
fa934db
Switch to HexStrings for better readability.
btoms20 Apr 12, 2023
4444431
Removed empty line at top of file
btoms20 Apr 12, 2023
5e4fc16
Fixed UInt32.max counter assertion.
btoms20 Apr 12, 2023
820660e
Moved the bindMemory calls out of the function and copied a note from…
btoms20 Apr 12, 2023
9926d93
Formatting
btoms20 Apr 12, 2023
17d166b
Implemented an _encryptContiguous function that prevents having to us…
btoms20 Apr 13, 2023
75681f7
Replaced the chacha20CTR function with a direct call to CCryptoBoring…
btoms20 Apr 13, 2023
2565363
Counter is now backed by a UInt32 instead of Data. Removed Sequence c…
btoms20 Apr 13, 2023
82c9ee6
Formatting
btoms20 Apr 13, 2023
a494479
Formatting
btoms20 Apr 14, 2023
67dd990
Replaced unsafe code (unsafeBytes and load) with a more generic and s…
btoms20 Apr 14, 2023
7fda9ff
Replaced counterAsUInt32 definitions with integer literals to avoid s…
btoms20 Apr 14, 2023
8378522
Merge branch 'apple:main' into feature/ChaCha20+Counter
btoms20 Apr 14, 2023
23e9c60
Updated _CryptoExtras/CMakeList.txt
btoms20 Apr 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftCrypto open source project
//
// Copyright (c) 2023 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 Crypto
@_implementationOnly import CryptoBoringWrapper
import Foundation

enum OpenSSLChaCha20CTRImpl {
static func encrypt<M: DataProtocol, N: ContiguousBytes>(key: SymmetricKey, message: M, counter: UInt32, nonce: N) throws -> Data {
guard key.bitCount == Insecure.ChaCha20CTR.keyBitsCount else {
throw CryptoKitError.incorrectKeySize
}

var ciphertext = Array<UInt8>(repeating: 0, count: message.count)
btoms20 marked this conversation as resolved.
Show resolved Hide resolved

key.withUnsafeBytes { keyPtr in
nonce.withUnsafeBytes { noncePtr in
message.withContiguousStorageIfAvailable { plaintext in
btoms20 marked this conversation as resolved.
Show resolved Hide resolved
// We bind both pointers here. These binds are not technically safe, but because we
// know the pointers don't persist they can't violate the aliasing rules. We really
// want a "with memory rebound" function but we don't have it yet.
let keyBytes = keyPtr.bindMemory(to: UInt8.self)
let nonceBytes = noncePtr.bindMemory(to: UInt8.self)

self.chacha20CTR(out: &ciphertext, plaintext: plaintext, inLen: plaintext.count, key: keyBytes, nonce: nonceBytes, counter: counter)
}
}
}

return Data(ciphertext)
}

static func chacha20CTR(out: UnsafeMutablePointer<UInt8>, plaintext: UnsafeBufferPointer<UInt8>, inLen: Int, key: UnsafeBufferPointer<UInt8>, nonce: UnsafeBufferPointer<UInt8>, counter: UInt32) {
CCryptoBoringSSL_CRYPTO_chacha_20(
out,
plaintext.baseAddress,
inLen,
btoms20 marked this conversation as resolved.
Show resolved Hide resolved
key.baseAddress,
nonce.baseAddress,
counter
)
}
}
117 changes: 117 additions & 0 deletions Sources/_CryptoExtras/ChaCha20CTR/ChaCha20CTR.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftCrypto open source project
//
// Copyright (c) 2023 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 Crypto
@_implementationOnly import CryptoBoringWrapper
import Foundation

typealias ChaCha20CTRImpl = OpenSSLChaCha20CTRImpl

extension Insecure {
/// ChaCha20-CTR with 96-bit nonces and a 32 bit counter.
public enum ChaCha20CTR {
static let keyBitsCount = 256
static let nonceByteCount = 12
static let counterByteCount = 4

/// Encrypts data using ChaCha20CTR
///
/// - Parameters:
/// - message: The message to encrypt
/// - key: A 256-bit encryption key
/// - counter: A 4 byte counter (UInt32), defaults to 0
/// - nonce: A 12 byte nonce for ChaCha20 encryption. The nonce must be unique for every use of the key to seal data.
/// - Returns: The encrypted ciphertext
/// - Throws: CipherError errors
/// - Warning: You most likely want to use the ChaChaPoly implemention with AuthenticatedData available at `Crypto.ChaChaPoly`
public static func encrypt<Plaintext: DataProtocol>
(_ message: Plaintext, using key: SymmetricKey, counter: Insecure.ChaCha20CTR.Counter = Counter(), nonce: Insecure.ChaCha20CTR.Nonce) throws -> Data {
btoms20 marked this conversation as resolved.
Show resolved Hide resolved
return try ChaCha20CTRImpl.encrypt(key: key, message: message, counter: counter.asUInt32(), nonce: nonce.bytes)
}
}
}

extension Insecure.ChaCha20CTR {
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: Insecure.ChaCha20CTR.nonceByteCount)
data.withUnsafeMutableBytes {
assert($0.count == Insecure.ChaCha20CTR.nonceByteCount)
$0.initializeWithRandomBytes(count: Insecure.ChaCha20CTR.nonceByteCount)
}
self.bytes = data
}

public init<D: DataProtocol>(data: D) throws {
if data.count != Insecure.ChaCha20CTR.nonceByteCount {
throw CryptoKitError.incorrectParameterSize
}

self.bytes = Data(data)
}

public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R {
return try self.bytes.withUnsafeBytes(body)
}

public func makeIterator() -> Array<UInt8>.Iterator {
self.withUnsafeBytes({ buffPtr in
Array(buffPtr).makeIterator()
})
}
}

public struct Counter: ContiguousBytes, Sequence {
btoms20 marked this conversation as resolved.
Show resolved Hide resolved
let bytes: Data
btoms20 marked this conversation as resolved.
Show resolved Hide resolved

/// Generates a fresh Counter set to 0. Unless required by a specification to provide a specific Counter, this is the recommended initializer.
public init() {
self.bytes = Data(repeating: 0, count: Insecure.ChaCha20CTR.counterByteCount)
}

/// Explicitly set the Counter's offset using a little endian byte sequence
public init<D: DataProtocol>(data: D) throws {
if data.count != Insecure.ChaCha20CTR.counterByteCount {
throw CryptoKitError.incorrectParameterSize
}

self.bytes = Data(data)
}

/// Explicitly set the Counter's offset using a UInt32
public init(offset: UInt32) throws {
var offset = offset
self.bytes = Data(bytes: &offset, count: MemoryLayout<UInt32>.size)
}

public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R {
return try self.bytes.withUnsafeBytes(body)
}

public func asUInt32() -> UInt32 {
return self.withUnsafeBytes { $0.load(as: UInt32.self) }
}

public func makeIterator() -> Array<UInt8>.Iterator {
self.withUnsafeBytes({ buffPtr in
Array(buffPtr).makeIterator()
})
}
}
}
99 changes: 99 additions & 0 deletions Tests/_CryptoExtrasTests/ChaCha20CTRTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftCrypto open source project
//
// Copyright (c) 2023 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

class ChaCha20CTRTests: XCTestCase {

/// Test Vector - https://datatracker.ietf.org/doc/html/rfc9001#name-chacha20-poly1305-short-hea
func testChaCha20CTR_v1() throws {
let hpKey = try Array(hexString: "25a282b9e82f06f21f488917a4fc8f1b73573685608597d0efcb076b0ab7a7a4")
/// Sample = 0x5e5cd55c41f69080575d7999c25a5bfb
let counterAsData = try Array(hexString: "5e5cd55c")
let counterAsUInt32 = counterAsData.withUnsafeBytes { $0.load(as: UInt32.self) }
btoms20 marked this conversation as resolved.
Show resolved Hide resolved
let iv = try Array(hexString: "41f69080575d7999c25a5bfb")

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

XCTAssertEqual(mask, try Data(hexString: "aefefe7d03"))
XCTAssertEqual(mask, mask2)
}

/// Test Vector - https://www.ietf.org/archive/id/draft-ietf-quic-v2-10.html#name-chacha20-poly1305-short-head
func testChaCha20CTR_v2() throws {
let hpKey = try Array(hexString: "d659760d2ba434a226fd37b35c69e2da8211d10c4f12538787d65645d5d1b8e2")
/// Sample = 0xe7b6b932bc27d786f4bc2bb20f2162ba
let counterAsData = try Array(hexString: "e7b6b932")
let counterAsUInt32 = counterAsData.withUnsafeBytes { $0.load(as: UInt32.self) }
btoms20 marked this conversation as resolved.
Show resolved Hide resolved
let iv = try Array(hexString: "bc27d786f4bc2bb20f2162ba")

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

XCTAssertEqual(mask, try Data(hexString: "97580e32bf"))
XCTAssertEqual(mask, mask2)
}

func testChaCha20CTR_InvalidParameters() throws {
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])
XCTAssertThrowsError(try Insecure.ChaCha20CTR.encrypt(Array<UInt8>(repeating: 0, count: 5), using: keyTooLong, nonce: Insecure.ChaCha20CTR.Nonce())) { error in
guard case CryptoKitError.incorrectKeySize = error else { return XCTFail("Error thrown was of unexpected type: \(error)") }
}

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])
XCTAssertThrowsError(try Insecure.ChaCha20CTR.encrypt(Array<UInt8>(repeating: 0, count: 5), using: keyTooShort, nonce: Insecure.ChaCha20CTR.Nonce())) { error in
guard case CryptoKitError.incorrectKeySize = error else { return XCTFail("Error thrown was of unexpected type: \(error)") }
}

let nonceTooLong: [UInt8] = [188, 39, 215, 134, 244, 188, 43, 178, 15, 33, 98, 186, 14]
XCTAssertThrowsError(try Insecure.ChaCha20CTR.Nonce(data: nonceTooLong)) { error in
guard case CryptoKitError.incorrectParameterSize = error else { return XCTFail("Error thrown was of unexpected type: \(error)") }
}

let nonceTooShort: [UInt8] = [188, 39, 215, 134, 244, 188, 43, 178, 15, 33, 98]
XCTAssertThrowsError(try Insecure.ChaCha20CTR.Nonce(data: nonceTooShort)) { error in
guard case CryptoKitError.incorrectParameterSize = error else { return XCTFail("Error thrown was of unexpected type: \(error)") }
}

let counterTooLong: [UInt8] = [231, 182, 185, 50, 82]
XCTAssertThrowsError(try Insecure.ChaCha20CTR.Counter(data: counterTooLong)) { error in
guard case CryptoKitError.incorrectParameterSize = error else { return XCTFail("Error thrown was of unexpected type: \(error)") }
}

let counterTooShort: [UInt8] = [231, 182, 185]
XCTAssertThrowsError(try Insecure.ChaCha20CTR.Counter(data: counterTooShort)) { error in
guard case CryptoKitError.incorrectParameterSize = error else { return XCTFail("Error thrown was of unexpected type: \(error)") }
}

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])

// Ensure UInt32.max Counter Supported
XCTAssertNoThrow(try Insecure.ChaCha20CTR.encrypt(Array<UInt8>(repeating: 0, count: 5), using: key, counter: Insecure.ChaCha20CTR.Counter(offset: UInt32.max), nonce: Insecure.ChaCha20CTR.Nonce()))

// Assert that two calls with the same Counter + Nonce params results in the same output
let nonce = Insecure.ChaCha20CTR.Nonce()
let counter = Insecure.ChaCha20CTR.Counter()
let ciphertext1 = try Insecure.ChaCha20CTR.encrypt(Array<UInt8>(repeating: 0, count: 5), using: key, counter: counter, nonce: nonce)
let ciphertext2 = try Insecure.ChaCha20CTR.encrypt(Array<UInt8>(repeating: 0, count: 5), using: key, counter: counter, nonce: nonce)
XCTAssertEqual(ciphertext1, ciphertext2)

// Assert that two calls with different Nonce params results in different output
let ciphertext3 = try Insecure.ChaCha20CTR.encrypt(Array<UInt8>(repeating: 0, count: 5), using: key, counter: counter, nonce: Insecure.ChaCha20CTR.Nonce())
let ciphertext4 = try Insecure.ChaCha20CTR.encrypt(Array<UInt8>(repeating: 0, count: 5), using: key, counter: counter, nonce: Insecure.ChaCha20CTR.Nonce())
XCTAssertNotEqual(ciphertext3, ciphertext4)
}
}