brunner56's picture
implement app
0bfe2e3
import {
randomBytes,
createCipheriv,
createDecipheriv,
createHash,
} from 'crypto';
import { deflateSync, inflateSync } from 'zlib';
import { Settings } from './settings';
import JSONCrush from 'jsoncrush';
import { createLogger } from './logger';
const logger = createLogger('crypto');
export const loadSecretKey = (log: boolean = false): Buffer | string => {
const secretKey = Settings.SECRET_KEY;
if (!secretKey) {
console.error('No secret key provided');
throw new Error('No secret key provided');
}
// must be 64 characters long and hex
if (secretKey.length === 32) {
// backwards compatibility
if (log)
logger.warn(
'Secret key is 32 characters long, consider updating to a 64 character key and reconfiguring for better security'
);
return secretKey;
} else if (secretKey.length !== 64) {
if (log) logger.error('Secret key must be 64 characters long');
throw new Error('Secret key must be 64 characters long');
}
if (!/^[0-9a-fA-F]+$/.test(secretKey)) {
if (log) logger.error('Secret key must be a hex string (0-9, a-f)');
throw new Error('Secret key must be a hex string (0-9, a-f)');
}
return Buffer.from(secretKey, 'hex');
};
const pad = (data: Buffer, blockSize: number): Buffer => {
const padding = blockSize - (data.length % blockSize);
return Buffer.concat([data, Buffer.alloc(padding, padding)]);
};
const unpad = (data: Buffer): Buffer => {
const padding = data[data.length - 1];
return data.subarray(0, data.length - padding);
};
export const crushJson = (data: string): string => {
return JSONCrush.crush(data);
};
export const uncrushJson = (data: string): string => {
return JSONCrush.uncrush(data);
};
export const compressData = (data: string): Buffer => {
return deflateSync(Buffer.from(data, 'utf-8'), {
level: 9,
});
};
export const decompressData = (data: Buffer): string => {
return inflateSync(data).toString('utf-8');
};
export const encryptData = (data: Buffer): { iv: string; data: string } => {
const secretKey = loadSecretKey();
// Then encrypt the compressed data
const iv = randomBytes(16);
const cipher = createCipheriv('aes-256-cbc', secretKey, iv);
// Ensure proper padding
const paddedData = pad(data, 16);
const encryptedData = Buffer.concat([
cipher.update(paddedData),
cipher.final(),
]);
return {
iv: iv.toString('base64'),
data: encryptedData.toString('base64'),
};
};
export const decryptData = (encryptedData: Buffer, iv: Buffer): Buffer => {
const secretKey = loadSecretKey();
const decipher = createDecipheriv('aes-256-cbc', secretKey, iv);
// Decrypt the data
const decryptedPaddedData = Buffer.concat([
decipher.update(encryptedData),
decipher.final(),
]);
// Remove padding
const decryptedData = unpad(decryptedPaddedData);
return decryptedData;
};
export function parseAndDecryptString(data: string): string | null {
try {
if (data.startsWith('E-') || data.startsWith('E2-')) {
const eVersion = data.startsWith('E2-') ? 2 : 1;
const encoding = eVersion === 1 ? 'hex' : 'base64';
const [ivHex, encryptedHex] = data
.replace('E-', '')
.replace('E2-', '')
.split('-')
.map(decodeURIComponent);
const iv = Buffer.from(ivHex, encoding);
const encrypted = Buffer.from(encryptedHex, encoding);
const decrypted = decryptData(encrypted, iv);
const decompressed = decompressData(decrypted);
return decompressed;
}
return data;
} catch (error: any) {
logger.error(`Failed to decrypt data: ${error.message}`);
return null;
}
}
export function getTextHash(text: string): string {
const hash = createHash('sha256');
hash.update(text);
return hash.digest('hex');
}
export function isValueEncrypted(value?: string): boolean {
if (!value) return false;
const tests =
/^E2-[^-]+-[^-]+$/.test(value) ||
/^E-[0-9a-fA-F]{32}-[0-9a-fA-F]+$/.test(value);
return tests;
}