UNPKG

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
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, };