diff --git a/CryptomatorCryptoLib.xcodeproj/project.pbxproj b/CryptomatorCryptoLib.xcodeproj/project.pbxproj index a545370..1c522af 100644 --- a/CryptomatorCryptoLib.xcodeproj/project.pbxproj +++ b/CryptomatorCryptoLib.xcodeproj/project.pbxproj @@ -34,6 +34,8 @@ 9E9BB81624558DFF00F9FF51 /* MasterkeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E9BB81524558DFF00F9FF51 /* MasterkeyTests.swift */; }; 9EB822C1248AF82200879838 /* AesCtr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB822C0248AF82200879838 /* AesCtr.swift */; }; 9EB822C3248AF9C500879838 /* AesCtrTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB822C2248AF9C500879838 /* AesCtrTests.swift */; }; + 9EBEC947283782E6002210DE /* CtrCryptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBEC946283782E6002210DE /* CtrCryptorTests.swift */; }; + 9EBEC94928378308002210DE /* GcmCryptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBEC94828378308002210DE /* GcmCryptorTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -97,6 +99,8 @@ 9E9BB81524558DFF00F9FF51 /* MasterkeyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterkeyTests.swift; sourceTree = ""; }; 9EB822C0248AF82200879838 /* AesCtr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AesCtr.swift; sourceTree = ""; }; 9EB822C2248AF9C500879838 /* AesCtrTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AesCtrTests.swift; sourceTree = ""; }; + 9EBEC946283782E6002210DE /* CtrCryptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CtrCryptorTests.swift; sourceTree = ""; }; + 9EBEC94828378308002210DE /* GcmCryptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GcmCryptorTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -172,6 +176,8 @@ 9E44EEA724599C7800A37B01 /* AesSivTests.swift */, 9E35C4EA24576A3D0006E50C /* CryptorTests.swift */, 74A5B57D25A86A69002D10F7 /* CryptoSupportMock.swift */, + 9EBEC946283782E6002210DE /* CtrCryptorTests.swift */, + 9EBEC94828378308002210DE /* GcmCryptorTests.swift */, 74A5B57525A869DD002D10F7 /* MasterkeyFileTests.swift */, 9E9BB81524558DFF00F9FF51 /* MasterkeyTests.swift */, ); @@ -443,10 +449,12 @@ files = ( 74A5B57E25A86A69002D10F7 /* CryptoSupportMock.swift in Sources */, 9E44EEA92459AB1500A37B01 /* AesSivTests.swift in Sources */, + 9EBEC947283782E6002210DE /* CtrCryptorTests.swift in Sources */, 9EB822C3248AF9C500879838 /* AesCtrTests.swift in Sources */, 74A5B57625A869DD002D10F7 /* MasterkeyFileTests.swift in Sources */, 9E9BB81624558DFF00F9FF51 /* MasterkeyTests.swift in Sources */, 9E35C4EB24576A3D0006E50C /* CryptorTests.swift in Sources */, + 9EBEC94928378308002210DE /* GcmCryptorTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -528,8 +536,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MACOSX_DEPLOYMENT_TARGET = 10.12; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -588,8 +596,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MACOSX_DEPLOYMENT_TARGET = 10.12; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-expression-type-checking=200 -Xfrontend -warn-long-function-bodies=200"; diff --git a/Package.swift b/Package.swift index db6c710..05a4d30 100644 --- a/Package.swift +++ b/Package.swift @@ -13,8 +13,8 @@ import PackageDescription let package = Package( name: "CryptomatorCryptoLib", platforms: [ - .iOS(.v9), - .macOS(.v10_12) + .iOS(.v13), + .macOS(.v10_15) ], products: [ .library(name: "CryptomatorCryptoLib", targets: ["CryptomatorCryptoLib"]) diff --git a/README.md b/README.md index 7b6b31c..d733576 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ For more information on the Cryptomator encryption scheme, visit the security ar ## Requirements -- iOS 9.0 or higher -- macOS 10.12 or higher +- iOS 13.0 or higher +- macOS 10.15 or higher ## Installation @@ -21,7 +21,7 @@ For more information on the Cryptomator encryption scheme, visit the security ar You can use [Swift Package Manager](https://swift.org/package-manager/ "Swift Package Manager"). ```swift -.package(url: "https://github.com/cryptomator/cryptolib-swift.git", .upToNextMinor(from: "1.0.0")) +.package(url: "https://github.com/cryptomator/cryptolib-swift.git", .upToNextMinor(from: "1.1.0")) ``` ## Usage @@ -121,13 +121,16 @@ try MasterkeyFile.changePassphrase(masterkeyFileData: masterkeyFileData, oldPass #### Constructor -Create a cryptor by providing a masterkey. +Create a cryptor by providing a masterkey and a scheme (e.g., `.sivGcm`). ```swift let masterkey = ... -let cryptor = Cryptor(masterkey: masterkey) +let scheme = ... +let cryptor = Cryptor(masterkey: masterkey, scheme: scheme) ``` +Make sure that the data you're working with is compatible with the provided scheme. + #### Path Encryption and Decryption Encrypt the directory ID in order to determine the encrypted directory URL. @@ -178,8 +181,9 @@ Please read our [contribution guide](.github/CONTRIBUTING.md), if you would like In general, the following preference is used to choose the implementation of cryptographic primitives: -1. Apple Swift Crypto (HMAC) -2. Apple CommonCrypto (AES-CTR, RFC 3394 Key Derivation) +1. Apple CryptoKit (AES-GCM) +2. Apple Swift Crypto (HMAC) +3. Apple CommonCrypto (AES-CTR, RFC 3394 Key Derivation) This project uses [SwiftFormat](https://github.com/nicklockwood/SwiftFormat) and [SwiftLint](https://github.com/realm/SwiftLint) to enforce code style and conventions. Install these tools if you haven't already. @@ -202,4 +206,4 @@ Help us keep Cryptomator open and inclusive. Please read and follow our [Code of ## License -Distributed under the AGPLv3. See the LICENSE file for more info. +This project is dual-licensed under the AGPLv3 for FOSS projects as well as a commercial license derived from the LGPL for independent software vendors and resellers. If you want to use this library in applications that are *not* licensed under the AGPL, feel free to contact our [sales team](https://cryptomator.org/enterprise/). diff --git a/Scripts/process.sh b/Scripts/process.sh index 9a0da33..9ab497b 100755 --- a/Scripts/process.sh +++ b/Scripts/process.sh @@ -26,8 +26,8 @@ function process_output() { } if [ "$staged_mode" = true ]; then - process_output "SwiftFormat" "python ./Scripts/git-format-staged.py -f 'swiftformat stdin --stdinpath \"{}\" --quiet' '*.swift'" - process_output "SwiftLint" "python ./Scripts/git-format-staged.py --no-write -f 'swiftlint --use-stdin --quiet >&2' '*.swift'" + process_output "SwiftFormat" "python3 ./Scripts/git-format-staged.py -f 'swiftformat stdin --stdinpath \"{}\" --quiet' '*.swift'" + process_output "SwiftLint" "python3 ./Scripts/git-format-staged.py --no-write -f 'swiftlint --use-stdin --quiet >&2' '*.swift'" if [[ "$final_status" -gt 0 ]]; then printf '\nChanges werde made or are required. Please review the output above for further details.\n' fi diff --git a/Sources/CryptomatorCryptoLib/ContentCryptor.swift b/Sources/CryptomatorCryptoLib/ContentCryptor.swift index ffc1014..bb4545f 100644 --- a/Sources/CryptomatorCryptoLib/ContentCryptor.swift +++ b/Sources/CryptomatorCryptoLib/ContentCryptor.swift @@ -7,6 +7,7 @@ // import CommonCrypto +import CryptoKit import Foundation protocol ContentCryptor { @@ -22,7 +23,7 @@ protocol ContentCryptor { - Parameter ad: Associated data, which needs to be authenticated during decryption. - Returns: Nonce/IV + ciphertext + MAC/tag, as a concatenated byte array. */ - func encrypt(_ chunk: [UInt8], key: [UInt8], nonce: [UInt8], ad: [UInt8]...) throws -> [UInt8] + func encrypt(_ chunk: [UInt8], key: [UInt8], nonce: [UInt8], ad: [UInt8]) throws -> [UInt8] /** Decrypts one single chunk of encrypted data. @@ -32,7 +33,63 @@ protocol ContentCryptor { - Parameter ad: Associated data, which needs to be authenticated during decryption. - Returns: The original cleartext. */ - func decrypt(_ chunk: [UInt8], key: [UInt8], ad: [UInt8]...) throws -> [UInt8] + func decrypt(_ chunk: [UInt8], key: [UInt8], ad: [UInt8]) throws -> [UInt8] + + /** + Constructs the associated data which will be authenticated during encryption/decryption of a single chunk + + - Parameter chunkNumber: The index of the chunk (starting at 0), preventing swapping of chunks + - Parameter headerNonce: The nonce used in the file header, binding the chunk to this particular file. + - Returns: The combined associated data. + */ + func ad(chunkNumber: UInt64, headerNonce: [UInt8]) -> [UInt8] +} + +extension ContentCryptor { + func encryptHeader(_ header: [UInt8], key: [UInt8], nonce: [UInt8]) throws -> [UInt8] { + return try encrypt(header, key: key, nonce: nonce, ad: []) + } + + func decryptHeader(_ header: [UInt8], key: [UInt8]) throws -> [UInt8] { + return try decrypt(header, key: key, ad: []) + } + + func encryptChunk(_ chunk: [UInt8], chunkNumber: UInt64, chunkNonce: [UInt8], fileKey: [UInt8], headerNonce: [UInt8]) throws -> [UInt8] { + let ad = ad(chunkNumber: chunkNumber, headerNonce: headerNonce) + return try encrypt(chunk, key: fileKey, nonce: chunkNonce, ad: ad) + } + + func decryptChunk(_ chunk: [UInt8], chunkNumber: UInt64, fileKey: [UInt8], headerNonce: [UInt8]) throws -> [UInt8] { + let ad = ad(chunkNumber: chunkNumber, headerNonce: headerNonce) + return try decrypt(chunk, key: fileKey, ad: ad) + } +} + +class GcmContentCryptor: ContentCryptor { + let nonceLen = 12 // 96 bit + let tagLen = 16 // 128 bit + + func ad(chunkNumber: UInt64, headerNonce: [UInt8]) -> [UInt8] { + return chunkNumber.bigEndian.byteArray() + headerNonce + } + + func encrypt(_ chunk: [UInt8], key keyBytes: [UInt8], nonce nonceBytes: [UInt8], ad: [UInt8]) throws -> [UInt8] { + let key = SymmetricKey(data: keyBytes) + let nonce = try AES.GCM.Nonce(data: nonceBytes) + let encrypted = try AES.GCM.seal(chunk, using: key, nonce: nonce, authenticating: ad) + + return [UInt8](encrypted.nonce + encrypted.ciphertext + encrypted.tag) + } + + func decrypt(_ chunk: [UInt8], key keyBytes: [UInt8], ad: [UInt8]) throws -> [UInt8] { + assert(chunk.count >= nonceLen + tagLen, "ciphertext chunk must at least contain nonce + tag") + + let key = SymmetricKey(data: keyBytes) + let encrypted = try AES.GCM.SealedBox(combined: chunk) + let decrypted = try AES.GCM.open(encrypted, using: key, authenticating: ad) + + return [UInt8](decrypted) + } } class CtrThenHmacContentCryptor: ContentCryptor { @@ -47,13 +104,17 @@ class CtrThenHmacContentCryptor: ContentCryptor { self.cryptoSupport = cryptoSupport } - func encrypt(_ chunk: [UInt8], key: [UInt8], nonce: [UInt8], ad: [UInt8]...) throws -> [UInt8] { + func ad(chunkNumber: UInt64, headerNonce: [UInt8]) -> [UInt8] { + return headerNonce + chunkNumber.bigEndian.byteArray() + } + + func encrypt(_ chunk: [UInt8], key: [UInt8], nonce: [UInt8], ad: [UInt8]) throws -> [UInt8] { let ciphertext = try AesCtr.compute(key: key, iv: nonce, data: chunk) let mac = computeHmac(ciphertext, nonce: nonce, ad: ad) return nonce + ciphertext + mac } - func decrypt(_ chunk: [UInt8], key: [UInt8], ad: [UInt8]...) throws -> [UInt8] { + func decrypt(_ chunk: [UInt8], key: [UInt8], ad: [UInt8]) throws -> [UInt8] { assert(chunk.count >= nonceLen + tagLen, "ciphertext chunk must at least contain nonce + tag") // decompose chunk: @@ -72,8 +133,8 @@ class CtrThenHmacContentCryptor: ContentCryptor { return try AesCtr.compute(key: key, iv: chunkNonce, data: ciphertext) } - private func computeHmac(_ ciphertext: [UInt8], nonce: [UInt8], ad: [[UInt8]]) -> [UInt8] { - let data = ad.reduce([UInt8](), +) + nonce + ciphertext + private func computeHmac(_ ciphertext: [UInt8], nonce: [UInt8], ad: [UInt8]) -> [UInt8] { + let data = ad + nonce + ciphertext var mac = [UInt8](repeating: 0x00, count: tagLen) CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), macKey, macKey.count, data, data.count, &mac) return mac diff --git a/Sources/CryptomatorCryptoLib/Cryptor.swift b/Sources/CryptomatorCryptoLib/Cryptor.swift index 3dae122..732813f 100644 --- a/Sources/CryptomatorCryptoLib/Cryptor.swift +++ b/Sources/CryptomatorCryptoLib/Cryptor.swift @@ -49,6 +49,11 @@ public extension InputStream { } } +public enum CryptorScheme: String, Codable { + case sivCtrMac = "SIV_CTRMAC" + case sivGcm = "SIV_GCM" +} + public enum FileNameEncoding: String { case base64url case base32 @@ -66,8 +71,8 @@ public class Cryptor { return contentCryptor.nonceLen + fileHeaderPayloadSize + contentCryptor.tagLen } - private let cleartextChunkSize = 32 * 1024 - private var ciphertextChunkSize: Int { + let cleartextChunkSize = 32 * 1024 + var ciphertextChunkSize: Int { return contentCryptor.nonceLen + cleartextChunkSize + contentCryptor.tagLen } @@ -81,9 +86,15 @@ public class Cryptor { self.contentCryptor = contentCryptor } - public convenience init(masterkey: Masterkey) { + public convenience init(masterkey: Masterkey, scheme: CryptorScheme) { let cryptoSupport = CryptoSupport() - let contentCryptor = CtrThenHmacContentCryptor(macKey: masterkey.macMasterKey, cryptoSupport: cryptoSupport) + let contentCryptor: ContentCryptor + switch scheme { + case .sivCtrMac: + contentCryptor = CtrThenHmacContentCryptor(macKey: masterkey.macMasterKey, cryptoSupport: cryptoSupport) + case .sivGcm: + contentCryptor = GcmContentCryptor() + } self.init(masterkey: masterkey, cryptoSupport: cryptoSupport, contentCryptor: contentCryptor) } @@ -154,19 +165,19 @@ public class Cryptor { // MARK: - File Header Encryption and Decryption func createHeader() throws -> FileHeader { - let nonce = try cryptoSupport.createRandomBytes(size: kCCBlockSizeAES128) + let nonce = try cryptoSupport.createRandomBytes(size: contentCryptor.nonceLen) let contentKey = try cryptoSupport.createRandomBytes(size: kCCKeySizeAES256) return FileHeader(nonce: nonce, contentKey: contentKey) } func encryptHeader(_ header: FileHeader) throws -> [UInt8] { let cleartext = [UInt8](repeating: 0xFF, count: fileHeaderLegacyPayloadSize) + header.contentKey - return try contentCryptor.encrypt(cleartext, key: masterkey.aesMasterKey, nonce: header.nonce) + return try contentCryptor.encryptHeader(cleartext, key: masterkey.aesMasterKey, nonce: header.nonce) } func decryptHeader(_ header: [UInt8]) throws -> FileHeader { let nonce = [UInt8](header[0 ..< contentCryptor.nonceLen]) - let cleartext = try contentCryptor.decrypt(header, key: masterkey.aesMasterKey) + let cleartext = try contentCryptor.decryptHeader(header, key: masterkey.aesMasterKey) let contentKey = [UInt8](cleartext[fileHeaderLegacyPayloadSize...]) return FileHeader(nonce: nonce, contentKey: contentKey) } @@ -301,12 +312,12 @@ public class Cryptor { } func encryptSingleChunk(_ chunk: [UInt8], chunkNumber: UInt64, headerNonce: [UInt8], fileKey: [UInt8]) throws -> [UInt8] { - let chunkNonce = try cryptoSupport.createRandomBytes(size: kCCBlockSizeAES128) - return try contentCryptor.encrypt(chunk, key: fileKey, nonce: chunkNonce, ad: headerNonce, chunkNumber.bigEndian.byteArray()) + let chunkNonce = try cryptoSupport.createRandomBytes(size: contentCryptor.nonceLen) + return try contentCryptor.encryptChunk(chunk, chunkNumber: chunkNumber, chunkNonce: chunkNonce, fileKey: fileKey, headerNonce: headerNonce) } func decryptSingleChunk(_ chunk: [UInt8], chunkNumber: UInt64, headerNonce: [UInt8], fileKey: [UInt8]) throws -> [UInt8] { - return try contentCryptor.decrypt(chunk, key: fileKey, ad: headerNonce, chunkNumber.bigEndian.byteArray()) + return try contentCryptor.decryptChunk(chunk, chunkNumber: chunkNumber, fileKey: fileKey, headerNonce: headerNonce) } // MARK: - File Size Calculation diff --git a/Tests/CryptomatorCryptoLibTests/CryptorTests.swift b/Tests/CryptomatorCryptoLibTests/CryptorTests.swift index bdd77ef..5f3f91d 100644 --- a/Tests/CryptomatorCryptoLibTests/CryptorTests.swift +++ b/Tests/CryptomatorCryptoLibTests/CryptorTests.swift @@ -10,17 +10,19 @@ import XCTest @testable import CryptomatorCryptoLib class CryptorTests: XCTestCase { + var contentCryptor: ContentCryptor! var cryptor: Cryptor! var tmpDirURL: URL! - override func setUpWithError() throws { - let aesKey = [UInt8](repeating: 0x55, count: 32) - let macKey = [UInt8](repeating: 0x77, count: 32) - let masterkey = Masterkey.createFromRaw(aesMasterKey: aesKey, macMasterKey: macKey) - let cryptoSupport = CryptoSupportMock() - let contentCryptor = CtrThenHmacContentCryptor(macKey: macKey, cryptoSupport: cryptoSupport) - cryptor = Cryptor(masterkey: masterkey, cryptoSupport: cryptoSupport, contentCryptor: contentCryptor) + override class var defaultTestSuite: XCTestSuite { + // Return empty `XCTestSuite` so that no tests from this "abstract" `XCTestCase` is run. + // Make sure to override this in subclasses so that the implemented test case can run. + return XCTestSuite(name: "InterfaceTests Excluded") + } + func setUpWithError(masterkey: Masterkey, cryptoSupport: CryptoSupport, contentCryptor: ContentCryptor) throws { + self.contentCryptor = contentCryptor + cryptor = Cryptor(masterkey: masterkey, cryptoSupport: cryptoSupport, contentCryptor: contentCryptor) tmpDirURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tmpDirURL, withIntermediateDirectories: true) } @@ -59,50 +61,6 @@ class CryptorTests: XCTestCase { } } - func testCreateHeader() throws { - let header = try cryptor.createHeader() - XCTAssertEqual([UInt8](repeating: 0xF0, count: 16), header.nonce) - XCTAssertEqual([UInt8](repeating: 0xF0, count: 32), header.contentKey) - } - - func testEncryptHeader() throws { - let header = try cryptor.createHeader() - let encrypted = try cryptor.encryptHeader(header) - let expected: [UInt8] = [ - 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, - 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, - 0x0D, 0x91, 0xF2, 0x9C, 0xC6, 0x35, 0xD7, 0x5E, - 0x1E, 0x42, 0x23, 0x1E, 0xC7, 0x90, 0x57, 0xE3, - 0x8D, 0x98, 0xF3, 0x58, 0x07, 0x2C, 0x9F, 0x03, - 0xBC, 0xEA, 0x5A, 0x98, 0x3B, 0x68, 0x62, 0x89, - 0x3E, 0xBC, 0x5E, 0x5E, 0x27, 0x39, 0xCB, 0x8E, - 0xD4, 0x27, 0x61, 0x06, 0x8E, 0x7F, 0x3A, 0x4E, - 0xC7, 0x9F, 0x4D, 0x3E, 0x20, 0x57, 0xDC, 0xE4, - 0x65, 0xA5, 0xFF, 0x93, 0xC2, 0x7B, 0xD2, 0xB8, - 0x3F, 0xE3, 0xD0, 0x8C, 0xB3, 0x92, 0xED, 0x96 - ] - XCTAssertEqual(expected, encrypted) - } - - func testDecryptHeader() throws { - let ciphertext: [UInt8] = [ - 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, - 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, - 0x0D, 0x91, 0xF2, 0x9C, 0xC6, 0x35, 0xD7, 0x5E, - 0x1E, 0x42, 0x23, 0x1E, 0xC7, 0x90, 0x57, 0xE3, - 0x8D, 0x98, 0xF3, 0x58, 0x07, 0x2C, 0x9F, 0x03, - 0xBC, 0xEA, 0x5A, 0x98, 0x3B, 0x68, 0x62, 0x89, - 0x3E, 0xBC, 0x5E, 0x5E, 0x27, 0x39, 0xCB, 0x8E, - 0xD4, 0x27, 0x61, 0x06, 0x8E, 0x7F, 0x3A, 0x4E, - 0xC7, 0x9F, 0x4D, 0x3E, 0x20, 0x57, 0xDC, 0xE4, - 0x65, 0xA5, 0xFF, 0x93, 0xC2, 0x7B, 0xD2, 0xB8, - 0x3F, 0xE3, 0xD0, 0x8C, 0xB3, 0x92, 0xED, 0x96 - ] - let decrypted = try cryptor.decryptHeader(ciphertext) - XCTAssertEqual([UInt8](repeating: 0xF0, count: 16), decrypted.nonce) - XCTAssertEqual([UInt8](repeating: 0xF0, count: 32), decrypted.contentKey) - } - func testEncryptAndDecryptContent() throws { let originalData = Data(repeating: 0x0F, count: 65 * 1024) let originalURL = tmpDirURL.appendingPathComponent(UUID().uuidString, isDirectory: false) @@ -128,7 +86,7 @@ class CryptorTests: XCTestCase { } func testEncryptAndDecryptSingleChunk() throws { - let nonce = [UInt8](repeating: 0x00, count: 16) + let nonce = [UInt8](repeating: 0x00, count: contentCryptor.nonceLen) let filekey = [UInt8](repeating: 0x00, count: 32) let cleartext = [UInt8]("hello world".data(using: .ascii)!) @@ -139,47 +97,57 @@ class CryptorTests: XCTestCase { } func testCalculateCiphertextSize() { + let overheadPerChunk = contentCryptor.nonceLen + contentCryptor.tagLen + XCTAssertEqual(0, cryptor.calculateCiphertextSize(0)) - XCTAssertEqual(1 + 48, cryptor.calculateCiphertextSize(1)) - XCTAssertEqual(32 * 1024 - 1 + 48, cryptor.calculateCiphertextSize(32 * 1024 - 1)) - XCTAssertEqual(32 * 1024 + 48, cryptor.calculateCiphertextSize(32 * 1024)) + XCTAssertEqual(1 + overheadPerChunk, cryptor.calculateCiphertextSize(1)) + XCTAssertEqual(32 * 1024 - 1 + overheadPerChunk, cryptor.calculateCiphertextSize(32 * 1024 - 1)) + XCTAssertEqual(32 * 1024 + overheadPerChunk, cryptor.calculateCiphertextSize(32 * 1024)) - XCTAssertEqual(32 * 1024 + 1 + 48 * 2, cryptor.calculateCiphertextSize(32 * 1024 + 1)) - XCTAssertEqual(32 * 1024 + 2 + 48 * 2, cryptor.calculateCiphertextSize(32 * 1024 + 2)) - XCTAssertEqual(64 * 1024 - 1 + 48 * 2, cryptor.calculateCiphertextSize(64 * 1024 - 1)) - XCTAssertEqual(64 * 1024 + 48 * 2, cryptor.calculateCiphertextSize(64 * 1024)) + XCTAssertEqual(32 * 1024 + 1 + overheadPerChunk * 2, cryptor.calculateCiphertextSize(32 * 1024 + 1)) + XCTAssertEqual(32 * 1024 + 2 + overheadPerChunk * 2, cryptor.calculateCiphertextSize(32 * 1024 + 2)) + XCTAssertEqual(64 * 1024 - 1 + overheadPerChunk * 2, cryptor.calculateCiphertextSize(64 * 1024 - 1)) + XCTAssertEqual(64 * 1024 + overheadPerChunk * 2, cryptor.calculateCiphertextSize(64 * 1024)) - XCTAssertEqual(64 * 1024 + 1 + 48 * 3, cryptor.calculateCiphertextSize(64 * 1024 + 1)) + XCTAssertEqual(64 * 1024 + 1 + overheadPerChunk * 3, cryptor.calculateCiphertextSize(64 * 1024 + 1)) } func testCalculateCleartextSize() throws { + let overheadPerChunk = contentCryptor.nonceLen + contentCryptor.tagLen + XCTAssertEqual(0, try cryptor.calculateCleartextSize(0)) - XCTAssertEqual(1, try cryptor.calculateCleartextSize(1 + 48)) - XCTAssertEqual(32 * 1024 - 1, try cryptor.calculateCleartextSize(32 * 1024 - 1 + 48)) - XCTAssertEqual(32 * 1024, try cryptor.calculateCleartextSize(32 * 1024 + 48)) + XCTAssertEqual(1, try cryptor.calculateCleartextSize(1 + overheadPerChunk)) + XCTAssertEqual(32 * 1024 - 1, try cryptor.calculateCleartextSize(32 * 1024 - 1 + overheadPerChunk)) + XCTAssertEqual(32 * 1024, try cryptor.calculateCleartextSize(32 * 1024 + overheadPerChunk)) - XCTAssertEqual(32 * 1024 + 1, try cryptor.calculateCleartextSize(32 * 1024 + 1 + 48 * 2)) - XCTAssertEqual(32 * 1024 + 2, try cryptor.calculateCleartextSize(32 * 1024 + 2 + 48 * 2)) - XCTAssertEqual(64 * 1024 - 1, try cryptor.calculateCleartextSize(64 * 1024 - 1 + 48 * 2)) - XCTAssertEqual(64 * 1024, try cryptor.calculateCleartextSize(64 * 1024 + 48 * 2)) + XCTAssertEqual(32 * 1024 + 1, try cryptor.calculateCleartextSize(32 * 1024 + 1 + overheadPerChunk * 2)) + XCTAssertEqual(32 * 1024 + 2, try cryptor.calculateCleartextSize(32 * 1024 + 2 + overheadPerChunk * 2)) + XCTAssertEqual(64 * 1024 - 1, try cryptor.calculateCleartextSize(64 * 1024 - 1 + overheadPerChunk * 2)) + XCTAssertEqual(64 * 1024, try cryptor.calculateCleartextSize(64 * 1024 + overheadPerChunk * 2)) - XCTAssertEqual(64 * 1024 + 1, try cryptor.calculateCleartextSize(64 * 1024 + 1 + 48 * 3)) + XCTAssertEqual(64 * 1024 + 1, try cryptor.calculateCleartextSize(64 * 1024 + 1 + overheadPerChunk * 3)) } func testCalculateCleartextSizeWithInvalidCiphertextSize() throws { XCTAssertThrowsError(try cryptor.calculateCleartextSize(1), "invalid ciphertext size") { error in XCTAssertEqual(.invalidParameter("Method not defined for input value 1"), error as? CryptoError) } - XCTAssertThrowsError(try cryptor.calculateCleartextSize(48), "invalid ciphertext size") { error in - XCTAssertEqual(.invalidParameter("Method not defined for input value 48"), error as? CryptoError) + + let emptyPayload = contentCryptor.nonceLen + contentCryptor.tagLen + XCTAssertThrowsError(try cryptor.calculateCleartextSize(emptyPayload), "invalid ciphertext size") { error in + XCTAssertEqual(.invalidParameter("Method not defined for input value \(emptyPayload)"), error as? CryptoError) } - XCTAssertThrowsError(try cryptor.calculateCleartextSize(32 * 1024 + 1 + 48), "invalid ciphertext size") { error in - XCTAssertEqual(.invalidParameter("Method not defined for input value 32817"), error as? CryptoError) + + let oneChunkPlusOneByte = cryptor.ciphertextChunkSize + 1 + XCTAssertThrowsError(try cryptor.calculateCleartextSize(oneChunkPlusOneByte), "invalid ciphertext size") { error in + XCTAssertEqual(.invalidParameter("Method not defined for input value \(oneChunkPlusOneByte)"), error as? CryptoError) } - XCTAssertThrowsError(try cryptor.calculateCleartextSize(32 * 1024 + 48 * 2), "invalid ciphertext size") { error in - XCTAssertEqual(.invalidParameter("Method not defined for input value 32864"), error as? CryptoError) + + let oneChunkPlusEmptySecondChunk = cryptor.ciphertextChunkSize + contentCryptor.nonceLen + contentCryptor.tagLen + XCTAssertThrowsError(try cryptor.calculateCleartextSize(oneChunkPlusEmptySecondChunk), "invalid ciphertext size") { error in + XCTAssertEqual(.invalidParameter("Method not defined for input value \(oneChunkPlusEmptySecondChunk)"), error as? CryptoError) } } } diff --git a/Tests/CryptomatorCryptoLibTests/CtrCryptorTests.swift b/Tests/CryptomatorCryptoLibTests/CtrCryptorTests.swift new file mode 100644 index 0000000..b8c6ff4 --- /dev/null +++ b/Tests/CryptomatorCryptoLibTests/CtrCryptorTests.swift @@ -0,0 +1,70 @@ +// +// CtrCryptorTests.swift +// CryptomatorCryptoLibTests +// +// Created by Sebastian Stenzel on 27.04.20. +// Copyright © 2020 Skymatic GmbH. All rights reserved. +// + +import XCTest +@testable import CryptomatorCryptoLib + +class CtrCryptorTest: CryptorTests { + override class var defaultTestSuite: XCTestSuite { + return XCTestSuite(forTestCaseClass: CtrCryptorTest.self) + } + + override func setUpWithError() throws { + let aesKey = [UInt8](repeating: 0x55, count: 32) + let macKey = [UInt8](repeating: 0x77, count: 32) + let masterkey = Masterkey.createFromRaw(aesMasterKey: aesKey, macMasterKey: macKey) + let cryptoSupport = CryptoSupportMock() + let contentCryptor = CtrThenHmacContentCryptor(macKey: macKey, cryptoSupport: cryptoSupport) + + try super.setUpWithError(masterkey: masterkey, cryptoSupport: cryptoSupport, contentCryptor: contentCryptor) + } + + func testCreateHeader() throws { + let header = try cryptor.createHeader() + XCTAssertEqual([UInt8](repeating: 0xF0, count: 16), header.nonce) + XCTAssertEqual([UInt8](repeating: 0xF0, count: 32), header.contentKey) + } + + func testEncryptHeader() throws { + let header = try cryptor.createHeader() + let encrypted = try cryptor.encryptHeader(header) + let expected: [UInt8] = [ + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0x0D, 0x91, 0xF2, 0x9C, 0xC6, 0x35, 0xD7, 0x5E, + 0x1E, 0x42, 0x23, 0x1E, 0xC7, 0x90, 0x57, 0xE3, + 0x8D, 0x98, 0xF3, 0x58, 0x07, 0x2C, 0x9F, 0x03, + 0xBC, 0xEA, 0x5A, 0x98, 0x3B, 0x68, 0x62, 0x89, + 0x3E, 0xBC, 0x5E, 0x5E, 0x27, 0x39, 0xCB, 0x8E, + 0xD4, 0x27, 0x61, 0x06, 0x8E, 0x7F, 0x3A, 0x4E, + 0xC7, 0x9F, 0x4D, 0x3E, 0x20, 0x57, 0xDC, 0xE4, + 0x65, 0xA5, 0xFF, 0x93, 0xC2, 0x7B, 0xD2, 0xB8, + 0x3F, 0xE3, 0xD0, 0x8C, 0xB3, 0x92, 0xED, 0x96 + ] + XCTAssertEqual(expected, encrypted) + } + + func testDecryptHeader() throws { + let ciphertext: [UInt8] = [ + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0x0D, 0x91, 0xF2, 0x9C, 0xC6, 0x35, 0xD7, 0x5E, + 0x1E, 0x42, 0x23, 0x1E, 0xC7, 0x90, 0x57, 0xE3, + 0x8D, 0x98, 0xF3, 0x58, 0x07, 0x2C, 0x9F, 0x03, + 0xBC, 0xEA, 0x5A, 0x98, 0x3B, 0x68, 0x62, 0x89, + 0x3E, 0xBC, 0x5E, 0x5E, 0x27, 0x39, 0xCB, 0x8E, + 0xD4, 0x27, 0x61, 0x06, 0x8E, 0x7F, 0x3A, 0x4E, + 0xC7, 0x9F, 0x4D, 0x3E, 0x20, 0x57, 0xDC, 0xE4, + 0x65, 0xA5, 0xFF, 0x93, 0xC2, 0x7B, 0xD2, 0xB8, + 0x3F, 0xE3, 0xD0, 0x8C, 0xB3, 0x92, 0xED, 0x96 + ] + let decrypted = try cryptor.decryptHeader(ciphertext) + XCTAssertEqual([UInt8](repeating: 0xF0, count: 16), decrypted.nonce) + XCTAssertEqual([UInt8](repeating: 0xF0, count: 32), decrypted.contentKey) + } +} diff --git a/Tests/CryptomatorCryptoLibTests/GcmCryptorTests.swift b/Tests/CryptomatorCryptoLibTests/GcmCryptorTests.swift new file mode 100644 index 0000000..683569b --- /dev/null +++ b/Tests/CryptomatorCryptoLibTests/GcmCryptorTests.swift @@ -0,0 +1,103 @@ +// +// GcmCryptorTests.swift +// CryptomatorCryptoLibTests +// +// Created by Sebastian Stenzel on 20.05.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import XCTest +@testable import CryptomatorCryptoLib + +class GcmCryptorTests: CryptorTests { + override class var defaultTestSuite: XCTestSuite { + return XCTestSuite(forTestCaseClass: GcmCryptorTests.self) + } + + override func setUpWithError() throws { + let aesKey = [UInt8](repeating: 0x55, count: 32) + let macKey = [UInt8](repeating: 0x77, count: 32) + let masterkey = Masterkey.createFromRaw(aesMasterKey: aesKey, macMasterKey: macKey) + let cryptoSupport = CryptoSupportMock() + let contentCryptor = GcmContentCryptor() + + try super.setUpWithError(masterkey: masterkey, cryptoSupport: cryptoSupport, contentCryptor: contentCryptor) + } + + func testCreateHeader() throws { + let header = try cryptor.createHeader() + XCTAssertEqual([UInt8](repeating: 0xF0, count: 12), header.nonce) + XCTAssertEqual([UInt8](repeating: 0xF0, count: 32), header.contentKey) + } + + func testEncryptHeader() throws { + let header = try cryptor.createHeader() + let encrypted = try cryptor.encryptHeader(header) + + // echo -n "///////////w8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8A==" | base64 --decode \ + // | openssl enc -aes-256-gcm -K 5555555555555555555555555555555555555555555555555555555555555555 -iv F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0 -a + let expected: [UInt8] = [ + // nonce + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, + // ciphertext + 0x1C, 0x87, 0x19, 0xF0, 0x31, 0x22, 0x86, 0x8F, + 0xDB, 0x9D, 0x97, 0x03, 0xA0, 0x86, 0x08, 0xD5, + 0x88, 0x58, 0x96, 0xC2, 0xE6, 0x60, 0x4B, 0xB9, + 0xEA, 0x64, 0x31, 0xD4, 0xA0, 0x5D, 0x47, 0x6F, + 0xE4, 0x1F, 0x32, 0x31, 0xF2, 0xC0, 0x61, 0x1F, + // tag + 0x6D, 0x42, 0x98, 0x82, 0x43, 0xF2, 0x1F, 0x43, + 0xF6, 0x44, 0xFD, 0x6D, 0xF7, 0xA9, 0x3F, 0x0B + ] + XCTAssertEqual(expected, encrypted) + } + + func testDecryptHeader() throws { + let ciphertext: [UInt8] = [ + // nonce + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, + // ciphertext + 0x1C, 0x87, 0x19, 0xF0, 0x31, 0x22, 0x86, 0x8F, + 0xDB, 0x9D, 0x97, 0x03, 0xA0, 0x86, 0x08, 0xD5, + 0x88, 0x58, 0x96, 0xC2, 0xE6, 0x60, 0x4B, 0xB9, + 0xEA, 0x64, 0x31, 0xD4, 0xA0, 0x5D, 0x47, 0x6F, + 0xE4, 0x1F, 0x32, 0x31, 0xF2, 0xC0, 0x61, 0x1F, + // tag + 0x6D, 0x42, 0x98, 0x82, 0x43, 0xF2, 0x1F, 0x43, + 0xF6, 0x44, 0xFD, 0x6D, 0xF7, 0xA9, 0x3F, 0x0B + ] + let decrypted = try cryptor.decryptHeader(ciphertext) + XCTAssertEqual([UInt8](repeating: 0xF0, count: 12), decrypted.nonce) + XCTAssertEqual([UInt8](repeating: 0xF0, count: 32), decrypted.contentKey) + } + + func testDecryptSingleChunk() throws { + let headerNonce: [UInt8] = [ + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55 + ] + let fileKey: [UInt8] = [ + 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, + 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, + 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, + 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77 + ] + let ciphertext: [UInt8] = [ + // nonce + 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x55, 0x55, + // payload + 0x52, 0xC5, 0xEE, 0x8D, 0x7F, 0xB4, 0x4E, 0xF2, + 0x8A, 0xEC, 0x55, + // tag + 0x3C, 0xC7, 0x02, 0x65, 0xE5, 0x35, 0x2C, 0xB5, + 0xA0, 0x9A, 0x43, 0xAE, 0x0F, 0x5C, 0xA1, 0x5D + ] + + let cleartext = try cryptor.decryptSingleChunk(ciphertext, chunkNumber: 0, headerNonce: headerNonce, fileKey: fileKey) + + XCTAssertEqual([UInt8]("hello world".utf8), cleartext) + } +}