diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/Const.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/Const.kt index 64489b2..ef45b12 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/Const.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/Const.kt @@ -1,6 +1,13 @@ package app.keemobile.kotpass.constants +import okio.ByteString.Companion.toByteString + internal object Const { const val TagsSeparator = ";" val TagsSeparatorsRegex = Regex("""\s*[;,:]\s*""") + + fun bytes(vararg values: Number) = values + .map(Number::toByte) + .toByteArray() + .toByteString() } diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/KdfConst.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/KdfConst.kt index 9e164e0..575a74a 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/KdfConst.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/constants/KdfConst.kt @@ -1,8 +1,5 @@ package app.keemobile.kotpass.constants -import app.keemobile.kotpass.extensions.b -import okio.ByteString - internal object KdfConst { object Keys { const val Uuid = "\$UUID" @@ -15,19 +12,4 @@ internal object KdfConst { const val SecretKey = "K" // Unsupported const val AssocData = "A" // Unsupported } - - val KdfAes = ByteString.of( - 0xC9.b, 0xD9.b, 0xF3.b, 0x9A.b, 0x62, 0x8A.b, 0x44, 0x60, - 0xBF.b, 0x74, 0x0D, 0x08, 0xC1.b, 0x8A.b, 0x4F, 0xEA.b - ) - - val KdfArgon2d = ByteString.of( - 0xEF.b, 0x63, 0x6D, 0xDF.b, 0x8C.b, 0x29, 0x44, 0x4B, 0x91.b, - 0xF7.b, 0xA9.b, 0xA4.b, 0x03, 0xE3.b, 0x0A, 0x0C - ) - - val KdfArgon2id = ByteString.of( - 0x9E.b, 0x29, 0x8B.b, 0x19, 0x56, 0xDB.b, 0x47, 0x73, 0xB2.b, - 0x3D, 0xFC.b, 0x3E, 0xC6.b, 0xF0.b, 0xA1.b, 0xE6.b - ) } diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/cryptography/Argon2Engine.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/cryptography/Argon2Engine.kt index a5f0255..a9cd0e5 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/cryptography/Argon2Engine.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/cryptography/Argon2Engine.kt @@ -38,7 +38,7 @@ private const val M32L = 0xFFFFFFFFL private val ZeroBytes = ByteArray(4) internal class Argon2Engine( - private val type: Type = Type.Argon2D, + private val variant: Variant = Variant.Argon2d, private val version: Version = Version.Ver13, private val salt: ByteArray, private val secret: ByteArray? = null, @@ -51,10 +51,10 @@ internal class Argon2Engine( private var segmentLength = 0 private var laneLength = 0 - enum class Type(val id: Int) { - Argon2D(0x00), - Argon2I(0x01), - Argon2Id(0x02) + enum class Variant(val id: Int) { + Argon2d(0x00), + Argon2i(0x01), + Argon2id(0x02) } enum class Version(val id: Int) { @@ -162,7 +162,8 @@ internal class Argon2Engine( } private fun isDataIndependentAddressing(position: Position): Boolean { - return type == Type.Argon2I || (type == Type.Argon2Id && position.pass == 0 && position.slice < Argon2SyncPoints / 2) + return variant == Variant.Argon2i || + (variant == Variant.Argon2id && position.pass == 0 && position.slice < Argon2SyncPoints / 2) } private fun initAddressBlocks( @@ -176,7 +177,7 @@ internal class Argon2Engine( inputBlock.v[2] = intToLong(position.slice) inputBlock.v[3] = intToLong(blocks.size) inputBlock.v[4] = intToLong(iterations) - inputBlock.v[5] = intToLong(type.id) + inputBlock.v[5] = intToLong(variant.id) if (position.pass == 0 && position.slice == 0) { // Don't forget to generate the first block of addresses: @@ -332,7 +333,7 @@ internal class Argon2Engine( */ private fun initialize(tmpBlockBytes: ByteArray, password: ByteArray, outputLength: Int) { val blake = Blake2bDigest(Argon2PreHashDigestLength * 8) - val values = intArrayOf(parallelism, outputLength, memory, iterations, version.id, type.id) + val values = intArrayOf(parallelism, outputLength, memory, iterations, version.id, variant.id) intToLittleEndian(values, tmpBlockBytes, 0) blake.update(tmpBlockBytes, 0, values.size * 4) diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/cryptography/Argon2Kdf.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/cryptography/Argon2Kdf.kt index e73099a..323088d 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/cryptography/Argon2Kdf.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/cryptography/Argon2Kdf.kt @@ -2,7 +2,7 @@ package app.keemobile.kotpass.cryptography internal object Argon2Kdf { fun transformKey( - type: Argon2Engine.Type, + variant: Argon2Engine.Variant, version: Argon2Engine.Version, password: ByteArray, secretKey: ByteArray?, @@ -14,7 +14,7 @@ internal object Argon2Kdf { ): ByteArray { val result = ByteArray(32) Argon2Engine( - type = type, + variant = variant, salt = salt, secret = secretKey, additional = additional, diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/cryptography/KeyTransform.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/cryptography/KeyTransform.kt index bde209d..cc76f5d 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/cryptography/KeyTransform.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/cryptography/KeyTransform.kt @@ -1,11 +1,9 @@ package app.keemobile.kotpass.cryptography -import app.keemobile.kotpass.constants.KdfConst import app.keemobile.kotpass.database.Credentials import app.keemobile.kotpass.database.header.DatabaseHeader -import app.keemobile.kotpass.database.header.KdfParameters -import app.keemobile.kotpass.errors.FormatError -import app.keemobile.kotpass.extensions.b +import app.keemobile.kotpass.database.header.KdfParameters.Aes +import app.keemobile.kotpass.database.header.KdfParameters.Argon2 import app.keemobile.kotpass.extensions.clear import app.keemobile.kotpass.extensions.sha256 import app.keemobile.kotpass.extensions.sha512 @@ -37,21 +35,18 @@ internal object KeyTransform { } is DatabaseHeader.Ver4x -> { when (header.kdfParameters) { - is KdfParameters.Aes -> { + is Aes -> { AesKdf.transformKey( key = compositeKey(credentials), seed = header.kdfParameters.seed.toByteArray(), rounds = header.kdfParameters.rounds ) } - is KdfParameters.Argon2 -> { + is Argon2 -> { Argon2Kdf.transformKey( - type = when (header.kdfParameters.uuid) { - KdfConst.KdfArgon2d -> Argon2Engine.Type.Argon2D - KdfConst.KdfArgon2id -> Argon2Engine.Type.Argon2Id - else -> throw FormatError.InvalidHeader( - "Unsupported Kdf UUID (Argon2): ${header.kdfParameters.uuid}" - ) + variant = when (header.kdfParameters.variant) { + Argon2.Variant.Argon2d -> Argon2Engine.Variant.Argon2d + Argon2.Variant.Argon2id -> Argon2Engine.Variant.Argon2id }, version = Argon2Engine.Version.from(header.kdfParameters.version), password = compositeKey(credentials), @@ -78,7 +73,7 @@ internal object KeyTransform { transformedKey: ByteArray ): ByteArray { val combined = byteArrayOf(*masterSeed, *transformedKey, 0x01) - return (ByteArray(8) { 0xFF.b } + combined.sha512()) + return (ByteArray(8) { 0xFF.toByte() } + combined.sha512()) .sha512() .also { combined.clear() } } diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/DatabaseHeader.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/DatabaseHeader.kt index 19b5edb..c30cfe5 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/DatabaseHeader.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/DatabaseHeader.kt @@ -2,8 +2,6 @@ package app.keemobile.kotpass.database.header import app.keemobile.kotpass.constants.CrsAlgorithm import app.keemobile.kotpass.constants.HeaderFieldId -import app.keemobile.kotpass.constants.KdfConst -import app.keemobile.kotpass.cryptography.Argon2Engine import app.keemobile.kotpass.errors.FormatError import app.keemobile.kotpass.extensions.asIntLe import app.keemobile.kotpass.extensions.asLongLe @@ -84,16 +82,7 @@ sealed class DatabaseHeader { compression = Compression.GZip, masterSeed = nextByteString(32), encryptionIV = nextByteString(CipherId.Aes.ivLength), - kdfParameters = KdfParameters.Argon2( - uuid = KdfConst.KdfArgon2d, - salt = nextByteString(32), - parallelism = 2U, - memory = 32UL * 1024UL * 1024UL, - iterations = 8U, - version = Argon2Engine.Version.Ver13.id.toUInt(), - secretKey = null, - associatedData = null - ), + kdfParameters = KdfParameters.Argon2.default(nextByteString(32)), publicCustomData = mapOf() ) } diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/DatabaseInnerHeader.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/DatabaseInnerHeader.kt index 6dcfedd..e23a847 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/DatabaseInnerHeader.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/DatabaseInnerHeader.kt @@ -2,7 +2,6 @@ package app.keemobile.kotpass.database.header import app.keemobile.kotpass.constants.CrsAlgorithm import app.keemobile.kotpass.errors.FormatError -import app.keemobile.kotpass.extensions.b import app.keemobile.kotpass.extensions.nextByteString import app.keemobile.kotpass.models.BinaryData import okio.BufferedSink @@ -74,7 +73,7 @@ data class DatabaseInnerHeader( randomStreamKey = source.readByteString(length) } InnerHeaderFieldId.Binary -> { - val memoryProtection = source.readByte() != 0x0.b + val memoryProtection = source.readByte() != 0x0.toByte() val content = source.readByteArray(length - BinaryFlagsSize) val binary = BinaryData.Uncompressed(memoryProtection, content) binaries[binary.hash] = binary diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/KdfParameters.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/KdfParameters.kt index b27d4e5..b1fc76a 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/KdfParameters.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/KdfParameters.kt @@ -1,32 +1,50 @@ package app.keemobile.kotpass.database.header +import app.keemobile.kotpass.constants.Const import app.keemobile.kotpass.constants.KdfConst +import app.keemobile.kotpass.cryptography.Argon2Engine import app.keemobile.kotpass.errors.FormatError import okio.ByteString /** - * Describes key-derivation function parameters + * Describes key-derivation function parameters. */ sealed class KdfParameters { - abstract val uuid: ByteString + /** + * Used to identify KDF in [DatabaseHeader]. The following KDFs + * are supported by KeePass format by default: + * + * ```properties + * AES-KDF C9:D9:F3:9A:62:8A:44:60:BF:74:0D:08:C1:8A:4F:EA + * Argon2d EF:63:6D:DF:8C:29:44:4B:91:F7:A9:A4:03:E3:0A:0C + * Argon2id 9E:29:8B:19:56:DB:47:73:B2:3D:FC:3E:C6:F0:A1:E6 + */ + internal abstract val uuid: ByteString /** * Uses AES as key-derivation function. * - * @property uuid Used to identify KDF in [DatabaseHeader]. * @property rounds How many times to hash the data. * @property seed Used as AES seed. */ data class Aes( - override val uuid: ByteString, val rounds: ULong, val seed: ByteString - ) : KdfParameters() + ) : KdfParameters() { + override val uuid = Uuid + + internal companion object { + val Uuid = Const.bytes( + 0xC9, 0xD9, 0xF3, 0x9A, 0x62, 0x8A, 0x44, 0x60, + 0xBF, 0x74, 0x0D, 0x08, 0xC1, 0x8A, 0x4F, 0xEA + ) + } + } /** * Uses Argon2 as key-derivation function. * - * @property uuid Used to identify KDF in [DatabaseHeader]. + * @property variant of Argon2 which is being used. * @property salt [ByteString] of salt to be used by the algorithm. * @property parallelism The number of threads (or lanes) used by the algorithm. * @property memory The amount of memory used by the algorithm (in bytes). @@ -36,7 +54,7 @@ sealed class KdfParameters { * @property associatedData Not used in KDBX format. */ data class Argon2( - override val uuid: ByteString, + val variant: Variant, val salt: ByteString, val parallelism: UInt, val memory: ULong, @@ -44,7 +62,43 @@ sealed class KdfParameters { val version: UInt, val secretKey: ByteString?, val associatedData: ByteString? - ) : KdfParameters() + ) : KdfParameters() { + override val uuid = variant.uuid + + enum class Variant(internal val uuid: ByteString) { + Argon2d( + Const.bytes( + 0xEF, 0x63, 0x6D, 0xDF, 0x8C, 0x29, 0x44, 0x4B, + 0x91, 0xF7, 0xA9, 0xA4, 0x03, 0xE3, 0x0A, 0x0C + ) + ), + Argon2id( + Const.bytes( + 0x9E, 0x29, 0x8B, 0x19, 0x56, 0xDB, 0x47, 0x73, + 0xB2, 0x3D, 0xFC, 0x3E, 0xC6, 0xF0, 0xA1, 0xE6 + ) + ); + + internal companion object { + val Uuids = entries.map(Variant::uuid) + + fun from(uuid: ByteString) = entries.first { it.uuid == uuid } + } + } + + companion object { + fun default(salt: ByteString) = Argon2( + variant = Variant.Argon2d, + salt = salt, + parallelism = 2U, + memory = 32UL * 1024UL * 1024UL, + iterations = 8U, + version = Argon2Engine.Version.Ver13.id.toUInt(), + secretKey = null, + associatedData = null + ) + } + } /** * Encodes [KdfParameters] as [VariantDictionary] to [ByteString]. @@ -85,19 +139,17 @@ sealed class KdfParameters { ?: throw FormatError.InvalidHeader("No KDF UUID found.") when (uuid) { - KdfConst.KdfAes -> { + Aes.Uuid -> { Aes( - uuid = uuid, rounds = (get(KdfConst.Keys.Rounds) as? VariantItem.UInt64)?.value ?: throw FormatError.InvalidHeader("No KDF rounds found."), seed = (get(KdfConst.Keys.SaltOrSeed) as? VariantItem.Bytes)?.value ?: throw FormatError.InvalidHeader("No KDF seed found.") ) } - KdfConst.KdfArgon2d, KdfConst.KdfArgon2id -> { + in Argon2.Variant.Uuids -> { Argon2( - uuid = (get(KdfConst.Keys.Uuid) as? VariantItem.Bytes)?.value - ?: throw FormatError.InvalidHeader("No KDF uuid found."), + variant = Argon2.Variant.from(uuid), salt = (get(KdfConst.Keys.SaltOrSeed) as? VariantItem.Bytes)?.value ?: throw FormatError.InvalidHeader("No KDF salt found."), parallelism = (get(KdfConst.Keys.Parallelism) as? VariantItem.UInt32)?.value diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/Signature.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/Signature.kt index 11dc912..d9d1a43 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/Signature.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/Signature.kt @@ -1,6 +1,6 @@ package app.keemobile.kotpass.database.header -import app.keemobile.kotpass.extensions.b +import app.keemobile.kotpass.constants.Const import app.keemobile.kotpass.io.BufferedStream import okio.BufferedSink import okio.ByteString @@ -15,8 +15,8 @@ class Signature( } companion object { - val Base = ByteString.of(0x03, 0xd9.b, 0xa2.b, 0x9a.b) - val Secondary = ByteString.of(0x67, 0xfb.b, 0x4b, 0xb5.b) + val Base = Const.bytes(0x03, 0xD9, 0xA2, 0x9A) + val Secondary = Const.bytes(0x67, 0xFB, 0x4B, 0xB5) val Default = Signature(Base, Secondary) internal fun readFrom(source: BufferedStream) = Signature( diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/VariantDictionary.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/VariantDictionary.kt index cd0e805..76863a8 100644 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/VariantDictionary.kt +++ b/kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/VariantDictionary.kt @@ -2,7 +2,6 @@ package app.keemobile.kotpass.database.header import app.keemobile.kotpass.constants.VariantTypeId import app.keemobile.kotpass.errors.FormatError -import app.keemobile.kotpass.extensions.b import okio.Buffer import okio.ByteString import okio.buffer @@ -12,7 +11,7 @@ import kotlin.experimental.and internal object VariantDictionary { private const val Version: Short = 0x0100 - private const val VersionFilter: Short = 0xff00.toShort() + private const val VersionFilter: Short = 0xFF00.toShort() fun readFrom(data: ByteString): Map { val result = mutableMapOf() @@ -58,7 +57,7 @@ internal object VariantDictionary { if (valueLength != Byte.SIZE_BYTES) { throw FormatError.InvalidHeader("Invalid item's value length for type: Bool.") } - result[key] = VariantItem.Bool(buffer.readByte() != 0x0.b) + result[key] = VariantItem.Bool(buffer.readByte() != 0x0.toByte()) } VariantTypeId.Int32 -> { if (valueLength != Int.SIZE_BYTES) { diff --git a/kotpass/src/main/kotlin/app/keemobile/kotpass/extensions/Number.kt b/kotpass/src/main/kotlin/app/keemobile/kotpass/extensions/Number.kt deleted file mode 100644 index 5ebf591..0000000 --- a/kotpass/src/main/kotlin/app/keemobile/kotpass/extensions/Number.kt +++ /dev/null @@ -1,3 +0,0 @@ -package app.keemobile.kotpass.extensions - -internal val Number.b: Byte get() = toByte() diff --git a/kotpass/src/test/kotlin/app/keemobile/kotpass/cryptography/AesKdfSpec.kt b/kotpass/src/test/kotlin/app/keemobile/kotpass/cryptography/AesKdfSpec.kt index 1fba344..b1a1052 100644 --- a/kotpass/src/test/kotlin/app/keemobile/kotpass/cryptography/AesKdfSpec.kt +++ b/kotpass/src/test/kotlin/app/keemobile/kotpass/cryptography/AesKdfSpec.kt @@ -1,7 +1,6 @@ package app.keemobile.kotpass.cryptography import app.keemobile.kotpass.database.Credentials -import app.keemobile.kotpass.extensions.b import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.shouldBe @@ -26,7 +25,7 @@ class AesKdfSpec : DescribeSpec({ it("Transforms key values as expected 2") { val credentials = Credentials.from(EncryptedValue.fromString("secret")) - val seed = ByteArray(32) { 0x1.b } + val seed = ByteArray(32) { 0x1.toByte() } val result = AesKdf.transformKey( key = KeyTransform.compositeKey(credentials), seed = seed, diff --git a/kotpass/src/test/kotlin/app/keemobile/kotpass/cryptography/Argon2Spec.kt b/kotpass/src/test/kotlin/app/keemobile/kotpass/cryptography/Argon2Spec.kt index b97da27..3d91998 100644 --- a/kotpass/src/test/kotlin/app/keemobile/kotpass/cryptography/Argon2Spec.kt +++ b/kotpass/src/test/kotlin/app/keemobile/kotpass/cryptography/Argon2Spec.kt @@ -13,7 +13,7 @@ class Argon2Spec : DescribeSpec({ val result = ByteArray(32) Argon2Engine( - type = Argon2Engine.Type.Argon2D, + variant = Argon2Engine.Variant.Argon2d, version = Argon2Engine.Version.Ver13, salt = Argon2Res.TestSalt, secret = Argon2Res.TestSecret, @@ -31,7 +31,7 @@ class Argon2Spec : DescribeSpec({ val result = ByteArray(32) Argon2Engine( - type = Argon2Engine.Type.Argon2I, + variant = Argon2Engine.Variant.Argon2i, version = Argon2Engine.Version.Ver13, salt = Argon2Res.TestSalt, secret = Argon2Res.TestSecret, @@ -49,7 +49,7 @@ class Argon2Spec : DescribeSpec({ val result = ByteArray(32) Argon2Engine( - type = Argon2Engine.Type.Argon2Id, + variant = Argon2Engine.Variant.Argon2id, version = Argon2Engine.Version.Ver13, salt = Argon2Res.TestSalt, secret = Argon2Res.TestSecret,