SyncIdentity.kt

3.7 KB · raw

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}