1package com.peachpasswords.vault23import org.junit.Assert.assertEquals4import org.junit.Assert.assertNotNull5import org.junit.Assert.assertNull6import org.junit.Test7import java.util.Base6489101112131415161718class BrowserSlotTest {1920 private val password = "correct horse battery staple"2122 private fun hex(b: ByteArray) = b.joinToString("") { "%02x".format(it) }23 private fun b64(b: ByteArray) = Base64.getEncoder().encodeToString(b)2425 26 private fun splitSealed(sealed: ByteArray): Triple<String, String, String> {27 val nonce = sealed.copyOfRange(0, VaultCrypto.NONCE_SIZE)28 val body = sealed.copyOfRange(VaultCrypto.NONCE_SIZE, sealed.size - 16)29 val tag = sealed.copyOfRange(sealed.size - 16, sealed.size)30 return Triple(hex(nonce), hex(body), hex(tag))31 }3233 private fun browserSlotJson(rootKey: ByteArray, entries: List<VaultEntry>): String {34 val salt = VaultCrypto.generateSalt()35 val key = VaultCrypto.deriveKey(password, salt)3637 38 val payloadJson = """{"entries":${entriesJson(entries)},"duressConfigured":false}"""39 val padded = payloadJson + " ".repeat(1024 - payloadJson.length)40 val (nonce, ct, tag) = splitSealed(41 VaultCrypto.encrypt(padded.toByteArray(Charsets.UTF_8), key, VaultCrypto.AAD_SLOT_A),42 )4344 45 val (pn, pc, pt) = splitSealed(46 VaultCrypto.encrypt(hex(rootKey).toByteArray(Charsets.UTF_8), key, VaultCrypto.AAD_ROOT_KEY),47 )4849 return """{"version":1,"salt":"${hex(salt)}","nonce":"$nonce","ciphertext":"$ct","tag":"$tag",""" +50 """"pw_root_ciphertext":"$pc","pw_root_nonce":"$pn","pw_root_tag":"$pt"}"""51 }5253 private fun entriesJson(entries: List<VaultEntry>): String =54 entries.joinToString(",", "[", "]") {55 """{"id":"${it.id}","title":"${it.title}","username":"${it.username}",""" +56 """"password":"${it.password}","createdAt":${it.createdAt},"modifiedAt":${it.modifiedAt}}"""57 }5859 @Test60 fun `translates a browser slot and preserves entries`() {61 val rootKey = ByteArray(32) { it.toByte() }62 val entries = listOf(63 VaultEntry(64 id = "e1",65 title = "Example",66 username = "june",67 password = "hunter2",68 createdAt = 1L,69 modifiedAt = 2L,70 ),71 )7273 val slot = BrowserSlot.toAndroidSlot(BrowserSlot.parse(browserSlotJson(rootKey, entries))!!, password)74 assertNotNull("browser slot should translate", slot)7576 val key = VaultCrypto.deriveKey(password, Base64.getDecoder().decode(slot!!.salt))77 val payload = VaultCodec.decode(78 VaultCrypto.decrypt(Base64.getDecoder().decode(slot.blob), key, VaultCrypto.AAD_SLOT_A),79 )80 assertEquals(1, payload.entries.size)81 assertEquals("Example", payload.entries[0].title)82 assertEquals("hunter2", payload.entries[0].password)83 }8485 @Test86 fun `carries the browser root key across so recovery words still work`() {87 val rootKey = ByteArray(32) { (it * 7).toByte() }88 val slot = BrowserSlot.toAndroidSlot(89 BrowserSlot.parse(browserSlotJson(rootKey, emptyList()))!!,90 password,91 )!!9293 val key = VaultCrypto.deriveKey(password, Base64.getDecoder().decode(slot.salt))94 val payload = VaultCodec.decode(95 VaultCrypto.decrypt(Base64.getDecoder().decode(slot.blob), key, VaultCrypto.AAD_SLOT_A),96 )97 assertEquals(98 "root key must survive the translation or the user's written-down words break",99 b64(rootKey),100 payload.rootKeyB64,101 )102 }103104 @Test105 fun `wrong password yields null rather than a corrupt-blob error`() {106 val json = browserSlotJson(ByteArray(32), emptyList())107 assertNull(BrowserSlot.toAndroidSlot(BrowserSlot.parse(json)!!, "not the password"))108 }109110 @Test111 fun `an Android-shaped slot is not mistaken for a browser one`() {112 val android = """{"version":1,"salt":"AAAA","blob":"BBBB"}"""113 assertNull(BrowserSlot.parse(android))114 }115}