diff --git a/taskchampion/src/server/crypto.rs b/taskchampion/src/server/crypto.rs index b2728acad..978a38fd8 100644 --- a/taskchampion/src/server/crypto.rs +++ b/taskchampion/src/server/crypto.rs @@ -317,4 +317,96 @@ mod test { let cryptor = Cryptor::new(client_key, &secret).unwrap(); assert!(cryptor.unseal(sealed).is_err()); } + + mod externally_valid { + // validate data generated by generate-test-data.py. The intent is to + // validate that this format matches the specification by implementing + // the specification in a second language + use super::*; + use pretty_assertions::assert_eq; + + /// The values in generate-test-data.py + fn defaults() -> (Uuid, Uuid, Vec) { + ( + Uuid::parse_str("b0517957-f912-4d49-8330-f612e73030c4").unwrap(), + Uuid::parse_str("0666d464-418a-4a08-ad53-6f15c78270cd").unwrap(), + b"b4a4e6b7b811eda1dc1a2693ded".to_vec(), + ) + } + + #[test] + fn good() { + let (version_id, client_key, encryption_secret) = defaults(); + let sealed = Sealed { + version_id, + payload: include_bytes!("test-good.data").to_vec(), + }; + + let cryptor = Cryptor::new(client_key, &Secret(encryption_secret)).unwrap(); + let unsealed = cryptor.unseal(sealed).unwrap(); + + assert_eq!(unsealed.payload, b"SUCCESS"); + assert_eq!(unsealed.version_id, version_id); + } + + #[test] + fn bad_version_id() { + let (version_id, client_key, encryption_secret) = defaults(); + let sealed = Sealed { + version_id, + payload: include_bytes!("test-bad-version-id.data").to_vec(), + }; + + let cryptor = Cryptor::new(client_key, &Secret(encryption_secret)).unwrap(); + assert!(cryptor.unseal(sealed).is_err()); + } + + #[test] + fn bad_client_key() { + let (version_id, client_key, encryption_secret) = defaults(); + let sealed = Sealed { + version_id, + payload: include_bytes!("test-bad-client-key.data").to_vec(), + }; + + let cryptor = Cryptor::new(client_key, &Secret(encryption_secret)).unwrap(); + assert!(cryptor.unseal(sealed).is_err()); + } + + #[test] + fn bad_secret() { + let (version_id, client_key, encryption_secret) = defaults(); + let sealed = Sealed { + version_id, + payload: include_bytes!("test-bad-secret.data").to_vec(), + }; + + let cryptor = Cryptor::new(client_key, &Secret(encryption_secret)).unwrap(); + assert!(cryptor.unseal(sealed).is_err()); + } + + #[test] + fn bad_version() { + let (version_id, client_key, encryption_secret) = defaults(); + let sealed = Sealed { + version_id, + payload: include_bytes!("test-bad-version.data").to_vec(), + }; + + let cryptor = Cryptor::new(client_key, &Secret(encryption_secret)).unwrap(); + assert!(cryptor.unseal(sealed).is_err()); + } + + #[test] + fn bad_app_id() { + let (version_id, client_key, encryption_secret) = defaults(); + let sealed = Sealed { + version_id, + payload: include_bytes!("test-bad-app-id.data").to_vec(), + }; + + let cryptor = Cryptor::new(client_key, &Secret(encryption_secret)).unwrap(); + assert!(cryptor.unseal(sealed).is_err()); + } + } } diff --git a/taskchampion/src/server/generate-test-data.py b/taskchampion/src/server/generate-test-data.py new file mode 100644 index 000000000..366597813 --- /dev/null +++ b/taskchampion/src/server/generate-test-data.py @@ -0,0 +1,77 @@ +# This file generates test-encrypted.data. To run it: +# - pip install cryptography pbkdf2 +# - python taskchampion/src/server/generate-test-data.py taskchampion/src/server/ + +import os +import hashlib +import pbkdf2 +import secrets +import sys +import uuid + +from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 + +# these values match values used in the rust tests +client_key = "0666d464-418a-4a08-ad53-6f15c78270cd" +encryption_secret = b"b4a4e6b7b811eda1dc1a2693ded" +version_id = "b0517957-f912-4d49-8330-f612e73030c4" + +def gen( + version_id=version_id, client_key=client_key, encryption_secret=encryption_secret, + app_id=1, version=1): + # first, generate the encryption key + salt = hashlib.sha256(uuid.UUID(client_key).bytes).digest() + key = pbkdf2.PBKDF2( + encryption_secret, + salt, + digestmodule=hashlib.sha256, + iterations=100000, + ).read(32) + + # create a nonce + nonce = secrets.token_bytes(12) + + assert len(b"\x01") == 1 + # create the AAD + aad = b''.join([ + bytes([app_id]), + uuid.UUID(version_id).bytes, + ]) + + # encrypt using AEAD + chacha = ChaCha20Poly1305(key) + ciphertext = chacha.encrypt(nonce, b"SUCCESS", aad) + + # create the envelope + envelope = b''.join([ + bytes([version]), + nonce, + ciphertext, + ]) + + return envelope + + +def main(): + dir = sys.argv[1] + + with open(os.path.join(dir, 'test-good.data'), "wb") as f: + f.write(gen()) + + with open(os.path.join(dir, 'test-bad-version-id.data'), "wb") as f: + f.write(gen(version_id=uuid.uuid4().hex)) + + with open(os.path.join(dir, 'test-bad-client-key.data'), "wb") as f: + f.write(gen(client_key=uuid.uuid4().hex)) + + with open(os.path.join(dir, 'test-bad-secret.data'), "wb") as f: + f.write(gen(encryption_secret=b"xxxxxxxxxxxxxxxxxxxxx")) + + with open(os.path.join(dir, 'test-bad-version.data'), "wb") as f: + f.write(gen(version=99)) + + with open(os.path.join(dir, 'test-bad-app-id.data'), "wb") as f: + f.write(gen(app_id=99)) + + +main() diff --git a/taskchampion/src/server/test-bad-app-id.data b/taskchampion/src/server/test-bad-app-id.data new file mode 100644 index 000000000..a1c6832a6 --- /dev/null +++ b/taskchampion/src/server/test-bad-app-id.data @@ -0,0 +1,2 @@ +#$ +^~B>n)ji19|~ \ No newline at end of file diff --git a/taskchampion/src/server/test-bad-client-key.data b/taskchampion/src/server/test-bad-client-key.data new file mode 100644 index 000000000..bfdd9635e --- /dev/null +++ b/taskchampion/src/server/test-bad-client-key.data @@ -0,0 +1 @@ +A4 t; pϦx^reJԤ \ No newline at end of file diff --git a/taskchampion/src/server/test-bad-secret.data b/taskchampion/src/server/test-bad-secret.data new file mode 100644 index 000000000..696da066f --- /dev/null +++ b/taskchampion/src/server/test-bad-secret.data @@ -0,0 +1 @@ +/}d EdIcX-!V%4d]} \ No newline at end of file diff --git a/taskchampion/src/server/test-bad-version-id.data b/taskchampion/src/server/test-bad-version-id.data new file mode 100644 index 000000000..2ccd8c638 --- /dev/null +++ b/taskchampion/src/server/test-bad-version-id.data @@ -0,0 +1 @@ +la|@ύS_zV9qх)+ \ No newline at end of file diff --git a/taskchampion/src/server/test-bad-version.data b/taskchampion/src/server/test-bad-version.data new file mode 100644 index 000000000..20fed792e --- /dev/null +++ b/taskchampion/src/server/test-bad-version.data @@ -0,0 +1 @@ +cTHp>溦m4~10PIW \ No newline at end of file diff --git a/taskchampion/src/server/test-bad-version_id.data b/taskchampion/src/server/test-bad-version_id.data new file mode 100644 index 000000000..4a228ec4a --- /dev/null +++ b/taskchampion/src/server/test-bad-version_id.data @@ -0,0 +1,2 @@ +B +-3%j,*ߺ7꩖QKOFPZ \ No newline at end of file diff --git a/taskchampion/src/server/test-good.data b/taskchampion/src/server/test-good.data new file mode 100644 index 000000000..9efec7577 --- /dev/null +++ b/taskchampion/src/server/test-good.data @@ -0,0 +1 @@ +pѿҟVTo"}cTY7 @dLT` \ No newline at end of file