Browse Source

Bring back seekable decryption to the CTR

Marcin Krzyzanowski 7 years ago
parent
commit
660f9778bc

+ 12 - 8
CryptoSwift.xcodeproj/project.pbxproj

@@ -37,6 +37,7 @@
 		7564F05A2072EAEB00CA5A96 /* CryptoSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 754BE45519693E190098E6F3 /* CryptoSwift.framework */; };
 		7564F0642072ED7000CA5A96 /* CryptoSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 754BE45519693E190098E6F3 /* CryptoSwift.framework */; };
 		7564F0652072ED7000CA5A96 /* CryptoSwift.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 754BE45519693E190098E6F3 /* CryptoSwift.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		756A64C62111083B00BE8805 /* StreamEncryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 756A64C52111083B00BE8805 /* StreamEncryptor.swift */; };
 		7576F64D20725BD6006688F8 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 7576F64C20725BD5006688F8 /* Default-568h@2x.png */; };
 		757DA2531A4ED0A4002BA3EF /* PaddingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 757DA2521A4ED0A4002BA3EF /* PaddingTests.swift */; };
 		757DA2551A4ED408002BA3EF /* AESTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 757DA2541A4ED408002BA3EF /* AESTests.swift */; };
@@ -52,7 +53,7 @@
 		7595C15D2072E5B900EA1A5F /* PBKDFPerf.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7576F6F6207290F8006688F8 /* PBKDFPerf.swift */; };
 		7595C1602072E64900EA1A5F /* CryptoSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 754BE45519693E190098E6F3 /* CryptoSwift.framework */; };
 		75B3ED77210F9DF7005D4ADA /* BlockDecryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B3ED76210F9DF7005D4ADA /* BlockDecryptor.swift */; };
-		75B3ED79210FA016005D4ADA /* Encryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B3ED78210FA016005D4ADA /* Encryptor.swift */; };
+		75B3ED79210FA016005D4ADA /* BlockEncryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B3ED78210FA016005D4ADA /* BlockEncryptor.swift */; };
 		75B601EB197D6A6C0009B53D /* CryptoSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 754BE45519693E190098E6F3 /* CryptoSwift.framework */; };
 		75C2E76D1D55F097003D2BCA /* Access.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75C2E76C1D55F097003D2BCA /* Access.swift */; };
 		75D7AF38208BFB1600D22BEB /* UInt128.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D7AF37208BFB1600D22BEB /* UInt128.swift */; };
@@ -99,7 +100,7 @@
 		75EC52A91EE8B83D0048EB3B /* PKCS7Padding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EC52691EE8B6CA0048EB3B /* PKCS7Padding.swift */; };
 		75EC52AA1EE8B83D0048EB3B /* Poly1305.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EC526A1EE8B6CA0048EB3B /* Poly1305.swift */; };
 		75EC52AB1EE8B83D0048EB3B /* Rabbit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EC526B1EE8B6CA0048EB3B /* Rabbit.swift */; };
-		75EC52AC1EE8B83D0048EB3B /* RandomAccessCryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EC526C1EE8B6CA0048EB3B /* RandomAccessCryptor.swift */; };
+		75EC52AC1EE8B83D0048EB3B /* Cryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EC526C1EE8B6CA0048EB3B /* Cryptor.swift */; };
 		75EC52AD1EE8B83D0048EB3B /* RandomBytesSequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EC526D1EE8B6CA0048EB3B /* RandomBytesSequence.swift */; };
 		75EC52AE1EE8B83D0048EB3B /* SecureBytes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EC526E1EE8B6CA0048EB3B /* SecureBytes.swift */; };
 		75EC52AF1EE8B83D0048EB3B /* SHA1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EC526F1EE8B6CA0048EB3B /* SHA1.swift */; };
@@ -270,6 +271,7 @@
 		755D27BC1D06DE6400C41692 /* CryptoSwift.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = CryptoSwift.playground; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
 		755FB1D9199E347D00475437 /* ExtensionsTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtensionsTest.swift; sourceTree = "<group>"; };
 		7564F0602072EAEB00CA5A96 /* TestsPerformance-iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "TestsPerformance-iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
+		756A64C52111083B00BE8805 /* StreamEncryptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StreamEncryptor.swift; sourceTree = "<group>"; };
 		756BFDCA1A82B87300B9D9A4 /* Bridging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Bridging.h; sourceTree = "<group>"; };
 		7576F64C20725BD5006688F8 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = "<group>"; };
 		7576F6EB20726319006688F8 /* DigestTestsPerf.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DigestTestsPerf.swift; sourceTree = "<group>"; };
@@ -287,7 +289,7 @@
 		7595C14C2072E48C00EA1A5F /* TestsPerformance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestsPerformance.swift; sourceTree = "<group>"; };
 		7595C14E2072E48C00EA1A5F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		75B3ED76210F9DF7005D4ADA /* BlockDecryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockDecryptor.swift; sourceTree = "<group>"; };
-		75B3ED78210FA016005D4ADA /* Encryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encryptor.swift; sourceTree = "<group>"; };
+		75B3ED78210FA016005D4ADA /* BlockEncryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockEncryptor.swift; sourceTree = "<group>"; };
 		75C2E76C1D55F097003D2BCA /* Access.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Access.swift; sourceTree = "<group>"; };
 		75D7AF37208BFB1600D22BEB /* UInt128.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UInt128.swift; sourceTree = "<group>"; };
 		75EC52381EE8B6CA0048EB3B /* AES.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AES.swift; sourceTree = "<group>"; };
@@ -335,7 +337,7 @@
 		75EC52691EE8B6CA0048EB3B /* PKCS7Padding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PKCS7Padding.swift; sourceTree = "<group>"; };
 		75EC526A1EE8B6CA0048EB3B /* Poly1305.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Poly1305.swift; sourceTree = "<group>"; };
 		75EC526B1EE8B6CA0048EB3B /* Rabbit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rabbit.swift; sourceTree = "<group>"; };
-		75EC526C1EE8B6CA0048EB3B /* RandomAccessCryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomAccessCryptor.swift; sourceTree = "<group>"; };
+		75EC526C1EE8B6CA0048EB3B /* Cryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cryptor.swift; sourceTree = "<group>"; };
 		75EC526D1EE8B6CA0048EB3B /* RandomBytesSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomBytesSequence.swift; sourceTree = "<group>"; };
 		75EC526E1EE8B6CA0048EB3B /* SecureBytes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBytes.swift; sourceTree = "<group>"; };
 		75EC526F1EE8B6CA0048EB3B /* SHA1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SHA1.swift; sourceTree = "<group>"; };
@@ -568,7 +570,7 @@
 				75EC52651EE8B6CA0048EB3B /* PKCS */,
 				75EC526A1EE8B6CA0048EB3B /* Poly1305.swift */,
 				75EC526B1EE8B6CA0048EB3B /* Rabbit.swift */,
-				75EC526C1EE8B6CA0048EB3B /* RandomAccessCryptor.swift */,
+				75EC526C1EE8B6CA0048EB3B /* Cryptor.swift */,
 				75EC526D1EE8B6CA0048EB3B /* RandomBytesSequence.swift */,
 				75EC526E1EE8B6CA0048EB3B /* SecureBytes.swift */,
 				75EC526F1EE8B6CA0048EB3B /* SHA1.swift */,
@@ -586,7 +588,8 @@
 				75EC52791EE8B6CA0048EB3B /* ZeroPadding.swift */,
 				754310432050111A003FB1DF /* CompactMap.swift */,
 				75B3ED76210F9DF7005D4ADA /* BlockDecryptor.swift */,
-				75B3ED78210FA016005D4ADA /* Encryptor.swift */,
+				75B3ED78210FA016005D4ADA /* BlockEncryptor.swift */,
+				756A64C52111083B00BE8805 /* StreamEncryptor.swift */,
 			);
 			path = CryptoSwift;
 			sourceTree = "<group>";
@@ -870,11 +873,11 @@
 				7529366A20683DFC00195874 /* AEADChaCha20Poly1305.swift in Sources */,
 				75EC529E1EE8B8230048EB3B /* Generics.swift in Sources */,
 				75EC52AA1EE8B83D0048EB3B /* Poly1305.swift in Sources */,
-				75EC52AC1EE8B83D0048EB3B /* RandomAccessCryptor.swift in Sources */,
+				75EC52AC1EE8B83D0048EB3B /* Cryptor.swift in Sources */,
 				75EC52821EE8B8170048EB3B /* BlockMode.swift in Sources */,
 				75EC52AE1EE8B83D0048EB3B /* SecureBytes.swift in Sources */,
 				75EC528F1EE8B81A0048EB3B /* Cipher.swift in Sources */,
-				75B3ED79210FA016005D4ADA /* Encryptor.swift in Sources */,
+				75B3ED79210FA016005D4ADA /* BlockEncryptor.swift in Sources */,
 				75EC52A01EE8B8290048EB3B /* Int+Extension.swift in Sources */,
 				75EC52B01EE8B83D0048EB3B /* SHA2.swift in Sources */,
 				752BED9D208C120D00FC4743 /* Blowfish+Foundation.swift in Sources */,
@@ -884,6 +887,7 @@
 				75EC52811EE8B8130048EB3B /* BlockCipher.swift in Sources */,
 				75EC52941EE8B81A0048EB3B /* DigestType.swift in Sources */,
 				75EC529B1EE8B8200048EB3B /* Rabbit+Foundation.swift in Sources */,
+				756A64C62111083B00BE8805 /* StreamEncryptor.swift in Sources */,
 				75EC52A61EE8B8390048EB3B /* PBKDF1.swift in Sources */,
 				75EC52B41EE8B83D0048EB3B /* UInt32+Extension.swift in Sources */,
 				75EC52911EE8B81A0048EB3B /* Cryptors.swift in Sources */,

+ 1 - 1
CryptoSwift.xcodeproj/xcshareddata/xcschemes/CryptoSwift.xcscheme

@@ -37,7 +37,7 @@
       </BuildActionEntries>
    </BuildAction>
    <TestAction
-      buildConfiguration = "Debug"
+      buildConfiguration = "Test"
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
       disableMainThreadChecker = "YES"

+ 4 - 1
Sources/CryptoSwift/AES.Cryptors.swift

@@ -18,7 +18,10 @@
 extension AES: Cryptors {
     public func makeEncryptor() throws -> Cryptor & Updatable {
         let worker = try blockMode.worker(blockSize: AES.blockSize, cipherOperation: encrypt)
-        return try Encryptor(blockSize: AES.blockSize, padding: padding, worker)
+        if worker is StreamModeWorker {
+            return try StreamEncryptor(blockSize: AES.blockSize, padding: padding, worker)
+        }
+        return try BlockEncryptor(blockSize: AES.blockSize, padding: padding, worker)
     }
 
     public func makeDecryptor() throws -> Cryptor & Updatable {

+ 12 - 34
Sources/CryptoSwift/BlockDecryptor.swift

@@ -12,15 +12,11 @@
 //  - This notice may not be removed or altered from any source or binary distribution.
 //
 
-public class BlockDecryptor: RandomAccessCryptor, Updatable {
+public class BlockDecryptor: Cryptor, Updatable {
     private let blockSize: Int
     private let padding: Padding
     private var worker: CipherModeWorker
     private var accumulated = Array<UInt8>()
-    private var processedBytesTotalCount: Int = 0
-
-    private var offset: Int = 0
-    private var offsetToRemove: Int = 0
 
     init(blockSize: Int, padding: Padding, _ worker: CipherModeWorker) throws {
         self.blockSize = blockSize
@@ -29,14 +25,7 @@ public class BlockDecryptor: RandomAccessCryptor, Updatable {
     }
 
     public func update(withBytes bytes: ArraySlice<UInt8>, isLast: Bool = false) throws -> Array<UInt8> {
-        // prepend "offset" number of bytes at the beginning
-        if offset > 0 {
-            accumulated += Array<UInt8>(repeating: 0, count: offset) + bytes
-            offsetToRemove = offset
-            offset = 0
-        } else {
-            accumulated += bytes
-        }
+        accumulated += bytes
 
         // If a worker (eg GCM) can combine ciphertext + tag
         // we need to remove tag from the ciphertext.
@@ -66,12 +55,6 @@ public class BlockDecryptor: RandomAccessCryptor, Updatable {
                     plaintext += worker.decrypt(block: chunk)
                 }
 
-                // remove "offset" from the beginning of first chunk
-                if offsetToRemove > 0 {
-                    plaintext.removeFirst(offsetToRemove)
-                    offsetToRemove = 0
-                }
-
                 if var finalizingWorker = worker as? BlockModeWorkerFinalizing, isLast == true {
                     plaintext = try finalizingWorker.didDecryptLast(block: plaintext.slice)
                 }
@@ -80,7 +63,6 @@ public class BlockDecryptor: RandomAccessCryptor, Updatable {
             }
         }
         accumulated.removeFirst(processedBytesCount) // super-slow
-        processedBytesTotalCount += processedBytesCount
 
         if isLast {
             plaintext = padding.remove(from: plaintext, blockSize: blockSize)
@@ -89,18 +71,14 @@ public class BlockDecryptor: RandomAccessCryptor, Updatable {
         return plaintext
     }
 
-    //        @discardableResult public func seek(to position: Int) -> Bool {
-    //            guard var worker = self.worker as? RandomAccessCipherModeWorker else {
-    //                return false
-    //            }
-    //
-    //            worker.counter = UInt(position / blockSize)
-    //            self.worker = worker
-    //
-    //            offset = position % blockSize
-    //
-    //            accumulated = []
-    //
-    //            return true
-    //        }
+    public func seek(to position: Int) throws {
+        guard var worker = self.worker as? StreamModeWorker else {
+            fatalError("Not supported")
+        }
+
+        try worker.seek(to: position)
+        self.worker = worker
+
+        accumulated = []
+    }
 }

+ 5 - 31
Sources/CryptoSwift/Encryptor.swift → Sources/CryptoSwift/BlockEncryptor.swift

@@ -11,53 +11,23 @@
 //  - 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.
 //
-final class Encryptor: Cryptor, Updatable {
+final class BlockEncryptor: Cryptor, Updatable {
     private let blockSize: Int
     private var worker: CipherModeWorker
     private let padding: Padding
     // Accumulated bytes. Not all processed bytes.
     private var accumulated = Array<UInt8>(reserveCapacity: 16)
 
-    private let isStream: Bool
     private var lastBlockRemainder = 0
 
     init(blockSize: Int, padding: Padding, _ worker: CipherModeWorker) throws {
         self.blockSize = blockSize
         self.padding = padding
         self.worker = worker
-        self.isStream = worker is StreamModeWorker
     }
 
     // MARK: Updatable
     public func update(withBytes bytes: ArraySlice<UInt8>, isLast: Bool) throws -> Array<UInt8> {
-        if isStream {
-            return try self.updateStream(withBytes: bytes, isLast: isLast)
-        } else {
-            return try self.updateBlocks(withBytes: bytes, isLast: isLast)
-        }
-    }
-
-    private func updateStream(withBytes bytes: ArraySlice<UInt8>, isLast: Bool) throws -> Array<UInt8> {
-        accumulated = Array(bytes)
-        if isLast {
-            // CTR doesn't need padding. Really. Add padding to the last block if really want. but... don't.
-            accumulated = padding.add(to: accumulated, blockSize: blockSize - lastBlockRemainder)
-        }
-
-        var encrypted = Array<UInt8>(reserveCapacity: bytes.count)
-        for chunk in accumulated.batched(by: blockSize) {
-            encrypted += worker.encrypt(block: chunk)
-        }
-
-        // omit unecessary calculation if not needed
-        if padding != .noPadding {
-            lastBlockRemainder = encrypted.count.quotientAndRemainder(dividingBy: blockSize).remainder
-        }
-
-        return encrypted
-    }
-
-    private func updateBlocks(withBytes bytes: ArraySlice<UInt8>, isLast: Bool) throws -> Array<UInt8> {
         accumulated += bytes
 
         if isLast {
@@ -80,4 +50,8 @@ final class Encryptor: Cryptor, Updatable {
 
         return encrypted
     }
+
+    func seek(to: Int) throws {
+        fatalError("Not supported")
+    }
 }

+ 11 - 2
Sources/CryptoSwift/BlockMode/CTR.swift

@@ -71,7 +71,8 @@ struct CTRModeWorker: StreamModeWorker, CounterModeWorker {
 
     let cipherOperation: CipherOperationOnBlock
     let additionalBufferSize: Int = 0
-    let counter: Counter
+    let iv: Array<UInt8>
+    var counter: CTRCounter
 
     private let blockSize: Int
 
@@ -83,12 +84,20 @@ struct CTRModeWorker: StreamModeWorker, CounterModeWorker {
     init(blockSize: Int, iv: ArraySlice<UInt8>, counter: Int, cipherOperation: @escaping CipherOperationOnBlock) {
         self.cipherOperation = cipherOperation
         self.blockSize = blockSize
+        self.iv = Array(iv)
 
         // the first keystream is calculated from the nonce = initial value of counter
-        self.counter = Counter(nonce: Array(iv), startAt: counter)
+        self.counter = CTRCounter(nonce: Array(iv), startAt: counter)
         self.keystream = Array(cipherOperation(self.counter.bytes.slice)!)
     }
 
+    mutating func seek(to position: Int) throws {
+        let offset = position % blockSize
+        counter = CTRCounter(nonce: iv, startAt: position / blockSize)
+        keystream = Array(cipherOperation(counter.bytes.slice)!)
+        keystreamPosIdx = offset
+    }
+
     // plaintext is at most blockSize long
     mutating func encrypt(block plaintext: ArraySlice<UInt8>) -> Array<UInt8> {
         var result = Array<UInt8>(reserveCapacity: plaintext.count)

+ 4 - 2
Sources/CryptoSwift/BlockMode/CipherModeWorker.swift

@@ -30,10 +30,12 @@ public protocol BlockModeWorker: CipherModeWorker {
 
 public protocol CounterModeWorker: CipherModeWorker {
     associatedtype Counter
-    var counter: Counter { get }
+    var counter: Counter { get set }
 }
 
-public protocol StreamModeWorker: CipherModeWorker { }
+public protocol StreamModeWorker: CipherModeWorker {
+    mutating func seek(to position: Int) throws
+}
 
 // TODO: remove and merge with BlockModeWorker
 public protocol BlockModeWorkerFinalizing: BlockModeWorker {

+ 9 - 0
Sources/CryptoSwift/ChaCha20.swift

@@ -19,6 +19,7 @@
 public final class ChaCha20: BlockCipher {
     public enum Error: Swift.Error {
         case invalidKeyOrInitializationVector
+        case notSupported
     }
 
     public static let blockSize = 64 // 512 / 8
@@ -275,6 +276,10 @@ extension ChaCha20 {
             }
             return encrypted
         }
+
+        public func seek(to: Int) throws {
+            throw Error.notSupported
+        }
     }
 }
 
@@ -320,6 +325,10 @@ extension ChaCha20 {
 
             return plaintext
         }
+
+        public func seek(to: Int) throws {
+            throw Error.notSupported
+        }
     }
 }
 

+ 2 - 7
Sources/CryptoSwift/RandomAccessCryptor.swift → Sources/CryptoSwift/Cryptor.swift

@@ -14,14 +14,9 @@
 //
 
 /// Cryptor (Encryptor or Decryptor)
-public protocol Cryptor { }
-
-/// Random access cryptor
-public protocol RandomAccessCryptor: Cryptor {
+public protocol Cryptor {
     /// Seek to position in file, if block mode allows random access.
     ///
     /// - parameter to: new value of counter
-    ///
-    /// - returns: true if seek succeed
-    /// @discardableResult mutating func seek(to: Int) -> Bool
+    mutating func seek(to: Int) throws
 }

+ 1 - 0
Sources/CryptoSwift/Cryptors.swift

@@ -21,6 +21,7 @@ import Glibc
 
 /// Worker cryptor/decryptor of `Updatable` types
 public protocol Cryptors: class {
+
     /// Cryptor suitable for encryption
     func makeEncryptor() throws -> Cryptor & Updatable
 

+ 54 - 0
Sources/CryptoSwift/StreamEncryptor.swift

@@ -0,0 +1,54 @@
+//  CryptoSwift
+//
+//  Copyright (C) 2014-__YEAR__ 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.
+//
+
+final class StreamEncryptor: Cryptor, Updatable {
+    private let blockSize: Int
+    private var worker: CipherModeWorker
+    private let padding: Padding
+    // Accumulated bytes. Not all processed bytes.
+    private var accumulated = Array<UInt8>(reserveCapacity: 16)
+
+    private var lastBlockRemainder = 0
+
+    init(blockSize: Int, padding: Padding, _ worker: CipherModeWorker) throws {
+        self.blockSize = blockSize
+        self.padding = padding
+        self.worker = worker
+    }
+
+    // MARK: Updatable
+    public func update(withBytes bytes: ArraySlice<UInt8>, isLast: Bool) throws -> Array<UInt8> {
+        accumulated = Array(bytes)
+        if isLast {
+            // CTR doesn't need padding. Really. Add padding to the last block if really want. but... don't.
+            accumulated = padding.add(to: accumulated, blockSize: blockSize - lastBlockRemainder)
+        }
+
+        var encrypted = Array<UInt8>(reserveCapacity: bytes.count)
+        for chunk in accumulated.batched(by: blockSize) {
+            encrypted += worker.encrypt(block: chunk)
+        }
+
+        // omit unecessary calculation if not needed
+        if padding != .noPadding {
+            lastBlockRemainder = encrypted.count.quotientAndRemainder(dividingBy: blockSize).remainder
+        }
+
+        return encrypted
+    }
+
+    func seek(to: Int) throws {
+        fatalError("Not supported")
+    }
+}

+ 41 - 40
Tests/Tests/AESTests.swift

@@ -246,45 +246,46 @@ final class AESTests: XCTestCase {
     }
 
     // https://github.com/krzyzanowskim/CryptoSwift/pull/290
-//    func testAESDecryptCTRSeek() {
-//        let key: Array<UInt8> = [0x52, 0x72, 0xb5, 0x9c, 0xab, 0x07, 0xc5, 0x01, 0x11, 0x7a, 0x39, 0xb6, 0x10, 0x35, 0x87, 0x02]
-//        let iv: Array<UInt8> = [0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]
-//        var plaintext: Array<UInt8> = Array<UInt8>(repeating: 0, count: 6000)
-//
-//        for i in 0..<plaintext.count / 6 {
-//            let s = String(format: "%05d", i).bytes
-//            plaintext[i * 6 + 0] = s[0]
-//            plaintext[i * 6 + 1] = s[1]
-//            plaintext[i * 6 + 2] = s[2]
-//            plaintext[i * 6 + 3] = s[3]
-//            plaintext[i * 6 + 4] = s[4]
-//            plaintext[i * 6 + 5] = "|".utf8.first!
-//        }
-//
-//        let aes = try! AES(key: key, blockMode: CTR(iv: iv), padding: .noPadding)
-//        let encrypted = try! aes.encrypt(plaintext)
-//
-//        var decryptor = try! aes.makeDecryptor()
-//        decryptor.seek(to: 2)
-//        var part1 = try! decryptor.update(withBytes: Array(encrypted[2..<5]))
-//        part1 += try! decryptor.finish()
-//        XCTAssertEqual(part1, Array(plaintext[2..<5]), "seek decryption failed")
-//
-//        decryptor.seek(to: 1000)
-//        var part2 = try! decryptor.update(withBytes: Array(encrypted[1000..<1200]))
-//        part2 += try! decryptor.finish()
-//        XCTAssertEqual(part2, Array(plaintext[1000..<1200]), "seek decryption failed")
-//
-//        decryptor.seek(to: 5500)
-//        var part3 = try! decryptor.update(withBytes: Array(encrypted[5500..<6000]))
-//        part3 += try! decryptor.finish()
-//        XCTAssertEqual(part3, Array(plaintext[5500..<6000]), "seek decryption failed")
-//
-//        decryptor.seek(to: 0)
-//        var part4 = try! decryptor.update(withBytes: Array(encrypted[0..<80]))
-//        part4 += try! decryptor.finish()
-//        XCTAssertEqual(part4, Array(plaintext[0..<80]), "seek decryption failed")
-//    }
+    func testAESDecryptCTRSeek() {
+        let key: Array<UInt8> = [0x52, 0x72, 0xb5, 0x9c, 0xab, 0x07, 0xc5, 0x01, 0x11, 0x7a, 0x39, 0xb6, 0x10, 0x35, 0x87, 0x02]
+        let iv: Array<UInt8> = [0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]
+        var plaintext: Array<UInt8> = Array<UInt8>(repeating: 0, count: 6000)
+
+        for i in 0..<plaintext.count / 6 {
+            let s = String(format: "%05d", i).bytes
+            plaintext[i * 6 + 0] = s[0]
+            plaintext[i * 6 + 1] = s[1]
+            plaintext[i * 6 + 2] = s[2]
+            plaintext[i * 6 + 3] = s[3]
+            plaintext[i * 6 + 4] = s[4]
+            plaintext[i * 6 + 5] = "|".utf8.first!
+        }
+
+        var aes = try! AES(key: key, blockMode: CTR(iv: iv), padding: .noPadding)
+        let encrypted = try! aes.encrypt(plaintext)
+
+        aes = try! AES(key: key, blockMode: CTR(iv: iv), padding: .noPadding)
+        var decryptor = try! aes.makeDecryptor()
+        try! decryptor.seek(to: 2)
+        var part1 = try! decryptor.update(withBytes: Array(encrypted[2..<5]))
+        part1 += try! decryptor.finish()
+        XCTAssertEqual(part1, Array(plaintext[2..<5]), "seek decryption failed")
+
+        try! decryptor.seek(to: 1000)
+        var part2 = try! decryptor.update(withBytes: Array(encrypted[1000..<1010]))
+        part2 += try! decryptor.finish()
+        XCTAssertEqual(part2, Array(plaintext[1000..<1010]), "seek decryption failed")
+
+        try! decryptor.seek(to: 5500)
+        var part3 = try! decryptor.update(withBytes: Array(encrypted[5500..<6000]))
+        part3 += try! decryptor.finish()
+        XCTAssertEqual(part3, Array(plaintext[5500..<6000]), "seek decryption failed")
+
+        try! decryptor.seek(to: 0)
+        var part4 = try! decryptor.update(withBytes: Array(encrypted[0..<80]))
+        part4 += try! decryptor.finish()
+        XCTAssertEqual(part4, Array(plaintext[0..<80]), "seek decryption failed")
+    }
 
     // https://github.com/krzyzanowskim/CryptoSwift/pull/289
     func testAESEncryptCTRIrregularLengthIncrementalUpdate() {
@@ -593,7 +594,7 @@ extension AESTests {
             ("testAESEncryptCTR", testAESEncryptCTR),
             ("testAESEncryptCTRZeroPadding", testAESEncryptCTRZeroPadding),
             ("testAESEncryptCTRIrregularLength", testAESEncryptCTRIrregularLength),
-//            ("testAESDecryptCTRSeek", testAESDecryptCTRSeek),
+            ("testAESDecryptCTRSeek", testAESDecryptCTRSeek),
             ("testAESEncryptCTRIrregularLengthIncrementalUpdate", testAESEncryptCTRIrregularLengthIncrementalUpdate),
             ("testAESEncryptCTRStream", testAESEncryptCTRStream),
             ("testIssue298", testIssue298),