Skip to main content

common-encryption

Python port of the Java module common-encryption: field-level encryption and decryption that is transparent to the caller, plus the underlying utility classes.

Interoperability with the Java original is a hard requirement, not a best effort. Payloads produced by either side decrypt on the other, byte for byte, and the test suite locks this down against vectors generated by the Java code.

Java Python
com.bld.crypto.pkcs12.CryptoPkcs12Utils common_encryption.CryptoPkcs12Utils
com.bld.crypto.jks.CryptoJksUtils common_encryption.CryptoJksUtils
@CryptoPkcs12 / @CryptoJks Annotated[T, CryptoPkcs12(...)]
@ConfigurationProperties wiring CryptoConfig.from_properties() / from_env()

Install

pip install common-encryption

Requires Python 3.10+ and cryptography>=43 (the first release that can read an X25519 key from a PKCS12 keystore).

Transparent field encryption

Java marks a field with an annotation and lets Jackson encrypt it on the way out and decrypt it on the way in. Python has no implicit hook, so the same contract is expressed with typing.Annotated metadata plus to_json / from_json.

from dataclasses import dataclass
from typing import Annotated

from common_encryption import CryptoPkcs12, CryptoConfig, from_json, to_json

CryptoConfig.from_properties({
    "com.bld": {"crypto": {"pkcs12": {
        "file": "/etc/keystore/app.p12",
        "password": "changeit",
        "alias": "mase-x25519",
        "instance-jks": "PKCS12",
    }}}
})

@dataclass
class IdUserFilter:
    id_user: Annotated[int | None, CryptoPkcs12("idUser", url=True)] = None
    roles: Annotated[list[int] | None, CryptoPkcs12("roles")] = None

body = to_json(IdUserFilter(1, [10, 20]))
# {"id_user":"TUNvd0JR...","roles":["MCowBQYD...","MCowBQYD..."]}

assert from_json(IdUserFilter, body) == IdUserFilter(1, [10, 20])

Field values are rendered the way the Java serializer renders them, and the same type comes back:

  • str and int/float are encrypted as text; bool as "true" / "false".
  • list, tuple and set are encrypted element by element.
  • anything else is serialised to JSON first, so nested dataclasses work.
  • None stays None; a blank word decrypts back to None.
Annotation Module What the token is
CryptoPkcs12("idUser", url=False) PKCS12 hybrid encrypted, bound to idUser
CryptoJks("name") JKS RSA encrypted, bound to name
CryptoAes("k1") AES AES encrypted, not bound
CryptoHmac("idUser") HMAC signed envelope, bound to idUser
CryptoPubKey("mase") public key RSA encrypted, not bound
CryptoPubKeyEncrypt("mase") / CryptoPubKeyDecrypt("mase") public key one direction only

The value argument is a logical key identifier that gets sealed into the token and checked on decryption, so a token cannot be moved from one field to another. AES and public-key tokens carry no envelope, exactly as in Java.

Configuration

CryptoConfig.from_properties() accepts every shape Spring produces, so the parsed application.yml can be handed over verbatim:

CryptoConfig.from_properties(yaml.safe_load(open("application.yml")))
CryptoConfig.from_properties({"com.bld.crypto.pkcs12.file": "app.p12", ...})
Property Environment variable
com.bld.crypto.pkcs12.file BLD_CRYPTO_PKCS12_FILE
com.bld.crypto.pkcs12.password BLD_CRYPTO_PKCS12_PASSWORD
com.bld.crypto.pkcs12.alias BLD_CRYPTO_PKCS12_ALIAS
com.bld.crypto.pkcs12.instance-jks BLD_CRYPTO_PKCS12_INSTANCE_JKS
com.bld.crypto.jks.* BLD_CRYPTO_JKS_*
com.bld.crypto.signature.* BLD_CRYPTO_SIGNATURE_*
com.bld.crypto.hmac.secret BLD_CRYPTO_HMAC_SECRET
com.bld.crypto.hmac.algorithm BLD_CRYPTO_HMAC_ALGORITHM
com.bld.crypto.hmac.salt BLD_CRYPTO_HMAC_SALT
com.bld.crypto.hmac.key-length BLD_CRYPTO_HMAC_KEY_LENGTH

A section without file is skipped rather than raising, mirroring Java's @ConditionalOnProperty. Property names are read relaxed, so keyLength, key-length and key_length are the same field.

The AES keys and the public keys are named maps in Java, so they are configured through properties or YAML rather than through a single environment variable:

com:
  bld:
    crypto:
      aes:
        keys:
          k1:
            password: 0123456789abcdef   # raw key, 16/24/32 bytes
      public-key:
        keys:
          mase: classpath:cert.pem      # PEM certificate or Base64 SubjectPublicKeyInfo

Setting password together with salt and keyLength switches to PBKDF2: HMAC-SHA1 with 65536 iterations for AES, HMAC-SHA256 for HMAC, as in AesConfiguration and HmacConfiguration.

Utility classes

from common_encryption import CryptoConfig, CryptoType

config = CryptoConfig.from_env()
pkcs12, jks = config.pkcs12, config.jks

pkcs12.encrypt_value("plain")                     # Base64(spki).Base64(nonce).Base64(ct+tag)
pkcs12.decrypt_uri(pkcs12.encrypt_uri("plain"))   # URL-safe variant
pkcs12.encrypt_key_value("email", "a@b.it")       # token bound to a field name
pkcs12.decrypt_object(pkcs12.encrypt_object({"id": 1}))

jks.encrypt_value("plain")                        # RSA, encrypt with the public key
jks.decrypt_value(token)                          # decrypt with the private key
jks.decrypt_value(jks.encrypt_value("x", CryptoType.private_key), CryptoType.public_key)

Both expose the same shape: encrypt_value / decrypt_value, encrypt_uri / decrypt_uri, encrypt_key_value / decrypt_key_value, encrypt_key_uri / decrypt_key_uri, and the *_object variants that round-trip JSON. CryptoJksUtils takes an explicit CryptoType for each direction, since JCA decides the operation from the key it is handed.

AES, HMAC, public keys and signatures

from common_encryption import (
    AesSecrets, CryptoAesUtils, CryptoHmacUtils, CryptoMapPublicKeyUtils,
    HmacSecret, SecretSpec, SignatureKeyUtils,
)

aes = CryptoAesUtils(AesSecrets({"k1": SecretSpec("0123456789abcdef")}))
aes.decrypt_value(aes.encrypt_value("plain", "k1"), "k1")

hmac = CryptoHmacUtils(HmacSecret(b"0123456789abcdef", "HmacSHA256"))
hmac.verify_value(hmac.sign_value("idUser", "plain"), "idUser")   # integrity, not secrecy

keys = CryptoMapPublicKeyUtils()          # sender side: public keys only
token = keys.encrypt_value("plain", "mase")

CryptoPublicKeyUtils holds both halves of one key pair and is the recipient side; CryptoMapPublicKeyUtils holds named public keys and is the sender side, which is what com.bld.crypto.public-key.keys builds.

SignatureKeyUtils signs and verifies with a keystore key:

signature = SignatureKeyUtils(jks_key, "SHA256withRSA")
signature.verify(signature.sign("payload"), "payload")

How each scheme works

PKCS12 is hybrid, per operation:

  1. generate a fresh ephemeral X25519 key pair;
  2. ECDH against the recipient's static X25519 public key, giving a raw 32-byte secret;
  3. HKDF-SHA256 with the 12-byte GCM nonce as salt and b"CryptoPkcs12" as info, producing a 256-bit AES key;
  4. AES-256-GCM encrypts the payload with a 128-bit tag;
  5. emit Base64(spki).Base64(nonce).Base64(ciphertext + tag).

The keystore must hold an XDH/X25519 key pair, with or without a certificate:

openssl genpkey -algorithm X25519 -out private.key
openssl pkcs12 -export -inkey private.key -out app.p12 -passout pass:changeit

JKS is RSA with PKCS#1 v1.5 padding, matching Cipher.getInstance("RSA"). ENCRYPT_MODE with an RSAPrivateKey runs the private operation over type 1 padding, and DECRYPT_MODE with an RSAPublicKey reverses it, which is what the CryptoType.private_key / CryptoType.public_key pair is for.

JKS files are parsed directly in Python, including the protected private key blob, so no external keytool call is needed at runtime.

AES uses Cipher.getInstance("AES"), whose JCA default is AES/ECB with PKCS5 padding. The key is the raw secret when it is already 16, 24 or 32 bytes, otherwise PBKDF2. ECB only repeats the block, which is why the annotations are for values that already need obscuring rather than for secrets you want hidden.

HMAC is integrity, not secrecy. The envelope {"key":"<field>","value":"<text>"} is signed and the token is Base64(envelope).Base64(mac), so the value travels in the clear and a token cannot be moved between fields. url=True switches both halves to the URL-safe alphabet.

Public keys are plain RSA with the same PKCS#1 v1.5 padding as JKS, with no envelope: the key name comes from the annotation and is not sealed into the token.

Signatures use Signature.getInstance(...), so SHA256withRSA, SHA512withECDSA and the other JCA names all work.

Tests

pip install -e ".[test]"
pytest

tests/fixtures/ holds keystores produced by keytool and openssl, plus java_vectors.json, a set of payloads encrypted by the Java implementation. To regenerate the vectors after a protocol change:

java -Dvec.pkcs12File=tests/fixtures/x25519.p12 -Dvec.pkcs12Password=pkcs12pass \
     -Dvec.pkcs12Alias=mase-x25519 -Dvec.jksFile=tests/fixtures/mase.jks \
     -Dvec.jksPassword=jkspass -Dvec.jksAlias=mase-rsa -Dvec.jksType=JKS \
     -Dvec.aesKey=0123456789abcdef -Dvec.hmacKey=0123456789abcdef \
     tests/interop/JavaVectors.java gen tests/fixtures/java_vectors.json

gen writes payloads for Python to decrypt; chk takes a JSON object of tokens and verifies them in Java, which covers the reverse direction.

Known limitations

  • cryptography returns a single key pair from a PKCS12 file and exposes no alias lookup. The alias is therefore validated against the store's friendlyName attributes when present, but a PKCS12 holding several keys is limited to its first entry. Use one keystore per key, or a JKS, when you need multiple aliases.
  • CryptoKeyUtils stays abstract, as in Java: the concrete key selection lives in CryptoJksUtils and CryptoPkcs12Utils.
  • There is no signature field annotation, in Python or in Java. The token replaces the value, so the text needed to verify it is gone by the time the field is read back; SignatureKeyUtils.sign / .verify stay explicit calls.
  • A @CryptoPubKey field cannot be read back by the service that wrote it: the map utilities hold public keys only, and a token made with a public key needs the private one. Use @CryptoPubKeyEncrypt for outbound fields and @CryptoPubKeyDecrypt for tokens that arrive signed by the key owner.

Metadata

Release files for common-encryption 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for common-encryption 1.0.0
File Size Uploaded Provenance
common_encryption-1.0.0.tar.gz 47.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for common-encryption 1.0.0
File Interpreter ABI Platform
common_encryption-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 91.2 kB

Release files / common_encryption-1.0.0.tar.gz

Download URL common_encryption-1.0.0.tar.gz
Size 47.2 kB
Tags Source
SHA-256 checksum
How to use checksums
9e4d8602b4eb0e0332cae5b83dd8ae74a70cce4804563dcc5059fdce7fcbf3b0
BLAKE2b-256 checksum
How to use checksums
46071fa8e6e85b515df331af8205b4aca0f62fc68446a9ee15769a0049206a31
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.3

Release files / common_encryption-1.0.0-py3-none-any.whl

Download URL common_encryption-1.0.0-py3-none-any.whl
Size 44.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
01699f7c22680365b17e985cf00dd4e5276f830b4557c378375ab8dcc89d9b19
BLAKE2b-256 checksum
How to use checksums
0fbf74cd0ec92fb61275d8a4bc4cb9e1cd50cf5d49cb636081ad38be036dd800
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.3

Security for common-encryption 1.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page