1package com.peachpasswords.vault23import kotlinx.serialization.SerialName4import kotlinx.serialization.Serializable5import kotlinx.serialization.json.Json6import java.util.Base6478910111213141516171819202122232425262728293031323334@Serializable35data class BrowserSlotA(36 val version: Int = 1,37 val salt: String,38 val nonce: String,39 val ciphertext: String,40 val tag: String,41 @SerialName("recovery_salt") val recoverySalt: String? = null,42 @SerialName("recovery_wrapped_root_key") val recoveryWrappedRootKey: String? = null,43 @SerialName("recovery_nonce") val recoveryNonce: String? = null,44 @SerialName("recovery_tag") val recoveryTag: String? = null,45 @SerialName("pw_root_ciphertext") val pwRootCiphertext: String? = null,46 @SerialName("pw_root_nonce") val pwRootNonce: String? = null,47 @SerialName("pw_root_tag") val pwRootTag: String? = null,48)4950object BrowserSlot {51 private val json = Json { ignoreUnknownKeys = true; isLenient = true }5253 54 fun parse(s: String): BrowserSlotA? =55 runCatching { json.decodeFromString<BrowserSlotA>(s) }.getOrNull()5657 58596061626364 fun toAndroidSlot(browser: BrowserSlotA, password: String): StoredVault? {65 val salt = unhex(browser.salt)66 val key = VaultCrypto.deriveKey(password, salt)67 try {68 val sealed = unhex(browser.nonce) + unhex(browser.ciphertext) + unhex(browser.tag)69 val payloadJson = try {70 VaultCrypto.decrypt(sealed, key, VaultCrypto.AAD_SLOT_A)71 } catch (_: Exception) {72 return null73 }74 75 76 val plain = payloadJson.toString(Charsets.UTF_8).trim()77 val payload = VaultCodec.decode(plain.toByteArray(Charsets.UTF_8))7879 val rootKey = unwrapRootKey(browser, key)80 val migrated = payload.copy(81 rootKeyB64 = rootKey?.let { b64(it) } ?: payload.rootKeyB64,82 )83 rootKey?.let(VaultCrypto::zero)8485 return StoredVault(86 version = 1,87 salt = b64(salt),88 blob = b64(VaultCrypto.encrypt(VaultCodec.encode(migrated), key, VaultCrypto.AAD_SLOT_A)),89 rootKeyEscrow = browser.toRootKeyEscrow(),90 )91 } finally {92 VaultCrypto.zero(key)93 }94 }9596 97 private fun unwrapRootKey(browser: BrowserSlotA, derivedKey: ByteArray): ByteArray? {98 val ct = browser.pwRootCiphertext ?: return null99 val nonce = browser.pwRootNonce ?: return null100 val tag = browser.pwRootTag ?: return null101 return runCatching {102 val hexBytes = VaultCrypto.decrypt(103 unhex(nonce) + unhex(ct) + unhex(tag),104 derivedKey,105 VaultCrypto.AAD_ROOT_KEY,106 )107 unhex(hexBytes.toString(Charsets.UTF_8))108 }.getOrNull()109 }110111 private fun BrowserSlotA.toRootKeyEscrow(): RootKeyEscrow? {112 val s = recoverySalt ?: return null113 val w = recoveryWrappedRootKey ?: return null114 val n = recoveryNonce ?: return null115 val t = recoveryTag ?: return null116 return RootKeyEscrow(117 salt = b64(unhex(s)),118 wrappedRootKey = b64(unhex(w)),119 nonce = b64(unhex(n)),120 tag = b64(unhex(t)),121 )122 }123124 private fun unhex(s: String): ByteArray {125 require(s.length % 2 == 0) { "odd-length hex" }126 return ByteArray(s.length / 2) { i ->127 ((hexDigit(s[i * 2]) shl 4) or hexDigit(s[i * 2 + 1])).toByte()128 }129 }130131 private fun hexDigit(c: Char): Int = when (c) {132 in '0'..'9' -> c - '0'133 in 'a'..'f' -> c - 'a' + 10134 in 'A'..'F' -> c - 'A' + 10135 else -> throw IllegalArgumentException("bad hex digit")136 }137138 private fun b64(b: ByteArray): String = Base64.getEncoder().encodeToString(b)139}