Last active
September 28, 2024 00:15
-
-
Save renatoaraujoc/11fab34592fd81c75800aa2934faa913 to your computer and use it in GitHub Desktop.
EncryptionUtil (Browser/Node)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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