package org.meshbay.client.keys import org.bouncycastle.crypto.digests.SHA256Digest import org.bouncycastle.crypto.generators.Argon2BytesGenerator import org.bouncycastle.crypto.generators.HKDFBytesGenerator import org.bouncycastle.crypto.params.Argon2Parameters import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters import org.bouncycastle.crypto.params.HKDFParameters import org.bouncycastle.crypto.params.X25519PrivateKeyParameters import java.util.Base64 import javax.crypto.Cipher import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.SecretKeySpec /** * The primitives keyring.js takes from node:crypto and the vendored Argon2, * with the same numbers. keyderive.js (the page), keyring.js (desktop) and this * are one format: a mismatch looks like an account nobody can open, not like * an error. meshbay-hub/tests/vectors/keyring.json holds them together. */ object Kdf { // keyderive.js: the same numbers, or no bundle opens across the clients. const val ARGON2_MEMORY_KIB = 131072 const val ARGON2_PASSES = 3 const val ARGON2_PARALLELISM = 1 const val ARGON2_TAG = 32 private val ED_PKCS8_PREFIX = hex("302e020100300506032b657004220420") private val X_PKCS8_PREFIX = hex("302e020100300506032b656e04220420") // One derivation at a time: 128 MiB each, on a phone. (Two concurrent // lanes=4 derivations deadlock inside OpenSSL on the hub — CLAUDE.md; not // this library, but there is no reason to find out.) @Synchronized fun argon2id(password: String, salt: ByteArray): ByteArray { val params = Argon2Parameters.Builder(Argon2Parameters.ARGON2_id) .withVersion(Argon2Parameters.ARGON2_VERSION_13) .withIterations(ARGON2_PASSES) .withMemoryAsKB(ARGON2_MEMORY_KIB) .withParallelism(ARGON2_PARALLELISM) .withSalt(salt) .build() val gen = Argon2BytesGenerator() gen.init(params) val out = ByteArray(ARGON2_TAG) gen.generateBytes(password.toByteArray(Charsets.UTF_8), out) return out } /** node:crypto hkdfSync('sha256', ikm, , info, 32). */ fun hkdf(ikm: ByteArray, info: String): ByteArray { val gen = HKDFBytesGenerator(SHA256Digest()) gen.init(HKDFParameters(ikm, null, info.toByteArray(Charsets.UTF_8))) val out = ByteArray(32) gen.generateBytes(out, 0, 32) return out } fun sha256(data: ByteArray): ByteArray { val d = SHA256Digest() d.update(data, 0, data.size) val out = ByteArray(32) d.doFinal(out, 0) return out } /** AES-256-GCM, 16-byte tag appended — the layout node:crypto's getAuthTag gives. */ fun gcmSeal(key: ByteArray, nonce: ByteArray, plain: ByteArray, aad: ByteArray?): ByteArray { val c = Cipher.getInstance("AES/GCM/NoPadding") c.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), GCMParameterSpec(128, nonce)) if (aad != null) c.updateAAD(aad) return c.doFinal(plain) } fun gcmOpen(key: ByteArray, nonce: ByteArray, ctAndTag: ByteArray, aad: ByteArray?): ByteArray { val c = Cipher.getInstance("AES/GCM/NoPadding") c.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), GCMParameterSpec(128, nonce)) if (aad != null) c.updateAAD(aad) return c.doFinal(ctAndTag) } // Keys are stored as Node exports them: PKCS#8 DER, RFC 8410, 48 bytes, no // public key attached. Written out by hand because a library's own PKCS#8 // encoder may add the optional public key, and the stored format is one. fun edToPkcs8(k: Ed25519PrivateKeyParameters) = ED_PKCS8_PREFIX + k.encoded fun xToPkcs8(k: X25519PrivateKeyParameters) = X_PKCS8_PREFIX + k.encoded fun edFromPkcs8(der: ByteArray): Ed25519PrivateKeyParameters { require(der.size == 48 && der.copyOfRange(0, 16).contentEquals(ED_PKCS8_PREFIX)) { "not an Ed25519 PKCS#8 key" } return Ed25519PrivateKeyParameters(der, 16) } fun xFromPkcs8(der: ByteArray): X25519PrivateKeyParameters { require(der.size == 48 && der.copyOfRange(0, 16).contentEquals(X_PKCS8_PREFIX)) { "not an X25519 PKCS#8 key" } return X25519PrivateKeyParameters(der, 16) } fun b64(b: ByteArray): String = Base64.getEncoder().encodeToString(b) fun unb64(s: String?): ByteArray = Base64.getDecoder().decode(s ?: "") fun hex(s: String): ByteArray = ByteArray(s.length / 2) { s.substring(2 * it, 2 * it + 2).toInt(16).toByte() } fun toHex(b: ByteArray): String = b.joinToString("") { "%02x".format(it) } }