SyncIdentity.kt

4.0 KB · raw

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        // Ed25519PrivateKeyParameters copies the seed into its own immutable58        // key object; the HKDF output itself is only constructor workspace.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}