├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── lint.yml │ ├── test-macos.yml │ ├── test-ubuntu.yml │ └── test-windows.yml ├── .gitignore ├── .npmrc ├── index.js ├── license ├── package.json ├── readme.md └── test ├── deserialize.js ├── fixtures ├── serialize-deserialize.js └── serialize-only.js └── serialize.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | push: 5 | branches: '*' 6 | pull_request: 7 | branches: '*' 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | name: XO & Prettier 13 | steps: 14 | - name: Setup repo 15 | uses: actions/checkout@v2 16 | - name: Setup node 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 10 20 | - name: Install dev dependencies 21 | run: | 22 | npm install 23 | npm list --dev --depth=0 24 | - name: Run lint 25 | run: npm run lint 26 | -------------------------------------------------------------------------------- /.github/workflows/test-macos.yml: -------------------------------------------------------------------------------- 1 | name: test-macos 2 | 3 | on: 4 | push: 5 | branches: '*' 6 | pull_request: 7 | branches: '*' 8 | 9 | jobs: 10 | test: 11 | runs-on: macos-latest 12 | name: AVA & Codecov 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | node: [14, 12, 10] 17 | steps: 18 | - name: Setup repo 19 | uses: actions/checkout@v2 20 | - name: Setup node ${{ matrix.node }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node }} 24 | - name: Install lib dependencies 25 | run: | 26 | npm install --production 27 | npm list --prod --depth=0 28 | - name: Install dev dependencies 29 | run: | 30 | npm install 31 | npm list --dev --depth=0 32 | - name: Run tests 33 | run: npm run test 34 | - name: Upload coverage to Codecov 35 | uses: codecov/codecov-action@v1 36 | -------------------------------------------------------------------------------- /.github/workflows/test-ubuntu.yml: -------------------------------------------------------------------------------- 1 | name: test-ubuntu 2 | 3 | on: 4 | push: 5 | branches: '*' 6 | pull_request: 7 | branches: '*' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | name: AVA & Codecov 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | node: [14, 12, 10] 17 | steps: 18 | - name: Setup repo 19 | uses: actions/checkout@v2 20 | - name: Setup node ${{ matrix.node }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node }} 24 | - name: Install lib dependencies 25 | run: | 26 | npm install --production 27 | npm list --prod --depth=0 28 | - name: Install dev dependencies 29 | run: | 30 | npm install 31 | npm list --dev --depth=0 32 | - name: Run tests 33 | run: npm run test 34 | - name: Upload coverage to Codecov 35 | uses: codecov/codecov-action@v1 36 | -------------------------------------------------------------------------------- /.github/workflows/test-windows.yml: -------------------------------------------------------------------------------- 1 | name: test-windows 2 | 3 | on: 4 | push: 5 | branches: '*' 6 | pull_request: 7 | branches: '*' 8 | 9 | jobs: 10 | test: 11 | runs-on: windows-latest 12 | name: AVA & Codecov 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | node: [14, 12, 10] 17 | steps: 18 | - name: Setup repo 19 | uses: actions/checkout@v2 20 | - name: Setup node ${{ matrix.node }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node }} 24 | - name: Install lib dependencies 25 | run: | 26 | npm install --production 27 | npm list --prod --depth=0 28 | - name: Install dev dependencies 29 | run: | 30 | npm install 31 | npm list --dev --depth=0 32 | - name: Run tests 33 | run: npm run test 34 | - name: Upload coverage to Codecov 35 | uses: codecov/codecov-action@v1 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/linux,macos,windows,node 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=linux,macos,windows,node 4 | 5 | ### Linux ### 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | 20 | ### macOS ### 21 | # General 22 | .DS_Store 23 | .AppleDouble 24 | .LSOverride 25 | 26 | # Icon must end with two \r 27 | Icon 28 | 29 | # Thumbnails 30 | ._* 31 | 32 | # Files that might appear in the root of a volume 33 | .DocumentRevisions-V100 34 | .fseventsd 35 | .Spotlight-V100 36 | .TemporaryItems 37 | .Trashes 38 | .VolumeIcon.icns 39 | .com.apple.timemachine.donotpresent 40 | 41 | # Directories potentially created on remote AFP share 42 | .AppleDB 43 | .AppleDesktop 44 | Network Trash Folder 45 | Temporary Items 46 | .apdisk 47 | 48 | ### Node ### 49 | # Logs 50 | logs 51 | *.log 52 | npm-debug.log* 53 | yarn-debug.log* 54 | yarn-error.log* 55 | lerna-debug.log* 56 | 57 | # Diagnostic reports (https://nodejs.org/api/report.html) 58 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 59 | 60 | # Runtime data 61 | pids 62 | *.pid 63 | *.seed 64 | *.pid.lock 65 | 66 | # Directory for instrumented libs generated by jscoverage/JSCover 67 | lib-cov 68 | 69 | # Coverage directory used by tools like istanbul 70 | coverage 71 | *.lcov 72 | 73 | # nyc test coverage 74 | .nyc_output 75 | 76 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 77 | .grunt 78 | 79 | # Bower dependency directory (https://bower.io/) 80 | bower_components 81 | 82 | # node-waf configuration 83 | .lock-wscript 84 | 85 | # Compiled binary addons (https://nodejs.org/api/addons.html) 86 | build/Release 87 | 88 | # Dependency directories 89 | node_modules/ 90 | jspm_packages/ 91 | 92 | # TypeScript v1 declaration files 93 | typings/ 94 | 95 | # TypeScript cache 96 | *.tsbuildinfo 97 | 98 | # Optional npm cache directory 99 | .npm 100 | 101 | # Optional eslint cache 102 | .eslintcache 103 | 104 | # Microbundle cache 105 | .rpt2_cache/ 106 | .rts2_cache_cjs/ 107 | .rts2_cache_es/ 108 | .rts2_cache_umd/ 109 | 110 | # Optional REPL history 111 | .node_repl_history 112 | 113 | # Output of 'npm pack' 114 | *.tgz 115 | 116 | # Yarn Integrity file 117 | .yarn-integrity 118 | 119 | # dotenv environment variables file 120 | .env 121 | .env.test 122 | 123 | # parcel-bundler cache (https://parceljs.org/) 124 | .cache 125 | 126 | # Next.js build output 127 | .next 128 | 129 | # Nuxt.js build / generate output 130 | .nuxt 131 | dist 132 | 133 | # Gatsby files 134 | .cache/ 135 | # Comment in the public line in if your project uses Gatsby and not Next.js 136 | # https://nextjs.org/blog/next-9-1#public-directory-support 137 | # public 138 | 139 | # vuepress build output 140 | .vuepress/dist 141 | 142 | # Serverless directories 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | .fusebox/ 147 | 148 | # DynamoDB Local files 149 | .dynamodb/ 150 | 151 | # TernJS port file 152 | .tern-port 153 | 154 | # Stores VSCode versions used for testing VSCode extensions 155 | .vscode-test 156 | 157 | ### Windows ### 158 | # Windows thumbnail cache files 159 | Thumbs.db 160 | Thumbs.db:encryptable 161 | ehthumbs.db 162 | ehthumbs_vista.db 163 | 164 | # Dump file 165 | *.stackdump 166 | 167 | # Folder config file 168 | [Dd]esktop.ini 169 | 170 | # Recycle Bin used on file shares 171 | $RECYCLE.BIN/ 172 | 173 | # Windows Installer files 174 | *.cab 175 | *.msi 176 | *.msix 177 | *.msm 178 | *.msp 179 | 180 | # Windows shortcuts 181 | *.lnk 182 | 183 | # End of https://www.toptal.com/developers/gitignore/api/linux,macos,windows,node 184 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const idRegex = /^[a-z0-9-]{1,32}$/; 2 | const nameRegex = /^[a-z0-9-]{1,32}$/; 3 | const valueRegex = /^[a-zA-Z0-9/+.-]+$/; 4 | const b64Regex = /^([a-zA-Z0-9/+.-]+|)$/; 5 | const decimalRegex = /^((-)?[1-9]\d*|0)$/; 6 | const versionRegex = /^v=(\d+)$/; 7 | 8 | function objToKeyVal(obj) { 9 | return objectKeys(obj) 10 | .map(k => [k, obj[k]].join('=')) 11 | .join(','); 12 | } 13 | 14 | function keyValtoObj(str) { 15 | const obj = {}; 16 | str.split(',').forEach(ps => { 17 | const pss = ps.split('='); 18 | if (pss.length < 2) { 19 | throw new TypeError(`params must be in the format name=value`); 20 | } 21 | 22 | obj[pss.shift()] = pss.join('='); 23 | }); 24 | return obj; 25 | } 26 | 27 | function objectKeys(object) { 28 | /* istanbul ignore next */ 29 | return Object.keys(object); 30 | } 31 | 32 | function objectValues(object) { 33 | /* istanbul ignore next */ 34 | if (typeof Object.values === 'function') return Object.values(object); 35 | /* istanbul ignore next */ 36 | return objectKeys(object).map(k => object[k]); 37 | } 38 | 39 | /** 40 | * Generates a PHC string using the data provided. 41 | * @param {Object} opts Object that holds the data needed to generate the PHC 42 | * string. 43 | * @param {string} opts.id Symbolic name for the function. 44 | * @param {Number} [opts.version] The version of the function. 45 | * @param {Object} [opts.params] Parameters of the function. 46 | * @param {Buffer} [opts.salt] The salt as a binary buffer. 47 | * @param {Buffer} [opts.hash] The hash as a binary buffer. 48 | * @return {string} The hash string adhering to the PHC format. 49 | */ 50 | function serialize(opts) { 51 | const fields = ['']; 52 | 53 | if (typeof opts !== 'object' || opts === null) { 54 | throw new TypeError('opts must be an object'); 55 | } 56 | 57 | // Identifier Validation 58 | if (typeof opts.id !== 'string') { 59 | throw new TypeError('id must be a string'); 60 | } 61 | 62 | if (!idRegex.test(opts.id)) { 63 | throw new TypeError(`id must satisfy ${idRegex}`); 64 | } 65 | 66 | fields.push(opts.id); 67 | 68 | if (typeof opts.version !== 'undefined') { 69 | if ( 70 | typeof opts.version !== 'number' || 71 | opts.version < 0 || 72 | !Number.isInteger(opts.version) 73 | ) { 74 | throw new TypeError('version must be a positive integer number'); 75 | } 76 | 77 | fields.push(`v=${opts.version}`); 78 | } 79 | 80 | // Parameters Validation 81 | if (typeof opts.params !== 'undefined') { 82 | if (typeof opts.params !== 'object' || opts.params === null) { 83 | throw new TypeError('params must be an object'); 84 | } 85 | 86 | const pk = objectKeys(opts.params); 87 | if (!pk.every(p => nameRegex.test(p))) { 88 | throw new TypeError(`params names must satisfy ${nameRegex}`); 89 | } 90 | 91 | // Convert Numbers into Numeric Strings and Buffers into B64 encoded strings. 92 | pk.forEach(k => { 93 | if (typeof opts.params[k] === 'number') { 94 | opts.params[k] = opts.params[k].toString(); 95 | } else if (Buffer.isBuffer(opts.params[k])) { 96 | opts.params[k] = opts.params[k].toString('base64').split('=')[0]; 97 | } 98 | }); 99 | const pv = objectValues(opts.params); 100 | if (!pv.every(v => typeof v === 'string')) { 101 | throw new TypeError('params values must be strings'); 102 | } 103 | 104 | if (!pv.every(v => valueRegex.test(v))) { 105 | throw new TypeError(`params values must satisfy ${valueRegex}`); 106 | } 107 | 108 | const strpar = objToKeyVal(opts.params); 109 | fields.push(strpar); 110 | } 111 | 112 | if (typeof opts.salt !== 'undefined') { 113 | // Salt Validation 114 | if (!Buffer.isBuffer(opts.salt)) { 115 | throw new TypeError('salt must be a Buffer'); 116 | } 117 | 118 | fields.push(opts.salt.toString('base64').split('=')[0]); 119 | 120 | if (typeof opts.hash !== 'undefined') { 121 | // Hash Validation 122 | if (!Buffer.isBuffer(opts.hash)) { 123 | throw new TypeError('hash must be a Buffer'); 124 | } 125 | 126 | fields.push(opts.hash.toString('base64').split('=')[0]); 127 | } 128 | } 129 | 130 | // Create the PHC formatted string 131 | const phcstr = fields.join('$'); 132 | 133 | return phcstr; 134 | } 135 | 136 | /** 137 | * Parses data from a PHC string. 138 | * @param {string} phcstr A PHC string to parse. 139 | * @return {Object} The object containing the data parsed from the PHC string. 140 | */ 141 | function deserialize(phcstr) { 142 | if (typeof phcstr !== 'string' || phcstr === '') { 143 | throw new TypeError('pchstr must be a non-empty string'); 144 | } 145 | 146 | if (phcstr[0] !== '$') { 147 | throw new TypeError('pchstr must contain a $ as first char'); 148 | } 149 | 150 | const fields = phcstr.split('$'); 151 | // Remove first empty $ 152 | fields.shift(); 153 | 154 | // Parse Fields 155 | let maxf = 5; 156 | if (!versionRegex.test(fields[1])) maxf--; 157 | if (fields.length > maxf) { 158 | throw new TypeError( 159 | `pchstr contains too many fileds: ${fields.length}/${maxf}` 160 | ); 161 | } 162 | 163 | // Parse Identifier 164 | const id = fields.shift(); 165 | if (!idRegex.test(id)) { 166 | throw new TypeError(`id must satisfy ${idRegex}`); 167 | } 168 | 169 | let version; 170 | // Parse Version 171 | if (versionRegex.test(fields[0])) { 172 | version = parseInt(fields.shift().match(versionRegex)[1], 10); 173 | } 174 | 175 | let hash; 176 | let salt; 177 | if (b64Regex.test(fields[fields.length - 1])) { 178 | if (fields.length > 1 && b64Regex.test(fields[fields.length - 2])) { 179 | // Parse Hash 180 | hash = Buffer.from(fields.pop(), 'base64'); 181 | // Parse Salt 182 | salt = Buffer.from(fields.pop(), 'base64'); 183 | } else { 184 | // Parse Salt 185 | salt = Buffer.from(fields.pop(), 'base64'); 186 | } 187 | } 188 | 189 | // Parse Parameters 190 | let params; 191 | if (fields.length > 0) { 192 | const parstr = fields.pop(); 193 | params = keyValtoObj(parstr); 194 | if (!objectKeys(params).every(p => nameRegex.test(p))) { 195 | throw new TypeError(`params names must satisfy ${nameRegex}`); 196 | } 197 | 198 | const pv = objectValues(params); 199 | if (!pv.every(v => valueRegex.test(v))) { 200 | throw new TypeError(`params values must satisfy ${valueRegex}`); 201 | } 202 | 203 | const pk = objectKeys(params); 204 | // Convert Decimal Strings into Numbers 205 | pk.forEach(k => { 206 | params[k] = decimalRegex.test(params[k]) 207 | ? parseInt(params[k], 10) 208 | : params[k]; 209 | }); 210 | } 211 | 212 | if (fields.length > 0) { 213 | throw new TypeError(`pchstr contains unrecognized fileds: ${fields}`); 214 | } 215 | 216 | // Build the output object 217 | const phcobj = {id}; 218 | if (version) phcobj.version = version; 219 | if (params) phcobj.params = params; 220 | if (salt) phcobj.salt = salt; 221 | if (hash) phcobj.hash = hash; 222 | 223 | return phcobj; 224 | } 225 | 226 | module.exports = { 227 | serialize, 228 | deserialize 229 | }; 230 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2020 Simone Primarosa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@phc/format", 3 | "version": "1.0.0", 4 | "description": "PHC string format serializer/deserializer", 5 | "license": "MIT", 6 | "homepage": "https://github.com/simonepri/phc-format#readme", 7 | "repository": "github:simonepri/phc-format", 8 | "bugs": { 9 | "url": "https://github.com/simonepri/phc-format/issues", 10 | "email": "simonepri@outlook.com" 11 | }, 12 | "author": "Simone Primarosa (https://simoneprimarosa.com)", 13 | "contributors": [ 14 | "Simone Primarosa (https://simoneprimarosa.com)" 15 | ], 16 | "keywords": [ 17 | "mcf", 18 | "phc", 19 | "modular", 20 | "crypt", 21 | "passwords", 22 | "hashing", 23 | "competition", 24 | "password", 25 | "standard", 26 | "crypto" 27 | ], 28 | "main": "index.js", 29 | "files": [ 30 | "index.js" 31 | ], 32 | "engines": { 33 | "node": ">=10" 34 | }, 35 | "scripts": { 36 | "lint": "xo", 37 | "test": "nyc ava", 38 | "release": "npx np", 39 | "update": "npx npm-check -u" 40 | }, 41 | "dependencies": {}, 42 | "devDependencies": { 43 | "ava": "^3.9.0", 44 | "nyc": "^15.1.0", 45 | "xo": "~0.27.2" 46 | }, 47 | "ava": { 48 | "verbose": true 49 | }, 50 | "nyc": { 51 | "reporter": [ 52 | "lcovonly", 53 | "text" 54 | ] 55 | }, 56 | "xo": { 57 | "prettier": true, 58 | "space": true 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 | phc-format 3 |

4 |

5 | 6 | 7 | Latest version on npm 8 | 9 | 10 | 11 | Downloads on npm 12 | 13 | 14 | 15 | Project license 16 | 17 | 18 |
19 | 20 | 21 | 22 | Lint status 23 | 24 | 25 | 26 | Test macOS status 27 | 28 | 29 | 30 | Test Ubuntu status 31 | 32 | 33 | 34 | Test Windows status 35 | 36 | 37 |
38 | 39 | 40 | 41 | Codecov Coverage report 42 | 43 | 44 | 45 | Known Vulnerabilities 46 | 47 | 48 | 49 | Dependency Status 50 | 51 | 52 |
53 | 54 | 55 | 56 | XO Code Style used 57 | 58 | 59 | 60 | AVA Test Runner used 61 | 62 | 63 | 64 | Istanbul Test Coverage used 65 | 66 | 67 | 68 | NI Scaffolding System used 69 | 70 | 71 | 72 | NP Release System used 73 | 74 |

75 |

76 | 📝 PHC string format serializer/deserializer 77 | 78 |
79 | 80 | 81 | Coded with ❤️ by Simone Primarosa. 82 | 83 |

84 | 85 | ## Motivation 86 | The [PHC String Format][specs:phc] is an attempt to specify a common hash string format that's a restricted & well defined subset of the Modular Crypt Format. New hashes are strongly encouraged to adhere to the PHC specification, rather than the much looser [Modular Crypt Format][specs:mcf]. 87 | 88 | ## Install 89 | 90 | ```bash 91 | npm install --save @phc/format 92 | ``` 93 | 94 | ## Usage 95 | 96 | ```js 97 | const phc = require('@phc/format'); 98 | 99 | const phcobj = { 100 | id: 'pbkdf2-sha256', 101 | params: {i: 6400}, 102 | salt: Buffer.from('0ZrzXitFSGltTQnBWOsdAw', 'base64'), 103 | hash: Buffer.from('Y11AchqV4b0sUisdZd0Xr97KWoymNE0LNNrnEgY4H9M', 'base64'), 104 | }; 105 | 106 | const phcstr = "$pbkdf2-sha256$i=6400$0ZrzXitFSGltTQnBWOsdAw$Y11AchqV4b0sUisdZd0Xr97KWoymNE0LNNrnEgY4H9M"; 107 | 108 | phc.serialize(phcobj); 109 | // => phcstr 110 | 111 | phc.deserialize(phcstr); 112 | // => phcobj 113 | ``` 114 | 115 | You can also pass an optional version parameter. 116 | 117 | ```js 118 | const phc = require('@phc/format'); 119 | 120 | const phcobj = { 121 | id: 'argon2i', 122 | version: 19, 123 | params: { 124 | m: 120, 125 | t: 5000, 126 | p: 2 127 | }, 128 | salt: Buffer.from('iHSDPHzUhPzK7rCcJgOFfg', 'base64'), 129 | hash: Buffer.from('J4moa2MM0/6uf3HbY2Tf5Fux8JIBTwIhmhxGRbsY14qhTltQt+Vw3b7tcJNEbk8ium8AQfZeD4tabCnNqfkD1g', 'base64'), 130 | }; 131 | 132 | const phcstr = "$argon2i$v=19$m=120,t=5000,p=2$iHSDPHzUhPzK7rCcJgOFfg$J4moa2MM0/6uf3HbY2Tf5Fux8JIBTwIhmhxGRbsY14qhTltQt+Vw3b7tcJNEbk8ium8AQfZeD4tabCnNqfkD1g"; 133 | 134 | phc.serialize(phcobj); 135 | // => phcstr 136 | 137 | phc.deserialize(phcstr); 138 | // => phcobj 139 | ``` 140 | 141 | ## API 142 | 143 | #### TOC 144 |
145 |
serialize(opts)string
146 |

Generates a PHC string using the data provided.

147 |
148 |
deserialize(phcstr)Object
149 |

Parses data from a PHC string.

150 |
151 |
152 | 153 | 154 | 155 | ### serialize(opts) ⇒ string 156 | Generates a PHC string using the data provided. 157 | 158 | **Kind**: global function 159 | **Returns**: string - The hash string adhering to the PHC format. 160 | 161 | | Param | Type | Description | 162 | | --- | --- | --- | 163 | | opts | Object | Object that holds the data needed to generate the PHC string. | 164 | | opts.id | string | Symbolic name for the function. | 165 | | [opts.version] | Number | The version of the function. | 166 | | [opts.params] | Object | Parameters of the function. | 167 | | [opts.salt] | Buffer | The salt as a binary buffer. | 168 | | [opts.hash] | Buffer | The hash as a binary buffer. | 169 | 170 | 171 | 172 | ### deserialize(phcstr) ⇒ Object 173 | Parses data from a PHC string. 174 | 175 | **Kind**: global function 176 | **Returns**: Object - The object containing the data parsed from the PHC string. 177 | 178 | | Param | Type | Description | 179 | | --- | --- | --- | 180 | | phcstr | string | A PHC string to parse. | 181 | 182 | ## Contributing 183 | Contributions are REALLY welcome and if you find a security flaw in this code, PLEASE [report it][new issue]. 184 | Please check the [contributing guidelines][contributing] for more details. Thanks! 185 | 186 | ## Authors 187 | - **Simone Primarosa** - *Github* ([@simonepri][github:simonepri]) • *Twitter* ([@simoneprimarosa][twitter:simoneprimarosa]) 188 | 189 | See also the list of [contributors][contributors] who participated in this project. 190 | 191 | ## License 192 | This project is licensed under the MIT License - see the [license][license] file for details. 193 | 194 | 195 | 196 | [start]: https://github.com/simonepri/phc-format#start-of-content 197 | [new issue]: https://github.com/simonepri/phc-format/issues/new 198 | [contributors]: https://github.com/simonepri/phc-format/contributors 199 | 200 | [license]: https://github.com/simonepri/phc-format/tree/master/license 201 | [contributing]: https://github.com/simonepri/phc-format/tree/master/.github/contributing.md 202 | 203 | [github:simonepri]: https://github.com/simonepri 204 | [twitter:simoneprimarosa]: http://twitter.com/intent/user?screen_name=simoneprimarosa 205 | 206 | [specs:mcf]: https://github.com/ademarre/binary-mcf 207 | [specs:phc]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md 208 | -------------------------------------------------------------------------------- /test/deserialize.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | 3 | const m = require('..'); 4 | const sdData = require('./fixtures/serialize-deserialize'); 5 | 6 | test('should deserialize correct phc strings', t => { 7 | sdData.serialized.forEach((g, i) => { 8 | t.deepEqual(m.deserialize(sdData.serialized[i]), sdData.deserialized[i]); 9 | }); 10 | }); 11 | 12 | test('should thow errors if trying to deserialize an invalid phc string', async t => { 13 | let err = await t.throws(() => m.deserialize(null)); 14 | t.is(err.message, 'pchstr must be a non-empty string'); 15 | 16 | err = await t.throws(() => m.deserialize('a$invalid')); 17 | t.is(err.message, 'pchstr must contain a $ as first char'); 18 | 19 | err = await t.throws(() => m.deserialize('$b$c$d$e$f')); 20 | t.is(err.message, 'pchstr contains too many fileds: 5/4'); 21 | 22 | err = await t.throws(() => m.deserialize('invalid')); 23 | t.is(err.message, 'pchstr must contain a $ as first char'); 24 | 25 | err = await t.throws(() => m.deserialize('$i_n_v_a_l_i_d')); 26 | t.regex(err.message, /id must satisfy/); 27 | 28 | err = await t.throws(() => m.deserialize('$pbkdf2$rounds_=1000')); 29 | t.regex(err.message, /params names must satisfy/); 30 | 31 | err = await t.throws(() => m.deserialize('$pbkdf2$rounds=1000@')); 32 | t.regex(err.message, /params values must satisfy/); 33 | 34 | err = await t.throws(() => m.deserialize('$pbkdf2$rounds:1000')); 35 | t.regex(err.message, /params must be in the format name=value/); 36 | 37 | err = await t.throws(() => 38 | m.deserialize('$argon2i$unrecognized$m=120,t=5000,p=2$EkCWX6pSTqWruiR0') 39 | ); 40 | t.regex(err.message, /pchstr contains unrecognized fileds/); 41 | 42 | err = await t.throws(() => 43 | m.deserialize( 44 | '$argon2i$unrecognized$v=19$m=120,t=5000,p=2$EkCWX6pSTqWruiR0' 45 | ) 46 | ); 47 | t.is(err.message, 'pchstr contains too many fileds: 5/4'); 48 | 49 | err = await t.throws(() => 50 | m.deserialize( 51 | '$argon2i$v=19$unrecognized$m=120,t=5000,p=2$EkCWX6pSTqWruiR0' 52 | ) 53 | ); 54 | t.regex(err.message, /pchstr contains unrecognized fileds/); 55 | }); 56 | -------------------------------------------------------------------------------- /test/fixtures/serialize-deserialize.js: -------------------------------------------------------------------------------- 1 | const serialized = [ 2 | '$argon2i$m=120,t=5000,p=2', 3 | '$argon2i$m=120,t=4294967295,p=2', 4 | '$argon2i$m=2040,t=5000,p=255', 5 | '$argon2i$m=120,t=5000,p=2,keyid=Hj5+dsK0', 6 | '$argon2i$m=120,t=5000,p=2,keyid=Hj5+dsK0ZQ', 7 | '$argon2i$m=120,t=5000,p=2,keyid=Hj5+dsK0ZQA', 8 | '$argon2i$m=120,t=5000,p=2,data=sRlHhRmKUGzdOmXn01XmXygd5Kc', 9 | '$argon2i$m=120,t=5000,p=2,keyid=Hj5+dsK0,data=sRlHhRmKUGzdOmXn01XmXygd5Kc', 10 | 11 | '$argon2i$m=120,t=5000,p=2$/LtFjH5rVL8', 12 | '$argon2i$m=120,t=5000,p=2$4fXXG0spB92WPB1NitT8/OH0VKI', 13 | '$argon2i$m=120,t=5000,p=2$BwUgJHHQaynE+a4nZrYRzOllGSjjxuxNXxyNRUtI6Dlw/zlbt6PzOL8Onfqs6TcG', 14 | '$argon2i$m=120,t=5000,p=2,keyid=Hj5+dsK0$4fXXG0spB92WPB1NitT8/OH0VKI', 15 | '$argon2i$m=120,t=5000,p=2,data=sRlHhRmKUGzdOmXn01XmXygd5Kc$4fXXG0spB92WPB1NitT8/OH0VKI', 16 | '$argon2i$m=120,t=5000,p=2,keyid=Hj5+dsK0,data=sRlHhRmKUGzdOmXn01XmXygd5Kc$4fXXG0spB92WPB1NitT8/OH0VKI', 17 | 18 | '$argon2i$m=120,t=5000,p=2$4fXXG0spB92WPB1NitT8/OH0VKI$iPBVuORECm5biUsjq33hn9/7BKqy9aPWKhFfK2haEsM', 19 | '$argon2i$m=120,t=5000,p=2,keyid=Hj5+dsK0$4fXXG0spB92WPB1NitT8/OH0VKI$iPBVuORECm5biUsjq33hn9/7BKqy9aPWKhFfK2haEsM', 20 | '$argon2i$m=120,t=5000,p=2,data=sRlHhRmKUGzdOmXn01XmXygd5Kc$4fXXG0spB92WPB1NitT8/OH0VKI$iPBVuORECm5biUsjq33hn9/7BKqy9aPWKhFfK2haEsM', 21 | '$argon2i$m=120,t=5000,p=2,keyid=Hj5+dsK0,data=sRlHhRmKUGzdOmXn01XmXygd5Kc$4fXXG0spB92WPB1NitT8/OH0VKI$iPBVuORECm5biUsjq33hn9/7BKqy9aPWKhFfK2haEsM', 22 | '$argon2i$m=120,t=5000,p=2,keyid=Hj5+dsK0,data=sRlHhRmKUGzdOmXn01XmXygd5Kc$iHSDPHzUhPzK7rCcJgOFfg$EkCWX6pSTqWruiR0', 23 | '$argon2i$m=120,t=5000,p=2,keyid=Hj5+dsK0,data=sRlHhRmKUGzdOmXn01XmXygd5Kc$iHSDPHzUhPzK7rCcJgOFfg$J4moa2MM0/6uf3HbY2Tf5Fux8JIBTwIhmhxGRbsY14qhTltQt+Vw3b7tcJNEbk8ium8AQfZeD4tabCnNqfkD1g', 24 | '$scrypt$ln=1,r=16,p=1$$d9ZXYjhleyA7GcpCwYoEl/FrSETjB0ro39/6P+3iFEL80Aad7QlI+DJqdToPyB8X6NPg+y4NNijPNeIMONGJBg', 25 | '$argon2i$v=19$m=120,t=5000,p=2,keyid=Hj5+dsK0,data=sRlHhRmKUGzdOmXn01XmXygd5Kc$iHSDPHzUhPzK7rCcJgOFfg$J4moa2MM0/6uf3HbY2Tf5Fux8JIBTwIhmhxGRbsY14qhTltQt+Vw3b7tcJNEbk8ium8AQfZeD4tabCnNqfkD1g' 26 | ]; 27 | 28 | const deserialized = [ 29 | { 30 | id: 'argon2i', 31 | params: {m: 120, t: 5000, p: 2} 32 | }, 33 | { 34 | id: 'argon2i', 35 | params: {m: 120, t: 4294967295, p: 2} 36 | }, 37 | { 38 | id: 'argon2i', 39 | params: {m: 2040, t: 5000, p: 255} 40 | }, 41 | { 42 | id: 'argon2i', 43 | params: {m: 120, t: 5000, p: 2, keyid: 'Hj5+dsK0'} 44 | }, 45 | { 46 | id: 'argon2i', 47 | params: {m: 120, t: 5000, p: 2, keyid: 'Hj5+dsK0ZQ'} 48 | }, 49 | { 50 | id: 'argon2i', 51 | params: {m: 120, t: 5000, p: 2, keyid: 'Hj5+dsK0ZQA'} 52 | }, 53 | { 54 | id: 'argon2i', 55 | params: {m: 120, t: 5000, p: 2, data: 'sRlHhRmKUGzdOmXn01XmXygd5Kc'} 56 | }, 57 | { 58 | id: 'argon2i', 59 | params: { 60 | m: 120, 61 | t: 5000, 62 | p: 2, 63 | keyid: 'Hj5+dsK0', 64 | data: 'sRlHhRmKUGzdOmXn01XmXygd5Kc' 65 | } 66 | }, 67 | { 68 | id: 'argon2i', 69 | params: {m: 120, t: 5000, p: 2}, 70 | salt: Buffer.from('/LtFjH5rVL8', 'base64') 71 | }, 72 | { 73 | id: 'argon2i', 74 | params: {m: 120, t: 5000, p: 2}, 75 | salt: Buffer.from('4fXXG0spB92WPB1NitT8/OH0VKI', 'base64') 76 | }, 77 | { 78 | id: 'argon2i', 79 | params: {m: 120, t: 5000, p: 2}, 80 | salt: Buffer.from( 81 | 'BwUgJHHQaynE+a4nZrYRzOllGSjjxuxNXxyNRUtI6Dlw/zlbt6PzOL8Onfqs6TcG', 82 | 'base64' 83 | ) 84 | }, 85 | { 86 | id: 'argon2i', 87 | params: {m: 120, t: 5000, p: 2, keyid: 'Hj5+dsK0'}, 88 | salt: Buffer.from('4fXXG0spB92WPB1NitT8/OH0VKI', 'base64') 89 | }, 90 | { 91 | id: 'argon2i', 92 | params: {m: 120, t: 5000, p: 2, data: 'sRlHhRmKUGzdOmXn01XmXygd5Kc'}, 93 | salt: Buffer.from('4fXXG0spB92WPB1NitT8/OH0VKI', 'base64') 94 | }, 95 | { 96 | id: 'argon2i', 97 | params: { 98 | m: 120, 99 | t: 5000, 100 | p: 2, 101 | keyid: 'Hj5+dsK0', 102 | data: 'sRlHhRmKUGzdOmXn01XmXygd5Kc' 103 | }, 104 | salt: Buffer.from('4fXXG0spB92WPB1NitT8/OH0VKI', 'base64') 105 | }, 106 | { 107 | id: 'argon2i', 108 | params: {m: 120, t: 5000, p: 2}, 109 | salt: Buffer.from('4fXXG0spB92WPB1NitT8/OH0VKI', 'base64'), 110 | hash: Buffer.from('iPBVuORECm5biUsjq33hn9/7BKqy9aPWKhFfK2haEsM', 'base64') 111 | }, 112 | { 113 | id: 'argon2i', 114 | params: {m: 120, t: 5000, p: 2, keyid: 'Hj5+dsK0'}, 115 | salt: Buffer.from('4fXXG0spB92WPB1NitT8/OH0VKI', 'base64'), 116 | hash: Buffer.from('iPBVuORECm5biUsjq33hn9/7BKqy9aPWKhFfK2haEsM', 'base64') 117 | }, 118 | { 119 | id: 'argon2i', 120 | params: {m: 120, t: 5000, p: 2, data: 'sRlHhRmKUGzdOmXn01XmXygd5Kc'}, 121 | salt: Buffer.from('4fXXG0spB92WPB1NitT8/OH0VKI', 'base64'), 122 | hash: Buffer.from('iPBVuORECm5biUsjq33hn9/7BKqy9aPWKhFfK2haEsM', 'base64') 123 | }, 124 | { 125 | id: 'argon2i', 126 | params: { 127 | m: 120, 128 | t: 5000, 129 | p: 2, 130 | keyid: 'Hj5+dsK0', 131 | data: 'sRlHhRmKUGzdOmXn01XmXygd5Kc' 132 | }, 133 | salt: Buffer.from('4fXXG0spB92WPB1NitT8/OH0VKI', 'base64'), 134 | hash: Buffer.from('iPBVuORECm5biUsjq33hn9/7BKqy9aPWKhFfK2haEsM', 'base64') 135 | }, 136 | { 137 | id: 'argon2i', 138 | params: { 139 | m: 120, 140 | t: 5000, 141 | p: 2, 142 | keyid: 'Hj5+dsK0', 143 | data: 'sRlHhRmKUGzdOmXn01XmXygd5Kc' 144 | }, 145 | salt: Buffer.from('iHSDPHzUhPzK7rCcJgOFfg', 'base64'), 146 | hash: Buffer.from('EkCWX6pSTqWruiR0', 'base64') 147 | }, 148 | { 149 | id: 'argon2i', 150 | params: { 151 | m: 120, 152 | t: 5000, 153 | p: 2, 154 | keyid: 'Hj5+dsK0', 155 | data: 'sRlHhRmKUGzdOmXn01XmXygd5Kc' 156 | }, 157 | salt: Buffer.from('iHSDPHzUhPzK7rCcJgOFfg', 'base64'), 158 | hash: Buffer.from( 159 | 'J4moa2MM0/6uf3HbY2Tf5Fux8JIBTwIhmhxGRbsY14qhTltQt+Vw3b7tcJNEbk8ium8AQfZeD4tabCnNqfkD1g', 160 | 'base64' 161 | ) 162 | }, 163 | { 164 | id: 'scrypt', 165 | params: { 166 | ln: 1, 167 | r: 16, 168 | p: 1 169 | }, 170 | salt: Buffer.from('', 'hex'), 171 | hash: Buffer.from( 172 | 'd9ZXYjhleyA7GcpCwYoEl/FrSETjB0ro39/6P+3iFEL80Aad7QlI+DJqdToPyB8X6NPg+y4NNijPNeIMONGJBg', 173 | 'base64' 174 | ) 175 | }, 176 | { 177 | id: 'argon2i', 178 | version: 19, 179 | params: { 180 | m: 120, 181 | t: 5000, 182 | p: 2, 183 | keyid: 'Hj5+dsK0', 184 | data: 'sRlHhRmKUGzdOmXn01XmXygd5Kc' 185 | }, 186 | salt: Buffer.from('iHSDPHzUhPzK7rCcJgOFfg', 'base64'), 187 | hash: Buffer.from( 188 | 'J4moa2MM0/6uf3HbY2Tf5Fux8JIBTwIhmhxGRbsY14qhTltQt+Vw3b7tcJNEbk8ium8AQfZeD4tabCnNqfkD1g', 189 | 'base64' 190 | ) 191 | } 192 | ]; 193 | module.exports = { 194 | serialized, 195 | deserialized 196 | }; 197 | -------------------------------------------------------------------------------- /test/fixtures/serialize-only.js: -------------------------------------------------------------------------------- 1 | const serialized = [ 2 | '$argon2i$v=19$m=120,t=5000,p=2,keyid=Hj5+dsK0,data=sRlHhRmKUGzdOmXn01XmXygd5Kc$iHSDPHzUhPzK7rCcJgOFfg$J4moa2MM0/6uf3HbY2Tf5Fux8JIBTwIhmhxGRbsY14qhTltQt+Vw3b7tcJNEbk8ium8AQfZeD4tabCnNqfkD1g' 3 | ]; 4 | 5 | const deserialized = [ 6 | { 7 | id: 'argon2i', 8 | version: 19, 9 | params: { 10 | m: 120, 11 | t: 5000, 12 | p: 2, 13 | keyid: 'Hj5+dsK0', 14 | data: Buffer.from('sRlHhRmKUGzdOmXn01XmXygd5Kc', 'base64') 15 | }, 16 | salt: Buffer.from('iHSDPHzUhPzK7rCcJgOFfg', 'base64'), 17 | hash: Buffer.from( 18 | 'J4moa2MM0/6uf3HbY2Tf5Fux8JIBTwIhmhxGRbsY14qhTltQt+Vw3b7tcJNEbk8ium8AQfZeD4tabCnNqfkD1g', 19 | 'base64' 20 | ) 21 | } 22 | ]; 23 | module.exports = { 24 | serialized, 25 | deserialized 26 | }; 27 | -------------------------------------------------------------------------------- /test/serialize.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | 3 | const m = require('..'); 4 | const sdData = require('./fixtures/serialize-deserialize'); 5 | const sData = require('./fixtures/serialize-only'); 6 | 7 | test('should serialize correct phc objects', t => { 8 | sdData.deserialized.forEach((g, i) => { 9 | t.deepEqual(m.serialize(sdData.deserialized[i]), sdData.serialized[i]); 10 | }); 11 | sData.deserialized.forEach((g, i) => { 12 | t.deepEqual(m.serialize(sData.deserialized[i]), sData.serialized[i]); 13 | }); 14 | }); 15 | 16 | test('should thow errors if trying to serialize with invalid arguments', async t => { 17 | let err = await t.throws(() => m.serialize(null)); 18 | t.is(err.message, 'opts must be an object'); 19 | 20 | err = await t.throws(() => m.serialize({})); 21 | t.is(err.message, 'id must be a string'); 22 | 23 | err = await t.throws(() => m.serialize({id: 'i_n_v_a_l_i_d'})); 24 | t.regex(err.message, /id must satisfy/); 25 | 26 | err = await t.throws(() => m.serialize({id: 'pbkdf2', params: null})); 27 | t.is(err.message, 'params must be an object'); 28 | 29 | err = await t.throws(() => m.serialize({id: 'pbkdf2', params: {i: {}}})); 30 | t.is(err.message, 'params values must be strings'); 31 | 32 | err = await t.throws(() => 33 | m.serialize({id: 'pbkdf2', params: {rounds_: '1000'}}) 34 | ); 35 | t.regex(err.message, /params names must satisfy/); 36 | 37 | err = await t.throws(() => 38 | m.serialize({id: 'pbkdf2', params: {rounds: '1000@'}}) 39 | ); 40 | t.regex(err.message, /params values must satisfy/); 41 | 42 | err = await t.throws(() => 43 | m.serialize({id: 'pbkdf2', params: {rounds: '1000'}, salt: 'string'}) 44 | ); 45 | t.is(err.message, 'salt must be a Buffer'); 46 | 47 | err = await t.throws(() => m.serialize({id: 'argon2id', version: -10})); 48 | t.is(err.message, 'version must be a positive integer number'); 49 | 50 | err = await t.throws(() => 51 | m.serialize({ 52 | id: 'pbkdf2', 53 | params: {rounds: '1000'}, 54 | salt: Buffer.from('string'), 55 | hash: 'string' 56 | }) 57 | ); 58 | t.is(err.message, 'hash must be a Buffer'); 59 | }); 60 | --------------------------------------------------------------------------------