Skip to content

Refine KDF parameters configuration #25

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

Merged
merged 3 commits into from
May 26, 2024
Merged
Show file tree
Hide file tree
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"
Expand All @@ -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
Expand Up @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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(
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?,
Expand All @@ -14,7 +14,7 @@ internal object Argon2Kdf {
): ByteArray {
val result = ByteArray(32)
Argon2Engine(
type = type,
variant = variant,
salt = salt,
secret = secretKey,
additional = additional,
Expand Down
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
Expand Down Expand Up @@ -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),
Expand All @@ -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() }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
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).
Expand All @@ -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].
Expand Down Expand Up @@ -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
Expand Down
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
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<String, VariantItem> {
val result = mutableMapOf<String, VariantItem>()
Expand Down Expand Up @@ -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) {
Expand Down

This file was deleted.

Loading