aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/transport.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/transport.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js71
1 files changed, 50 insertions, 21 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index b504a28..3586cb7 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -39,6 +39,33 @@ async function _pkEdFromSk(skPkcs8B64) {
return pad ? b64 + '='.repeat(4 - pad) : b64;
}
+/**
+ * This account's identity on one node, as the rest of the transport sees it:
+ * two public keys, a signature and an X25519 agreement — never a private key.
+ *
+ * In a browser the keys are in this page, and this wraps them. In the desktop
+ * application they stay in the main process, which signs and agrees on the
+ * page's behalf (`platform.keys`), and the object has the same shape — so
+ * nothing below knows or cares where the keys are. `raw` exists only for keys
+ * held here, for the one thing that needs them: re-sealing a bundle during a
+ * passphrase change.
+ */
+async function _identityFromKeys(skEdB64, skXB64) {
+ const skXRaw = Uint8Array.from(atob(skXB64), c => c.charCodeAt(0));
+ return {
+ pkEdB64: await _pkEdFromSk(skEdB64),
+ pkXB64: await _pkFromSk(skXB64),
+ sign: (bytes) => window.MeshBayKeys.signBytes(skEdB64, bytes),
+ async shared(peerPkRaw) {
+ const sk = await crypto.subtle.importKey(
+ 'pkcs8', skXRaw, { name: 'X25519' }, false, ['deriveBits']);
+ const pk = await crypto.subtle.importKey('raw', peerPkRaw, { name: 'X25519' }, false, []);
+ return crypto.subtle.deriveBits({ name: 'X25519', public: pk }, sk, 256);
+ },
+ raw: { skEdB64, skXB64 },
+ };
+}
+
// Segments of 256 KB: 24 in flight is 6 MB, enough to keep playback fed over a
// slow link and small enough that nothing accumulates.
// How long to collect ICE candidates before sending the offer anyway. Long
@@ -506,7 +533,7 @@ class MeshBayTransport {
// is being torn down.
this._closed = false;
// The arguments connect() was last given, minus the token (refreshed at
- // reconnect time — see onNeedToken) and sessionKeys (kept live on `this`,
+ // reconnect time — see onNeedToken) and the identity (kept live on `this`,
// since a reconnect must reuse the identity connect() settled on, not
// whatever the very first caller passed in — see _reconnectLoop).
this._connectArgs = null;
@@ -613,7 +640,12 @@ class MeshBayTransport {
// refresh the hub session token (see group-page.js's ensureFreshToken).
set onNeedToken(fn) { this._onNeedToken = fn; }
- get sessionKeys() { return this._sessionKeys; }
+ get identity() { return this._identity; }
+ /** Signs with this node's identity, or null when there is none yet. */
+ get signFn() {
+ const id = this._identity;
+ return id ? (transcript) => id.sign(transcript) : null;
+ }
/** Set on a first join: the identity created for this node, still to be left with it. */
get newNodeBundle() { return this._newNodeBundle || null; }
@@ -662,7 +694,7 @@ class MeshBayTransport {
return (await r.json()).mnp_token;
}
- async connect(nodeId, jwtToken, groupId, gekRaw, sessionKeys, bundleKey, username,
+ async connect(nodeId, jwtToken, groupId, gekRaw, identity, bundleKey, username,
userId, joinCode, recoveryKey, joinNodePk, nodePk) {
// Remembered for _reconnectLoop, which calls connect() again with these
// same values (plus a freshly-fetched token and the identity connect()
@@ -683,7 +715,7 @@ class MeshBayTransport {
// never actually trying the fresh token connect() had just been handed.
this._accessToken = jwtToken;
this._gekRaw = gekRaw || null;
- this._sessionKeys = sessionKeys || null;
+ this._identity = identity || null;
this._bundleKey = bundleKey || null;
this._recoveryKey = recoveryKey || null;
this._username = username || null;
@@ -965,7 +997,7 @@ class MeshBayTransport {
// them — and an operator who cracks the copy on their own disk gets a key
// that opens nothing anywhere else.
let fresh = false;
- if (!this._sessionKeys && this._bundleKey && window.MeshBayKeys) {
+ if (!this._identity && this._bundleKey && window.MeshBayKeys) {
const K = window.MeshBayKeys;
// A bundle is sealed for this account on this node — the key the node
// just proved above, and no other.
@@ -1021,8 +1053,7 @@ class MeshBayTransport {
}
if (keys) {
- const pkXB64 = await _pkFromSk(keys.skX);
- this._sessionKeys = { skXB64: keys.skX, skEdB64: keys.skEd, pkXB64 };
+ this._identity = await _identityFromKeys(keys.skEd, keys.skX);
} else {
// Either the node has never seen us, or it holds a stale bundle we
// cannot open (wrapped under a passphrase we no longer use, with no
@@ -1032,9 +1063,7 @@ class MeshBayTransport {
// copy is left too when a recovery key is in hand (§4.3).
const id = await K.generateNodeIdentity(
this._bundleKey, this._recoveryKey, sealedFor);
- this._sessionKeys = {
- skEdB64: id.skEdB64, skXB64: id.skXB64, pkXB64: id.pkXB64,
- };
+ this._identity = await _identityFromKeys(id.skEdB64, id.skXB64);
this._newNodeBundle = id.bundleEnc;
this._newNodeBundleRecovery = id.bundleEncRecovery || null;
fresh = true;
@@ -1043,15 +1072,16 @@ class MeshBayTransport {
// An identity this node already knows still needs its group key, which the
// node wraps on every connection.
- if (!gekRaw && this._sessionKeys && !fresh) {
+ if (!gekRaw && this._identity && !fresh) {
const bundleResp = await this._sendAndWait({
type: 'gek_bundle_fetch', v: '0.1',
});
if (bundleResp.type === 'gek_bundle_resp' && bundleResp.found) {
- const skXRaw = Uint8Array.from(atob(this._sessionKeys.skXB64), c => c.charCodeAt(0));
- const myPkX = Uint8Array.from(atob(this._sessionKeys.pkXB64), c => c.charCodeAt(0));
+ const id = this._identity;
+ const myPkX = Uint8Array.from(atob(id.pkXB64), c => c.charCodeAt(0));
try {
- gekRaw = await window.MeshBayCrypto.unwrapGEK(bundleResp, skXRaw, myPkX);
+ gekRaw = await window.MeshBayCrypto.unwrapGEK(
+ bundleResp, (pk) => id.shared(pk), myPkX);
this._gekRaw = gekRaw;
} catch (e) {
console.warn('[MeshBay] stored GEK bundle did not open; joining instead');
@@ -1071,7 +1101,7 @@ class MeshBayTransport {
const linkRefusal = _linkJoinRefusal(joinNodePk, joinCode, this.nodePk);
if (linkRefusal) {
this._joinError = linkRefusal;
- } else if (!gekRaw && this._sessionKeys && userId) {
+ } else if (!gekRaw && this._identity && userId) {
try {
gekRaw = await this.joinGroup(userId, groupId, joinCode);
} catch (e) {
@@ -1080,7 +1110,7 @@ class MeshBayTransport {
}
}
- if (!gekRaw && !this._sessionKeys) {
+ if (!gekRaw && !this._identity) {
// No key in this browser to sign or unwrap with — `bundleKey` was null.
// The caller (group-page.js) shows a passphrase prompt on this reason
// and retries; a code prompt would be useless, since a code proves who
@@ -1237,7 +1267,7 @@ class MeshBayTransport {
* hangs, the textbox is dead" report. Every exit below names itself.
*/
async _announceDevice() {
- if (!this._sessionKeys || !this._sessionKeys.skEdB64) {
+ if (!this._identity) {
return this._setDevicePk('', 'no identity key in this session');
}
if (!this._nonceNode || !this.nodePk || !this._userId) {
@@ -1248,13 +1278,12 @@ class MeshBayTransport {
// Derived from our own secret key, never read back from anywhere — the same
// rule as pairOperator: signing a public key someone handed us is the
// substitution this mechanism exists to close.
- const pkEdB64 = await _pkEdFromSk(this._sessionKeys.skEdB64);
+ const pkEdB64 = this._identity.pkEdB64;
const ts = Math.floor(Date.now() / 1000);
const transcript = C.deviceHelloTranscript(
this.nodePk, this._groupId || '', this._userId, pkEdB64,
this._nonceNode, ts);
- const sig = await window.MeshBayKeys.signBytes(
- this._sessionKeys.skEdB64, transcript);
+ const sig = await this._identity.sign(transcript);
const resp = await this._sendAndWait({
type: 'device_hello', v: '2.0', pk_ed25519: pkEdB64, ts, sig,
@@ -1325,7 +1354,7 @@ class MeshBayTransport {
let ack;
try {
ack = await this.connect(args.nodeId, token, args.groupId, args.gekRaw,
- this._sessionKeys, args.bundleKey, args.username,
+ this._identity, args.bundleKey, args.username,
args.userId, args.joinCode, undefined,
args.joinNodePk, args.nodePk);
} finally {