const { generateKeyPairSync, publicEncrypt, privateDecrypt, randomBytes, createPublicKey } = require('crypto'); const selfsigned = require('selfsigned'); const aesjs = require('aes-js'); class CryptorException extends Error { constructor (opt) { super(opt); this.name = 'CryptorException'; } } class RSA { constructor (keys, passphrase = null) { if (typeof keys !== "object" && keys !== null) throw new TypeError("keys must be object: { public_key: , private_key: } "); if (![undefined, null].includes(keys.public_key) && keys?.public_key?.__proto__ !== CryptKey.prototype) { throw new TypeError("public_key must be a CryptKey object"); } if (![undefined, null].includes(keys.private_key) && keys?.private_key?.__proto__ !== CryptKey.prototype) { throw new TypeError("private_key must be a CryptKey object"); } this.private_key = keys.private_key; this.public_key = keys.public_key; this.passphrase = passphrase; } enterPassphrase (passphrase) { this.passphrase = passphrase; } encrypt (data) { if (!this.public_key) throw new CryptorException('Can not find public key from encryption'); const toEncrypt = data; const encryptBuffer = Buffer.from(toEncrypt); if (encryptBuffer.length > 117) throw new CryptorException("RSA can encrypt maximum 117 byte"); let encrypted; if (!this.passphrase) encrypted = publicEncrypt(this.public_key.bytes.toString('utf8'), encryptBuffer); else encrypted = publicEncrypt({key: this.public_key.bytes.toString('utf8'), passphrase: this.passphrase}, encryptBuffer); return encrypted; } decrypt (data, toStringMethod = false) { if (!this.private_key) throw new CryptorException('Can not find private key from encryption'); let decryptBuffer = Buffer.from(data.toString("base64"), "base64"); let decrypted; if (!this.passphrase) decrypted = privateDecrypt(this.private_key.bytes.toString('utf8'), decryptBuffer); else decrypted = privateDecrypt({key: this.private_key.bytes.toString('utf8'), passphrase: this.passphrase}, decryptBuffer); if (toStringMethod) return decrypted.toString('utf8'); else return decrypted; } } class AES { constructor (key, counter=5) { if (key.__proto__ !== CryptKey.prototype) { throw new TypeError("key must be a CryptKey object"); } if (!Number.isInteger(counter)) { throw new TypeError("counter must be integer"); } this.key = key; this.counter = counter; } encrypt (data) { data = Buffer.from(data); const aesCtr = new aesjs.ModeOfOperation.ctr(this.key.bytes, new aesjs.Counter(this.counter)); return Buffer.from(aesCtr.encrypt(data)); } decrypt (data, toStringMethod = true) { const aesCtr = new aesjs.ModeOfOperation.ctr(this.key.bytes, new aesjs.Counter(this.counter)); if (toStringMethod) return Buffer.from(aesCtr.decrypt(data)).toString('utf-8'); else return Buffer.from(aesCtr.decrypt(data)); } } class AESLegacy { constructor (key, iv) { if (key.__proto__ !== CryptKey.prototype) { throw new TypeError("key must be a CryptKey object"); } if (!Array.isArray(iv)) { throw new TypeError("iv (initialization vector) must be Array!"); } else if (iv.length !== 16 || iv.map(i => Number.isInteger(i) && (i >= 0 && i < 256)).includes(false)) { // console.log({ iv }); throw new TypeError("iv (initialization vector) must be have 16 int(0-256) elements!"); } this.key = key; this.iv = iv; // Init test // const testStr = `{"response":"ack"}`; // const encryptedStr = this.encrypt(testStr); // if (this.decrypt(encryptedStr, true) !== testStr) // throw new Error("Failed to check unit test"); } encrypt (data) { // let formatedKey = ...this.key; const dataArray = [...Buffer.from(data)]; // let aesCtr = new aesjs.ModeOfOperation.ctr(this.key.bytes, new aesjs.Counter(5)); const aesCbc = new aesjs.ModeOfOperation.cbc(this.key.bytes, this.iv); const result = new Array(); let offset = 0; let addedEndless = false; let lastByteFirst = 0; let fromMain = false; do { let slicedData = dataArray.slice(offset, offset + 16); if (slicedData.length === 0) break; if (slicedData.length !== 16) { const difference = 16 - slicedData.length; slicedData = [...slicedData]; slicedData.push(...[...new Array(difference)].map((_, i) => i !== (difference - 1) ? 0 : difference)); lastByteFirst = slicedData.at(-1); slicedData = Buffer.from(slicedData); addedEndless = true; fromMain = true; } const pushData = Buffer.from(aesCbc.encrypt(Buffer.from(slicedData))); result.push(...pushData); if (addedEndless) break; offset += 16; } while (true); if (!addedEndless) { fromMain = false; const unencryptedChunks = [...new Array(16)].map((_, i) => i !== 15 ? 0 : 16); const pushData = Buffer.from(aesCbc.encrypt(Buffer.from(unencryptedChunks))); lastByteFirst = pushData.at(-1); result.push(...pushData); } // return Buffer.from(aesCtr.encrypt(data)); const lastDecryptedByte = [...this.decrypt(Buffer.from(result), false, false)].at(-1); if (lastDecryptedByte > 16) throw new Error(`Last byte error (${lastDecryptedByte}) last writted byte: (${lastByteFirst}) last encrypted byte: (${result.at(-1)}) from main: ${fromMain ? 'true' : 'false'} allstring: ${this.decrypt(Buffer.from(result), false).toString("hex")}`); return Buffer.from(result); } decrypt (data, toStringMethod = true, sliceAfterEncryption = false) { const aesCbc = new aesjs.ModeOfOperation.cbc(this.key.bytes, this.iv); // const aesCtr = new aesjs.ModeOfOperation.ctr(this.key.bytes, new aesjs.Counter(5)); // if (toStringMethod) return Buffer.from(aesCtr.decrypt(data)).toString('utf-8'); // else return Buffer.from(aesCtr.decrypt(data)); let offset = 0; let decryptedChunks = new Array(); const dataArray = [...data]; do { const slicedData = dataArray.slice(offset, offset + 16); if (![16, 0].includes(slicedData.length)) throw new Error("data length must be multiple of 16"); else if (slicedData.length === 0) break; decryptedChunks.push(...aesCbc.decrypt(slicedData)); offset += 16; } while (true); if (sliceAfterEncryption) { const sliceLength = decryptedChunks.at(-1); decryptedChunks = decryptedChunks.slice(0, decryptedChunks.length - sliceLength); } // if (toStringMethod) return Buffer.from(aesCbc.decrypt(data)).toString('utf-8'); // else return Buffer.from(aesCbc.decrypt(data)); if (toStringMethod) return Buffer.from(decryptedChunks).toString('utf-8'); else return Buffer.from(decryptedChunks); } } class OpenSSH extends RSA { constructor (keys) { if (keys.private_key === undefined && keys.cert?.__proto__ === CryptKey.prototype) keys.private_key = OpenSSH.parsePubKeyFromCrt(keys.cert); super(keys); this.private_key = keys.private_key; this.public_key = keys.public_key; this.cert = keys.cert; this.fingerprint = keys.fingerprint; } static parsePubKeyFromCrt (cert, options = {}) { const result = createPublicKey(cert).export({type:'pkcs1', format:'pem', ...options}); // console.log(result); return new CryptKey(result); } } class CryptKey { constructor (key) { this.bytes = Buffer.from(key); } toString () { return this.bytes.toString(); } } /* Example of options { location: __dirname + '/foo_rsa', comment: 'joe@foobar.com', password: '123321', read: true, format: "PEM" } */ function generateOpenSSH (attrs = {}, options = { days: 365 }) { attrs = Object.entries(attrs).map(([name, value]) => ({ name, value })); const keys = selfsigned.generate(attrs, options); console.log(keys); console.log(OpenSSH.parsePubKeyFromCrt(keys.cert)); return new OpenSSH({ public_key: new CryptKey(Buffer.from(keys.public)), private_key: new CryptKey(Buffer.from(keys.private)), cert: keys.cert, fingerprint: keys.fingerprint, }); } module.exports = { RSA, OpenSSH, AES, CryptKey, generateOpenSSH, generateRSA: (keySize = 1024, passphrase = null) => { let keysObj; if (!passphrase) keysObj = generateKeyPairSync('rsa', { modulusLength: keySize, publicKeyEncoding: { type: 'pkcs1', format: 'pem' }, privateKeyEncoding: { type: 'pkcs1', format: 'pem' } }); else keysObj = generateKeyPairSync('rsa', { modulusLength: keySize, publicKeyEncoding: { type: 'pkcs1', format: 'pem' }, privateKeyEncoding: { type: 'pkcs1', format: 'pem', cipher: 'aes-256-cbc', passphrase } }); let { privateKey, publicKey } = keysObj; return new RSA({ private_key : new CryptKey(Buffer.from(privateKey)), public_key : new CryptKey(Buffer.from(publicKey)) }); }, generateAES: (length = 32) => { if (![24, 32, 16].includes(length)) throw new CryptorException('AES key must be 16, 24 or 32 bytes.'); return new AES(new CryptKey(randomBytes(length))); } }