Marco Ricci commited on 2024-10-10 12:18:23
Zeige 3 geänderte Dateien mit 275 Einfügungen und 39 Löschungen.
Before this commit, certain consistency checks within the storeroom exporter that seemed difficult to test remained untested: a payload size check in the master keys decryption routine, another payload size check in the session keys decryption routine, and object connectivity and type correctness checks in the top-level exporter routine. The master and session keys decryption routines, it turns out, don't need this explicit size check: the `struct` library, used for decoding the payload even further, already checks this automatically. (What *is* needed is a wrapper to convert the exception type, in general, for the whole decryption block.) For the connectivity and type correctness checks in the top-level exporter routine, I generated another couple of broken storeroom configurations (e.g. where directory contents, encoded as a JSON array, contain non-string elements). We now test for each of these configurations if they correctly fail to parse. Finally, it turns out that many of the docstrings reported the ciphertext sizes incorrectly, because they wrongly neglected the padding in their calculations. Fix this, of course.
... | ... |
@@ -182,14 +182,14 @@ def derive_master_keys_keys(password: str | bytes, iterations: int) -> KeyPair: |
182 | 182 |
|
183 | 183 |
|
184 | 184 |
def decrypt_master_keys_data(data: bytes, keys: KeyPair) -> MasterKeys: |
185 |
- """Decrypt the master keys data. |
|
185 |
+ r"""Decrypt the master keys data. |
|
186 | 186 |
|
187 | 187 |
The master keys data contains: |
188 | 188 |
|
189 | 189 |
- a 16-byte IV, |
190 |
- - a 96-byte AES256-CBC-encrypted payload (using PKCS7 padding on the |
|
191 |
- inside), and |
|
192 |
- - a 32-byte MAC of the preceding 112 bytes. |
|
190 |
+ - a 96-byte AES256-CBC-encrypted payload, plus 16 further bytes of |
|
191 |
+ PKCS7 padding, and |
|
192 |
+ - a 32-byte MAC of the preceding 128 bytes. |
|
193 | 193 |
|
194 | 194 |
The decrypted payload itself consists of three 32-byte keys: the |
195 | 195 |
hashing, encryption and signing keys, in that order. |
... | ... |
@@ -199,8 +199,8 @@ def decrypt_master_keys_data(data: bytes, keys: KeyPair) -> MasterKeys: |
199 | 199 |
cryptographic procedure, the MAC can be verified before attempting |
200 | 200 |
to decrypt the payload. |
201 | 201 |
|
202 |
- Because the payload size is both fixed and a multiple of the |
|
203 |
- cipher blocksize, in this case, the PKCS7 padding is a no-op. |
|
202 |
+ Because the payload size is both fixed and a multiple of the cipher |
|
203 |
+ blocksize, in this case, the PKCS7 padding always is `b'\x10' * 16`. |
|
204 | 204 |
|
205 | 205 |
Args: |
206 | 206 |
data: |
... | ... |
@@ -247,6 +247,7 @@ def decrypt_master_keys_data(data: bytes, keys: KeyPair) -> MasterKeys: |
247 | 247 |
) |
248 | 248 |
actual_mac.verify(claimed_mac) |
249 | 249 |
|
250 |
+ try: |
|
250 | 251 |
iv, payload = struct.unpack( |
251 | 252 |
f'{IV_SIZE}s {len(ciphertext) - IV_SIZE}s', ciphertext |
252 | 253 |
) |
... | ... |
@@ -260,15 +261,12 @@ def decrypt_master_keys_data(data: bytes, keys: KeyPair) -> MasterKeys: |
260 | 261 |
plaintext = bytearray() |
261 | 262 |
plaintext.extend(unpadder.update(padded_plaintext)) |
262 | 263 |
plaintext.extend(unpadder.finalize()) |
263 |
- if len(plaintext) != 3 * KEY_SIZE: |
|
264 |
- msg = ( |
|
265 |
- f'Expecting 3 encrypted keys at {3 * KEY_SIZE} bytes total, ' |
|
266 |
- f'but found {len(plaintext)} instead' |
|
267 |
- ) |
|
268 |
- raise ValueError(msg) |
|
269 | 264 |
hashing_key, encryption_key, signing_key = struct.unpack( |
270 | 265 |
f'{KEY_SIZE}s {KEY_SIZE}s {KEY_SIZE}s', plaintext |
271 | 266 |
) |
267 |
+ except (ValueError, struct.error) as exc: |
|
268 |
+ msg = 'Invalid encrypted master keys payload' |
|
269 |
+ raise ValueError(msg) from exc |
|
272 | 270 |
return { |
273 | 271 |
'hashing_key': hashing_key, |
274 | 272 |
'encryption_key': encryption_key, |
... | ... |
@@ -277,24 +275,24 @@ def decrypt_master_keys_data(data: bytes, keys: KeyPair) -> MasterKeys: |
277 | 275 |
|
278 | 276 |
|
279 | 277 |
def decrypt_session_keys(data: bytes, master_keys: MasterKeys) -> KeyPair: |
280 |
- """Decrypt the bucket item's session keys. |
|
278 |
+ r"""Decrypt the bucket item's session keys. |
|
281 | 279 |
|
282 | 280 |
The bucket item's session keys are single-use keys for encrypting |
283 | 281 |
and signing a single item in the storage bucket. The encrypted |
284 | 282 |
session key data consists of: |
285 | 283 |
|
286 | 284 |
- a 16-byte IV, |
287 |
- - a 64-byte AES256-CBC-encrypted payload (using PKCS7 padding on the |
|
288 |
- inside), and |
|
289 |
- - a 32-byte MAC of the preceding 80 bytes. |
|
285 |
+ - a 64-byte AES256-CBC-encrypted payload, plus 16 further bytes of |
|
286 |
+ PKCS7 padding, and |
|
287 |
+ - a 32-byte MAC of the preceding 96 bytes. |
|
290 | 288 |
|
291 | 289 |
The encrypted payload is encrypted with the master encryption key, |
292 | 290 |
and the MAC is created with the master signing key. As per standard |
293 | 291 |
cryptographic procedure, the MAC can be verified before attempting |
294 | 292 |
to decrypt the payload. |
295 | 293 |
|
296 |
- Because the payload size is both fixed and a multiple of the |
|
297 |
- cipher blocksize, in this case, the PKCS7 padding is a no-op. |
|
294 |
+ Because the payload size is both fixed and a multiple of the cipher |
|
295 |
+ blocksize, in this case, the PKCS7 padding always is `b'\x10' * 16`. |
|
298 | 296 |
|
299 | 297 |
Args: |
300 | 298 |
data: |
... | ... |
@@ -341,6 +339,7 @@ def decrypt_session_keys(data: bytes, master_keys: MasterKeys) -> KeyPair: |
341 | 339 |
) |
342 | 340 |
actual_mac.verify(claimed_mac) |
343 | 341 |
|
342 |
+ try: |
|
344 | 343 |
iv, payload = struct.unpack( |
345 | 344 |
f'{IV_SIZE}s {len(ciphertext) - IV_SIZE}s', ciphertext |
346 | 345 |
) |
... | ... |
@@ -354,11 +353,13 @@ def decrypt_session_keys(data: bytes, master_keys: MasterKeys) -> KeyPair: |
354 | 353 |
plaintext = bytearray() |
355 | 354 |
plaintext.extend(unpadder.update(padded_plaintext)) |
356 | 355 |
plaintext.extend(unpadder.finalize()) |
357 |
- |
|
358 |
- session_encryption_key, session_signing_key, inner_payload = struct.unpack( |
|
359 |
- f'{KEY_SIZE}s {KEY_SIZE}s {len(plaintext) - 2 * KEY_SIZE}s', |
|
360 |
- plaintext, |
|
356 |
+ session_encryption_key, session_signing_key = struct.unpack( |
|
357 |
+ f'{KEY_SIZE}s {KEY_SIZE}s', plaintext |
|
361 | 358 |
) |
359 |
+ except (ValueError, struct.error) as exc: |
|
360 |
+ msg = 'Invalid encrypted session keys payload' |
|
361 |
+ raise ValueError(msg) from exc |
|
362 |
+ |
|
362 | 363 |
session_keys: KeyPair = { |
363 | 364 |
'encryption_key': session_encryption_key, |
364 | 365 |
'signing_key': session_signing_key, |
... | ... |
@@ -381,12 +382,6 @@ def decrypt_session_keys(data: bytes, master_keys: MasterKeys) -> KeyPair: |
381 | 382 |
repr(session_keys['signing_key'].hex(' ')), |
382 | 383 |
) |
383 | 384 |
|
384 |
- if inner_payload: |
|
385 |
- logger.debug( |
|
386 |
- 'ignoring misplaced inner payload bytes.fromhex(%s)', |
|
387 |
- repr(inner_payload.hex(' ')), |
|
388 |
- ) |
|
389 |
- |
|
390 | 385 |
return session_keys |
391 | 386 |
|
392 | 387 |
|
... | ... |
@@ -398,7 +393,7 @@ def decrypt_contents(data: bytes, session_keys: KeyPair) -> bytes: |
398 | 393 |
- a 16-byte IV, |
399 | 394 |
- a variable-sized AES256-CBC-encrypted payload (using PKCS7 padding |
400 | 395 |
on the inside), and |
401 |
- - a 32-byte MAC of the preceding 80 bytes. |
|
396 |
+ - a 32-byte MAC of the preceding bytes. |
|
402 | 397 |
|
403 | 398 |
The encrypted payload is encrypted with the bucket item's session |
404 | 399 |
encryption key, and the MAC is created with the bucket item's |
... | ... |
@@ -726,16 +721,24 @@ def export_storeroom_data( # noqa: C901,PLR0912,PLR0914,PLR0915 |
726 | 721 |
json_content.decode('utf-8'), |
727 | 722 |
) |
728 | 723 |
_store(config_structure, path, json_content) |
729 |
- for _dir, namelist in dirs_to_check.items(): |
|
724 |
+ # Sorted order is important; see `mabye_obj` below. |
|
725 |
+ for _dir, namelist in sorted(dirs_to_check.items()): |
|
730 | 726 |
namelist = [x.rstrip('/') for x in namelist] # noqa: PLW2901 |
731 |
- try: |
|
732 |
- obj = config_structure |
|
727 |
+ obj: dict[Any, Any] = config_structure |
|
733 | 728 |
for part in _dir.split('/'): |
734 | 729 |
if part: |
735 |
- obj = obj[part] |
|
736 |
- except KeyError as exc: |
|
737 |
- msg = f'Cannot traverse storage path: {_dir!r}' |
|
738 |
- raise RuntimeError(msg) from exc |
|
730 |
+ # Because we iterate paths in sorted order, parent |
|
731 |
+ # directories are encountered before child directories. |
|
732 |
+ # So parent directories always exist (lest we would have |
|
733 |
+ # aborted earlier). |
|
734 |
+ # |
|
735 |
+ # Of course, the type checker doesn't necessarily know |
|
736 |
+ # this, so we need to use assertions anyway. |
|
737 |
+ maybe_obj = obj.get(part) |
|
738 |
+ assert isinstance( |
|
739 |
+ maybe_obj, dict |
|
740 |
+ ), f'Cannot traverse storage path {_dir!r}' |
|
741 |
+ obj = maybe_obj |
|
739 | 742 |
if set(obj.keys()) != set(namelist): |
740 | 743 |
msg = f'Object key mismatch for path {_dir!r}' |
741 | 744 |
raise RuntimeError(msg) |
... | ... |
@@ -1026,7 +1026,7 @@ _VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED_JAVASCRIPT_SOURCE = """ |
1026 | 1026 |
// Executed in the top-level directory of the vault project code, in Node.js. |
1027 | 1027 |
const storeroom = require('storeroom') |
1028 | 1028 |
const Store = require('./lib/store.js') |
1029 |
-let store = new Store(storeroom.createFileAdapter('./broken-dir', 'vault key')) |
|
1029 |
+let store = new Store(storeroom.createFileAdapter('./broken-dir'), 'vault key') |
|
1030 | 1030 |
await store._storeroom.put('/services/array/', ['entry1','entry2']) |
1031 | 1031 |
// The resulting "broken-dir" was then zipped manually. |
1032 | 1032 |
""" |
... | ... |
@@ -1067,6 +1067,157 @@ AgAAAAAAAAABAAAApIHtAgAAMWFQSwECHgMUAAIACAB4ox9ZGgj3mrkBAAAXAgAAAgAAAAAAAAAB |
1067 | 1067 |
AAAApIHIBAAAMWVQSwUGAAAAAAQABADDAAAAoQYAAAAA |
1068 | 1068 |
""" |
1069 | 1069 |
|
1070 |
+_VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2_JAVASCRIPT_SOURCE = """ |
|
1071 |
+// Executed in the top-level directory of the vault project code, in Node.js. |
|
1072 |
+const storeroom = require('storeroom') |
|
1073 |
+const Store = require('./lib/store.js') |
|
1074 |
+let store = new Store(storeroom.createFileAdapter('./broken-dir'), 'vault key') |
|
1075 |
+await store._storeroom.put('/services/array/', 'not a directory index') |
|
1076 |
+// The resulting "broken-dir" was then zipped manually. |
|
1077 |
+""" |
|
1078 |
+VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2 = b""" |
|
1079 |
+UEsDBAoAAAAAAM6NSVmrcHdV5gAAAOYAAAAFAAAALmtleXN7InZlcnNpb24iOjF9CkV3ZS9LZkJp |
|
1080 |
+L0V0OUcrZmxYM3gxaFU4ZjE4YlE3S253bHoxN0IxSDE3cUhVOGdWK2RpWWY5MTdFZ0YrSStidEpZ |
|
1081 |
+VXBzWVZVck45OC9uLzdsZnl2NUdGVEg2NWZxVy93YjlOc2MxeEZ4ck43Q3p4eTZ5MVAxZzFPb2VK |
|
1082 |
+b0RZU3J6YXlwT0E2M3pidmk0ZTRiREMyNXhPTXl5NHBoMDFGeGdnQmpSNnpUcmR2UDk2UlZQd0I5 |
|
1083 |
+WitOZkZWZUlXT1NQN254ZFNYMGdFbkZ4SDBmWDkzNTFaTTZnPVBLAwQKAAAAAADOjUlZJg3/BhcC |
|
1084 |
+AAAXAgAAAgAAADBieyJ2ZXJzaW9uIjoxfQpBVXJJMjNDQ2VpcW14cUZRMlV4SUpBaUoxNEtyUzh2 |
|
1085 |
+SXpIa2xROURBaFRlVHNFMmxPVUg4WUhTcUk1cXRGSHBqY3c1WkRkZmRtUlEwQXVGRjllY3lkam14 |
|
1086 |
+dDdUemRYLzNmNFUvTGlVV2dLRmQ1K1FEN3BlVlE1bWpqeHNlUEpHTDlhTWlKaGxSUVB4SmtUbjBx |
|
1087 |
+U2poM1RUT0ZZbVAzV0JkdlUyWnF2RzhaSDk2cU1WcnZsQ0dMRmZTc2svVXlvcHZKdENONUVXcTRZ |
|
1088 |
+SDUwNFNiejFIUVhWd2RjejlrS1BuR3J6SVA4ZmZtZnhXQ0U0TmtLb0ZPQXZuNkZvS3FZdGlGbFE9 |
|
1089 |
+PQpBVXBMUVMrMG9VeEZTeCtxbTB3SUtyM1MvTVJxYWJJTFlEUnY0aHlBMVE2TGR2Nlk0UmJ0enVz |
|
1090 |
+NzRBc0cxbVhhenlRU2hlZVowdk0xM2ZyTFA4YlV0VHBaRyszNXF1eUhLM2NaWVJRZUxKM0JzejZz |
|
1091 |
+b0xaQjNZTkpNenFxTTQrdzM1U0FZZ2lMU1NkN05NeWVrTHNhRUIzRDFOajlTRk85K3NGNEpFMWVL |
|
1092 |
+UXpNMkltNk9qOUNVQjZUSTV3UitibksxN1BnY2RaeTZUMVRMWElVREVxcDg4dWdsWmRFTVcrNU9k |
|
1093 |
+aE5ZbXEzZERWVWV4UnJpM1AwUmVBSi9KMGdJNkNoUUE9PVBLAwQKAAAAAADOjUlZTNfdphcCAAAX |
|
1094 |
+AgAAAgAAADBmeyJ2ZXJzaW9uIjoxfQpBWVJqOVpIUktGUEVKOHM2YVY2TkRoTk5jQlZ5cGVYUmdz |
|
1095 |
+cnBldFQ0cGhJRGROWFdGYzRia0daYkJxMngwRDFkcVNjYWk5UzEveDZ2K28zRE0rVEF2OVE3ZFVR |
|
1096 |
+QWVKR3RmRkhJZDZxWW0ybEdNSnF5WTRNWm14aE9YdXliend0V3Q4Mnhvb041QTZNcWpINmxKQllD |
|
1097 |
+UUN3ZEJjb3RER0EwRnlnVTEzeHV2WnIzT1puZnFFRGRqbzMxNkw5aExDN1RxMTYwUHpBOXJOSDMz |
|
1098 |
+ZkNBcUhIVXZiYlFQQWErekw1d3dEN3FlWkY2MHdJaEwvRmk5L3JhNGJDcHZRNC9ORWpRd3c9PQpB |
|
1099 |
+WWNGUDB1Y2xMMHh3ZDM2UXZXbm4wWXFsOU5WV0s3c05CMTdjdmM3N3VDZ0J2OE9XYkR5UHk5d05h |
|
1100 |
+R2NQQzdzcVdZdHpZRlBHR0taVjhVUzA1YTVsV1BabDNGVFNuQXNtekxPelBlcFZxaitleDU3aEsx |
|
1101 |
+QnV1bHkrUCtYQkE0YUtsaDM3c0RJL3I0UE1BVlJuMDNoSDJ5dEhDMW9PbjF0V1M5Q1NLV1pSMThh |
|
1102 |
+djdTT0RBMVBNRnFYTmZKZVNTaVJiQ2htbDdOcFVLbjlXSGJZandybDlqN0JSdy9kWjhNQldCb3Ns |
|
1103 |
+Nlc1dGZtdnJMVHhGRFBXYUgzSUp0T0czMEI1M3c9PVBLAwQKAAAAAADOjUlZn9rNID8CAAA/AgAA |
|
1104 |
+AgAAADFkeyJ2ZXJzaW9uIjoxfQpBYWFBb3lqaGljVDZ4eXh1c0U0RVlDZCtxbE81Z0dEYTBNSFVS |
|
1105 |
+MmgrSW9QMHV4UkY3b1BRS2czOHlQUEN3Ny9MYVJLQ0dQZ0RyZ2RpTWJTeUwzZ3ZNMFhseVpVMVBW |
|
1106 |
+QVJvNEFETU9lbXgrOWhtS0hjQWNKMG5EeW5oSkhGYTYyb2xyQUNxekZzblhKNVBSeEVTVzVEbUh0 |
|
1107 |
+Ui9nRm5Wa1FvalhyVW4ybmpYMjVVanZQaXhlMU96Y0daMmQ0MjdVTGdnY1hqMkhSdjJiZldDNDUw |
|
1108 |
+SGFXS3FDckZlYWlrQ2xkUUM2WGV3SkxZUjdvQUY3UjVha2ttK3M2MXNCRTVCaTg0QmJLWHluc1NG |
|
1109 |
+ejE0TXFrd2JMK1VMYVk9CkFUT3dqTUFpa3Q4My9NTW5KRXQ2b3EyNFN4KzJKNDc2K2gyTmEzbHUr |
|
1110 |
+MDg0cjlBT25aaUk0TmlYV0N1Q0lzakEzcTBwUHFJS1VXZHlPQW9uM2VHY0huZUppWUtVYllBaUJI |
|
1111 |
+MVNmbnhQQkMzZkFMRklybkQ4Y0VqeGpPcUFUaTQ5dE1mRmtib0dNQ3dEdFY0V3NJL0tLUlRCOFd1 |
|
1112 |
+MnNXK2J0V3QzVWlvZG9ZeUVLTDk3ekNNemZqdGptejF4SDhHTXY5WDVnaG9NSW5RQVNvYlRreVZ4 |
|
1113 |
+dWo5YnlDazdNbU0vK21ZL3AwZE9oYVY0Nncwcm04UGlvWEtzdzR4bXB3ditDWC9PRXV3Uy9meDJT |
|
1114 |
+Y0lOQnNuYVRiWT1QSwECHgMKAAAAAADOjUlZq3B3VeYAAADmAAAABQAAAAAAAAAAAAAApIEAAAAA |
|
1115 |
+LmtleXNQSwECHgMKAAAAAADOjUlZJg3/BhcCAAAXAgAAAgAAAAAAAAAAAAAApIEJAQAAMGJQSwEC |
|
1116 |
+HgMKAAAAAADOjUlZTNfdphcCAAAXAgAAAgAAAAAAAAAAAAAApIFAAwAAMGZQSwECHgMKAAAAAADO |
|
1117 |
+jUlZn9rNID8CAAA/AgAAAgAAAAAAAAAAAAAApIF3BQAAMWRQSwUGAAAAAAQABADDAAAA1gcAAAAA |
|
1118 |
+""" |
|
1119 |
+ |
|
1120 |
+_VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3_JAVASCRIPT_SOURCE = """ |
|
1121 |
+// Executed in the top-level directory of the vault project code, in Node.js. |
|
1122 |
+const storeroom = require('storeroom') |
|
1123 |
+const Store = require('./lib/store.js') |
|
1124 |
+let store = new Store(storeroom.createFileAdapter('./broken-dir'), 'vault key') |
|
1125 |
+await store._storeroom.put('/services/array/', [null, 1, true, [], {}]) |
|
1126 |
+// The resulting "broken-dir" was then zipped manually. |
|
1127 |
+""" |
|
1128 |
+VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3 = b""" |
|
1129 |
+UEsDBAoAAAAAAEOPSVnVlcff5gAAAOYAAAAFAAAALmtleXN7InZlcnNpb24iOjF9CkV4dVBHUDBi |
|
1130 |
+YkxrUVdvWnV5ZUJQRy8xdmM2MCt6MThOa3BsS09ydFAvUTVnQmxkYVpIOG10dTE5VWZFNGdGRGRj |
|
1131 |
+eHJtWUd4eXZDZFNqcVlOaDh4cTlzM3VydkdRTWFwcnhtdlZGZUxoSW4zZnVlTDAweEk0ZmlLenZN |
|
1132 |
+MmthUlRsNWNORGh3eUNlWVk4dzhBcXNhYjNyVWVsOEE0eVQ0cHU2d2tmQ3dTWUdqeG5HR29EcWJK |
|
1133 |
+VnVJVWNpZVBEcU9PTzU2b0MyMG9lT01adFVkTUtxV28zYnFZPVBLAwQKAAAAAABDj0lZ77OVHxcC |
|
1134 |
+AAAXAgAAAgAAADBjeyJ2ZXJzaW9uIjoxfQpBZllFQVVobEkyU2lZeGlrdWh0RzRNbUN3L1V2THBN |
|
1135 |
+VVhwVlB0NlRwdzRyNGdocVJhbGZWZ0hxUHFtbTczSnltdFFrNnZnR2JRdUpiQmVlYjYwOHNrMGk4 |
|
1136 |
+ZFJVZjNwdlc2SnUyejljQkdwOG5mTFpTdlNad1lLN09UK2gzSDNDcmoxbXNicEZUcHVldW81NXc1 |
|
1137 |
+dGdYMnBuWXNWTVcrczdjaHEyMUIya2lIVEZrdGt1MXlaRzhPYkVUQjNCOFNGODVVbi9CUjFEMHJ1 |
|
1138 |
+ME9zOWl4ZWM2VmNTMitTZndtNnNtSlk2ZW9ZNTJzOGJNRGdYMndjQ0srREdkOEo2VWp0NG5OQVE9 |
|
1139 |
+PQpBUWlPRnRZcmJybWUycEwxRFpGT1BjU0RHOUN2cVkvbHhTWGIwaVJUdmtIWFc2bEtHL0p4RUtU |
|
1140 |
+d3RTc0RTeDhsMTUvaHRmbWpOQ2tuTzhLVEFoKzhRQm5FbjZ0a2x5Y3BmeEIrTUxLRjFCM1Q1bjcv |
|
1141 |
+T0VUMExMdmgxU2k1bnRRNXhTUHZZNWtXeUMyZjhXUXFZb3FSNU5JVENMeDV6dWNsQ3dGb2kvVXc4 |
|
1142 |
+OWNNWjM1MHBSbThzUktJbjJFeDUrQ1JwS3ZHdnBHbFJaTmk5VHZmVkNic1FCalR3MC9aeklTdzVQ |
|
1143 |
+NW9BVWE2U1ExUVFnNHg4VUNkY0s2QUNLaFluY0d4TVE9PVBLAwQKAAAAAABDj0lZGk9LVj8CAAA/ |
|
1144 |
+AgAAAgAAADE0eyJ2ZXJzaW9uIjoxfQpBY1g2NVpMUWk4ck9pUlIyWGEwQlFHQVhQVWF2aHNJVGVY |
|
1145 |
+c2dzRk9OUmFTRzJCQlg0SGxJRHpwRUd5aDUrZ2czZVRwWDFNOERua3pMeTVzcWRkMFpmK3padTgz |
|
1146 |
+Qm52Y1JPREVIVDllUW91YUtPTWltdlRYanNuSXAxUHo5VGY1TlRkRjNJVTd2V1lhUDg4WTI5NG1i |
|
1147 |
+c1VVL2RKVTZqZ3ZDbUw2cE1VZ28xUU12bGJnaVp3cDV1RDFQZXlrSXdKVWdJSEgxTEpnYi9xU2tW |
|
1148 |
+c25leW1XY1RXR0NobzRvZGx3S2hJWmFCelhvNFhlN2U1V2I2VHA3Rkk5VUpVcmZIRTAvcVdrZUZE |
|
1149 |
+VmxlazY3cUx3ZFZXcU9DdFk9CkFhSGR0QjhydmQ0U3N4ZmJ5eU1OOHIzZEoxeHA5NmFIRTQvalNi |
|
1150 |
+Z05hZWttaDkyb2ROM1F4MUlqYXZsYVkxeEt1eFF3KzlwTHFIcTF5a1JSRjQzL2RVWGFIRk5UU0NX |
|
1151 |
+OVFsdmd3KzMwa1ZhSEdXRllvbFRnRWE4djQ3b3VrbGlmc01PZGM0YVNKb2R4ZUFJcVc3Q1cwdDVR |
|
1152 |
+b2RUbWREUXpqc3phZkQ4R2VOd2NFQjdGMHI2RzNoZEJlQndxd3Z6eENVYnpSUmU5bEQ3NjQ3RFp1 |
|
1153 |
+bEo1U3c4amlvV0paTW40NlZhV3BYUXk4UnNva3hHaW00WUpybUZIQ2JkVU9qSWJsUmQ1Z3VhUDNU |
|
1154 |
+M0NxeHRPdC94b1BhOD1QSwMECgAAAAAAQ49JWVJM8QYXAgAAFwIAAAIAAAAxNnsidmVyc2lvbiI6 |
|
1155 |
+MX0KQVlCWDF6M21qUlQrand4M2FyNkFpemxnalJZbUM0ZHg5NkxVQVBTVHNMWXJKVHFtWnd5N0Jy |
|
1156 |
+OFlCcElVamorMHdlT3lNaUtLVnFwaER3RXExNWFqUmlSZUVEQURTVHZwWmlLZUlnZjR5elUzZXNP |
|
1157 |
+eDJ2U2J1bXhTK0swUGZVa2tsSy9TRmRiU3EvUHFMRjBDRTVCMXNyKzJLYTB2WlJmak94R3VFeFRD |
|
1158 |
+RXozN0ZlWDNNR3NCNkhZVHEzaUJWcUR6NVB6eHpCWWM5Kyt6RitLS1RnMVp2NGRtRmVQTC9JSEY5 |
|
1159 |
+WnV6TWlqRXdCRkE3WnJ0dkRqd3ZYcWtsMVpsR0c4eUV3PT0KQVhUWkRLVnNleldpR1RMUVZqa2hX |
|
1160 |
+bXBnK05MYlM0M2MxZEpvK2xGcC9yWUJYZkw3Wll5cGdjWE5IWXNzd01nc2VSSTAzNmt6bGZkdGNa |
|
1161 |
+bTdiUUN6M2JuQmZ6ZlorZFFuT2Y5STVSU2l0QzB2UmsydkQrOFdwbmRPSzNucGY5S0VpWklOSzVq |
|
1162 |
+TEZGTTJDTkNmQzBabXNRUlF3T0k2N3l5ZHhjVnFDMXBnWHV6QXRXamlsSUpnN0p6eUtsY3BJUGJu |
|
1163 |
+SUc0UzRSUlhIdW1wZnpoeWFZWkd6T0FDamRSYTZIMWJxYkJkZXFaSHMvQXJvM25mVjdlbjhxSUE5 |
|
1164 |
+aVUrbnNweXFnPT1QSwECHgMKAAAAAABDj0lZ1ZXH3+YAAADmAAAABQAAAAAAAAAAAAAApIEAAAAA |
|
1165 |
+LmtleXNQSwECHgMKAAAAAABDj0lZ77OVHxcCAAAXAgAAAgAAAAAAAAAAAAAApIEJAQAAMGNQSwEC |
|
1166 |
+HgMKAAAAAABDj0lZGk9LVj8CAAA/AgAAAgAAAAAAAAAAAAAApIFAAwAAMTRQSwECHgMKAAAAAABD |
|
1167 |
+j0lZUkzxBhcCAAAXAgAAAgAAAAAAAAAAAAAApIGfBQAAMTZQSwUGAAAAAAQABADDAAAA1gcAAAAA |
|
1168 |
+""" |
|
1169 |
+ |
|
1170 |
+_VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4_JAVASCRIPT_SOURCE = """ |
|
1171 |
+// Executed in the top-level directory of the vault project code, in Node.js. |
|
1172 |
+const storeroom = require('storeroom') |
|
1173 |
+const Store = require('./lib/store.js') |
|
1174 |
+let store = new Store(storeroom.createFileAdapter('./broken-dir'), 'vault key') |
|
1175 |
+await store._storeroom.put('/dir/subdir/', []) |
|
1176 |
+await store._storeroom.put('/dir/', []) |
|
1177 |
+// The resulting "broken-dir" was then zipped manually. |
|
1178 |
+""" |
|
1179 |
+VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4 = b""" |
|
1180 |
+UEsDBAoAAAAAAE+5SVloORS+5gAAAOYAAAAFAAAALmtleXN7InZlcnNpb24iOjF9CkV6dWRoNkRQ |
|
1181 |
+YTlNSWFabHZ5TytVYTFuamhjV2hIaTFBU0lKYW5zcXBxVlA0blN2V0twUzdZOUc2bjFSbi8vUnVM |
|
1182 |
+VitwcHp5SC9RQk83R0hFenNVMzdCUzFwUmVVeGhxUVlVTE56OXZvQ0crM1ZaL3VncU44dDJiU05m |
|
1183 |
+Nyt5K3hiNng2aVlFUmNZYTJ0UkhzZVdIc0laTE9ha2lDb0lRVGV3cndwYjVMM2pnd0E3SXBzaDkz |
|
1184 |
+QkxHSzM5dXNYNmo0R0I2WkRUeW5JcGk4V3JkbDhnWVZCN0tVPVBLAwQKAAAAAABPuUlZ663uUhcC |
|
1185 |
+AAAXAgAAAgAAADAzeyJ2ZXJzaW9uIjoxfQpBV2wzS2gzd21ZSFVZZU1RR3BLSVowdVd1VXFna09h |
|
1186 |
+YmRjNzNYYXVsZTNtVS9sN2Zvd1AyS21jbFp3ZDM5V3lYVzRTcEw4R0l4YStDZW51S3V0Wm5nb0FR |
|
1187 |
+bWlnaUJUbkFaais5TENCcGNIWlZNY2RBVkgxKzBFNGpsanZ1UkVwZ0tPS05LZjRsTUl1QnZ4VmFB |
|
1188 |
+ZkdwNHJYNEZ4MmpPSlk1Y3NQZzBBRFBoZVAwN29GWVQ3alorSUNEK1AxNGZPdWpwMGRUeDRrTDIy |
|
1189 |
+LzlqalRDNXBCNVF5NW5iOUx3Zk5DUWViSUVpaTZpbU0vRmFrK1dtV05tMndqMERSTEc4RHY3ZkE9 |
|
1190 |
+PQpBU0c3NTNGTVVwWmxjK3E1YXRzcC93OUNqN2JPOFlpY24wZHg2UGloTmwzUS9WSjVVeGJmU3l0 |
|
1191 |
+ZDFDNDBRU2xXeTJqOTJDWUd3VER6eEdBMXVnb0FCYi9kTllTelVwbHJFb3BuUVphYXdsdTVwV2x0 |
|
1192 |
+Y1E5WTcveWN4S2E4b0JaaGY3RkFYcGo2c01wUW9zNzI5VFVabFd4UmI4VFRtN2FrVnR1OXcvYXlK |
|
1193 |
+RS9reDh4ZUYxSGJlc3Q4N1IxTGg2ODd3dS9XVUN2ZjNXYXo1VjNnZWY0RnpUTXg0bkpqSlZOd0U0 |
|
1194 |
+SzAxUTlaVzQ0bmVvbExPUVI1MkZDeDZvbml3RW9tenc9PVBLAwQKAAAAAABPuUlZRXky4CsCAAAr |
|
1195 |
+AgAAAgAAADEweyJ2ZXJzaW9uIjoxfQpBWmlYWVlvNUdCY2d5dkFRaGtyK2ZjUkdVSkdabDd2dE5w |
|
1196 |
+T2Mrd1VzbXJhQWhRN3dKdlYraGhKcTlrcWNKQnBWU0gyUTBTTVVhb29iNjBJM1NYNUNtTkJRU2FH |
|
1197 |
+M3prd0Y0T2F4TnpCZUh0NFlpaDd4Y3p2ak4xR0hISDJQYW0xam05K09ja3JLVmNMVURtNXRKb2ZC |
|
1198 |
+Z1E4Q2NwMGZMVkdEaURjNWF0MjVMc2piQVcvNkZFSnJ5VVBHWis4UVdYRmlWMGdtVVZybVc3VUFy |
|
1199 |
+dGhJQitWNTdZS1BORi95Nng2OU43UTFQbmp1cUczdlpybzljMEJ3d012NWoyc3BMMTJHcTdzTDZE |
|
1200 |
+alB1d0dHbnB2MkVZQTFLbmc9CkFTdjQwUkgzRmxzbGVlU1NjRlZNRmh3dEx6eEYxK2xpcmxEL29X |
|
1201 |
+alJLQ05qVWZhUVpJTWpqMWRoVkhOakNUTWhWZ1ZONkl3b04xTnFOMEV6cmdhaTFBWnNiMm9UczYw |
|
1202 |
+QkI1UGh0U0hhQ2U2WllUeE1JemFPS2FIK0w2eHhtaXIrTlQxNTRXS0x5amJMams3MU1na3Nwa0Yy |
|
1203 |
+WDBJMnlaWW5IUUM0bmdEL24yZzRtSVI2Q1hWL0JOUXNzeTBEeXdGLzN6eGRRYWw5cFBtVk1qYnFu |
|
1204 |
+cHY5SFNqRTg4S25naVpBWFhJWU1OVGF2L3Q3Y3dEWGdNekhKTlU0Y2xnVUtIQVZ3QT09UEsDBAoA |
|
1205 |
+AAAAAE+5SVkPfKx9FwIAABcCAAACAAAAMWR7InZlcnNpb24iOjF9CkFYbHNLRzQwZG5ibTJvcXdY |
|
1206 |
+U2ZrSWp3Mmxpa0lDS3hVOXU3TU52VkZ1NEJ2R1FVVitSVVdsS3MxL25TSlBtM2U2OTRvVHdoeDFo |
|
1207 |
+RFF3U0M5U0QvbXd5bnpjSTloUnRCUWVXMkVMOVU5L1ZGcHFsVWY3Z1ZOMHZ0ZWpXYnV4QnhsZlRD |
|
1208 |
+Tys4SFBwU2Zaa2VOUld5R2JNdzBFSU9LTmxRYjk3OUF0c1g3THR0NytaTkJnakZHYkZxaHdwa3kx |
|
1209 |
+WUNDVng1UmNZZ2tma2ZjWnVncGpzc1RzNVFvK1p3QXBEcDZ4V3JjSHMxUDhvNktBRzAwcjZZbkNM |
|
1210 |
+N2ErU1dwZmVNTUJhZz09CkFadVF0cFZMWmVvb292NkdyQlpnb3B6VmRGUXBlK1h6QXZuZ2dPVnZM |
|
1211 |
+VWtCYVF2akl5K1VLdXVUVlFoQ1JiMVp6dGZQL2dsNnoxOEsyZW5sQlo2bGJTZnoxTlBWeUVzYXB3 |
|
1212 |
+dDVpUVh4azd5UkJlZks1cFlsNTduUXlmcFZQbzlreFpnOVdHTkV3NVJ5MkExemhnNGl6TWxLRmJh |
|
1213 |
+UjZFZ0FjQ3NFOXAveGRLa29ZNjhOUlZmNXJDM3lMQjc3ZWgyS1hCUld2WDNZcE9XdW00OGtsbmtI |
|
1214 |
+akJjMFpiQmUrT3NZb3d5cXpoRFA2ZGQxRlFnMlFjK09vc3B4V0sycld4M01HZz09UEsBAh4DCgAA |
|
1215 |
+AAAAT7lJWWg5FL7mAAAA5gAAAAUAAAAAAAAAAAAAAKSBAAAAAC5rZXlzUEsBAh4DCgAAAAAAT7lJ |
|
1216 |
+Weut7lIXAgAAFwIAAAIAAAAAAAAAAAAAAKSBCQEAADAzUEsBAh4DCgAAAAAAT7lJWUV5MuArAgAA |
|
1217 |
+KwIAAAIAAAAAAAAAAAAAAKSBQAMAADEwUEsBAh4DCgAAAAAAT7lJWQ98rH0XAgAAFwIAAAIAAAAA |
|
1218 |
+AAAAAAAAAKSBiwUAADFkUEsFBgAAAAAEAAQAwwAAAMIHAAAAAA== |
|
1219 |
+""" |
|
1220 |
+ |
|
1070 | 1221 |
CANNOT_LOAD_CRYPTOGRAPHY = ( |
1071 | 1222 |
'Cannot load the required Python module "cryptography".' |
1072 | 1223 |
) |
... | ... |
@@ -17,6 +17,17 @@ from derivepassphrase.exporter import storeroom, vault_native |
17 | 17 |
|
18 | 18 |
cryptography = pytest.importorskip('cryptography', minversion='38.0') |
19 | 19 |
|
20 |
+from cryptography.hazmat.primitives import ( # noqa: E402 |
|
21 |
+ ciphers, |
|
22 |
+ hashes, |
|
23 |
+ hmac, |
|
24 |
+ padding, |
|
25 |
+) |
|
26 |
+from cryptography.hazmat.primitives.ciphers import ( # noqa: E402 |
|
27 |
+ algorithms, |
|
28 |
+ modes, |
|
29 |
+) |
|
30 |
+ |
|
20 | 31 |
if TYPE_CHECKING: |
21 | 32 |
from collections.abc import Callable |
22 | 33 |
from typing import Any |
... | ... |
@@ -284,22 +295,93 @@ class TestStoreroom: |
284 | 295 |
with pytest.raises(RuntimeError, match=err_msg): |
285 | 296 |
storeroom.export_storeroom_data() |
286 | 297 |
|
298 |
+ @pytest.mark.parametrize( |
|
299 |
+ ['zipped_config', 'error_text'], |
|
300 |
+ [ |
|
301 |
+ pytest.param( |
|
302 |
+ tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED, |
|
303 |
+ 'Object key mismatch', |
|
304 |
+ id='VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED', |
|
305 |
+ ), |
|
306 |
+ pytest.param( |
|
307 |
+ tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2, |
|
308 |
+ 'Directory index is not actually an index', |
|
309 |
+ id='VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2', |
|
310 |
+ ), |
|
311 |
+ pytest.param( |
|
312 |
+ tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3, |
|
313 |
+ 'Directory index is not actually an index', |
|
314 |
+ id='VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3', |
|
315 |
+ ), |
|
316 |
+ pytest.param( |
|
317 |
+ tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4, |
|
318 |
+ 'Object key mismatch', |
|
319 |
+ id='VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4', |
|
320 |
+ ), |
|
321 |
+ ], |
|
322 |
+ ) |
|
287 | 323 |
def test_403_export_storeroom_data_bad_directory_listing( |
288 | 324 |
self, |
289 | 325 |
monkeypatch: pytest.MonkeyPatch, |
326 |
+ zipped_config: bytes, |
|
327 |
+ error_text: str, |
|
290 | 328 |
) -> None: |
291 | 329 |
runner = click.testing.CliRunner(mix_stderr=False) |
292 | 330 |
with ( |
293 | 331 |
tests.isolated_vault_exporter_config( |
294 | 332 |
monkeypatch=monkeypatch, |
295 | 333 |
runner=runner, |
296 |
- vault_config=tests.VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED, |
|
334 |
+ vault_config=zipped_config, |
|
297 | 335 |
vault_key=tests.VAULT_MASTER_KEY, |
298 | 336 |
), |
299 |
- pytest.raises(RuntimeError, match='Object key mismatch'), |
|
337 |
+ pytest.raises(RuntimeError, match=error_text), |
|
300 | 338 |
): |
301 | 339 |
storeroom.export_storeroom_data() |
302 | 340 |
|
341 |
+ def test_404_decrypt_keys_wrong_data_length(self) -> None: |
|
342 |
+ payload = ( |
|
343 |
+ b"Any text here, as long as it isn't " |
|
344 |
+ b'exactly 64 or 96 bytes long.' |
|
345 |
+ ) |
|
346 |
+ assert len(payload) not in frozenset({ |
|
347 |
+ 2 * storeroom.KEY_SIZE, |
|
348 |
+ 3 * storeroom.KEY_SIZE, |
|
349 |
+ }) |
|
350 |
+ key = b'DEADBEEFdeadbeefDeAdBeEfdEaDbEeF' |
|
351 |
+ padder = padding.PKCS7(storeroom.IV_SIZE * 8).padder() |
|
352 |
+ plaintext = bytearray(padder.update(payload)) |
|
353 |
+ plaintext.extend(padder.finalize()) |
|
354 |
+ iv = b'deadbeefDEADBEEF' |
|
355 |
+ assert len(iv) == storeroom.IV_SIZE |
|
356 |
+ encryptor = ciphers.Cipher( |
|
357 |
+ algorithms.AES256(key), modes.CBC(iv) |
|
358 |
+ ).encryptor() |
|
359 |
+ ciphertext = bytearray(encryptor.update(plaintext)) |
|
360 |
+ ciphertext.extend(encryptor.finalize()) |
|
361 |
+ mac_obj = hmac.HMAC(key, hashes.SHA256()) |
|
362 |
+ mac_obj.update(iv) |
|
363 |
+ mac_obj.update(ciphertext) |
|
364 |
+ data = iv + bytes(ciphertext) + mac_obj.finalize() |
|
365 |
+ with pytest.raises( |
|
366 |
+ ValueError, |
|
367 |
+ match=r'Invalid encrypted master keys payload', |
|
368 |
+ ): |
|
369 |
+ storeroom.decrypt_master_keys_data( |
|
370 |
+ data, {'encryption_key': key, 'signing_key': key} |
|
371 |
+ ) |
|
372 |
+ with pytest.raises( |
|
373 |
+ ValueError, |
|
374 |
+ match=r'Invalid encrypted session keys payload', |
|
375 |
+ ): |
|
376 |
+ storeroom.decrypt_session_keys( |
|
377 |
+ data, |
|
378 |
+ { |
|
379 |
+ 'hashing_key': key, |
|
380 |
+ 'encryption_key': key, |
|
381 |
+ 'signing_key': key, |
|
382 |
+ }, |
|
383 |
+ ) |
|
384 |
+ |
|
303 | 385 |
|
304 | 386 |
class TestVaultNativeConfig: |
305 | 387 |
@pytest.mark.parametrize( |
306 | 388 |