mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 08:06:13 +00:00
91 lines
2.8 KiB
TypeScript
91 lines
2.8 KiB
TypeScript
// Cryptographically strong random bytes for OAuth `state`, PKCE verifiers,
|
|
// TOTP secrets and UUIDs. Hermes exposes no `crypto` global by default, so
|
|
// the previous helpers quietly fell back to Math.random — fine for a cache
|
|
// key, not for a long-lived 2FA secret or the only guard against a forged
|
|
// `bulwarkmobile://` redirect. `expo-crypto` is the platform CSPRNG; the web
|
|
// / test fallbacks below keep the module usable off-device.
|
|
|
|
let expoCrypto: { getRandomValues?: <T extends ArrayBufferView>(a: T) => T; getRandomBytes?: (n: number) => Uint8Array; randomUUID?: () => string } | null = null;
|
|
try {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
expoCrypto = require('expo-crypto');
|
|
} catch {
|
|
expoCrypto = null;
|
|
}
|
|
|
|
export function randomBytes(length: number): Uint8Array {
|
|
const bytes = new Uint8Array(length);
|
|
|
|
if (expoCrypto?.getRandomValues) {
|
|
try {
|
|
expoCrypto.getRandomValues(bytes);
|
|
return bytes;
|
|
} catch {
|
|
// fall through
|
|
}
|
|
}
|
|
if (expoCrypto?.getRandomBytes) {
|
|
try {
|
|
const out = expoCrypto.getRandomBytes(length);
|
|
if (out?.length === length) return out;
|
|
} catch {
|
|
// fall through
|
|
}
|
|
}
|
|
|
|
const c = (globalThis as {
|
|
crypto?: { getRandomValues?: (a: Uint8Array) => Uint8Array; randomUUID?: () => string };
|
|
}).crypto;
|
|
if (c?.getRandomValues) {
|
|
try {
|
|
c.getRandomValues(bytes);
|
|
return bytes;
|
|
} catch {
|
|
// fall through
|
|
}
|
|
}
|
|
if (c?.randomUUID) {
|
|
try {
|
|
let hex = '';
|
|
while (hex.length < length * 2) hex += c.randomUUID().replace(/-/g, '');
|
|
for (let i = 0; i < length; i++) bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
return bytes;
|
|
} catch {
|
|
// fall through
|
|
}
|
|
}
|
|
|
|
throw new Error('No secure random source available');
|
|
}
|
|
|
|
/** Lowercase hex of `length` random bytes. */
|
|
export function randomHex(length: number): string {
|
|
let s = '';
|
|
for (const b of randomBytes(length)) s += b.toString(16).padStart(2, '0');
|
|
return s;
|
|
}
|
|
|
|
/** RFC 4648 §5 base64url without padding. */
|
|
export function base64UrlEncode(bytes: Uint8Array): string {
|
|
let bin = '';
|
|
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
const b64 = typeof btoa === 'function' ? btoa(bin) : Buffer.from(bin, 'binary').toString('base64');
|
|
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
}
|
|
|
|
/** RFC 4122 v4 UUID from the secure source. */
|
|
export function secureRandomUUID(): string {
|
|
if (expoCrypto?.randomUUID) {
|
|
try {
|
|
return expoCrypto.randomUUID();
|
|
} catch {
|
|
// fall through
|
|
}
|
|
}
|
|
const b = randomBytes(16);
|
|
b[6] = (b[6] & 0x0f) | 0x40;
|
|
b[8] = (b[8] & 0x3f) | 0x80;
|
|
const hex = Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
|
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
}
|