package com.peachpasswords.vault import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Test /** * MF-4: the Argon2id KDF must hash the password's explicit UTF-8 bytes (matching the * browser's `TextEncoder().encode`), not its UTF-16 code units. These pin determinism * and the UTF-8 contract for a non-BMP password. */ class VaultCryptoKdfTest { // U+1F511 (๐Ÿ”‘) is non-BMP โ€” a UTF-16 surrogate pair โ€” exactly the case where a // char[]/UTF-16 path could diverge from the browser's UTF-8 encoding. private val nonBmpPassword = "test๐Ÿ”‘" // "test๐Ÿ”‘" private val fixedSalt = ByteArray(VaultCrypto.SALT_SIZE) { it.toByte() } @Test fun derive_is_deterministic_for_non_bmp_password() { val a = VaultCrypto.deriveKey(nonBmpPassword, fixedSalt) val b = VaultCrypto.deriveKey(nonBmpPassword, fixedSalt) assertArrayEquals("same password + salt yields the same key", a, b) assertEquals(VaultCrypto.ARGON2_HASH_LENGTH, a.size) } @Test fun derive_matches_browser_argon2id_known_answer() { // Canonical browser vector: src/crypto/argon2id.kat.test.ts (m=65536, t=3, p=4). val salt = ByteArray(VaultCrypto.SALT_SIZE) { 0xab.toByte() } val key = VaultCrypto.deriveKey("testpassword", salt) assertEquals( "df187c8d241e8f3fd42810e1379660605aad1119dcb133d8ff916f87160d6a33", key.joinToString("") { "%02x".format(it) }, ) } @Test fun derive_keys_off_utf8_bytes_of_the_password() { // The 6-codepoint string "test๐Ÿ”‘" is 8 UTF-8 bytes (4 ASCII + 4 for the emoji). assertEquals(8, nonBmpPassword.toByteArray(Charsets.UTF_8).size) val key = VaultCrypto.deriveKey(nonBmpPassword, fixedSalt) val asciiKey = VaultCrypto.deriveKey("test", fixedSalt) assertFalse("the emoji must contribute to the derived key", key.contentEquals(asciiKey)) } }