jojonas / pyage

Actually good encryption. In Python.
MIT License
30 stars 2 forks source link

pyage fails to decrypt one of the official AGE test vectors #13

Closed marekyggdrasil closed 7 months ago

marekyggdrasil commented 7 months ago

Hello! I was playing with your library and found a problem. Not sure if I am using it incorrectly or if there is a bug.

from age.file import Decryptor as ageStreamDecrypt
from age.keys.agekey import AgePrivateKey

from io import BytesIO

# official AGE test vector
# https://github.com/C2SP/CCTV/blob/3ec4d716e80597545ed285cf62af3dded3a14f65/age/testdata/x25519

expected_payload = '013f54400c82da08037759ada907a8b864e97de81c088a182062c4b5622fd2ab'
file_key = '59454c4c4f57205355424d4152494e45'
identity = 'AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6'

age_payload = 'age-encryption.org/v1\n-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc\nEmECAEcKN+n/Vs9SbWiV+Hu0r+E8R77DdWYyd83nw7U\n--- Vn+54jqiiUCE+WZcEVY3f1sqHjlu/z1LCQ/T7Xm7qI0\nîÏbÇΑ´3\'NhÔòùL·L[þ÷¾ªRÈð¼™,ƒ1ûf'.encode('utf-8')

keys = [AgePrivateKey.from_private_string(identity)]

buffer_in = BytesIO()
buffer_in.write(age_payload)
buffer_in.seek(0)

# prepare the output buffer and decrypt
buffer_out = BytesIO()
with ageStreamDecrypt(keys, buffer_in) as decryptor:
    buffer_out.write(decryptor.read())

# return the content of the output buffer
buffer_out.seek(0)
decrypted = buffer_out.read()
print(decrypted)

fails with

Traceback (most recent call last):
  File "/Users/marek/Development/open-source/demonstrate-hmac-problem/page.py", line 23, in <module>
    with ageStreamDecrypt(keys, buffer_in) as decryptor:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/marek/.pyenv/versions/mimblewimble/lib/python3.11/site-packages/age/file.py", line 78, in __init__
    self._decrypt_body()
  File "/Users/marek/.pyenv/versions/mimblewimble/lib/python3.11/site-packages/age/file.py", line 129, in _decrypt_body
    plaintext = stream_decrypt(stream_key, ciphertext)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/marek/.pyenv/versions/mimblewimble/lib/python3.11/site-packages/age/stream.py", line 48, in stream_decrypt
    decrypted += aead.decrypt(nonce=packed_nonce, data=block, associated_data=None)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
cryptography.exceptions.InvalidTag

the test vector is

expect: success
payload: 013f54400c82da08037759ada907a8b864e97de81c088a182062c4b5622fd2ab
file key: 59454c4c4f57205355424d4152494e45
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6

age-encryption.org/v1
-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc
EmECAEcKN+n/Vs9SbWiV+Hu0r+E8R77DdWYyd83nw7U
--- Vn+54jqiiUCE+WZcEVY3f1sqHjlu/z1LCQ/T7Xm7qI0
îÏbÇΑ´3'NhÔòùL·L[þ÷¾ªRÈð¼™,ƒ1ûf

ref https://github.com/C2SP/CCTV/blob/3ec4d716e80597545ed285cf62af3dded3a14f65/age/testdata/x25519

many thanks in advance for your comments!

marekyggdrasil commented 7 months ago

I found the payload is not valid. I examined the test vector using a hex editor and I found the bytes of the payload should be eecf62c7ce91b433274e68d4f2f9134cb74c5bfef7beaa52c8f0bc0e992c1e8331fb66.

from age.file import Decryptor as ageStreamDecrypt
from age.keys.agekey import AgePrivateKey

from io import BytesIO

# official AGE test vector
# https://github.com/C2SP/CCTV/blob/3ec4d716e80597545ed285cf62af3dded3a14f65/age/testdata/x25519

expected_payload = '013f54400c82da08037759ada907a8b864e97de81c088a182062c4b5622fd2ab'
file_key = '59454c4c4f57205355424d4152494e45'
identity = 'AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6'

age_payload = 'age-encryption.org/v1\n-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc\nEmECAEcKN+n/Vs9SbWiV+Hu0r+E8R77DdWYyd83nw7U\n--- Vn+54jqiiUCE+WZcEVY3f1sqHjlu/z1LCQ/T7Xm7qI0\n'.encode('utf-8') + bytes.fromhex('eecf62c7ce91b433274e68d4f2f9134cb74c5bfef7beaa52c8f0bc0e992c1e8331fb66')

keys = [AgePrivateKey.from_private_string(identity)]

buffer_in = BytesIO()
buffer_in.write(age_payload)
buffer_in.seek(0)

# prepare the output buffer and decrypt
buffer_out = BytesIO()
with ageStreamDecrypt(keys, buffer_in) as decryptor:
    buffer_out.write(decryptor.read())

# return the content of the output buffer
buffer_out.seek(0)
decrypted = buffer_out.read()

print()
print('decrypted payload is')
print(decrypted)

HMAC verification is passing and decrypted payload is age string. The script outputs

encoded mac Vn+54jqiiUCE+WZcEVY3f1sqHjlu/z1LCQ/T7Xm7qI0
mac 567fb9e23aa2894084f9665c1156377f5b2a1e396eff3d4b090fd3ed79bba88d
decrypted payload is
b'age'

I will probably ask if the testvector is correct, probably just payload entry is invalid.

marekyggdrasil commented 7 months ago

Ok nevermind, I found SHA256(age)=013f54400c82da08037759ada907a8b864e97de81c088a182062c4b5622fd2ab, that makes sense and all is working now.

Thanks for great lib!