Skip to content

Commit 8dbcb54

Browse files
remicolinclaude
andcommitted
fix(extension): port disclose-check and demo-check harnesses to transfer protocol v3
Both senders still spoke the v1 protocol (no hello room, no linkSecret binding, no AAD) and were refused by the extension since 22c6bb0. They are not CI gates, so the rot was silent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 00552be commit 8dbcb54

2 files changed

Lines changed: 108 additions & 42 deletions

File tree

packages/chrome-extension/harness/demo-check.mjs

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { tmpdir } from 'node:os';
55
import { dirname, join, resolve } from 'node:path';
66
import { fileURLToPath } from 'node:url';
77

8+
import {
9+
deriveTransferKey,
10+
transferAad,
11+
} from '@selfxyz/mobile-sdk-alpha/utils/sas';
812
import puppeteer from 'puppeteer';
913
import { io } from 'socket.io-client';
1014

@@ -19,6 +23,7 @@ const DEMO_URL = 'http://localhost:5199';
1923
const DOC_ID = 'c'.repeat(64);
2024
const ACCOUNT = {
2125
version: 1,
26+
linkedAt: '2026-07-29T00:00:00.000Z',
2227
mnemonic: {
2328
phrase: 'test test test test test test test test test test test junk',
2429
password: '',
@@ -47,9 +52,10 @@ const ACCOUNT = {
4752
},
4853
};
4954

50-
function encryptEnvelope(sharedKey, buf) {
55+
function encryptEnvelope(sharedKey, buf, aad) {
5156
const nonce = randomBytes(12);
5257
const cipher = createCipheriv('aes-256-gcm', sharedKey, nonce);
58+
if (aad) cipher.setAAD(Buffer.from(aad));
5359
const cipherText = Buffer.concat([cipher.update(buf), cipher.final()]);
5460
return {
5561
nonce: nonce.toString('base64'),
@@ -60,33 +66,60 @@ function encryptEnvelope(sharedKey, buf) {
6066

6167
function importAccount(qr) {
6268
return new Promise((resolvePromise, reject) => {
69+
const timer = setTimeout(() => {
70+
socket.close();
71+
reject(new Error('import ack timeout'));
72+
}, 60_000);
73+
6374
const socket = io(`${qr.relay}/websocket`, {
6475
path: '/',
6576
transports: ['websocket'],
6677
forceNew: true,
6778
query: { sessionId: qr.transferSessionId, clientType: 'web' },
6879
});
69-
const timer = setTimeout(() => {
70-
socket.close();
71-
reject(new Error('import ack timeout'));
72-
}, 60_000);
73-
socket.on('mobile_status', data => {
74-
if (data?.status === 'mobile_connected') {
75-
const ecdh = createECDH('prime256v1');
76-
ecdh.generateKeys();
77-
const shared = ecdh.computeSecret(
78-
Buffer.from(qr.receiverPublicKey, 'hex'),
79-
);
80-
socket.emit('self_app', {
81-
sessionId: qr.transferSessionId,
82-
transferType: 'self-account-transfer',
83-
senderPublicKey: ecdh.getPublicKey('hex', 'uncompressed'),
84-
envelope: encryptEnvelope(
85-
shared,
86-
Buffer.from(JSON.stringify(ACCOUNT)),
87-
),
80+
81+
socket.on('connect', () => {
82+
const ecdh = createECDH('prime256v1');
83+
ecdh.generateKeys();
84+
const shared = ecdh.computeSecret(
85+
Buffer.from(qr.receiverPublicKey, 'hex'),
86+
);
87+
const senderPublicKey = ecdh.getPublicKey('hex', 'uncompressed');
88+
const binding = {
89+
sessionId: qr.transferSessionId,
90+
receiverPublicKey: qr.receiverPublicKey,
91+
senderPublicKey,
92+
linkSecret: qr.linkSecret,
93+
};
94+
const helloSocket = io(`${qr.relay}/websocket`, {
95+
path: '/',
96+
transports: ['websocket'],
97+
forceNew: true,
98+
query: { sessionId: qr.helloSessionId, clientType: 'web' },
99+
});
100+
helloSocket.on('connect', () => {
101+
helloSocket.emit('self_app', {
102+
sessionId: qr.helloSessionId,
103+
transferType: 'self-account-transfer-hello',
104+
senderPublicKey,
88105
});
89-
}
106+
setTimeout(() => {
107+
helloSocket.close();
108+
socket.emit('self_app', {
109+
sessionId: qr.transferSessionId,
110+
transferType: 'self-account-transfer',
111+
senderPublicKey,
112+
envelope: encryptEnvelope(
113+
Buffer.from(deriveTransferKey(new Uint8Array(shared), binding)),
114+
Buffer.from(JSON.stringify(ACCOUNT)),
115+
transferAad(binding),
116+
),
117+
});
118+
}, 1_500);
119+
});
120+
});
121+
122+
socket.on('mobile_status', data => {
90123
if (data?.status === 'proof_verified') {
91124
clearTimeout(timer);
92125
socket.close();

packages/chrome-extension/harness/disclose-check.mjs

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import { tmpdir } from 'node:os';
99
import { dirname, join, resolve } from 'node:path';
1010
import { fileURLToPath } from 'node:url';
1111

12+
import {
13+
deriveTransferKey,
14+
transferAad,
15+
} from '@selfxyz/mobile-sdk-alpha/utils/sas';
1216
import puppeteer from 'puppeteer';
1317
import { io } from 'socket.io-client';
1418

@@ -22,6 +26,7 @@ const RELAY = 'wss://websocket.staging.self.xyz';
2226
const DOC_ID = 'b'.repeat(64);
2327
const ACCOUNT = {
2428
version: 1,
29+
linkedAt: '2026-07-29T00:00:00.000Z',
2530
mnemonic: {
2631
phrase: 'test test test test test test test test test test test junk',
2732
password: '',
@@ -50,9 +55,10 @@ const ACCOUNT = {
5055
},
5156
};
5257

53-
function encryptEnvelope(sharedKey, plaintextBuf) {
58+
function encryptEnvelope(sharedKey, plaintextBuf, aad) {
5459
const nonce = randomBytes(12);
5560
const cipher = createCipheriv('aes-256-gcm', sharedKey, nonce);
61+
if (aad) cipher.setAAD(Buffer.from(aad));
5662
const cipherText = Buffer.concat([
5763
cipher.update(plaintextBuf),
5864
cipher.final(),
@@ -66,33 +72,60 @@ function encryptEnvelope(sharedKey, plaintextBuf) {
6672

6773
function importAccount(qr) {
6874
return new Promise((resolvePromise, reject) => {
75+
const timer = setTimeout(() => {
76+
socket.close();
77+
reject(new Error('import ack timeout'));
78+
}, 60_000);
79+
6980
const socket = io(`${qr.relay}/websocket`, {
7081
path: '/',
7182
transports: ['websocket'],
7283
forceNew: true,
7384
query: { sessionId: qr.transferSessionId, clientType: 'web' },
7485
});
75-
const timer = setTimeout(() => {
76-
socket.close();
77-
reject(new Error('import ack timeout'));
78-
}, 60_000);
79-
socket.on('mobile_status', data => {
80-
if (data?.status === 'mobile_connected') {
81-
const ecdh = createECDH('prime256v1');
82-
ecdh.generateKeys();
83-
const shared = ecdh.computeSecret(
84-
Buffer.from(qr.receiverPublicKey, 'hex'),
85-
);
86-
socket.emit('self_app', {
87-
sessionId: qr.transferSessionId,
88-
transferType: 'self-account-transfer',
89-
senderPublicKey: ecdh.getPublicKey('hex', 'uncompressed'),
90-
envelope: encryptEnvelope(
91-
shared,
92-
Buffer.from(JSON.stringify(ACCOUNT), 'utf8'),
93-
),
86+
87+
socket.on('connect', () => {
88+
const ecdh = createECDH('prime256v1');
89+
ecdh.generateKeys();
90+
const shared = ecdh.computeSecret(
91+
Buffer.from(qr.receiverPublicKey, 'hex'),
92+
);
93+
const senderPublicKey = ecdh.getPublicKey('hex', 'uncompressed');
94+
const binding = {
95+
sessionId: qr.transferSessionId,
96+
receiverPublicKey: qr.receiverPublicKey,
97+
senderPublicKey,
98+
linkSecret: qr.linkSecret,
99+
};
100+
const helloSocket = io(`${qr.relay}/websocket`, {
101+
path: '/',
102+
transports: ['websocket'],
103+
forceNew: true,
104+
query: { sessionId: qr.helloSessionId, clientType: 'web' },
105+
});
106+
helloSocket.on('connect', () => {
107+
helloSocket.emit('self_app', {
108+
sessionId: qr.helloSessionId,
109+
transferType: 'self-account-transfer-hello',
110+
senderPublicKey,
94111
});
95-
}
112+
setTimeout(() => {
113+
helloSocket.close();
114+
socket.emit('self_app', {
115+
sessionId: qr.transferSessionId,
116+
transferType: 'self-account-transfer',
117+
senderPublicKey,
118+
envelope: encryptEnvelope(
119+
Buffer.from(deriveTransferKey(new Uint8Array(shared), binding)),
120+
Buffer.from(JSON.stringify(ACCOUNT), 'utf8'),
121+
transferAad(binding),
122+
),
123+
});
124+
}, 1_500);
125+
});
126+
});
127+
128+
socket.on('mobile_status', data => {
96129
if (data?.status === 'proof_verified') {
97130
clearTimeout(timer);
98131
socket.close();

0 commit comments

Comments
 (0)