276 lines
9.0 KiB
JavaScript
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)));
|
|
}
|
|
} |