@session.js/blinded-session-id
v1.0.8
Published
Utility JavaScript library with methods to work with Session's blinded Session ID
Readme
@session.js/blinded-session-id
Utility JavaScript library with methods to work with Session's blinded Session ID. Uses @noble/curves v2, @noble/hashes v2 and @session.js/scalars under the hood.
Example of unblinded Session ID (05-prefixed):
057aeb66e45660c3bdfb7c62706f6440226af43ec13f3b6f899c1dd4db1b8fce5b
Example of blinded Session ID (15-prefixed):
15d9fd3a6c3c5ddf7500b862174f205ab27164232d9b10fe31f145e61629b676e3
Example of blinded Session ID (25-prefixed):
253b991dcbba44cfdb45d5b38880d95cff723309e3ece6fd01415ad5fa1dccc7ac
Blinded IDs are used on Session SOGS to conceal identity of room's users.
Usage
Blinding
Blinding with Session ID or x25519 public key
To "blind" Session ID, we need the Sesion ID itself and the SOGS's public key (the part after ?public_key= in the SOGS url).
You have multiple choices on input format. All blindSessionId signatures require type property defining type of the output Session ID (15- or 25-prefixed). The simpliest format is passing both sessionId and sogsPublicKey as hex-encoded strings.
You'll get two variants of the blinded Session ID. Both outputs are valid and possible and there is absolutely no way to determine which one is correct (unless you have user's ed25519 key, more on that below).
import { blindSessionId } from "@session.js/blinded-session-id";
blindSessionId({
type: "15",
sessionId: "057aeb66e45660c3bdfb7c62706f6440226af43ec13f3b6f899c1dd4db1b8fce5b",
sogsPublicKey: "cb4fd6199b84dc3664f0373354341a01007ecaa99a388496fe8775b9b76a253b",
});
// Either of those will be the blinded ID
// => [
// "15383d0a3ba605abe3b5b7343102be3fc0026056b9812e06f6daee3be62a6a5663",
// "15383d0a3ba605abe3b5b7343102be3fc0026056b9812e06f6daee3be62a6a56e3"
// ]You can also pass sogsPublicKey as Uint8Array, in case you already have it decoded in your app. You can also pass Session ID as x25519 public key as Uint8Array in case you already have it decoded in your app, but keep in mind that Session ID is 33 bytes long and x25519 public key is 32 bytes long, because Session ID has a prefix byte (0x05, 0x15 or 0x25).
import { blindSessionId } from "@session.js/blinded-session-id";
blindSessionId({
type: "15",
x25519PublicKey: new Uint8Array([
122, 235, 102, 228, 86, 96, 195, 189, 251, 124, 98, 112, 111, 100, 64, 34, 106, 244, 62, 193,
63, 59, 111, 137, 156, 29, 212, 219, 27, 143, 206, 91,
]),
sogsPublicKey: new Uint8Array([
203, 79, 214, 25, 155, 132, 220, 54, 100, 240, 55, 51, 84, 52, 26, 1, 0, 126, 202, 169, 154, 56,
132, 150, 254, 135, 117, 185, 183, 106, 37, 59,
]),
});
// Either of those will be the blinded ID
// => [
// "15383d0a3ba605abe3b5b7343102be3fc0026056b9812e06f6daee3be62a6a5663",
// "15383d0a3ba605abe3b5b7343102be3fc0026056b9812e06f6daee3be62a6a56e3"
// ]When converting Session's x25519 (Session ID) to the ed25519 key (needed for blinding), the sign (+/-) information is lost, producing two equally valid possibilities. So unless you have user's ed25519 public key, you cannot have a guaranteed determenistic correct single output for blinding, you will always have to account for two outputs.
Hang on tight, we're diving into some real nerdy cryptography here!
You know how Session actually has two keypairs — x25519 (public, secret) and ed25519 (public, secret), all of which are derived from the main secret seed (which can be encoded into words with mnemonic/secret phrase) and Session ID is the public x25519 key?
In order to "blind" Session ID we first calculate a "blinding factor" k using modulo of SOGS public key and ed25519's ORDER constant. Then we calculate kA by multiplying user's ed25519 public key and k. Finally, we convert kA to hex and prepend it with 15 to mark it as a "blinded" Session ID.
But wait a second, where did "user's ed25519 public key" come from? Remember, Session ID is a x25519 public key, not ed25519 public key. These are two different keypairs, two different elliptic curves and they behave in very different ways!
That's where the major flaw of Session comes in. Technically, we could just settle on some way of converting value from one curve to another and vice verse. In fact, that's precisely what libsodium (and official Session clients) do using crypto_sign_curve25519_pk_to_ed25519. In the end, both curves are just mathematical curves and translating one to another should be determenistic.
There is just one problem though: x25519 does not have negative values, while ed25519 does have them. So ed25519's 123 converted to x25519 would be 456 and vice-verse, but what happens when we convert ed25519's -123 to x25519 curve is... 456 too! See the problem? Two different values producing same output.
And this wouldn't be bad if we only wanted x25519 for blinding, but as you saw earlier, we need ed25519 keys. Now you want to convert x25519 back to ed25519 and only have this x25519 value: 456. Was it 123 or -123 in ed25519 that produced that number?
So what happens when we try to convert x25519 public key (Session ID) -> ed25519 public key (for blinding) is that we end up with two keys since we don't know whether ed25519 should be positive or negative.
If only we had user's secret seed (mnemonic), we could easily calculate ed25519 and avoid x25519 -> ed25519 conversion issue at all... But alas, Session IDs are using x25519 and converting x25519 to ed25519 produces two results because of the lost signing bit.
More on blinding: “Blinded ID в Session и что с ними не так” by li0ard
Blinding with ed25519 public key
If you do have user's ed25519 public key (e.g. you control the secret seed, running from the user's client, etc), you can pass it directly instead of Session ID or x25519PublicKey and get a single, correct blinded ID.
import { blindSessionId } from "@session.js/blinded-session-id"
blindSessionId({
type: "15",
ed25519PublicKey: new Uint8Array([
240, 55, 163, 9, 91, 114, 247, 212, 93, 217, 12, 245, 213, 114, 251, 182, 195, 95, 46, 4, 211,
85, 177, 217, 201, 218, 3, 95, 202, 132, 158, 151,
]),
sogsPublicKey: "cb4fd6199b84dc3664f0373354341a01007ecaa99a388496fe8775b9b76a253b",
});
// => 15383d0a3ba605abe3b5b7343102be3fc0026056b9812e06f6daee3be62a6a56e3There is no way to convert x25519 public key to ed25519 public key without losing the sign bit and thus getting two possible ed25519 keys and two possible blinded Session IDs.
Unblinding
Unblinding is a security vulnerability discovered by li0ard possible because 15-prefixed IDs use SOGS's public key as blake2b hash input, which is publicly known, making it possible to reverse 15-prefixed blinding by using the same sogsPublicKey.
In 25-prefixed IDs this issue was fixed by using both x25519 public key and SOGS's public key as blake2b hash input, so unless you know user's 05-prefixed Session ID it's impossible to unblind 25-prefixed Session ID, and if you do, there is no sense to unblind it anyway.
Unblinding 15-prefixed Session IDs
To unblind a Session ID, pass it as sessionId and SOGS's public key in either hex-encoded string or as Uint8Array.
import { unblindSessionId } from "@session.js/blinded-session-id"
unblindSessionId({
sessionId: "15264c132e2e72a9c50b7a981eac11a48b3e51ae5a0ea45ea47deb519a3fa76612",
sogsPublicKey: "ac9c872e525a58970df6971655abb944a30b38853442a793b29843d20795e840",
});
// => 057aeb66e45660c3bdfb7c62706f6440226af43ec13f3b6f899c1dd4db1b8fce5bUnblinding 25-prefixed Session IDs
Unblinding 25-prefixed Session IDs is impossible.
Advanced usage
getBlindingK
generates a hash for blinding/unblinding using blake2b
import { getBlindingK } from "@session.js/blinded-session-id/utils";
const sogsPublicKey = new Uint8Array([
203, 79, 214, 25, 155, 132, 220, 54, 100, 240, 55, 51, 84, 52, 26, 1, 0, 126, 202, 169, 154, 56,
132, 150, 254, 135, 117, 185, 183, 106, 37, 59,
]);
getBlindingK(sogsPublicKey);
// => Uint8Array(32) [ 27, 203, 111, 10, 221, 88, 187, 146, 221, 11, 206, 55, 7, 86, 218, 223, 21, 123, 29, 214, 198, 182, 3, 40, 188, 123, 190, 73, 35, 122, 140, 13 ]Made for Session.js
Use Session messenger programmatically with Session.js: Session bots, custom Session clients, and more.
Donate
hloth.dev/donate · Tor: hlothdevzkti6suoksy7lcy7hmpxnr3msu5waokzaslsi2mnx5ouu4qd.onion/donate
