@onekeyfe/hd-transport-react-native 1.2.3-alpha.10 → 1.2.3-alpha.11
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/dist/BleManager.d.ts +5 -6
- package/dist/BleManager.d.ts.map +1 -1
- package/dist/index.d.ts +10 -35
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +151 -396
- package/package.json +6 -6
- package/src/BleManager.ts +38 -78
- package/src/__tests__/enumerate.test.ts +0 -15
- package/src/__tests__/protocolReprobe.test.ts +93 -1
- package/src/__tests__/protocolV2Link.test.ts +0 -15
- package/src/index.ts +178 -162
- package/dist/bleEncryption.d.ts +0 -17
- package/dist/bleEncryption.d.ts.map +0 -1
- package/dist/bleKeyMissing.d.ts +0 -7
- package/dist/bleKeyMissing.d.ts.map +0 -1
- package/src/__tests__/androidEncryption.test.ts +0 -482
- package/src/__tests__/androidKeyMissing.test.ts +0 -307
- package/src/bleEncryption.ts +0 -267
- package/src/bleKeyMissing.ts +0 -106
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
import EventEmitter from 'events';
|
|
2
|
-
|
|
3
|
-
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
4
|
-
|
|
5
|
-
import ReactNativeBleTransport, {
|
|
6
|
-
ANDROID_KEY_MISSING_GRACE_MS,
|
|
7
|
-
ANDROID_KEY_MISSING_LINK_WINDOW_MS,
|
|
8
|
-
} from '../index';
|
|
9
|
-
import { SYSTEM_BONDING_RESTART_WINDOW_MS, onDeviceBondState } from '../BleManager';
|
|
10
|
-
import { stopBleKeyMissingTracking } from '../bleKeyMissing';
|
|
11
|
-
import protocolV1Schema from './protocolV1SchemaFixture';
|
|
12
|
-
|
|
13
|
-
let mockKeyMissingSupported = true;
|
|
14
|
-
let mockEmitKeyMissing: ((event: { id?: string }) => void) | undefined;
|
|
15
|
-
let mockEmitBondState: ((peripheral: unknown) => void) | undefined;
|
|
16
|
-
|
|
17
|
-
jest.mock(
|
|
18
|
-
'react-native',
|
|
19
|
-
() => ({
|
|
20
|
-
Platform: { OS: 'android', select: (spec: Record<string, unknown>) => spec.android },
|
|
21
|
-
PermissionsAndroid: {
|
|
22
|
-
PERMISSIONS: {},
|
|
23
|
-
RESULTS: {},
|
|
24
|
-
request: jest.fn(),
|
|
25
|
-
requestMultiple: jest.fn(),
|
|
26
|
-
},
|
|
27
|
-
}),
|
|
28
|
-
{ virtual: true }
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
jest.mock('react-native-ble-plx', () => ({
|
|
32
|
-
BleATTErrorCode: { InvalidHandle: 1 },
|
|
33
|
-
BleError: Error,
|
|
34
|
-
BleErrorCode: { DeviceDisconnected: 201, OperationStartFailed: 601 },
|
|
35
|
-
BleManager: jest.fn(),
|
|
36
|
-
ScanMode: { LowLatency: 2 },
|
|
37
|
-
}));
|
|
38
|
-
|
|
39
|
-
jest.mock('@onekeyfe/react-native-ble-utils', () => ({
|
|
40
|
-
__esModule: true,
|
|
41
|
-
default: {
|
|
42
|
-
getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
43
|
-
getBondedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
44
|
-
pairDevice: jest.fn(() => Promise.resolve({ bonded: true, bonding: false })),
|
|
45
|
-
supportsDeviceKeyMissing: jest.fn(() => mockKeyMissingSupported),
|
|
46
|
-
onDeviceKeyMissing: jest.fn((callback: (event: { id?: string }) => void) => {
|
|
47
|
-
mockEmitKeyMissing = callback;
|
|
48
|
-
return () => {
|
|
49
|
-
mockEmitKeyMissing = undefined;
|
|
50
|
-
};
|
|
51
|
-
}),
|
|
52
|
-
onDeviceBondState: jest.fn((callback: (peripheral: unknown) => void) => {
|
|
53
|
-
mockEmitBondState = callback;
|
|
54
|
-
return () => {
|
|
55
|
-
mockEmitBondState = undefined;
|
|
56
|
-
};
|
|
57
|
-
}),
|
|
58
|
-
},
|
|
59
|
-
}));
|
|
60
|
-
|
|
61
|
-
const UUID = 'CC:06:1C:FD:48:E9';
|
|
62
|
-
|
|
63
|
-
const flush = () =>
|
|
64
|
-
new Promise(resolve => {
|
|
65
|
-
setImmediate(resolve);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
const advance = async (ms: number) => {
|
|
69
|
-
jest.advanceTimersByTime(ms);
|
|
70
|
-
await flush();
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// What ble-plx reports when the device drops the link while the first write is queued.
|
|
74
|
-
const peerTerminated = () =>
|
|
75
|
-
Promise.reject(
|
|
76
|
-
Object.assign(new Error(`Device ${UUID} was disconnected`), {
|
|
77
|
-
errorCode: 201,
|
|
78
|
-
attErrorCode: 19,
|
|
79
|
-
reason: 'Disconnected with status 19 (GATT_CONN_TERMINATE_PEER_USER)',
|
|
80
|
-
})
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
function createConnectedTransport(linkAgeMs = 0) {
|
|
84
|
-
const t = new ReactNativeBleTransport({});
|
|
85
|
-
t.configure(protocolV1Schema);
|
|
86
|
-
(t as any).deviceProtocol.set(UUID, 'V1');
|
|
87
|
-
(t as any).getCachedTransport = () => ({
|
|
88
|
-
writeCharacteristic: { writeWithoutResponse: jest.fn(peerTerminated) },
|
|
89
|
-
});
|
|
90
|
-
(t as any).androidLinkStartedAt.set(UUID, Date.now() - linkAgeMs);
|
|
91
|
-
return t;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const settle = (promise: Promise<unknown>) => {
|
|
95
|
-
const outcome: { error?: { errorCode?: unknown }; done: boolean } = { done: false };
|
|
96
|
-
promise.then(
|
|
97
|
-
() => {
|
|
98
|
-
outcome.done = true;
|
|
99
|
-
},
|
|
100
|
-
error => {
|
|
101
|
-
outcome.error = error;
|
|
102
|
-
outcome.done = true;
|
|
103
|
-
}
|
|
104
|
-
);
|
|
105
|
-
return outcome;
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
describe('Android key missing', () => {
|
|
109
|
-
beforeEach(() => {
|
|
110
|
-
jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
|
|
111
|
-
mockKeyMissingSupported = true;
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
afterEach(() => {
|
|
115
|
-
stopBleKeyMissingTracking();
|
|
116
|
-
jest.clearAllTimers();
|
|
117
|
-
jest.useRealTimers();
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe('link dropped right after connect', () => {
|
|
121
|
-
test('reports an invalid bond when the system says the keys are missing', async () => {
|
|
122
|
-
const outcome = settle(createConnectedTransport().call(UUID, 'GetFeatures', {}));
|
|
123
|
-
await flush();
|
|
124
|
-
// The broadcast is delivered separately and can trail the GATT disconnect.
|
|
125
|
-
mockEmitKeyMissing?.({ id: UUID.toLowerCase() });
|
|
126
|
-
await flush();
|
|
127
|
-
|
|
128
|
-
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleBondInvalid);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test('keeps the original error when no key missing follows', async () => {
|
|
132
|
-
const outcome = settle(createConnectedTransport().call(UUID, 'GetFeatures', {}));
|
|
133
|
-
await flush();
|
|
134
|
-
expect(outcome.done).toBe(false);
|
|
135
|
-
|
|
136
|
-
await advance(ANDROID_KEY_MISSING_GRACE_MS);
|
|
137
|
-
|
|
138
|
-
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test('ignores key missing reported for an earlier link', async () => {
|
|
142
|
-
const transport = createConnectedTransport();
|
|
143
|
-
const earlier = settle(transport.call(UUID, 'GetFeatures', {}));
|
|
144
|
-
await flush();
|
|
145
|
-
mockEmitKeyMissing?.({ id: UUID });
|
|
146
|
-
await flush();
|
|
147
|
-
expect(earlier.error?.errorCode).toBe(HardwareErrorCode.BleBondInvalid);
|
|
148
|
-
|
|
149
|
-
// The user re-paired; a later link that drops is an ordinary disconnect.
|
|
150
|
-
await advance(1000);
|
|
151
|
-
(transport as any).androidLinkStartedAt.set(UUID, Date.now());
|
|
152
|
-
const later = settle(transport.call(UUID, 'GetFeatures', {}));
|
|
153
|
-
await flush();
|
|
154
|
-
await advance(ANDROID_KEY_MISSING_GRACE_MS);
|
|
155
|
-
|
|
156
|
-
expect(later.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
test('ignores key missing for another device', async () => {
|
|
160
|
-
const outcome = settle(createConnectedTransport().call(UUID, 'GetFeatures', {}));
|
|
161
|
-
await flush();
|
|
162
|
-
mockEmitKeyMissing?.({ id: 'E9:B9:F4:DD:DE:CD' });
|
|
163
|
-
await advance(ANDROID_KEY_MISSING_GRACE_MS);
|
|
164
|
-
|
|
165
|
-
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test('does not wait on a link that has been up for a while', async () => {
|
|
169
|
-
const transport = createConnectedTransport(ANDROID_KEY_MISSING_LINK_WINDOW_MS + 1);
|
|
170
|
-
const outcome = settle(transport.call(UUID, 'GetFeatures', {}));
|
|
171
|
-
await flush();
|
|
172
|
-
|
|
173
|
-
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test('does not wait when the OS or native build never reports key missing', async () => {
|
|
177
|
-
mockKeyMissingSupported = false;
|
|
178
|
-
const outcome = settle(createConnectedTransport().call(UUID, 'GetFeatures', {}));
|
|
179
|
-
await flush();
|
|
180
|
-
|
|
181
|
-
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
describe('acquire over a bond the device no longer holds', () => {
|
|
186
|
-
// Android still reports BONDED, so connect, MTU and GATT setup all succeed and the
|
|
187
|
-
// protocol probe is the first operation to meet the dropped link.
|
|
188
|
-
function createStaleBondHarness() {
|
|
189
|
-
const writeCharacteristic = {
|
|
190
|
-
uuid: '00000002-0000-1000-8000-00805f9b34fb',
|
|
191
|
-
isWritableWithoutResponse: true,
|
|
192
|
-
writeWithoutResponse: jest.fn(peerTerminated),
|
|
193
|
-
};
|
|
194
|
-
const notifyCharacteristic = {
|
|
195
|
-
uuid: '00000003-0000-1000-8000-00805f9b34fb',
|
|
196
|
-
isNotifiable: true,
|
|
197
|
-
monitor: jest.fn(() => ({ remove: jest.fn() })),
|
|
198
|
-
};
|
|
199
|
-
const device = {
|
|
200
|
-
id: UUID,
|
|
201
|
-
name: 'Pro 2 48E9',
|
|
202
|
-
localName: 'Pro 2 48E9',
|
|
203
|
-
mtu: 247,
|
|
204
|
-
serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
|
|
205
|
-
isConnected: jest.fn(() => Promise.resolve(true)),
|
|
206
|
-
cancelConnection: jest.fn(() => Promise.resolve()),
|
|
207
|
-
discoverAllServicesAndCharacteristics: jest.fn(() => Promise.resolve()),
|
|
208
|
-
characteristicsForService: jest.fn(() =>
|
|
209
|
-
Promise.resolve([writeCharacteristic, notifyCharacteristic])
|
|
210
|
-
),
|
|
211
|
-
services: jest.fn(() => Promise.resolve([])),
|
|
212
|
-
onDisconnected: jest.fn(() => ({ remove: jest.fn() })),
|
|
213
|
-
};
|
|
214
|
-
const transport = new ReactNativeBleTransport({});
|
|
215
|
-
(transport as any).blePlxManager = {
|
|
216
|
-
devices: jest.fn(() => Promise.resolve([device])),
|
|
217
|
-
connectedDevices: jest.fn(() => Promise.resolve([])),
|
|
218
|
-
cancelTransaction: jest.fn(() => Promise.resolve()),
|
|
219
|
-
cancelDeviceConnection: jest.fn(() => Promise.resolve()),
|
|
220
|
-
onStateChange: jest.fn((listener: (state: string) => void) => {
|
|
221
|
-
setImmediate(() => listener('PoweredOn'));
|
|
222
|
-
return { remove: jest.fn() };
|
|
223
|
-
}),
|
|
224
|
-
state: jest.fn(() => Promise.resolve('PoweredOn')),
|
|
225
|
-
};
|
|
226
|
-
transport.init(
|
|
227
|
-
{ debug: jest.fn(), error: jest.fn(), warn: jest.fn() } as any,
|
|
228
|
-
new EventEmitter()
|
|
229
|
-
);
|
|
230
|
-
transport.configure(protocolV1Schema);
|
|
231
|
-
return { transport, write: writeCharacteristic.writeWithoutResponse };
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async function acquireUntilSettled(keyMissing: boolean) {
|
|
235
|
-
const { transport, write } = createStaleBondHarness();
|
|
236
|
-
const outcome = settle(transport.acquire({ uuid: UUID }));
|
|
237
|
-
for (let i = 0; i < 100 && !outcome.done; i += 1) {
|
|
238
|
-
// eslint-disable-next-line no-await-in-loop
|
|
239
|
-
await advance(100);
|
|
240
|
-
// The broadcast follows the dropped link, which the probe write is first to meet.
|
|
241
|
-
if (keyMissing && write.mock.calls.length > 0) mockEmitKeyMissing?.({ id: UUID });
|
|
242
|
-
}
|
|
243
|
-
expect(write).toHaveBeenCalled();
|
|
244
|
-
return JSON.parse(JSON.stringify(ERRORS.serializeError({ error: outcome.error })));
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
test('ends as an invalid bond that survives the Core response path', async () => {
|
|
248
|
-
await expect(acquireUntilSettled(true)).resolves.toEqual({
|
|
249
|
-
code: HardwareErrorCode.BleBondInvalid,
|
|
250
|
-
error: expect.stringContaining('Forget the device'),
|
|
251
|
-
params: { phase: 'connect', reason: 'key_missing' },
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
test('keeps the legacy error when the system reports nothing', async () => {
|
|
256
|
-
await expect(acquireUntilSettled(false)).resolves.toMatchObject({
|
|
257
|
-
code: HardwareErrorCode.BleDeviceNotBonded,
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
describe('waiting for bonding', () => {
|
|
263
|
-
const bondState = (preState: string, state: string) =>
|
|
264
|
-
mockEmitBondState?.({ id: UUID, bondState: { preState, state } });
|
|
265
|
-
|
|
266
|
-
test('fails as an invalid bond when the system re-pair ends in key missing', async () => {
|
|
267
|
-
const outcome = settle(onDeviceBondState(UUID, undefined, { systemInitiated: true }));
|
|
268
|
-
mockEmitKeyMissing?.({ id: UUID });
|
|
269
|
-
await flush();
|
|
270
|
-
|
|
271
|
-
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleBondInvalid);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
test('follows a system re-pair that replaces the bond', async () => {
|
|
275
|
-
const outcome = settle(onDeviceBondState(UUID, undefined, { systemInitiated: true }));
|
|
276
|
-
bondState('BOND_BONDING', 'BOND_NONE');
|
|
277
|
-
bondState('BOND_NONE', 'BOND_BONDING');
|
|
278
|
-
await advance(SYSTEM_BONDING_RESTART_WINDOW_MS);
|
|
279
|
-
expect(outcome.done).toBe(false);
|
|
280
|
-
|
|
281
|
-
bondState('BOND_BONDING', 'BOND_BONDED');
|
|
282
|
-
await flush();
|
|
283
|
-
|
|
284
|
-
expect(outcome.done).toBe(true);
|
|
285
|
-
expect(outcome.error).toBeUndefined();
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
test('fails a system re-pair that ends without a bond', async () => {
|
|
289
|
-
const outcome = settle(onDeviceBondState(UUID, undefined, { systemInitiated: true }));
|
|
290
|
-
bondState('BOND_BONDING', 'BOND_NONE');
|
|
291
|
-
await flush();
|
|
292
|
-
expect(outcome.done).toBe(false);
|
|
293
|
-
|
|
294
|
-
await advance(SYSTEM_BONDING_RESTART_WINDOW_MS);
|
|
295
|
-
|
|
296
|
-
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
test('fails a bonding this transport started as soon as it ends without a bond', async () => {
|
|
300
|
-
const outcome = settle(onDeviceBondState(UUID));
|
|
301
|
-
bondState('BOND_BONDING', 'BOND_NONE');
|
|
302
|
-
await flush();
|
|
303
|
-
|
|
304
|
-
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
|
-
});
|
package/src/bleEncryption.ts
DELETED
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
import { Platform } from 'react-native';
|
|
2
|
-
import BleUtils from '@onekeyfe/react-native-ble-utils';
|
|
3
|
-
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
hasBleKeyMissingSince,
|
|
7
|
-
onBleKeyMissing,
|
|
8
|
-
startBleKeyMissingTracking,
|
|
9
|
-
} from './bleKeyMissing';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Android 16+ starts encrypting a bonded LE link on its own right after connecting, with the
|
|
13
|
-
* stored key. A GATT request that needs encryption and is sent before that attempt finishes is
|
|
14
|
-
* rejected by the peer, and the framework retries it by encrypting again with the same key. When
|
|
15
|
-
* the peer has lost the bond, that second failure makes firmware that tolerates a single one per
|
|
16
|
-
* link drop the connection, before the system's own re-pairing can finish.
|
|
17
|
-
*
|
|
18
|
-
* Tracking the link's encryption result lets the transport hold GATT until then, so a lost bond
|
|
19
|
-
* is re-paired in place (the system shows a pairing request) instead of failing.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
/** HCI "PIN or Key Missing": the peer no longer has the keys of the stored bond. */
|
|
23
|
-
export const HCI_PIN_OR_KEY_MISSING = 0x06;
|
|
24
|
-
|
|
25
|
-
export type AndroidLinkEncryption =
|
|
26
|
-
/** This link was encrypted with the stored bond. */
|
|
27
|
-
| 'encrypted'
|
|
28
|
-
/** The link was reused and had already been encrypted. */
|
|
29
|
-
| 'already-encrypted'
|
|
30
|
-
/** The stored bond was lost and the system re-paired on this link. */
|
|
31
|
-
| 're-paired'
|
|
32
|
-
/** No result arrived in time; callers proceed as if nothing was known. */
|
|
33
|
-
| 'unresolved';
|
|
34
|
-
|
|
35
|
-
type EncryptionRecord = { at: number; status: number; enabled: boolean };
|
|
36
|
-
|
|
37
|
-
// An app can still resolve an older react-native-ble-utils, and the JS bundle can run on a native
|
|
38
|
-
// build that predates the events, so none of these members is assumed to exist.
|
|
39
|
-
type EncryptionCapableBleUtils = Partial<
|
|
40
|
-
Pick<
|
|
41
|
-
typeof BleUtils,
|
|
42
|
-
'supportsDeviceEncryptionChange' | 'onDeviceEncryptionChange' | 'onDeviceAclDisconnected'
|
|
43
|
-
>
|
|
44
|
-
>;
|
|
45
|
-
|
|
46
|
-
type LinkListener = (deviceId: string) => void;
|
|
47
|
-
|
|
48
|
-
const lastEncryption = new Map<string, EncryptionRecord>();
|
|
49
|
-
const lastAclDisconnectAt = new Map<string, number>();
|
|
50
|
-
const listeners = new Set<LinkListener>();
|
|
51
|
-
let unsubscribeNative: Array<() => void> | undefined;
|
|
52
|
-
|
|
53
|
-
const normalizeDeviceId = (deviceId: string) => deviceId.toLowerCase();
|
|
54
|
-
|
|
55
|
-
const isEncrypted = (record: EncryptionRecord) => record.status === 0 && record.enabled;
|
|
56
|
-
|
|
57
|
-
const getCapableBleUtils = (): EncryptionCapableBleUtils | undefined => {
|
|
58
|
-
if (Platform.OS !== 'android') return undefined;
|
|
59
|
-
const bleUtils: EncryptionCapableBleUtils = BleUtils;
|
|
60
|
-
if (
|
|
61
|
-
typeof bleUtils.supportsDeviceEncryptionChange !== 'function' ||
|
|
62
|
-
typeof bleUtils.onDeviceEncryptionChange !== 'function' ||
|
|
63
|
-
typeof bleUtils.onDeviceAclDisconnected !== 'function'
|
|
64
|
-
) {
|
|
65
|
-
return undefined;
|
|
66
|
-
}
|
|
67
|
-
return bleUtils.supportsDeviceEncryptionChange() ? bleUtils : undefined;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const notify = (deviceId: string) => {
|
|
71
|
-
listeners.forEach(listener => listener(deviceId));
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
/** False means the encryption result can never be observed here, so nothing should wait for it. */
|
|
75
|
-
export const isBleEncryptionTrackingSupported = () => getCapableBleUtils() !== undefined;
|
|
76
|
-
|
|
77
|
-
/** Idempotent. Returns whether encryption results are observable on this OS and native build. */
|
|
78
|
-
export const startBleEncryptionTracking = (): boolean => {
|
|
79
|
-
if (unsubscribeNative) return true;
|
|
80
|
-
const bleUtils = getCapableBleUtils();
|
|
81
|
-
if (!bleUtils?.onDeviceEncryptionChange || !bleUtils.onDeviceAclDisconnected) return false;
|
|
82
|
-
|
|
83
|
-
unsubscribeNative = [
|
|
84
|
-
bleUtils.onDeviceEncryptionChange(event => {
|
|
85
|
-
if (typeof event?.id !== 'string') return;
|
|
86
|
-
const deviceId = normalizeDeviceId(event.id);
|
|
87
|
-
lastEncryption.set(deviceId, {
|
|
88
|
-
at: Date.now(),
|
|
89
|
-
status: Number(event.status),
|
|
90
|
-
enabled: event.enabled === true,
|
|
91
|
-
});
|
|
92
|
-
notify(deviceId);
|
|
93
|
-
}),
|
|
94
|
-
bleUtils.onDeviceAclDisconnected(event => {
|
|
95
|
-
if (typeof event?.id !== 'string') return;
|
|
96
|
-
const deviceId = normalizeDeviceId(event.id);
|
|
97
|
-
lastAclDisconnectAt.set(deviceId, Date.now());
|
|
98
|
-
notify(deviceId);
|
|
99
|
-
}),
|
|
100
|
-
];
|
|
101
|
-
return true;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
export const stopBleEncryptionTracking = () => {
|
|
105
|
-
unsubscribeNative?.forEach(unsubscribe => unsubscribe());
|
|
106
|
-
unsubscribeNative = undefined;
|
|
107
|
-
lastEncryption.clear();
|
|
108
|
-
lastAclDisconnectAt.clear();
|
|
109
|
-
listeners.clear();
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Records a link that carried requests needing encryption, so reusing it later does not wait for
|
|
114
|
-
* an encryption result that was reported before tracking started.
|
|
115
|
-
*/
|
|
116
|
-
export const markBleLinkEncrypted = (deviceId: string) => {
|
|
117
|
-
if (!unsubscribeNative) return;
|
|
118
|
-
lastEncryption.set(normalizeDeviceId(deviceId), { at: Date.now(), status: 0, enabled: true });
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
export type WaitForAndroidLinkEncryptionOptions = {
|
|
122
|
-
deviceId: string;
|
|
123
|
-
/** When the current link attempt began; earlier results describe another link. */
|
|
124
|
-
linkStartedAt: number;
|
|
125
|
-
/** How long to wait for the first encryption result of this link. */
|
|
126
|
-
resultTimeoutMs: number;
|
|
127
|
-
/** How long a system re-pairing may take once the stored bond was reported lost. */
|
|
128
|
-
repairTimeoutMs: number;
|
|
129
|
-
/** After the link drops during re-pairing, how long to wait for the key-missing report. */
|
|
130
|
-
keyMissingGraceMs: number;
|
|
131
|
-
signal?: AbortSignal;
|
|
132
|
-
onRepairStarted?: () => void;
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Resolves once the link can carry requests that need encryption. Rejects with `BleBondInvalid`
|
|
137
|
-
* when the system reports that re-pairing failed, with `BleDeviceDisconnected` when the link drops
|
|
138
|
-
* during re-pairing, and with `BleDeviceNotBonded` when re-pairing does not finish in time.
|
|
139
|
-
*/
|
|
140
|
-
export const waitForAndroidLinkEncryption = ({
|
|
141
|
-
deviceId,
|
|
142
|
-
linkStartedAt,
|
|
143
|
-
resultTimeoutMs,
|
|
144
|
-
repairTimeoutMs,
|
|
145
|
-
keyMissingGraceMs,
|
|
146
|
-
signal,
|
|
147
|
-
onRepairStarted,
|
|
148
|
-
}: WaitForAndroidLinkEncryptionOptions): Promise<AndroidLinkEncryption> => {
|
|
149
|
-
if (!startBleEncryptionTracking()) return Promise.resolve('unresolved');
|
|
150
|
-
startBleKeyMissingTracking();
|
|
151
|
-
const target = normalizeDeviceId(deviceId);
|
|
152
|
-
|
|
153
|
-
const previous = lastEncryption.get(target);
|
|
154
|
-
const previousDropAt = lastAclDisconnectAt.get(target);
|
|
155
|
-
if (
|
|
156
|
-
previous &&
|
|
157
|
-
previous.at < linkStartedAt &&
|
|
158
|
-
isEncrypted(previous) &&
|
|
159
|
-
(previousDropAt === undefined || previousDropAt < previous.at)
|
|
160
|
-
) {
|
|
161
|
-
return Promise.resolve('already-encrypted');
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return new Promise((resolve, reject) => {
|
|
165
|
-
if (signal?.aborted) {
|
|
166
|
-
reject(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected));
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** Set once this link's first encryption failed because the peer lost the bond. */
|
|
171
|
-
let repairStartedAt: number | undefined;
|
|
172
|
-
let droppedDuringRepair = false;
|
|
173
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
174
|
-
let settled = false;
|
|
175
|
-
|
|
176
|
-
const cleanup = () => {
|
|
177
|
-
settled = true;
|
|
178
|
-
if (timer) clearTimeout(timer);
|
|
179
|
-
listeners.delete(onLinkEvent);
|
|
180
|
-
cleanupKeyMissing();
|
|
181
|
-
signal?.removeEventListener('abort', onAbort);
|
|
182
|
-
};
|
|
183
|
-
const finish = (result: AndroidLinkEncryption) => {
|
|
184
|
-
cleanup();
|
|
185
|
-
resolve(result);
|
|
186
|
-
};
|
|
187
|
-
const fail = (error: Error) => {
|
|
188
|
-
cleanup();
|
|
189
|
-
reject(error);
|
|
190
|
-
};
|
|
191
|
-
const arm = (ms: number, onExpire: () => void) => {
|
|
192
|
-
if (timer) clearTimeout(timer);
|
|
193
|
-
timer = setTimeout(onExpire, ms);
|
|
194
|
-
};
|
|
195
|
-
const bondInvalid = () =>
|
|
196
|
-
ERRORS.TypedError(HardwareErrorCode.BleBondInvalid, undefined, {
|
|
197
|
-
phase: 'connect',
|
|
198
|
-
reason: 'key_missing',
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
const evaluate = () => {
|
|
202
|
-
if (settled) return;
|
|
203
|
-
const record = lastEncryption.get(target);
|
|
204
|
-
const dropAt = lastAclDisconnectAt.get(target);
|
|
205
|
-
|
|
206
|
-
if (repairStartedAt === undefined) {
|
|
207
|
-
if (hasBleKeyMissingSince(target, linkStartedAt)) {
|
|
208
|
-
fail(bondInvalid());
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
if (dropAt !== undefined && dropAt >= linkStartedAt) {
|
|
212
|
-
// The link is gone; the caller's next request reports it with its usual error.
|
|
213
|
-
finish('unresolved');
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
if (!record || record.at < linkStartedAt) return;
|
|
217
|
-
if (isEncrypted(record)) {
|
|
218
|
-
finish('encrypted');
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
if (record.status !== HCI_PIN_OR_KEY_MISSING) {
|
|
222
|
-
finish('unresolved');
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
// The system re-pairs on its own after a lost bond. Nothing may touch GATT until then.
|
|
226
|
-
repairStartedAt = record.at;
|
|
227
|
-
onRepairStarted?.();
|
|
228
|
-
arm(repairTimeoutMs, () =>
|
|
229
|
-
fail(
|
|
230
|
-
ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded, 'Bluetooth pairing timed out', {
|
|
231
|
-
phase: 'bond',
|
|
232
|
-
reason: 'timeout',
|
|
233
|
-
})
|
|
234
|
-
)
|
|
235
|
-
);
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (hasBleKeyMissingSince(target, repairStartedAt)) {
|
|
240
|
-
fail(bondInvalid());
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
if (record && record.at > repairStartedAt && isEncrypted(record)) {
|
|
244
|
-
finish('re-paired');
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
if (!droppedDuringRepair && dropAt !== undefined && dropAt >= repairStartedAt) {
|
|
248
|
-
droppedDuringRepair = true;
|
|
249
|
-
// A failed re-pairing drops the link and reports key missing right after.
|
|
250
|
-
arm(keyMissingGraceMs, () =>
|
|
251
|
-
fail(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected))
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
const onLinkEvent: LinkListener = id => {
|
|
257
|
-
if (id === target) evaluate();
|
|
258
|
-
};
|
|
259
|
-
const onAbort = () => fail(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected));
|
|
260
|
-
|
|
261
|
-
listeners.add(onLinkEvent);
|
|
262
|
-
const cleanupKeyMissing = onBleKeyMissing(target, evaluate);
|
|
263
|
-
signal?.addEventListener('abort', onAbort, { once: true });
|
|
264
|
-
arm(resultTimeoutMs, () => finish('unresolved'));
|
|
265
|
-
evaluate();
|
|
266
|
-
});
|
|
267
|
-
};
|
package/src/bleKeyMissing.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { Platform } from 'react-native';
|
|
2
|
-
import BleUtils from '@onekeyfe/react-native-ble-utils';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Android 16+ keeps its side of a bond the device no longer has keys for, and reports it
|
|
6
|
-
* with ACTION_KEY_MISSING instead of a GATT status. The device then drops the link, so
|
|
7
|
-
* without this signal a wiped device is indistinguishable from any other disconnect.
|
|
8
|
-
*
|
|
9
|
-
* The signal only describes the link that was being encrypted when it fired. Callers must
|
|
10
|
-
* bound it to their own connection attempt with `sinceMs`; reusing an older one would turn
|
|
11
|
-
* an ordinary disconnect (a firmware-update reboot, for example) into a terminal bond error.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
// An app can still resolve a react-native-ble-utils older than 0.1.8, and the JS bundle can
|
|
15
|
-
// run on a native build that predates the event, so neither method is assumed to exist.
|
|
16
|
-
type KeyMissingCapableBleUtils = Partial<
|
|
17
|
-
Pick<typeof BleUtils, 'supportsDeviceKeyMissing' | 'onDeviceKeyMissing'>
|
|
18
|
-
>;
|
|
19
|
-
|
|
20
|
-
type KeyMissingListener = (deviceId: string) => void;
|
|
21
|
-
|
|
22
|
-
const lastKeyMissingAt = new Map<string, number>();
|
|
23
|
-
const listeners = new Set<KeyMissingListener>();
|
|
24
|
-
let unsubscribeNative: (() => void) | undefined;
|
|
25
|
-
|
|
26
|
-
const normalizeDeviceId = (deviceId: string) => deviceId.toLowerCase();
|
|
27
|
-
|
|
28
|
-
const getCapableBleUtils = (): KeyMissingCapableBleUtils | undefined => {
|
|
29
|
-
if (Platform.OS !== 'android') return undefined;
|
|
30
|
-
const bleUtils: KeyMissingCapableBleUtils = BleUtils;
|
|
31
|
-
if (
|
|
32
|
-
typeof bleUtils.supportsDeviceKeyMissing !== 'function' ||
|
|
33
|
-
typeof bleUtils.onDeviceKeyMissing !== 'function'
|
|
34
|
-
) {
|
|
35
|
-
return undefined;
|
|
36
|
-
}
|
|
37
|
-
return bleUtils.supportsDeviceKeyMissing() ? bleUtils : undefined;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
/** False means the event can never fire here, so nothing should wait for it. */
|
|
41
|
-
export const isBleKeyMissingSupported = () => getCapableBleUtils() !== undefined;
|
|
42
|
-
|
|
43
|
-
/** Idempotent. Returns whether the signal is available on this OS and native build. */
|
|
44
|
-
export const startBleKeyMissingTracking = (): boolean => {
|
|
45
|
-
if (unsubscribeNative) return true;
|
|
46
|
-
const bleUtils = getCapableBleUtils();
|
|
47
|
-
if (!bleUtils?.onDeviceKeyMissing) return false;
|
|
48
|
-
|
|
49
|
-
unsubscribeNative = bleUtils.onDeviceKeyMissing(event => {
|
|
50
|
-
if (typeof event?.id !== 'string') return;
|
|
51
|
-
const deviceId = normalizeDeviceId(event.id);
|
|
52
|
-
lastKeyMissingAt.set(deviceId, Date.now());
|
|
53
|
-
listeners.forEach(listener => listener(deviceId));
|
|
54
|
-
});
|
|
55
|
-
return true;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
export const stopBleKeyMissingTracking = () => {
|
|
59
|
-
unsubscribeNative?.();
|
|
60
|
-
unsubscribeNative = undefined;
|
|
61
|
-
lastKeyMissingAt.clear();
|
|
62
|
-
listeners.clear();
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export const hasBleKeyMissingSince = (deviceId: string, sinceMs: number): boolean => {
|
|
66
|
-
const at = lastKeyMissingAt.get(normalizeDeviceId(deviceId));
|
|
67
|
-
return at !== undefined && at >= sinceMs;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
/** Calls back for every later signal on this device until the returned cleanup runs. */
|
|
71
|
-
export const onBleKeyMissing = (deviceId: string, callback: () => void): (() => void) => {
|
|
72
|
-
const target = normalizeDeviceId(deviceId);
|
|
73
|
-
const listener: KeyMissingListener = id => {
|
|
74
|
-
if (id === target) callback();
|
|
75
|
-
};
|
|
76
|
-
listeners.add(listener);
|
|
77
|
-
return () => {
|
|
78
|
-
listeners.delete(listener);
|
|
79
|
-
};
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Resolves true once a signal at or after `sinceMs` exists for the device, false when
|
|
84
|
-
* none arrives within `timeoutMs`. The broadcast and the GATT disconnect it explains are
|
|
85
|
-
* delivered on separate paths, so the disconnect can reach JS first.
|
|
86
|
-
*/
|
|
87
|
-
export const waitForBleKeyMissing = (
|
|
88
|
-
deviceId: string,
|
|
89
|
-
sinceMs: number,
|
|
90
|
-
timeoutMs: number
|
|
91
|
-
): Promise<boolean> => {
|
|
92
|
-
if (!startBleKeyMissingTracking()) return Promise.resolve(false);
|
|
93
|
-
if (hasBleKeyMissingSince(deviceId, sinceMs)) return Promise.resolve(true);
|
|
94
|
-
|
|
95
|
-
return new Promise(resolve => {
|
|
96
|
-
const settle = (found: boolean) => {
|
|
97
|
-
clearTimeout(timer);
|
|
98
|
-
cleanup();
|
|
99
|
-
resolve(found);
|
|
100
|
-
};
|
|
101
|
-
const cleanup = onBleKeyMissing(deviceId, () => {
|
|
102
|
-
if (hasBleKeyMissingSince(deviceId, sinceMs)) settle(true);
|
|
103
|
-
});
|
|
104
|
-
const timer = setTimeout(() => settle(false), timeoutMs);
|
|
105
|
-
});
|
|
106
|
-
};
|