Skip to content

Instantly share code, notes, and snippets.

@renatoaraujoc
Last active September 28, 2024 00:15
Show Gist options
  • Save renatoaraujoc/11fab34592fd81c75800aa2934faa913 to your computer and use it in GitHub Desktop.
Save renatoaraujoc/11fab34592fd81c75800aa2934faa913 to your computer and use it in GitHub Desktop.
EncryptionUtil (Browser/Node)
export class EncryptionUtil {
private static _textEncoder = new TextEncoder();
private static _textDecoder = new TextDecoder();
private static ENC_DEC_SIMPLE_CHARS =
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_';
/**
* Simple synchronous encryption using `XOR` algorithm, returning only allowed characters.
* This method is fast but offers only low security. Supports UTF-8 characters.
*/
static encryptSimpleSync(params: {
data: unknown;
password: string;
applyBase64?: boolean;
}): string {
const { data, password, applyBase64 = true } = params;
const allowedChars = this.ENC_DEC_SIMPLE_CHARS;
const stringified = JSON.stringify(data);
const encoder = new TextEncoder();
const dataBytes = encoder.encode(stringified);
const passwordBytes = encoder.encode(password);
let result = '';
for (let i = 0; i < dataBytes.length; i++) {
const byte = dataBytes[i] ^ passwordBytes[i % passwordBytes.length];
result += allowedChars[byte & 0x3f];
result += allowedChars[(byte >> 6) & 0x3];
}
return applyBase64 ? btoa(result) : result;
}
/**
* Simple synchronous decryption for the alphanumeric XOR algorithm.
* This method is fast but offers low security. Supports UTF-8 characters.
*/
static decryptSimpleSync<T = string>(params: {
encryptedData: string;
password: string;
wasBase64Encoded?: boolean;
}): T | false {
const { encryptedData, password, wasBase64Encoded = true } = params;
const chars = this.ENC_DEC_SIMPLE_CHARS;
try {
const data = wasBase64Encoded ? atob(encryptedData) : encryptedData;
const passwordBytes = this._textEncoder.encode(password);
const decryptedBytes = new Uint8Array(data.length / 2);
for (let i = 0; i < data.length; i += 2) {
const lowBits = chars.indexOf(data[i]);
const highBits = chars.indexOf(data[i + 1]);
const byte = (highBits << 6) | lowBits;
decryptedBytes[i / 2] =
byte ^ passwordBytes[(i / 2) % passwordBytes.length];
}
const decryptedString = this._textDecoder.decode(decryptedBytes);
return JSON.parse(decryptedString) as T;
} catch (e) {
console.error('Decryption error:', e);
return false;
}
}
/**
* Encrypts anything using `AES-GCM`.
* The value will be stringified to JSON with a default
* `deriveKeyIterations` of `10_000`.
* Incrase `deriveKeyIterations` if you want more security in expense of
* performance, when decrypting with `aesDecrypt`,
* inform the same `deriveKeyIterations` or else the decryption will fail.
*/
static async aesEncrypt(params: {
data: unknown;
password: string;
deriveKeyIterations?: number;
}): Promise<string> {
return this.__isPlatformBrowser()
? this.__aesEncryptBrowser(params)
: this.__aesEncryptNode(params);
}
/**
* Decrypts an AES-GCM encoded string.
* Inform the same `deriveKeyIterations` used to encrypt the `encryptedValue`
* or else the decryption will fail.
*/
static async aesDecrypt<T = string>(params: {
encryptedValue: string;
password: string;
deriveKeyIterations?: number;
}): Promise<T | false> {
return this.__isPlatformBrowser()
? this.__aesDecryptBrowser(params)
: this.__aesDecryptNode(params);
}
/**
* SHA-1 Hash
*/
static async sha1(params: {
value: string | number | number[] | Uint8Array;
salt?: string;
}): Promise<string> {
return this.__isPlatformBrowser()
? this.__sha1Browser(params)
: this.__sha1Node(params);
}
/**
* SHA-256 Hash
*/
static async sha256(params: {
value: string | number;
salt?: string;
}): Promise<string> {
return this.__isPlatformBrowser()
? this.__sha256Browser(params)
: this.__sha256Node(params);
}
private static __isPlatformBrowser() {
return (
typeof window !== 'undefined' &&
!!window?.crypto &&
!!window.crypto?.subtle
);
}
private static async __aesEncryptBrowser(params: {
data: unknown;
password: string;
deriveKeyIterations?: number;
}): Promise<string> {
const { data, password, deriveKeyIterations = 10_000 } = params;
const stringified = JSON.stringify(data);
const encodedData = this._textEncoder.encode(stringified);
const salt = window.crypto.getRandomValues(new Uint8Array(16));
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const key = await this.__deriveKeyBrowser({
password,
salt,
iterations: deriveKeyIterations
});
const encrypted = await window.crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
encodedData
);
const encryptedArray = new Uint8Array(encrypted);
const resultArray = new Uint8Array(
salt.length + iv.length + encryptedArray.length
);
resultArray.set(salt, 0);
resultArray.set(iv, salt.length);
resultArray.set(encryptedArray, salt.length + iv.length);
return btoa(String.fromCharCode.apply(null, Array.from(resultArray)));
}
private static async __aesEncryptNode(params: {
data: unknown;
password: string;
deriveKeyIterations?: number;
}): Promise<string> {
const { data, password, deriveKeyIterations = 10_000 } = params;
const crypto = await import('node:crypto');
const stringified = JSON.stringify(data);
const encodedData = this._textEncoder.encode(stringified);
const salt = crypto.randomBytes(16);
const iv = crypto.randomBytes(12);
const key = await this.__deriveKeyNode({
password,
salt,
iterations: deriveKeyIterations
});
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const encrypted = Buffer.concat([
cipher.update(encodedData),
cipher.final()
]);
const authTag = cipher.getAuthTag();
const resultArray = Buffer.concat([salt, iv, authTag, encrypted]);
return resultArray.toString('base64');
}
private static async __aesDecryptBrowser<T = string>(params: {
encryptedValue: string;
password: string;
deriveKeyIterations?: number;
}): Promise<T | false> {
const {
encryptedValue,
password,
deriveKeyIterations = 10_000
} = params;
try {
const dataArray = new Uint8Array(
atob(encryptedValue)
.split('')
.map((char) => char.charCodeAt(0))
);
const salt = dataArray.slice(0, 16);
const iv = dataArray.slice(16, 28);
const authTag = dataArray.slice(28, 44);
const encryptedContent = dataArray.slice(44);
const key = await this.__deriveKeyBrowser({
password,
salt,
iterations: deriveKeyIterations
});
const decrypted = await window.crypto.subtle.decrypt(
{ name: 'AES-GCM', iv, tagLength: 128 },
key,
new Uint8Array([...encryptedContent, ...authTag])
);
const decodedText = new TextDecoder().decode(
new Uint8Array(decrypted)
);
return JSON.parse(decodedText) as T;
} catch (e) {
return false;
}
}
private static async __aesDecryptNode<T = string>(params: {
encryptedValue: string;
password: string;
deriveKeyIterations?: number;
}): Promise<T | false> {
const {
encryptedValue,
password,
deriveKeyIterations = 10_000
} = params;
const { createDecipheriv } = await import('node:crypto');
try {
const data = Buffer.from(encryptedValue, 'base64');
const salt = data.subarray(0, 16);
const iv = data.subarray(16, 28);
const authTag = data.subarray(28, 44);
const encrypted = data.subarray(44);
const key = await this.__deriveKeyNode({
password,
salt,
iterations: deriveKeyIterations
});
const decipher = createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(authTag);
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final()
]);
const decoded = this._textDecoder.decode(decrypted);
return JSON.parse(decoded) as T;
} catch (e) {
return false;
}
}
private static async __sha1Browser(params: {
value: string | number | number[] | Uint8Array;
salt?: string;
}): Promise<string> {
const { value, salt } = params;
let data: Uint8Array;
if (typeof value === 'string' || typeof value === 'number') {
data = this._textEncoder.encode(
salt ? `${salt}${value}` : `${value}`
);
} else if (Array.isArray(value)) {
data = new Uint8Array(value);
} else {
data = value;
}
const hashBuffer = await window.crypto.subtle.digest('SHA-1', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
}
private static async __sha1Node(params: {
value: string | number | number[] | Uint8Array;
salt?: string;
}): Promise<string> {
const { value, salt } = params;
const { createHash } = await import('node:crypto');
let data: Buffer;
if (typeof value === 'string' || typeof value === 'number') {
data = Buffer.from(salt ? `${salt}${value}` : `${value}`);
} else if (Array.isArray(value)) {
data = Buffer.from(value);
} else {
data = Buffer.from(value);
}
const hash = createHash('sha1');
hash.update(data);
return hash.digest('hex');
}
private static async __sha256Browser(params: {
value: string | number;
salt?: string;
}): Promise<string> {
const { value, salt } = params;
const data = this._textEncoder.encode(
salt ? `${salt}${value}` : `${value}`
);
const hashBuffer = await window.crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
}
private static async __sha256Node(params: {
value: string | number;
salt?: string;
}): Promise<string> {
const { value, salt } = params;
const { createHash } = await import('node:crypto');
const data = this._textEncoder.encode(
salt ? `${salt}${value}` : `${value}`
);
const hash = createHash('sha256');
hash.update(data);
return hash.digest('hex');
}
private static async __deriveKeyNode(params: {
password: string;
salt: Buffer;
iterations: number;
}): Promise<Buffer> {
const { password, salt, iterations } = params;
const { pbkdf2 } = await import('node:crypto');
return new Promise((resolve, reject) => {
pbkdf2(
password,
salt,
iterations,
32,
'sha256',
(err, derivedKey) => {
if (err) reject(err);
else resolve(derivedKey);
}
);
});
}
private static async __deriveKeyBrowser(params: {
password: string;
salt: Uint8Array;
iterations: number;
}): Promise<CryptoKey> {
const { password, salt, iterations } = params;
const baseKey = await window.crypto.subtle.importKey(
'raw',
this._textEncoder.encode(password),
'PBKDF2',
false,
['deriveKey']
);
return window.crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations, hash: 'SHA-256' },
baseKey,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment