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:
strandint/floatare encrypted as text;boolas"true"/"false".list,tupleandsetare encrypted element by element.- anything else is serialised to JSON first, so nested dataclasses work.
NonestaysNone; a blank word decrypts back toNone.
| 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:
- generate a fresh ephemeral X25519 key pair;
- ECDH against the recipient's static X25519 public key, giving a raw 32-byte secret;
- HKDF-SHA256 with the 12-byte GCM nonce as salt and
b"CryptoPkcs12"as info, producing a 256-bit AES key; - AES-256-GCM encrypts the payload with a 128-bit tag;
- 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
cryptographyreturns a single key pair from a PKCS12 file and exposes no alias lookup. The alias is therefore validated against the store'sfriendlyNameattributes 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.CryptoKeyUtilsstays abstract, as in Java: the concrete key selection lives inCryptoJksUtilsandCryptoPkcs12Utils.- 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/.verifystay explicit calls. - A
@CryptoPubKeyfield 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@CryptoPubKeyEncryptfor outbound fields and@CryptoPubKeyDecryptfor 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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| common_encryption-1.0.0.tar.gz | 47.2 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.