1package com.peachpasswords.sync23import com.peachpasswords.vault.VaultCrypto4import org.bouncycastle.crypto.digests.Blake3Digest5import org.bouncycastle.crypto.params.X25519PrivateKeyParameters6import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters7import org.bouncycastle.crypto.generators.HKDFBytesGenerator8import org.bouncycastle.crypto.params.HKDFParameters910data class SyncIdentityKeypair(11 val publicKey: ByteArray,12 val secretKey: ByteArray,13)1415data class SyncSigningKeypair(16 val privateKey: Ed25519PrivateKeyParameters,17 val publicKeyRaw: ByteArray,18)1920private val SYNC_IDENTITY_SALT = "peach-sync-identity-v1".encodeToByteArray()21private val SYNC_SIGNING_SALT = "peach-sync-signing-v1".encodeToByteArray()22private val VAULT_ID_INFO = "peach-vault-id".encodeToByteArray()2324fun deriveSyncIdentityKeypair(masterKey: ByteArray, deviceId: String): SyncIdentityKeypair {25 require(masterKey.isNotEmpty()) { "masterKey must not be empty" }26 require(deviceId.isNotEmpty()) { "deviceId must not be empty" }27 val info = deviceId.encodeToByteArray()28 val seed = hkdf(masterKey, SYNC_IDENTITY_SALT, info, 32)29 val priv = X25519PrivateKeyParameters(seed, 0)30 return SyncIdentityKeypair(publicKey = priv.generatePublicKey().encoded, secretKey = seed)31}3233fun computeVaultId(masterKey: ByteArray): String {34 require(masterKey.isNotEmpty()) { "masterKey must not be empty" }35 val buf = masterKey + VAULT_ID_INFO36 val digest = Blake3Digest(256)37 digest.update(buf, 0, buf.size)38 val out = ByteArray(32)39 return try {40 digest.doFinal(out, 0)41 out.joinToString("") { "%02x".format(it) }42 } finally {43 VaultCrypto.zero(out)44 VaultCrypto.zero(buf)45 }46}4748fun deriveSyncSigningKeypair(masterKey: ByteArray, deviceId: String): SyncSigningKeypair {49 require(masterKey.isNotEmpty()) { "masterKey must not be empty" }50 require(deviceId.isNotEmpty()) { "deviceId must not be empty" }51 val info = deviceId.encodeToByteArray()52 val seed = hkdf(masterKey, SYNC_SIGNING_SALT, info, 32)53 return try {54 val privateKey = Ed25519PrivateKeyParameters(seed, 0)55 SyncSigningKeypair(privateKey = privateKey, publicKeyRaw = privateKey.generatePublicKey().encoded)56 } finally {57 58 59 VaultCrypto.zero(seed)60 }61}6263fun verifyEd25519(publicKeyRaw: ByteArray, message: ByteArray, signature: ByteArray): Boolean {64 return try {65 if (publicKeyRaw.size != 32 || signature.size != 64) return false66 val publicKey = org.bouncycastle.crypto.params.Ed25519PublicKeyParameters(publicKeyRaw, 0)67 val verifier = org.bouncycastle.crypto.signers.Ed25519Signer()68 verifier.init(false, publicKey)69 verifier.update(message, 0, message.size)70 verifier.verifySignature(signature)71 } catch (_: Exception) {72 false73 }74}7576fun signEd25519(privateKey: Ed25519PrivateKeyParameters, message: ByteArray): ByteArray {77 val signer = org.bouncycastle.crypto.signers.Ed25519Signer()78 signer.init(true, privateKey)79 signer.update(message, 0, message.size)80 return signer.generateSignature()81}8283fun pubkeyToHex(pk: ByteArray): String = pk.joinToString("") { "%02x".format(it) }8485fun hexToBytes(hex: String): ByteArray {86 require(hex.length % 2 == 0) { "hex length must be even" }87 val out = ByteArray(hex.length / 2)88 for (i in out.indices) {89 val byte = hex.substring(i * 2, i * 2 + 2).toInt(16)90 out[i] = byte.toByte()91 }92 return out93}9495fun hexToPubkey(hex: String): ByteArray {96 require(hex.length == 64) { "public key must be exactly 32 bytes" }97 return hexToBytes(hex)98}99100private fun hkdf(ikm: ByteArray, salt: ByteArray, info: ByteArray, length: Int): ByteArray {101 val generator = HKDFBytesGenerator(org.bouncycastle.crypto.digests.SHA256Digest())102 generator.init(HKDFParameters(ikm, salt, info))103 val out = ByteArray(length)104 generator.generateBytes(out, 0, length)105 return out106}