@livedesk/client 0.1.235 → 0.1.237
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/livedesk-client-node.js +82 -190
- package/bin/livedesk-client-update-bootstrap.cjs +316 -56
- package/bin/livedesk-client.js +111 -482
- package/package.json +6 -6
- package/src/runtime/client-runtime-server.js +20 -114
- package/src/runtime/fast-runtime-repair.js +0 -300
- package/src/security/device-credential-store.js +0 -219
- package/src/security/secure-direct-client.js +0 -224
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { decodeCanonicalBase64Url } from '@livedesk/runtime-core';
|
|
6
|
-
import { createOsSecretStore, OS_SECRET_REFERENCE } from '@livedesk/runtime-core/os-secret-store';
|
|
7
|
-
|
|
8
|
-
const STORE_VERSION = 1;
|
|
9
|
-
|
|
10
|
-
function credentialError(code) {
|
|
11
|
-
const error = new Error(code);
|
|
12
|
-
error.code = code;
|
|
13
|
-
return error;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function clean(value, maximum = 512) {
|
|
17
|
-
return String(value || '').replace(/[\0\r\n]/g, '').trim().slice(0, maximum);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function credentialRoot() {
|
|
21
|
-
const configured = clean(process.env.LIVEDESK_CLIENT_CREDENTIAL_ROOT, 4096);
|
|
22
|
-
return configured ? path.resolve(configured) : path.join(os.homedir(), '.livedesk-client');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function defaultCredentialPath(deviceId) {
|
|
26
|
-
const deviceHash = crypto.createHash('sha256').update(String(deviceId), 'utf8').digest('hex');
|
|
27
|
-
return path.join(credentialRoot(), 'security', 'device-credentials', `${deviceHash}.json`);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function legacyCredentialPath() {
|
|
31
|
-
return path.join(credentialRoot(), 'device-credential-v1.json');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function atomicPrivateJson(filePath, value) {
|
|
35
|
-
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
36
|
-
const temporary = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
37
|
-
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
38
|
-
renameSync(temporary, filePath);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function loadJson(filePath) {
|
|
42
|
-
try {
|
|
43
|
-
const value = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
44
|
-
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
45
|
-
} catch {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function requirePrivateKey(value) {
|
|
51
|
-
const der = decodeCanonicalBase64Url(value, 100, 512);
|
|
52
|
-
let key;
|
|
53
|
-
try {
|
|
54
|
-
key = crypto.createPrivateKey({ key: der, format: 'der', type: 'pkcs8' });
|
|
55
|
-
} catch {
|
|
56
|
-
throw credentialError('device-private-key-invalid');
|
|
57
|
-
}
|
|
58
|
-
if (key.asymmetricKeyType !== 'ec' || key.asymmetricKeyDetails?.namedCurve !== 'prime256v1') {
|
|
59
|
-
throw credentialError('device-private-key-invalid');
|
|
60
|
-
}
|
|
61
|
-
return key;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function parseCredential(credential) {
|
|
65
|
-
const text = String(credential || '');
|
|
66
|
-
const parts = text.split('.');
|
|
67
|
-
if (parts.length !== 2 || text.length > 8192) throw credentialError('device-credential-invalid');
|
|
68
|
-
const payloadBytes = decodeCanonicalBase64Url(parts[0], 32, 4096);
|
|
69
|
-
const signature = decodeCanonicalBase64Url(parts[1], 64, 64);
|
|
70
|
-
let payload;
|
|
71
|
-
try {
|
|
72
|
-
payload = JSON.parse(payloadBytes.toString('utf8'));
|
|
73
|
-
} catch {
|
|
74
|
-
throw credentialError('device-credential-invalid');
|
|
75
|
-
}
|
|
76
|
-
if (!payload || Number(payload.version) !== 1) throw credentialError('device-credential-invalid');
|
|
77
|
-
const hubPublicDer = decodeCanonicalBase64Url(payload.hubPublicKey, 80, 160);
|
|
78
|
-
let hubPublicKey;
|
|
79
|
-
try {
|
|
80
|
-
hubPublicKey = crypto.createPublicKey({ key: hubPublicDer, format: 'der', type: 'spki' });
|
|
81
|
-
} catch {
|
|
82
|
-
throw credentialError('device-credential-invalid');
|
|
83
|
-
}
|
|
84
|
-
if (hubPublicKey.asymmetricKeyType !== 'ec'
|
|
85
|
-
|| hubPublicKey.asymmetricKeyDetails?.namedCurve !== 'prime256v1'
|
|
86
|
-
|| !crypto.verify('sha256', Buffer.from(parts[0], 'utf8'), { key: hubPublicKey, dsaEncoding: 'ieee-p1363' }, signature)) {
|
|
87
|
-
throw credentialError('device-credential-signature-invalid');
|
|
88
|
-
}
|
|
89
|
-
const normalized = {
|
|
90
|
-
serial: clean(payload.serial, 128),
|
|
91
|
-
accountId: clean(payload.accountId, 128),
|
|
92
|
-
hubId: clean(payload.hubId, 128),
|
|
93
|
-
deviceId: clean(payload.deviceId, 128),
|
|
94
|
-
devicePublicKey: clean(payload.devicePublicKey, 512),
|
|
95
|
-
hubPublicKey: hubPublicDer.toString('base64url'),
|
|
96
|
-
issuedAt: Number(payload.issuedAt),
|
|
97
|
-
expiresAt: Number(payload.expiresAt)
|
|
98
|
-
};
|
|
99
|
-
if (!normalized.serial || !normalized.accountId || !normalized.hubId || !normalized.deviceId
|
|
100
|
-
|| !Number.isSafeInteger(normalized.issuedAt) || !Number.isSafeInteger(normalized.expiresAt)
|
|
101
|
-
|| normalized.expiresAt <= Date.now()) {
|
|
102
|
-
throw credentialError('device-credential-expired');
|
|
103
|
-
}
|
|
104
|
-
return { text, payloadText: parts[0], payload: normalized, hubPublicKey };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function createClientDeviceCredentialStore({
|
|
108
|
-
filePath = String(process.env.LIVEDESK_DEVICE_CREDENTIAL_PATH || '').trim(),
|
|
109
|
-
deviceId
|
|
110
|
-
} = {}) {
|
|
111
|
-
const normalizedDeviceId = clean(deviceId, 128);
|
|
112
|
-
if (!normalizedDeviceId) throw credentialError('device-id-required');
|
|
113
|
-
const configuredPath = clean(filePath, 4096);
|
|
114
|
-
const resolvedFilePath = configuredPath
|
|
115
|
-
? path.resolve(configuredPath)
|
|
116
|
-
: defaultCredentialPath(normalizedDeviceId);
|
|
117
|
-
const privateKeyStore = createOsSecretStore({
|
|
118
|
-
service: 'LiveDesk',
|
|
119
|
-
account: `client-device-private-key:${normalizedDeviceId}`,
|
|
120
|
-
dataDir: path.dirname(resolvedFilePath)
|
|
121
|
-
});
|
|
122
|
-
let state = loadJson(resolvedFilePath);
|
|
123
|
-
let migratedLegacy = false;
|
|
124
|
-
if (!state && !configuredPath && !existsSync(resolvedFilePath)) {
|
|
125
|
-
const legacyState = loadJson(legacyCredentialPath());
|
|
126
|
-
if (Number(legacyState?.version) === STORE_VERSION && legacyState?.deviceId === normalizedDeviceId) {
|
|
127
|
-
state = legacyState;
|
|
128
|
-
migratedLegacy = true;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
let privateKey;
|
|
132
|
-
let publicKey;
|
|
133
|
-
if (state) {
|
|
134
|
-
if (Number(state.version) !== STORE_VERSION || state.deviceId !== normalizedDeviceId) throw credentialError('device-key-state-invalid');
|
|
135
|
-
const plaintextPrivateKey = clean(state.privateKey, 1024);
|
|
136
|
-
if (plaintextPrivateKey) {
|
|
137
|
-
if (!privateKeyStore.write(plaintextPrivateKey)) throw credentialError('device-private-key-migration-failed');
|
|
138
|
-
state = { ...state, privateKeyRef: OS_SECRET_REFERENCE, updatedAt: new Date().toISOString() };
|
|
139
|
-
delete state.privateKey;
|
|
140
|
-
atomicPrivateJson(resolvedFilePath, state);
|
|
141
|
-
}
|
|
142
|
-
if (state.privateKeyRef !== OS_SECRET_REFERENCE) throw credentialError('device-private-key-reference-invalid');
|
|
143
|
-
const privateKeyText = privateKeyStore.read();
|
|
144
|
-
if (!privateKeyText) throw credentialError('device-private-key-secure-store-unavailable');
|
|
145
|
-
privateKey = requirePrivateKey(privateKeyText);
|
|
146
|
-
publicKey = crypto.createPublicKey(privateKey);
|
|
147
|
-
const publicText = publicKey.export({ format: 'der', type: 'spki' }).toString('base64url');
|
|
148
|
-
if (publicText !== state.publicKey) throw credentialError('device-key-mismatch');
|
|
149
|
-
if (migratedLegacy) {
|
|
150
|
-
atomicPrivateJson(resolvedFilePath, state);
|
|
151
|
-
try { rmSync(legacyCredentialPath(), { force: true }); } catch {}
|
|
152
|
-
}
|
|
153
|
-
} else {
|
|
154
|
-
const generated = crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
|
|
155
|
-
privateKey = generated.privateKey;
|
|
156
|
-
publicKey = generated.publicKey;
|
|
157
|
-
const privateKeyText = privateKey.export({ format: 'der', type: 'pkcs8' }).toString('base64url');
|
|
158
|
-
if (!privateKeyStore.write(privateKeyText)) throw credentialError('device-private-key-secure-store-unavailable');
|
|
159
|
-
state = {
|
|
160
|
-
version: STORE_VERSION,
|
|
161
|
-
deviceId: normalizedDeviceId,
|
|
162
|
-
publicKey: publicKey.export({ format: 'der', type: 'spki' }).toString('base64url'),
|
|
163
|
-
privateKeyRef: OS_SECRET_REFERENCE,
|
|
164
|
-
credential: '',
|
|
165
|
-
createdAt: new Date().toISOString(),
|
|
166
|
-
updatedAt: new Date().toISOString()
|
|
167
|
-
};
|
|
168
|
-
atomicPrivateJson(resolvedFilePath, state);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function saveCredential(credential) {
|
|
172
|
-
const parsed = parseCredential(credential);
|
|
173
|
-
if (parsed.payload.deviceId !== normalizedDeviceId || parsed.payload.devicePublicKey !== state.publicKey) {
|
|
174
|
-
throw credentialError('device-credential-binding-invalid');
|
|
175
|
-
}
|
|
176
|
-
state.credential = parsed.text;
|
|
177
|
-
state.accountId = parsed.payload.accountId;
|
|
178
|
-
state.hubId = parsed.payload.hubId;
|
|
179
|
-
state.updatedAt = new Date().toISOString();
|
|
180
|
-
atomicPrivateJson(resolvedFilePath, state);
|
|
181
|
-
return parsed;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function readCredential() {
|
|
185
|
-
if (!state.credential) return null;
|
|
186
|
-
try {
|
|
187
|
-
const parsed = parseCredential(state.credential);
|
|
188
|
-
if (parsed.payload.deviceId !== normalizedDeviceId || parsed.payload.devicePublicKey !== state.publicKey) return null;
|
|
189
|
-
return parsed;
|
|
190
|
-
} catch {
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function clearCredential() {
|
|
196
|
-
state.credential = '';
|
|
197
|
-
state.accountId = '';
|
|
198
|
-
state.hubId = '';
|
|
199
|
-
state.updatedAt = new Date().toISOString();
|
|
200
|
-
atomicPrivateJson(resolvedFilePath, state);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return Object.freeze({
|
|
204
|
-
filePath: resolvedFilePath,
|
|
205
|
-
deviceId: normalizedDeviceId,
|
|
206
|
-
publicKey: state.publicKey,
|
|
207
|
-
privateKey,
|
|
208
|
-
sign(message) {
|
|
209
|
-
return crypto.sign('sha256', Buffer.from(String(message || ''), 'utf8'), {
|
|
210
|
-
key: privateKey,
|
|
211
|
-
dsaEncoding: 'ieee-p1363'
|
|
212
|
-
}).toString('base64url');
|
|
213
|
-
},
|
|
214
|
-
readCredential,
|
|
215
|
-
saveCredential,
|
|
216
|
-
clearCredential,
|
|
217
|
-
parseCredential
|
|
218
|
-
});
|
|
219
|
-
}
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
import net from 'node:net';
|
|
3
|
-
import {
|
|
4
|
-
SECURE_SESSION_MAX_CLOCK_SKEW_MS,
|
|
5
|
-
SECURE_SESSION_MAX_HANDSHAKE_BYTES,
|
|
6
|
-
SECURE_SESSION_PROTOCOL,
|
|
7
|
-
SecureRecordSocket,
|
|
8
|
-
decodeCanonicalBase64Url,
|
|
9
|
-
deriveSecureSessionKey,
|
|
10
|
-
enrollmentProof,
|
|
11
|
-
fixedTimeBase64UrlEqual,
|
|
12
|
-
normalizeSecureChannel,
|
|
13
|
-
secureClientTranscript,
|
|
14
|
-
secureServerTranscript
|
|
15
|
-
} from '@livedesk/runtime-core';
|
|
16
|
-
|
|
17
|
-
const CONNECT_TIMEOUT_MS = 5_000;
|
|
18
|
-
|
|
19
|
-
function secureError(code) {
|
|
20
|
-
const error = new Error(code);
|
|
21
|
-
error.code = code;
|
|
22
|
-
return error;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function clean(value, maximum = 256) {
|
|
26
|
-
return String(value || '').replace(/[\0\r\n]/g, '').trim().slice(0, maximum);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function requireP256PublicKey(value) {
|
|
30
|
-
const der = decodeCanonicalBase64Url(value, 80, 160);
|
|
31
|
-
let key;
|
|
32
|
-
try {
|
|
33
|
-
key = crypto.createPublicKey({ key: der, format: 'der', type: 'spki' });
|
|
34
|
-
} catch {
|
|
35
|
-
throw secureError('secure-public-key-invalid');
|
|
36
|
-
}
|
|
37
|
-
if (key.asymmetricKeyType !== 'ec' || key.asymmetricKeyDetails?.namedCurve !== 'prime256v1') {
|
|
38
|
-
throw secureError('secure-public-key-invalid');
|
|
39
|
-
}
|
|
40
|
-
return key;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function connectRaw(host, port, timeoutMs) {
|
|
44
|
-
return await new Promise((resolve, reject) => {
|
|
45
|
-
const socket = net.createConnection({ host, port });
|
|
46
|
-
const timer = setTimeout(() => {
|
|
47
|
-
socket.destroy();
|
|
48
|
-
reject(secureError('secure-direct-connect-timeout'));
|
|
49
|
-
}, timeoutMs);
|
|
50
|
-
timer.unref?.();
|
|
51
|
-
socket.once('connect', () => {
|
|
52
|
-
clearTimeout(timer);
|
|
53
|
-
socket.setNoDelay(true);
|
|
54
|
-
resolve(socket);
|
|
55
|
-
});
|
|
56
|
-
socket.once('error', error => {
|
|
57
|
-
clearTimeout(timer);
|
|
58
|
-
reject(error);
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function readHandshakeLine(socket, timeoutMs) {
|
|
64
|
-
return await new Promise((resolve, reject) => {
|
|
65
|
-
let chunks = [];
|
|
66
|
-
let bytes = 0;
|
|
67
|
-
const timer = setTimeout(() => finish(secureError('secure-server-hello-timeout')), timeoutMs);
|
|
68
|
-
timer.unref?.();
|
|
69
|
-
const cleanup = () => {
|
|
70
|
-
clearTimeout(timer);
|
|
71
|
-
socket.removeListener('data', onData);
|
|
72
|
-
socket.removeListener('error', onError);
|
|
73
|
-
socket.removeListener('close', onClose);
|
|
74
|
-
};
|
|
75
|
-
const finish = (error, value) => {
|
|
76
|
-
cleanup();
|
|
77
|
-
if (error) reject(error);
|
|
78
|
-
else resolve(value);
|
|
79
|
-
};
|
|
80
|
-
const onError = error => finish(error);
|
|
81
|
-
const onClose = () => finish(secureError('secure-server-hello-closed'));
|
|
82
|
-
const onData = chunk => {
|
|
83
|
-
const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk || []);
|
|
84
|
-
bytes += incoming.length;
|
|
85
|
-
if (bytes > SECURE_SESSION_MAX_HANDSHAKE_BYTES) {
|
|
86
|
-
finish(secureError('secure-server-hello-too-large'));
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
chunks.push(incoming);
|
|
90
|
-
const combined = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, bytes);
|
|
91
|
-
const newline = combined.indexOf(0x0a);
|
|
92
|
-
if (newline < 0) return;
|
|
93
|
-
let message;
|
|
94
|
-
try {
|
|
95
|
-
message = JSON.parse(combined.subarray(0, newline).toString('utf8'));
|
|
96
|
-
} catch {
|
|
97
|
-
finish(secureError('secure-server-hello-invalid'));
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
finish(null, { message, remainder: combined.subarray(newline + 1) });
|
|
101
|
-
};
|
|
102
|
-
socket.on('data', onData);
|
|
103
|
-
socket.once('error', onError);
|
|
104
|
-
socket.once('close', onClose);
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export async function connectSecureDirect({
|
|
109
|
-
host,
|
|
110
|
-
port,
|
|
111
|
-
channel = 'control',
|
|
112
|
-
deviceId,
|
|
113
|
-
enrollmentToken,
|
|
114
|
-
credentialStore,
|
|
115
|
-
timeoutMs = CONNECT_TIMEOUT_MS
|
|
116
|
-
} = {}) {
|
|
117
|
-
const normalizedChannel = normalizeSecureChannel(channel);
|
|
118
|
-
const normalizedDeviceId = clean(deviceId, 128);
|
|
119
|
-
if (!normalizedDeviceId || !credentialStore?.privateKey || credentialStore.deviceId !== normalizedDeviceId) {
|
|
120
|
-
throw secureError('device-credential-store-required');
|
|
121
|
-
}
|
|
122
|
-
const existingCredential = credentialStore.readCredential();
|
|
123
|
-
const mode = existingCredential ? 'resume' : 'enroll';
|
|
124
|
-
if (mode === 'enroll' && normalizedChannel !== 'control') throw secureError('secure-enrollment-control-channel-required');
|
|
125
|
-
const generated = crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
|
|
126
|
-
const hello = {
|
|
127
|
-
type: 'secure.client-hello',
|
|
128
|
-
protocol: SECURE_SESSION_PROTOCOL,
|
|
129
|
-
mode,
|
|
130
|
-
channel: normalizedChannel,
|
|
131
|
-
timestamp: Date.now(),
|
|
132
|
-
nonce: crypto.randomBytes(16).toString('base64url'),
|
|
133
|
-
deviceId: normalizedDeviceId,
|
|
134
|
-
devicePublicKey: credentialStore.publicKey,
|
|
135
|
-
clientEphemeralPublicKey: generated.publicKey.export({ format: 'der', type: 'spki' }).toString('base64url'),
|
|
136
|
-
...(existingCredential ? { credential: existingCredential.text } : {})
|
|
137
|
-
};
|
|
138
|
-
const clientTranscript = secureClientTranscript(hello);
|
|
139
|
-
hello.deviceSignature = credentialStore.sign(clientTranscript);
|
|
140
|
-
if (mode === 'enroll') hello.enrollmentProof = enrollmentProof(enrollmentToken, clientTranscript);
|
|
141
|
-
|
|
142
|
-
const socket = await connectRaw(host, port, Math.max(250, Math.min(30_000, Number(timeoutMs) || CONNECT_TIMEOUT_MS)));
|
|
143
|
-
try {
|
|
144
|
-
socket.write(`${JSON.stringify(hello)}\n`);
|
|
145
|
-
const { message: response, remainder } = await readHandshakeLine(socket, timeoutMs);
|
|
146
|
-
if (response?.type === 'secure.error') throw secureError(clean(response.error, 100) || 'secure-handshake-rejected');
|
|
147
|
-
if (response?.type !== 'secure.server-hello'
|
|
148
|
-
|| response.protocol !== SECURE_SESSION_PROTOCOL
|
|
149
|
-
|| response.channel !== normalizedChannel
|
|
150
|
-
|| response.deviceId !== normalizedDeviceId) {
|
|
151
|
-
throw secureError('secure-server-hello-invalid');
|
|
152
|
-
}
|
|
153
|
-
if (!Number.isSafeInteger(Number(response.timestamp))
|
|
154
|
-
|| Math.abs(Date.now() - Number(response.timestamp)) > SECURE_SESSION_MAX_CLOCK_SKEW_MS) {
|
|
155
|
-
throw secureError('secure-server-timestamp-invalid');
|
|
156
|
-
}
|
|
157
|
-
decodeCanonicalBase64Url(response.sessionId, 16, 32);
|
|
158
|
-
decodeCanonicalBase64Url(response.serverNonce, 16, 32);
|
|
159
|
-
const serverEphemeralPublicKey = requireP256PublicKey(response.serverEphemeralPublicKey);
|
|
160
|
-
const parsedCredential = credentialStore.parseCredential(response.credential);
|
|
161
|
-
if (parsedCredential.payload.deviceId !== normalizedDeviceId
|
|
162
|
-
|| parsedCredential.payload.devicePublicKey !== credentialStore.publicKey
|
|
163
|
-
|| response.hubId !== parsedCredential.payload.hubId
|
|
164
|
-
|| response.accountId !== parsedCredential.payload.accountId) {
|
|
165
|
-
throw secureError('device-credential-binding-invalid');
|
|
166
|
-
}
|
|
167
|
-
if (existingCredential
|
|
168
|
-
&& (parsedCredential.payload.hubId !== existingCredential.payload.hubId
|
|
169
|
-
|| parsedCredential.payload.accountId !== existingCredential.payload.accountId
|
|
170
|
-
|| parsedCredential.payload.hubPublicKey !== existingCredential.payload.hubPublicKey)) {
|
|
171
|
-
throw secureError('secure-hub-identity-changed');
|
|
172
|
-
}
|
|
173
|
-
const serverTranscript = secureServerTranscript(response, clientTranscript);
|
|
174
|
-
if (mode === 'enroll') {
|
|
175
|
-
const expectedServerProof = enrollmentProof(enrollmentToken, serverTranscript);
|
|
176
|
-
if (!fixedTimeBase64UrlEqual(response.enrollmentServerProof, expectedServerProof, 32)) {
|
|
177
|
-
throw secureError('secure-enrollment-server-proof-invalid');
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
const hubSignature = decodeCanonicalBase64Url(response.hubSignature, 64, 64);
|
|
181
|
-
if (!crypto.verify('sha256', Buffer.from(serverTranscript, 'utf8'), {
|
|
182
|
-
key: parsedCredential.hubPublicKey,
|
|
183
|
-
dsaEncoding: 'ieee-p1363'
|
|
184
|
-
}, hubSignature)) {
|
|
185
|
-
throw secureError('secure-hub-signature-invalid');
|
|
186
|
-
}
|
|
187
|
-
const sharedSecret = crypto.diffieHellman({ privateKey: generated.privateKey, publicKey: serverEphemeralPublicKey });
|
|
188
|
-
let sessionKey;
|
|
189
|
-
try {
|
|
190
|
-
sessionKey = deriveSecureSessionKey({
|
|
191
|
-
sharedSecret,
|
|
192
|
-
clientTranscript,
|
|
193
|
-
serverTranscript,
|
|
194
|
-
sessionId: response.sessionId,
|
|
195
|
-
channel: normalizedChannel
|
|
196
|
-
});
|
|
197
|
-
} finally {
|
|
198
|
-
sharedSecret.fill(0);
|
|
199
|
-
}
|
|
200
|
-
if (!existingCredential || existingCredential.text !== parsedCredential.text) credentialStore.saveCredential(parsedCredential.text);
|
|
201
|
-
const secureSocket = new SecureRecordSocket(socket, {
|
|
202
|
-
sessionKey,
|
|
203
|
-
sessionId: response.sessionId,
|
|
204
|
-
channel: normalizedChannel,
|
|
205
|
-
role: 'client',
|
|
206
|
-
securityContext: {
|
|
207
|
-
protocol: SECURE_SESSION_PROTOCOL,
|
|
208
|
-
accountId: parsedCredential.payload.accountId,
|
|
209
|
-
hubId: parsedCredential.payload.hubId,
|
|
210
|
-
deviceId: normalizedDeviceId,
|
|
211
|
-
credentialSerial: parsedCredential.payload.serial,
|
|
212
|
-
authenticated: true,
|
|
213
|
-
encrypted: true
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
sessionKey.fill(0);
|
|
217
|
-
secureSocket.__liveDeskDirectSecure = true;
|
|
218
|
-
if (remainder.length > 0) secureSocket.feedEncrypted(remainder);
|
|
219
|
-
return secureSocket;
|
|
220
|
-
} catch (error) {
|
|
221
|
-
socket.destroy();
|
|
222
|
-
throw error;
|
|
223
|
-
}
|
|
224
|
-
}
|