Merge main: relay hotfixes and the docs pages
Conflicts were comment wording only, from the WordPress purge. Kept this
branch's prose: main's side carries the rename's artifacts ('the the
previous platform'), and the code either side registers is identical.
Verified after resolving — one api index, no wp-json index, the
federation refusal path, HomeRelays, and FN_ACCOUNTS_URL all present.
This commit is contained in:
@@ -0,0 +1,66 @@
|
|||||||
|
package io.flamenet.e2e
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global account identity — the address a person keeps when a relay does not.
|
||||||
|
*
|
||||||
|
* A relay's `user_id` is a row number it assigned itself; the account service's
|
||||||
|
* pseudonym is HMAC'd per product so two services cannot correlate their users.
|
||||||
|
* Neither survives a trip to a second relay. This does, because it is a public
|
||||||
|
* key the client derives and no server issues.
|
||||||
|
*
|
||||||
|
* Implements `engine/src/fnid.js`, which is normative.
|
||||||
|
*
|
||||||
|
* ⚠️ This engine can *encode* an address but cannot yet *derive* one: the
|
||||||
|
* account key comes from PBKDF2 over the passphrase under
|
||||||
|
* `flamenet/account/identity/v1`, and this module has no KDF at all — the JS
|
||||||
|
* and Swift engines each have one and Kotlin does not. Until that exists, an
|
||||||
|
* Android client can verify and display an address it is given and cannot
|
||||||
|
* produce its own.
|
||||||
|
*/
|
||||||
|
object FNID {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crockford base32: no padding, and no `I`, `L`, `O` or `U`, so the
|
||||||
|
* alphabet holds no pair a person can confuse reading an address aloud or
|
||||||
|
* copying it off a screen. Matches the recovery codes Files already issues
|
||||||
|
* rather than introducing a second encoding.
|
||||||
|
*/
|
||||||
|
const val ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz"
|
||||||
|
|
||||||
|
/** 32 raw bytes of an Ed25519 public key encode to 52 characters. */
|
||||||
|
const val ENCODED_LENGTH = 52
|
||||||
|
|
||||||
|
fun encode(bytes: ByteArray): String {
|
||||||
|
val out = StringBuilder()
|
||||||
|
var buffer = 0
|
||||||
|
var bits = 0
|
||||||
|
for (b in bytes) {
|
||||||
|
buffer = (buffer shl 8) or (b.toInt() and 0xFF)
|
||||||
|
bits += 8
|
||||||
|
while (bits >= 5) {
|
||||||
|
bits -= 5
|
||||||
|
out.append(ALPHABET[(buffer shr bits) and 31])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bits > 0) out.append(ALPHABET[(buffer shl (5 - bits)) and 31])
|
||||||
|
return out.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The address for an account key, prefixed so a loose comparison cannot
|
||||||
|
* mistake some other 52-character string for one of these.
|
||||||
|
*/
|
||||||
|
fun address(accountPublicKey: ByteArray): String = "fnid:" + encode(accountPublicKey)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What a client signs to claim an address at a relay.
|
||||||
|
*
|
||||||
|
* Binds the audience (so a claim cannot be replayed at another relay), the
|
||||||
|
* local user id (so it cannot be lifted onto another account there) and the
|
||||||
|
* key itself (so the signature cannot be transplanted). Concatenated rather
|
||||||
|
* than encoded, because three implementations must reproduce these bytes
|
||||||
|
* exactly and an encoder may respace between releases.
|
||||||
|
*/
|
||||||
|
fun claimCanonical(audience: String, userId: Long, accountKeyB64: String): ByteArray =
|
||||||
|
"fnacct-v1|$audience|$userId|$accountKeyB64".toByteArray(Charsets.UTF_8)
|
||||||
|
}
|
||||||
@@ -84,6 +84,29 @@ class PublishedVectorsTest {
|
|||||||
assertEquals(12, formatted.split(" ").size)
|
assertEquals(12, formatted.split(" ").size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `account identity encodes exactly as published`() {
|
||||||
|
val ai = vectors.getJSONObject("accountIdentity")
|
||||||
|
val key = java.util.Base64.getDecoder().decode(ai.getString("accountKeyB64"))
|
||||||
|
|
||||||
|
assertEquals(ai.getString("encoded"), FNID.encode(key))
|
||||||
|
assertEquals(ai.getString("address"), FNID.address(key))
|
||||||
|
|
||||||
|
val claim = ai.getJSONObject("claim")
|
||||||
|
val input = claim.getJSONObject("input")
|
||||||
|
assertEquals(
|
||||||
|
claim.getString("canonical"),
|
||||||
|
String(
|
||||||
|
FNID.claimCanonical(
|
||||||
|
audience = input.getString("audience"),
|
||||||
|
userId = input.getLong("userId"),
|
||||||
|
accountKeyB64 = input.getString("accountKeyB64"),
|
||||||
|
),
|
||||||
|
Charsets.UTF_8,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `canonical leaf encodes and hashes exactly as published`() {
|
fun `canonical leaf encodes and hashes exactly as published`() {
|
||||||
val leaf = vectors.getJSONObject("keyLog").getJSONObject("canonicalLeaf")
|
val leaf = vectors.getJSONObject("keyLog").getJSONObject("canonicalLeaf")
|
||||||
|
|||||||
Reference in New Issue
Block a user