Cryptography
SuperJ ships a batteries-included crypto stack: hashing, digital signatures, key exchange, symmetric encryption, encoding, and web-authentication primitives — all in the SDK, compiled into a single native binary.
The primitives you use most (SHA-256, Ed25519, secp256k1, P-256, BLS12-381, AES-128-GCM, X25519, BLAKE3, Keccak-256, RIPEMD-160) are vendored or implemented directly in the runtime — no system library needed. Two primitives (SHA-512/SHA-384 and P-384 ECDSA) use system OpenSSL because its hand-optimized assembly is faster than portable C; OpenSSL is already a dependency of the installed toolchain, so this adds no new requirement.
Hashing — sj.util.Hash
import sj.util.Hash;
byte[] data = "hello".getBytes();
byte[] out32 = new byte[32];
Hash.sha256(data, 0, data.length, out32); // 32-byte SHA-256
byte[] out48 = new byte[48];
Hash.sha384(data, 0, data.length, out48); // 48-byte SHA-384
byte[] out20 = new byte[20];
Hash.ripemd160(data, 0, data.length, out20); // 20-byte RIPEMD-160
byte[] k = new byte[32];
Hash.keccak256(data, 0, data.length, k); // 32-byte Keccak-256
byte[] b3 = Hash.blake3(data); // 32-byte BLAKE3
byte[] xof = Hash.blake3Xof(data, 64); // 64-byte XOF
| Algorithm | Digest size | Method | Notes |
|---|---|---|---|
| SHA-256 | 32 B | sha256 | hardware-accelerated (SHA-NI / ARM NEON) |
| SHA-512 | 64 B | sha512 | portable C |
| SHA-384 | 48 B | sha384 | system OpenSSL |
| SHA-1 | 20 B | sha1 | portable C |
| RIPEMD-160 | 20 B | ripemd160 | portable C; Bitcoin HASH160 |
| Keccak-256 | 32 B | keccak256 | Ethereum |
| BLAKE3 | 32 B (XOF: any) | blake3 / blake3Xof | — |
SHA-256 also has a multi-buffer path (sha256Batch) for hashing N equal-length messages at once.
Non-crypto hashes (SipHash, xxHash, MurmurHash2) are in the same class for convenience.
Digital signatures
Ed25519 — sj.crypto.Ed25519
import sj.crypto.Ed25519;
byte[] seed = new byte[32];
// fill seed from a CSPRNG
byte[] pub = Ed25519.derivePublic(seed); // 32 bytes
byte[] sig = Ed25519.sign(seed, message); // 64 bytes
boolean ok = Ed25519.verify(sig, pub, message);
boolean valid = Ed25519.isValidPublicKey(pub);
secp256k1 — sj.crypto.Secp256k1
ECDSA for Ethereum and Bitcoin. Compact 64-byte r‖s signatures, low-S normalized. Includes public-key recovery and DER conversion.
import sj.crypto.Secp256k1;
byte[] priv = new byte[32];
byte[] pub = new byte[64];
Secp256k1.generateKeyPair(priv, pub);
byte[] sig = new byte[64];
Secp256k1.signRaw(hash, priv, sig);
boolean ok = Secp256k1.verifyRaw(hash, pub, sig);
byte[] recovered = new byte[64];
Secp256k1.recover(hash, sig, 0, recovered); // recId 0 or 1
secp384r1 (P-384) — sj.crypto.Secp384r1
ECDSA on P-384 with SHA-384. AWS ES384 / IoT / KMS. 96-byte raw signatures.
import sj.crypto.Secp384r1;
import sj.util.Hash;
byte[] priv = new byte[48];
byte[] pub = new byte[97];
Secp384r1.derivePublic(priv, pub); // 04 || X || Y
byte[] hash = new byte[48];
Hash.sha384(message, 0, message.length, hash);
byte[] sig = new byte[96];
Secp384r1.sign(hash, priv, sig);
boolean ok = Secp384r1.verify(sig, pub, hash);
P-256 — sj.crypto.Crypto
ECDSA on P-256. 64-byte compact r‖s of SHA-256(content). Used for ACME JWS and TLS server auth.
import sj.crypto.Crypto;
byte[] priv = new byte[32];
byte[] pub = new byte[64];
Crypto.ecdsaP256Keygen(priv, pub);
byte[] sig = new byte[64];
Crypto.ecdsaP256SignRaw(priv, 0, content, 0, content.length, sig, 0);
boolean ok = Crypto.ecdsaP256VerifyRaw(pub, 0, content, 0, content.length, sig, 0) == 1;
BLS12-381 — sj.crypto.Bls
Aggregate signatures for consensus (HotStuff QCs). G1 signatures (48 bytes), G2 public keys (96 bytes), proof-of-possession scheme.
import sj.crypto.Bls;
byte[] sk = new byte[32];
byte[] pk = new byte[96];
Bls.blsKeyGen(sk, pk);
byte[] sig = new byte[48];
Bls.blsSign(sk, message, 0, message.length, sig);
boolean ok = Bls.blsVerify(pk, message, 0, message.length, sig);
boolean aggOk = Bls.blsFastAggregateVerify(pubkeys, n, message, aggSig);
Key exchange and symmetric crypto — sj.crypto.Crypto
X25519 (ECDH)
byte[] priv = new byte[32];
byte[] pub = new byte[32];
Crypto.x25519Keygen(priv, pub);
byte[] shared = new byte[32];
Crypto.x25519Shared(priv, 0, peerPub, 0, shared, 0);
AES-128-GCM
byte[] out = new byte[pt.length + 16]; // ciphertext + 16-byte tag
int len = Crypto.aes128GcmEncrypt(key, 0, nonce, 0, pt, 0, pt.length,
aad, 0, aad.length, out, 0);
byte[] dec = new byte[pt.length];
int dlen = Crypto.aes128GcmDecrypt(key, 0, nonce, 0, out, 0, len,
aad, 0, aad.length, dec, 0);
// dlen == -1 on tag mismatch
AES-256-CBC
Two forms. Use the padded one unless you know the data has no padding.
aes256CbcDecryptPkcs7/aes256CbcEncryptPkcs7: PKCS#7 padding. This is the content cipher of CMS EnvelopedData (RFC 3565), which is what AWS KMS returns to a Nitro Enclave asCiphertextForRecipient. Unwrap the content key withrsaOaepSha256Decrypt, then decrypt:
``java byte[] out = new byte[ct.length - 1]; // the largest possible plaintext int n = Crypto.aes256CbcDecryptPkcs7(key, 0, iv, 0, ct, 0, ct.length, out, 0, out.length); if (n < 0) { /* rejected -- never say why */ } ``
Every failure returns -1: bad length, bad padding, a short out. The padding is checked in constant time, and a failed call wipes out. As with RSA-OAEP, don't tell failures apart in your own code; a decryptor that reports "bad padding" differently from anything else is a padding oracle. out must hold ctLen - 1 bytes, or the call refuses before decrypting.
aes256CbcDecrypt/aes256CbcEncrypt: raw, no padding. The input must be a multiple of 16 bytes and the output is the same length.
CBC has no integrity check of its own: a wrong key or IV can still decrypt to valid-looking bytes. Where the data needs integrity, AES-128-GCM does both. For a KMS envelope, integrity comes from the key being unwrappable only inside the enclave. Hardware AES (AES-NI / ARMv8 crypto) is used where present, which covers every current x86-64 and ARM64 host.
HMAC-SHA256 and HKDF
byte[] mac = new byte[32];
Crypto.hmacSha256(key, 0, key.length, data, 0, data.length, mac, 0);
byte[] prk = new byte[32];
Crypto.hkdfExtract(salt, 0, salt.length, ikm, 0, ikm.length, prk, 0);
byte[] okm = new byte[64];
Crypto.hkdfExpand(prk, 0, prk.length, info, 0, info.length, 64, okm, 0);
RSA verify
rsaPkcs1VerifySha256/384/512, rsaPssVerifySha256, rsaModexp.
RSA keygen and RSAES-OAEP-SHA-256 decrypt
For receiving a key sealed to your public key: an AWS KMS Decrypt with a Recipient (the Nitro Enclave flow), JWE RSA-OAEP-256, or CMS key transport. Built on OpenSSL.
byte[] priv = new byte[Crypto.RSA_PRIV_MAX]; // PKCS#8 PrivateKeyInfo DER
byte[] pub = new byte[Crypto.RSA_PUB_MAX]; // SubjectPublicKeyInfo DER
int[] lens = new int[2];
Crypto.rsaGenerate(2048, priv, 0, pub, 0, lens); // 0 on success; 2048/3072/4096
byte[] out = new byte[Crypto.RSA_OAEP_SHA256_MAX_PLAINTEXT];
int n = Crypto.rsaOaepSha256Decrypt(priv, 0, lens[0], ct, 0, ct.length, out, 0, out.length);
if (n < 0) { /* rejected -- never say why */ }
Arrays.fill(priv, (byte) 0); // done with the key
- OAEP with SHA-256 (MGF1-SHA-256, empty label) only. That's KMS's
RSAES_OAEP_SHA_256. There is no PKCS#1 v1.5 decryption: that padding is what Bleichenbacher's attack exploits. - Every decrypt failure returns -1: bad padding, a wrong key, a truncated ciphertext, a malformed key, or too small an output buffer. Don't try to tell failures apart in your own code either (different log lines, different responses). Telling a sender which check failed is what turns a decryptor into an oracle.
outmust hold the largest plaintext the key allows (k - 66bytes), or the call refuses before decrypting.rsaOaepSha256Encrypt(pub, …)is the public-key side, for senders and tests. Both directions interoperate withopenssl pkeyutl -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -pkeyopt rsa_mgf1_md:sha256(checked in the gate).
Encoding
Base58 — sj.util.Base58
Bitcoin/Solana alphabet. Raw and Base58Check (4-byte SHA-256 double-hash checksum) variants.
import sj.util.Base58;
String addr = Base58.encodeChecked(pubKey); // Bitcoin address
byte[] decoded = Base58.decodeChecked(addr); // throws on checksum mismatch
String raw = Base58.encode(seed); // Solana address (no checksum)
CBOR — sj.crypto.cbor.Cbor
RFC 7049/8949 codec. All core types: unsigned/negative int, byte string, text string, array, map, tagged, bool, null.
import sj.crypto.cbor.Cbor;
import sj.crypto.cbor.CborValue;
CborValue val = Cbor.map(
new CborValue[]{ Cbor.uint(1), Cbor.uint(2) },
new CborValue[]{ Cbor.tstr("hello"), Cbor.bstr(new byte[]{0x42}) }
);
byte[] encoded = Cbor.encode(val);
CborValue decoded = Cbor.decode(encoded);
COSE_Sign1 — sj.crypto.cose.CoseSign1
RFC 8152 §4.2. Single-signer COSE signature. Two backends: EdDSA (Ed25519) and ES384 (P-384 + SHA-384).
import sj.crypto.cose.CoseSign1;
byte[] cose = CoseSign1.sign(CoseSign1.ALG_EDDSA, kid, seed, payload, externalAad);
boolean ok = CoseSign1.verify(cose, publicKey, externalAad);
Ethereum
EIP-712 — sj.crypto.Eip712
Typed-data hashing. typeHash, hashStruct, hashEip712Message (the final keccak256(0x1901 || domain || struct) that is signed).
EthereumAddress — sj.crypto.EthereumAddress
import sj.crypto.EthereumAddress;
byte[] addr = new byte[20];
EthereumAddress.derive(pubKeyXY, addr);
String hex = EthereumAddress.toChecksummedHex(addr); // "0x..." (EIP-55)
Web authentication
JWS — sj.crypto.Jws
JSON Web Signature (RFC 7515) ES256. jwkP256, signEs256, verifyEs256.
CSR — sj.crypto.Csr
PKCS#10 CSR builder for ACME.
import sj.crypto.Csr;
String csr = Csr.buildP256("example.com", priv, 0, pub, 0);
X.509 — sj.crypto.X509
DER field extraction: validity (notBeforeUnix / notAfterUnix), SPKI keys (spkiEcPoint, spkiRsaModulus), SAN hostname matching (matchesHost), signature type predicates, CA flag, DER→raw signature conversion.
PEM — sj.crypto.Pem
RFC 7468 PEM parse/encode. parse, encode, certDer, certBundle.
TLS 1.3 — sj.crypto.TlsEngine*
Full TLS 1.3 engine (TLS_AES_128_GCM_SHA256; X25519 or secp256r1 key exchange; peer authentication is ECDSA-P256 or RSA — the ClientHello's signature_algorithms offers only ecdsa_secp256r1_sha256, rsa_pss_rsae_sha256 and rsa_pkcs1_sha256, so a P-384 leaf key cannot authenticate the peer; P-384 is accepted only as a chain-link issuer). Wired into NioAdapter.openTls / connectTls — you don't call the engine directly.
Summary table
| Category | Primitive | Class |
|---|---|---|
| Hash | SHA-256, SHA-512, SHA-384, SHA-1, RIPEMD-160, Keccak-256, BLAKE3 | Hash |
| Signatures | Ed25519 | Ed25519 |
| secp256k1 (ECDSA + recover) | Secp256k1 | |
| secp384r1 (P-384) | Secp384r1 | |
| P-256 ECDSA | Crypto | |
| BLS12-381 | Bls | |
| RSA (verify) | Crypto | |
| RSA keygen, OAEP-SHA-256 encrypt/decrypt | Crypto | |
| Key exchange | X25519 | Crypto |
| Symmetric | AES-128-GCM, AES-256-CBC | Crypto |
| HMAC-SHA256, HKDF | Crypto | |
| Encoding | Base58 / Base58Check | Base58 |
| CBOR | cbor.Cbor | |
| COSE_Sign1 | cose.CoseSign1 | |
| Ethereum | EIP-712 | Eip712 |
| Address derivation | EthereumAddress | |
| Web auth | JWS (ES256) | Jws |
| CSR (P-256) | Csr | |
| X.509 | X509 | |
| PEM | Pem | |
| TLS 1.3 | TlsEngine* |