decrypt.py
Raw
1from cryptography.hazmat.primitives import hashes
2from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
3from cryptography.hazmat.primitives.ciphers.aead import AESGCM
4from cryptography.hazmat.backends import default_backend
5import sys
6
7CHUNK = 64 * 1024 # plaintext bytes per chunk (must match src/crypto.ts)
8TAG = 16
9ENC_CHUNK = CHUNK + TAG # ciphertext bytes for a full chunk
10HEADER = 16 + 7 # salt[16] | noncePrefix[7]
11
12
13def decrypt(data: bytes, password: str) -> bytes:
14 """
15 Decrypts content produced by encrypt.py / the server / browser.
16
17 Uses chunked AES-256-GCM with a PBKDF2-HMAC-SHA512 derived key, matching the
18 implementation in src/crypto.ts.
19
20 Wire format: salt[16] | noncePrefix[7] | encChunk_0 | encChunk_1 | ...
21 where encChunk_i = AES-GCM(key, nonce_i, plaintext_i) (16-byte tag appended)
22 and nonce_i = noncePrefix[7] || uint32_be(i) || flag, flag=1 on the final
23 chunk else 0. Each non-final plaintext chunk is exactly CHUNK bytes, so chunk
24 boundaries (and which chunk is last) follow from the total length.
25
26 Args:
27 data (bytes): The encrypted wire format.
28 password (str): The password used for encryption.
29
30 Returns:
31 bytes: the decrypted content
32
33 Raises:
34 Exception: if the password is wrong or the data is corrupt/truncated (the
35 AES-GCM tag, chunk counter, and final-chunk flag are all authenticated).
36 """
37
38 if len(data) < HEADER + TAG:
39 raise ValueError("ciphertext too short")
40
41 salt = data[:16]
42 prefix = data[16:HEADER]
43 body = data[HEADER:]
44
45 kdf = PBKDF2HMAC(
46 algorithm=hashes.SHA512(),
47 length=32,
48 salt=salt,
49 iterations=210000,
50 backend=default_backend()
51 )
52 aes = AESGCM(kdf.derive(password.encode()))
53
54 out = bytearray()
55 offset = 0
56 i = 0
57 while offset < len(body):
58 enc_len = min(ENC_CHUNK, len(body) - offset)
59 last = 1 if offset + enc_len >= len(body) else 0
60 nonce = prefix + i.to_bytes(4, "big") + bytes([last])
61 out += aes.decrypt(nonce, body[offset:offset + enc_len], None)
62 offset += enc_len
63 i += 1
64 return bytes(out)
65
66
67if __name__ == "__main__":
68 if len(sys.argv) != 4:
69 print("Usage: decrypt.py <file> <password> <outfile>")
70 sys.exit(1)
71 data = decrypt(open(sys.argv[1], "rb").read(), sys.argv[2])
72 open(sys.argv[3], "wb").write(data)
73