1package com.peachpasswords.vault23import com.github.luben.zstd.Zstd4import org.bouncycastle.crypto.digests.Blake3Digest5import org.bouncycastle.crypto.generators.Argon2BytesGenerator6import org.bouncycastle.crypto.params.Argon2Parameters7import java.io.ByteArrayOutputStream8import java.security.SecureRandom9import javax.crypto.Cipher10import javax.crypto.spec.GCMParameterSpec11import javax.crypto.spec.SecretKeySpec12import kotlinx.serialization.json.Json13import kotlinx.serialization.json.JsonObject14import kotlinx.serialization.json.JsonObjectBuilder15import kotlinx.serialization.json.JsonPrimitive16import kotlinx.serialization.json.add17import kotlinx.serialization.json.booleanOrNull18import kotlinx.serialization.json.buildJsonArray19import kotlinx.serialization.json.buildJsonObject20import kotlinx.serialization.json.contentOrNull21import kotlinx.serialization.json.intOrNull22import kotlinx.serialization.json.jsonArray23import kotlinx.serialization.json.jsonObject24import kotlinx.serialization.json.jsonPrimitive25import kotlinx.serialization.json.longOrNull262728293031323334353637383940object PeachPayloadCodec {4142 43 private const val MAGIC = 0x50434853 44 private const val VERSION = 0x0145 private const val HEADER_LEN = 846 private const val CHECKSUM_LEN = 447 private const val SALT_LEN = 1648 private const val NONCE_LEN = 1249 private const val TAG_LEN = 165051 private val SIZE_BUCKETS = intArrayOf(1024, 4096, 16384, 65536, 262144)5253 private const val FORMAT_V2 = 0x0254 private const val FORMAT_V3 = 0x035556 private const val ARGON2_MEMORY_KIB = 6553657 private const val ARGON2_ITERATIONS = 358 private const val ARGON2_PARALLELISM = 45960 private const val SECTION_ENTRIES = 0x0161 private const val SECTION_SETTINGS = 0x0262 private const val SECTION_LICENSE = 0x0363 private const val SECTION_RECALL = 0x0464 private const val SECTION_DECLINED_CARDS = 0x056566 private const val ENTRY_TYPE_LOGIN = 0x0167 private const val ENTRY_TYPE_NOTE = 0x0268 private const val ENTRY_TYPE_CARD = 0x0369 private const val ENTRY_TYPE_PASSKEY = 0x047071 private const val F_HAS_TOTP = 1 shl 072 private const val F_HAS_TAGS = 1 shl 173 private const val F_HAS_NOTES = 1 shl 274 private const val F_HAS_CANARY = 1 shl 375 private const val F_HAS_PWCHANGED = 1 shl 476 private const val F_HAS_BREACH = 1 shl 577 private const val F_HAS_ADDITIONAL_URLS = 1 shl 678 private const val F_HAS_DELETED = 1 shl 77980 private const val F2_HAS_EMAIL = 1 shl 08182 private val NETWORK_VALUES = listOf("visa", "mastercard", "amex", "discover", "unknown")8384 private const val ZSTD_LEVEL = 1985 private val secureRandom = SecureRandom()86 private val json = Json { ignoreUnknownKeys = true; encodeDefaults = false }8788 class PeachScriptException(message: String) : Exception(message)8990 9192 93 fun buildPayload(entries: List<VaultEntry>, passphrase: String): ByteArray {94 95 96 97 val entriesRaw = encodeEntriesSection(entries)98 val sections = listOf(buildSection(SECTION_ENTRIES, entriesRaw))99100 val header = ByteArray(HEADER_LEN)101 writeU32BE(header, 0, MAGIC.toLong())102 header[4] = VERSION.toByte()103 header[5] = sections.size.toByte()104 105106 val body = concat(listOf(header) + sections)107 val checksum = blake3Truncated(body)108 val inner = concat(listOf(body, checksum))109110 val header5 = ByteArray(5)111 header5[0] = FORMAT_V3.toByte()112 writeU32LE(header5, 1, inner.size.toLong())113 val wrapped = padToNearestBucket(concat(listOf(header5, inner)))114115 val salt = ByteArray(SALT_LEN).also(secureRandom::nextBytes)116 val nonce = ByteArray(NONCE_LEN).also(secureRandom::nextBytes)117 val key = deriveKey(passphrase, salt)118 return try {119 val ctTag = aesGcmEncrypt(key, nonce, wrapped)120 concat(listOf(salt, nonce, ctTag))121 } finally {122 key.fill(0)123 }124 }125126 127 fun parsePayload(envelope: ByteArray, passphrase: String): List<VaultEntry> {128 if (envelope.size < SALT_LEN + NONCE_LEN + TAG_LEN + HEADER_LEN + CHECKSUM_LEN) {129 throw PeachScriptException("PAYLOAD_TOO_SHORT")130 }131 val salt = envelope.copyOfRange(0, SALT_LEN)132 val nonce = envelope.copyOfRange(SALT_LEN, SALT_LEN + NONCE_LEN)133 val ctTag = envelope.copyOfRange(SALT_LEN + NONCE_LEN, envelope.size)134135 val key = deriveKey(passphrase, salt)136 val decrypted = try {137 aesGcmDecrypt(key, nonce, ctTag)138 } catch (_: Exception) {139 throw PeachScriptException("INVALID_PASSPHRASE")140 } finally {141 key.fill(0)142 }143144 145 val inner: ByteArray146 if (decrypted.size >= 5 && (decrypted[0].toInt() == FORMAT_V2 || decrypted[0].toInt() == FORMAT_V3)) {147 val innerLen = readU32LE(decrypted, 1).toInt()148 if (5 + innerLen > decrypted.size) throw PeachScriptException("PAYLOAD_TOO_SHORT")149 inner = decrypted.copyOfRange(5, 5 + innerLen)150 } else {151 inner = decrypted 152 }153154 if (inner.size < HEADER_LEN + CHECKSUM_LEN) throw PeachScriptException("PAYLOAD_TOO_SHORT")155 val body = inner.copyOfRange(0, inner.size - CHECKSUM_LEN)156 val stored = inner.copyOfRange(inner.size - CHECKSUM_LEN, inner.size)157 if (!constantTimeEqual(stored, blake3Truncated(body))) throw PeachScriptException("CHECKSUM_FAILED")158159 if (readU32BE(body, 0) != MAGIC.toLong()) throw PeachScriptException("BAD_MAGIC")160 val version = body[4].toInt() and 0xff161 if (version != VERSION) throw PeachScriptException("UNSUPPORTED_VERSION:$version")162 val sectionCount = body[5].toInt() and 0xff163164 var entries = emptyList<VaultEntry>()165 var cursor = HEADER_LEN166 for (i in 0 until sectionCount) {167 if (cursor + 9 > body.size) throw PeachScriptException("PAYLOAD_TOO_SHORT")168 val type = body[cursor].toInt() and 0xff169 val length = readU32LE(body, cursor + 1).toInt()170 val clength = readU32LE(body, cursor + 5).toInt()171 val dataStart = cursor + 9172 val dataEnd = dataStart + clength173 if (dataEnd > body.size) throw PeachScriptException("PAYLOAD_TOO_SHORT")174 val compressed = body.copyOfRange(dataStart, dataEnd)175 val data = Zstd.decompress(compressed, length)176 if (data.size != length) throw PeachScriptException("SECTION_LENGTH_MISMATCH")177 when (type) {178 SECTION_ENTRIES -> entries = decodeEntriesSection(data)179 180 181 else -> {}182 }183 cursor = dataEnd184 }185 return entries186 }187188 189190 private fun buildSection(type: Int, raw: ByteArray): ByteArray {191 val compressed = Zstd.compress(raw, ZSTD_LEVEL)192 val out = ByteArray(9 + compressed.size)193 out[0] = type.toByte()194 writeU32LE(out, 1, raw.size.toLong())195 writeU32LE(out, 5, compressed.size.toLong())196 System.arraycopy(compressed, 0, out, 9, compressed.size)197 return out198 }199200 201202 private class StringTable {203 private val map = LinkedHashMap<String, Int>()204205 206 fun intern(value: String?): Int {207 if (value.isNullOrEmpty()) return 0208 map[value]?.let { return it }209 val idx = map.size + 1210 if (idx > 0xffff) throw PeachScriptException("STRING_TABLE_OVERFLOW")211 map[value] = idx212 return idx213 }214215 fun serialize(): ByteArray {216 val out = BinaryWriter()217 out.u16(map.size)218 for (s in map.keys) {219 val e = s.toByteArray(Charsets.UTF_8)220 if (e.size > 0xffff) throw PeachScriptException("STRING_TOO_LONG")221 out.u16(e.size)222 out.bytes(e)223 }224 return out.toBytes()225 }226 }227228 private fun encodeEntriesSection(entries: List<VaultEntry>): ByteArray {229 val table = StringTable()230 231 for (e in entries) table.intern(e.title)232 val bodies = entries.map { encodeEntry(it, table) }233 val tableBytes = table.serialize()234235 val out = BinaryWriter()236 out.u32(entries.size.toLong())237 out.bytes(tableBytes)238 for (b in bodies) out.bytes(b)239 return out.toBytes()240 }241242 private fun decodeEntriesSection(data: ByteArray): List<VaultEntry> {243 val r = BinaryReader(data, 0)244 val count = r.u32().toInt()245 val strings = readStringTable(r)246 val out = ArrayList<VaultEntry>(count)247 for (i in 0 until count) out.add(decodeEntry(r, strings))248 return out249 }250251 private fun readStringTable(r: BinaryReader): List<String?> {252 val count = r.u16()253 val strings = ArrayList<String?>(count + 1)254 strings.add(null) 255 for (i in 0 until count) {256 val len = r.u16()257 strings.add(String(r.read(len), Charsets.UTF_8))258 }259 return strings260 }261262 private fun lookup(strings: List<String?>, idx: Int): String? =263 if (idx == 0 || idx >= strings.size) null else strings[idx]264265 266267 private fun encodeEntry(entry: VaultEntry, table: StringTable): ByteArray {268 val type = entryTypeByte(entry)269 val w = BinaryWriter()270 w.u8(type)271 w.bytes(uuidToBytes(entry.id))272273 val createdSec = entry.createdAt / 1000L274 val updatedSec = entry.modifiedAt / 1000L275 val updatedDelta = maxOf(0L, updatedSec - createdSec)276 val deletedSec = entry.deletedAt?.let { it / 1000L } ?: 0L277278 279 var flags = 0280 var flags2 = 0281 if (entry.deletedAt != null) flags = flags or F_HAS_DELETED282283 when (type) {284 ENTRY_TYPE_LOGIN -> {285 if (!entry.email.isNullOrEmpty()) flags2 = flags2 or F2_HAS_EMAIL286 if (!entry.totp.isNullOrEmpty()) flags = flags or F_HAS_TOTP287 if (!entry.tags.isNullOrEmpty()) flags = flags or F_HAS_TAGS288 if (!entry.notes.isNullOrEmpty()) flags = flags or F_HAS_NOTES289 if (entry.canary != null) flags = flags or F_HAS_CANARY290 if (entry.passwordChangedAt != null) flags = flags or F_HAS_PWCHANGED291 if ((entry.breach ?: 0) > 0) flags = flags or F_HAS_BREACH292 if (!entry.additionalUrls.isNullOrEmpty()) flags = flags or F_HAS_ADDITIONAL_URLS293 }294 ENTRY_TYPE_NOTE -> {295 if (!entry.notes.isNullOrEmpty()) flags = flags or F_HAS_NOTES296 if (!entry.tags.isNullOrEmpty()) flags = flags or F_HAS_TAGS297 }298 ENTRY_TYPE_CARD -> {299 if (!entry.tags.isNullOrEmpty()) flags = flags or F_HAS_TAGS300 if (!entry.notes.isNullOrEmpty()) flags = flags or F_HAS_NOTES301 }302 }303304 w.u8(flags)305 w.u8(flags2) 306 w.u32(createdSec)307 w.u32(updatedDelta)308 w.u32(deletedSec)309 w.u16(table.intern(entry.title))310311 when (type) {312 ENTRY_TYPE_LOGIN -> {313 w.u16(table.intern(entry.url))314 w.u16(table.intern(entry.username))315 w.lpString(entry.password ?: "")316 if (flags2 and F2_HAS_EMAIL != 0) w.lpString(entry.email!!)317 if (flags and F_HAS_TOTP != 0) w.lpString(entry.totp!!)318 if (flags and F_HAS_TAGS != 0) writeTags(w, table, entry.tags!!)319 if (flags and F_HAS_NOTES != 0) w.u16(table.intern(entry.notes))320 if (flags and F_HAS_CANARY != 0) {321 w.u16(table.intern(entry.canary!!.webhookUrl))322 w.u16(table.intern(entry.canary.label))323 }324 if (flags and F_HAS_PWCHANGED != 0) {325 val pwSec = entry.passwordChangedAt!! / 1000L326 w.u32(maxOf(0L, pwSec - createdSec))327 }328 if (flags and F_HAS_BREACH != 0) w.u32((entry.breach ?: 0).toLong())329 if (flags and F_HAS_ADDITIONAL_URLS != 0) writeStrings(w, table, entry.additionalUrls!!)330 }331 ENTRY_TYPE_NOTE -> {332 if (flags and F_HAS_NOTES != 0) w.u16(table.intern(entry.notes))333 if (flags and F_HAS_TAGS != 0) writeTags(w, table, entry.tags!!)334 }335 ENTRY_TYPE_CARD -> {336 w.u16(table.intern(entry.cardholderName))337 w.lpString(entry.cardNumber ?: "")338 w.lpString(entry.expMonth ?: "")339 w.lpString(entry.expYear ?: "")340 w.lpString(entry.cvc ?: "")341 w.u16(table.intern(entry.cardNickname))342 w.u8(networkToByte(entry.cardNetwork))343 344 345 w.u16(table.intern(deriveLast4(entry)))346 if (flags and F_HAS_TAGS != 0) writeTags(w, table, entry.tags!!)347 if (flags and F_HAS_NOTES != 0) w.u16(table.intern(entry.notes))348 }349 ENTRY_TYPE_PASSKEY -> {350 val blob = encodePasskeyBlob(entry)351 w.u32(blob.size.toLong())352 w.bytes(blob)353 }354 }355 return w.toBytes()356 }357358 private fun decodeEntry(r: BinaryReader, strings: List<String?>): VaultEntry {359 val type = r.u8()360 val id = bytesToUuid(r.read(16))361 val flags = r.u8()362 val flags2 = r.u8() 363 val createdSec = r.u32()364 val updatedDelta = r.u32()365 val deletedSec = r.u32()366 val titleIdx = r.u16()367368 val createdAt = createdSec * 1000L369 val modifiedAt = (createdSec + updatedDelta) * 1000L370 val deletedAt = if (deletedSec > 0) deletedSec * 1000L else null371 val title = lookup(strings, titleIdx) ?: ""372373 return when (type) {374 ENTRY_TYPE_LOGIN -> {375 val urlIdx = r.u16()376 val usernameIdx = r.u16()377 val password = r.lpString()378 val email = if (flags2 and F2_HAS_EMAIL != 0) r.lpString() else null379 val totp = if (flags and F_HAS_TOTP != 0) r.lpString() else null380 val tags = if (flags and F_HAS_TAGS != 0) readTags(r, strings) else null381 val notes = if (flags and F_HAS_NOTES != 0) lookup(strings, r.u16()) else null382 var canary: Canary? = null383 if (flags and F_HAS_CANARY != 0) {384 val webhookUrl = lookup(strings, r.u16()) ?: ""385 val label = lookup(strings, r.u16())386 canary = Canary(webhookUrl = webhookUrl, label = label)387 }388 val pwChanged = if (flags and F_HAS_PWCHANGED != 0) (createdSec + r.u32()) * 1000L else null389 val breach = if (flags and F_HAS_BREACH != 0) r.u32().toInt() else null390 val additionalUrls = if (flags and F_HAS_ADDITIONAL_URLS != 0) readStrings(r, strings) else null391 VaultEntry(392 id = id, type = "login", title = title,393 url = lookup(strings, urlIdx), username = lookup(strings, usernameIdx),394 password = password, email = email, totp = totp, tags = tags, notes = notes,395 canary = canary, passwordChangedAt = pwChanged, breach = breach,396 additionalUrls = additionalUrls, createdAt = createdAt, modifiedAt = modifiedAt,397 deletedAt = deletedAt,398 )399 }400 ENTRY_TYPE_NOTE -> {401 val notes = if (flags and F_HAS_NOTES != 0) lookup(strings, r.u16()) else null402 val tags = if (flags and F_HAS_TAGS != 0) readTags(r, strings) else null403 VaultEntry(404 id = id, type = "note", title = title, notes = notes, tags = tags,405 createdAt = createdAt, modifiedAt = modifiedAt, deletedAt = deletedAt,406 )407 }408 ENTRY_TYPE_CARD -> {409 val cardholderIdx = r.u16()410 val cardNumber = r.lpString()411 val expMonth = r.lpString()412 val expYear = r.lpString()413 val cvc = r.lpString()414 val nicknameIdx = r.u16()415 val network = networkFromByte(r.u8())416 r.u16() 417 val tags = if (flags and F_HAS_TAGS != 0) readTags(r, strings) else null418 val notes = if (flags and F_HAS_NOTES != 0) lookup(strings, r.u16()) else null419 VaultEntry(420 id = id, type = "card", title = title,421 cardholderName = lookup(strings, cardholderIdx),422 cardNumber = cardNumber.ifEmpty { null }, expMonth = expMonth.ifEmpty { null },423 expYear = expYear.ifEmpty { null }, cvc = cvc.ifEmpty { null },424 cardNickname = lookup(strings, nicknameIdx), cardNetwork = network,425 tags = tags, notes = notes,426 createdAt = createdAt, modifiedAt = modifiedAt, deletedAt = deletedAt,427 )428 }429 ENTRY_TYPE_PASSKEY -> {430 val blobLen = r.u32().toInt()431 val blob = r.read(blobLen)432 decodePasskeyBlob(id, title, createdAt, modifiedAt, deletedAt, blob)433 }434 else -> throw PeachScriptException("UNKNOWN_ENTRY_TYPE:$type")435 }436 }437438 private fun writeTags(w: BinaryWriter, table: StringTable, tags: List<String>) {439 val n = minOf(tags.size, 0xff)440 w.u8(n)441 for (i in 0 until n) w.u16(table.intern(tags[i]))442 }443444 private fun writeStrings(w: BinaryWriter, table: StringTable, items: List<String>) {445 val n = minOf(items.size, 0xff)446 w.u8(n)447 for (i in 0 until n) w.u16(table.intern(items[i]))448 }449450 private fun readTags(r: BinaryReader, strings: List<String?>): List<String> {451 val n = r.u8()452 val out = ArrayList<String>(n)453 for (i in 0 until n) lookup(strings, r.u16())?.let { out.add(it) }454 return out455 }456457 private fun readStrings(r: BinaryReader, strings: List<String?>): List<String> = readTags(r, strings)458459 private fun entryTypeByte(entry: VaultEntry): Int {460 if (entry.passkey != null) return ENTRY_TYPE_PASSKEY461 return when (entry.type) {462 "note" -> ENTRY_TYPE_NOTE463 "card" -> ENTRY_TYPE_CARD464 "passkey" -> ENTRY_TYPE_PASSKEY465 else -> ENTRY_TYPE_LOGIN466 }467 }468469 private fun networkToByte(network: String?): Int {470 if (network.isNullOrEmpty()) return 0xff471 val idx = NETWORK_VALUES.indexOf(network)472 return if (idx >= 0) idx else NETWORK_VALUES.indexOf("unknown")473 }474475 private fun networkFromByte(b: Int): String? {476 if (b == 0xff) return null477 return NETWORK_VALUES.getOrNull(b) ?: "unknown"478 }479480 private fun deriveLast4(entry: VaultEntry): String? {481 val digits = entry.cardNumber?.filter { it.isDigit() } ?: return null482 return if (digits.length >= 4) digits.takeLast(4) else digits.ifEmpty { null }483 }484485 486 487 488 489490 private fun encodePasskeyBlob(entry: VaultEntry): ByteArray {491 val pk = entry.passkey492 val obj = buildJsonObject {493 putIfPresent("rpId", pk?.rpId ?: entry.rpId)494 putIfPresent("rpName", pk?.rpName ?: entry.rpName)495 putIfPresent("credentialId", pk?.credentialId ?: entry.credentialId)496 putIfPresent("publicKeyCose", pk?.publicKeyCose ?: entry.publicKeyCose)497 putIfPresent("userHandle", pk?.userHandle ?: entry.userHandle)498 putIfPresent("userName", pk?.userName ?: entry.userName)499 putIfPresent("userDisplayName", pk?.userDisplayName ?: entry.userDisplayName)500 putIfPresent("algorithm", (pk?.algorithm ?: entry.algorithm))501 putIfPresent("signCount", (pk?.signCount ?: entry.signCount))502 putIfPresent("aaguid", pk?.aaguid ?: entry.aaguid)503 putIfPresent("backupEligible", pk?.backupEligible ?: entry.backupEligible)504 putIfPresent("backupState", pk?.backupState ?: entry.backupState)505 putIfPresent("lastUsedAt", entry.lastUsedAt)506 507 putIfPresent("password", entry.password)508 putIfPresent("url", entry.url)509 putIfPresent("username", entry.username)510 putIfPresent("email", entry.email)511 putIfPresent("totp", entry.totp)512 putIfPresent("notes", entry.notes)513 if (!entry.tags.isNullOrEmpty()) {514 put("tags", buildJsonArray { entry.tags.forEach { add(JsonPrimitive(it)) } })515 }516 }517 return json.encodeToString(JsonObject.serializer(), obj).toByteArray(Charsets.UTF_8)518 }519520 private fun decodePasskeyBlob(521 id: String, title: String, createdAt: Long, modifiedAt: Long, deletedAt: Long?, blob: ByteArray,522 ): VaultEntry {523 val obj = json.parseToJsonElement(String(blob, Charsets.UTF_8)).jsonObject524 fun str(k: String) = obj[k]?.jsonPrimitive?.contentOrNull525 fun intf(k: String) = obj[k]?.jsonPrimitive?.intOrNull526 fun longf(k: String) = obj[k]?.jsonPrimitive?.longOrNull527 fun boolf(k: String) = obj[k]?.jsonPrimitive?.booleanOrNull528 val tags = obj["tags"]?.jsonArray?.mapNotNull { it.jsonPrimitive.contentOrNull }529 val credentialId = str("credentialId")530 val publicKeyCose = str("publicKeyCose")531 val rpId = str("rpId")532 val userHandle = str("userHandle")533 val userName = str("userName")534 val passkey = if (credentialId != null && publicKeyCose != null && rpId != null &&535 userHandle != null && userName != null) {536 PasskeyData(537 credentialId = credentialId, publicKeyCose = publicKeyCose, rpId = rpId,538 rpName = str("rpName"), userHandle = userHandle, userName = userName,539 userDisplayName = str("userDisplayName"), algorithm = intf("algorithm") ?: -7,540 signCount = intf("signCount") ?: 0, aaguid = str("aaguid"),541 backupEligible = boolf("backupEligible") ?: false, backupState = boolf("backupState") ?: false,542 )543 } else null544 return VaultEntry(545 id = id, type = "passkey", title = title,546 url = str("url"), username = str("username"), email = str("email"),547 password = str("password"), totp = str("totp"), notes = str("notes"), tags = tags,548 createdAt = createdAt, modifiedAt = modifiedAt, deletedAt = deletedAt,549 passkey = passkey, lastUsedAt = longf("lastUsedAt"),550 rpId = rpId, rpName = str("rpName"), credentialId = credentialId,551 publicKeyCose = publicKeyCose, userHandle = userHandle, userName = userName,552 userDisplayName = str("userDisplayName"), algorithm = intf("algorithm"),553 aaguid = str("aaguid"), backupEligible = boolf("backupEligible"),554 backupState = boolf("backupState"), signCount = intf("signCount"),555 )556 }557558 559560 fun uuidToBytes(uuid: String): ByteArray {561 val hex = uuid.replace("-", "")562 if (hex.length != 32) throw PeachScriptException("UUID_FORMAT")563 return ByteArray(16) { i -> hex.substring(i * 2, i * 2 + 2).toInt(16).toByte() }564 }565566 fun bytesToUuid(bytes: ByteArray): String {567 if (bytes.size != 16) throw PeachScriptException("UUID_LENGTH")568 val hex = bytes.joinToString("") { "%02x".format(it) }569 return "${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-" +570 "${hex.substring(16, 20)}-${hex.substring(20)}"571 }572573 574575 private fun deriveKey(passphrase: String, salt: ByteArray): ByteArray {576 577 578 val params = Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)579 .withSalt(salt)580 .withParallelism(ARGON2_PARALLELISM)581 .withMemoryAsKB(ARGON2_MEMORY_KIB)582 .withIterations(ARGON2_ITERATIONS)583 .build()584 val gen = Argon2BytesGenerator().apply { init(params) }585 val out = ByteArray(32)586 gen.generateBytes(passphrase.toByteArray(Charsets.UTF_8), out)587 return out588 }589590 private fun aesGcmEncrypt(key: ByteArray, nonce: ByteArray, plaintext: ByteArray): ByteArray {591 val cipher = Cipher.getInstance("AES/GCM/NoPadding")592 cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), GCMParameterSpec(128, nonce))593 return cipher.doFinal(plaintext) 594 }595596 private fun aesGcmDecrypt(key: ByteArray, nonce: ByteArray, ctTag: ByteArray): ByteArray {597 val cipher = Cipher.getInstance("AES/GCM/NoPadding")598 cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), GCMParameterSpec(128, nonce))599 return cipher.doFinal(ctTag)600 }601602 private fun blake3Truncated(data: ByteArray): ByteArray {603 val digest = Blake3Digest(256)604 digest.update(data, 0, data.size)605 val full = ByteArray(32)606 digest.doFinal(full, 0)607 return full.copyOfRange(0, CHECKSUM_LEN)608 }609610 private fun padToNearestBucket(data: ByteArray): ByteArray {611 val bucket = SIZE_BUCKETS.firstOrNull { it >= data.size }612 var target = bucket ?: run {613 var t = 262144614 while (t < data.size) t *= 2615 t616 }617 if (target == data.size) return data618 return data.copyOf(target)619 }620621 private fun constantTimeEqual(a: ByteArray, b: ByteArray): Boolean {622 if (a.size != b.size) return false623 var diff = 0624 for (i in a.indices) diff = diff or (a[i].toInt() xor b[i].toInt())625 return diff == 0626 }627628 private fun concat(parts: List<ByteArray>): ByteArray {629 val total = parts.sumOf { it.size }630 val out = ByteArray(total)631 var c = 0632 for (p in parts) { System.arraycopy(p, 0, out, c, p.size); c += p.size }633 return out634 }635636 637638 private fun writeU32LE(buf: ByteArray, off: Int, v: Long) {639 buf[off] = (v and 0xff).toByte()640 buf[off + 1] = ((v ushr 8) and 0xff).toByte()641 buf[off + 2] = ((v ushr 16) and 0xff).toByte()642 buf[off + 3] = ((v ushr 24) and 0xff).toByte()643 }644645 private fun writeU32BE(buf: ByteArray, off: Int, v: Long) {646 buf[off] = ((v ushr 24) and 0xff).toByte()647 buf[off + 1] = ((v ushr 16) and 0xff).toByte()648 buf[off + 2] = ((v ushr 8) and 0xff).toByte()649 buf[off + 3] = (v and 0xff).toByte()650 }651652 private fun readU32LE(buf: ByteArray, off: Int): Long {653 return (buf[off].toLong() and 0xff) or654 ((buf[off + 1].toLong() and 0xff) shl 8) or655 ((buf[off + 2].toLong() and 0xff) shl 16) or656 ((buf[off + 3].toLong() and 0xff) shl 24)657 }658659 private fun readU32BE(buf: ByteArray, off: Int): Long {660 return ((buf[off].toLong() and 0xff) shl 24) or661 ((buf[off + 1].toLong() and 0xff) shl 16) or662 ((buf[off + 2].toLong() and 0xff) shl 8) or663 (buf[off + 3].toLong() and 0xff)664 }665666 private class BinaryWriter {667 private val out = ByteArrayOutputStream()668 fun u8(v: Int) { out.write(v and 0xff) }669 fun u16(v: Int) { out.write(v and 0xff); out.write((v ushr 8) and 0xff) }670 fun u32(v: Long) {671 out.write((v and 0xff).toInt()); out.write(((v ushr 8) and 0xff).toInt())672 out.write(((v ushr 16) and 0xff).toInt()); out.write(((v ushr 24) and 0xff).toInt())673 }674 fun bytes(b: ByteArray) { out.write(b) }675 fun lpString(value: String) {676 val enc = value.toByteArray(Charsets.UTF_8)677 if (enc.size > 0xff) throw PeachScriptException("STRING_TOO_LONG_U8")678 u8(enc.size); bytes(enc)679 }680 fun toBytes(): ByteArray = out.toByteArray()681 }682683 private class BinaryReader(private val data: ByteArray, start: Int) {684 private var cursor = start685 fun u8(): Int = data[cursor++].toInt() and 0xff686 fun u16(): Int { val v = (data[cursor].toInt() and 0xff) or ((data[cursor + 1].toInt() and 0xff) shl 8); cursor += 2; return v }687 fun u32(): Long {688 val v = (data[cursor].toLong() and 0xff) or ((data[cursor + 1].toLong() and 0xff) shl 8) or689 ((data[cursor + 2].toLong() and 0xff) shl 16) or ((data[cursor + 3].toLong() and 0xff) shl 24)690 cursor += 4; return v691 }692 fun read(n: Int): ByteArray { val out = data.copyOfRange(cursor, cursor + n); cursor += n; return out }693 fun lpString(): String = String(read(u8()), Charsets.UTF_8)694 }695696 private fun JsonObjectBuilder.putIfPresent(key: String, value: String?) {697 if (!value.isNullOrEmpty()) put(key, JsonPrimitive(value))698 }699 private fun JsonObjectBuilder.putIfPresent(key: String, value: Int?) {700 if (value != null) put(key, JsonPrimitive(value))701 }702 private fun JsonObjectBuilder.putIfPresent(key: String, value: Long?) {703 if (value != null) put(key, JsonPrimitive(value))704 }705 private fun JsonObjectBuilder.putIfPresent(key: String, value: Boolean?) {706 if (value != null) put(key, JsonPrimitive(value))707 }708}