106 lines
3.0 KiB
JavaScript
106 lines
3.0 KiB
JavaScript
const crypto = require('crypto');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { decryptBuffer } = require('./driverLicenseCrypto');
|
|
|
|
const DEFAULT_STORAGE_DIR = path.resolve(__dirname, '..', '..', 'secure_storage', 'driver-license');
|
|
|
|
const getStorageDir = () =>
|
|
process.env.DRIVER_LICENSE_STORAGE_DIR || DEFAULT_STORAGE_DIR;
|
|
|
|
const assertSafeStorageKey = (storageKey) => {
|
|
const normalized = String(storageKey || '').trim();
|
|
|
|
if (!normalized || normalized.includes('..') || normalized.startsWith('/')) {
|
|
throw new Error('Invalid storage key');
|
|
}
|
|
|
|
return normalized;
|
|
};
|
|
|
|
const resolveStoragePath = (storageKey) => {
|
|
const safeStorageKey = assertSafeStorageKey(storageKey);
|
|
const baseDir = path.resolve(getStorageDir());
|
|
const candidatePath = path.resolve(baseDir, ...safeStorageKey.split('/'));
|
|
|
|
if (candidatePath !== baseDir && !candidatePath.startsWith(`${baseDir}${path.sep}`)) {
|
|
throw new Error('Invalid storage key path');
|
|
}
|
|
|
|
return candidatePath;
|
|
};
|
|
|
|
const ensureStorageDirExists = async (absolutePath) => {
|
|
await fs.promises.mkdir(path.dirname(absolutePath), {
|
|
recursive: true,
|
|
mode: 0o700
|
|
});
|
|
};
|
|
|
|
const generateStorageKey = () => {
|
|
const now = new Date();
|
|
const year = String(now.getUTCFullYear());
|
|
const month = String(now.getUTCMonth() + 1).padStart(2, '0');
|
|
const uuid = crypto.randomUUID();
|
|
const hash = crypto
|
|
.createHash('sha256')
|
|
.update(`${uuid}:${Date.now()}:${crypto.randomBytes(32).toString('hex')}`)
|
|
.digest('hex')
|
|
.slice(0, 24);
|
|
|
|
return path.posix.join(year, month, `${uuid}_${hash}.bin`);
|
|
};
|
|
|
|
const persistEncryptedBuffer = async (encryptedBuffer) => {
|
|
if (!Buffer.isBuffer(encryptedBuffer)) {
|
|
throw new Error('encryptedBuffer must be a Buffer');
|
|
}
|
|
|
|
const storageKey = generateStorageKey();
|
|
const absolutePath = resolveStoragePath(storageKey);
|
|
|
|
await ensureStorageDirExists(absolutePath);
|
|
await fs.promises.writeFile(absolutePath, encryptedBuffer, {
|
|
mode: 0o600,
|
|
flag: 'wx'
|
|
});
|
|
|
|
return {
|
|
storageKey
|
|
};
|
|
};
|
|
|
|
const readEncryptedBuffer = async (storageKey) => {
|
|
const absolutePath = resolveStoragePath(storageKey);
|
|
return fs.promises.readFile(absolutePath);
|
|
};
|
|
|
|
const readDecryptedBuffer = async (storageKey, encryptionMetadata) => {
|
|
const ciphertext = await readEncryptedBuffer(storageKey);
|
|
|
|
return decryptBuffer({
|
|
ciphertext,
|
|
ivHex: encryptionMetadata?.ivHex,
|
|
authTagHex: encryptionMetadata?.authTagHex,
|
|
algorithm: encryptionMetadata?.algorithm
|
|
});
|
|
};
|
|
|
|
const removeStoredFile = async (storageKey) => {
|
|
try {
|
|
await fs.promises.unlink(resolveStoragePath(storageKey));
|
|
} catch (error) {
|
|
if (error.code !== 'ENOENT') {
|
|
throw error;
|
|
}
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
getStorageDir,
|
|
persistEncryptedBuffer,
|
|
readEncryptedBuffer,
|
|
readDecryptedBuffer,
|
|
removeStoredFile
|
|
};
|