VaultCryptoKdfTest.kt

1.9 KB · raw

1package com.peachpasswords.vault23import org.junit.Assert.assertArrayEquals4import org.junit.Assert.assertEquals5import org.junit.Assert.assertFalse6import org.junit.Test78/**9 * MF-4: the Argon2id KDF must hash the password's explicit UTF-8 bytes (matching the10 * browser's `TextEncoder().encode`), not its UTF-16 code units. These pin determinism11 * and the UTF-8 contract for a non-BMP password.12 */13class VaultCryptoKdfTest {1415    // U+1F511 (🔑) is non-BMP — a UTF-16 surrogate pair — exactly the case where a16    // char[]/UTF-16 path could diverge from the browser's UTF-8 encoding.17    private val nonBmpPassword = "test🔑" // "test🔑"18    private val fixedSalt = ByteArray(VaultCrypto.SALT_SIZE) { it.toByte() }1920    @Test fun derive_is_deterministic_for_non_bmp_password() {21        val a = VaultCrypto.deriveKey(nonBmpPassword, fixedSalt)22        val b = VaultCrypto.deriveKey(nonBmpPassword, fixedSalt)23        assertArrayEquals("same password + salt yields the same key", a, b)24        assertEquals(VaultCrypto.ARGON2_HASH_LENGTH, a.size)25    }2627    @Test fun derive_matches_browser_argon2id_known_answer() {28        // Canonical browser vector: src/crypto/argon2id.kat.test.ts (m=65536, t=3, p=4).29        val salt = ByteArray(VaultCrypto.SALT_SIZE) { 0xab.toByte() }30        val key = VaultCrypto.deriveKey("testpassword", salt)31        assertEquals(32            "df187c8d241e8f3fd42810e1379660605aad1119dcb133d8ff916f87160d6a33",33            key.joinToString("") { "%02x".format(it) },34        )35    }3637    @Test fun derive_keys_off_utf8_bytes_of_the_password() {38        // The 6-codepoint string "test🔑" is 8 UTF-8 bytes (4 ASCII + 4 for the emoji).39        assertEquals(8, nonBmpPassword.toByteArray(Charsets.UTF_8).size)4041        val key = VaultCrypto.deriveKey(nonBmpPassword, fixedSalt)42        val asciiKey = VaultCrypto.deriveKey("test", fixedSalt)43        assertFalse("the emoji must contribute to the derived key", key.contentEquals(asciiKey))44    }45}