-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
483faa2
commit 4481f03
Showing
31 changed files
with
923 additions
and
191 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/AesKdf.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package io.spherelabs.crypto.cipher | ||
|
||
import io.spherelabs.crypto.hash.sha256 | ||
|
||
|
||
object AesKdf { | ||
fun transformKey( | ||
key: ByteArray, | ||
seed: ByteArray, | ||
rounds: ULong, | ||
): ByteArray { | ||
for (r in 0 until rounds.toLong()) { | ||
AES(seed)[CipherMode.ECB, CipherPadding.NoPadding].encrypt(key, offset = 0, len = 16) | ||
AES(seed)[CipherMode.ECB, CipherPadding.NoPadding].encrypt(key, offset = 16, len = 16) | ||
} | ||
return key.sha256().also { | ||
key.clear() | ||
} | ||
} | ||
} | ||
|
||
|
||
fun ByteArray.clear() { | ||
for (i in indices) this[i] = 0x0 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
crypto/cipher/src/commonMain/kotlin/io/spherelabs/crypto/cipher/Argon2Kdf.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package io.spherelabs.crypto.cipher | ||
|
||
object Argon2Kdf { | ||
fun transformKey( | ||
type: Argon2Engine.Type, | ||
version: Argon2Engine.Version, | ||
password: ByteArray, | ||
secretKey: ByteArray?, | ||
additional: ByteArray?, | ||
salt: ByteArray, | ||
iterations: ULong, | ||
parallelism: UInt, | ||
memory: ULong | ||
): ByteArray { | ||
val result = ByteArray(32) | ||
Argon2Engine( | ||
type = type, | ||
salt = salt, | ||
secret = secretKey, | ||
additional = additional, | ||
iterations = iterations.toInt(), | ||
parallelism = parallelism.toInt(), | ||
memory = memory.toInt() / 1024, | ||
version = version | ||
).encrypt(password, result) | ||
|
||
return result | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,43 +1,32 @@ | ||
import io.spherelabs.anycrypto.securerandom.SecureRandom | ||
import io.spherelabs.anycrypto.securerandom.buildSecureRandom | ||
import io.spherelabs.crypto.cipher.Argon2 | ||
import kotlin.test.BeforeTest | ||
import io.spherelabs.crypto.cipher.Argon2Engine | ||
import kotlin.test.Test | ||
import kotlin.test.assertEquals | ||
|
||
class Argon2Test { | ||
|
||
private lateinit var random: SecureRandom | ||
|
||
@BeforeTest | ||
fun setup() { | ||
random = buildSecureRandom() | ||
} | ||
|
||
@OptIn(ExperimentalStdlibApi::class) | ||
@Test | ||
fun `GIVEN the argon2 WHEN generate hash value THEN equals same value`() { | ||
val randomOutput = random.nextBytes(32) | ||
val result = "011ed3fc11e41d40bceb3d48e3443417a57a9f7bbfce2c9360ff56956edc9fe4" | ||
val password = "1dc787126c3261d3891e016a7d6022f60589a982b006ee5006da7eb35b1261ac" | ||
val randomOutput = ByteArray(32) | ||
|
||
val salt = random.nextBytes(16) | ||
val salt = "8c4402516655e458b6935cf609292512".encodeToByteArray() | ||
val secret = "b89290567f720edbc3bbbf1171e435ca".encodeToByteArray() | ||
val additional = "7958dbdb5c5f47bb29e1a585e7ae4c96".encodeToByteArray() | ||
|
||
val plainText = random.nextBytes(32) | ||
val secret = random.nextBytes(16) | ||
val additional = random.nextBytes(16) | ||
|
||
Argon2( | ||
type = Argon2.Type.Argon2Id, | ||
version = Argon2.Version.Ver13, | ||
Argon2Engine( | ||
type = Argon2Engine.Type.Argon2Id, | ||
version = Argon2Engine.Version.Ver13, | ||
salt = salt, | ||
secret = secret, | ||
additional = additional, | ||
iterations = 3, | ||
parallelism = 4, | ||
memory = 32, | ||
).encrypt(plainText, randomOutput) | ||
).encrypt(password.encodeToByteArray(), randomOutput) | ||
|
||
val expected = "346a6a7563f3d03b0a95548e93cdafe0f61d7d7ae55f4ebc58b0c404cec220f2" | ||
|
||
println(randomOutput.toHexString()) | ||
assertEquals(result, randomOutput.toHexString()) | ||
assertEquals(expected, randomOutput.toHexString()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
41 changes: 41 additions & 0 deletions
41
tinypass/src/commonMain/kotlin/io/spherelabs/crypto/tinypass/database/common/XmlExtension.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package io.spherelabs.crypto.tinypass.database.common | ||
|
||
import com.fleeksoft.ksoup.nodes.Document | ||
import com.fleeksoft.ksoup.nodes.Element | ||
import com.fleeksoft.ksoup.nodes.XmlDeclaration | ||
import com.fleeksoft.ksoup.parser.Parser | ||
|
||
private const val KEY_FILE = "keyfile" | ||
private const val XML = "xml" | ||
private const val VERSION = "version" | ||
private const val ENCODING = "encoding" | ||
private const val EMPTY_URI = "" | ||
|
||
fun xml( | ||
version: String, | ||
charset: String, | ||
block: (Element.() -> Element), | ||
): Document { | ||
return Document(EMPTY_URI).apply { | ||
appendElement(KEY_FILE).apply { | ||
block.invoke(this) | ||
} | ||
outputSettings().syntax(Document.OutputSettings.Syntax.xml) | ||
val xmlDeclaration = XmlDeclaration(XML, false) | ||
xmlDeclaration.attr(VERSION, version) | ||
xmlDeclaration.attr(ENCODING, charset) | ||
prependChild(xmlDeclaration) | ||
} | ||
} | ||
|
||
fun xmlParser( | ||
content: String, | ||
): Document { | ||
return Parser.xmlParser().parseInput(html = content, EMPTY_URI) | ||
} | ||
|
||
fun xmlParser( | ||
content: ByteArray, | ||
): Document { | ||
return Parser.xmlParser().parseInput(htmlBytes = content, EMPTY_URI) | ||
} |
92 changes: 92 additions & 0 deletions
92
tinypass/src/commonMain/kotlin/io/spherelabs/crypto/tinypass/database/core/CompositeKey.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
package io.spherelabs.crypto.tinypass.database.core | ||
|
||
import io.spherelabs.crypto.cipher.AesKdf | ||
import io.spherelabs.crypto.cipher.Argon2Engine | ||
import io.spherelabs.crypto.cipher.Argon2Kdf | ||
import io.spherelabs.crypto.cipher.clear | ||
import io.spherelabs.crypto.hash.sha256 | ||
import io.spherelabs.crypto.hash.sha512 | ||
import io.spherelabs.crypto.tinypass.database.header.KeyDerivationParameters | ||
import io.spherelabs.crypto.tinypass.database.header.OuterHeader | ||
|
||
/** | ||
* Creating a composite key | ||
* | ||
* Altogether, there seem to be three possible sources of key data: a database password, a key file | ||
* and some abstract key provider (typically a hardware device). Key providers are assumed to implement | ||
* a challenge-response scheme, the challenge used to produce key data being the contents of the MainSeed header field. | ||
* | ||
* The details of key provider implementations differ depending on the product: | ||
* in KeePass you can only have either a key file or a key provider, in kdbxweb you can have both, | ||
* and KeePassXC even has provisions for multiple key providers. keepass-rs on the other hand does not support key providers at all. | ||
* | ||
* All the various key sources are mashed together into a composite key. | ||
* Since a database password has a wrong size, it is being hashed with SHA-256 first, resulting in 32 bytes. | ||
* Then all key sources present are concatenated and hashed with SHA-256. | ||
* | ||
* In other words: if the database has only a password, its composite key will be SHA256(SHA256(password)). | ||
* If there is also a key file, it will be SHA256(SHA256(password) + keyfile). | ||
* If there is a key provider, its data will come instead (KeePass) or after (KeePassXC and kxdbweb) the keyfile data. | ||
* | ||
* https://palant.info/2023/03/29/documenting-keepass-kdbx4-file-format/ | ||
*/ | ||
fun compositeKey(key: KeyHelper): ByteArray { | ||
val keys = listOfNotNull( | ||
key.passphrase?.raw, | ||
key.key?.raw, | ||
) | ||
|
||
val composite = when { | ||
keys.isNotEmpty() -> { | ||
keys.reduce { a, b -> a + b } | ||
} | ||
else -> ByteArray(0) | ||
} | ||
return composite.sha256().also { composite.clear() } | ||
} | ||
|
||
fun transformKey(header: OuterHeader, keys: KeyHelper): ByteArray { | ||
return when (header.keyDerivationParameters) { | ||
is KeyDerivationParameters.AES -> { | ||
AesKdf.transformKey( | ||
key = compositeKey(keys), | ||
seed = header.keyDerivationParameters.seed.toByteArray(), | ||
rounds = header.keyDerivationParameters.rounds, | ||
) | ||
} | ||
is KeyDerivationParameters.Argon2 -> { | ||
Argon2Kdf.transformKey( | ||
type = when (header.keyDerivationParameters.uuid) { | ||
KeyDerivationParameters.KdfArgon2d -> Argon2Engine.Type.Argon2D | ||
KeyDerivationParameters.KdfArgon2id -> Argon2Engine.Type.Argon2Id | ||
else -> throw Exception("") | ||
}, | ||
version = Argon2Engine.Version.from(header.keyDerivationParameters.version), | ||
password = compositeKey(keys), | ||
salt = header.keyDerivationParameters.salt.toByteArray(), | ||
secretKey = header.keyDerivationParameters.key?.toByteArray(), | ||
additional = header.keyDerivationParameters.associatedData?.toByteArray(), | ||
iterations = header.keyDerivationParameters.iterations, | ||
parallelism = header.keyDerivationParameters.parallelism, | ||
memory = header.keyDerivationParameters.memory, | ||
) | ||
} | ||
} | ||
} | ||
|
||
fun masterKey( | ||
masterSeed: ByteArray, | ||
transformedKey: ByteArray, | ||
) = (masterSeed + transformedKey).sha256() | ||
|
||
|
||
fun hmacKey( | ||
masterSeed: ByteArray, | ||
transformedKey: ByteArray, | ||
): ByteArray { | ||
val combined = byteArrayOf(*masterSeed, *transformedKey, 0x01) | ||
return (ByteArray(8) { 0xFF.toByte() } + combined.sha512()) | ||
.sha512() | ||
.also { combined.clear() } | ||
} | ||
|
Oops, something went wrong.