code refactoring and added extensive documentation

This commit is contained in:
2020-10-07 16:08:32 +02:00
parent 4a3a303420
commit 86632dd3f0
32 changed files with 4648 additions and 380 deletions

46
src/config.type.ts Normal file
View File

@@ -0,0 +1,46 @@
/**
* Dennis Gunia (c) 2020
*
* Interface for global config object
*
* @summary Open-Token entry point
* @author Dennis Gunia <info@dennisgunia.de>
* @license Licensed under the Apache License, Version 2.0 (the "License").
*
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import SMTPTransport from 'nodemailer/lib/smtp-transport';
import { MLParser } from './mailParser';
/** Interface containing all config properties for opentoken */
export interface OTGlobalConfig {
/** nodemailer SMTP configuration */
mail: SMTPTransport,
/** sender alias */
mailFrom: string,
/** path to file containing matches */
outFileMatch: string
/** path to file containing mails */
inFileMail?: string
/** path to file containing mail template */
htmlPath?: string
/** List of used tokens */
usedTokens? : string[]
/** List of used mail adresses */
usedMails? : MLParser.MLItem[]
/** switch for dryrun */
dryrun: boolean
/** switch for force */
force: boolean
}

View File

@@ -1,151 +1,233 @@
/**
* Dennis Gunia (c) 2020
*
* Generator Implementation.
* This File provides the implementation of the code generator and mail sending functionality.
*
*
* @summary Generator Implementation.
* @author Dennis Gunia <info@dennisgunia.de>
* @license Licensed under the Apache License, Version 2.0 (the "License").
*
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import * as fs from 'fs'
import * as path from 'path'
import * as nodemailer from 'nodemailer';
import * as cliProgress from 'cli-progress'
import { shuffleArray } from './util/shuffle';
import { mkstring } from './util/token';
import * as Handlebars from "handlebars";
import Mail from 'nodemailer/lib/mailer';
import { SecureVault } from './vault';
import { parseMails } from './mailParser';
import { SVault } from './vault';
import { MLParser } from './mailParser';
import { shuffleArray } from './util/shuffle';
import { delay, mkstringCN } from './util/misc';
interface mail{
mail: string;
name: string;
}
export interface genReturn{
codes: string[];
mails: mail[];
}
export function generateToken(config: any,dataSafe: SecureVault): Promise<genReturn>{
return new Promise<genReturn>((resolve,error) => {
parseMails(config, dataSafe).then(res => {
generateCodes(resolve,error,res,config,dataSafe);
})
});
}
// generate codes
async function generateCodes(resolve: (value?: genReturn) => void,error: (reason?: any) => void,mailArray: mail[],config: any,dataSafe: SecureVault){
console.log("\nGenerating codes")
const pbar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
pbar.start(mailArray.length, 0,{
speed: "N/A"
});
let position = 0;
let codeArray: string[] = [];
let checkString: string = '';
for(let i = 0; i < mailArray.length; i++){ // as many codes as adresses
// check that codes are unique
let code = '';
do{
code = mkstring(4);
}while ( (config.force ? codeArray : [...codeArray, ...config.usedTokens]).includes(code))
codeArray.push(code);
checkString = `${checkString}|${code}`
position ++;
pbar.update(position);
/**
* Namespace containing the code for Generating the Code and delivering the mails
*/
export namespace MLGenerator {
/**
* Interface used to Return codes and mails form the main Function {@link generateToken}
*/
export interface MLGenReturn{
/** List of generated codes */
codes: string[];
/** List of processed mails */
mails: MLParser.MLItem[];
}
checkString = checkString.substr(1);
pbar.stop();
//write code lists
try {
if (!fs.existsSync(path.dirname(config.outFileMatch))){
fs.mkdirSync(path.dirname(config.outFileMatch));
}
fs.writeFileSync(config.outFileMatch, checkString);
} catch (err) {
error(err);
}
sendMails(resolve,error,mailArray,codeArray,config,dataSafe);
}
// randomize mails and tokens
async function sendMails(resolve: (value?: genReturn) => void,error: (reason?: any) => void,mailArray: mail[],codeArray: string[],config: any,dataSafe: SecureVault){
let mailserver = nodemailer.createTransport(config.mail);
// read mail template
let template!: HandlebarsTemplateDelegate<any>;
try {
const htmlSrc=fs.readFileSync(config.htmlPath, "utf8")
template = Handlebars.compile(htmlSrc)
} catch (error) {
console.error("Cannote read template file!")
error(error);
}
console.log("\nSending mails")
const pbar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
pbar.start(mailArray.length, 0,{
speed: "N/A"
});
let position = 0;
shuffleArray(mailArray);
shuffleArray(codeArray);
if (config.force){dataSafe.clearVault();}
for(let i = 0; i < mailArray.length; i++){
// send mail
dataSafe.writeTransaction(`process: ${mailArray[i].mail}`);
if (!config.dryrun){
dataSafe.pushData({
name: mailArray[i].name,
mail: mailArray[i].mail,
code: codeArray[i]
/**
* Main function used to generate and deliver codes
* @param config Main configuration object
* @param dataSafe Main safe used for logging and storing data
* @returns used codes and processed mails
*/
export function generateToken(config: any,dataSafe: SVault.SecureVault): Promise<MLGenReturn>{
return new Promise<MLGenReturn>((resolve,error) => {
MLParser.parseMails(config, dataSafe).then(res => {
// next step
generateCodes(resolve,error,res,config,dataSafe);
})
}
await send(mailArray[i].name, mailArray[i].mail, codeArray[i],template,mailserver,config,dataSafe);
position ++;
pbar.update(position);
});
}
pbar.stop();
shuffleArray(mailArray);
shuffleArray(codeArray);
shuffleArray(mailArray);
shuffleArray(codeArray);
resolve({
codes: config.force ? codeArray : (config.dryrun ? config.usedTokens : [...codeArray, ...config.usedTokens]),
mails: config.force ? mailArray : (config.dryrun ? config.usedMails : [...mailArray, ...config.usedMails])
});
}
async function send(name: string, mail: string, code: string,template: HandlebarsTemplateDelegate<any>,mailserver: Mail,config: any,dataSafe: SecureVault){
if (config.dryrun){
await delay(100);
console.log(`\n\x1b[36m -> dryrun: would send to ${mail}\x1b[0m`);
}else{
// fill template
let html = template({
"name": name,
"mail": mail,
"code": code
})
let mailOptions = {
from: `${config.mailFrom} <${config.mail.auth.user}>`, // sender address
to: mail, // list of receivers
subject: `Dein Zugangscode zur BJR Wahl`, // Subject line
html: html
};
/**
* Generate the same amout of codes as distinct mail adresses and stores them to the list file
* This function also includes previously used tokens to prevent duplicate tokens.
* Rejects if matchfile cannot be saved.
* @internal
* @param resolve Callback to resolve promise
* @param error Callback to reject promise
* @param mailArray list of mail adresses
* @param config Main configuration object
* This Function uses the following variables:
* usedTokens -> List of previosly used tokens
* outFileMatch -> Path to match file
* @param dataSafe Main safe used for logging and storing data
*/
async function generateCodes(resolve: (value?: MLGenReturn) => void,error: (reason?: any) => void,mailArray: MLParser.MLItem[],config: any,dataSafe: SVault.SecureVault){
console.log("\nGenerating codes")
const pbar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
pbar.start(mailArray.length, 0,{
speed: "N/A"
});
let position = 0;
let codeArray: string[] = [];
let checkString: string = '';
for(let i = 0; i < mailArray.length; i++){ // as many codes as adresses
// check that codes are unique
let code = '';
do{
code = mkstringCN(4);
}while ( (config.force ? codeArray : [...codeArray, ...config.usedTokens]).includes(code))
codeArray.push(code);
checkString = `${checkString}|${code}`
position ++;
pbar.update(position);
}
checkString = checkString.substr(1);
pbar.stop();
//write code lists
try {
await mailserver.sendMail(mailOptions);
dataSafe.writeTransaction(` -> mail sent`);
if (!fs.existsSync(path.dirname(config.outFileMatch))){
fs.mkdirSync(path.dirname(config.outFileMatch));
}
fs.writeFileSync(config.outFileMatch, checkString);
} catch (err) {
error(err);
}
// next
sendMails(resolve,error,mailArray,codeArray,config,dataSafe);
}
/**
* Reads template file and compiles template.
* Iterate through mails and codes, randomly assign code to mail and send mail to recipient.
* If dryrun is enabled, mails will not be sent and new mails won't be included in return.
* Rejects if template cannot be read.
* @internal
* @param resolve Callback to resolve promise
* @param error Callback to reject promise
* @param mailArray list of mail adresses
* @param codeArray list of generated codes
* @param config Main configuration object
* This Function uses the following variables:
* htmlPath -> Path to html template
* dryrun -> Boolean value. If true no mails will be sent and list won't be updated.
* force -> Boolean value. If true all mails are resent.
* usedTokens -> Array of Strings. Specifies already used tokens adresses.
* usedMails -> Array of Strings. Specifies already served mail adresses.
* mail -> mailserver settings
* @param dataSafe Main safe used for logging and storing data
*/
async function sendMails(resolve: (value?: MLGenReturn) => void,error: (reason?: any) => void,mailArray: MLParser.MLItem[],codeArray: string[],config: any,dataSafe: SVault.SecureVault){
let mailserver = nodemailer.createTransport(config.mail);
// read mail template and compile
let template!: HandlebarsTemplateDelegate<any>;
try {
const htmlSrc=fs.readFileSync(config.htmlPath, "utf8")
template = Handlebars.compile(htmlSrc)
} catch (error) {
console.log(`Error sendign mail to ${mail} : ${error}`)
dataSafe.writeTransaction(` -> mail failed : ${error}`);
console.error("Cannote read template file!")
error(error);
}
// send mails
console.log("\nSending mails")
const pbar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
pbar.start(mailArray.length, 0,{
speed: "N/A"
});
let position = 0;
// randomize arrays
shuffleArray(mailArray);
shuffleArray(codeArray);
if (config.force){dataSafe.clearVault();}
for(let i = 0; i < mailArray.length; i++){
// send mail
dataSafe.writeTransaction(`process: ${mailArray[i].mail}`);
if (!config.dryrun){
dataSafe.pushData({
name: mailArray[i].name,
mail: mailArray[i].mail,
code: codeArray[i]
})
}
await send(mailArray[i].name, mailArray[i].mail, codeArray[i],template,mailserver,config,dataSafe);
position ++;
pbar.update(position);
}
pbar.stop();
shuffleArray(mailArray);
shuffleArray(codeArray);
shuffleArray(mailArray);
shuffleArray(codeArray);
resolve({
codes: config.force ? codeArray : (config.dryrun ? config.usedTokens : [...codeArray, ...config.usedTokens]),
mails: config.force ? mailArray : (config.dryrun ? config.usedMails : [...mailArray, ...config.usedMails])
});
}
/**
* Reads template file and compiles template.
* Iterate through mails and codes, randomly assign code to mail and send mail to recipient.
* If dryrun is enabled, mails will not be sent and new mails won't be included in return.
* Rejects if template cannot be read.
* @internal
* @param name Name of recpipient
* @param mail Mail of recpipient
* @param code Code of recpipient
* @param template compiled mail template
* @param mailserver Mailserver settings
* @param config Main configuration object
* This Function uses the following variables:
* mail.auth.user -> sender mail adress
* mailFrom -> sender mail ailas
* dryrun -> Boolean value. If true no mails will be sent.
* @param dataSafe Main safe used for logging and storing data
*/
async function send(name: string, mail: string, code: string,template: HandlebarsTemplateDelegate<any>,mailserver: Mail,config: any,dataSafe: SVault.SecureVault){
if (config.dryrun){
await delay(100);
console.log(`\n\x1b[36m -> dryrun: would send to ${mail}\x1b[0m`);
}else{
// fill template
let html = template({
"name": name,
"mail": mail,
"code": code
})
let mailOptions = {
from: `${config.mailFrom} <${config.mail.auth.user}>`, // sender address
to: mail, // list of receivers
subject: `Dein Zugangscode zur BJR Wahl`, // Subject line
html: html
};
try {
await mailserver.sendMail(mailOptions);
dataSafe.writeTransaction(` -> mail sent`);
} catch (error) {
console.log(`Error sendign mail to ${mail} : ${error}`)
dataSafe.writeTransaction(` -> mail failed : ${error}`);
}
}
}
}
function delay(t: number, val?: number) {
return new Promise(function(resolve) {
setTimeout(function() {
resolve(val);
}, t);
});
}

View File

@@ -1,70 +1,118 @@
/**
* Dennis Gunia (c) 2020
*
* MailParser Implementation.
* This File provides a vault for storing encrypted and unencrypted data. Each encrypted element will be encrypted before storing it in an array.
* Each element (encrypted or unencrypted) ist stored in an array with an unique identifier.
* Safes can be stored and loaded. Unencrypted data can be stored, retrieved and modified. Encrypted data can be stored and retrieved.
* This class also implements an transaction function.
*
* @summary SecureVault Class Implementation
* @author Dennis Gunia <info@dennisgunia.de>
* @license Licensed under the Apache License, Version 2.0 (the "License").
*
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import * as fs from 'fs'
import { SecureVault } from "./vault";
import { SVault } from "./vault";
export interface MLItem{
mail: string;
name: string;
}
export function parseMails(config: any, dataSafe: SecureVault) {
return new Promise<MLItem[]>((resolve,reject) => {
let mailArray: MLItem[] = [];
let currSection: string = "global";
let lineCounter: number = 0;
let curCounter: number = 0;
console.log(`Reading mails for section ${currSection}`)
// read and process mail list
let readline = require('readline'),
instream = fs.createReadStream(config.inFileMail),
outstream = new (require('stream'))(),
rl = readline.createInterface(instream, outstream);
rl.on('line', function (line:string) {
lineCounter ++;
if(line.startsWith('[')){
if(line.endsWith(']')){
console.log(`Read ${curCounter} adresses for section ${currSection}`)
curCounter = 0;
currSection=line.substring(1,line.length -1);
console.log(`Reading mails for section ${currSection}`)
}else{
console.error(`Error parsing section on line ${lineCounter}: Syntax Error. Missing closing bracket ]`)
}
}else if (!line.startsWith('#')){
const ix = line.indexOf(";")
if (ix !== -1){
// check if already exist
dataSafe.writeTransaction(`reading mail ${line.substr(0,ix)} from category ${currSection}`);
if (config.force || config.usedMails.filter((el: MLItem) => el.mail == line.substr(0,ix)).length == 0){
// check for duplicate
if ( mailArray.filter((el: MLItem) => el.mail == line.substr(0,ix)).length == 0){
mailArray.push({
mail: line.substr(0,ix),
name: line.substr(ix + 1)
})
curCounter ++;
/**
* Namespace containing the code for Parsing the mail list.
*/
export namespace MLParser{
/**
* Interface containing properties of a single mail line
*/
export interface MLItem{
/** mail adress parsed form file */
mail: string;
/** name parsed form file */
name: string;
}
/**
* Encrypts and appends data to SecureVault.
* Also writes data to transaction log using @function writeTransaction
* @param config - Reference to config object.
* This Function uses the following variables:
* inFileMail -> String reference to mail list
* force -> Boolean value. If true all mails are resent.
* usedMails -> Array of Strings. Specifies already served mail adresses.
*
* @param dataSafe - Reference to safe object. This is needed for writing to the vault.log file
* @return Returns an array of all parsed mail adresses as promise
*/
export function parseMails(config: any, dataSafe: SVault.SecureVault) {
return new Promise<MLItem[]>((resolve,reject) => {
let mailArray: MLItem[] = [];
let currSection: string = "global";
let lineCounter: number = 0;
let curCounter: number = 0;
console.log(`Reading mails for section ${currSection}`)
// read and process mail list
let readline = require('readline'),
instream = fs.createReadStream(config.inFileMail),
outstream = new (require('stream'))(),
rl = readline.createInterface(instream, outstream);
rl.on('line', function (line:string) {
lineCounter ++;
if(line.startsWith('[')){
if(line.endsWith(']')){
console.log(`Read ${curCounter} adresses for section ${currSection}`)
curCounter = 0;
currSection=line.substring(1,line.length -1);
console.log(`Reading mails for section ${currSection}`)
}else{
console.error(`Error parsing section on line ${lineCounter}: Syntax Error. Missing closing bracket ]`)
}
}else if (!line.startsWith('#')){
const ix = line.indexOf(";")
if (ix !== -1){
// check if already exist
dataSafe.writeTransaction(`reading mail ${line.substr(0,ix)} from category ${currSection}`);
if (config.force || config.usedMails.filter((el: MLItem) => el.mail == line.substr(0,ix)).length == 0){
// check for duplicate
if ( mailArray.filter((el: MLItem) => el.mail == line.substr(0,ix)).length == 0){
mailArray.push({
mail: line.substr(0,ix),
name: line.substr(ix + 1)
})
curCounter ++;
}else{
dataSafe.writeTransaction(` -> duplicate mail. Skipping`);
console.error(`Skipping ${line.substr(0,ix)}: Duplicate`)
}
}else{
dataSafe.writeTransaction(` -> duplicate mail. Skipping`);
console.error(`Skipping ${line.substr(0,ix)}: Duplicate`)
dataSafe.writeTransaction(` -> already exists. Skipping`);
console.error(`Skipping ${line.substr(0,ix)}: Already sent`)
}
}else{
dataSafe.writeTransaction(` -> already exists. Skipping`);
console.error(`Skipping ${line.substr(0,ix)}: Already sent`)
console.error(`Error parsing mail on line ${lineCounter}: Syntax Error. Missing ;`)
}
}else{
console.error(`Error parsing mail on line ${lineCounter}: Syntax Error. Missing ;`)
}
}
});
rl.on('close', function (line:string) {
// next step
console.log(`Read ${curCounter} adresses for section ${currSection}\n${mailArray.length} mails read!`)
resolve(mailArray);
});
})
}
});
rl.on('close', function (line:string) {
// next step
console.log(`Read ${curCounter} adresses for section ${currSection}\n${mailArray.length} mails read!`)
resolve(mailArray);
});
})
}
}

50
src/util/misc.ts Normal file
View File

@@ -0,0 +1,50 @@
/**
* Dennis Gunia (c) 2020
*
* Reusable functions for different projects.
*
* @summary Open-Token entry point
* @author Dennis Gunia <info@dennisgunia.de>
* @license Licensed under the Apache License, Version 2.0 (the "License").
*
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
/**
* Generate random string with specified length.
* Generates only numbers and capital letters.
* @param length length of String
* @return generated string
*/
export function mkstringCN (length:number ) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
var charactersLength = characters.length;
for ( var i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
/**
* Wraps setTimeout into Promise
* @param t Millisceonds
*/
export function delay(t: number) {
return new Promise(function(resolve) {
setTimeout(function() {
resolve();
}, t);
});
}

View File

@@ -1,3 +1,7 @@
/**
* Randomize items in an array
* @param array Array to be shuffeled
*/
export function shuffleArray(array: any[]) {
for (var i = array.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
@@ -5,4 +9,4 @@ export function shuffleArray(array: any[]) {
array[i] = array[j];
array[j] = temp;
}
}
}

View File

@@ -1,9 +0,0 @@
export function mkstring (length:number ) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
var charactersLength = characters.length;
for ( var i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}

View File

@@ -1,184 +1,285 @@
/**
* Dennis Gunia (c) 2020
*
* SecureVault Class Implementation.
* This Class provides a vault for storing encrypted and unencrypted data. Each encrypted element will be encrypted before storing it in an array.
* Each element (encrypted or unencrypted) ist stored in an array with an unique identifier.
* Safes can be stored and loaded. Unencrypted data can be stored, retrieved and modified. Encrypted data can be stored and retrieved.
* This class also implements an transaction function.
*
* @summary SecureVault Class Implementation
* @author Dennis Gunia <info@dennisgunia.de>
* @license Licensed under the Apache License, Version 2.0 (the "License").
*
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import * as crypto from 'crypto'
import * as uuid from 'uuid'
import path from 'path';
import * as fs from 'fs'
import { generateKeyPair } from 'crypto';
import { Console } from 'console';
const vaultVersion = 'v1.2'
/**
* Namespace containing the code for the SecureVault.
*/
export namespace SVault {
/** vault version number. This variable will be added to the safe file and is used to chack compatibility. */
const vaultVersion = 'v1.2'
export interface SecureVaultItem {
u: string; // uuid
d: string; // data
k: string; // key
iv: string; // init vector
}
export interface StorageItem {
u: string; // uuid
d: string; // data
t: string; // tag
}
export interface secureVaultList {
items: SecureVaultItem[];
publicKey?: Buffer;
privateKey?: Buffer;
}
export class SecureVault {
safe: secureVaultList;
privPath?: string;
pubPath?: string;
storage: StorageItem[];
constructor (publicKey: string, privateKey?: string) {
this.storage = [];
this.safe = {
items: [],
publicKey: publicKey ?fs.readFileSync(path.resolve(publicKey)): undefined,
privateKey: privateKey ? fs.readFileSync(path.resolve(privateKey)): undefined
};
this.privPath = publicKey ? path.resolve(publicKey): undefined,
this.pubPath = privateKey ? path.resolve(privateKey): undefined
/** Interface for an vault item containing encrypted data */
export interface SecureVaultItem {
/** uuid */
u: string;
/** data */
d: string;
/** key */
k: string;
/** init vector */
iv: string;
}
async pushData(data: any): Promise<string>{
// encrypt payload
const txtData = JSON.stringify(data);
const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(16);
let cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv);
let encrypted = cipher.update(txtData);
encrypted = Buffer.concat([encrypted, cipher.final()]);
// encrypt key
var buffer = new Buffer(key);
if (!this.safe.publicKey){
throw new Error("Public Key not found");
/** Interface for an vault item containing unencrypted data */
export interface StorageItem {
/** uuid */
u: string;
/** data */
d: string;
/** tag (can be used to find specific items) */
t: string;
}
/** Interface for secureVault Array */
export interface secureVaultList {
items: SecureVaultItem[]; /** Array of encrypted items */
publicKey?: Buffer; /** Binary of public key */
privateKey?: Buffer; /** Binary of private key */
}
/** Class representing a SecureVault. */
export class SecureVault {
/** Safe object */
safe: secureVaultList;
/** Path to private key */
privPath?: string;
/** Path to public key */
pubPath?: string;
/** Array of unencrypted items */
storage: StorageItem[];
/**
* Create a SecureVault.
* @param publicKey - Path to public key.
* @param privateKey - Path to private key.
*/
constructor (publicKey?: string, privateKey?: string) {
this.storage = [];
this.safe = {
items: [],
publicKey: publicKey ?fs.readFileSync(path.resolve(publicKey)): undefined,
privateKey: privateKey ? fs.readFileSync(path.resolve(privateKey)): undefined
};
this.privPath = publicKey ? path.resolve(publicKey): undefined,
this.pubPath = privateKey ? path.resolve(privateKey): undefined
}
var asym_encrypted = crypto.publicEncrypt(this.safe.publicKey, buffer);
const u = uuid.v4()
const item = {
u,
d: encrypted.toString('hex'),
k: asym_encrypted.toString("base64"),
iv: iv.toString('hex')
}
this.writeTransaction("push: " + JSON.stringify(item))
this.safe.items.push(item)
return u;
}
writeTransaction(payload: string){
fs.appendFileSync('vault.log', `${payload}\n`);
}
async saveData(path: string): Promise<void>{
fs.writeFileSync(path, JSON.stringify({
version: vaultVersion,
vault: this.safe.items,
storage: this.storage
}));
}
async loadData(path: string): Promise<void>{
const loaded = JSON.parse(fs.readFileSync(path, 'utf8'));
switch (loaded.version){
case 'v1.1':
this.safe.items = loaded.vault;
break;
case 'v1.2':
this.safe.items = loaded.vault;
this.storage = loaded.storage;
break;
default:
console.error(`Unknown or unsupported vault file version: ${loaded.version}`)
}
}
async decryptData(): Promise<void>{
this.safe.items.forEach(el => {
// decrpyt key
let buffer = new Buffer(el.k, "base64");
if (!this.safe.privateKey){
throw new Error("Private Key not found");
/**
* Encrypts and appends data to SecureVault.
* Also writes data to transaction log using @function writeTransaction
* @param data - Path to public key.
* @return Returns the uuid of the added object as promise
*/
async pushData(data: any): Promise<string>{
// encrypt payload
const txtData = JSON.stringify(data);
const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(16);
let cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv);
let encrypted = cipher.update(txtData);
encrypted = Buffer.concat([encrypted, cipher.final()]);
// encrypt key
var buffer = new Buffer(key);
if (!this.safe.publicKey){
throw new Error("Public Key not found");
}
var key = crypto.privateDecrypt(this.safe.privateKey, buffer);
// decrpyt payload
let iv = Buffer.from(el.iv, 'hex');
let encryptedText = Buffer.from(el.d, 'hex');
let decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(encryptedText);
decrypted = Buffer.concat([decrypted, decipher.final()]);
const obj = JSON.parse(decrypted.toString());
console.log(obj);
})
}
var asym_encrypted = crypto.publicEncrypt(this.safe.publicKey, buffer);
const u = uuid.v4()
static genKey(publicKeyDir: string, privateKeyDir: string){
generateKeyPair('rsa', {
modulusLength: 4096,
publicKeyEncoding: {
type: 'pkcs1',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs1',
format: 'pem',
const item = {
u,
d: encrypted.toString('hex'),
k: asym_encrypted.toString("base64"),
iv: iv.toString('hex')
}
}, (err, publicKey, privateKey) => {
fs.writeFileSync(privateKeyDir, privateKey);
fs.writeFileSync(publicKeyDir, publicKey);
});
}
this.writeTransaction("push: " + JSON.stringify(item))
this.safe.items.push(item)
return u;
}
pushStorage(tag:string, data: any){
if (vaultVersion !== 'v1.2'){
throw new Error(`Storage not supported in ${vaultVersion}`);
}else{
let objJsonStr = JSON.stringify(data);
let objJsonB64 = Buffer.from(objJsonStr).toString("base64");
this.storage.push({
u: uuid.v4(),
d: objJsonB64,
t: tag
/**
* Writes data to the vault log file located at ./vault.log
* @param payload - Text to append
*/
writeTransaction(payload: string){
fs.appendFileSync('vault.log', `${payload}\n`);
}
/**
* Saves safe to file
* @param path - Path to safefile.
* @return Resolves promise after loaded
*/
async saveData(path: string): Promise<void>{
fs.writeFileSync(path, JSON.stringify({
version: vaultVersion,
vault: this.safe.items,
storage: this.storage
}));
}
/**
* Loads safe from file and check compatibility
* @param path - Path to safefile.
* @return Resolves promise after loaded
*/
async loadData(path: string): Promise<void>{
const loaded = JSON.parse(fs.readFileSync(path, 'utf8'));
switch (loaded.version){
case 'v1.1':
this.safe.items = loaded.vault;
break;
case 'v1.2':
this.safe.items = loaded.vault;
this.storage = loaded.storage;
break;
default:
console.error(`Unknown or unsupported vault file version: ${loaded.version}`)
}
}
/**
* Decrypts safe data.
* Requires specified and loaded private key.
* Prints data to console.
* @return Resolves promise after decrypted
*/
async decryptData(): Promise<void>{
this.safe.items.forEach(el => {
// decrpyt key
let buffer = new Buffer(el.k, "base64");
if (!this.safe.privateKey){
throw new Error("Private Key not found");
}
var key = crypto.privateDecrypt(this.safe.privateKey, buffer);
// decrpyt payload
let iv = Buffer.from(el.iv, 'hex');
let encryptedText = Buffer.from(el.d, 'hex');
let decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(encryptedText);
decrypted = Buffer.concat([decrypted, decipher.final()]);
const obj = JSON.parse(decrypted.toString());
console.log(obj);
})
}
/**
* Generates RSA keypair.
* @param publicKey - Path to public key.
* @param privateKey - Path to private key.
*/
static genKey(publicKeyDir: string, privateKeyDir: string){
generateKeyPair('rsa', {
modulusLength: 4096,
publicKeyEncoding: {
type: 'pkcs1',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs1',
format: 'pem',
}
}, (err, publicKey, privateKey) => {
fs.writeFileSync(privateKeyDir, privateKey);
fs.writeFileSync(publicKeyDir, publicKey);
});
}
}
setStorage(suuid:string, data: any){
if (vaultVersion !== 'v1.2'){
throw new Error(`Storage not supported in ${vaultVersion}`);
}else{
let objJsonStr = JSON.stringify(data);
let objJsonB64 = Buffer.from(objJsonStr,"utf8").toString("base64");
this.storage.filter(el => el.u == suuid)[0].d = objJsonB64;
/**
* Appends unencrypted data to safe.
* @param tag - Tag for item
* @param data - Data to store.
*/
pushStorage(tag:string, data: any){
if (vaultVersion !== 'v1.2'){
throw new Error(`Storage not supported in ${vaultVersion}`);
}else{
let objJsonStr = JSON.stringify(data);
let objJsonB64 = Buffer.from(objJsonStr).toString("base64");
this.storage.push({
u: uuid.v4(),
d: objJsonB64,
t: tag
});
}
}
/**
* Sets unencrypted data for item specified by suuid.
* @param suuid - UUID for item
* @param data - Data to store.
*/
setStorage(suuid:string, data: any){
if (vaultVersion !== 'v1.2'){
throw new Error(`Storage not supported in ${vaultVersion}`);
}else{
let objJsonStr = JSON.stringify(data);
let objJsonB64 = Buffer.from(objJsonStr,"utf8").toString("base64");
this.storage.filter(el => el.u == suuid)[0].d = objJsonB64;
}
}
/**
* Gets unencrypted data of item specified by suuid.
* @param suuid - UUID for item
* @return Data from item.
*/
getStorage(suuid:string){
if (vaultVersion !== 'v1.2'){
throw new Error(`Storage not supported in ${vaultVersion}`);
}else{
const data = this.storage.filter(el => el.u == suuid)[0];
let objJsonB64 = new Buffer(data.d, 'base64');
return JSON.parse(objJsonB64.toString('utf8'));
}
}
/**
* Gets list of UUIDs matching the tag.
* @param tag - tag to search for
* @return UUID from item.
*/
findStorage(tag:string){
if (vaultVersion !== 'v1.2'){
throw new Error(`Storage not supported in ${vaultVersion}`);
}else{
return this.storage.filter(el => el.t == tag);
}
}
/**
* Clears all encrypted items from safe.
*/
clearVault(){
this.safe.items = [];
}
}
getStorage(suuid:string){
if (vaultVersion !== 'v1.2'){
throw new Error(`Storage not supported in ${vaultVersion}`);
}else{
const data = this.storage.filter(el => el.u == suuid)[0];
let objJsonB64 = new Buffer(data.d, 'base64');
return JSON.parse(objJsonB64.toString('utf8'));
}
}
findStorage(tag:string){
if (vaultVersion !== 'v1.2'){
throw new Error(`Storage not supported in ${vaultVersion}`);
}else{
return this.storage.filter(el => el.t == tag);
}
}
clearVault(){
this.safe.items = [];
}
}
}