1package com.peachpasswords23import com.peachpasswords.sync.DeviceManifestBody4import com.peachpasswords.sync.DeviceRevocation5import com.peachpasswords.sync.ManifestDeviceEntry6import com.peachpasswords.sync.SASDerivation7import com.peachpasswords.sync.canonicalDeviceManifestBody8import com.peachpasswords.sync.computeVaultId9import com.peachpasswords.sync.noiseNKHkdf2ForTest10import com.peachpasswords.sync.noiseNKInitHashForTest11import com.peachpasswords.vault.VaultCrypto12import org.junit.Assert.assertArrayEquals13import org.junit.Assert.assertEquals14import org.junit.Assert.assertFalse15import org.junit.Assert.assertNotEquals16import org.junit.Test17import java.text.Normalizer18192021222324252627class CrossPlatformKATTest {2829 private fun hex(b: ByteArray) = b.joinToString("") { "%02x".format(it) }3031 32 private val argonSalt = ByteArray(VaultCrypto.SALT_SIZE) { it.toByte() }3334 @Test fun argon2id_non_bmp_password_vector() {35 36 37 38 val key = VaultCrypto.deriveKey("test🔑", argonSalt)39 assertEquals("a519dd4e695d25c6cd69a2f89d51ddcabd952c979f5714ab29fb77f6d1ad3728", hex(key))40 }4142 @Test fun nfc_normalization_makes_composed_and_decomposed_match() {43 44 45 val nfd = Normalizer.normalize("café", Normalizer.Form.NFD)46 val nfc = Normalizer.normalize("café", Normalizer.Form.NFC)47 assertNotEquals(nfd, nfc)48 assertArrayEquals(VaultCrypto.deriveKey(nfd, argonSalt), VaultCrypto.deriveKey(nfc, argonSalt))49 }5051 @Test fun deriveKeyRaw_does_not_normalize() {52 val nfd = Normalizer.normalize("café", Normalizer.Form.NFD)53 val nfc = Normalizer.normalize("café", Normalizer.Form.NFC)54 assertFalse(55 VaultCrypto.deriveKeyRaw(nfd, argonSalt).contentEquals(VaultCrypto.deriveKeyRaw(nfc, argonSalt)),56 )57 }5859 @Test fun compute_vault_id_vector() {60 61 val masterKey = ByteArray(32) { it.toByte() }62 assertEquals("ae0ec54041c9ba3e20c0c2d33bdeb2552f3b9a3fb4be3286ec0ef0a61be2fc9c", computeVaultId(masterKey))63 }6465 @Test fun device_manifest_v3_canonical_form_vector() {66 67 68 69 70 val body = DeviceManifestBody(71 manifestVersion = 3,72 issuerDeviceId = "issuer-1",73 issuedAt = 1_700_000_000_000L,74 devices = listOf(75 ManifestDeviceEntry(76 deviceId = "peer-1",77 deviceName = "Peer One",78 deviceType = "android",79 longTermPublicKeyHex = "aa".repeat(32),80 signingPublicKeyHex = "bb".repeat(32),81 wsPort = 49142,82 relayUrl = "wss://relay.peachpasswords.com",83 pairedAt = 1_700_000_000_001L,84 ),85 ),86 revoked = listOf(87 DeviceRevocation(88 deviceId = "gone-1",89 longTermPublicKeyHex = "cc".repeat(32),90 revokedAt = 1_700_000_000_002L,91 revokedByDeviceId = "issuer-1",92 ),93 ),94 )95 val expected = "{\"devices\":[{\"deviceId\":\"peer-1\",\"deviceName\":\"Peer One\"," +96 "\"deviceType\":\"android\",\"longTermPublicKeyHex\":\"" + "aa".repeat(32) + "\"," +97 "\"pairedAt\":1700000000001,\"relayUrl\":\"wss://relay.peachpasswords.com\"," +98 "\"signingPublicKeyHex\":\"" + "bb".repeat(32) + "\"," +99 "\"wsPort\":49142}],\"issuedAt\":1700000000000,\"issuerDeviceId\":\"issuer-1\"," +100 "\"manifestVersion\":3,\"revoked\":[{\"deviceId\":\"gone-1\"," +101 "\"longTermPublicKeyHex\":\"" + "cc".repeat(32) + "\",\"revokedAt\":1700000000002," +102 "\"revokedByDeviceId\":\"issuer-1\"}]}"103 assertEquals(expected, canonicalDeviceManifestBody(body))104 }105106 @Test fun sas_derivation_vector() {107 108 val handshakeHash = byteArrayOf(0x12, 0x34, 0x56, 0x78) + ByteArray(28)109 assertEquals("67721216", SASDerivation.derive(handshakeHash))110 }111112 113 114 115 116 117 @Test fun noise_nk_init_hash_is_blake2s_vector() {118 assertEquals(119 "e01688d376ef280a27a8af5c559024a02017585b3763763e4f3c7118a979b09e",120 hex(noiseNKInitHashForTest()),121 )122 }123124 @Test fun noise_hkdf2_blake2s_vector() {125 126 val ck = ByteArray(32) { it.toByte() }127 val ikm = ByteArray(32) { (it + 32).toByte() }128 val (nk1, nk2) = noiseNKHkdf2ForTest(ck, ikm)129 assertEquals("6a96444e20e8d4c1cee974416acae1c10b3c92886010e54ed94dafb2c3b80ea0", hex(nk1))130 assertEquals("57af120b0de7acbe7907ec149c5ae870a2dbb74232b65777ba4123f1f7f888f5", hex(nk2))131 }132133 @Test fun device_manifest_v3_canonical_form_omits_null_ws_port_and_relay_url() {134 135 136 137 138 139 140 val body = DeviceManifestBody(141 manifestVersion = 3,142 issuerDeviceId = "issuer-1",143 issuedAt = 1_700_000_000_000L,144 devices = listOf(145 ManifestDeviceEntry(146 deviceId = "peer-1",147 deviceName = "Peer One",148 deviceType = "extension",149 longTermPublicKeyHex = "aa".repeat(32),150 signingPublicKeyHex = "bb".repeat(32),151 wsPort = null,152 relayUrl = null,153 pairedAt = 1_700_000_000_001L,154 ),155 ),156 revoked = listOf(157 DeviceRevocation(158 deviceId = "gone-1",159 longTermPublicKeyHex = "cc".repeat(32),160 revokedAt = 1_700_000_000_002L,161 revokedByDeviceId = "issuer-1",162 ),163 ),164 )165 val canonical = canonicalDeviceManifestBody(body)166 167 assertFalse("wsPort must be omitted when null but got: $canonical", canonical.contains("wsPort"))168 assertFalse("relayUrl must be omitted when null but got: $canonical", canonical.contains("relayUrl"))169 170 val expected = "{\"devices\":[{\"deviceId\":\"peer-1\",\"deviceName\":\"Peer One\"," +171 "\"deviceType\":\"extension\",\"longTermPublicKeyHex\":\"" + "aa".repeat(32) + "\"," +172 "\"pairedAt\":1700000000001," +173 "\"signingPublicKeyHex\":\"" + "bb".repeat(32) + "\"}]," +174 "\"issuedAt\":1700000000000,\"issuerDeviceId\":\"issuer-1\"," +175 "\"manifestVersion\":3," +176 "\"revoked\":[{\"deviceId\":\"gone-1\"," +177 "\"longTermPublicKeyHex\":\"" + "cc".repeat(32) + "\",\"revokedAt\":1700000000002," +178 "\"revokedByDeviceId\":\"issuer-1\"}]}"179 assertEquals(expected, canonical)180 }181}