1package com.peachpasswords.sync23import org.bouncycastle.crypto.digests.Blake3Digest4import org.bouncycastle.crypto.params.X25519PrivateKeyParameters5import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters6import org.bouncycastle.crypto.generators.HKDFBytesGenerator7import org.bouncycastle.crypto.params.HKDFParameters89data class SyncIdentityKeypair(10 val publicKey: ByteArray,11 val secretKey: ByteArray,12)1314data class SyncSigningKeypair(15 val privateKey: Ed25519PrivateKeyParameters,16 val publicKeyRaw: ByteArray,17)1819private val SYNC_IDENTITY_SALT = "peach-sync-identity-v1".encodeToByteArray()20private val SYNC_SIGNING_SALT = "peach-sync-signing-v1".encodeToByteArray()21private val VAULT_ID_INFO = "peach-vault-id".encodeToByteArray()2223fun deriveSyncIdentityKeypair(masterKey: ByteArray, deviceId: String): SyncIdentityKeypair {24 require(masterKey.isNotEmpty()) { "masterKey must not be empty" }25 require(deviceId.isNotEmpty()) { "deviceId must not be empty" }26 val info = deviceId.encodeToByteArray()27 val seed = hkdf(masterKey, SYNC_IDENTITY_SALT, info, 32)28 val priv = X25519PrivateKeyParameters(seed, 0)29 return SyncIdentityKeypair(publicKey = priv.generatePublicKey().encoded, secretKey = seed)30}3132fun computeVaultId(masterKey: ByteArray): String {33 require(masterKey.isNotEmpty()) { "masterKey must not be empty" }34 val buf = masterKey + VAULT_ID_INFO35 val digest = Blake3Digest(256)36 digest.update(buf, 0, buf.size)37 val out = ByteArray(32)38 digest.doFinal(out, 0)39 return out.joinToString("") { "%02x".format(it) }40}4142fun deriveSyncSigningKeypair(masterKey: ByteArray, deviceId: String): SyncSigningKeypair {43 require(masterKey.isNotEmpty()) { "masterKey must not be empty" }44 require(deviceId.isNotEmpty()) { "deviceId must not be empty" }45 val info = deviceId.encodeToByteArray()46 val seed = hkdf(masterKey, SYNC_SIGNING_SALT, info, 32)47 val privateKey = Ed25519PrivateKeyParameters(seed, 0)48 return SyncSigningKeypair(privateKey = privateKey, publicKeyRaw = privateKey.generatePublicKey().encoded)49}5051fun verifyEd25519(publicKeyRaw: ByteArray, message: ByteArray, signature: ByteArray): Boolean {52 return try {53 if (publicKeyRaw.size != 32 || signature.size != 64) return false54 val publicKey = org.bouncycastle.crypto.params.Ed25519PublicKeyParameters(publicKeyRaw, 0)55 val verifier = org.bouncycastle.crypto.signers.Ed25519Signer()56 verifier.init(false, publicKey)57 verifier.update(message, 0, message.size)58 verifier.verifySignature(signature)59 } catch (_: Exception) {60 false61 }62}6364fun signEd25519(privateKey: Ed25519PrivateKeyParameters, message: ByteArray): ByteArray {65 val signer = org.bouncycastle.crypto.signers.Ed25519Signer()66 signer.init(true, privateKey)67 signer.update(message, 0, message.size)68 return signer.generateSignature()69}7071fun pubkeyToHex(pk: ByteArray): String = pk.joinToString("") { "%02x".format(it) }7273fun hexToBytes(hex: String): ByteArray {74 require(hex.length % 2 == 0) { "hex length must be even" }75 val out = ByteArray(hex.length / 2)76 for (i in out.indices) {77 val byte = hex.substring(i * 2, i * 2 + 2).toInt(16)78 out[i] = byte.toByte()79 }80 return out81}8283fun hexToPubkey(hex: String): ByteArray {84 require(hex.length == 64) { "public key must be exactly 32 bytes" }85 return hexToBytes(hex)86}8788private fun hkdf(ikm: ByteArray, salt: ByteArray, info: ByteArray, length: Int): ByteArray {89 val generator = HKDFBytesGenerator(org.bouncycastle.crypto.digests.SHA256Digest())90 generator.init(HKDFParameters(ikm, salt, info))91 val out = ByteArray(length)92 generator.generateBytes(out, 0, length)93 return out94}