Files
CBS-Mobile-App/src/services/security/deviceIdentity.ts
T

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;
},
};