|
|
@@ -0,0 +1,232 @@
|
|
|
+//
|
|
|
+// CryptoSwift
|
|
|
+//
|
|
|
+// Copyright (C) Marcin Krzyżanowski <marcin@krzyzanowskim.com>
|
|
|
+// This software is provided 'as-is', without any express or implied warranty.
|
|
|
+//
|
|
|
+// In no event will the authors be held liable for any damages arising from the use of this software.
|
|
|
+//
|
|
|
+// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
|
|
|
+//
|
|
|
+// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required.
|
|
|
+// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
|
|
|
+// - This notice may not be removed or altered from any source or binary distribution.
|
|
|
+//
|
|
|
+
|
|
|
+/// XChaCha20 is a Swift implementation of the XChaCha20 stream cipher, which is an extension of the ChaCha20 cipher
|
|
|
+/// that uses a 192-bit nonce instead of the original 64-bit nonce. XChaCha20 provides a higher security level by
|
|
|
+/// allowing a larger number of safe random nonces, reducing the risk of nonce reuse.
|
|
|
+///
|
|
|
+/// For more information about the XChaCha20 algorithm, refer to the IETF draft: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha
|
|
|
+public final class XChaCha20: BlockCipher, BlockMode {
|
|
|
+
|
|
|
+ public enum Error: Swift.Error {
|
|
|
+ case invalidKeyOrInitializationVector
|
|
|
+ case notSupported
|
|
|
+ }
|
|
|
+
|
|
|
+ fileprivate var chacha20: ChaCha20
|
|
|
+
|
|
|
+ // MARK: BlockCipher
|
|
|
+
|
|
|
+ public static let blockSize = 64 // 512 / 8
|
|
|
+
|
|
|
+ // MARK: Cipher
|
|
|
+
|
|
|
+ public let keySize: Int
|
|
|
+
|
|
|
+ /// Initializes a new instance of XChaCha20 with the provided key, nonce, and optional block counter.
|
|
|
+ /// - Parameters:
|
|
|
+ /// - key: A 256-bit (32-byte) key for the XChaCha20 cipher.
|
|
|
+ /// - nonce: A 192-bit (24-byte) nonce for the XChaCha20 cipher.
|
|
|
+ /// - blockCounter: An optional initial block counter value, defaulting to 0.
|
|
|
+ /// - Throws: Error.invalidKeyOrInitializationVector if the key or nonce lengths are not valid.
|
|
|
+ public init(key: Array<UInt8>, iv nonce: Array<UInt8>, blockCounter: UInt32 = 0) throws {
|
|
|
+ guard key.count == 32 && nonce.count == 24 else {
|
|
|
+ throw Error.invalidKeyOrInitializationVector
|
|
|
+ }
|
|
|
+
|
|
|
+ self.keySize = key.count
|
|
|
+
|
|
|
+ // From https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#section-2.3
|
|
|
+ // XChaCha20 can be constructed from an existing ChaCha20 implementation
|
|
|
+ // and HChaCha20. All one needs to do is:
|
|
|
+ //
|
|
|
+ // 1. Pass the key and the first 16 bytes of the 24-byte nonce to
|
|
|
+ // HChaCha20 to obtain the subkey.
|
|
|
+ //
|
|
|
+ // 2. Use the subkey and remaining 8 byte nonce with ChaCha20 as normal
|
|
|
+ // (prefixed by 4 NUL bytes, since [RFC8439] specifies a 12-byte
|
|
|
+ // nonce).
|
|
|
+ self.chacha20 = try .init(
|
|
|
+ key: XChaCha20.hChaCha20(key: key, nonce: Array(nonce[0..<16])),
|
|
|
+ iv: [0, 0, 0, 0] + Array(nonce[16..<24]),
|
|
|
+ blockCounter: blockCounter
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: BlockMode
|
|
|
+
|
|
|
+ /// Options specific to the block mode.
|
|
|
+ public let options: BlockModeOption = [.none]
|
|
|
+ /// The custom block size for the block mode, if any. XChaCha20 does not have a custom block size.
|
|
|
+ public let customBlockSize: Int? = nil
|
|
|
+
|
|
|
+ public func worker(blockSize: Int, cipherOperation: @escaping CipherOperationOnBlock, encryptionOperation: @escaping CipherOperationOnBlock) throws -> CipherModeWorker {
|
|
|
+ return XChaCha20Worker(
|
|
|
+ blockSize: blockSize,
|
|
|
+ cipherOperation: cipherOperation,
|
|
|
+ xChaCha20: self
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Computes the HChaCha20 function on the provided key and nonce.
|
|
|
+ ///
|
|
|
+ /// See: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#section-2.2
|
|
|
+ ///
|
|
|
+ /// - Parameters:
|
|
|
+ /// - key: A 256-bit (32-byte) key.
|
|
|
+ /// - nonce: A 128-bit (16-byte) nonce.
|
|
|
+ /// - Returns: A 256-bit (32-byte) derived key.
|
|
|
+ static func hChaCha20(key: [UInt8], nonce: [UInt8]) -> [UInt8] {
|
|
|
+ precondition(key.count == 32)
|
|
|
+ precondition(nonce.count == 16)
|
|
|
+
|
|
|
+ // HChaCha20 is initialized the same way as the ChaCha cipher, except
|
|
|
+ // that HChaCha20 uses a 128-bit nonce and has no counter. Instead, the
|
|
|
+ // block counter is replaced by the first 32 bits of the nonce.
|
|
|
+
|
|
|
+ var state = Array<UInt32>(repeating: 0, count: 16)
|
|
|
+
|
|
|
+ state[0] = 0x61707865
|
|
|
+ state[1] = 0x3320646e
|
|
|
+ state[2] = 0x79622d32
|
|
|
+ state[3] = 0x6b206574
|
|
|
+ for i in 0..<8 {
|
|
|
+ state[4 + i] = UInt32(bytes: key[i * 4..<(i + 1) * 4]).bigEndian
|
|
|
+ }
|
|
|
+ for i in 0..<4 {
|
|
|
+ state[12 + i] = UInt32(bytes: nonce[i * 4..<(i + 1) * 4]).bigEndian
|
|
|
+ }
|
|
|
+
|
|
|
+ // After initialization, proceed through the ChaCha rounds as usual.
|
|
|
+
|
|
|
+ for _ in 1...10 {
|
|
|
+ self.innerBlock(&state)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Once the 20 ChaCha rounds have been completed, the first 128 bits and
|
|
|
+ // last 128 bits of the ChaCha state (both little-endian) are
|
|
|
+ // concatenated, and this 256-bit subkey is returned.
|
|
|
+
|
|
|
+ var output = Array<UInt8>()
|
|
|
+ for i in 0..<4 {
|
|
|
+ output += state[i].bigEndian.bytes()
|
|
|
+ }
|
|
|
+ for i in 0..<4 {
|
|
|
+ output += state[12 + i].bigEndian.bytes()
|
|
|
+ }
|
|
|
+
|
|
|
+ return output
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Performs the "quarter round" operation on the provided state at the specified indices.
|
|
|
+ /// - Parameters:
|
|
|
+ /// - state: The state on which to perform the operation.
|
|
|
+ /// - a: The index of the first element in the state.
|
|
|
+ /// - b: The index of the second element in the state.
|
|
|
+ /// - c: The index of the third element in the state.
|
|
|
+ /// - d: The index of the fourth element in the state.
|
|
|
+ static func qRound(_ state: inout [UInt32], _ a: Int, _ b: Int, _ c: Int, _ d: Int) {
|
|
|
+ state[a] = state[a] &+ state[b]
|
|
|
+ state[d] ^= state[a]
|
|
|
+ state[d] = (state[d] << 16) | (state[d] >> 16)
|
|
|
+ state[c] = state[c] &+ state[d]
|
|
|
+ state[b] ^= state[c]
|
|
|
+ state[b] = (state[b] << 12) | (state[b] >> 20)
|
|
|
+ state[a] = state[a] &+ state[b]
|
|
|
+ state[d] ^= state[a]
|
|
|
+ state[d] = (state[d] << 8) | (state[d] >> 24)
|
|
|
+ state[c] = state[c] &+ state[d]
|
|
|
+ state[b] ^= state[c]
|
|
|
+ state[b] = (state[b] << 7) | (state[b] >> 25)
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Performs the inner block operation on the provided state.
|
|
|
+ /// - Parameter state: The state on which to perform the operation.
|
|
|
+ static func innerBlock(_ state: inout [UInt32]) {
|
|
|
+ self.qRound(&state, 0, 4, 8, 12)
|
|
|
+ self.qRound(&state, 1, 5, 9, 13)
|
|
|
+ self.qRound(&state, 2, 6, 10, 14)
|
|
|
+ self.qRound(&state, 3, 7, 11, 15)
|
|
|
+ self.qRound(&state, 0, 5, 10, 15)
|
|
|
+ self.qRound(&state, 1, 6, 11, 12)
|
|
|
+ self.qRound(&state, 2, 7, 8, 13)
|
|
|
+ self.qRound(&state, 3, 4, 9, 14)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// MARK: Cipher
|
|
|
+
|
|
|
+extension XChaCha20: Cipher {
|
|
|
+ public func encrypt(_ bytes: ArraySlice<UInt8>) throws -> Array<UInt8> {
|
|
|
+ try self.chacha20.encrypt(bytes)
|
|
|
+ }
|
|
|
+
|
|
|
+ public func decrypt(_ bytes: ArraySlice<UInt8>) throws -> Array<UInt8> {
|
|
|
+ try self.encrypt(bytes)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// MARK: Cryptors
|
|
|
+
|
|
|
+extension XChaCha20: Cryptors {
|
|
|
+
|
|
|
+ public func makeEncryptor() throws -> Cryptor & Updatable {
|
|
|
+ return try BlockEncryptor(
|
|
|
+ blockSize: XChaCha20.blockSize,
|
|
|
+ padding: .noPadding,
|
|
|
+ self.worker(
|
|
|
+ blockSize: XChaCha20.blockSize,
|
|
|
+ cipherOperation: { _ in nil },
|
|
|
+ encryptionOperation: { _ in nil }
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ public func makeDecryptor() throws -> Cryptor & Updatable {
|
|
|
+ return try BlockDecryptor(
|
|
|
+ blockSize: XChaCha20.blockSize,
|
|
|
+ padding: .noPadding,
|
|
|
+ self.worker(
|
|
|
+ blockSize: XChaCha20.blockSize,
|
|
|
+ cipherOperation: { _ in nil },
|
|
|
+ encryptionOperation: { _ in nil }
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class XChaCha20Worker: CipherModeWorker {
|
|
|
+ let blockSize: Int
|
|
|
+ let cipherOperation: CipherOperationOnBlock
|
|
|
+ let xChaCha20: XChaCha20
|
|
|
+
|
|
|
+ init(blockSize: Int, cipherOperation: @escaping CipherOperationOnBlock, xChaCha20: XChaCha20) {
|
|
|
+ self.blockSize = blockSize
|
|
|
+ self.cipherOperation = cipherOperation
|
|
|
+ self.xChaCha20 = xChaCha20
|
|
|
+ }
|
|
|
+
|
|
|
+ var additionalBufferSize: Int {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+
|
|
|
+ func encrypt(block plaintext: ArraySlice<UInt8>) -> Array<UInt8> {
|
|
|
+ return (try? self.xChaCha20.encrypt(plaintext)) ?? .init()
|
|
|
+ }
|
|
|
+
|
|
|
+ func decrypt(block ciphertext: ArraySlice<UInt8>) -> Array<UInt8> {
|
|
|
+ return (try? self.xChaCha20.decrypt(ciphertext)) ?? .init()
|
|
|
+ }
|
|
|
+}
|