Home · ← SuperJ Manual EN|中文

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
AlgorithmDigest sizeMethodNotes
SHA-25632 Bsha256hardware-accelerated (SHA-NI / ARM NEON)
SHA-51264 Bsha512portable C
SHA-38448 Bsha384system OpenSSL
SHA-120 Bsha1portable C
RIPEMD-16020 Bripemd160portable C; Bitcoin HASH160
Keccak-25632 Bkeccak256Ethereum
BLAKE332 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.

``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.

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

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

CategoryPrimitiveClass
HashSHA-256, SHA-512, SHA-384, SHA-1, RIPEMD-160, Keccak-256, BLAKE3Hash
SignaturesEd25519Ed25519
secp256k1 (ECDSA + recover)Secp256k1
secp384r1 (P-384)Secp384r1
P-256 ECDSACrypto
BLS12-381Bls
RSA (verify)Crypto
RSA keygen, OAEP-SHA-256 encrypt/decryptCrypto
Key exchangeX25519Crypto
SymmetricAES-128-GCM, AES-256-CBCCrypto
HMAC-SHA256, HKDFCrypto
EncodingBase58 / Base58CheckBase58
CBORcbor.Cbor
COSE_Sign1cose.CoseSign1
EthereumEIP-712Eip712
Address derivationEthereumAddress
Web authJWS (ES256)Jws
CSR (P-256)Csr
X.509X509
PEMPem
TLS 1.3TlsEngine*
SuperJ — manual · generated from cryptography.md at pack time EN|中文