Merge remote-tracking branch 'origin/main' into feat/android-account-kdf
# Conflicts: # Sources/Views/SignInView.swift
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
package io.flamenet.e2e
|
||||
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* The Kotlin account KDF against the vectors pinned from the enclave client.
|
||||
*
|
||||
* Not against the JavaScript or Swift engines: those were pinned from the same
|
||||
* source, and an implementation checked only against our other implementations
|
||||
* inherits whatever we all misread. The cases include a non-ASCII passphrase
|
||||
* and an empty one, which is where a `CharArray` API and a UTF-8 wire format
|
||||
* quietly disagree.
|
||||
*
|
||||
* ⚠️ Passing here says nothing about a phone. The provider on a device is
|
||||
* Conscrypt, not the JDK's, and this engine already has one case — X25519 —
|
||||
* that passed twenty-one JVM tests and threw on the first tap on real hardware.
|
||||
* The on-device suite in `:engine-android` is what settles Android.
|
||||
*/
|
||||
class AccountKdfTest {
|
||||
|
||||
private val vectors: JSONObject by lazy {
|
||||
val candidates = listOfNotNull(
|
||||
System.getenv("FLAMENET_ACCOUNT_VECTORS"),
|
||||
"../../engine/test/vectors/account-kdf-v1.json",
|
||||
"../../../flamenet-e2e/test/vectors/account-kdf-v1.json",
|
||||
"../../flamenet-e2e/test/vectors/account-kdf-v1.json",
|
||||
).map { File(it) }
|
||||
val file = candidates.firstOrNull { it.exists() }
|
||||
// Fails rather than skips: a vector suite that quietly stops checking is
|
||||
// worth less than none, because it still reports green.
|
||||
assertTrue(
|
||||
file != null,
|
||||
"account KDF vectors not found. Looked in: " +
|
||||
candidates.joinToString(", ") { it.absolutePath },
|
||||
)
|
||||
JSONObject(file!!.readText())
|
||||
}
|
||||
|
||||
private fun hex(bytes: ByteArray) = bytes.joinToString("") { "%02x".format(it) }
|
||||
private fun unhex(s: String) =
|
||||
ByteArray(s.length / 2) { s.substring(it * 2, it * 2 + 2).toInt(16).toByte() }
|
||||
|
||||
@Test
|
||||
fun `derives exactly the published bytes for every case`() {
|
||||
val kdf = vectors.getJSONObject("kdf")
|
||||
val params = AccountKdf.Params.withId(kdf.getInt("id"))
|
||||
assertEquals(kdf.getInt("iterations"), params.iterations)
|
||||
assertEquals(kdf.getInt("saltBytes"), params.saltBytes)
|
||||
|
||||
val cases = vectors.getJSONArray("cases")
|
||||
assertTrue(cases.length() > 0, "the vector file carries no cases")
|
||||
|
||||
for (i in 0 until cases.length()) {
|
||||
val c = cases.getJSONObject(i)
|
||||
val out = AccountKdf.derive(
|
||||
passphrase = c.getString("passphrase"),
|
||||
salt = unhex(c.getString("saltHex")),
|
||||
context = c.getString("context"),
|
||||
params = params,
|
||||
)
|
||||
assertEquals(c.getString("expectedHex"), hex(out), "case: ${c.getString("name")}")
|
||||
assertEquals(c.getString("expectedB64"), Bytes.b64(out), "case: ${c.getString("name")}")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the context is what separates one secret from another`() {
|
||||
val salt = unhex("000102030405060708090a0b0c0d0e0f")
|
||||
val pass = "correct horse battery staple"
|
||||
|
||||
val auth = AccountKdf.authSecret(pass, salt)
|
||||
val identity = AccountKdf.identityKey(pass, salt)
|
||||
val account = AccountKdf.accountKeySeed(pass, salt)
|
||||
|
||||
// Same passphrase, same salt, same stretch — three unrelated secrets.
|
||||
// If any pair matched, a compromise of one product's key would hand over
|
||||
// another's, which is the entire reason the contexts exist.
|
||||
assertFalse(auth.contentEquals(identity), "auth and identity keys collided")
|
||||
assertFalse(auth.contentEquals(account), "auth and account keys collided")
|
||||
assertFalse(identity.contentEquals(account), "identity and account keys collided")
|
||||
assertEquals(32, account.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the account seed produces the address the other engines publish`() {
|
||||
// Ties the two halves together: the KDF above and the encoder in FNID.
|
||||
// A seed that derives correctly but encodes differently would still
|
||||
// give this account a different name on every client.
|
||||
val seed = ByteArray(32) { 0x2a }
|
||||
val ai = JSONObject(
|
||||
File(
|
||||
listOfNotNull(
|
||||
System.getenv("FLAMENET_VECTORS"),
|
||||
"../../engine/test/vectors/protocol-v1.json",
|
||||
"../../../flamenet-e2e/test/vectors/protocol-v1.json",
|
||||
).first { File(it).exists() },
|
||||
).readText(),
|
||||
).getJSONObject("accountIdentity")
|
||||
|
||||
assertEquals(ai.getString("address"), FNID.address(seed))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user