Skip to content

Commit

Permalink
Merge pull request #25 from keemobile/refine/configuration
Browse files Browse the repository at this point in the history
Refine KDF parameters configuration
  • Loading branch information
Anvell authored May 26, 2024
2 parents 2a240dc + 52b9574 commit c5fae4c
Show file tree
Hide file tree
Showing 13 changed files with 102 additions and 82 deletions.
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

0 comments on commit c5fae4c

Please sign in to comment.