• 
      

    Add base support for keys, including AES and EC keys.

    Review Request #15168 — Created July 14, 2026 and updated

    Information

    cryptozoology
    master

    Reviewers

    This introduces a foundation for encryption/signing keys, with specific
    implementations for AES symmetric keys and EC public/private keys.

    All keys derive from BaseKey, which can be deserialized from bytes or
    serialized to bytes, can optionally perform encryption/decryption and
    key wrapping/unwrapping, and has a SHA256 fingerprint and optional key
    ID.

    There are subclasses for bytes-based keys and for public/private keys.

    AES keys are a bytes-based key. They can be generated for a supported
    key size, can be derived from another key, or derived from a key
    exchange. They support CFB8, GCM, and GCM-SIV modes.

    EC public/private key pairs support ECDH key exchange.

    Keys can be invalidated, which will clear out state on the key. While
    optional, keys should be used as a context manager, which will
    automatically invalidate the key once the context manager exits. This is
    a precaution to keep key material in memory for as little time as
    possible.

    All bytes storage for key material is done using bytearrays. These can
    be cleared out, affecting all holders of the state. This can help
    prevent keys from accidentally staying around in memory long-term (as
    best as can be prevented in Python).

    Encryption returns a a representation of the encrypted state, providing
    a full resulting ciphertext and the component parts. Key wrapping does
    similar. This will be used to help with secrets storage and defining
    envelopes (in a future change).

    All crypto operations take an algorithm, which are standardized. Keys
    can indicate their support for different algorithms (which will be used
    for secrets management later), validate against them when performing
    the operation, and select the appropriate parameters for the operation.

    An upcoming change will build upon this to provide higher-level
    operations for secrets storage and key type registries, managing
    envelope creation and parsing.

    Unit tests pass.

    Summary ID
    Add base support for keys, including AES and EC keys.
    This introduces a foundation for encryption/signing keys, with specific implementations for AES symmetric keys and EC public/private keys. All keys derive from `BaseKey`, which can be deserialized from bytes or serialized to bytes, can optionally perform encryption/decryption and key wrapping/unwrapping, and has a SHA256 fingerprint and optional key ID. There are subclasses for bytes-based keys and for public/private keys. AES keys are a bytes-based key. They can be generated for a supported key size, can be derived from another key, or derived from a key exchange. They support CFB8, GCM, and GCM-SIV modes. EC public/private key pairs support ECDH key exchange. Keys can be invalidated, which will clear out state on the key. While optional, keys should be used as a context manager, which will automatically invalidate the key once the context manager exits. This is a precaution to keep key material in memory for as little time as possible. All bytes storage for key material is done using `bytearray`s. These can be cleared out, affecting all holders of the state. This can help prevent keys from accidentally staying around in memory long-term (as best as can be prevented in Python). Encryption returns a a representation of the encrypted state, providing a full resulting ciphertext and the component parts. Key wrapping does similar. This will be used to help with secrets storage and defining envelopes (in a future change). All crypto operations take an algorithm, which are standardized. Keys can indicate their support for different algorithms (which will be used for secrets management later), validate against them when performing the operation, and select the appropriate parameters for the operation. An upcoming change will build upon this to provide higher-level operations for secrets storage and key type registries, managing envelope creation and parsing.
    24628ad7fee460bf2460c6b1d8bbbc7ea11f979c
    Description From Last Updated

    Can we add a round-trip test for each supported algorithm that encrypts with a random nonce and decrypts back to …

    david david

    'cryptozoology.keys.aes.AESKey' imported but unused Column: 1 Error code: F401

    reviewbot reviewbot

    'cryptozoology.keys.ec.ECPrivateKey' imported but unused Column: 1 Error code: F401

    reviewbot reviewbot

    'cryptozoology.keys.ec.ECPublicKey' imported but unused Column: 1 Error code: F401

    reviewbot reviewbot

    blank line at end of file Column: 1 Error code: W391

    reviewbot reviewbot

    local variable 'e' is assigned to but never used Column: 9 Error code: F841

    reviewbot reviewbot

    This is the only one of these that doesn't inherit from CryptozoologyError. Should we use that here so callers can …

    david david

    This should fit on one line.

    david david

    Typo: bibts -> bits

    david david

    If this isn't passed in, we generate a salt and then discard it (only the key gets returned), making it …

    david david

    Do we want to raise ... from e?

    david david

    Can we recommend in here that callers not use this parameter and keep it reserved for unit tests? If the …

    david david

    With a malformed input, we could get a ValueError. Should we catch that as well to turn it into a …

    david david

    Typo: alt -> alg

    david david

    Can we wrap as: from cryptozoology.utils.invalidation import ( InvalidatableMixin, SensitiveBytes, )

    david david

    Seems like this got truncated.

    david david

    This is returning the internal mutable bytearray. Can we return a copy instead?

    david david

    The standard term is "Elliptic Curve", not "Elliptical Curve". Several throughout the change.

    david david

    The registry isn't part of this change so I can't tell, but this key is duplicated on both ECPublicKey and …

    david david

    Do we want to raise ... from e?

    david david

    Since this is greenfield, can we use pytest test cases and real docstrings instead of a UnitTest subclass? Same for …

    david david

    This docstring doesn't seem to match the function name.

    david david

    This seems like a copy from the test just below. This is hitting the expected error before that triggers, but …

    david david

    There aren't any tests for the methods in this file. Since they sit on the crypto path, it would be …

    david david

    'cryptozoology.utils.random.generate_aes_salt' imported but unused Column: 1 Error code: F401

    reviewbot reviewbot

    redefinition of unused 'BytesLike' from line 67 Column: 5 Error code: F811

    reviewbot reviewbot

    'typing.TypeAlias' imported but unused Column: 5 Error code: F401

    reviewbot reviewbot
    Checks run (1 failed, 1 succeeded)
    flake8 failed.
    JSHint passed.

    flake8

    chipx86
    chipx86
    david
    1. 
        
    2. Show all issues

      Can we add a round-trip test for each supported algorithm that encrypts with a random nonce and decrypts back to plaintext?

      1. We effectively have that. It's the same encrypted payloads being used for the decryption tests. I didn't think it'd be worth duplicating all that.

    3. cryptozoology/errors.py (Diff revision 3)
       
       
      Show all issues

      This is the only one of these that doesn't inherit from CryptozoologyError. Should we use that here so callers can just catch that one thing?

      1. Yep, not sure how I missed that.

    4. cryptozoology/keys/aes.py (Diff revision 3)
       
       
      Show all issues

      Typo: bibts -> bits

    5. cryptozoology/keys/aes.py (Diff revision 3)
       
       
       
      Show all issues

      If this isn't passed in, we generate a salt and then discard it (only the key gets returned), making it so the derived key can never be reproduced. This means data encrypted with this key could never be decrypted.

      We should either require that the salt be passed in, or return it along with the key so the caller can save it. I'd lean towards requiring it.

      Same with the derive_from_key_exchange API.

    6. cryptozoology/keys/aes.py (Diff revision 3)
       
       
      Show all issues

      Do we want to raise ... from e?

    7. cryptozoology/keys/aes.py (Diff revision 3)
       
       
       
       
       
      Show all issues

      Can we recommend in here that callers not use this parameter and keep it reserved for unit tests? If the nonce isn't properly random, it's Very Bad™ for GCM and CFB8.

      Even better would be for this public method to not have the parameter, and delegate (mostly) to a private method that we can use for the tests.

      1. Going to keep it as an option because it's useful during ECIES (basically, letting a unit test pass it in there and then it passes it in here), but I'll change the documentation around it.

    8. cryptozoology/keys/aes.py (Diff revision 3)
       
       
      Show all issues

      With a malformed input, we could get a ValueError. Should we catch that as well to turn it into a DecryptionError?

      1. I haven't been able to get a ValueError out of this. Got a repro case?

      2. I think you should be able to get it with ciphertext that's shorter than 16 bytes.

    9. cryptozoology/keys/aes.py (Diff revision 3)
       
       
      Show all issues

      Typo: alt -> alg

    10. cryptozoology/keys/base.py (Diff revision 3)
       
       
      Show all issues

      Seems like this got truncated.

    11. cryptozoology/keys/base.py (Diff revision 3)
       
       
      Show all issues

      This is returning the internal mutable bytearray. Can we return a copy instead?

      1. No, the internal one is correct. The goal is to actively avoid copies of sensitive data. I plan to remove the mutability aspect but as later work.

    12. cryptozoology/keys/ec.py (Diff revision 3)
       
       
      Show all issues

      The standard term is "Elliptic Curve", not "Elliptical Curve". Several throughout the change.

      1. You know what's funny? I had Elliptic Curve everywhere, and then changed it at like 3AM because I thought I had it backwards in that moment.

    13. cryptozoology/keys/ec.py (Diff revision 3)
       
       
      Show all issues

      The registry isn't part of this change so I can't tell, but this key is duplicated on both ECPublicKey and ECPrivateKey. Is that ok?

      1. Yeah, that's okay. Only the private keys will end up registered, but this is ensuring that either key will map to the type of key (regardless of public/private) being used.

    14. cryptozoology/keys/ec.py (Diff revision 3)
       
       
      Show all issues

      Do we want to raise ... from e?

    15. Show all issues

      This docstring doesn't seem to match the function name.

    16. Show all issues

      This seems like a copy from the test just below. This is hitting the expected error before that triggers, but can we change this to be AES-256-GCM just to keep it so the only "incorrect" thing about this is the key?

      1. I think you commented on the wrong test. I assume you mean the unsupported algorithm vs. key size? They should be separate, but where I had messed up was a change to move to constants for the key bytes coming in. I'll fix that up. One test is indeed meant to choose an unsupported algorithm, the other a supported but with wrong key size.

      2. This is in the test about invalidated keys, but this algorithm doesn't exist. Would be good to have a correct algorithm name for this test.

      3. Ahhh I see.

    17. cryptozoology/utils/encoding.py (Diff revision 3)
       
       
      Show all issues

      There aren't any tests for the methods in this file. Since they sit on the crypto path, it would be nice to have (especially the padding calc in b64u_decode)

      1. I had initially intended to fold those into a separate commit. I do have unit tests for those, so I'll pull this out and rebase on top of that.

    18. 
        
    david
    1. 
        
    2. Show all issues

      Since this is greenfield, can we use pytest test cases and real docstrings instead of a UnitTest subclass? Same for the other test files here.

      1. I started with that, but moved away from it. I intentionally want to keep all this unittest. Class-level test setup/teardown, class-namespaced tests, and the flexibility with mixins are very helpful here and for upcoming work.

    3. 
        
    chipx86
    chipx86
    Review request changed
    Change Summary:
    • Moved the utils code out of this change.
    • An explicit salt is now required for key derivation and exchange.
    • Documentation for encrypt() now has strong guidance on randomly-generating nonces.
    • Fixed DecryptionError to inherit from CryptozoologyError.
    • Fixed error handling (and a test) when a ciphertext is truncated.
    • Some exceptions now raise .. from e.
    • Fixed several typos.
    Commits:
    Summary ID
    Add base support for keys, including AES and EC keys.
    This introduces a foundation for encryption/signing keys, with specific implementations for AES symmetric keys and EC public/private keys. All keys derive from `BaseKey`, which can be deserialized from bytes or serialized to bytes, can optionally perform encryption/decryption and key wrapping/unwrapping, and has a SHA256 fingerprint and optional key ID. There are subclasses for bytes-based keys and for public/private keys. AES keys are a bytes-based key. They can be generated for a supported key size, can be derived from another key, or derived from a key exchange. They support CFB8, GCM, and GCM-SIV modes. EC public/private key pairs support ECDH key exchange. Keys can be invalidated, which will clear out state on the key. While optional, keys should be used as a context manager, which will automatically invalidate the key once the context manager exits. This is a precaution to keep key material in memory for as little time as possible. All bytes storage for key material is done using `bytearray`s. These can be cleared out, affecting all holders of the state. This can help prevent keys from accidentally staying around in memory long-term (as best as can be prevented in Python). Encryption returns a a representation of the encrypted state, providing a full resulting ciphertext and the component parts. Key wrapping does similar. This will be used to help with secrets storage and defining envelopes (in a future change). All crypto operations take an algorithm, which are standardized. Keys can indicate their support for different algorithms (which will be used for secrets management later), validate against them when performing the operation, and select the appropriate parameters for the operation. An upcoming change will build upon this to provide higher-level operations for secrets storage and key type registries, managing envelope creation and parsing.
    a2af5cad3f78c8ff07335d2a71ed2cda1c7da1f3
    Add base support for keys, including AES and EC keys.
    This introduces a foundation for encryption/signing keys, with specific implementations for AES symmetric keys and EC public/private keys. All keys derive from `BaseKey`, which can be deserialized from bytes or serialized to bytes, can optionally perform encryption/decryption and key wrapping/unwrapping, and has a SHA256 fingerprint and optional key ID. There are subclasses for bytes-based keys and for public/private keys. AES keys are a bytes-based key. They can be generated for a supported key size, can be derived from another key, or derived from a key exchange. They support CFB8, GCM, and GCM-SIV modes. EC public/private key pairs support ECDH key exchange. Keys can be invalidated, which will clear out state on the key. While optional, keys should be used as a context manager, which will automatically invalidate the key once the context manager exits. This is a precaution to keep key material in memory for as little time as possible. All bytes storage for key material is done using `bytearray`s. These can be cleared out, affecting all holders of the state. This can help prevent keys from accidentally staying around in memory long-term (as best as can be prevented in Python). Encryption returns a a representation of the encrypted state, providing a full resulting ciphertext and the component parts. Key wrapping does similar. This will be used to help with secrets storage and defining envelopes (in a future change). All crypto operations take an algorithm, which are standardized. Keys can indicate their support for different algorithms (which will be used for secrets management later), validate against them when performing the operation, and select the appropriate parameters for the operation. An upcoming change will build upon this to provide higher-level operations for secrets storage and key type registries, managing envelope creation and parsing.
    4600299fef0f9eba6b51d0f8cefc777a6c582b4d

    Checks run (1 failed, 1 succeeded)

    flake8 failed.
    JSHint passed.

    flake8

    chipx86
    Review request changed
    Change Summary:
    • Removes unused/duplicate imports.

    Also missed in the previous update:

    • Removed KeyBytesLike in favor of the centralized BytesLike.
    • Used SensitiveBytes for storage and sensitive results to support invalidation.
    • More functions take a BytesLike instead of just bytes.
    Commits:
    Summary ID
    Add base support for keys, including AES and EC keys.
    This introduces a foundation for encryption/signing keys, with specific implementations for AES symmetric keys and EC public/private keys. All keys derive from `BaseKey`, which can be deserialized from bytes or serialized to bytes, can optionally perform encryption/decryption and key wrapping/unwrapping, and has a SHA256 fingerprint and optional key ID. There are subclasses for bytes-based keys and for public/private keys. AES keys are a bytes-based key. They can be generated for a supported key size, can be derived from another key, or derived from a key exchange. They support CFB8, GCM, and GCM-SIV modes. EC public/private key pairs support ECDH key exchange. Keys can be invalidated, which will clear out state on the key. While optional, keys should be used as a context manager, which will automatically invalidate the key once the context manager exits. This is a precaution to keep key material in memory for as little time as possible. All bytes storage for key material is done using `bytearray`s. These can be cleared out, affecting all holders of the state. This can help prevent keys from accidentally staying around in memory long-term (as best as can be prevented in Python). Encryption returns a a representation of the encrypted state, providing a full resulting ciphertext and the component parts. Key wrapping does similar. This will be used to help with secrets storage and defining envelopes (in a future change). All crypto operations take an algorithm, which are standardized. Keys can indicate their support for different algorithms (which will be used for secrets management later), validate against them when performing the operation, and select the appropriate parameters for the operation. An upcoming change will build upon this to provide higher-level operations for secrets storage and key type registries, managing envelope creation and parsing.
    4600299fef0f9eba6b51d0f8cefc777a6c582b4d
    Add base support for keys, including AES and EC keys.
    This introduces a foundation for encryption/signing keys, with specific implementations for AES symmetric keys and EC public/private keys. All keys derive from `BaseKey`, which can be deserialized from bytes or serialized to bytes, can optionally perform encryption/decryption and key wrapping/unwrapping, and has a SHA256 fingerprint and optional key ID. There are subclasses for bytes-based keys and for public/private keys. AES keys are a bytes-based key. They can be generated for a supported key size, can be derived from another key, or derived from a key exchange. They support CFB8, GCM, and GCM-SIV modes. EC public/private key pairs support ECDH key exchange. Keys can be invalidated, which will clear out state on the key. While optional, keys should be used as a context manager, which will automatically invalidate the key once the context manager exits. This is a precaution to keep key material in memory for as little time as possible. All bytes storage for key material is done using `bytearray`s. These can be cleared out, affecting all holders of the state. This can help prevent keys from accidentally staying around in memory long-term (as best as can be prevented in Python). Encryption returns a a representation of the encrypted state, providing a full resulting ciphertext and the component parts. Key wrapping does similar. This will be used to help with secrets storage and defining envelopes (in a future change). All crypto operations take an algorithm, which are standardized. Keys can indicate their support for different algorithms (which will be used for secrets management later), validate against them when performing the operation, and select the appropriate parameters for the operation. An upcoming change will build upon this to provide higher-level operations for secrets storage and key type registries, managing envelope creation and parsing.
    24628ad7fee460bf2460c6b1d8bbbc7ea11f979c

    Checks run (2 succeeded)

    flake8 passed.
    JSHint passed.
    david
    1. 
        
    2. cryptozoology/keys/aes.py (Diff revisions 3 - 5)
       
       
       
       
      Show all issues

      This should fit on one line.

    3. cryptozoology/keys/base.py (Diff revisions 3 - 5)
       
       
       
      Show all issues

      Can we wrap as:

      from cryptozoology.utils.invalidation import (
          InvalidatableMixin,
          SensitiveBytes,
      )
      
    4.