1package com.peachpasswords.vault23import org.junit.Assert.assertArrayEquals4import org.junit.Assert.assertEquals5import org.junit.Assert.assertFalse6import org.junit.Assert.assertNotEquals7import org.junit.Assert.assertTrue8import org.junit.Test9101112131415class VaultCryptoTest {1617 private val salt = ByteArray(VaultCrypto.SALT_SIZE) { it.toByte() }18 private val salt2 = ByteArray(VaultCrypto.SALT_SIZE) { (it + 1).toByte() }1920 2122 @Test fun argon2_memory_is_64_mib() = assertEquals(65536, VaultCrypto.ARGON2_MEMORY_KB)23 @Test fun argon2_iterations_is_3() = assertEquals(3, VaultCrypto.ARGON2_ITERATIONS)24 @Test fun argon2_parallelism_is_4() = assertEquals(4, VaultCrypto.ARGON2_PARALLELISM)25 @Test fun argon2_output_is_32_bytes() {26 assertEquals(32, VaultCrypto.ARGON2_HASH_LENGTH)27 assertEquals(32, VaultCrypto.deriveKey("pw", salt).size)28 }2930 3132 @Test fun same_password_and_salt_is_deterministic() {33 assertArrayEquals(VaultCrypto.deriveKey("hunter2", salt), VaultCrypto.deriveKey("hunter2", salt))34 }3536 @Test fun different_passwords_derive_different_keys() {37 assertFalse(VaultCrypto.deriveKey("a-password", salt).contentEquals(VaultCrypto.deriveKey("b-password", salt)))38 }3940 @Test fun different_salts_derive_different_keys() {41 assertFalse(VaultCrypto.deriveKey("same", salt).contentEquals(VaultCrypto.deriveKey("same", salt2)))42 }4344 @Test fun empty_password_is_handled() {45 val k = VaultCrypto.deriveKey("", salt)46 assertEquals(32, k.size)47 }4849 @Test fun very_long_password_is_handled() {50 val k = VaultCrypto.deriveKey("x".repeat(2000), salt)51 assertEquals(32, k.size)52 }5354 @Test fun unicode_password_is_handled() {55 assertEquals(32, VaultCrypto.deriveKey("日本語🔑Ωpäss", salt).size)56 }5758 @Test fun nfc_and_nfd_forms_derive_the_same_key() {59 60 val nfc = "café"61 val nfd = "café"62 assertNotEquals("the two forms are distinct strings", nfc, nfd)63 assertArrayEquals(VaultCrypto.deriveKey(nfc, salt), VaultCrypto.deriveKey(nfd, salt))64 }6566 @Test fun raw_derive_differs_from_nfc_for_non_nfc_password() {67 val nfd = "café"68 69 assertFalse(VaultCrypto.deriveKeyRaw(nfd, salt).contentEquals(VaultCrypto.deriveKey(nfd, salt)))70 }7172 @Test fun raw_and_nfc_match_for_already_nfc_password() {73 74 assertArrayEquals(VaultCrypto.deriveKeyRaw("plain-ascii", salt), VaultCrypto.deriveKey("plain-ascii", salt))75 }7677 7879 private val key = VaultCrypto.deriveKey("enc-key", salt)8081 @Test fun encrypt_then_decrypt_round_trips() {82 val pt = "secret message".toByteArray()83 val ct = VaultCrypto.encrypt(pt, key, VaultCrypto.AAD_SLOT_A)84 assertArrayEquals(pt, VaultCrypto.decrypt(ct, key, VaultCrypto.AAD_SLOT_A))85 }8687 @Test fun ciphertext_layout_is_nonce_plus_ct_plus_tag() {88 val pt = "abc".toByteArray()89 val ct = VaultCrypto.encrypt(pt, key, VaultCrypto.AAD_SLOT_A)90 91 assertEquals(VaultCrypto.NONCE_SIZE + pt.size + 16, ct.size)92 }9394 @Test fun fresh_nonce_per_call_so_ciphertexts_differ() {95 val pt = "same plaintext".toByteArray()96 val a = VaultCrypto.encrypt(pt, key, VaultCrypto.AAD_SLOT_A)97 val b = VaultCrypto.encrypt(pt, key, VaultCrypto.AAD_SLOT_A)98 assertFalse("nonces must differ", a.contentEquals(b))99 100 assertArrayEquals(pt, VaultCrypto.decrypt(a, key, VaultCrypto.AAD_SLOT_A))101 assertArrayEquals(pt, VaultCrypto.decrypt(b, key, VaultCrypto.AAD_SLOT_A))102 }103104 @Test fun modifying_one_byte_fails_authentication() {105 val ct = VaultCrypto.encrypt("payload".toByteArray(), key, VaultCrypto.AAD_SLOT_A).copyOf()106 ct[ct.size - 1] = (ct[ct.size - 1].toInt() xor 0xFF).toByte()107 assertThrows { VaultCrypto.decrypt(ct, key, VaultCrypto.AAD_SLOT_A) }108 }109110 @Test fun wrong_key_fails_authentication() {111 val ct = VaultCrypto.encrypt("payload".toByteArray(), key, VaultCrypto.AAD_SLOT_A)112 val wrong = VaultCrypto.deriveKey("other", salt)113 assertThrows { VaultCrypto.decrypt(ct, wrong, VaultCrypto.AAD_SLOT_A) }114 }115116 @Test fun aad_is_enforced_mismatch_fails() {117 val ct = VaultCrypto.encrypt("payload".toByteArray(), key, VaultCrypto.AAD_SLOT_A)118 assertThrows { VaultCrypto.decrypt(ct, key, VaultCrypto.AAD_SLOT_B) }119 }120121 @Test fun slot_aad_constants_are_distinct() {122 val all = listOf(VaultCrypto.AAD_SLOT_A, VaultCrypto.AAD_SLOT_B, VaultCrypto.AAD_RECOVERY, VaultCrypto.AAD_ROOT_KEY)123 assertEquals("all slot AADs are unique", all.size, all.toSet().size)124 }125126 @Test fun recovery_rootkey_aad_is_the_single_byte_0x01() {127 128 assertArrayEquals(byteArrayOf(1), VaultCrypto.AAD_RECOVERY_ROOTKEY)129 }130131 @Test fun byte_aad_round_trips_and_is_enforced() {132 val ct = VaultCrypto.encrypt("rk".toByteArray(), key, VaultCrypto.AAD_RECOVERY_ROOTKEY)133 assertArrayEquals("rk".toByteArray(), VaultCrypto.decrypt(ct, key, VaultCrypto.AAD_RECOVERY_ROOTKEY))134 assertThrows { VaultCrypto.decrypt(ct, key, byteArrayOf(2)) }135 }136137 @Test fun decrypt_rejects_too_short_data() {138 assertThrows { VaultCrypto.decrypt(ByteArray(4), key, VaultCrypto.AAD_SLOT_A) }139 }140141 @Test fun empty_plaintext_round_trips() {142 val ct = VaultCrypto.encrypt(ByteArray(0), key, VaultCrypto.AAD_SLOT_A)143 assertEquals(0, VaultCrypto.decrypt(ct, key, VaultCrypto.AAD_SLOT_A).size)144 }145146 147148 @Test fun pad_to_bucket_uses_smallest_fitting_bucket() {149 assertEquals(1024, VaultCrypto.padToBucket(ByteArray(500)).size)150 assertEquals(4096, VaultCrypto.padToBucket(ByteArray(1025)).size)151 assertEquals(262144, VaultCrypto.padToBucket(ByteArray(70000)).size)152 }153154 @Test fun pad_to_bucket_keeps_the_top_bucket_boundary() {155 assertEquals(262144, VaultCrypto.padToBucket(ByteArray(262144)).size)156 }157158 @Test fun pad_to_bucket_rounds_above_top_to_the_next_power_of_two() {159 160 161 assertEquals(524288, VaultCrypto.padToBucket(ByteArray(262145)).size)162 assertEquals(524288, VaultCrypto.padToBucket(ByteArray(300000)).size)163 assertEquals(262144, VaultCrypto.padToBucket(ByteArray(262144)).size)164 }165166 @Test fun both_padding_encoders_agree_above_the_top_bucket() {167 for (size in listOf(262_143, 262_144, 262_145, 300_000, 1_000_000)) {168 assertEquals(169 "padding size for $size bytes must not depend on the encoder",170 PeachPayloadCodec.paddedBucketSize(size),171 VaultCrypto.padToBucket(ByteArray(size)).size,172 )173 }174 }175176 @Test fun pad_to_block_rounds_up_to_1024() {177 assertEquals(1024, VaultCrypto.padToBlock(ByteArray(1)).size)178 assertEquals(1024, VaultCrypto.padToBlock(ByteArray(1024)).size)179 assertEquals(2048, VaultCrypto.padToBlock(ByteArray(1025)).size)180 }181182 183184 @Test fun monotonic_timestamp_strictly_increases() {185 var prev = VaultCrypto.monotonicEntryTimestamp()186 repeat(100) {187 val next = VaultCrypto.monotonicEntryTimestamp()188 assertTrue(next > prev)189 prev = next190 }191 }192193 @Test fun monotonic_timestamp_beats_supplied_baseline() {194 val future = System.currentTimeMillis() + 1_000_000195 try {196 assertTrue(VaultCrypto.monotonicEntryTimestamp(future) > future)197 } finally {198 199 200 201 VaultCrypto.resetMonotonicClockForTest()202 }203 }204205 @Test fun generate_salt_and_keys_have_expected_sizes() {206 assertEquals(VaultCrypto.SALT_SIZE, VaultCrypto.generateSalt().size)207 assertEquals(VaultCrypto.KEY_SIZE, VaultCrypto.generateRootKey().size)208 }209210 @Test fun generated_salts_are_not_all_equal() {211 assertFalse(VaultCrypto.generateSalt().contentEquals(VaultCrypto.generateSalt()))212 }213214 @Test fun zero_clears_the_buffer() {215 val b = byteArrayOf(1, 2, 3, 4)216 VaultCrypto.zero(b)217 assertArrayEquals(ByteArray(4), b)218 }219220 private fun assertThrows(block: () -> Unit) {221 var threw = false222 try { block() } catch (_: Exception) { threw = true }223 assertTrue("expected an exception", threw)224 }225}