@synotech/utils
Version:
a collection of utilities for internal use
760 lines (713 loc) • 19.8 kB
text/typescript
import {
createCipheriv,
createDecipheriv,
createHmac,
createPublicKey,
createSign,
createVerify,
generateKeyPairSync,
pbkdf2Sync,
randomBytes,
} from 'crypto';
import jwt from 'jsonwebtoken';
import { trace } from 'node:console';
interface CryptographyOptions {
keys?: string[];
encryptKey?: string;
encryptKeySingle?: string;
}
interface RandomOptions {
length?: number;
useLowerCase?: boolean;
useUpperCase?: boolean;
useNumbers?: boolean;
useSpecial?: boolean;
useHex?: boolean;
}
type KeyStrength =
| 'decent_pw'
| 'strong_pw'
| 'ft_knox_pw'
| 'ci_key'
| '160_wpa'
| '504_wpa'
| '64_wep'
| '128_wep'
| '152_wep'
| '256_wep';
/**
* The Cryptography class provides methods for encryption, decryption, signing, and verification of data using various cryptographic algorithms.
*
* @class Cryptography
*
* @example
* const cryptography = new Cryptography({
* keys: ['key1', 'key2', 'key3'],
* encryptKey: '',
* encryptKeySingle: '',
* })
*
* const encryptedData = cryptography.encrypt('Hello, World!')
* const decryptedData = cryptography.decrypt(encryptedData)
*
* const signature = cryptography.signature()
* const isVerified = cryptography.signatureVerify(signature)
*
* // creates a KrugerGold Token Address
* const address = cryptography.address()
*
* const encodedData = cryptography.base64Encode('Hello, World!')
* const decodedData = cryptography.base64Decode(encodedData)
* const isEncodedOrNot = cryptography.isBase64Encoded(encodedData)
*
* cryptography.get('strong_pw') // returns i=SQ_qa3W[<RxoM
* cryptography.random({ length: 20, useLowerCase: true, useUpperCase: true, useNumbers: true, useSpecial: true, useHex: false }) // returns *ajz:74,*ak0
*
*/
class Cryptography {
ALG: any;
KEYS: { encryptKey: string; encryptKeySingle: string };
keys: string[];
private lowerCase: string = 'abcdefghijklmnopqrstuvwxyz';
private upperCase: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
private numbers: string = '1234567890';
private special: string = '`~!@#$%^&*()-=_+[]{}|;\\\':",./<>?';
private hex: string = '123456789ABCDEF';
/**
* Initializes a new instance of the Cryptography class.
* @param keys - An array of keys used for encryption and decryption.
*/
constructor(options: CryptographyOptions) {
const { keys, encryptKey, encryptKeySingle } = options;
this.keys = keys || [
`D5uBt7z18on1Z1I2`,
`fqIlGkitP9Ik7xmp`,
`tttiKz8PaaExA6gZ`,
];
this.KEYS = {
encryptKey:
encryptKey ?? (process.env.ENCRYPTION_TWO_WAY_KEY as string),
encryptKeySingle:
encryptKeySingle ??
(process.env.ENCRYPTION_ONE_WAY_KEY as string),
};
this.ALG = {
/**
* GCM is an authenticated encryption mode that
* not only provides confidentiality but also
* provides integrity in a secured way.
*/
NODE_CIPHER: 'aes-256-gcm',
/**
* 128 bit auth tag is recommended for GCM.
*/
AUTH_TAG_BYTE_LEN: 16,
/**
* NIST recommends 96 bits or 12 bytes IV for GCM
* to promote interoperability, efficiency, and
* simplicity of design.
*/
IV_BYTE_LEN: 12,
/**
* Note: 256 (in ALG name) is key size.
* NODE size for AES is always 128.
*/
KEY_BYTE_LEN: 32,
/**
* To prevent rainbow table attacks.
*/
SALT_BYTE_LEN: 16,
};
}
private randomizer(): number {
return randomBytes(1).readUInt8(0) / 255;
}
/**
* @method random
* The Random module class provides methods for generating random strings.
*
* @example
* cryptography.random({ length: 20, useLowerCase: true, useUpperCase: true, useNumbers: true, useSpecial: true, useHex: false }) // returns *ajz:74,*ak0
*
* // Decent Passwords - Good for nothing really, public accounts and other non-critical things.
* cryptography.get('decent_pw') // returns rXjdx36oro
*
* // Strong Passwords - Robust enough to keep your web hosting account secure.
* cryptography.get('strong_pw') // returns i=SQ_qa3W[<RxoM
*
* // Fort Knox Passwords - Secure enough for almost anything, like root or administrator passwords.
* cryptography.get('ft_knox_pw') // returns P}U%H\OOYAYb;wc"3hgI,3Lz[gd-z]
*
* // Encryption Keys - Can be used for any other 256-bit key requirement.
* cryptography.get('ci_key') // returns CeXHpM3nDgzdv0o3AkMCs3OuxzepLGW8
*
* // 160-bit WPA Key
* cryptography.get('160_wpa') // returns oHI#gR8z#h7BS>cZ!zH(
*
* // 504-bit WPA Key
* cryptography.get('504_wpa') // returns <os[g`s}u06jqt"Ea]t11,HsI[UipHD)%F";:9RhJ@kTU8GknLpMAXtoCzsJjT`
*
* // 64-bit WEP Keys
* cryptography.get('64_wep') // returns 8911B
*
* // 128-bit WEP Keys
* cryptography.get('128_wep') // returns 9F4E4F933BCCC
*
* // 152-bit WEP Keys
* cryptography.get('152_wep') // returns 695E1EE96E483961
*
* // 256-bit WEP Keys
* cryptography.get('256_wep') // returns AC7E866246BA6B71BF5D88A6861AB
*
*/
public random(options: RandomOptions): string {
const {
length = 32,
useLowerCase = true,
useUpperCase = true,
useNumbers = true,
useSpecial = false,
useHex = false,
} = options;
let chars = '';
let key = '';
if (useLowerCase) chars += this.lowerCase;
if (useUpperCase) chars += this.upperCase;
if (useNumbers) chars += this.numbers;
if (useSpecial) chars += this.special;
if (useHex) chars += this.hex;
for (let i = 0; i < length; i++) {
key += chars[Math.floor(this.randomizer() * chars.length)];
}
return key;
}
/**
*
* @method get
* @param strength - The strength of the key to generate. decent_pw|strong_pw|ft_knox_pw|ci_key|160_wpa|504_wpa|64_wep|128_wep|152_wep|256_wep
*/
public get(strength: KeyStrength): string {
switch (strength) {
case 'decent_pw':
return this.random({
length: 10,
useLowerCase: true,
useUpperCase: true,
useNumbers: true,
useSpecial: false,
useHex: false,
});
case 'strong_pw':
return this.random({
length: 15,
useLowerCase: true,
useUpperCase: true,
useNumbers: true,
useSpecial: true,
useHex: false,
});
case 'ft_knox_pw':
return this.random({
length: 30,
useLowerCase: true,
useUpperCase: true,
useNumbers: true,
useSpecial: true,
useHex: false,
});
case 'ci_key':
return this.random({
length: 32,
useLowerCase: true,
useUpperCase: true,
useNumbers: true,
useSpecial: false,
useHex: false,
});
case '160_wpa':
return this.random({
length: 20,
useLowerCase: true,
useUpperCase: true,
useNumbers: true,
useSpecial: true,
useHex: false,
});
case '504_wpa':
return this.random({
length: 63,
useLowerCase: true,
useUpperCase: true,
useNumbers: true,
useSpecial: true,
useHex: false,
});
case '64_wep':
return this.random({
length: 5,
useLowerCase: false,
useUpperCase: false,
useNumbers: false,
useSpecial: false,
useHex: true,
});
case '128_wep':
return this.random({
length: 13,
useLowerCase: false,
useUpperCase: false,
useNumbers: false,
useSpecial: false,
useHex: true,
});
case '152_wep':
return this.random({
length: 16,
useLowerCase: false,
useUpperCase: false,
useNumbers: false,
useSpecial: false,
useHex: true,
});
case '256_wep':
return this.random({
length: 29,
useLowerCase: false,
useUpperCase: false,
useNumbers: false,
useSpecial: false,
useHex: true,
});
default:
throw new Error(`No such strength`);
}
}
/**
* @method salt
* Generates a random salt value.
* @returns The generated salt value as a hexadecimal string.
*
* @example
* const salt = cryptography.salt()
* console.log(salt)
* // Output:
* // 5eb63bbbe01eeed093cb22bb8f5acdc3
*/
salt() {
return randomBytes(this.ALG.SALT_BYTE_LEN).toString('hex');
}
/**
* @method iv
* Generates a random initialization vector (IV).
* @returns The generated IV as a Buffer.
*/
iv() {
return randomBytes(this.ALG.AUTH_TAG_BYTE_LEN);
}
/**
* @method signature
* Generates a signature using the encryption keys.
* @returns The generated signature as a string.
*
* @example
* const signature = cryptography.signature()
* console.log(signature)
* // Output:
* // 6a3a4b5c6d7e8f9a
*/
signature() {
return this.encrypt(this.getKeys(2).join(' '));
}
/**
* @method signatureVerify
* Verifies a signature against the encryption keys.
* @param signature - The signature to verify.
* @returns True if the signature is valid, false otherwise.
* @throws An error if the signature contains unknown identifiers.
*
* @example
* const signature = '6a3a4b5c6d7e8f9a'
* const isValid = cryptography.signatureVerify(signature)
* console.log(isValid)
* // Output:
* // true
*/
signatureVerify(signature: string) {
const signatures = this.decrypt(signature).split(' ');
if (signatures.every((value) => this.keys.includes(value))) {
return true;
} else {
throw `Unknown identifier`;
}
}
/**
* @method address
* Generates a random crypto address.
* @returns The generated address as a string.
* @throws An error if an error occurs during address generation.
*
* @example
* const address = cryptography.address()
* console.log(address)
* // Output:
* // kK5HCVlAmmEnnlKh6wVX9TiJ6YGI7FCl
*/
address() {
try {
const address = this.random({
length: this.ALG.KEY_BYTE_LEN - 1,
});
return `k${address}`;
} catch (error) {
trace(error);
throw error;
}
}
/**
* @method getKeys
* Generates random keys from the provided key array.
* @param count - The number of keys to generate. Defaults to 12.
* @returns An array of randomly generated keys.
*/
getKeys = (count?: number) => {
count = count ?? 12;
const random = [];
for (let i = 0; i < count; i++) {
const randomIndex = Math.floor(Math.random() * this.keys.length);
const randomWord = this.keys[randomIndex];
// @ts-ignore
random.push(randomWord);
}
return random;
};
/**
* @method encryptSingle
* Encrypts data one way using an encryption key.
* @param data - The data to encrypt.
* @returns The encrypted data as a base64-encoded string.
*
* @example
* const crypto = new Cryptography(['key1', 'key2', 'key3'], 'encryptKey', 'encryptKeySingle')
* const data = 'Hello, World!'
* const encryptedData = cryptography.encryptSingle(data)
* console.log(encryptedData)
* // Output:
* // 5eb63bbbe01eeed093cb22bb8f5acdc3
*/
encryptSingle(data: any) {
try {
const signature = createHmac(`sha256`, this.KEYS.encryptKeySingle)
.update(data)
.digest(`hex`);
const b64Buffer = Buffer.from(signature);
return b64Buffer.toString(`base64`);
} catch (error) {
trace(error);
return Buffer.alloc(16);
}
}
/**
* @method encrypt
* Encrypts text two way using the encryption keys.
* @param text - The text to encrypt.
* @returns The encrypted text as a string.
* @throws An error if an error occurs during encryption.
*
* @example
* const text = 'Hello, World!'
* const encryptedText = cryptography.encrypt(text)
* console.log(encryptedText)
* // Output:
* // 6a3a4b5c6d7e8f9a:6a3a4b5c6d7e8f9a:6a3a4b5c6d7e8f9a
*/
encrypt(text: any) {
try {
const iv = randomBytes(this.ALG.AUTH_TAG_BYTE_LEN);
const cipher = createCipheriv(
this.ALG.NODE_CIPHER,
this.KEYS.encryptKey,
iv
);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const tag = cipher.getAuthTag();
return `${iv.toString('hex')}:${encrypted}:${tag.toString('hex')}`;
} catch (error) {
trace(error);
throw error;
}
}
/**
* @method decrypt
* Decrypts encrypted text using the encryption keys.
* @param encryptedText - The encrypted text to decrypt.
* @returns The decrypted text as a string.
* @throws An error if an error occurs during decryption.
*
* @example
* const encryptedText = '6a3a4b5c6d7e8f9a:6a3a4b5c6d7e8f9a:6a3a4b5c6d7e8f9a'
* const decryptedText = cryptography.decrypt(encryptedText)
* console.log(decryptedText)
* // Output:
* // Hello, World!
*/
decrypt(encryptedText: any) {
try {
const [ivHex, encryptedHex, tagHex] = encryptedText.split(':');
const iv = Buffer.from(ivHex, 'hex');
const encrypted = Buffer.from(encryptedHex, 'hex');
const tag = Buffer.from(tagHex, 'hex');
const decipher = createDecipheriv(
this.ALG.NODE_CIPHER,
this.KEYS.encryptKey,
iv
);
decipher.setAuthTag(tag);
let decrypted = decipher.update(encrypted, undefined, 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
} catch (error) {
trace(error);
throw error;
}
}
/**
* @method password
* Generates a password hash using the provided password and salt.
* @param password - The password to hash.
* @param salt - The salt value to use for hashing.
* @returns The hashed password as a hexadecimal string.
* @throws An error if an error occurs during password hashing.
*
* @example
* const password = 'myPassword'
* const salt = cryptography.salt()
* const hashedPassword = cryptography.password(password, salt)
* console.log(hashedPassword)
* // Output:
* // 5ebe2294ecd0e0f08eab7690d2a6ee69
*/
password(password: string, salt: string) {
try {
const pbkdf = pbkdf2Sync(password, salt, 10000, 256, `sha256`);
return pbkdf.toString('hex');
} catch (error) {
trace(error);
throw error;
}
}
/**
* @method jwtIssue
* Issues a JSON Web Token (JWT) with the provided payload and expiry.
* @param payload - The payload to include in the JWT.
* @param expiry - The expiry time for the JWT.
* @returns The issued JWT as a string.
* @throws An error if an error occurs during JWT issuance.
*
* @example
* const payload = {
* sub: '1234567890',
* name: 'John Doe',
* iat: 1516239022
* }
*
* const expiry = '1h'
* const token = cryptography.jwtIssue(payload, expiry)
* console.log(token)
* // Output:
* // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
*/
jwtIssue(payload: any = {}, expiry: string) {
try {
return jwt.sign(
{
...payload,
},
this.KEYS.encryptKeySingle,
{
expiresIn: expiry,
}
);
} catch (error) {
trace(error);
throw error;
}
}
/**
* @method jwtVerify
* Verifies a JSON Web Token (JWT) and returns the decoded payload.
* @param token - The JWT to verify.
* @returns The decoded payload if the JWT is valid.
* @throws An error if the JWT is invalid or an error occurs during verification.
*
* @example
* const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'
* const payload = cryptography.jwtVerify(token)
* console.log(payload)
* // Output:
* // {
* // sub: '1234567890',
* // name: 'John Doe',
* // iat: 1516239022
* // }
*/
jwtVerify(token: string) {
try {
return jwt.verify(token, this.KEYS.encryptKeySingle);
} catch (error) {
trace(error);
throw error;
}
}
/**
* @method publicKey
* Generates a private key for asymmetric encryption.
* @returns The generated private key as a PEM-encoded string.
* @throws An error if an error occurs during private key generation.
*
* @example
* const privateKey = cryptography.privateKey()
* console.log(privateKey)
* // Output:
* // -----BEGIN PRIVATE KEY-----
* // MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBA4GCAWqjggFMMIIB
* // ...
* // -----END PRIVATE KEY-----
*/
privateKey() {
try {
const { privateKey } = generateKeyPairSync(`ec`, {
namedCurve: 'secp521r1',
});
return privateKey.export({ type: 'pkcs8', format: 'pem' });
} catch (error) {
trace(error);
throw error;
}
}
/**
* @method publicKey
*
* Generates a public key from the provided private key.
* @param privateKey - The private key to generate the public key from.
* @returns The generated public key as a PEM-encoded string.
*
* @example
* const privateKey = `-----BEGIN PRIVATE KEY-----
* MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDZ1Ck6vJQK0J5T
* ...
* -----END PRIVATE KEY-----`
*
* const publicKey = cryptography.publicKey(privateKey)
* console.log(publicKey)
* // Output:
* // -----BEGIN PUBLIC KEY-----
* // MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2dQpOryUCtCeUz8vZ6zB
* // ...
* // -----END PUBLIC KEY-----
*/
publicKey(privateKey: string) {
const publicKey = createPublicKey(privateKey);
return publicKey.export({ type: 'spki', format: 'pem' });
}
/**
* @method publicKeyVerify
* Verifies the authenticity of a public key using the provided private and public keys.
* @param privateKey - The private key used to sign the data.
* @param publicKey - The public key used to verify the signature.
* @returns True if the public key is authentic, false otherwise.
* @throws An error if the public key fails to authenticate.
*
* @example
* const privateKey = `-----BEGIN PRIVATE KEY-----
* MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDZ1Ck6vJQK0J5T
* ...
* -----END PRIVATE KEY-----`
* const publicKey = `-----BEGIN PUBLIC KEY-----
* MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2dQpOryUCtCeUz8vZ6zB
* ...
* -----END PUBLIC KEY-----`
* const isAuthentic = cryptography.publicKeyVerify({ privateKey, publicKey })
* console.log(isAuthentic)
* // Output:
* // true
*/
publicKeyVerify({
privateKey,
publicKey,
}: {
privateKey: string;
publicKey: string;
}) {
try {
const data = this.KEYS.encryptKey;
const sign = createSign(`RSA-SHA256`);
sign.update(data);
const signature = sign.sign(privateKey, `base64`);
const verify = createVerify(`RSA-SHA256`);
verify.update(data);
const isValid = verify.verify(publicKey, signature, `base64`);
if (isValid) {
return true;
} else {
throw `Failed to authenticate the public key`;
}
} catch (error) {
trace(error);
throw error;
}
}
/**
* @method isBase64Encoded
* Checks if a string is base64 encoded.
* @param string - The string to check.
* @returns True if the string is base64 encoded, false otherwise.
*
* @example
* const encodedString = 'SGVsbG8sIFdvcmxkIQ=='
* const isEncoded = cryptography.isBase64Encoded(encodedString)
* console.log(isEncoded)
* // Output:
* // true
*/
isBase64Encoded(string: string) {
try {
return Buffer.from(string, 'base64').toString('base64') === string;
} catch (err) {
return false;
}
}
/**
* @method base64Encode
* Encodes data as base64.
* @param data - The data to encode.
* @returns The base64-encoded string.
*
* @example
* const data = 'Hello, World!'
* const encodedData = cryptography.base64Encode(data)
* console.log(encodedData)
* // Output:
* // SGVsbG8sIFdvcmxkIQ==
*/
base64Encode(data: string) {
return Buffer.from(data).toString('base64');
}
/**
* @method base64Decode
* Decodes a base64-encoded string.
* @param encodedString - The base64-encoded string to decode.
* @returns The decoded string.
*
* @example
* const encodedString = 'SGVsbG8sIFdvcmxkIQ=='
* const decodedString = cryptography.base64Decode(encodedString)
* console.log(decodedString)
* // Output:
* // Hello, World!
*/
base64Decode(encodedString: string) {
return Buffer.from(encodedString, 'base64').toString('utf-8');
}
}
export { Cryptography };