Skip to content

Commit 12069a0

Browse files
authored
Merge pull request #9 from cryptomator/feature/gcm
GCM support
2 parents ed3a6d0 + e7f83ae commit 12069a0

File tree

8 files changed

+327
-102
lines changed

8 files changed

+327
-102
lines changed

CryptomatorCryptoLib.xcodeproj/project.pbxproj

+12-4
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
9E9BB81624558DFF00F9FF51 /* MasterkeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E9BB81524558DFF00F9FF51 /* MasterkeyTests.swift */; };
3535
9EB822C1248AF82200879838 /* AesCtr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB822C0248AF82200879838 /* AesCtr.swift */; };
3636
9EB822C3248AF9C500879838 /* AesCtrTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB822C2248AF9C500879838 /* AesCtrTests.swift */; };
37+
9EBEC947283782E6002210DE /* CtrCryptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBEC946283782E6002210DE /* CtrCryptorTests.swift */; };
38+
9EBEC94928378308002210DE /* GcmCryptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBEC94828378308002210DE /* GcmCryptorTests.swift */; };
3739
/* End PBXBuildFile section */
3840

3941
/* Begin PBXContainerItemProxy section */
@@ -97,6 +99,8 @@
9799
9E9BB81524558DFF00F9FF51 /* MasterkeyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterkeyTests.swift; sourceTree = "<group>"; };
98100
9EB822C0248AF82200879838 /* AesCtr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AesCtr.swift; sourceTree = "<group>"; };
99101
9EB822C2248AF9C500879838 /* AesCtrTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AesCtrTests.swift; sourceTree = "<group>"; };
102+
9EBEC946283782E6002210DE /* CtrCryptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CtrCryptorTests.swift; sourceTree = "<group>"; };
103+
9EBEC94828378308002210DE /* GcmCryptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GcmCryptorTests.swift; sourceTree = "<group>"; };
100104
/* End PBXFileReference section */
101105

102106
/* Begin PBXFrameworksBuildPhase section */
@@ -172,6 +176,8 @@
172176
9E44EEA724599C7800A37B01 /* AesSivTests.swift */,
173177
9E35C4EA24576A3D0006E50C /* CryptorTests.swift */,
174178
74A5B57D25A86A69002D10F7 /* CryptoSupportMock.swift */,
179+
9EBEC946283782E6002210DE /* CtrCryptorTests.swift */,
180+
9EBEC94828378308002210DE /* GcmCryptorTests.swift */,
175181
74A5B57525A869DD002D10F7 /* MasterkeyFileTests.swift */,
176182
9E9BB81524558DFF00F9FF51 /* MasterkeyTests.swift */,
177183
);
@@ -443,10 +449,12 @@
443449
files = (
444450
74A5B57E25A86A69002D10F7 /* CryptoSupportMock.swift in Sources */,
445451
9E44EEA92459AB1500A37B01 /* AesSivTests.swift in Sources */,
452+
9EBEC947283782E6002210DE /* CtrCryptorTests.swift in Sources */,
446453
9EB822C3248AF9C500879838 /* AesCtrTests.swift in Sources */,
447454
74A5B57625A869DD002D10F7 /* MasterkeyFileTests.swift in Sources */,
448455
9E9BB81624558DFF00F9FF51 /* MasterkeyTests.swift in Sources */,
449456
9E35C4EB24576A3D0006E50C /* CryptorTests.swift in Sources */,
457+
9EBEC94928378308002210DE /* GcmCryptorTests.swift in Sources */,
450458
);
451459
runOnlyForDeploymentPostprocessing = 0;
452460
};
@@ -528,8 +536,8 @@
528536
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
529537
GCC_WARN_UNUSED_FUNCTION = YES;
530538
GCC_WARN_UNUSED_VARIABLE = YES;
531-
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
532-
MACOSX_DEPLOYMENT_TARGET = 10.12;
539+
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
540+
MACOSX_DEPLOYMENT_TARGET = 10.15;
533541
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
534542
MTL_FAST_MATH = YES;
535543
ONLY_ACTIVE_ARCH = YES;
@@ -588,8 +596,8 @@
588596
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
589597
GCC_WARN_UNUSED_FUNCTION = YES;
590598
GCC_WARN_UNUSED_VARIABLE = YES;
591-
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
592-
MACOSX_DEPLOYMENT_TARGET = 10.12;
599+
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
600+
MACOSX_DEPLOYMENT_TARGET = 10.15;
593601
MTL_ENABLE_DEBUG_INFO = NO;
594602
MTL_FAST_MATH = YES;
595603
OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-expression-type-checking=200 -Xfrontend -warn-long-function-bodies=200";

Package.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import PackageDescription
1313
let package = Package(
1414
name: "CryptomatorCryptoLib",
1515
platforms: [
16-
.iOS(.v9),
17-
.macOS(.v10_12)
16+
.iOS(.v13),
17+
.macOS(.v10_15)
1818
],
1919
products: [
2020
.library(name: "CryptomatorCryptoLib", targets: ["CryptomatorCryptoLib"])

README.md

+10-6
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ For more information on the Cryptomator encryption scheme, visit the security ar
1111

1212
## Requirements
1313

14-
- iOS 9.0 or higher
15-
- macOS 10.12 or higher
14+
- iOS 13.0 or higher
15+
- macOS 10.15 or higher
1616

1717
## Installation
1818

@@ -121,13 +121,16 @@ try MasterkeyFile.changePassphrase(masterkeyFileData: masterkeyFileData, oldPass
121121

122122
#### Constructor
123123

124-
Create a cryptor by providing a masterkey.
124+
Create a cryptor by providing a masterkey and a scheme (e.g., `.sivGcm`).
125125

126126
```swift
127127
let masterkey = ...
128-
let cryptor = Cryptor(masterkey: masterkey)
128+
let scheme = ...
129+
let cryptor = Cryptor(masterkey: masterkey, scheme: scheme)
129130
```
130131

132+
Make sure that the data you're working with is compatible with the provided scheme.
133+
131134
#### Path Encryption and Decryption
132135

133136
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
178181

179182
In general, the following preference is used to choose the implementation of cryptographic primitives:
180183

181-
1. Apple Swift Crypto (HMAC)
182-
2. Apple CommonCrypto (AES-CTR, RFC 3394 Key Derivation)
184+
1. Apple CryptoKit (AES-GCM)
185+
2. Apple Swift Crypto (HMAC)
186+
3. Apple CommonCrypto (AES-CTR, RFC 3394 Key Derivation)
183187

184188
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.
185189

Sources/CryptomatorCryptoLib/ContentCryptor.swift

+67-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//
88

99
import CommonCrypto
10+
import CryptoKit
1011
import Foundation
1112

1213
protocol ContentCryptor {
@@ -22,7 +23,7 @@ protocol ContentCryptor {
2223
- Parameter ad: Associated data, which needs to be authenticated during decryption.
2324
- Returns: Nonce/IV + ciphertext + MAC/tag, as a concatenated byte array.
2425
*/
25-
func encrypt(_ chunk: [UInt8], key: [UInt8], nonce: [UInt8], ad: [UInt8]...) throws -> [UInt8]
26+
func encrypt(_ chunk: [UInt8], key: [UInt8], nonce: [UInt8], ad: [UInt8]) throws -> [UInt8]
2627

2728
/**
2829
Decrypts one single chunk of encrypted data.
@@ -32,7 +33,63 @@ protocol ContentCryptor {
3233
- Parameter ad: Associated data, which needs to be authenticated during decryption.
3334
- Returns: The original cleartext.
3435
*/
35-
func decrypt(_ chunk: [UInt8], key: [UInt8], ad: [UInt8]...) throws -> [UInt8]
36+
func decrypt(_ chunk: [UInt8], key: [UInt8], ad: [UInt8]) throws -> [UInt8]
37+
38+
/**
39+
Constructs the associated data which will be authenticated during encryption/decryption of a single chunk
40+
41+
- Parameter chunkNumber: The index of the chunk (starting at 0), preventing swapping of chunks
42+
- Parameter headerNonce: The nonce used in the file header, binding the chunk to this particular file.
43+
- Returns: The combined associated data.
44+
*/
45+
func ad(chunkNumber: UInt64, headerNonce: [UInt8]) -> [UInt8]
46+
}
47+
48+
extension ContentCryptor {
49+
func encryptHeader(_ header: [UInt8], key: [UInt8], nonce: [UInt8]) throws -> [UInt8] {
50+
return try encrypt(header, key: key, nonce: nonce, ad: [])
51+
}
52+
53+
func decryptHeader(_ header: [UInt8], key: [UInt8]) throws -> [UInt8] {
54+
return try decrypt(header, key: key, ad: [])
55+
}
56+
57+
func encryptChunk(_ chunk: [UInt8], chunkNumber: UInt64, chunkNonce: [UInt8], fileKey: [UInt8], headerNonce: [UInt8]) throws -> [UInt8] {
58+
let ad = ad(chunkNumber: chunkNumber, headerNonce: headerNonce)
59+
return try encrypt(chunk, key: fileKey, nonce: chunkNonce, ad: ad)
60+
}
61+
62+
func decryptChunk(_ chunk: [UInt8], chunkNumber: UInt64, fileKey: [UInt8], headerNonce: [UInt8]) throws -> [UInt8] {
63+
let ad = ad(chunkNumber: chunkNumber, headerNonce: headerNonce)
64+
return try decrypt(chunk, key: fileKey, ad: ad)
65+
}
66+
}
67+
68+
class GcmContentCryptor: ContentCryptor {
69+
let nonceLen = 12 // 96 bit
70+
let tagLen = 16 // 128 bit
71+
72+
func ad(chunkNumber: UInt64, headerNonce: [UInt8]) -> [UInt8] {
73+
return chunkNumber.bigEndian.byteArray() + headerNonce
74+
}
75+
76+
func encrypt(_ chunk: [UInt8], key keyBytes: [UInt8], nonce nonceBytes: [UInt8], ad: [UInt8]) throws -> [UInt8] {
77+
let key = SymmetricKey(data: keyBytes)
78+
let nonce = try AES.GCM.Nonce(data: nonceBytes)
79+
let encrypted = try AES.GCM.seal(chunk, using: key, nonce: nonce, authenticating: ad)
80+
81+
return [UInt8](encrypted.nonce + encrypted.ciphertext + encrypted.tag)
82+
}
83+
84+
func decrypt(_ chunk: [UInt8], key keyBytes: [UInt8], ad: [UInt8]) throws -> [UInt8] {
85+
assert(chunk.count >= nonceLen + tagLen, "ciphertext chunk must at least contain nonce + tag")
86+
87+
let key = SymmetricKey(data: keyBytes)
88+
let encrypted = try AES.GCM.SealedBox(combined: chunk)
89+
let decrypted = try AES.GCM.open(encrypted, using: key, authenticating: ad)
90+
91+
return [UInt8](decrypted)
92+
}
3693
}
3794

3895
class CtrThenHmacContentCryptor: ContentCryptor {
@@ -47,13 +104,17 @@ class CtrThenHmacContentCryptor: ContentCryptor {
47104
self.cryptoSupport = cryptoSupport
48105
}
49106

50-
func encrypt(_ chunk: [UInt8], key: [UInt8], nonce: [UInt8], ad: [UInt8]...) throws -> [UInt8] {
107+
func ad(chunkNumber: UInt64, headerNonce: [UInt8]) -> [UInt8] {
108+
return headerNonce + chunkNumber.bigEndian.byteArray()
109+
}
110+
111+
func encrypt(_ chunk: [UInt8], key: [UInt8], nonce: [UInt8], ad: [UInt8]) throws -> [UInt8] {
51112
let ciphertext = try AesCtr.compute(key: key, iv: nonce, data: chunk)
52113
let mac = computeHmac(ciphertext, nonce: nonce, ad: ad)
53114
return nonce + ciphertext + mac
54115
}
55116

56-
func decrypt(_ chunk: [UInt8], key: [UInt8], ad: [UInt8]...) throws -> [UInt8] {
117+
func decrypt(_ chunk: [UInt8], key: [UInt8], ad: [UInt8]) throws -> [UInt8] {
57118
assert(chunk.count >= nonceLen + tagLen, "ciphertext chunk must at least contain nonce + tag")
58119

59120
// decompose chunk:
@@ -72,8 +133,8 @@ class CtrThenHmacContentCryptor: ContentCryptor {
72133
return try AesCtr.compute(key: key, iv: chunkNonce, data: ciphertext)
73134
}
74135

75-
private func computeHmac(_ ciphertext: [UInt8], nonce: [UInt8], ad: [[UInt8]]) -> [UInt8] {
76-
let data = ad.reduce([UInt8](), +) + nonce + ciphertext
136+
private func computeHmac(_ ciphertext: [UInt8], nonce: [UInt8], ad: [UInt8]) -> [UInt8] {
137+
let data = ad + nonce + ciphertext
77138
var mac = [UInt8](repeating: 0x00, count: tagLen)
78139
CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), macKey, macKey.count, data, data.count, &mac)
79140
return mac

Sources/CryptomatorCryptoLib/Cryptor.swift

+21-10
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ public extension InputStream {
4949
}
5050
}
5151

52+
public enum CryptorScheme: String, Codable {
53+
case sivCtrMac = "SIV_CTRMAC"
54+
case sivGcm = "SIV_GCM"
55+
}
56+
5257
public enum FileNameEncoding: String {
5358
case base64url
5459
case base32
@@ -66,8 +71,8 @@ public class Cryptor {
6671
return contentCryptor.nonceLen + fileHeaderPayloadSize + contentCryptor.tagLen
6772
}
6873

69-
private let cleartextChunkSize = 32 * 1024
70-
private var ciphertextChunkSize: Int {
74+
let cleartextChunkSize = 32 * 1024
75+
var ciphertextChunkSize: Int {
7176
return contentCryptor.nonceLen + cleartextChunkSize + contentCryptor.tagLen
7277
}
7378

@@ -81,9 +86,15 @@ public class Cryptor {
8186
self.contentCryptor = contentCryptor
8287
}
8388

84-
public convenience init(masterkey: Masterkey) {
89+
public convenience init(masterkey: Masterkey, scheme: CryptorScheme) {
8590
let cryptoSupport = CryptoSupport()
86-
let contentCryptor = CtrThenHmacContentCryptor(macKey: masterkey.macMasterKey, cryptoSupport: cryptoSupport)
91+
let contentCryptor: ContentCryptor
92+
switch scheme {
93+
case .sivCtrMac:
94+
contentCryptor = CtrThenHmacContentCryptor(macKey: masterkey.macMasterKey, cryptoSupport: cryptoSupport)
95+
case .sivGcm:
96+
contentCryptor = GcmContentCryptor()
97+
}
8798
self.init(masterkey: masterkey, cryptoSupport: cryptoSupport, contentCryptor: contentCryptor)
8899
}
89100

@@ -154,19 +165,19 @@ public class Cryptor {
154165
// MARK: - File Header Encryption and Decryption
155166

156167
func createHeader() throws -> FileHeader {
157-
let nonce = try cryptoSupport.createRandomBytes(size: kCCBlockSizeAES128)
168+
let nonce = try cryptoSupport.createRandomBytes(size: contentCryptor.nonceLen)
158169
let contentKey = try cryptoSupport.createRandomBytes(size: kCCKeySizeAES256)
159170
return FileHeader(nonce: nonce, contentKey: contentKey)
160171
}
161172

162173
func encryptHeader(_ header: FileHeader) throws -> [UInt8] {
163174
let cleartext = [UInt8](repeating: 0xFF, count: fileHeaderLegacyPayloadSize) + header.contentKey
164-
return try contentCryptor.encrypt(cleartext, key: masterkey.aesMasterKey, nonce: header.nonce)
175+
return try contentCryptor.encryptHeader(cleartext, key: masterkey.aesMasterKey, nonce: header.nonce)
165176
}
166177

167178
func decryptHeader(_ header: [UInt8]) throws -> FileHeader {
168179
let nonce = [UInt8](header[0 ..< contentCryptor.nonceLen])
169-
let cleartext = try contentCryptor.decrypt(header, key: masterkey.aesMasterKey)
180+
let cleartext = try contentCryptor.decryptHeader(header, key: masterkey.aesMasterKey)
170181
let contentKey = [UInt8](cleartext[fileHeaderLegacyPayloadSize...])
171182
return FileHeader(nonce: nonce, contentKey: contentKey)
172183
}
@@ -301,12 +312,12 @@ public class Cryptor {
301312
}
302313

303314
func encryptSingleChunk(_ chunk: [UInt8], chunkNumber: UInt64, headerNonce: [UInt8], fileKey: [UInt8]) throws -> [UInt8] {
304-
let chunkNonce = try cryptoSupport.createRandomBytes(size: kCCBlockSizeAES128)
305-
return try contentCryptor.encrypt(chunk, key: fileKey, nonce: chunkNonce, ad: headerNonce, chunkNumber.bigEndian.byteArray())
315+
let chunkNonce = try cryptoSupport.createRandomBytes(size: contentCryptor.nonceLen)
316+
return try contentCryptor.encryptChunk(chunk, chunkNumber: chunkNumber, chunkNonce: chunkNonce, fileKey: fileKey, headerNonce: headerNonce)
306317
}
307318

308319
func decryptSingleChunk(_ chunk: [UInt8], chunkNumber: UInt64, headerNonce: [UInt8], fileKey: [UInt8]) throws -> [UInt8] {
309-
return try contentCryptor.decrypt(chunk, key: fileKey, ad: headerNonce, chunkNumber.bigEndian.byteArray())
320+
return try contentCryptor.decryptChunk(chunk, chunkNumber: chunkNumber, fileKey: fileKey, headerNonce: headerNonce)
310321
}
311322

312323
// MARK: - File Size Calculation

0 commit comments

Comments
 (0)