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_keys_off_utf8_bytes_of_the_password() {28        // The 6-codepoint string "test🔑" is 8 UTF-8 bytes (4 ASCII + 4 for the emoji).29        assertEquals(8, nonBmpPassword.toByteArray(Charsets.UTF_8).size)3031        // TODO("Verify against browser KAT"): MN-4 pins deriveKey(nonBmpPassword, fixedSalt)32        // to the exact 32 bytes the browser's argon2id produces for the same input. Until33        // that vector is wired here, assert the UTF-8 contract: changing only the encoding34        // assumption (UTF-16 vs UTF-8) must change the key, so a regression to char[] is35        // observable via the cross-platform vector in MN-4.36        val key = VaultCrypto.deriveKey(nonBmpPassword, fixedSalt)37        val asciiKey = VaultCrypto.deriveKey("test", fixedSalt)38        assertFalse("the emoji must contribute to the derived key", key.contentEquals(asciiKey))39    }40}