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 var salt: ByteArray? = null66 var key: ByteArray? = null67 var sealed: ByteArray? = null68 var payloadJson: ByteArray? = null69 var plainBytes: ByteArray? = null70 var encoded: ByteArray? = null71 var encrypted: ByteArray? = null72 var rootKey: ByteArray? = null73 try {74 salt = unhex(browser.salt)75 key = VaultCrypto.deriveKey(password, salt!!)76 sealed = sealedHex(browser.nonce, browser.ciphertext, browser.tag)77 payloadJson = try {78 VaultCrypto.decrypt(sealed!!, key!!, VaultCrypto.AAD_SLOT_A)79 } catch (_: Exception) {80 return null81 }82 83 84 85 86 87 88 89 90 91 92 val plain = payloadJson!!.toString(Charsets.UTF_8).trimBucketPadding()93 plainBytes = plain.toByteArray(Charsets.UTF_8)94 val payload = VaultCodec.decode(plainBytes!!)9596 rootKey = unwrapRootKey(browser, key!!)97 val migrated = payload.copy(98 rootKeyB64 = rootKey?.let { b64(it) } ?: payload.rootKeyB64,99 )100101 encoded = VaultCodec.encode(migrated)102 encrypted = VaultCrypto.encrypt(encoded!!, key!!, VaultCrypto.AAD_SLOT_A)103104 return StoredVault(105 version = 1,106 salt = b64(salt!!),107 blob = b64(encrypted!!),108 rootKeyEscrow = browser.toRootKeyEscrow(),109 )110 } finally {111 encrypted?.let(VaultCrypto::zero)112 encoded?.let(VaultCrypto::zero)113 plainBytes?.let(VaultCrypto::zero)114 payloadJson?.let(VaultCrypto::zero)115 sealed?.let(VaultCrypto::zero)116 rootKey?.let(VaultCrypto::zero)117 salt?.let(VaultCrypto::zero)118 key?.let(VaultCrypto::zero)119 }120 }121122 123 private fun unwrapRootKey(browser: BrowserSlotA, derivedKey: ByteArray): ByteArray? {124 val ct = browser.pwRootCiphertext ?: return null125 val nonce = browser.pwRootNonce ?: return null126 val tag = browser.pwRootTag ?: return null127 var sealed: ByteArray? = null128 var hexBytes: ByteArray? = null129 return try {130 sealed = sealedHex(nonce, ct, tag)131 hexBytes = VaultCrypto.decrypt(132 sealed!!,133 derivedKey,134 VaultCrypto.AAD_ROOT_KEY,135 )136 unhex(hexBytes!!.toString(Charsets.UTF_8).trimBucketPadding())137 } catch (_: Exception) {138 null139 } finally {140 hexBytes?.let(VaultCrypto::zero)141 sealed?.let(VaultCrypto::zero)142 }143 }144145 private fun BrowserSlotA.toRootKeyEscrow(): RootKeyEscrow? {146 val s = recoverySalt ?: return null147 val w = recoveryWrappedRootKey ?: return null148 val n = recoveryNonce ?: return null149 val t = recoveryTag ?: return null150 return RootKeyEscrow(151 salt = b64Hex(s),152 wrappedRootKey = b64Hex(w),153 nonce = b64Hex(n),154 tag = b64Hex(t),155 )156 }157158 159 private fun sealedHex(vararg parts: String): ByteArray {160 val decoded = ArrayList<ByteArray>(parts.size)161 return try {162 parts.forEach { decoded += unhex(it) }163 val out = ByteArray(decoded.sumOf { it.size })164 var offset = 0165 decoded.forEach {166 System.arraycopy(it, 0, out, offset, it.size)167 offset += it.size168 }169 out170 } finally {171 decoded.forEach(VaultCrypto::zero)172 }173 }174175 private fun b64Hex(hex: String): String {176 val bytes = unhex(hex)177 return try {178 b64(bytes)179 } finally {180 VaultCrypto.zero(bytes)181 }182 }183184 private fun b64(b: ByteArray): String = Base64.getEncoder().encodeToString(b)185}186187188internal fun unhex(s: String): ByteArray {189 require(s.length % 2 == 0) { "odd-length hex" }190 return ByteArray(s.length / 2) { i ->191 ((hexDigit(s[i * 2]) shl 4) or hexDigit(s[i * 2 + 1])).toByte()192 }193}194195private fun hexDigit(c: Char): Int = when (c) {196 in '0'..'9' -> c - '0'197 in 'a'..'f' -> c - 'a' + 10198 in 'A'..'F' -> c - 'A' + 10199 else -> throw IllegalArgumentException("bad hex digit")200}201202203204205206207private fun String.trimBucketPadding(): String = trim { it == '\u0000' || it.isWhitespace() }