Skip to content

Commit e7f83ae

Browse files
allow different AD order for GCM and CTR+HMAC
1 parent 0976141 commit e7f83ae

File tree

3 files changed

+79
-16
lines changed

3 files changed

+79
-16
lines changed

Sources/CryptomatorCryptoLib/ContentCryptor.swift

+47-12
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ protocol ContentCryptor {
2323
- Parameter ad: Associated data, which needs to be authenticated during decryption.
2424
- Returns: Nonce/IV + ciphertext + MAC/tag, as a concatenated byte array.
2525
*/
26-
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]
2727

2828
/**
2929
Decrypts one single chunk of encrypted data.
@@ -33,29 +33,60 @@ protocol ContentCryptor {
3333
- Parameter ad: Associated data, which needs to be authenticated during decryption.
3434
- Returns: The original cleartext.
3535
*/
36-
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+
}
3766
}
3867

3968
class GcmContentCryptor: ContentCryptor {
4069
let nonceLen = 12 // 96 bit
4170
let tagLen = 16 // 128 bit
4271

43-
func encrypt(_ chunk: [UInt8], key keyBytes: [UInt8], nonce nonceBytes: [UInt8], ad: [UInt8]...) throws -> [UInt8] {
44-
let concatAd = ad.reduce([], +)
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] {
4577
let key = SymmetricKey(data: keyBytes)
4678
let nonce = try AES.GCM.Nonce(data: nonceBytes)
47-
let encrypted = try AES.GCM.seal(chunk, using: key, nonce: nonce, authenticating: concatAd)
79+
let encrypted = try AES.GCM.seal(chunk, using: key, nonce: nonce, authenticating: ad)
4880

4981
return [UInt8](encrypted.nonce + encrypted.ciphertext + encrypted.tag)
5082
}
5183

52-
func decrypt(_ chunk: [UInt8], key keyBytes: [UInt8], ad: [UInt8]...) throws -> [UInt8] {
84+
func decrypt(_ chunk: [UInt8], key keyBytes: [UInt8], ad: [UInt8]) throws -> [UInt8] {
5385
assert(chunk.count >= nonceLen + tagLen, "ciphertext chunk must at least contain nonce + tag")
5486

55-
let concatAd = ad.reduce([], +)
5687
let key = SymmetricKey(data: keyBytes)
5788
let encrypted = try AES.GCM.SealedBox(combined: chunk)
58-
let decrypted = try AES.GCM.open(encrypted, using: key, authenticating: concatAd)
89+
let decrypted = try AES.GCM.open(encrypted, using: key, authenticating: ad)
5990

6091
return [UInt8](decrypted)
6192
}
@@ -73,13 +104,17 @@ class CtrThenHmacContentCryptor: ContentCryptor {
73104
self.cryptoSupport = cryptoSupport
74105
}
75106

76-
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] {
77112
let ciphertext = try AesCtr.compute(key: key, iv: nonce, data: chunk)
78113
let mac = computeHmac(ciphertext, nonce: nonce, ad: ad)
79114
return nonce + ciphertext + mac
80115
}
81116

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

85120
// decompose chunk:
@@ -98,8 +133,8 @@ class CtrThenHmacContentCryptor: ContentCryptor {
98133
return try AesCtr.compute(key: key, iv: chunkNonce, data: ciphertext)
99134
}
100135

101-
private func computeHmac(_ ciphertext: [UInt8], nonce: [UInt8], ad: [[UInt8]]) -> [UInt8] {
102-
let data = ad.reduce([UInt8](), +) + nonce + ciphertext
136+
private func computeHmac(_ ciphertext: [UInt8], nonce: [UInt8], ad: [UInt8]) -> [UInt8] {
137+
let data = ad + nonce + ciphertext
103138
var mac = [UInt8](repeating: 0x00, count: tagLen)
104139
CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), macKey, macKey.count, data, data.count, &mac)
105140
return mac

Sources/CryptomatorCryptoLib/Cryptor.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,12 @@ public class Cryptor {
172172

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

178178
func decryptHeader(_ header: [UInt8]) throws -> FileHeader {
179179
let nonce = [UInt8](header[0 ..< contentCryptor.nonceLen])
180-
let cleartext = try contentCryptor.decrypt(header, key: masterkey.aesMasterKey)
180+
let cleartext = try contentCryptor.decryptHeader(header, key: masterkey.aesMasterKey)
181181
let contentKey = [UInt8](cleartext[fileHeaderLegacyPayloadSize...])
182182
return FileHeader(nonce: nonce, contentKey: contentKey)
183183
}
@@ -313,11 +313,11 @@ public class Cryptor {
313313

314314
func encryptSingleChunk(_ chunk: [UInt8], chunkNumber: UInt64, headerNonce: [UInt8], fileKey: [UInt8]) throws -> [UInt8] {
315315
let chunkNonce = try cryptoSupport.createRandomBytes(size: contentCryptor.nonceLen)
316-
return try contentCryptor.encrypt(chunk, key: fileKey, nonce: chunkNonce, ad: headerNonce, chunkNumber.bigEndian.byteArray())
316+
return try contentCryptor.encryptChunk(chunk, chunkNumber: chunkNumber, chunkNonce: chunkNonce, fileKey: fileKey, headerNonce: headerNonce)
317317
}
318318

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

323323
// MARK: - File Size Calculation

Tests/CryptomatorCryptoLibTests/GcmCryptorTests.swift

+28
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,32 @@ class GcmCryptorTests: CryptorTests {
7272
XCTAssertEqual([UInt8](repeating: 0xF0, count: 12), decrypted.nonce)
7373
XCTAssertEqual([UInt8](repeating: 0xF0, count: 32), decrypted.contentKey)
7474
}
75+
76+
func testDecryptSingleChunk() throws {
77+
let headerNonce: [UInt8] = [
78+
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
79+
0x55, 0x55, 0x55, 0x55
80+
]
81+
let fileKey: [UInt8] = [
82+
0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77,
83+
0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77,
84+
0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77,
85+
0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77
86+
]
87+
let ciphertext: [UInt8] = [
88+
// nonce
89+
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
90+
0x55, 0x55, 0x55, 0x55,
91+
// payload
92+
0x52, 0xC5, 0xEE, 0x8D, 0x7F, 0xB4, 0x4E, 0xF2,
93+
0x8A, 0xEC, 0x55,
94+
// tag
95+
0x3C, 0xC7, 0x02, 0x65, 0xE5, 0x35, 0x2C, 0xB5,
96+
0xA0, 0x9A, 0x43, 0xAE, 0x0F, 0x5C, 0xA1, 0x5D
97+
]
98+
99+
let cleartext = try cryptor.decryptSingleChunk(ciphertext, chunkNumber: 0, headerNonce: headerNonce, fileKey: fileKey)
100+
101+
XCTAssertEqual([UInt8]("hello world".utf8), cleartext)
102+
}
75103
}

0 commit comments

Comments
 (0)