ai-adventure-labs/server/crypt/index.js

276 lines
9.0 KiB
JavaScript

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: <null|undefined|CryptKey>, private_key: <null|undefined|CryptKey> } ");
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)));
}
}