Browse Source

Merge branch 'develop'

Marcin Krzyzanowski 7 years ago
parent
commit
f7d6fa6121

+ 4 - 2
README.md

@@ -431,7 +431,8 @@ encryption
 
 ```swift
 do {
-    let gcm = GCM(iv: iv)
+    // In combined mode, the authentication tag is directly appended to the encrypted message. This is usually what you want.
+    let gcm = GCM(iv: iv, mode: .combined)
     let aes = try AES(key: key, blockMode: gcm, padding: .noPadding)
     let encrypted = try aes.encrypt(plaintext)
     let tag = gcm.authenticationTag
@@ -444,7 +445,8 @@ decryption
 
 ```swift
 do {
-    let gcm = GCM(iv: iv, authenticationTag: tag)
+    // In combined mode, the authentication tag is directly appended to the encrypted message. This is usually what you want.
+    let gcm = GCM(iv: iv, mode: .combined)
     let aes = try AES(key: key, blockMode: gcm, padding: .noPadding)
     return try aes.decrypt(encrypted)
 } catch {

+ 13 - 6
Sources/CryptoSwift/AES.Cryptors.swift

@@ -101,9 +101,16 @@ extension AES {
 
             var processedBytes = 0
             var plaintext = Array<UInt8>(reserveCapacity: accumulated.count)
-            for chunk in accumulated.batched(by: AES.blockSize) {
+            for var chunk in accumulated.batched(by: AES.blockSize) {
                 if isLast || (accumulated.count - processedBytes) >= AES.blockSize {
-                    plaintext += worker.decrypt(chunk)
+
+                    if isLast, var finalizingWorker = worker as? BlockModeWorkerFinalizing {
+                        chunk = try finalizingWorker.willDecryptLast(ciphertext: chunk)
+                    }
+
+                    if !chunk.isEmpty {
+                        plaintext += worker.decrypt(chunk)
+                    }
 
                     // remove "offset" from the beginning of first chunk
                     if offsetToRemove > 0 {
@@ -111,6 +118,10 @@ extension AES {
                         offsetToRemove = 0
                     }
 
+                    if var finalizingWorker = worker as? BlockModeWorkerFinalizing, isLast == true {
+                        plaintext = try finalizingWorker.didDecryptLast(plaintext: plaintext.slice)
+                    }
+
                     processedBytes += chunk.count
                 }
             }
@@ -121,10 +132,6 @@ extension AES {
                 plaintext = padding.remove(from: plaintext, blockSize: AES.blockSize)
             }
 
-            if var finalizingWorker = worker as? BlockModeWorkerFinalizing, isLast == true {
-                plaintext = try finalizingWorker.finalize(decrypt: plaintext.slice)
-            }
-
             return plaintext
         }
 

+ 5 - 1
Sources/CryptoSwift/BlockMode/BlockModeWorker.swift

@@ -23,6 +23,10 @@ public protocol BlockModeWorker {
 // TODO: remove and merge with BlockModeWorker
 public protocol BlockModeWorkerFinalizing: BlockModeWorker {
     // Any final calculations, eg. calculate tag
+    // Called after the last block is encrypted
     mutating func finalize(encrypt ciphertext: ArraySlice<UInt8>) throws -> Array<UInt8>
-    mutating func finalize(decrypt plaintext: ArraySlice<UInt8>) throws -> Array<UInt8>
+    // Called before decryption, hence input is ciphertext
+    mutating func willDecryptLast(ciphertext: ArraySlice<UInt8>) throws -> ArraySlice<UInt8>
+    // Called after decryption, hence input is ciphertext
+    mutating func didDecryptLast(plaintext: ArraySlice<UInt8>) throws -> Array<UInt8>
 }

+ 51 - 20
Sources/CryptoSwift/BlockMode/GCM.swift

@@ -19,6 +19,13 @@
 //
 
 public final class GCM: BlockMode {
+    public enum Mode {
+        /// In combined mode, the authentication tag is directly appended to the encrypted message. This is usually what you want.
+        case combined
+        /// Some applications may need to store the authentication tag and the encrypted message at different locations
+        case detached
+    }
+
     public let options: BlockModeOptions = [.initializationVectorRequired, .useEncryptToDecrypt]
 
     public enum Error: Swift.Error {
@@ -30,6 +37,7 @@ public final class GCM: BlockMode {
 
     private let iv: Array<UInt8>
     private let additionalAuthenticatedData: Array<UInt8>?
+    private let mode: Mode
 
     // `authenticationTag` nil for encryption, known tag for decryption
     /// For encryption, the value is set at the end of the encryption.
@@ -37,14 +45,15 @@ public final class GCM: BlockMode {
     public var authenticationTag: Array<UInt8>?
 
     // encrypt
-    public init(iv: Array<UInt8>, additionalAuthenticatedData: Array<UInt8>? = nil) {
+    public init(iv: Array<UInt8>, additionalAuthenticatedData: Array<UInt8>? = nil, mode: Mode = .combined) {
         self.iv = iv
         self.additionalAuthenticatedData = additionalAuthenticatedData
+        self.mode = mode
     }
 
     // decrypt
-    public convenience init(iv: Array<UInt8>, authenticationTag: Array<UInt8>, additionalAuthenticatedData: Array<UInt8>? = nil) {
-        self.init(iv: iv, additionalAuthenticatedData: additionalAuthenticatedData)
+    public convenience init(iv: Array<UInt8>, authenticationTag: Array<UInt8>, additionalAuthenticatedData: Array<UInt8>? = nil, mode: Mode = .combined) {
+        self.init(iv: iv, additionalAuthenticatedData: additionalAuthenticatedData, mode: mode)
         self.authenticationTag = authenticationTag
     }
 
@@ -53,23 +62,21 @@ public final class GCM: BlockMode {
             throw Error.invalidInitializationVector
         }
 
-        let worker = GCMModeWorker(iv: iv.slice, aad: additionalAuthenticatedData?.slice, expectedTag: authenticationTag, cipherOperation: cipherOperation)
-        worker.didCalculateTag = { tag in
-            self.authenticationTag = tag
+        let worker = GCMModeWorker(iv: iv.slice, aad: additionalAuthenticatedData?.slice, expectedTag: authenticationTag, mode: mode, cipherOperation: cipherOperation)
+        worker.didCalculateTag = { [weak self] tag in
+            self?.authenticationTag = tag
         }
         return worker
     }
 }
 
-
 // MARK: - Worker
 
 final class GCMModeWorker: BlockModeWorkerFinalizing {
-
     let cipherOperation: CipherOperationOnBlock
 
     // Callback called when authenticationTag is ready
-    var didCalculateTag: ((Array<UInt8>) -> Void)? = nil
+    var didCalculateTag: ((Array<UInt8>) -> Void)?
 
     // 128 bit tag. Other possible tags 4,8,12,13,14,15,16
     private static let tagSize = 16
@@ -79,6 +86,7 @@ final class GCMModeWorker: BlockModeWorkerFinalizing {
     // GCM is designed for 128-bit ciphers like AES (but not really for Blowfish). 64-bit mode is not implemented.
     private let blockSize = 16 // 128 bit
     private let iv: ArraySlice<UInt8>
+    private let mode: GCM.Mode
     private var counter: UInt128
     private let eky0: UInt128 // move to GF?
     private let h: UInt128
@@ -86,7 +94,7 @@ final class GCMModeWorker: BlockModeWorkerFinalizing {
     // Additional authenticated data
     private let aad: ArraySlice<UInt8>?
     // Known Tag used to validate during decryption
-    private let expectedTag: Array<UInt8>?
+    private var expectedTag: Array<UInt8>?
 
     // Note: need new worker to reset instance
     // Use empty aad if not specified. AAD is optional.
@@ -97,13 +105,14 @@ final class GCMModeWorker: BlockModeWorkerFinalizing {
         return GF(aad: [UInt8](), h: h, blockSize: blockSize)
     }()
 
-    init(iv: ArraySlice<UInt8>, aad: ArraySlice<UInt8>? = nil, expectedTag: Array<UInt8>? = nil, cipherOperation: @escaping CipherOperationOnBlock) {
+    init(iv: ArraySlice<UInt8>, aad: ArraySlice<UInt8>? = nil, expectedTag: Array<UInt8>? = nil, mode: GCM.Mode, cipherOperation: @escaping CipherOperationOnBlock) {
         self.cipherOperation = cipherOperation
         self.iv = iv
+        self.mode = mode
         self.aad = aad
         self.expectedTag = expectedTag
-        self.h = UInt128(cipherOperation(Array<UInt8>(repeating: 0, count: blockSize).slice)!) // empty block
-        
+        h = UInt128(cipherOperation(Array<UInt8>(repeating: 0, count: blockSize).slice)!) // empty block
+
         // Assume nonce is 12 bytes long, otherwise initial counter would be calulated from GHASH
         // counter = GF.ghash(aad: [UInt8](), ciphertext: nonce)
         if iv.count == GCMModeWorker.nonceSize {
@@ -154,14 +163,35 @@ final class GCMModeWorker: BlockModeWorkerFinalizing {
 
         // Notify handler
         didCalculateTag?(tag)
-        
-        return Array(ciphertext)
+
+        switch mode {
+        case .combined:
+            return ciphertext + tag
+        case .detached:
+            return Array(ciphertext)
+        }
     }
 
     // The authenticated decryption operation has five inputs: K, IV , C, A, and T. It has only a single
     // output, either the plaintext value P or a special symbol FAIL that indicates that the inputs are not
     // authentic.
-    func finalize(decrypt plaintext: ArraySlice<UInt8>) throws -> Array<UInt8> {
+    func willDecryptLast(ciphertext: ArraySlice<UInt8>) throws -> ArraySlice<UInt8> {
+        // Validate tag
+        switch mode {
+        case .combined:
+            // overwrite expectedTag property used later for verification
+            self.expectedTag = Array(ciphertext.suffix(GCMModeWorker.tagSize))
+            // gf.ciphertextLength = gf.ciphertextLength - GCMModeWorker.tagSize
+            // strip tag from the plaintext.
+            var strippedCiphertext = ciphertext
+            strippedCiphertext.removeLast(GCMModeWorker.tagSize)
+            return strippedCiphertext
+        case .detached:
+            return ciphertext
+        }
+    }
+
+    func didDecryptLast(plaintext: ArraySlice<UInt8>) throws -> Array<UInt8> {
         // Calculate MAC tag.
         let ghash = gf.ghashFinish()
         let computedTag = Array((ghash ^ eky0).bytes.prefix(GCMModeWorker.tagSize))
@@ -173,6 +203,7 @@ final class GCMModeWorker: BlockModeWorkerFinalizing {
 
         throw GCM.Error.fail
     }
+
 }
 
 // MARK: - Local utils
@@ -228,10 +259,10 @@ private final class GF {
 
     init(aad: [UInt8], h: UInt128, blockSize: Int) {
         self.blockSize = blockSize
-        self.aadLength = aad.count
-        self.ciphertextLength = 0
+        aadLength = aad.count
+        ciphertextLength = 0
         self.h = h
-        self.x = 0
+        x = 0
 
         // Calculate for AAD at the begining
         x = GF.calculateX(aad: aad, x: x, h: h, blockSize: blockSize)
@@ -254,7 +285,7 @@ private final class GF {
     // GHASH. One-time calculation
     static func ghash(x startx: UInt128 = 0, h: UInt128, aad: Array<UInt8>, ciphertext: Array<UInt8>, blockSize: Int) -> UInt128 {
         var x = calculateX(aad: aad, x: startx, h: h, blockSize: blockSize)
-            x = calculateX(ciphertext: ciphertext, x: x, h: h, blockSize: blockSize)
+        x = calculateX(ciphertext: ciphertext, x: x, h: h, blockSize: blockSize)
 
         // len(aad) || len(ciphertext)
         let len = UInt128(a: UInt64(aad.count * 8), b: UInt64(ciphertext.count * 8))

+ 34 - 8
Tests/Tests/AESTests.swift

@@ -370,7 +370,7 @@ extension AESTests {
         let key = Array<UInt8>(hex: "0x00000000000000000000000000000000")
         let iv = Array<UInt8>(hex: "0x000000000000000000000000")
 
-        let gcm = GCM(iv: iv)
+        let gcm = GCM(iv: iv, mode: .detached)
         let aes = try! AES(key: key, blockMode: gcm, padding: .noPadding)
         let encrypted = try! aes.encrypt([UInt8]())
         XCTAssertEqual(Array(encrypted), [UInt8](hex: "")) // C
@@ -383,7 +383,7 @@ extension AESTests {
         let plaintext = Array<UInt8>(hex: "0x00000000000000000000000000000000")
         let iv = Array<UInt8>(hex: "0x000000000000000000000000")
 
-        let gcm = GCM(iv: iv)
+        let gcm = GCM(iv: iv, mode: .detached)
         let aes = try! AES(key: key, blockMode: gcm, padding: .noPadding)
         let encrypted = try! aes.encrypt(plaintext)
         XCTAssertEqual(Array(encrypted), [UInt8](hex: "0388dace60b6a392f328c2b971b2fe78")) // C
@@ -396,7 +396,7 @@ extension AESTests {
         let plaintext = Array<UInt8>(hex: "0xd9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255")
         let iv = Array<UInt8>(hex: "0xcafebabefacedbaddecaf888")
 
-        let encGCM = GCM(iv: iv)
+        let encGCM = GCM(iv: iv, mode: .detached)
         let aes = try! AES(key: key, blockMode: encGCM, padding: .noPadding)
         let encrypted = try! aes.encrypt(plaintext)
 
@@ -406,7 +406,7 @@ extension AESTests {
 
         // decrypt
         func decrypt(_ encrypted: Array<UInt8>, tag: Array<UInt8>) -> Array<UInt8> {
-            let decGCM = GCM(iv: iv, authenticationTag: tag)
+            let decGCM = GCM(iv: iv, authenticationTag: tag, mode: .detached)
             let aes = try! AES(key: key, blockMode: decGCM, padding: .noPadding)
             return try! aes.decrypt(encrypted)
         }
@@ -415,6 +415,31 @@ extension AESTests {
         XCTAssertEqual(decrypted, plaintext)
     }
 
+    func testAESGCMTestCase3Combined() {
+        // Test Case 3
+        let key = Array<UInt8>(hex: "0xfeffe9928665731c6d6a8f9467308308")
+        let plaintext = Array<UInt8>(hex: "0xd9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255")
+        let iv = Array<UInt8>(hex: "0xcafebabefacedbaddecaf888")
+
+        let encGCM = GCM(iv: iv, mode: .combined)
+        let aes = try! AES(key: key, blockMode: encGCM, padding: .noPadding)
+        let encrypted = try! aes.encrypt(plaintext)
+
+        XCTAssertNotNil(encGCM.authenticationTag)
+        XCTAssertEqual(Array(encrypted), [UInt8](hex: "0x42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091473f59854d5c2af327cd64a62cf35abd2ba6fab4")) // C
+        XCTAssertEqual(encGCM.authenticationTag, [UInt8](hex: "0x4d5c2af327cd64a62cf35abd2ba6fab4")) // T (128-bit)
+
+        // decrypt
+        func decrypt(_ encrypted: Array<UInt8>) -> Array<UInt8> {
+            let decGCM = GCM(iv: iv, mode: .combined)
+            let aes = try! AES(key: key, blockMode: decGCM, padding: .noPadding)
+            return try! aes.decrypt(encrypted)
+        }
+
+        let decrypted = decrypt(encrypted)
+        XCTAssertEqual(decrypted, plaintext)
+    }
+
     func testAESGCMTestCase4() {
         // Test Case 4
         let key = Array<UInt8>(hex: "0xfeffe9928665731c6d6a8f9467308308")
@@ -422,7 +447,7 @@ extension AESTests {
         let iv = Array<UInt8>(hex: "0xcafebabefacedbaddecaf888")
         let auth = Array<UInt8>(hex: "0xfeedfacedeadbeeffeedfacedeadbeefabaddad2")
 
-        let gcm = GCM(iv: iv, additionalAuthenticatedData: auth)
+        let gcm = GCM(iv: iv, additionalAuthenticatedData: auth, mode: .detached)
         let aes = try! AES(key: key, blockMode: gcm, padding: .noPadding)
         let encrypted = try! aes.encrypt(plaintext)
 
@@ -437,7 +462,7 @@ extension AESTests {
         let iv = Array<UInt8>(hex: "0xcafebabefacedbad")
         let auth = Array<UInt8>(hex: "0xfeedfacedeadbeeffeedfacedeadbeefabaddad2")
 
-        let gcm = GCM(iv: iv, additionalAuthenticatedData: auth)
+        let gcm = GCM(iv: iv, additionalAuthenticatedData: auth, mode: .detached)
         let aes = try! AES(key: key, blockMode: gcm, padding: .noPadding)
         let encrypted = try! aes.encrypt(plaintext)
 
@@ -452,7 +477,7 @@ extension AESTests {
         let iv = Array<UInt8>(hex: "0x9313225df88406e555909c5aff5269aa6a7a9538534f7da1e4c303d2a318a728c3c0c95156809539fcf0e2429a6b525416aedbf5a0de6a57a637b39b")
         let auth = Array<UInt8>(hex: "0xfeedfacedeadbeeffeedfacedeadbeefabaddad2")
 
-        let gcm = GCM(iv: iv, additionalAuthenticatedData: auth)
+        let gcm = GCM(iv: iv, additionalAuthenticatedData: auth, mode: .detached)
         let aes = try! AES(key: key, blockMode: gcm, padding: .noPadding)
         let encrypted = try! aes.encrypt(plaintext)
 
@@ -466,7 +491,7 @@ extension AESTests {
         let plaintext = Array<UInt8>(hex: "")
         let iv = Array<UInt8>(hex: "0x000000000000000000000000")
 
-        let gcm = GCM(iv: iv)
+        let gcm = GCM(iv: iv, mode: .detached)
         let aes = try! AES(key: key, blockMode: gcm, padding: .noPadding)
         let encrypted = try! aes.encrypt(plaintext)
 
@@ -503,6 +528,7 @@ extension AESTests {
             ("testAESGCMTestCase1", testAESGCMTestCase1),
             ("testAESGCMTestCase2", testAESGCMTestCase2),
             ("testAESGCMTestCase3", testAESGCMTestCase3),
+            ("testAESGCMTestCase3Combined", testAESGCMTestCase3Combined),
             ("testAESGCMTestCase4", testAESGCMTestCase4),
             ("testAESGCMTestCase5", testAESGCMTestCase5),
             ("testAESGCMTestCase6", testAESGCMTestCase6),