1package com.peachpasswords.vault23import org.bouncycastle.crypto.generators.Argon2BytesGenerator4import org.bouncycastle.crypto.params.Argon2Parameters5import java.security.SecureRandom6import java.text.Normalizer7import javax.crypto.Cipher8import javax.crypto.spec.GCMParameterSpec9import javax.crypto.spec.SecretKeySpec1011object VaultCrypto {12 const val ARGON2_MEMORY_KB = 6553613 const val ARGON2_ITERATIONS = 314 const val ARGON2_PARALLELISM = 415 const val ARGON2_HASH_LENGTH = 3216 const val SALT_SIZE = 1617 const val NONCE_SIZE = 1218 const val TAG_LENGTH_BITS = 12819 const val KEY_SIZE = 3220 const val PAD_BLOCK = 102421 const val DURESS_SLOT_SIZE = 262_1442223 val PADDING_BUCKETS = intArrayOf(1024, 4096, 16384, 65536, 262144)2425 const val AAD_SLOT_A = "slot_a_v1"26 const val AAD_SLOT_B = "slot_b_v1"27 const val AAD_ROOT_KEY = "pw_root_v1"2829 3031323334 const val AAD_RECOVERY = "recovery_escrow_v1"3536 373839404142 val AAD_RECOVERY_ROOTKEY = byteArrayOf(1)4344 private val secureRandom = SecureRandom()45 private var _lastTimestamp = 0L4647 484950515253545556 fun deriveKey(password: String, salt: ByteArray): ByteArray =57 deriveKeyFromBytes(58 Normalizer.normalize(password, Normalizer.Form.NFC).toByteArray(Charsets.UTF_8),59 salt,60 )6162 6364656667 fun deriveKeyRaw(password: String, salt: ByteArray): ByteArray =68 deriveKeyFromBytes(password.toByteArray(Charsets.UTF_8), salt)6970 private fun deriveKeyFromBytes(passwordBytes: ByteArray, salt: ByteArray): ByteArray {71 val params = Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)72 .withSalt(salt)73 .withParallelism(ARGON2_PARALLELISM)74 .withMemoryAsKB(ARGON2_MEMORY_KB)75 .withIterations(ARGON2_ITERATIONS)76 .build()77 val generator = Argon2BytesGenerator().apply { init(params) }78 val hash = ByteArray(ARGON2_HASH_LENGTH)79 try {80 generator.generateBytes(passwordBytes, hash)81 } finally {82 83 passwordBytes.fill(0)84 }85 return hash86 }8788 fun encrypt(plaintext: ByteArray, key: ByteArray, aad: String): ByteArray =89 encrypt(plaintext, key, aad.toByteArray(Charsets.UTF_8))9091 fun decrypt(data: ByteArray, key: ByteArray, aad: String): ByteArray =92 decrypt(data, key, aad.toByteArray(Charsets.UTF_8))9394 9596979899 fun encrypt(plaintext: ByteArray, key: ByteArray, aad: ByteArray): ByteArray {100 val nonce = ByteArray(NONCE_SIZE).also(secureRandom::nextBytes)101 val cipher = Cipher.getInstance("AES/GCM/NoPadding")102 cipher.init(103 Cipher.ENCRYPT_MODE,104 SecretKeySpec(key, "AES"),105 GCMParameterSpec(TAG_LENGTH_BITS, nonce),106 )107 cipher.updateAAD(aad)108 return nonce + cipher.doFinal(plaintext)109 }110111 fun decrypt(data: ByteArray, key: ByteArray, aad: ByteArray): ByteArray {112 require(data.size >= NONCE_SIZE) { "Data too short" }113 val nonce = data.copyOfRange(0, NONCE_SIZE)114 val ciphertext = data.copyOfRange(NONCE_SIZE, data.size)115 val cipher = Cipher.getInstance("AES/GCM/NoPadding")116 cipher.init(117 Cipher.DECRYPT_MODE,118 SecretKeySpec(key, "AES"),119 GCMParameterSpec(TAG_LENGTH_BITS, nonce),120 )121 cipher.updateAAD(aad)122 return cipher.doFinal(ciphertext)123 }124125 fun generateSalt(): ByteArray = ByteArray(SALT_SIZE).also(secureRandom::nextBytes)126 fun generateRootKey(): ByteArray = ByteArray(KEY_SIZE).also(secureRandom::nextBytes)127 fun generateNoise(): ByteArray = ByteArray(DURESS_SLOT_SIZE).also(secureRandom::nextBytes)128129 fun padToBucket(data: ByteArray): ByteArray {130 val target = PeachPayloadCodec.paddedBucketSize(data.size)131 require(target <= Int.MAX_VALUE) { "padded plaintext is too large" }132 return data.copyOf(target)133 }134135 fun padToBlock(data: ByteArray): ByteArray {136 val remainder = data.size % PAD_BLOCK137 if (remainder == 0) return data138 return data.copyOf(data.size + (PAD_BLOCK - remainder))139 }140141 @Synchronized142 fun monotonicEntryTimestamp(currentModifiedAt: Long? = null): Long {143 val base = maxOf(System.currentTimeMillis(), (currentModifiedAt ?: 0L) + 1L, _lastTimestamp + 1L)144 _lastTimestamp = base145 return base146 }147148 149150151152153154155156157 fun resetMonotonicClockForTest() {158 _lastTimestamp = 0L159 }160161 fun zero(bytes: ByteArray) {162 bytes.fill(0)163 }164}