import NativeRNEncryptedStorage from 'react-native-encrypted-storage/src/specs/v1/NativeRNEncryptedStorage';
export interface RawStorageCheckResult {
rawBase64: string | null;
rawByteLength: number;
plaintextHits: string[];
looksEncrypted: boolean;
}
const BASE64_CHARS =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
function decodeBase64ToBytes(base64: string): Uint8Array {
if (typeof globalThis.atob === 'function') {
const binary = globalThis.atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
const normalized = base64.replace(/[\r\n\s]/g, '');
const len = normalized.length;
if (len === 0 || len % 4 !== 0) {
throw new Error('Invalid base64 input');
}
let padding = 0;
if (normalized[len - 1] === '=') {
padding++;
}
if (normalized[len - 2] === '=') {
padding++;
}
const outputLength = (len * 3) / 4 - padding;
const bytes = new Uint8Array(outputLength);
let byteIndex = 0;
const decodeChar = (char: string): number => {
if (char === '=') {
return 0;
}
const value = BASE64_CHARS.indexOf(char);
if (value < 0) {
throw new Error('Invalid base64 input');
}
return value;
};
for (let i = 0; i < len; i += 4) {
const a = decodeChar(normalized[i]);
const b = decodeChar(normalized[i + 1]);
const c = decodeChar(normalized[i + 2]);
const d = decodeChar(normalized[i + 3]);
bytes[byteIndex++] = (a << 2) | (b >> 4);
if (byteIndex < outputLength) {
bytes[byteIndex++] = ((b & 0x0f) << 4) | (c >> 2);
}
if (byteIndex < outputLength) {
bytes[byteIndex++] = ((c & 0x03) << 6) | d;
}
}
return bytes;
}
function stringToUtf8Bytes(text: string): Uint8Array {
if (typeof TextEncoder !== 'undefined') {
return new TextEncoder().encode(text);
}
const codes: number[] = [];
for (let i = 0; i < text.length; i++) {
const code = text.charCodeAt(i);
if (code < 0x80) {
codes.push(code);
} else if (code < 0x800) {
codes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
} else {
codes.push(
0xe0 | (code >> 12),
0x80 | ((code >> 6) & 0x3f),
0x80 | (code & 0x3f),
);
}
}
return new Uint8Array(codes);
}
function containsBytes(haystack: Uint8Array, needle: Uint8Array): boolean {
if (needle.length === 0 || haystack.length < needle.length) {
return false;
}
outer: for (let i = 0; i <= haystack.length - needle.length; i++) {
for (let j = 0; j < needle.length; j++) {
if (haystack[i + j] !== needle[j]) {
continue outer;
}
}
return true;
}
return false;
}
export async function readRawStoredValueBase64(key: string): Promise<string | null> {
const native = NativeRNEncryptedStorage as {
getRawStoredValueBase64?: (storageKey: string) => Promise<string | null>;
};
if (typeof native.getRawStoredValueBase64 !== 'function') {
throw new Error(
'getRawStoredValueBase64 不可用:设备/HAP 中的 encrypted_storage.har 版本过旧。' +
'请在 ohos/ 执行 npm run build:har && npm pack,再在 example_auto 执行 npm install,' +
'然后重新 bundle JS 并安装 HAP。',
);
}
return native.getRawStoredValueBase64(key);
}
export async function inspectRawStorageForPlaintext(
key: string,
plaintextMarkers: string[],
): Promise<RawStorageCheckResult> {
const rawBase64 = await readRawStoredValueBase64(key);
if (rawBase64 == null) {
return {
rawBase64: null,
rawByteLength: 0,
plaintextHits: [],
looksEncrypted: false,
};
}
const rawBytes = decodeBase64ToBytes(rawBase64);
const plaintextHits: string[] = [];
for (const marker of plaintextMarkers) {
if (marker.length === 0) {
continue;
}
if (rawBase64.includes(marker)) {
plaintextHits.push(marker);
continue;
}
const markerBytes = stringToUtf8Bytes(marker);
if (containsBytes(rawBytes, markerBytes)) {
plaintextHits.push(marker);
}
}
const looksEncrypted = rawBytes.length >= 28 && plaintextHits.length === 0;
return {
rawBase64,
rawByteLength: rawBytes.length,
plaintextHits,
looksEncrypted,
};
}