hashon
Version:
Encrypt, decrypt and hash JSON data with AES and SHA — made for secure local files and syncing.
109 lines (92 loc) • 3.12 kB
JavaScript
const fs = require('fs').promises;
const CryptoJS = require('crypto-js');
const LOG_PREFIX = '[hashon]';
function encrypt(obj, secret) {
if (!secret) throw new Error('Missing encryption secret');
const str = JSON.stringify(obj);
return CryptoJS.AES.encrypt(str, secret).toString();
}
function decrypt(encryptedStr, secret) {
if (!secret) throw new Error('Missing decryption secret');
if (!encryptedStr || typeof encryptedStr !== 'string') {
throw new Error('Encrypted input is invalid or missing');
}
const bytes = CryptoJS.AES.decrypt(encryptedStr, secret);
const decryptedStr = bytes.toString(CryptoJS.enc.Utf8);
if (!decryptedStr) {
throw new Error('Decryption failed: possibly wrong secret or corrupt input');
}
try {
return JSON.parse(decryptedStr);
} catch (err) {
throw new Error('Invalid JSON after decryption');
}
}
function secure(obj, secret) {
if (!secret) throw new Error('Missing secret for hashing');
if (typeof obj !== 'object' || obj === null) return hash(obj, secret);
if (Array.isArray(obj)) return obj.map((v) => secure(v, secret));
const result = {};
for (const key in obj) {
const value = obj[key];
result[key] = (typeof value === 'object' && value !== null)
? secure(value, secret)
: hash(value, secret);
}
return result;
}
function hash(value, secret) {
if (!secret) throw new Error('Missing secret for hash');
const str = String(value);
if (str.startsWith('$HASH$')) return str;
const hashed = CryptoJS.SHA512(str + secret).toString();
const shortHash = hashed.slice(0, 12);
return `$HASH$${shortHash}`;
}
async function tryDecryptFile(path, secret) {
try {
const data = await fs.readFile(path, 'utf-8');
return decrypt(data, secret);
} catch (err) {
if (err.code !== 'ENOENT') {
console.warn(`${LOG_PREFIX} ⚠️ Could not decrypt file at ${path}: ${err.message}`);
}
return null;
}
}
async function syncEncryptedData(inputPath, outputPath, secret) {
try {
const raw = await fs.readFile(inputPath, 'utf-8');
let json;
try {
json = JSON.parse(raw);
} catch (parseErr) {
throw new Error(`Input JSON (${inputPath}) is not valid: ${parseErr.message}`);
}
const currentDecrypted = await tryDecryptFile(outputPath, secret);
const unchanged = JSON.stringify(currentDecrypted) === JSON.stringify(json);
if (!unchanged) {
const encrypted = encrypt(json, secret);
await fs.writeFile(outputPath, encrypted, 'utf-8');
console.log(`${LOG_PREFIX} 🔐 Data updated (encrypted).`);
} else {
console.log(`${LOG_PREFIX} ✅ No changes – everything is up to date.`);
}
} catch (err) {
console.error(`${LOG_PREFIX} ❌ Error during sync:`, err.message);
}
}
async function autoEncryptIfChanged(inputPath, secret) {
if (inputPath.endsWith('.json')) {
const outputPath = inputPath.replace(/\.json$/, '.sec.json');
await syncEncryptedData(inputPath, outputPath, secret);
}
}
module.exports = {
encrypt,
decrypt,
secure,
hash,
syncEncryptedData,
autoEncryptIfChanged,
};