81 lines
2.3 KiB
TypeScript
81 lines
2.3 KiB
TypeScript
import 'react-native-get-random-values';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import * as Keychain from 'react-native-keychain';
|
|
|
|
const DEVICE_SERVICE = 'com.bankcbs.mobile.deviceId';
|
|
|
|
/**
|
|
* Kept in memory as well so the many calls that need it during one flow do not
|
|
* each hit the Keystore.
|
|
*/
|
|
let cachedDeviceId: string | null = null;
|
|
let inflight: Promise<string> | null = null;
|
|
|
|
const persist = async (deviceId: string) => {
|
|
await Keychain.setGenericPassword('deviceId', deviceId, {
|
|
service: DEVICE_SERVICE,
|
|
accessible: Keychain.ACCESSIBLE.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY,
|
|
securityLevel: Keychain.SECURITY_LEVEL.SECURE_SOFTWARE,
|
|
});
|
|
};
|
|
|
|
const load = async (): Promise<string> => {
|
|
const stored = await Keychain.getGenericPassword({ service: DEVICE_SERVICE });
|
|
|
|
if (stored && stored.password) {
|
|
return stored.password;
|
|
}
|
|
|
|
const created = uuidv4();
|
|
await persist(created);
|
|
|
|
return created;
|
|
};
|
|
|
|
/**
|
|
* The profile the customer registers is bound to this value. Registration and
|
|
* every later sign-in must send the *same* one — a different value comes back
|
|
* as `DEVICE_NOT_BOUND`, which reads like a server fault but is a client bug.
|
|
* So it is generated exactly once, on first launch, and never regenerated
|
|
* except by an explicit `reset()`.
|
|
*
|
|
* `AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY` rather than `WHEN_UNLOCKED_*`: the id
|
|
* is not a secret, and sign-in must read it before the user has authenticated.
|
|
*/
|
|
export const deviceIdentity = {
|
|
async get(): Promise<string> {
|
|
if (cachedDeviceId) {
|
|
return cachedDeviceId;
|
|
}
|
|
|
|
// Concurrent callers on a cold start would otherwise each mint a UUID and
|
|
// the last write would win, silently unbinding the device.
|
|
inflight = inflight ?? load();
|
|
|
|
try {
|
|
cachedDeviceId = await inflight;
|
|
return cachedDeviceId;
|
|
} finally {
|
|
inflight = null;
|
|
}
|
|
},
|
|
|
|
/** In-memory peek for code that cannot await. Null before the first `get()`. */
|
|
peek() {
|
|
return cachedDeviceId;
|
|
},
|
|
|
|
/**
|
|
* Mints a fresh id, unbinding this install from its registered profile. Only
|
|
* for a deliberate "Register again" action — the customer will
|
|
* have to go through registration once more.
|
|
*/
|
|
async reset(): Promise<string> {
|
|
const next = uuidv4();
|
|
await persist(next);
|
|
cachedDeviceId = next;
|
|
|
|
return next;
|
|
},
|
|
};
|