├── .eslintrc.js ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── cli.js ├── crx2.js ├── crx3.js ├── crx3.js.pb ├── index.js └── resolver.js └── test ├── expectations ├── updateCRX2.xml ├── updateCRX3.xml └── updateProdVersionMin.xml ├── index.js ├── key.pem └── myFirstExtension ├── icon.png ├── key.pem └── manifest.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | es6: true, 6 | node: true 7 | }, 8 | extends: "eslint:recommended", 9 | parserOptions: { 10 | ecmaVersion: 2016 11 | }, 12 | rules: { 13 | indent: ["error", 2], 14 | "linebreak-style": ["error", "unix"], 15 | quotes: ["error", "double"], 16 | semi: ["error", "always"] 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Unit Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build-front: 9 | runs-on: ${{ matrix.os }} 10 | continue-on-error: ${{ matrix.allow-failure || false }} 11 | 12 | strategy: 13 | matrix: 14 | node-version: [8.x, 10.x, 14.x] 15 | os: [ubuntu-latest] 16 | include: 17 | - node-version: 8.x 18 | allow-failure: true 19 | - node-version: 14.x 20 | os: windows-latest 21 | allow-failure: true 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | 26 | - name: Use Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v2 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | 31 | - name: Install and run tests 32 | run: npm install-ci-test 33 | 34 | - name: Run CLI commands 35 | run: | 36 | npm install --global . 37 | 38 | crx keygen ./test --force 39 | crx pack ./test/myFirstExtension -p ./test/key.pem -o test.crx 40 | crx pack ./test/myFirstExtension --zip-output test.zip 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test/*.crx 2 | test/*.zip 3 | test/*.xml 4 | node_modules 5 | coverage 6 | .nyc_output 7 | tmp 8 | 9 | build.crx 10 | update.xml 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | .travis.yml -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Change Log 2 | 3 | ### 5.0.1 (2019/07/22 09:57 +00:00) 4 | - [#107](https://github.com/oncletom/crx/pull/107) fix: loading relative path (@ahwayakchih) 5 | 6 | ### v5.0.0 (2019/04/17 08:00 +00:00) 7 | - [#106](https://github.com/oncletom/crx/pull/106) chore: update CHANGELOG (@ahwayakchih) 8 | - [#105](https://github.com/oncletom/crx/pull/105) Update dependencies and prepare for new release (v5.0.0) (@ahwayakchih) 9 | - [#104](https://github.com/oncletom/crx/pull/104) fix: create private key file outside extension directory by default (@ahwayakchih) 10 | - [#103](https://github.com/oncletom/crx/pull/103) Move CLI script to src folder (@arkon) 11 | - [#100](https://github.com/oncletom/crx/pull/100) Move CRX2 logic to separate file (@arkon) 12 | - [#102](https://github.com/oncletom/crx/pull/102) feat: use `manifest.minimum_chrome_version` as XML's `prodversionmin` (@ahwayakchih) 13 | - [#99](https://github.com/oncletom/crx/pull/99) docs: add `--crx-format` to README (@ahwayakchih) 14 | - [#98](https://github.com/oncletom/crx/pull/98) feat: add support for CRXv3 (@ahwayakchih) 15 | - [#97](https://github.com/oncletom/crx/pull/97) chore: update dependencies (#97) (@ahwayakchih) 16 | - [#78](https://github.com/oncletom/crx/pull/78) Add generateAppId sample (#78) (@NN---) 17 | 18 | ### v4.0.1 (2019/02/03 16:17 +00:00) 19 | - [#96](https://github.com/oncletom/crx/pull/96) Remove deprecated crx.writeFile() (@oncletom) 20 | 21 | ### v4.0.0 (2019/02/03 15:57 +00:00) 22 | - [#95](https://github.com/oncletom/crx/pull/95) Release crx@4 (@oncletom) 23 | - [#93](https://github.com/oncletom/crx/pull/93) fix demo code (@g8up) 24 | - [#88](https://github.com/oncletom/crx/pull/88) Bump Node.js version requirement (@oncletom) 25 | - [#90](https://github.com/oncletom/crx/pull/90) Fix syntax in module usage (@blimmer) 26 | - [#83](https://github.com/oncletom/crx/pull/83) Update dependencies (@oncletom) 27 | - [#81](https://github.com/oncletom/crx/pull/81) Fix extension ID calculation from path (@oncletom, @conioh) 28 | - [#76](https://github.com/oncletom/crx/pull/76) Add Appveyor configuration to test build on Windows (@oncletom) 29 | - [#71](https://github.com/oncletom/crx/pull/71) Remove the manifest data from cache on crx.load() (@binhqx) 30 | - [#75](https://github.com/oncletom/crx/pull/75) [Snyk Update] New fixes for 1 vulnerable dependency path (@snyk-bot) 31 | 32 | ### v3.2.1 (2016/10/13 13:13 +00:00) 33 | - [#67](https://github.com/oncletom/crx/pull/67) Drop iojs from package.engines (@dsblv) 34 | 35 | ### v3.2.0 (2016/09/22 23:25 +00:00) 36 | - [#66](https://github.com/oncletom/crx/pull/66) Add the ability to load a list of files in addition to a path. (@oncletom) 37 | 38 | ### v3.1.0 (2016/09/22 14:06 +00:00) 39 | - [#62](https://github.com/oncletom/crx/pull/62) Remove intermediate copy and use of temporary files (@oncletom) 40 | - [#63](https://github.com/oncletom/crx/pull/63) Add code coverage and add additional tests (@oncletom) 41 | 42 | ### v3.0.4 (2016/09/21 13:00 +00:00) 43 | - [#58](https://github.com/oncletom/crx/pull/58) [security fix] Updated archiver dependency; drops support for node 0.8.x (@PavelVanecek) 44 | - [#56](https://github.com/oncletom/crx/pull/56) Update CLI documentation with -o instead of deprecated -f (@nhoizey) 45 | - [#52](https://github.com/oncletom/crx/pull/52) Generate 2048-bit keys at the keygen CLI (@ngyikp) 46 | - [#50](https://github.com/oncletom/crx/pull/50) test crx on node 4.1 and 5 (@joscha) 47 | 48 | ### v3.0.3 (2015/07/22 16:10 +00:00) 49 | - [#47](https://github.com/oncletom/crx/pull/47) added .npmignore file (@PavelVanecek) 50 | 51 | ### v3.0.2 (2015/02/06 11:28 +00:00) 52 | - [#39](https://github.com/oncletom/crx/pull/39) Do not concatenate a possible null buffer. (@oncletom) 53 | - [#37](https://github.com/oncletom/crx/pull/37) fix pack instruction in Module example (@qbarlas) 54 | 55 | ### v2.0.1 (2015/01/15 18:28 +00:00) 56 | - [#34](https://github.com/oncletom/crx/pull/34) Fix various issues with CLI and creating .pem and .crx files (@Batterii) 57 | - [#35](https://github.com/oncletom/crx/pull/35) destroy() is not used (@okuryu) 58 | - [#26](https://github.com/oncletom/crx/pull/26) fixes minor code problems, such as missing semicolons, etc. (@joscha) 59 | - [#29](https://github.com/oncletom/crx/pull/29) generatePublicKey promise and generateAppId state (@joscha) 60 | - [#30](https://github.com/oncletom/crx/pull/30) use temp module (@joscha) 61 | - [#27](https://github.com/oncletom/crx/pull/27) throw error if public key is not set, yet (@joscha) 62 | 63 | ### v2.0.0 (2014/11/29 16:39 +00:00) 64 | - [#24](https://github.com/oncletom/crx/pull/24) Unsigned archives (@oncletom) 65 | 66 | ### v1.1.0 (2014/11/29 15:55 +00:00) 67 | - [#25](https://github.com/oncletom/crx/pull/25) Pure JavaScript public key (@oncletom) 68 | 69 | ### v1.0.0 (2014/11/25 11:45 +00:00) 70 | - [#22](https://github.com/oncletom/crx/pull/22) Remove system ssh-keygen dependency (@oncletom) 71 | - [#20](https://github.com/oncletom/crx/pull/20) Promise-based interface (@oncletom) 72 | - [#19](https://github.com/oncletom/crx/pull/19) Event 'finished' no longer valid #18 (@yuryoparin) 73 | - [#17](https://github.com/oncletom/crx/pull/17) Fix deleting remaining ./tmp directory (@jokesterfr) 74 | - [#16](https://github.com/oncletom/crx/pull/16) removed bufferstream dependency (@christian-bromann) 75 | - [#14](https://github.com/oncletom/crx/pull/14) Support long private keys in latest Chrome 32 (@vitalets) 76 | - [#10](https://github.com/oncletom/crx/pull/10) Windows Compatibility (@adotout) 77 | - [#8](https://github.com/oncletom/crx/pull/8) Use proper stdin options in child_process.spawn() instead of passing a file child.stdin.end() (@nkakuev) 78 | - [#3](https://github.com/oncletom/crx/pull/3) Added support for maxBuffer (@oncletom) 79 | - [#2](https://github.com/oncletom/crx/pull/2) Updated the README example upon `crx` API (@oncletom) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Jed Schmidt, Thomas Parisot and contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # crx [![Build Status](https://secure.travis-ci.org/oncletom/crx.svg)](http://travis-ci.org/oncletom/crx) [![Build status](https://ci.appveyor.com/api/projects/status/i8v95qmgwwxic5wn?svg=true)](https://ci.appveyor.com/project/oncletom/crx) 2 | 3 | > crx is a utility to **package Google Chrome extensions** via a *Node API* and the *command line*. It is written **purely in JavaScript** and **does not require OpenSSL**! 4 | 5 | Packages are available to use `crx` with: 6 | 7 | - *grunt*: [grunt-crx](https://npmjs.com/grunt-crx) 8 | - *gulp*: [gulp-crx-pack](https://npmjs.com/gulp-crx-pack) 9 | - *webpack*: [crx-webpack-plugin](https://npmjs.com/crx-webpack-plugin) 10 | 11 | Massive hat tip to the [node-rsa project](https://npmjs.com/node-rsa) for the pure JavaScript encryption! 12 | 13 | **Compatibility**: this extension is compatible with `node>=10`. 14 | 15 | ## Install 16 | 17 | ```bash 18 | $ npm install crx 19 | ``` 20 | 21 | ## Module API 22 | 23 | Asynchronous functions returns a native ECMAScript Promise. 24 | 25 | ```js 26 | const fs = require('fs'); 27 | const path = require('path'); 28 | 29 | const ChromeExtension = require('crx'); 30 | 31 | const crx = new ChromeExtension({ 32 | codebase: 'http://localhost:8000/myExtension.crx', 33 | privateKey: fs.readFileSync('./key.pem') 34 | }); 35 | 36 | crx.load( path.resolve(__dirname, './myExtension') ) 37 | .then(crx => crx.pack()) 38 | .then(crxBuffer => { 39 | const updateXML = crx.generateUpdateXML() 40 | 41 | fs.writeFileSync('../update.xml', updateXML); 42 | fs.writeFileSync('../myExtension.crx', crxBuffer); 43 | }) 44 | .catch(err=>{ 45 | console.error( err ); 46 | }); 47 | ``` 48 | 49 | ### ChromeExtension = require("crx") 50 | ### crx = new ChromeExtension(attrs) 51 | 52 | This module exports the `ChromeExtension` constructor directly, which can take an optional attribute object, which is used to extend the instance. 53 | 54 | ### crx.load(path|files) 55 | 56 | Prepares the temporary workspace for the Chrome Extension located at `path` — which is expected to directly contain `manifest.json`. 57 | 58 | ```js 59 | crx.load('/path/to/extension').then(crx => { 60 | // ... 61 | }); 62 | ``` 63 | 64 | Alternatively, you can pass a list of files — the first `manifest.json` file to be found will be considered as the root of the application. 65 | 66 | ```js 67 | crx.load(['/my/extension/manifest.json', '/my/extension/background.json']).then(crx => { 68 | // ... 69 | }); 70 | ``` 71 | 72 | ### crx.pack() 73 | 74 | Packs the Chrome Extension and resolves the promise with a Buffer containing the `.crx` file. 75 | 76 | ```js 77 | crx.load('/path/to/extension') 78 | .then(crx => crx.pack()) 79 | .then(crxBuffer => { 80 | fs.writeFileSync('/tmp/foobar.crx', crxBuffer); 81 | }); 82 | ``` 83 | 84 | ### crx.generateUpdateXML() 85 | 86 | Returns a Buffer containing the update.xml file used for `autoupdate`, as specified for `update_url` in the manifest. In this case, the instance must have a property called `codebase`. 87 | 88 | ```js 89 | const crx = new ChromeExtension({ ..., codebase: 'https://autoupdateserver.com/myFirstExtension.crx' }); 90 | 91 | crx.load('/path/to/extension') 92 | .then(crx => crx.pack()) 93 | .then(crxBuffer => { 94 | // ... 95 | const xmlBuffer = crx.generateUpdateXML(); 96 | fs.writeFileSync('/foo/bar/update.xml', xmlBuffer); 97 | }); 98 | ``` 99 | 100 | ### crx.generateAppId 101 | 102 | Generates application id (extension id) from given path. 103 | 104 | ```js 105 | new crx().generateAppId('/path/to/ext') // epgkjnfaepceeghkjflpimappmlalchn 106 | ``` 107 | 108 | ## CLI API 109 | 110 | ### crx pack [directory] [--crx-version number] [-o file] [--zip-output file] [-p private-key] 111 | 112 | Pack the specified directory into a .crx package, and output it to stdout. If no directory is specified, the current working directory is used. 113 | 114 | Use the `--crx-version` option to specify which CRX format version to output. Can be either "2" or "3", defaults to "3". 115 | 116 | Use the `-o` option to write the signed extension to a file instead of stdout. 117 | 118 | Use the `--zip-output` option to write the unsigned extension to a file. 119 | 120 | Use the `-p` option to specify an external private key. If this is not used, `key.pem` is used from within the directory. If this option is not used and no `key.pem` file exists, one will be generated automatically. 121 | 122 | Use the `-b` option to specify the maximum buffer allowed to generate extension. By default, will rely on `node` internal setting (~200KB). 123 | 124 | ### crx keygen [directory] 125 | 126 | Generate a 2048-bit RSA private key within the directory. This is called automatically if a key is not specified, and `key.pem` does not exist. 127 | 128 | Use the `--force` option to overwrite an existing private key located in the same given folder. 129 | 130 | ### crx --help 131 | 132 | Show information about using this utility, generated by [commander](https://github.com/visionmedia/commander.js). 133 | 134 | ## CLI example 135 | 136 | Given the following directory structure: 137 | 138 | ``` 139 | └─┬ myFirstExtension 140 | ├── manifest.json 141 | └── icon.png 142 | ``` 143 | 144 | run this: 145 | 146 | ```bash 147 | $ cd myFirstExtension 148 | $ crx pack -o 149 | ``` 150 | 151 | to generate this: 152 | 153 | ```bash 154 | ├─┬ myFirstExtension 155 | │ ├── manifest.json 156 | │ ├── icon.png 157 | │ └── key.pem 158 | └── myFirstExtension.crx 159 | ``` 160 | 161 | You can also name the output file like this: 162 | 163 | ```bash 164 | $ cd myFirstExtension 165 | $ crx pack -o myFirstExtension.crx 166 | ``` 167 | 168 | to get the same results, or also pipe to the file manually like this. 169 | 170 | ```bash 171 | $ cd myFirstExtension 172 | $ crx pack > ../myFirstExtension.crx 173 | ``` 174 | 175 | As you can see a key is generated for you at `key.pem` if none exists. You can also specify an external key. So if you have this: 176 | 177 | ``` 178 | ├─┬ myFirstExtension 179 | │ ├── manifest.json 180 | │ └── icon.png 181 | └── myPrivateKey.pem 182 | ``` 183 | 184 | you can run this: 185 | 186 | ```bash 187 | $ crx pack myFirstExtension -p myPrivateKey.pem -o 188 | ``` 189 | 190 | to sign your package without keeping the key in the directory. 191 | 192 | # License 193 | 194 | [MIT License](LICENSE). 195 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Jed Schmidt (http://jed.is)", 3 | "name": "crx", 4 | "description": "crx is a utility to package Google Chrome extensions via a Node API and the command line", 5 | "version": "5.0.1", 6 | "license": "MIT", 7 | "homepage": "https://github.com/oncletom/crx", 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/oncletom/crx.git" 11 | }, 12 | "main": "./src/index.js", 13 | "bin": { 14 | "crx": "./src/cli.js" 15 | }, 16 | "engines": { 17 | "node": ">=10" 18 | }, 19 | "scripts": { 20 | "test": "nyc tape ./test/*.js", 21 | "posttest": "eslint src/*.js", 22 | "version": "npm run changelog && git add CHANGELOG.md", 23 | "changelog": "github-changes -o oncletom -r crx -n ${npm_package_version} --only-pulls --use-commit-body" 24 | }, 25 | "nyc": { 26 | "functions": 100, 27 | "statements": 100, 28 | "branches": 100, 29 | "check-coverage": true, 30 | "reporter": [ 31 | "text", 32 | "html" 33 | ] 34 | }, 35 | "dependencies": { 36 | "archiver": "^4.0.2", 37 | "commander": "^2.20.0", 38 | "node-rsa": "^1.0.5", 39 | "pbf": "^3.2.0" 40 | }, 41 | "devDependencies": { 42 | "adm-zip": "^0.4.13", 43 | "eslint": "^5.16.0", 44 | "github-changes": "^2.0.0", 45 | "nyc": "^14.1.1", 46 | "tape": "^4.11.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require("path"); 4 | var fs = require("fs"); 5 | var rsa = require("node-rsa"); 6 | var {promisify} = require("util"); 7 | var writeFile = promisify(fs.writeFile); 8 | var readFile = promisify(fs.readFile); 9 | 10 | var program = require("commander"); 11 | var ChromeExtension = require("."); 12 | var pkg = require("../package.json"); 13 | 14 | var resolve = path.resolve; 15 | var join = path.join; 16 | 17 | var cwd = process.cwd(); 18 | 19 | program.version(pkg.version); 20 | // coming soon 21 | // .option("-x, --xml", "output autoupdate xml instead of extension ") 22 | 23 | program 24 | .command("keygen [directory]") 25 | .option("--force", "overwrite the private key if it exists") 26 | .option( 27 | "-c, --crx-version [number]", 28 | "CRX format version, can be either 2 or 3, defaults to 3", 29 | parseInt 30 | ) 31 | .description("generate a private key in [directory]/key.pem") 32 | .action(keygen); 33 | 34 | program 35 | .command("pack [directory]") 36 | .description("pack [directory] into a .crx extension") 37 | .option( 38 | "-o, --output ", 39 | "write the crx content to instead of stdout" 40 | ) 41 | .option("--zip-output ", "write the zip content to ") 42 | .option( 43 | "-p, --private-key ", 44 | "relative path to private key [key.pem], defaults to [directory/../key.pem]") 45 | .option( 46 | "-b, --max-buffer ", 47 | "max amount of memory allowed to generate the crx, in byte" 48 | ) 49 | .option( 50 | "-c, --crx-version [number]", 51 | "CRX format version, can be either 2 or 3, defaults to 3", 52 | parseInt 53 | ) 54 | .action(pack); 55 | 56 | program.parse(process.argv); 57 | 58 | 59 | /** 60 | * Generate a new key file 61 | * @param {String} keyPath path of the key file to create 62 | * @param {Object} opts 63 | * @returns {Promise} 64 | */ 65 | function generateKeyFile(keyPath, opts) { 66 | // Chromium (tested on 72.0.3626.109) which generates CRX v3 files requires pkcs8 key 67 | var pkcs = "pkcs" + (opts.crxVersion === 2 ? "1" : "8") + "-private-pem"; 68 | 69 | return Promise.resolve(new rsa({ b: 2048 })) 70 | .then(key => key.exportKey(pkcs)) 71 | .then(keyVal => writeFile(keyPath, keyVal)) 72 | ; 73 | } 74 | 75 | function keygen(dir, program) { 76 | dir = dir ? resolve(cwd, dir) : cwd; 77 | 78 | var keyPath = join(dir, "key.pem"); 79 | 80 | fs.exists(keyPath, function(exists) { 81 | if (exists && !program.force) { 82 | throw new Error("key.pem already exists in the given location."); 83 | } 84 | 85 | generateKeyFile(keyPath, program); 86 | }); 87 | } 88 | 89 | function pack(dir, program) { 90 | var input = dir ? resolve(cwd, dir) : cwd; 91 | var keyPath = program.privateKey 92 | ? resolve(cwd, program.privateKey) 93 | : join(input, "..", "key.pem"); 94 | var output; 95 | 96 | if (program.output) { 97 | if (path.extname(program.output) !== ".crx") { 98 | throw new Error( 99 | "-o file is expected to have a `.crx` suffix: [" + 100 | program.output + 101 | "] was given." 102 | ); 103 | } 104 | } 105 | 106 | if (program.zipOutput) { 107 | if (path.extname(program.zipOutput) !== ".zip") { 108 | throw new Error( 109 | "--zip-output file is expected to have a `.zip` suffix: [" + 110 | program.zipOutput + 111 | "] was given." 112 | ); 113 | } 114 | } 115 | 116 | var crx = new ChromeExtension({ 117 | rootDirectory: input, 118 | maxBuffer: program.maxBuffer, 119 | version: program.crxVersion || 3 120 | }); 121 | 122 | readFile(keyPath) 123 | .then(null, function(err) { 124 | // If the key file doesn't exist, create one 125 | if (err.code === "ENOENT") { 126 | return generateKeyFile(keyPath, program).then(() => { 127 | process.stderr.write("Created new private key at: " + keyPath + ".\n"); 128 | return readFile(keyPath); 129 | }); 130 | } else { 131 | throw err; 132 | } 133 | }) 134 | .then(function(key) { 135 | crx.privateKey = key; 136 | }) 137 | .then(function() { 138 | crx 139 | .load() 140 | .then(() => crx.loadContents()) 141 | .then(function(fileBuffer) { 142 | if (program.zipOutput) { 143 | var outFile = resolve(cwd, program.zipOutput); 144 | 145 | fs.createWriteStream(outFile).end(fileBuffer); 146 | } 147 | else { 148 | return crx.pack(fileBuffer); 149 | } 150 | }) 151 | .then(function(crxBuffer) { 152 | if (program.zipOutput) { 153 | return; 154 | } 155 | else if (program.output) { 156 | output = program.output; 157 | } 158 | else { 159 | output = path.basename(cwd) + ".crx"; 160 | } 161 | 162 | var outFile = resolve(cwd, output); 163 | if (outFile) { 164 | fs.createWriteStream(outFile).end(crxBuffer); 165 | } 166 | else { 167 | process.stdout.end(crxBuffer); 168 | } 169 | }); 170 | }); 171 | } 172 | 173 | module.exports = program; 174 | -------------------------------------------------------------------------------- /src/crx2.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var crypto = require("crypto"); 4 | 5 | /** 6 | * Generates and returns a signed package from extension content. 7 | * 8 | * BC BREAK `this.package` is not stored anymore (since 1.0.0) 9 | * 10 | * @param {Buffer} signature 11 | * @param {Buffer} publicKey 12 | * @param {Buffer} contents 13 | * @returns {Buffer} 14 | */ 15 | module.exports = function generatePackage (privateKey, publicKey, contents) { 16 | var signature = generateSignature(privateKey, contents); 17 | 18 | var keyLength = publicKey.length; 19 | var sigLength = signature.length; 20 | var zipLength = contents.length; 21 | var length = 16 + keyLength + sigLength + zipLength; 22 | 23 | var crx = Buffer.alloc(length); 24 | 25 | crx.write("Cr24" + new Array(13).join("\x00"), "binary"); 26 | 27 | crx[4] = 2; 28 | crx.writeUInt32LE(keyLength, 8); 29 | crx.writeUInt32LE(sigLength, 12); 30 | 31 | publicKey.copy(crx, 16); 32 | signature.copy(crx, 16 + keyLength); 33 | contents.copy(crx, 16 + keyLength + sigLength); 34 | 35 | return crx; 36 | }; 37 | 38 | /** 39 | * Generates a SHA1 package signature. 40 | * 41 | * BC BREAK `this.signature` is not stored anymore (since 1.0.0) 42 | * 43 | * @param {Buffer} privateKey 44 | * @param {Buffer} contents 45 | * @returns {Buffer} 46 | */ 47 | function generateSignature (privateKey, contents) { 48 | return Buffer.from( 49 | crypto 50 | .createSign("sha1") 51 | .update(contents) 52 | .sign(privateKey), 53 | "binary" 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/crx3.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var PBf = require("pbf"); 4 | var crypto = require("crypto"); 5 | var crx = require("./crx3.js.pb"); 6 | 7 | /** 8 | * Generates and returns a signed package from extension content. 9 | * 10 | * Based on `crx_creator` from Chromium project. 11 | * 12 | * @see {@link https://github.com/chromium/chromium/blob/master/components/crx_file/crx_creator.cc} 13 | * @param {Buffer} privateKey 14 | * @param {Buffer} publicKey 15 | * @param {Buffer} contents 16 | * @returns {Buffer} 17 | */ 18 | module.exports = function generatePackage (privateKey, publicKey, contents) { 19 | var pb; 20 | 21 | pb = new PBf(); 22 | crx.SignedData.write({ 23 | crx_id: getCrxId(publicKey) 24 | }, pb); 25 | var signedHeaderData = pb.finish(); 26 | 27 | pb = new PBf(); 28 | crx.CrxFileHeader.write({ 29 | sha256_with_rsa: [{ 30 | public_key: publicKey, 31 | signature : generateSignature(privateKey, signedHeaderData, contents) 32 | }], 33 | signed_header_data: signedHeaderData 34 | }, pb); 35 | var header = Buffer.from(pb.finish()); 36 | 37 | var size = 38 | kSignature.length + // Magic constant 39 | kVersion.length + // Version number 40 | SIZE_BYTES + // Header size 41 | header.length + 42 | contents.length; 43 | 44 | var result = Buffer.allocUnsafe(size); 45 | 46 | var index = 0; 47 | kSignature.copy(result, index); 48 | kVersion.copy(result, index += kSignature.length); 49 | result.writeUInt32LE(header.length, index += kVersion.length); 50 | header.copy(result, index += SIZE_BYTES); 51 | contents.copy(result, index += header.length); 52 | 53 | return result; 54 | }; 55 | 56 | /** 57 | * CRX IDs are 16 bytes long 58 | * @constant 59 | */ 60 | const CRX_ID_SIZE = 16; 61 | 62 | /** 63 | * CRX3 uses 32bit numbers in various places, 64 | * so let's prepare size constant for that. 65 | * @constant 66 | */ 67 | const SIZE_BYTES = 4; 68 | 69 | /** 70 | * Used for file format. 71 | * @see {@link https://github.com/chromium/chromium/blob/master/components/crx_file/crx3.proto} 72 | * @constant 73 | */ 74 | const kSignature = Buffer.from("Cr24", "utf8"); 75 | 76 | /** 77 | * Used for file format. 78 | * @see {@link https://github.com/chromium/chromium/blob/master/components/crx_file/crx3.proto} 79 | * @constant 80 | */ 81 | const kVersion = Buffer.from([3, 0, 0, 0]); 82 | 83 | /** 84 | * Used for generating package signatures. 85 | * @see {@link https://github.com/chromium/chromium/blob/master/components/crx_file/crx3.proto} 86 | * @constant 87 | */ 88 | const kSignatureContext = Buffer.from("CRX3 SignedData\x00", "utf8"); 89 | 90 | /** 91 | * Given public key data, returns CRX ID. 92 | * 93 | * @param {Buffer} publicKey 94 | * @returns {Buffer} 95 | */ 96 | function getCrxId (publicKey) { 97 | var hash = crypto.createHash("sha256"); 98 | hash.update(publicKey); 99 | return hash.digest().slice(0, CRX_ID_SIZE); 100 | } 101 | 102 | /** 103 | * Generates and returns a signature. 104 | * 105 | * @param {Buffer} privateKey 106 | * @param {Buffer} signedHeaderData 107 | * @param {Buffer} contents 108 | * @returns {Buffer} 109 | */ 110 | function generateSignature (privateKey, signedHeaderData, contents) { 111 | var hash = crypto.createSign("sha256"); 112 | 113 | // Magic constant 114 | hash.update(kSignatureContext); 115 | 116 | // Size of signed_header_data 117 | var sizeOctets = Buffer.allocUnsafe(SIZE_BYTES); 118 | sizeOctets.writeUInt32LE(signedHeaderData.length, 0); 119 | hash.update(sizeOctets); 120 | 121 | // Content of signed_header_data 122 | hash.update(signedHeaderData); 123 | 124 | // ZIP content 125 | hash.update(contents); 126 | 127 | return Buffer.from(hash.sign(privateKey), "binary"); 128 | } 129 | -------------------------------------------------------------------------------- /src/crx3.js.pb: -------------------------------------------------------------------------------- 1 | 'use strict'; // code generated by pbf v3.1.0 from https://github.com/chromium/chromium/blob/e4a3bada6aab7aed90460ec7d27f8c7167c5666e/components/crx_file/crx3.proto 2 | 3 | // CrxFileHeader ======================================== 4 | 5 | var CrxFileHeader = exports.CrxFileHeader = {}; 6 | 7 | CrxFileHeader.read = function (pbf, end) { 8 | return pbf.readFields(CrxFileHeader._readField, {sha256_with_rsa: [], sha256_with_ecdsa: [], signed_header_data: null}, end); 9 | }; 10 | CrxFileHeader._readField = function (tag, obj, pbf) { 11 | if (tag === 2) obj.sha256_with_rsa.push(AsymmetricKeyProof.read(pbf, pbf.readVarint() + pbf.pos)); 12 | else if (tag === 3) obj.sha256_with_ecdsa.push(AsymmetricKeyProof.read(pbf, pbf.readVarint() + pbf.pos)); 13 | else if (tag === 10000) obj.signed_header_data = pbf.readBytes(); 14 | }; 15 | CrxFileHeader.write = function (obj, pbf) { 16 | if (obj.sha256_with_rsa) for (var i = 0; i < obj.sha256_with_rsa.length; i++) pbf.writeMessage(2, AsymmetricKeyProof.write, obj.sha256_with_rsa[i]); 17 | if (obj.sha256_with_ecdsa) for (i = 0; i < obj.sha256_with_ecdsa.length; i++) pbf.writeMessage(3, AsymmetricKeyProof.write, obj.sha256_with_ecdsa[i]); 18 | if (obj.signed_header_data) pbf.writeBytesField(10000, obj.signed_header_data); 19 | }; 20 | 21 | // AsymmetricKeyProof ======================================== 22 | 23 | var AsymmetricKeyProof = exports.AsymmetricKeyProof = {}; 24 | 25 | AsymmetricKeyProof.read = function (pbf, end) { 26 | return pbf.readFields(AsymmetricKeyProof._readField, {public_key: null, signature: null}, end); 27 | }; 28 | AsymmetricKeyProof._readField = function (tag, obj, pbf) { 29 | if (tag === 1) obj.public_key = pbf.readBytes(); 30 | else if (tag === 2) obj.signature = pbf.readBytes(); 31 | }; 32 | AsymmetricKeyProof.write = function (obj, pbf) { 33 | if (obj.public_key) pbf.writeBytesField(1, obj.public_key); 34 | if (obj.signature) pbf.writeBytesField(2, obj.signature); 35 | }; 36 | 37 | // SignedData ======================================== 38 | 39 | var SignedData = exports.SignedData = {}; 40 | 41 | SignedData.read = function (pbf, end) { 42 | return pbf.readFields(SignedData._readField, {crx_id: null}, end); 43 | }; 44 | SignedData._readField = function (tag, obj, pbf) { 45 | if (tag === 1) obj.crx_id = pbf.readBytes(); 46 | }; 47 | SignedData.write = function (obj, pbf) { 48 | if (obj.crx_id) pbf.writeBytesField(1, obj.crx_id); 49 | }; 50 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var path = require("path"); 4 | var join = path.join; 5 | var crypto = require("crypto"); 6 | var RSA = require("node-rsa"); 7 | var archiver = require("archiver"); 8 | var resolve = require("./resolver.js"); 9 | var crx2 = require("./crx2.js"); 10 | var crx3 = require("./crx3.js"); 11 | 12 | const DEFAULTS = { 13 | appId: null, 14 | rootDirectory: "", 15 | publicKey: null, 16 | privateKey: null, 17 | codebase: null, 18 | path: null, 19 | src: "**", 20 | version: 3, 21 | }; 22 | 23 | class ChromeExtension { 24 | constructor(attrs) { 25 | // Setup defaults 26 | Object.assign(this, DEFAULTS, attrs); 27 | 28 | this.loaded = false; 29 | } 30 | 31 | /** 32 | * Packs the content of the extension in a crx file. 33 | * 34 | * @param {Buffer=} contentsBuffer 35 | * @returns {Promise} 36 | * @example 37 | * 38 | * crx.pack().then(function(crxContent){ 39 | * // do something with the crxContent binary data 40 | * }); 41 | * 42 | */ 43 | pack (contentsBuffer) { 44 | if (!this.loaded) { 45 | return this.load().then(this.pack.bind(this, contentsBuffer)); 46 | } 47 | 48 | var selfie = this; 49 | var packP = [ 50 | this.generatePublicKey(), 51 | contentsBuffer || selfie.loadContents() 52 | ]; 53 | 54 | return Promise.all(packP).then(function(outputs) { 55 | var publicKey = outputs[0]; 56 | var contents = outputs[1]; 57 | 58 | selfie.publicKey = publicKey; 59 | 60 | if (selfie.version === 2) { 61 | return crx2(selfie.privateKey, publicKey, contents); 62 | } 63 | 64 | return crx3(selfie.privateKey, publicKey, contents); 65 | }); 66 | } 67 | 68 | /** 69 | * Loads extension manifest and copies its content to a workable path. 70 | * 71 | * @param {string=} path 72 | * @returns {Promise} 73 | */ 74 | load (path) { 75 | var selfie = this; 76 | 77 | return resolve(path || selfie.rootDirectory).then(function(metadata) { 78 | selfie.path = metadata.path; 79 | selfie.src = metadata.src; 80 | 81 | var manifestPath = join(selfie.path, "manifest.json"); 82 | delete require.cache[manifestPath]; 83 | 84 | selfie.manifest = require(manifestPath); 85 | selfie.loaded = true; 86 | 87 | return selfie; 88 | }); 89 | } 90 | 91 | /** 92 | * Generates a public key. 93 | * 94 | * BC BREAK `this.publicKey` is not stored anymore (since 1.0.0) 95 | * BC BREAK callback parameter has been removed in favor to the promise interface. 96 | * 97 | * @returns {Promise} Resolves to {Buffer} containing the public key 98 | * @example 99 | * 100 | * crx.generatePublicKey(function(publicKey){ 101 | * // do something with publicKey 102 | * }); 103 | */ 104 | generatePublicKey () { 105 | var privateKey = this.privateKey; 106 | 107 | return new Promise(function(resolve, reject) { 108 | if (!privateKey) { 109 | return reject( 110 | "Impossible to generate a public key: privateKey option has not been defined or is empty." 111 | ); 112 | } 113 | 114 | var key = new RSA(privateKey); 115 | 116 | resolve(key.exportKey("pkcs8-public-der")); 117 | }); 118 | } 119 | 120 | /** 121 | * 122 | * BC BREAK `this.contents` is not stored anymore (since 1.0.0) 123 | * 124 | * @returns {Promise} 125 | */ 126 | loadContents () { 127 | var selfie = this; 128 | 129 | return new Promise(function(resolve, reject) { 130 | var archive = archiver("zip", { zlib: { level: 9 }}); 131 | var contents = Buffer.from(""); 132 | 133 | if (!selfie.loaded) { 134 | throw new Error( 135 | "crx.load needs to be called first in order to prepare the workspace." 136 | ); 137 | } 138 | 139 | archive.on("error", reject); 140 | 141 | /* 142 | TODO: Remove in v4. 143 | It will be better to resolve an archive object 144 | rather than fitting everything in memory. 145 | 146 | @see https://github.com/oncletom/crx/issues/61 147 | */ 148 | archive.on("data", function(buf) { 149 | contents = Buffer.concat([contents, buf]); 150 | }); 151 | 152 | archive.on("finish", function() { 153 | resolve(contents); 154 | }); 155 | 156 | archive 157 | .glob(selfie.src, { 158 | cwd: selfie.path, 159 | matchBase: true, 160 | ignore: ["*.pem", ".git", "*.crx"] 161 | }) 162 | .finalize(); 163 | }); 164 | } 165 | 166 | /** 167 | * Generates an appId from the publicKey. 168 | * Public key has to be set for this to work, otherwise an error is thrown. 169 | * 170 | * BC BREAK `this.appId` is not stored anymore (since 1.0.0) 171 | * BC BREAK introduced `publicKey` parameter as it is not stored any more since 2.0.0 172 | * 173 | * @param {Buffer|string} [publicKey] the public key to use to generate the app ID 174 | * @returns {string} 175 | */ 176 | generateAppId (keyOrPath) { 177 | keyOrPath = keyOrPath || this.publicKey; 178 | 179 | if (typeof keyOrPath !== "string" && !(keyOrPath instanceof Buffer)) { 180 | throw new Error("Public key is neither set, nor given"); 181 | } 182 | 183 | // Handling Windows Path 184 | // Possibly to be moved in a different method 185 | if (typeof keyOrPath === "string") { 186 | var charCode = keyOrPath.charCodeAt(0); 187 | 188 | // 65 (A) < charCode < 122 (z) 189 | if (charCode >= 65 && charCode <= 122 && keyOrPath[1] === ":") { 190 | keyOrPath = keyOrPath[0].toUpperCase() + keyOrPath.slice(1); 191 | 192 | keyOrPath = Buffer.from(keyOrPath, "utf-16le"); 193 | } 194 | } 195 | 196 | return crypto 197 | .createHash("sha256") 198 | .update(keyOrPath) 199 | .digest() 200 | .toString("hex") 201 | .split("") 202 | .map(x => (parseInt(x, 16) + 0x0a).toString(26)) 203 | .join("") 204 | .slice(0, 32); 205 | } 206 | 207 | /** 208 | * Generates an updateXML file from the extension content. 209 | * 210 | * If manifest does not include `minimum_chrome_version`, defaults to: 211 | * - '29.0.0' for CRX2, which is earliest extensions API available 212 | * - '64.0.3242' for CRX3, which is when Chrome etension packager switched to CRX3 213 | * 214 | * BC BREAK `this.updateXML` is not stored anymore (since 1.0.0) 215 | * 216 | * @see 217 | * [Chrome Extensions APIs]{@link https://developer.chrome.com/extensions/api_index} 218 | * @see 219 | * [Chrome verions]{@link https://en.wikipedia.org/wiki/Google_Chrome_version_history} 220 | * @see 221 | * [Chromium switches to CRX3]{@link https://chromium.googlesource.com/chromium/src.git/+/b8bc9f99ef4ad6223dfdcafd924051561c05ac75} 222 | * @returns {Buffer} 223 | */ 224 | generateUpdateXML () { 225 | if (!this.codebase) { 226 | throw new Error("No URL provided for update.xml."); 227 | } 228 | 229 | var browserVersion = this.manifest.minimum_chrome_version 230 | || (this.version < 3 && "29.0.0") // Earliest version with extensions API 231 | || "64.0.3242"; // Chrome started generating CRX3 packages 232 | 233 | return Buffer.from(` 234 | 235 | 236 | 237 | 238 | `); 239 | } 240 | } 241 | 242 | module.exports = ChromeExtension; 243 | -------------------------------------------------------------------------------- /src/resolver.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var path = require("path"); 4 | 5 | module.exports = function resolve(pathOrFiles) { 6 | return new Promise(function(resolve, reject) { 7 | // legacy and original mode 8 | if (typeof pathOrFiles === "string") { 9 | return resolve({ 10 | path: path.resolve(pathOrFiles), 11 | src: "**" 12 | }); 13 | } 14 | 15 | // new mode, with a list of files 16 | else if (Array.isArray(pathOrFiles)) { 17 | var manifestFile = ""; 18 | 19 | pathOrFiles.some(function(f) { 20 | if (/(^|\/)manifest.json$/.test(f)) { 21 | manifestFile = f; 22 | return true; 23 | } 24 | }); 25 | 26 | if (!manifestFile) { 27 | return reject( 28 | new Error("Unable to locate a manifest file in your list of files.") 29 | ); 30 | } 31 | 32 | var manifestDir = path.dirname(manifestFile); 33 | 34 | return resolve({ 35 | path: path.resolve(manifestDir), 36 | src: 37 | "{" + 38 | pathOrFiles 39 | .map(function(f) { 40 | return path.relative(manifestDir, f); 41 | }) 42 | .join(",") + 43 | "}" 44 | }); 45 | } 46 | 47 | // 48 | else { 49 | reject( 50 | new Error( 51 | "load path is none of a folder location nor a list of files to pack" 52 | ) 53 | ); 54 | } 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /test/expectations/updateCRX2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/expectations/updateCRX3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/expectations/updateProdVersionMin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* global require, __dirname, Buffer */ 2 | 'use strict'; 3 | 4 | var fs = require("fs"); 5 | var test = require("tape"); 6 | var Zip = require("adm-zip"); 7 | var ChromeExtension = require("../"); 8 | var join = require("path").join; 9 | var privateKey = fs.readFileSync(join(__dirname, "key.pem")); 10 | var updateXml2 = fs.readFileSync(join(__dirname, "expectations", "updateCRX2.xml")); 11 | var updateXml3 = fs.readFileSync(join(__dirname, "expectations", "updateCRX3.xml")); 12 | var updateXmlCustom = fs.readFileSync(join(__dirname, "expectations", "updateProdVersionMin.xml")); 13 | 14 | function newCrx(opts){ 15 | return new ChromeExtension(Object.assign({ 16 | privateKey: privateKey, 17 | path: '/tmp', 18 | codebase: "http://localhost:8000/myFirstExtension.crx", 19 | rootDirectory: join(__dirname, "myFirstExtension") 20 | }, opts)); 21 | } 22 | 23 | const TESTS = {}; 24 | 25 | TESTS.ChromeExtension = function(t, opts){ 26 | t.plan(2); 27 | 28 | t.throws(() => ChromeExtension({})); 29 | t.ok(newCrx(opts)); 30 | }; 31 | 32 | 33 | TESTS.load = function(t, opts){ 34 | t.plan(6); 35 | 36 | newCrx(opts).load().then(t.pass); 37 | 38 | // Test relative path 39 | newCrx().load("./test/myFirstExtension").then(function(crx){ 40 | t.ok(crx); 41 | }).catch(t.error.bind(t)); 42 | 43 | // Test absolute path 44 | newCrx().load(join(__dirname, "myFirstExtension")).then(function(crx){ 45 | t.ok(crx); 46 | }).catch(t.error.bind(t)); 47 | 48 | // Test list of files 49 | var fileList = [ 50 | 'test/myFirstExtension/manifest.json', 51 | 'test/myFirstExtension/icon.png', 52 | ]; 53 | 54 | newCrx(opts).load(fileList).then(function(crx){ 55 | t.ok(crx); 56 | }); 57 | 58 | var fileList = [ 59 | 'test/myFirstExtension/icon.png' 60 | ]; 61 | 62 | newCrx(opts).load(fileList).catch(function(err){ 63 | t.ok(err); 64 | }); 65 | 66 | newCrx(opts).load(Buffer.from('')).catch(function(err){ 67 | t.ok(err); 68 | }) 69 | }; 70 | 71 | TESTS.pack = function(t, opts){ 72 | t.plan(1); 73 | 74 | var crx = newCrx(opts); 75 | crx.pack().then(function(packageData){ 76 | t.ok(packageData instanceof Buffer); 77 | }) 78 | .catch(t.error.bind(t)); 79 | }; 80 | 81 | TESTS.writeFile = function(t, opts){ 82 | t.plan(1); 83 | 84 | var crx = newCrx(opts); 85 | 86 | t.throws(() => crx.writeFile('/tmp/crx')); 87 | }; 88 | 89 | TESTS.loadContents = function(t, opts){ 90 | t.plan(3); 91 | 92 | newCrx(opts).loadContents().catch(function(err){ 93 | t.ok(err instanceof Error); 94 | }); 95 | 96 | var crx = newCrx(opts); 97 | 98 | crx.load().then(function(){ 99 | return crx.loadContents(); 100 | }) 101 | .then(function(contentsBuffer){ 102 | t.ok(contentsBuffer instanceof Buffer); 103 | 104 | return contentsBuffer; 105 | }) 106 | .then(function(packageData){ 107 | var entries = new Zip(packageData) 108 | .getEntries() 109 | .map(function(entry){ 110 | return entry.entryName; 111 | }) 112 | .sort(function(a, b){ 113 | return a.localeCompare(b); 114 | }); 115 | 116 | t.deepEqual(entries, ['icon.png', 'manifest.json']); 117 | 118 | return packageData; 119 | }) 120 | .catch(t.error.bind(t)); 121 | }; 122 | 123 | 124 | TESTS.generateUpdateXML = function(t, opts){ 125 | t.plan(3); 126 | 127 | t.throws(() => new ChromeExtension({}).generateUpdateXML(), 'No URL provided for update.xml'); 128 | 129 | var crx = newCrx(opts); 130 | var expected = crx.version === 2 ? updateXml2 : updateXml3; 131 | 132 | crx.pack().then(function(){ 133 | var xmlBuffer = crx.generateUpdateXML(); 134 | 135 | t.equals(xmlBuffer.toString(), expected.toString()); 136 | }) 137 | .catch(t.error.bind(t)); 138 | 139 | var crxCustom = newCrx(opts); 140 | crxCustom.load().then(() => { 141 | crxCustom.manifest.minimum_chrome_version = '99.99.99-crxtest'; 142 | crxCustom.pack().then(function(){ 143 | var xmlBuffer = crxCustom.generateUpdateXML(); 144 | 145 | t.equals(xmlBuffer.toString(), updateXmlCustom.toString()); 146 | }) 147 | .catch(t.error.bind(t)); 148 | }); 149 | }; 150 | 151 | TESTS.generatePublicKey = function(t, opts) { 152 | t.plan(2); 153 | 154 | var crx = newCrx(opts); 155 | crx.privateKey = null; 156 | 157 | crx.generatePublicKey().catch(function(err){ 158 | t.ok(err); 159 | }); 160 | 161 | newCrx(opts).generatePublicKey().then(function(publicKey){ 162 | t.equals(publicKey.length, 162); 163 | }); 164 | }; 165 | 166 | TESTS.generateAppId = function(t, opts) { 167 | t.plan(4); 168 | 169 | t.throws(function() { newCrx(opts).generateAppId(); }, /Public key is neither set, nor given/); 170 | 171 | var crx = newCrx(opts) 172 | 173 | // from Public Key 174 | crx.generatePublicKey().then(function(publicKey){ 175 | t.equals(crx.generateAppId(publicKey), 'eoilidhiokfphdhpmhoaengdkehanjif'); 176 | }) 177 | .catch(t.error.bind(t)); 178 | 179 | // from Linux Path 180 | t.equals(crx.generateAppId('/usr/local/extension'), 'ioglhmppkolgcgoonkfdbjkcedfjhbcd'); 181 | 182 | // from Windows Path 183 | t.equals(crx.generateAppId('c:\\a'), 'igchicfaapedlfgmepccnpolhajaphik'); 184 | }; 185 | 186 | TESTS["end to end"] = function (t, opts) { 187 | var crx = newCrx(opts); 188 | 189 | crx.load() 190 | .then(function(crx) { 191 | return crx.pack(); 192 | }) 193 | .then(function(crxBuffer) { 194 | fs.writeFile('build.crx', crxBuffer, t.error); 195 | fs.writeFile('update.xml', crx.generateUpdateXML(), t.error); 196 | }) 197 | .then(t.end); 198 | }; 199 | 200 | // Setup list of different configurations to test 201 | // Each key is the test name prefix. 202 | // Each value is an options obect to be passed to test implementation. 203 | const TEST_OPTIONS = { 204 | "": undefined, // use defaults 205 | v2: {version: 2}, 206 | v3: {version: 3} 207 | }; 208 | 209 | // Run whole list of tests for each of the configurations 210 | Reflect.ownKeys(TEST_OPTIONS).forEach(key => { 211 | for (const name in TESTS) { 212 | test(`${key}: ${name}`, t => TESTS[name](t, TEST_OPTIONS[key])); 213 | } 214 | }); 215 | -------------------------------------------------------------------------------- /test/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICWwIBAAKBgQCx1nHfHr1yCt21hPRWE9yKwC8Xuq+y/dPDrkjJ8cVsgPRoq25i 3 | iej2siZEWryDR7WWuAmdGaUtBFgQyRvCyGQS4YtkKot8iOTFzJo656hgHJUvZP2Q 4 | Yy7ERJ3rZRwLxpWmvYQiXx92LSy19eC6Bi5+FAaTMCQNDdklanijb5D6fwIDAQAB 5 | AoGAUQmPSkUPvvAEp7q2PKNAVFnPG9kOR1ozLXA16xApDpCUzz2PR4fgiMoVdgCC 6 | 9q+up8elWdld022vU7bQ16nJL64GFzgCHubRBM7d5UJjSaVkcDL6QbzAEIwwa1IU 7 | LQA7BheshHs3d+gYRjedNGR0kKu3HeUofhhXLC1WXPCOZYECQQDrKqFPro6jRYjD 8 | T65qvoIetRKw1L8J9iw6UhWb1jMtCgvTKS99zmnJzM2hOsCAy+HDj/YwlCWEKIjh 9 | ZICZBFkpAkEAwZellq94wisJd4ptfQvHSWVPdMXR59jQcW23dBqU4D+itc1wCVT/ 10 | xg5iIBaujTHgaM9oS0kawhWE1mmQbyyjZwJAHfVvWXRWbYxlMOSMxsKAVyMgP3DK 11 | 6Zz343IjmJfAK0O1X/BGQZOzPGcf5yNR9NaEa2KCrYuh/+UeEwC3tUatiQJANM9/ 12 | lomrsZw36upST+hkpvsCH+LPDiYxRqAdiYiu0DXL1ziBtaoAVDEcR5CocVAH3c+m 13 | rdL1f7iLEkqd4hYVRQJAP+uM18zCINdxYeZn+HVF3p5JOJii658+x08+0L69+DtV 14 | FFfRJfW98rCa5F2mQivCkwpChFq4NWSqpLT3x5WuXg== 15 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /test/myFirstExtension/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thom4parisot/crx/23766455f8df72b3d13d3e72101fe632e052383c/test/myFirstExtension/icon.png -------------------------------------------------------------------------------- /test/myFirstExtension/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICWwIBAAKBgQCx1nHfHr1yCt21hPRWE9yKwC8Xuq+y/dPDrkjJ8cVsgPRoq25i 3 | iej2siZEWryDR7WWuAmdGaUtBFgQyRvCyGQS4YtkKot8iOTFzJo656hgHJUvZP2Q 4 | Yy7ERJ3rZRwLxpWmvYQiXx92LSy19eC6Bi5+FAaTMCQNDdklanijb5D6fwIDAQAB 5 | AoGAUQmPSkUPvvAEp7q2PKNAVFnPG9kOR1ozLXA16xApDpCUzz2PR4fgiMoVdgCC 6 | 9q+up8elWdld022vU7bQ16nJL64GFzgCHubRBM7d5UJjSaVkcDL6QbzAEIwwa1IU 7 | LQA7BheshHs3d+gYRjedNGR0kKu3HeUofhhXLC1WXPCOZYECQQDrKqFPro6jRYjD 8 | T65qvoIetRKw1L8J9iw6UhWb1jMtCgvTKS99zmnJzM2hOsCAy+HDj/YwlCWEKIjh 9 | ZICZBFkpAkEAwZellq94wisJd4ptfQvHSWVPdMXR59jQcW23dBqU4D+itc1wCVT/ 10 | xg5iIBaujTHgaM9oS0kawhWE1mmQbyyjZwJAHfVvWXRWbYxlMOSMxsKAVyMgP3DK 11 | 6Zz343IjmJfAK0O1X/BGQZOzPGcf5yNR9NaEa2KCrYuh/+UeEwC3tUatiQJANM9/ 12 | lomrsZw36upST+hkpvsCH+LPDiYxRqAdiYiu0DXL1ziBtaoAVDEcR5CocVAH3c+m 13 | rdL1f7iLEkqd4hYVRQJAP+uM18zCINdxYeZn+HVF3p5JOJii658+x08+0L69+DtV 14 | FFfRJfW98rCa5F2mQivCkwpChFq4NWSqpLT3x5WuXg== 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /test/myFirstExtension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "My First Extension", 3 | "manifest_version": 2, 4 | "version": "1.0", 5 | "description": "The first extension that I made.", 6 | "browser_action": { 7 | "default_icon": "icon.png" 8 | }, 9 | "permissions": [ 10 | "http://api.flickr.com/" 11 | ] 12 | } --------------------------------------------------------------------------------