SHA256
141 lines
5.1 KiB
JavaScript
141 lines
5.1 KiB
JavaScript
import {
|
|
decryptJsonWithStoragePwd,
|
|
encryptJsonWithStoragePwd,
|
|
} from './crypto-utils.js';
|
|
|
|
const DB_NAME = 'shine-ui-auth';
|
|
const DB_VERSION = 1;
|
|
const STORE_SECRETS = 'encrypted-secrets';
|
|
const STORE_SESSIONS = 'session-keys';
|
|
|
|
function normalizeLoginStorageKey(login) {
|
|
return String(login || '').trim().toLowerCase();
|
|
}
|
|
|
|
function sanitizePersistentSecrets(keys) {
|
|
const source = keys && typeof keys === 'object' ? keys : {};
|
|
const safe = { ...source };
|
|
// Recovery/root private key is intentionally ephemeral: it may exist in RAM
|
|
// during a protected operation, but must never be persisted on the device.
|
|
delete safe.rootKey;
|
|
return safe;
|
|
}
|
|
|
|
function openDb() {
|
|
return new Promise((resolve, reject) => {
|
|
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
request.onupgradeneeded = () => {
|
|
const db = request.result;
|
|
if (!db.objectStoreNames.contains(STORE_SECRETS)) {
|
|
db.createObjectStore(STORE_SECRETS, { keyPath: 'login' });
|
|
}
|
|
if (!db.objectStoreNames.contains(STORE_SESSIONS)) {
|
|
db.createObjectStore(STORE_SESSIONS, { keyPath: 'login' });
|
|
}
|
|
};
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onerror = () => reject(request.error || new Error('IndexedDB недоступен'));
|
|
});
|
|
}
|
|
|
|
async function put(storeName, value) {
|
|
const db = await openDb();
|
|
await new Promise((resolve, reject) => {
|
|
const tx = db.transaction(storeName, 'readwrite');
|
|
tx.objectStore(storeName).put(value);
|
|
tx.oncomplete = () => resolve();
|
|
tx.onerror = () => reject(tx.error || new Error('Ошибка записи в IndexedDB'));
|
|
});
|
|
db.close();
|
|
}
|
|
|
|
async function get(storeName, key) {
|
|
const db = await openDb();
|
|
const result = await new Promise((resolve, reject) => {
|
|
const tx = db.transaction(storeName, 'readonly');
|
|
const req = tx.objectStore(storeName).get(key);
|
|
req.onsuccess = () => resolve(req.result || null);
|
|
req.onerror = () => reject(req.error || new Error('Ошибка чтения из IndexedDB'));
|
|
});
|
|
db.close();
|
|
return result;
|
|
}
|
|
|
|
export async function saveEncryptedUserSecrets(login, storagePwd, keys) {
|
|
const safeKeys = sanitizePersistentSecrets(keys);
|
|
const encrypted = await encryptJsonWithStoragePwd(safeKeys, storagePwd);
|
|
const normalizedLogin = normalizeLoginStorageKey(login);
|
|
await put(STORE_SECRETS, {
|
|
login: normalizedLogin,
|
|
encrypted,
|
|
updatedAtMs: Date.now(),
|
|
});
|
|
}
|
|
|
|
export async function loadEncryptedUserSecrets(login, storagePwd) {
|
|
const normalizedLogin = normalizeLoginStorageKey(login);
|
|
let row = await get(STORE_SECRETS, normalizedLogin);
|
|
if (!row?.encrypted && normalizedLogin !== String(login || '').trim()) {
|
|
row = await get(STORE_SECRETS, String(login || '').trim());
|
|
}
|
|
if (!row?.encrypted) {
|
|
throw new Error('На устройстве нет сохранённых ключей для этого логина');
|
|
}
|
|
|
|
const decrypted = await decryptJsonWithStoragePwd(row.encrypted, storagePwd);
|
|
const safe = sanitizePersistentSecrets(decrypted);
|
|
|
|
// Одноразовая очистка контейнеров, созданных старыми версиями UI, где rootKey
|
|
// мог быть сохранён. Перезаписываем тем же storagePwd уже без Recovery private key.
|
|
if (decrypted && typeof decrypted === 'object' && Object.prototype.hasOwnProperty.call(decrypted, 'rootKey')) {
|
|
const encrypted = await encryptJsonWithStoragePwd(safe, storagePwd);
|
|
await put(STORE_SECRETS, {
|
|
login: normalizedLogin,
|
|
encrypted,
|
|
updatedAtMs: Date.now(),
|
|
});
|
|
}
|
|
|
|
return safe;
|
|
}
|
|
|
|
export async function updateEncryptedUserSecrets(login, storagePwd, updater) {
|
|
if (typeof updater !== 'function') {
|
|
throw new Error('updateEncryptedUserSecrets: updater должен быть функцией');
|
|
}
|
|
|
|
const current = await loadEncryptedUserSecrets(login, storagePwd);
|
|
const next = await updater(current);
|
|
if (!next || typeof next !== 'object') {
|
|
throw new Error('updateEncryptedUserSecrets: updater должен вернуть объект secrets');
|
|
}
|
|
const safe = sanitizePersistentSecrets(next);
|
|
await saveEncryptedUserSecrets(login, storagePwd, safe);
|
|
return safe;
|
|
}
|
|
|
|
export async function saveSessionMaterial(login, material) {
|
|
const normalizedLogin = normalizeLoginStorageKey(login);
|
|
await put(STORE_SESSIONS, {
|
|
login: normalizedLogin,
|
|
...material,
|
|
updatedAtMs: Date.now(),
|
|
});
|
|
}
|
|
|
|
export async function loadSessionMaterial(login) {
|
|
const normalizedLogin = normalizeLoginStorageKey(login);
|
|
const row = await get(STORE_SESSIONS, normalizedLogin);
|
|
if (row || normalizedLogin === String(login || '').trim()) return row;
|
|
return get(STORE_SESSIONS, String(login || '').trim());
|
|
}
|
|
|
|
export async function clearClientAuthData() {
|
|
await new Promise((resolve, reject) => {
|
|
const request = indexedDB.deleteDatabase(DB_NAME);
|
|
request.onsuccess = () => resolve();
|
|
request.onerror = () => reject(request.error || new Error('Не удалось очистить IndexedDB'));
|
|
request.onblocked = () => reject(new Error('Очистка IndexedDB заблокирована открытыми соединениями'));
|
|
});
|
|
}
|