1package com.peachpasswords.vault23import org.bouncycastle.crypto.generators.Argon2BytesGenerator4import org.bouncycastle.crypto.params.Argon2Parameters5import java.security.SecureRandom6import javax.crypto.Cipher7import javax.crypto.spec.GCMParameterSpec8import javax.crypto.spec.SecretKeySpec910object VaultCrypto {11 const val ARGON2_MEMORY_KB = 6553612 const val ARGON2_ITERATIONS = 313 const val ARGON2_PARALLELISM = 414 const val ARGON2_HASH_LENGTH = 3215 const val SALT_SIZE = 1616 const val NONCE_SIZE = 1217 const val TAG_LENGTH_BITS = 12818 const val KEY_SIZE = 3219 const val PAD_BLOCK = 102420 const val DURESS_SLOT_SIZE = 262_1442122 val PADDING_BUCKETS = intArrayOf(1024, 4096, 16384, 65536, 262144)2324 const val AAD_SLOT_A = "slot_a_v1"25 const val AAD_SLOT_B = "slot_b_v1"26 const val AAD_ROOT_KEY = "pw_root_v1"2728 2930313233 const val AAD_RECOVERY = "recovery_escrow_v1"3435 363738394041 val AAD_RECOVERY_ROOTKEY = byteArrayOf(1)4243 private val secureRandom = SecureRandom()44 private var _lastTimestamp = 0L4546 fun deriveKey(password: String, salt: ByteArray): ByteArray {47 val params = Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)48 .withSalt(salt)49 .withParallelism(ARGON2_PARALLELISM)50 .withMemoryAsKB(ARGON2_MEMORY_KB)51 .withIterations(ARGON2_ITERATIONS)52 .build()53 val generator = Argon2BytesGenerator().apply { init(params) }54 val hash = ByteArray(ARGON2_HASH_LENGTH)55 generator.generateBytes(password.toCharArray(), hash)56 return hash57 }5859 fun encrypt(plaintext: ByteArray, key: ByteArray, aad: String): ByteArray =60 encrypt(plaintext, key, aad.toByteArray(Charsets.UTF_8))6162 fun decrypt(data: ByteArray, key: ByteArray, aad: String): ByteArray =63 decrypt(data, key, aad.toByteArray(Charsets.UTF_8))6465 6667686970 fun encrypt(plaintext: ByteArray, key: ByteArray, aad: ByteArray): ByteArray {71 val nonce = ByteArray(NONCE_SIZE).also(secureRandom::nextBytes)72 val cipher = Cipher.getInstance("AES/GCM/NoPadding")73 cipher.init(74 Cipher.ENCRYPT_MODE,75 SecretKeySpec(key, "AES"),76 GCMParameterSpec(TAG_LENGTH_BITS, nonce),77 )78 cipher.updateAAD(aad)79 return nonce + cipher.doFinal(plaintext)80 }8182 fun decrypt(data: ByteArray, key: ByteArray, aad: ByteArray): ByteArray {83 require(data.size >= NONCE_SIZE) { "Data too short" }84 val nonce = data.copyOfRange(0, NONCE_SIZE)85 val ciphertext = data.copyOfRange(NONCE_SIZE, data.size)86 val cipher = Cipher.getInstance("AES/GCM/NoPadding")87 cipher.init(88 Cipher.DECRYPT_MODE,89 SecretKeySpec(key, "AES"),90 GCMParameterSpec(TAG_LENGTH_BITS, nonce),91 )92 cipher.updateAAD(aad)93 return cipher.doFinal(ciphertext)94 }9596 fun generateSalt(): ByteArray = ByteArray(SALT_SIZE).also(secureRandom::nextBytes)97 fun generateRootKey(): ByteArray = ByteArray(KEY_SIZE).also(secureRandom::nextBytes)98 fun generateNoise(): ByteArray = ByteArray(DURESS_SLOT_SIZE).also(secureRandom::nextBytes)99100 fun padToBucket(data: ByteArray): ByteArray {101 val target = PADDING_BUCKETS.firstOrNull { it >= data.size } ?: data.size102 return data.copyOf(target)103 }104105 fun padToBlock(data: ByteArray): ByteArray {106 val remainder = data.size % PAD_BLOCK107 if (remainder == 0) return data108 return data.copyOf(data.size + (PAD_BLOCK - remainder))109 }110111 @Synchronized112 fun monotonicEntryTimestamp(currentModifiedAt: Long? = null): Long {113 val base = maxOf(System.currentTimeMillis(), (currentModifiedAt ?: 0L) + 1L, _lastTimestamp + 1L)114 _lastTimestamp = base115 return base116 }117118 fun zero(bytes: ByteArray) {119 bytes.fill(0)120 }121}