Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refine KDF parameters configuration #25

Merged
merged 3 commits into from
May 26, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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
)
}
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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,
Original file line number Diff line number Diff line change
@@ -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 @@
}
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

Check warning on line 49 in kotpass/src/main/kotlin/app/keemobile/kotpass/cryptography/KeyTransform.kt

Codecov / codecov/patch

kotpass/src/main/kotlin/app/keemobile/kotpass/cryptography/KeyTransform.kt#L49

Added line #L49 was not covered by tests
},
version = Argon2Engine.Version.from(header.kdfParameters.version),
password = compositeKey(credentials),
@@ -78,7 +73,7 @@
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() }
}
Original file line number Diff line number Diff line change
@@ -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()
)
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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,15 +54,51 @@ 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,
val iterations: ULong,
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
Original file line number Diff line number Diff line change
@@ -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(
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@

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 @@

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<String, VariantItem> {
val result = mutableMapOf<String, VariantItem>()
@@ -58,7 +57,7 @@
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())

Check warning on line 60 in kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/VariantDictionary.kt

Codecov / codecov/patch

kotpass/src/main/kotlin/app/keemobile/kotpass/database/header/VariantDictionary.kt#L60

Added line #L60 was not covered by tests
}
VariantTypeId.Int32 -> {
if (valueLength != Int.SIZE_BYTES) {

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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,
Original file line number Diff line number Diff line change
@@ -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,