├── .gitignore ├── .vscode └── settings.json ├── README.md ├── dist └── hide ├── gulpfile.js ├── package-lock.json ├── package.json ├── src ├── config.ts ├── index.ts └── libs │ ├── AccountMgmt.spec.ts │ ├── AccountMgmt.ts │ ├── Encryption.spec.ts │ ├── Encryption.ts │ ├── FileHandler.ts │ ├── Import.ts │ ├── Readline.ts │ └── Vomit.ts ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore 2 | *.log 3 | *.rdb 4 | *.DS_Store 5 | /node_modules 6 | /files 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.tabSize": 2, 5 | "editor.wordWrap": "off", 6 | "editor.renderWhitespace": "all", 7 | "prettier.semi": false, 8 | "prettier.singleQuote": true, 9 | "prettier.vueIndentScriptAndStyle": true 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hide 2 | 3 | Open source, AES-256 bit encrypted password manager with all encrypted passwords stored locally on your machine. 4 | 5 | ## Usage 6 | 7 | ``` 8 | $ hide search my_new_account 9 | I found the following accounts: 10 | 11 | 0 of 412 total accounts returned 12 | 13 | 14 | $ hide add -n my_new_account -u myname -p the_secret_password -e "Some extra stuff!!!!" 15 | Successfully added account 'my_new_account'! 16 | 17 | 18 | $ hide search my_new_account 19 | I found the following accounts: 20 | NAME USERNAME EXTRA UUID 21 | my_new_account myname Some extra stuff!!!! 964c0e29-9732-4f03-9920-86b35cd04afe 22 | 1 of 413 total accounts returned 23 | 24 | 25 | $ hide show my_new_account 26 | NAME USERNAME EXTRA UUID 27 | my_new_account myname Some extra stuff!!!! 964c0e29-9732-4f03-9920-86b35cd04afe 28 | 29 | 30 | $ hide show my_new_account -p 31 | NAME USERNAME PASSWORD EXTRA UUID 32 | my_new_account myname the_secret_password Some extra stuff!!!! 964c0e29-9732-4f03-9920-86b35cd04afe 33 | ``` 34 | 35 | ## Description 36 | 37 | This is a CLI utility that can be used to store your account information, 38 | including websites, usernames, passwords, and additional info, securely using 39 | AES-256 bit encryption using a master secret that you configure. 40 | 41 | ## Install 42 | 43 | ``` 44 | $ npm install -g hide 45 | ``` 46 | 47 | ## Why? 48 | 49 | I've previously used Last Pass to securely store passwords for me within the 50 | context of a browser extension, and it worked decent. The problem is I not only 51 | have a ton of accounts with different passwords that I like to see, 52 | but I'm a developer so basically always live in a terminal window. 53 | This tool gives me the freedom to retrieve a username and/or password (among other information) 54 | about an account with a single command, and store it on my machine using AES-256 55 | bit encryption with a password I set. 56 | 57 | ## Config 58 | 59 | #### CRYPT_SECRET (required) 60 | 61 | The following should be set to control the global secret that's used with AES-256 bit encryption to secure the data stored on your machine. 62 | 63 | **DON'T LOSE/FORGET THIS** 64 | 65 | ``` 66 | $ export CRYPT_SECRET=[your all time master secret value] 67 | ``` 68 | 69 | #### NODE_HIDE_FILEPATH 70 | 71 | Directory where the encrypted file will live -- default: home directory (either process.env.HOME on unix/linux/mac or process.env.USERPROFILE on windows) 72 | 73 | ``` 74 | $ export NODE_HIDE_FILEPATH=~ 75 | ``` 76 | 77 | #### NODE_HIDE_FILENAME 78 | 79 | Name of flat file that holds the encrypted data of your accounts -- default: '\_\_node-hide-accounts' 80 | 81 | ``` 82 | $ export NODE_HIDE_FILENAME=my_encrypted_file 83 | ``` 84 | 85 | ## Usage 86 | 87 | ### Add an account 88 | 89 | #### PARAMETERS 90 | 91 | - -n / --name (REQUIRED): The name of the account you're storing. It can be any combination of alphanumeric characters. 92 | - -u / --username (optional): The username for the account. 93 | - -p / --password (optional): The password for the account. 94 | - -e / --extra (optional): Any additional information you'd like to provide about the account. 95 | 96 | ``` 97 | $ hide add -n my_new_account -u myname -p the_secret_password -e "Some extra stuff!!!!" 98 | 99 | Successfully added account 'my_new_account'! 100 | ``` 101 | 102 | ### Search your accounts 103 | 104 | #### PARAMETERS 105 | 106 | - -s / --search (optional): An optional term to look for accounts based on 107 | a case-insensitive search against the NAME or USERNAME. 108 | NOTE: the `search` command never shows the password for the account. Use `show` to retrieve the password. 109 | 110 | ``` 111 | $ hide search 112 | 113 | I found the following accounts: 114 | NAME USERNAME EXTRA UUID 115 | facebook.com userna def7f984-c2d7-4069-907c-facfad597123 116 | instagram.com iguser def7f984-abc1-1111-2222-facfad597123 117 | 2 of 2 total accounts returned 118 | 119 | $ hide search -s facebook 120 | 121 | I found the following accounts: 122 | NAME USERNAME EXTRA UUID 123 | facebook.com userna def7f984-c2d7-4069-907c-facfad597123 124 | 1 of 2 total accounts returned 125 | ``` 126 | 127 | ### Show a single account 128 | 129 | #### PARAMETERS 130 | 131 | Either uuid or name are at least required: 132 | 133 | - -i / --uuid: The unique identifier of the account you want to review. 134 | - -n / --name: The name of the account you're reviewing. 135 | 136 | Optional 137 | 138 | - -p / --password (optional): Whether to show the password. DEFAULT: false 139 | 140 | ``` 141 | $ hide show -i f62d5a21-4119-4a05-bced-0dca8f310d4b 142 | $ hide show -n facebook.com 143 | 144 | NAME USERNAME EXTRA UUID 145 | facebook.com fbuser f62d5a21-4119-4a05-bced-0dca8f310d4b 146 | 147 | $ hide show -n facebook.com -p 148 | 149 | NAME USERNAME PASSWORD EXTRA UUID 150 | facebook.com fbuser my_password1 f62d5a21-4119-4a05-bced-0dca8f310d4b 151 | ``` 152 | 153 | ### Update an account 154 | 155 | #### PARAMETERS 156 | 157 | Either uuid or name are at least required: 158 | 159 | - -i / --uuid: The unique identifier of the account you want to update. 160 | - -n / --name: The name of the account you're updating. 161 | 162 | Optional 163 | 164 | - -u / --username (optional): The username for the account. 165 | - -p / --password (optional): The password for the account. 166 | - -e / --extra (optional): Any additional information you'd like to provide about the account. 167 | 168 | ``` 169 | $ hide update -n facebook.com -u fbuser -p my_password1 170 | 171 | Successfully updated account with name: 'facebook.com'! 172 | ``` 173 | 174 | ### Delete an account 175 | 176 | #### PARAMETERS 177 | 178 | - -i / --uuid: The unique identifier of the account you want to delete. 179 | 180 | ``` 181 | $ hide delete -i f62d5a21-4119-4a05-bced-0dca8f310d4b 182 | 183 | Successfully deleted account with uuid: 'f62d5a21-4119-4a05-bced-0dca8f310d4b' 184 | ``` 185 | 186 | ### Re-encrypt your current file with a new secret 187 | 188 | ``` 189 | $ hide recrypt yourNew_Ultra_Secret_secret!@# 190 | 191 | Successfully updated your encrypted file with new secret to: /Users/username/__node-hide-accounts 192 | ``` 193 | 194 | ### Get the full file path of the encrypted flat file 195 | 196 | Returns the file location on your machine where the 197 | encrypted file lives. 198 | 199 | #### PARAMETERS 200 | 201 | None 202 | 203 | ``` 204 | $ hide file 205 | 206 | Your encrypted file is in the following location: 207 | /Users/yourname/__node-hide-accounts 208 | ``` 209 | 210 | ### Decrypt file and store on disk 211 | 212 | Decrypts the encrypted file that is used by hide to store your account 213 | information, and stores it on disk in the same location as your file 214 | with an appended '.json' to the file name. 215 | 216 | #### PARAMETERS 217 | 218 | None 219 | 220 | ``` 221 | $ hide decryptfile 222 | Are you sure you want to decrypt your file and save it to disk (yes/no): yes 223 | 224 | Successfully saved your decrypted account data to: 225 | /Users/yourname/__node-hide-accounts.json 226 | ``` 227 | 228 | ### Import from a CSV 229 | 230 | Note: This requires the CSV have headers that match the following: 231 | 232 | - `name`: the account name 233 | - `username`: the username of the account 234 | - `password`: the password of the account 235 | - `extra`: any desired extra information you want to store with the account 236 | 237 | #### PARAMETERS 238 | 239 | - -f / --filepath: The full filepath of the CSV that we're importing data from. 240 | 241 | ``` 242 | $ hide import -f /Users/yourname/myfile.csv 243 | 244 | Successfully added 123 accounts from CSV: /Users/yourname/myfile.csv! 245 | ``` 246 | 247 | ### Encrypt text or a file 248 | 249 | #### PARAMETERS 250 | 251 | - -t / --text: Text you want to encrypt. 252 | - -f / --file: A local file path containing data you want to encrypt. 253 | 254 | ``` 255 | $ hide encrypt -t testing123 256 | $ hide encrypt testing123 257 | 258 | 0f318802819cb67ea05c 259 | ``` 260 | 261 | ### Decrypt text or a file 262 | 263 | #### PARAMETERS 264 | 265 | - -t / --text: Text you want to decrypt. 266 | - -f / --file: A local file path containing data you want to decrypt. 267 | 268 | ``` 269 | $ hide decrypt -t 0f318802819cb67ea05c 270 | $ hide decrypt 0f318802819cb67ea05c 271 | 272 | testing123 273 | ``` 274 | 275 | ## Development 276 | 277 | If you want to clone and add/update functionality, you can build 278 | using the following. 279 | 280 | ### Build 281 | 282 | ``` 283 | $ npm run build 284 | ``` 285 | 286 | ### Tests 287 | 288 | ``` 289 | $ npm test 290 | ``` 291 | -------------------------------------------------------------------------------- /dist/hide: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | !function(e){var t={};function a(r){if(t[r])return t[r].exports;var n=t[r]={i:r,l:!1,exports:{}};return e[r].call(n.exports,n,n.exports,a),n.l=!0,n.exports}a.m=e,a.c=t,a.d=function(e,t,r){a.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,t){if(1&t&&(e=a(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(a.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)a.d(r,n,function(t){return e[t]}.bind(null,n));return r},a.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(t,"a",t),t},a.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},a.p="",a(a.s=5)}([function(e,t){e.exports=require("fs")},function(e,t,a){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const r={filepath:process.env.NODE_HIDE_FILEPATH||process.env["win32"==process.platform?"USERPROFILE":"HOME"]||".",filename:process.env.NODE_HIDE_FILENAME||"__node-hide-accounts",cryptography:{password:process.env.CRYPT_SECRET}};t.default=r},function(e,t){e.exports=require("path")},function(e,t,a){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const n=r(a(0)),s=r(a(2)),i=r(a(4)),o=r(a(1)),u=n.default.promises.mkdir,c=n.default.promises.writeFile,l=n.default.promises.readFile,d=i.default();t.default={filepath:s.default.join(o.default.filepath,o.default.filename),async getAndDecryptFlatFile(){if(this.doesDirectoryExist(o.default.filepath)){if(!this.doesFileExist(this.filepath))return await c(this.filepath,""),null;{const e=await l(this.filepath,"utf-8");if(0===e.length)return null;try{return JSON.parse(await d.decrypt(e))}catch(e){throw`We're having a problem parsing your flat file at '${this.filepath}'.\n This is likely due to a different master password, environment variable CRYPT_SECRET,\n being used that previously was set. Make sure you have the correct\n secret you used before and try again.`.replace(/\n\s+/g,"\n")}}}return await u(o.default.filepath),await c(this.filepath,""),""},async writeObjToFile(e,t={}){const a=Object.assign(t,e),r=await d.encrypt(JSON.stringify(a));return await c(this.filepath,r)},doesDirectoryExist(e){try{return n.default.statSync(e).isDirectory()}catch(e){return!1}},doesFileExist(e){try{return n.default.statSync(e).isFile()}catch(e){return!1}}}},function(e,t,a){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const n=r(a(10)),s=r(a(0)),i=a(11),o=r(a(12)),u=r(a(1)),c=s.default.promises.readFile,l=i.promisify(o.default.inflate),d=i.promisify(o.default.deflate);function f(e){const t=n.default.createHash("sha256");return t.update(e),t.digest("base64")}function p(e,t){const a=t||n.default.randomBytes(16);return{iv:a,key:n.default.pbkdf2Sync(e,a,1e5,32,"sha256")}}t.default=function(e={}){return{_algorithm:"aes-256-ctr",_secret:e.secret||u.default.cryptography.password,async encrypt(e){const t=f(this._secret),{iv:a,key:r}=p(t),s=n.default.createCipheriv(this._algorithm,r,a),i=e instanceof Buffer?e.toString("base64"):e;let o=s.update(i,"utf8","base64");return o+=s.final("base64"),await this.parseData(`${o}:${a.toString("base64")}`)},async encryptFileUtf8(e){const t=await c(e,{encoding:"utf8"});return await this.encrypt(t)},async decrypt(e){const t=(await this.parseData(e,!1)).toString(),[a,r]=t.split(":"),s=Buffer.from(r,"base64"),i=f(this._secret),{key:o}=p(i,s),u=n.default.createDecipheriv(this._algorithm,o,s);let c=u.update(a,"base64","utf8");return c+=u.final("utf8"),c},async decryptFileUtf8(e){const t=await c(e,{encoding:"utf8"});return await this.decrypt(t)},fileToHash:async e=>await new Promise((t,a)=>{const r=n.default.createHash("sha256"),i=s.default.createReadStream(e);i.on("data",e=>r.update(e)),i.on("error",a),i.on("end",()=>t(r.digest("base64")))}),async parseData(e,t=!0){switch(t){case!1:return await l(Buffer.from(e,"base64"));default:const t=await d(e);return Buffer.from(t).toString("base64")}}}}},function(e,t,a){e.exports=a(6)},function(e,t,a){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const n=r(a(7)),s=r(a(0)),i=r(a(2)),o=r(a(8)),u=r(a(4)),c=r(a(3)),l=r(a(13)),d=r(a(15)),f=r(a(17)),p=r(a(1)),y=s.default.promises.writeFile,h=n.default(process.argv.slice(2)),[w,m]=h._;p.default.cryptography.password||["file","version"].includes(w)||(f.default.noCryptSecret(),process.exit()),(async()=>{try{const e=h.e||h.extra,t=h.n||h.name,r=h.p||h.password,n=h.i||h.id||h.uuid,s=h.u||h.username,g=h.t||h.text,b=h.f||h.file,_=u.default(),v=i.default.join(p.default.filepath,p.default.filename);switch(w){case"file":f.default.twoLinesDifferentColors("Your encrypted file is in the following location:",v,"blue","green");break;case"version":f.default.success(a(20)("package.json").version,!1);break;case"add":const w=t||m;if(!w)return f.default.error("An account name (-n or --name) parameter is a required at a minimum to add a new account.");await o.default.addAccount(w,s,r,e),f.default.success(`Successfully added account '${w}'!`);break;case"delete":if(!n)return f.default.error("A uuid (-i or --id or --uuid) is a required to delete an account.");if(await o.default.deleteAccountByUuid(n))return f.default.success(`Successfully deleted account with uuid: '${n}'`);f.default.error(`We didn't find an account with uuid: '${n}'`);break;case"search":const F=h.s||h.search||m,x=await c.default.getAndDecryptFlatFile(),A=await o.default.searchForAccountsByName(F,x),S=await o.default.searchForAccountsByUsername(F,x),O={matches:[].concat(A.matches).concat(S.matches).sort(o.default.sortByName).reduce((e,t)=>(-1===e.indexOf(t)&&e.push(t),e),[]),total:A.total};f.default.listAccounts(O.matches,O.total);break;case"show":if(n){const e=await o.default.findAccountByUuid(n);return e?(r||delete e.password,f.default.listSingleAccount(e)):f.default.error("We didn't find an account with uuid: "+n)}if(t||m){const e=t||m,a=await o.default.findAccountByName(e);return a?(r||delete a.password,f.default.listSingleAccount(a)):f.default.error("We didn't find an account with name: "+e)}f.default.error("Either a name (-n or --name) or uuid (-i or --id or --uuid) parameter is a required at a minimum to show the details for an account.");break;case"update":if(n){const a=await o.default.findAccountByUuid(n);return a?(await o.default.updateAccount(n,{name:t,username:s,password:r,extra:e},a),f.default.success(`Successfully updated account with uuid: '${n}'!`)):f.default.error("We didn't find an account with uuid: "+n)}if(t){const a=await o.default.findAccountByName(t);return a?(await o.default.updateAccount(a.uuid,{name:t,username:s,password:r,extra:e},a),f.default.success(`Successfully updated account with name: '${t}'!`)):f.default.error("We didn't find an account with name: "+t)}f.default.error("Either a name (-n or --name) or uuid (-i or --id or --uuid) parameter is a required at a minimum to show the details for an account.");break;case"decryptfile":if("yes"===(await d.default().ask("Are you sure you want to decrypt your file and save it to disk (yes/no): ")).toLowerCase()){const e=v+".json",t=await c.default.getAndDecryptFlatFile();await y(e,JSON.stringify(t)),f.default.success("Successfully saved your decrypted account data to:\n"+e)}break;case"recrypt":const D=i.default.join(p.default.filepath,p.default.filename),j=u.default({secret:m}),E=await c.default.getAndDecryptFlatFile(),P=await j.encrypt(JSON.stringify(E));await y(D,P),f.default.success("Successfully updated your encrypted file with new secret to: "+D);break;case"import":const k=h.f||h.filepath||m;if(k&&c.default.doesFileExist(k)){let e=await l.default.csv(k),t=0,a=0;const r=e.length;for(;e.length>0;){const r=e.shift();r.name?(t++,await o.default.addAccount(r.name,r.username,r.password,r.extra)):a++}const n=`Successfully added ${t} accounts from CSV: ${k}!`,s=a>0?`Did not add ${a} accounts because we didn't see an account name ('name' CSV header).`:"";return f.default.twoLinesDifferentColors(n,s,"green","red"),f.default.singleLine(`Total number of rows in spreadsheet: ${r}\n`,"blue",0)}f.default.error("We can't find filepath provided: "+(k||"NO FILE PROVIDED"));break;case"encrypt":const B=g||m;let C;B?(C=await _.encrypt(B),f.default.success(C.toString())):b?(C=await _.encryptFileUtf8(b),f.default.success(C.toString())):f.default.error("Please enter text (-t or --text) or a file path (-f or --file) to encrypt text.");break;case"decrypt":const N=g||m;let T;N?(T=await _.decrypt(N),f.default.success(T)):b?(T=await _.decryptFileUtf8(b),f.default.success(T)):f.default.error("Please enter text (-t or --text) or a file path (-f or --file) to encrypt text.");break;default:f.default.error("I don't recognize what you are trying to do.\nPlease refer to the documentation for what commands I support.")}process.exit()}catch(e){"string"==typeof e?f.default.error(e):"TypeError: Bad input string"==e.toString()?(f.default.error(`Uh oh, The error we got is '${e.toString()}'\n\nThis usually means the CRYPT_SECRET is different for the info you're trying to decrypt than was used to encrypt it. Full stack trace below.`),console.log(e)):console.log(e),process.exit()}})()},function(e,t){e.exports=require("minimist")},function(e,t,a){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const n=a(9),s=r(a(3));t.default={createUuid:()=>n.v4(),async addAccount(e,t,a,r=""){const n={[this.createUuid()]:{name:e,username:t||"",password:a||"",extra:r||""}},i=await s.default.getAndDecryptFlatFile();return await s.default.writeObjToFile(n,i||{})},async updateAccount(e,t={},a={}){const r={[e]:{name:t.name||a.name||"",username:t.username||a.username||"",password:t.password||a.password||"",extra:t.extra||a.extra||""}},n=await s.default.getAndDecryptFlatFile();return await s.default.writeObjToFile(r,n||{})},async deleteAccountByUuid(e){let t=await s.default.getAndDecryptFlatFile();return!(!t||!t[e])&&(delete t[e],await s.default.writeObjToFile({},t),!0)},async findAccountByUuid(e){const t=await s.default.getAndDecryptFlatFile();return!(!t||!t[e])&&Object.assign(t[e],{uuid:e})},async findAccountByName(e){const t=await s.default.getAndDecryptFlatFile();if(!t)return!1;const a=Object.keys(t).filter(a=>t[a].name.toLowerCase()==e.toLowerCase())[0];return!!a&&Object.assign(t[a],{uuid:a})},async searchForAccountsByName(e,t){return await this.searchForAccountsByField("name",e,t)},async searchForAccountsByUsername(e,t){return await this.searchForAccountsByField("username",e,t)},async searchForAccountsByField(e,t,a){if(!(a=a||await s.default.getAndDecryptFlatFile()))return{matches:[],total:0};const r=Object.keys(a),n=r.length;return{matches:r.map(r=>{const n=a[r];if(!n)return null;if(t){const a=new RegExp(t,"i");return n[e]&&a.test(n[e])?Object.assign(n,{uuid:r}):null}return Object.assign(n,{uuid:r})}).filter(e=>!!e),total:n}},sortByName:(e,t)=>e.name.toLowerCase()new Promise((t,a)=>{const r=s.default({columns:!0});r.on("error",a),r.on("end",t),n.default.createReadStream(e).pipe(r)})}},function(e,t){e.exports=require("csv-parse")},function(e,t,a){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const n=r(a(16));t.default=function(){return{rl:n.default.createInterface({input:process.stdin,output:process.stdout}),ask(e,t=!0){return new Promise(a=>{this.rl.question(e,e=>{a(e),t&&this.close()})})},close(){this.rl.close()}}}},function(e,t){e.exports=require("readline")},function(e,t,a){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),a(18);const n=r(a(19)),s=()=>{};t.default={noCryptSecret(){this.wrapInNewlines(()=>{console.log("You don't have environment variable CRYPT_SECRET set.".red),console.log(">export CRYPT_SECRET=[your all time master secret value]".green)})},listSingleAccount(e){this.wrapInNewlines(()=>console.log(this.columnify([e]).green))},listAccounts(e=[],t=0){const a=e.map(e=>"string"==typeof e?{name:e}:(delete e.password,e));this.wrapInNewlines(()=>{console.log("I found the following accounts:".blue),console.log(this.columnify(a).green),console.log(`${e.length} of ${t} total accounts returned`.blue)})},twoLinesDifferentColors(e,t,a="blue",r="green"){this.wrapInNewlines(()=>{e.length>0&&console.log(e[a]),t.length>0&&console.log(t[r])})},singleLine(e,t="blue",a=1){this.wrapInNewlines(()=>console.log(e[t]),a)},success(e,t=!0){let a=e=>e();t&&(a=this.wrapInNewlines),a(()=>console.log(e.green))},error(e){this.wrapInNewlines(()=>console.log(e.red))},wrapInNewlines(e=s,t=1){const a=t-1>0?new Array(t-1).fill("\n").join(""):"";t>0&&console.log(a),e(),t>0&&console.log(a)},columnify:e=>n.default(e,{minWidth:15})}},function(e,t){e.exports=require("colors")},function(e,t){e.exports=require("columnify")},function(e,t){e.exports=require("root-require")}]); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp') 2 | const plumber = require('gulp-plumber') 3 | const insert = require('gulp-insert') 4 | const uglify = require('gulp-uglify-es').default 5 | const webpack = require('webpack-stream') 6 | const webpackConfig = require('./webpack.config.js') 7 | 8 | gulp.task('src', function () { 9 | return gulp 10 | .src(['src/**/*.ts', '!src/**/*.spec.ts']) 11 | .pipe(plumber()) 12 | .pipe(webpack(webpackConfig)) 13 | .pipe(uglify()) 14 | .pipe(gulp.dest('./dist')) 15 | }) 16 | 17 | gulp.task('index', function () { 18 | return gulp 19 | .src('./dist/hide') 20 | .pipe(insert.prepend('#!/usr/bin/env node\n\n')) 21 | .pipe(gulp.dest('./dist')) 22 | }) 23 | 24 | gulp.task('build', gulp.series('src', 'index')) 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hide", 3 | "version": "6.0.0", 4 | "description": "Open source, AES-256 bit encrypted password manager with all encrypted passwords stored locally on your machine.", 5 | "author": "Lance Whatley (https://lance.to)", 6 | "license": "MIT", 7 | "engines": { 8 | "node": ">= 12.0.0" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/whatl3y/hide.git" 13 | }, 14 | "scripts": { 15 | "build": "gulp build", 16 | "test": "mocha --require ts-node/register --recursive './src/**/*.spec.ts'" 17 | }, 18 | "bin": { 19 | "hide": "./dist/hide" 20 | }, 21 | "devDependencies": { 22 | "@types/columnify": "^1.5.0", 23 | "@types/minimist": "^1.2.0", 24 | "@types/mocha": "^7.0.2", 25 | "@types/node": "^14.0.22", 26 | "@types/uuid": "^8.0.0", 27 | "gulp": "^4.0.2", 28 | "gulp-insert": "^0.5.0", 29 | "gulp-plumber": "^1.2.1", 30 | "gulp-uglify-es": "^2.0.0", 31 | "mocha": "^8.0.1", 32 | "ts-loader": "^8.0.0", 33 | "ts-node": "^8.10.2", 34 | "typescript": "^3.9.6", 35 | "webpack-node-externals": "^2.3.0", 36 | "webpack-stream": "^5.2.1" 37 | }, 38 | "dependencies": { 39 | "bunyan": "^1.8.14", 40 | "colors": "^1.4.0", 41 | "columnify": "^1.5.4", 42 | "csv-parse": "^4.10.1", 43 | "minimist": "^1.2.5", 44 | "root-require": "^0.3.1", 45 | "uuid": "^8.2.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | interface IConfig { 2 | filepath: string 3 | filename: string 4 | cryptography: IConfigCrypto 5 | } 6 | 7 | interface IConfigCrypto { 8 | password: string | undefined 9 | } 10 | 11 | const config: IConfig = { 12 | filepath: process.env.NODE_HIDE_FILEPATH || getHomeDirectory(), 13 | filename: process.env.NODE_HIDE_FILENAME || '__node-hide-accounts', 14 | 15 | cryptography: { 16 | password: process.env.CRYPT_SECRET, 17 | }, 18 | } 19 | 20 | export default config 21 | 22 | function getHomeDirectory() { 23 | return ( 24 | process.env[process.platform == 'win32' ? 'USERPROFILE' : 'HOME'] || '.' 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import minimist from 'minimist' 2 | import fs from 'fs' 3 | import path from 'path' 4 | import AccountMgmt from './libs/AccountMgmt' 5 | import Encryption from './libs/Encryption' 6 | import FileHandler from './libs/FileHandler' 7 | import Import from './libs/Import' 8 | import Readline from './libs/Readline' 9 | import Vomit from './libs/Vomit' 10 | import config from './config' 11 | 12 | const writeFile = fs.promises.writeFile 13 | 14 | const argv = minimist(process.argv.slice(2)) 15 | const [command, second] = argv._ 16 | 17 | // we want to enforce CRYPT_SECRET to be set manually 18 | if (!config.cryptography.password && !['file', 'version'].includes(command)) { 19 | Vomit.noCryptSecret() 20 | process.exit() 21 | } 22 | 23 | ;(async () => { 24 | try { 25 | const extra = argv.e || argv.extra 26 | const name = argv.n || argv.name 27 | const password = argv.p || argv.password 28 | const uid = argv.i || argv.id || argv.uuid 29 | const username = argv.u || argv.username 30 | 31 | // encryption/decryption methods 32 | const text = argv.t || argv.text 33 | const file = argv.f || argv.file 34 | 35 | const encryption = Encryption() 36 | const fullPath = path.join(config.filepath, config.filename) 37 | 38 | switch (command) { 39 | case 'file': 40 | Vomit.twoLinesDifferentColors( 41 | `Your encrypted file is in the following location:`, 42 | fullPath, 43 | 'blue', 44 | 'green' 45 | ) 46 | break 47 | 48 | case 'version': 49 | Vomit.success(require('root-require')('package.json').version, false) 50 | break 51 | 52 | case 'add': 53 | const accountName = name || second 54 | if (!accountName) 55 | return Vomit.error( 56 | 'An account name (-n or --name) parameter is a required at a minimum to add a new account.' 57 | ) 58 | 59 | await AccountMgmt.addAccount(accountName, username, password, extra) 60 | Vomit.success(`Successfully added account '${accountName}'!`) 61 | 62 | break 63 | 64 | case 'delete': 65 | if (!uid) 66 | return Vomit.error( 67 | 'A uuid (-i or --id or --uuid) is a required to delete an account.' 68 | ) 69 | 70 | const result = await AccountMgmt.deleteAccountByUuid(uid) 71 | if (result) 72 | return Vomit.success( 73 | `Successfully deleted account with uuid: '${uid}'` 74 | ) 75 | 76 | Vomit.error(`We didn't find an account with uuid: '${uid}'`) 77 | 78 | break 79 | 80 | case 'search': 81 | const searchString = argv.s || argv.search || second 82 | const allAccounts = await FileHandler.getAndDecryptFlatFile() 83 | const nameMatchInfo = await AccountMgmt.searchForAccountsByName( 84 | searchString, 85 | allAccounts 86 | ) 87 | const usernameMatchInfo = await AccountMgmt.searchForAccountsByUsername( 88 | searchString, 89 | allAccounts 90 | ) 91 | const allMatches = [] 92 | .concat(nameMatchInfo.matches) 93 | .concat(usernameMatchInfo.matches) 94 | .sort(AccountMgmt.sortByName) 95 | .reduce((acc, val) => { 96 | if (acc.indexOf(val) === -1) acc.push(val) 97 | return acc 98 | }, []) 99 | 100 | const info = { 101 | matches: allMatches, 102 | total: nameMatchInfo.total, 103 | } 104 | Vomit.listAccounts(info.matches, info.total) 105 | break 106 | 107 | case 'show': 108 | if (uid) { 109 | const account = await AccountMgmt.findAccountByUuid(uid) 110 | if (account) { 111 | if (!password) delete account.password 112 | return Vomit.listSingleAccount(account) 113 | } 114 | return Vomit.error(`We didn't find an account with uuid: ${uid}`) 115 | } else if (name || second) { 116 | const nameStringToTry = name || second 117 | const account = await AccountMgmt.findAccountByName(nameStringToTry) 118 | if (account) { 119 | if (!password) delete account.password 120 | return Vomit.listSingleAccount(account) 121 | } 122 | return Vomit.error( 123 | `We didn't find an account with name: ${nameStringToTry}` 124 | ) 125 | } 126 | Vomit.error( 127 | 'Either a name (-n or --name) or uuid (-i or --id or --uuid) parameter is a required at a minimum to show the details for an account.' 128 | ) 129 | 130 | break 131 | 132 | case 'update': 133 | if (uid) { 134 | const account = await AccountMgmt.findAccountByUuid(uid) 135 | if (account) { 136 | await AccountMgmt.updateAccount( 137 | uid, 138 | { 139 | name: name, 140 | username: username, 141 | password: password, 142 | extra: extra, 143 | }, 144 | account 145 | ) 146 | return Vomit.success( 147 | `Successfully updated account with uuid: '${uid}'!` 148 | ) 149 | } 150 | return Vomit.error(`We didn't find an account with uuid: ${uid}`) 151 | } else if (name) { 152 | const account = await AccountMgmt.findAccountByName(name) 153 | if (account) { 154 | await AccountMgmt.updateAccount( 155 | account.uuid, 156 | { 157 | name: name, 158 | username: username, 159 | password: password, 160 | extra: extra, 161 | }, 162 | account 163 | ) 164 | return Vomit.success( 165 | `Successfully updated account with name: '${name}'!` 166 | ) 167 | } 168 | return Vomit.error(`We didn't find an account with name: ${name}`) 169 | } 170 | Vomit.error( 171 | 'Either a name (-n or --name) or uuid (-i or --id or --uuid) parameter is a required at a minimum to show the details for an account.' 172 | ) 173 | 174 | break 175 | 176 | case 'decryptfile': 177 | const answer = await Readline().ask( 178 | 'Are you sure you want to decrypt your file and save it to disk (yes/no): ' 179 | ) 180 | if (answer.toLowerCase() === 'yes') { 181 | const decryptedFullPath = `${fullPath}.json` 182 | const fileData = await FileHandler.getAndDecryptFlatFile() 183 | await writeFile(decryptedFullPath, JSON.stringify(fileData)) 184 | Vomit.success( 185 | `Successfully saved your decrypted account data to:\n${decryptedFullPath}` 186 | ) 187 | } 188 | break 189 | 190 | case 'recrypt': 191 | const dest = path.join(config.filepath, config.filename) 192 | const encryption2 = Encryption({ secret: second }) 193 | const currentFileData = await FileHandler.getAndDecryptFlatFile() 194 | const encryptedString = await encryption2.encrypt( 195 | JSON.stringify(currentFileData) 196 | ) 197 | await writeFile(dest, encryptedString) 198 | Vomit.success( 199 | `Successfully updated your encrypted file with new secret to: ${dest}` 200 | ) 201 | break 202 | 203 | case 'import': 204 | const importFilePath = argv.f || argv.filepath || second 205 | if (importFilePath && FileHandler.doesFileExist(importFilePath)) { 206 | let rows = await Import.csv(importFilePath) 207 | let numAccountsImported = 0 208 | let numAccountsNotImported = 0 209 | const totalRows = rows.length 210 | while (rows.length > 0) { 211 | const row = rows.shift() 212 | if (row.name) { 213 | numAccountsImported++ 214 | await AccountMgmt.addAccount( 215 | row.name, 216 | row.username, 217 | row.password, 218 | row.extra 219 | ) 220 | } else { 221 | numAccountsNotImported++ 222 | } 223 | } 224 | const str1 = `Successfully added ${numAccountsImported} accounts from CSV: ${importFilePath}!` 225 | const str2 = 226 | numAccountsNotImported > 0 227 | ? `Did not add ${numAccountsNotImported} accounts because we didn't see an account name ('name' CSV header).` 228 | : '' 229 | Vomit.twoLinesDifferentColors(str1, str2, 'green', 'red') 230 | return Vomit.singleLine( 231 | `Total number of rows in spreadsheet: ${totalRows}\n`, 232 | 'blue', 233 | 0 234 | ) 235 | } 236 | Vomit.error( 237 | `We can't find filepath provided: ${ 238 | importFilePath || 'NO FILE PROVIDED' 239 | }` 240 | ) 241 | 242 | break 243 | 244 | case 'encrypt': 245 | const plain = text || second 246 | let cipherText 247 | if (plain) { 248 | cipherText = await encryption.encrypt(plain) 249 | Vomit.success(cipherText.toString()) 250 | } else if (file) { 251 | cipherText = await encryption.encryptFileUtf8(file) 252 | Vomit.success(cipherText.toString()) 253 | } else { 254 | Vomit.error( 255 | `Please enter text (-t or --text) or a file path (-f or --file) to encrypt text.` 256 | ) 257 | } 258 | break 259 | 260 | case 'decrypt': 261 | const cipher = text || second 262 | let plainText 263 | if (cipher) { 264 | plainText = await encryption.decrypt(cipher) 265 | Vomit.success(plainText) 266 | } else if (file) { 267 | plainText = await encryption.decryptFileUtf8(file) 268 | Vomit.success(plainText) 269 | } else { 270 | Vomit.error( 271 | `Please enter text (-t or --text) or a file path (-f or --file) to encrypt text.` 272 | ) 273 | } 274 | break 275 | 276 | default: 277 | Vomit.error( 278 | `I don't recognize what you are trying to do.\nPlease refer to the documentation for what commands I support.` 279 | ) 280 | } 281 | 282 | process.exit() 283 | } catch (err) { 284 | if (typeof err === 'string') { 285 | Vomit.error(err) 286 | } else if (err.toString() == 'TypeError: Bad input string') { 287 | Vomit.error( 288 | `Uh oh, The error we got is '${err.toString()}'\n\nThis usually means the CRYPT_SECRET is different for the info you're trying to decrypt than was used to encrypt it. Full stack trace below.` 289 | ) 290 | console.log(err) 291 | } else { 292 | console.log(err) 293 | } 294 | process.exit() 295 | } 296 | })() 297 | -------------------------------------------------------------------------------- /src/libs/AccountMgmt.spec.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import AccountMgmt from './AccountMgmt' 3 | 4 | describe('AccountMgmt', function () { 5 | describe('#createUuid()', function () { 6 | it(`should create a UUID without error`, () => { 7 | const newUuid = AccountMgmt.createUuid() 8 | assert.equal(typeof newUuid, 'string') 9 | assert.equal(true, newUuid.length > 10) 10 | }) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/libs/AccountMgmt.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid' 2 | import FileHandler from './FileHandler' 3 | 4 | export interface IAccounts { 5 | [uid: string]: IAccountInfo 6 | } 7 | 8 | export interface IAccountInfo { 9 | name: string 10 | username: string 11 | password: string 12 | extra: string 13 | } 14 | 15 | export default { 16 | createUuid() { 17 | return uuidv4() 18 | }, 19 | 20 | async addAccount( 21 | name: string, 22 | username: string, 23 | password: string, 24 | extraInfo: string = '' 25 | ) { 26 | const newAccount: IAccounts = { 27 | [this.createUuid()]: { 28 | name: name, 29 | username: username || '', 30 | password: password || '', 31 | extra: extraInfo || '', 32 | }, 33 | } 34 | const rawAccountInfo = await FileHandler.getAndDecryptFlatFile() 35 | return await FileHandler.writeObjToFile(newAccount, rawAccountInfo || {}) 36 | }, 37 | 38 | async updateAccount( 39 | uuid: string, 40 | updatedInformation: any = {}, 41 | originalInformation: any = {} 42 | ) { 43 | const updatedAccount = { 44 | [uuid]: { 45 | name: updatedInformation.name || originalInformation.name || '', 46 | username: 47 | updatedInformation.username || originalInformation.username || '', 48 | password: 49 | updatedInformation.password || originalInformation.password || '', 50 | extra: updatedInformation.extra || originalInformation.extra || '', 51 | }, 52 | } 53 | const rawAccountInfo = await FileHandler.getAndDecryptFlatFile() 54 | return await FileHandler.writeObjToFile( 55 | updatedAccount, 56 | rawAccountInfo || {} 57 | ) 58 | }, 59 | 60 | async deleteAccountByUuid(uid: string) { 61 | let rawAccountInfo = await FileHandler.getAndDecryptFlatFile() 62 | if (!(rawAccountInfo && rawAccountInfo[uid])) return false 63 | 64 | delete rawAccountInfo[uid] 65 | await FileHandler.writeObjToFile({}, rawAccountInfo) 66 | return true 67 | }, 68 | 69 | async findAccountByUuid(uid: string) { 70 | const currentAccounts = await FileHandler.getAndDecryptFlatFile() 71 | if (!(currentAccounts && currentAccounts[uid])) return false 72 | return Object.assign(currentAccounts[uid], { uuid: uid }) 73 | }, 74 | 75 | async findAccountByName(name: string) { 76 | const currentAccounts = await FileHandler.getAndDecryptFlatFile() 77 | if (!currentAccounts) return false 78 | 79 | const matchingUuid = Object.keys(currentAccounts).filter( 80 | (uuid) => currentAccounts[uuid].name.toLowerCase() == name.toLowerCase() 81 | )[0] 82 | 83 | if (!matchingUuid) return false 84 | return Object.assign(currentAccounts[matchingUuid], { uuid: matchingUuid }) 85 | }, 86 | 87 | async searchForAccountsByName( 88 | searchString?: string, 89 | currentAccounts?: IAccounts 90 | ) { 91 | return await this.searchForAccountsByField( 92 | 'name', 93 | searchString, 94 | currentAccounts 95 | ) 96 | }, 97 | 98 | async searchForAccountsByUsername( 99 | searchString?: string, 100 | currentAccounts?: IAccounts 101 | ) { 102 | return await this.searchForAccountsByField( 103 | 'username', 104 | searchString, 105 | currentAccounts 106 | ) 107 | }, 108 | 109 | async searchForAccountsByField( 110 | field: 'name' | 'username' | 'password' | 'extra', 111 | searchString?: string, 112 | currentAccounts?: IAccounts 113 | ): Promise { 114 | currentAccounts = 115 | currentAccounts || (await FileHandler.getAndDecryptFlatFile()) 116 | if (!currentAccounts) { 117 | return { 118 | matches: [], 119 | total: 0, 120 | } 121 | } 122 | 123 | const uuids = Object.keys(currentAccounts) 124 | const totalNumAccounts = uuids.length 125 | const matchingAccounts = uuids 126 | .map((uuid) => { 127 | const account: IAccountInfo = (currentAccounts as IAccounts)[uuid] 128 | if (!account) return null 129 | if (searchString) { 130 | const searchRegexp = new RegExp(searchString, 'i') 131 | const fieldMatches = 132 | account[field] && searchRegexp.test(account[field]) 133 | if (fieldMatches) return Object.assign(account, { uuid: uuid }) 134 | return null 135 | } 136 | return Object.assign(account, { uuid: uuid }) 137 | }) 138 | .filter((info) => !!info) 139 | 140 | return { 141 | matches: matchingAccounts, 142 | total: totalNumAccounts, 143 | } 144 | }, 145 | 146 | sortByName(acc1: IAccountInfo, acc2: IAccountInfo) { 147 | return acc1.name.toLowerCase() < acc2.name.toLowerCase() ? -1 : 1 148 | }, 149 | } 150 | -------------------------------------------------------------------------------- /src/libs/Encryption.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import assert from 'assert' 3 | import Encryption from './Encryption' 4 | 5 | describe('Encryption', function () { 6 | const enc = Encryption({ secret: 'abc123' }) 7 | const originalText = 'test123' 8 | let cipherTextAndIv: any 9 | let plainText: string 10 | 11 | describe('#encrypt()', function () { 12 | it(`should encrypt string without issue`, async () => { 13 | cipherTextAndIv = await enc.encrypt(originalText) 14 | assert.equal(typeof cipherTextAndIv, 'string') 15 | // assert.equal(2, cipherTextAndIv.split(':').length) 16 | }) 17 | }) 18 | 19 | describe('#decrypt()', function () { 20 | it(`should decrypt cipher string without issue`, async () => { 21 | plainText = await enc.decrypt(cipherTextAndIv) 22 | assert.equal(typeof plainText, 'string') 23 | assert.equal(plainText, originalText) 24 | }) 25 | }) 26 | 27 | describe('#fileToHash()', function () { 28 | it(`should hash file contents without errors`, async () => { 29 | await enc.fileToHash(path.join(__dirname, 'Encryption.ts')) 30 | }) 31 | }) 32 | 33 | describe(`#parseData()`, () => { 34 | const key = 'test_compress_1' 35 | 36 | it('should be a valid base64 string on deflate, then be a valid Buffer and the correct value on inflate', async () => { 37 | const base64string = await enc.parseData('lance123') 38 | if (typeof base64string !== 'string') 39 | throw new Error('needs to be string') 40 | 41 | const buff = await enc.parseData(base64string, false) 42 | const lance123 = buff.toString() 43 | 44 | assert.equal(true, typeof base64string === 'string') 45 | assert.equal(true, buff instanceof Buffer) 46 | assert.equal('lance123', lance123) 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /src/libs/Encryption.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | import fs from 'fs' 3 | import { promisify } from 'util' 4 | import zlib from 'zlib' 5 | import config from '../config' 6 | 7 | const readFile = fs.promises.readFile 8 | const inflate: any = promisify(zlib.inflate) 9 | const deflate: any = promisify(zlib.deflate) 10 | 11 | export default function Encryption(options: any = {}) { 12 | const alg = 'aes-256-ctr' 13 | const sec = options.secret || config.cryptography.password 14 | 15 | return { 16 | _algorithm: alg, 17 | _secret: sec, 18 | 19 | async encrypt(input: Buffer | string) { 20 | const secret = getFilledSecret(this._secret) 21 | const { iv, key } = getKeyAndIV(secret) 22 | const cipher = crypto.createCipheriv(this._algorithm, key, iv) 23 | 24 | const inputStr = 25 | input instanceof Buffer ? input.toString('base64') : input 26 | let cipherText = cipher.update(inputStr, 'utf8', 'base64') 27 | cipherText += cipher.final('base64') 28 | return await this.parseData(`${cipherText}:${iv.toString('base64')}`) 29 | }, 30 | 31 | async encryptFileUtf8(filePath: string) { 32 | const fileText = await readFile(filePath, { encoding: 'utf8' }) 33 | return await this.encrypt(fileText) 34 | }, 35 | 36 | async decrypt(text: string) { 37 | const inflatedString = (await this.parseData(text, false)).toString() 38 | const [rawBase64, ivBase64] = inflatedString.split(':') 39 | const iv = Buffer.from(ivBase64, 'base64') 40 | const secret = getFilledSecret(this._secret) 41 | const { key } = getKeyAndIV(secret, iv) 42 | const decipher = crypto.createDecipheriv(this._algorithm, key, iv) 43 | 44 | let dec = decipher.update(rawBase64, 'base64', 'utf8') 45 | dec += decipher.final('utf8') 46 | return dec 47 | }, 48 | 49 | async decryptFileUtf8(filePath: string) { 50 | const fileText = await readFile(filePath, { encoding: 'utf8' }) 51 | return await this.decrypt(fileText) 52 | }, 53 | 54 | async fileToHash(filePath: string) { 55 | return await new Promise((resolve, reject) => { 56 | const sha256Sum = crypto.createHash('sha256') 57 | 58 | const s = fs.createReadStream(filePath) 59 | s.on('data', (data) => sha256Sum.update(data)) 60 | s.on('error', reject) 61 | s.on('end', () => resolve(sha256Sum.digest('base64'))) 62 | }) 63 | }, 64 | 65 | // Handles any gzip/deflating/inflating we might be doing to data 66 | // we're passing to and from Redis. 67 | // NOTE: if inflating, we will always return a raw Buffer. If deflating, 68 | // we return a base64 encoded string. 69 | async parseData( 70 | value: string, 71 | isRawData: boolean = true 72 | ): Promise { 73 | switch (isRawData) { 74 | case false: 75 | return await inflate(Buffer.from(value, 'base64')) 76 | 77 | default: 78 | //true 79 | const compressedValue = await deflate(value) 80 | return Buffer.from(compressedValue as any).toString('base64') 81 | } 82 | }, 83 | } 84 | } 85 | 86 | // Private methods 87 | function getFilledSecret(secret: string): string { 88 | const sha256Sum = crypto.createHash('sha256') 89 | sha256Sum.update(secret) 90 | return sha256Sum.digest('base64') 91 | } 92 | 93 | function getKeyAndIV(key: string, iv?: Buffer) { 94 | const ivBuffer = iv || crypto.randomBytes(16) 95 | const derivedKey = crypto.pbkdf2Sync(key, ivBuffer, 1e5, 32, 'sha256') 96 | return { 97 | iv: ivBuffer, 98 | key: derivedKey, 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/libs/FileHandler.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import Encryption from './Encryption' 4 | import config from '../config' 5 | 6 | const mkdirPromise = fs.promises.mkdir 7 | const writeFilePromise = fs.promises.writeFile 8 | const readFilePromise = fs.promises.readFile 9 | 10 | const encryption = Encryption() 11 | 12 | export default { 13 | filepath: path.join(config.filepath, config.filename), 14 | 15 | async getAndDecryptFlatFile() { 16 | if (this.doesDirectoryExist(config.filepath)) { 17 | if (this.doesFileExist(this.filepath)) { 18 | const rawFileData = await readFilePromise(this.filepath, 'utf-8') 19 | if (rawFileData.length === 0) return null 20 | else { 21 | try { 22 | const accountsJson = JSON.parse( 23 | await encryption.decrypt(rawFileData) 24 | ) 25 | return accountsJson 26 | } catch (err) { 27 | throw `We're having a problem parsing your flat file at '${this.filepath}'. 28 | This is likely due to a different master password, environment variable CRYPT_SECRET, 29 | being used that previously was set. Make sure you have the correct 30 | secret you used before and try again.`.replace(/\n\s+/g, '\n') 31 | } 32 | } 33 | } else { 34 | await writeFilePromise(this.filepath, '') 35 | return null 36 | } 37 | } 38 | 39 | await mkdirPromise(config.filepath) 40 | await writeFilePromise(this.filepath, '') 41 | return '' 42 | }, 43 | 44 | async writeObjToFile(obj: any, origObj = {}) { 45 | const newObj = Object.assign(origObj, obj) 46 | const encryptedString = await encryption.encrypt(JSON.stringify(newObj)) 47 | return await writeFilePromise(this.filepath, encryptedString) 48 | }, 49 | 50 | doesDirectoryExist(dirPath: string): boolean { 51 | try { 52 | const exists = fs.statSync(dirPath).isDirectory() 53 | return exists 54 | } catch (e) { 55 | return false 56 | } 57 | }, 58 | 59 | doesFileExist(filePath: string): boolean { 60 | try { 61 | const exists = fs.statSync(filePath).isFile() 62 | return exists 63 | } catch (e) { 64 | return false 65 | } 66 | }, 67 | } 68 | -------------------------------------------------------------------------------- /src/libs/Import.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import parse from 'csv-parse' 3 | 4 | export default { 5 | csv(filepath: string): Promise { 6 | return new Promise((resolve, reject) => { 7 | const parser = parse({ columns: true }) 8 | parser.on('error', reject) 9 | parser.on('end', resolve) 10 | fs.createReadStream(filepath).pipe(parser) 11 | }) 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/libs/Readline.ts: -------------------------------------------------------------------------------- 1 | import readline from 'readline' 2 | 3 | export default function Readline() { 4 | return { 5 | rl: readline.createInterface({ 6 | input: process.stdin, 7 | output: process.stdout, 8 | }), 9 | 10 | ask(question: string, close: boolean = true): Promise { 11 | return new Promise((resolve) => { 12 | this.rl.question(question, (answer) => { 13 | resolve(answer) 14 | 15 | if (close) this.close() 16 | }) 17 | }) 18 | }, 19 | 20 | close() { 21 | this.rl.close() 22 | }, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/libs/Vomit.ts: -------------------------------------------------------------------------------- 1 | import 'colors' 2 | import columnify from 'columnify' 3 | import { IAccountInfo } from './AccountMgmt' 4 | 5 | const NOOP = () => {} 6 | 7 | export default { 8 | noCryptSecret() { 9 | this.wrapInNewlines(() => { 10 | console.log(`You don't have environment variable CRYPT_SECRET set.`.red) 11 | console.log( 12 | `>export CRYPT_SECRET=[your all time master secret value]`.green 13 | ) 14 | }) 15 | }, 16 | 17 | listSingleAccount(accountRecord: IAccountInfo) { 18 | this.wrapInNewlines(() => 19 | console.log(this.columnify([accountRecord]).green) 20 | ) 21 | }, 22 | 23 | listAccounts(accountsAry: IAccountInfo[] = [], totalNumAccounts = 0) { 24 | const accounts = accountsAry.map((a) => { 25 | if (typeof a === 'string') return { name: a } 26 | 27 | delete a.password 28 | return a 29 | }) 30 | 31 | this.wrapInNewlines(() => { 32 | console.log('I found the following accounts:'.blue) 33 | console.log(this.columnify(accounts).green) 34 | console.log( 35 | `${accountsAry.length} of ${totalNumAccounts} total accounts returned` 36 | .blue 37 | ) 38 | }) 39 | }, 40 | 41 | twoLinesDifferentColors( 42 | str1: string, 43 | str2: string, 44 | color1: any = 'blue', 45 | color2: any = 'green' 46 | ) { 47 | this.wrapInNewlines(() => { 48 | if (str1.length > 0) console.log(str1[color1]) 49 | if (str2.length > 0) console.log(str2[color2]) 50 | }) 51 | }, 52 | 53 | singleLine(str: string, color: any = 'blue', numWrappedRows = 1) { 54 | this.wrapInNewlines(() => console.log(str[color]), numWrappedRows) 55 | }, 56 | 57 | success(string: string, twoLineWrap = true) { 58 | let wrapper = (foo: any) => foo() 59 | if (twoLineWrap) wrapper = this.wrapInNewlines 60 | wrapper(() => console.log(string.green)) 61 | }, 62 | 63 | error(string: string) { 64 | this.wrapInNewlines(() => console.log(string.red)) 65 | }, 66 | 67 | wrapInNewlines(functionToWriteMoreOutput = NOOP, howMany = 1) { 68 | const newlineString = 69 | howMany - 1 > 0 ? new Array(howMany - 1).fill('\n').join('') : '' 70 | if (howMany > 0) console.log(newlineString) 71 | functionToWriteMoreOutput() 72 | if (howMany > 0) console.log(newlineString) 73 | }, 74 | 75 | columnify(data: any) { 76 | return columnify(data, { 77 | minWidth: 15, 78 | }) 79 | }, 80 | } 81 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": false, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "dist", /* Redirect output structure to the directory. */ 18 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var nodeExternals = require('webpack-node-externals') 2 | 3 | module.exports = { 4 | mode: 'production', 5 | entry: ['./src/index.ts'], 6 | target: 'node', 7 | output: { 8 | filename: 'hide', 9 | }, 10 | externals: [nodeExternals()], 11 | module: { 12 | rules: [ 13 | // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader` 14 | { test: /\.ts$/, loader: 'ts-loader' }, 15 | ], 16 | }, 17 | resolve: { 18 | extensions: ['.ts', '.js'], 19 | alias: { 20 | // https://medium.com/js-dojo/how-to-reduce-your-vue-js-bundle-size-with-webpack-3145bf5019b7 21 | moment: 'moment/src/moment', 22 | }, 23 | }, 24 | } 25 | --------------------------------------------------------------------------------