package com.peachpasswords.sync import com.peachpasswords.vault.VaultCrypto import org.bouncycastle.crypto.digests.Blake3Digest import org.bouncycastle.crypto.params.X25519PrivateKeyParameters import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters data class SyncIdentityKeypair( val publicKey: ByteArray, val secretKey: ByteArray, ) data class SyncSigningKeypair( val privateKey: Ed25519PrivateKeyParameters, val publicKeyRaw: ByteArray, ) /** Random per-installation credentials; private material is stored only in the local vault. */ data class LocalIdentityCredentials( val deviceId: String, val x25519PrivateKey: ByteArray, val x25519PublicKey: ByteArray, val ed25519Seed: ByteArray, val ed25519PublicKey: ByteArray, ) fun randomLocalIdentityCredentials(deviceId: String, random: java.security.SecureRandom = java.security.SecureRandom()): LocalIdentityCredentials { require(deviceId.isNotBlank()) val x = ByteArray(32).also(random::nextBytes) val e = ByteArray(32).also(random::nextBytes) val xPrivate = X25519PrivateKeyParameters(x, 0) val ePrivate = Ed25519PrivateKeyParameters(e, 0) return LocalIdentityCredentials(deviceId, x, xPrivate.generatePublicKey().encoded, e, ePrivate.generatePublicKey().encoded) } fun localSyncKeypair(credentials: LocalIdentityCredentials): SyncIdentityKeypair = SyncIdentityKeypair(credentials.x25519PublicKey.copyOf(), credentials.x25519PrivateKey.copyOf()) fun localSigningKeypair(credentials: LocalIdentityCredentials): SyncSigningKeypair = SyncSigningKeypair(Ed25519PrivateKeyParameters(credentials.ed25519Seed, 0), credentials.ed25519PublicKey.copyOf()) private val VAULT_ID_INFO = "peach-vault-id".encodeToByteArray() fun computeVaultId(masterKey: ByteArray): String { require(masterKey.isNotEmpty()) { "masterKey must not be empty" } val buf = masterKey + VAULT_ID_INFO val digest = Blake3Digest(256) digest.update(buf, 0, buf.size) val out = ByteArray(32) return try { digest.doFinal(out, 0) out.joinToString("") { "%02x".format(it) } } finally { VaultCrypto.zero(out) VaultCrypto.zero(buf) } } fun verifyEd25519(publicKeyRaw: ByteArray, message: ByteArray, signature: ByteArray): Boolean { return try { if (publicKeyRaw.size != 32 || signature.size != 64) return false val publicKey = org.bouncycastle.crypto.params.Ed25519PublicKeyParameters(publicKeyRaw, 0) val verifier = org.bouncycastle.crypto.signers.Ed25519Signer() verifier.init(false, publicKey) verifier.update(message, 0, message.size) verifier.verifySignature(signature) } catch (_: Exception) { false } } fun signEd25519(privateKey: Ed25519PrivateKeyParameters, message: ByteArray): ByteArray { val signer = org.bouncycastle.crypto.signers.Ed25519Signer() signer.init(true, privateKey) signer.update(message, 0, message.size) return signer.generateSignature() } fun pubkeyToHex(pk: ByteArray): String = pk.joinToString("") { "%02x".format(it) } fun hexToBytes(hex: String): ByteArray { require(hex.length % 2 == 0) { "hex length must be even" } val out = ByteArray(hex.length / 2) for (i in out.indices) { val byte = hex.substring(i * 2, i * 2 + 2).toInt(16) out[i] = byte.toByte() } return out } fun hexToPubkey(hex: String): ByteArray { require(hex.length == 64) { "public key must be exactly 32 bytes" } return hexToBytes(hex) }