1package com.peachpasswords.sync23import com.peachpasswords.vault.VaultCrypto4import org.bouncycastle.crypto.digests.Blake3Digest5import org.bouncycastle.crypto.params.X25519PrivateKeyParameters6import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters78data class SyncIdentityKeypair(9 val publicKey: ByteArray,10 val secretKey: ByteArray,11)1213data class SyncSigningKeypair(14 val privateKey: Ed25519PrivateKeyParameters,15 val publicKeyRaw: ByteArray,16)1718data class LocalIdentityCredentials(19 val deviceId: String,20 val x25519PrivateKey: ByteArray,21 val x25519PublicKey: ByteArray,22 val ed25519Seed: ByteArray,23 val ed25519PublicKey: ByteArray,24)2526fun randomLocalIdentityCredentials(deviceId: String, random: java.security.SecureRandom = java.security.SecureRandom()): LocalIdentityCredentials {27 require(deviceId.isNotBlank())28 val x = ByteArray(32).also(random::nextBytes)29 val e = ByteArray(32).also(random::nextBytes)30 val xPrivate = X25519PrivateKeyParameters(x, 0)31 val ePrivate = Ed25519PrivateKeyParameters(e, 0)32 return LocalIdentityCredentials(deviceId, x, xPrivate.generatePublicKey().encoded, e, ePrivate.generatePublicKey().encoded)33}3435fun localSyncKeypair(credentials: LocalIdentityCredentials): SyncIdentityKeypair =36 SyncIdentityKeypair(credentials.x25519PublicKey.copyOf(), credentials.x25519PrivateKey.copyOf())3738fun localSigningKeypair(credentials: LocalIdentityCredentials): SyncSigningKeypair =39 SyncSigningKeypair(Ed25519PrivateKeyParameters(credentials.ed25519Seed, 0), credentials.ed25519PublicKey.copyOf())4041private val VAULT_ID_INFO = "peach-vault-id".encodeToByteArray()4243fun computeVaultId(masterKey: ByteArray): String {44 require(masterKey.isNotEmpty()) { "masterKey must not be empty" }45 val buf = masterKey + VAULT_ID_INFO46 val digest = Blake3Digest(256)47 digest.update(buf, 0, buf.size)48 val out = ByteArray(32)49 return try {50 digest.doFinal(out, 0)51 out.joinToString("") { "%02x".format(it) }52 } finally {53 VaultCrypto.zero(out)54 VaultCrypto.zero(buf)55 }56}5758fun verifyEd25519(publicKeyRaw: ByteArray, message: ByteArray, signature: ByteArray): Boolean {59 return try {60 if (publicKeyRaw.size != 32 || signature.size != 64) return false61 val publicKey = org.bouncycastle.crypto.params.Ed25519PublicKeyParameters(publicKeyRaw, 0)62 val verifier = org.bouncycastle.crypto.signers.Ed25519Signer()63 verifier.init(false, publicKey)64 verifier.update(message, 0, message.size)65 verifier.verifySignature(signature)66 } catch (_: Exception) {67 false68 }69}7071fun signEd25519(privateKey: Ed25519PrivateKeyParameters, message: ByteArray): ByteArray {72 val signer = org.bouncycastle.crypto.signers.Ed25519Signer()73 signer.init(true, privateKey)74 signer.update(message, 0, message.size)75 return signer.generateSignature()76}7778fun pubkeyToHex(pk: ByteArray): String = pk.joinToString("") { "%02x".format(it) }7980fun hexToBytes(hex: String): ByteArray {81 require(hex.length % 2 == 0) { "hex length must be even" }82 val out = ByteArray(hex.length / 2)83 for (i in out.indices) {84 val byte = hex.substring(i * 2, i * 2 + 2).toInt(16)85 out[i] = byte.toByte()86 }87 return out88}8990fun hexToPubkey(hex: String): ByteArray {91 require(hex.length == 64) { "public key must be exactly 32 bytes" }92 return hexToBytes(hex)93}