├── .gitignore ├── .github └── workflows │ ├── test.yml │ └── publish.yml ├── LICENSE ├── package.json ├── test ├── utils.js └── webcryptobox.js ├── CHANGELOG.md ├── index.html ├── webcryptobox.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: 13 | - 16.x 14 | - 17.x 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm install 23 | - run: npm test 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Johannes J. Schmidt 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Node.js Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v2 13 | with: 14 | node-version: 16 15 | - run: npm install 16 | - run: npm test 17 | 18 | publish: 19 | needs: test 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: actions/setup-node@v2 24 | with: 25 | node-version: 16 26 | registry-url: https://registry.npmjs.org/ 27 | - run: npm install 28 | - run: npm publish 29 | env: 30 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webcryptobox", 3 | "version": "4.0.0", 4 | "description": "Tiny utility library for asymetric encryption with WebCrypto.", 5 | "main": "webcryptobox.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "tap --no-cov test/*.js" 9 | }, 10 | "keywords": ["WebCrypto", "crypto", "subtle", "encryption", "decryption", "nacl", "e2ee"], 11 | "author": "Johannes J. Schmidt", 12 | "license": "Apache-2.0", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/jo/webcryptobox-js" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/jo/webcryptobox-js/issues" 19 | }, 20 | "homepage": { 21 | "url": "https://github.com/jo/webcryptobox-js" 22 | }, 23 | "files": [ 24 | "webcryptobox.js", 25 | "LICENSE" 26 | ], 27 | "devDependencies": { 28 | "tap": "^15.1.6" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap' 2 | import * as utils from '../webcryptobox.js' 3 | 4 | tap.test('cipher', async t => { 5 | t.same(utils.cipher, 'ECDH-P-521-AES-256-CBC') 6 | }) 7 | 8 | tap.test('utils', async t => { 9 | const testData = new Uint8Array([97, 98, 99]) 10 | const testText = 'abc' 11 | const testHex = '616263' 12 | const testBase64 = 'YWJj' 13 | 14 | t.test('decodeText', async t => { 15 | const decoded = utils.decodeText(testText) 16 | t.type(decoded, 'Uint8Array') 17 | t.same(decoded, testData) 18 | }) 19 | 20 | t.test('encodeText', async t => { 21 | const encoded = utils.encodeText(testData) 22 | t.equal(encoded, testText) 23 | }) 24 | 25 | t.test('decodeHex', async t => { 26 | const decoded = utils.decodeHex(testHex) 27 | t.type(decoded, 'Uint8Array') 28 | t.same(decoded, testData) 29 | }) 30 | 31 | t.test('encodeHex', async t => { 32 | const encoded = utils.encodeHex(testData) 33 | t.equal(encoded, testHex) 34 | }) 35 | 36 | t.test('decodeBase64', async t => { 37 | const decoded = utils.decodeBase64(testBase64) 38 | t.type(decoded, 'Uint8Array') 39 | t.same(decoded, testData) 40 | }) 41 | 42 | t.test('encodeBase64', async t => { 43 | const encoded = utils.encodeBase64(testData) 44 | t.equal(encoded, testBase64) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v4.0.0 - Preselected Cipher 4 | Went full circle. Back to a pure collection of functions. Cipher is set to ECDH P-521 AES-256-CBC. 5 | 6 | **Features:** 7 | * initialization vector included in cipher, no manual iv generation needed anymore 8 | * import and export encrypted private key pems 9 | 10 | **Breaking changes:** 11 | * all functions previously under `utils` are now toplevel exports 12 | * no more `Webcryptobox` class, class methods moved to toplevel exports using preselected cipher 13 | * `derivePrivateKey` has been renamed to `getPrivateKey` 14 | 15 | 16 | ## v3.0.0 - Configurable Cipher 17 | A restructured Webcryptobox, with configurable cipher. The export is now a class, which has to be initialized. 18 | 19 | **Features:** 20 | * configurable ciphers: 21 | - choose between `P-256`, `P-384` and `P-521` ec curves 22 | - choose a aes cipher: `GCM` or `CBC` 23 | - choose aes key length: `128` or `256` 24 | * new functions `importKey` and `exportKey` to exchange aes keys 25 | 26 | **Breaking changes:** 27 | * decode and encode functions have been moved to `utils` export: 28 | - `decodeText` 29 | - `encodeText` 30 | - `decodeHex` 31 | - `encodeHex` 32 | - `decodeBase64` 33 | - `encodeBase64` 34 | * `Webcryptobox` export is now a class, which is instantiated with `curve`, `mode` and `length` parameters configuring the ciphers 35 | * `generateSha1Fingerprint` has been renamed to `sha1Fingerprint` 36 | * `generateSha256Fingerprint` has been renamed to `sha256Fingerprint` 37 | 38 | **Fix:** 39 | * PEM wrapping has been set to 64 chars according to the standard 40 | 41 | 42 | ## v2.0.0 - SHA-1 Fingerprints 43 | Introduction of a SHA-1 fingerprint function: `generateSha1Fingerprint`. 44 | 45 | **Breaking change:** 46 | * rename `generateFingerprint` to `generateSha256Fingerprint` 47 | 48 | 49 | ## v1.1.0 - Wrap PEM Lines 50 | Small improvement: break lines after 80 chars in PEMs. 51 | 52 | 53 | ## v1.0.2 - README Typos 54 | Fixes typo in example on README. 55 | 56 | 57 | ## v1.0.1 - Tight Package 58 | Tightened the npm package by removing non-essential files. 59 | 60 | 61 | ## v1.0.0 - Hello World! 62 | Initial release of Webcryptobox 63 | -------------------------------------------------------------------------------- /test/webcryptobox.js: -------------------------------------------------------------------------------- 1 | import tap from 'tap' 2 | import { webcrypto as crypto } from 'crypto' 3 | import * as wcb from '../webcryptobox.js' 4 | 5 | tap.Test.prototype.addAssert('sameKey', 2, async function (key, otherKey, message, extra) { 6 | message = message || 'keys should be identical' 7 | 8 | const format = key.type === 'private' ? 'pkcs8' : 'raw' 9 | 10 | const keyRaw = await crypto.subtle.exportKey(format, key) 11 | const otherKeyRaw = await crypto.subtle.exportKey(format, otherKey) 12 | 13 | return this.same(new Uint8Array(keyRaw), new Uint8Array(otherKeyRaw), message, extra) 14 | }) 15 | 16 | const alice = { 17 | privateKeyPem: `-----BEGIN PRIVATE KEY----- 18 | MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBcf8zEjlssqn4aTEB 19 | RR43ofwH/4BAXDAAd83Kz1Dyd+Ko0pit4ESgqSu/bJMdnDrpiGYuz0Klarwip8LD 20 | rYd9mEahgYkDgYYABAF2Nu9XKPs2CVFocuqCfaX5FzDUt6/nT/3Evqq8jBhK/ziN 21 | TrEs4wkZjuei5TS25aabX6iMex3etoN/GOw1KYpI4QBtIUnWudG8FT8N+USHSL9G 22 | h9fi+Yofeq4Io9DxPU1ChCKPIoQ6ORAMWoOCk9bTdIy6yqx33+RIM04wub4QAgDo 23 | LQ== 24 | -----END PRIVATE KEY-----`, 25 | publicKeyPem: `-----BEGIN PUBLIC KEY----- 26 | MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBdjbvVyj7NglRaHLqgn2l+Rcw1Lev 27 | 50/9xL6qvIwYSv84jU6xLOMJGY7nouU0tuWmm1+ojHsd3raDfxjsNSmKSOEAbSFJ 28 | 1rnRvBU/DflEh0i/RofX4vmKH3quCKPQ8T1NQoQijyKEOjkQDFqDgpPW03SMusqs 29 | d9/kSDNOMLm+EAIA6C0= 30 | -----END PUBLIC KEY-----`, 31 | sha1Fingerprint: 'd91829d8fc9a28608e007149e1cf3c8f35d26c5f', 32 | sha256Fingerprint: '0c8584b5a48138cde0cb3788734870108a90ed0a7eb62498f00c0838b6868653', 33 | derivedBits: new Uint8Array([ 34 | 0, 212, 225, 22, 80, 136, 36, 142, 33, 144, 117, 93, 78, 201, 53, 127 35 | ]) 36 | } 37 | 38 | const bob = { 39 | privateKeyPem: `-----BEGIN PRIVATE KEY----- 40 | MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAvVtKGBatnJz0J+Tt 41 | L3MFjdHp4JXE4pVs+mUJNYaIxnLyLHnUDQhgNo6va7EJeupHDpL8ixwz6pb6qoZZ 42 | x3G21wOhgYkDgYYABAFtE04yjeLeUC8V4RvDY6tlCv5wz5g8etFduTOqhYvw/GzN 43 | aY1VbKa6W9MjlpYyYnfBQmyZCbvoeHTmULAWscQ8NAGCj9gH+T6D5lPhKR8WuNtB 44 | CvKGKDtCwTxzJDFEo2F6ZhJ11ucV/sLNJrd62LXjN5aURArbSsEKuib7l4rvAN8A 45 | 0g== 46 | -----END PRIVATE KEY-----`, 47 | publicKeyPem: `-----BEGIN PUBLIC KEY----- 48 | MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBbRNOMo3i3lAvFeEbw2OrZQr+cM+Y 49 | PHrRXbkzqoWL8PxszWmNVWymulvTI5aWMmJ3wUJsmQm76Hh05lCwFrHEPDQBgo/Y 50 | B/k+g+ZT4SkfFrjbQQryhig7QsE8cyQxRKNhemYSddbnFf7CzSa3eti14zeWlEQK 51 | 20rBCrom+5eK7wDfANI= 52 | -----END PUBLIC KEY-----`, 53 | sha1Fingerprint: '12a5fc4b7fd94d291d94f8f9e1357675b4bd25c8', 54 | sha256Fingerprint: 'fd5397c78d0c249d864408f9cf90994f3e7a6505077b6262845ad6d6e7609e9c', 55 | derivedBits: new Uint8Array([ 56 | 0, 221, 22, 12, 113, 57, 255, 119, 187, 119, 232, 29, 78, 236, 137, 62 57 | ]) 58 | } 59 | 60 | // 'a secret message' 61 | const message = new Uint8Array([ 62 | 97, 32, 115, 101, 99, 114, 101, 116, 32, 109, 101, 115, 115, 97, 103, 101 63 | ]) 64 | 65 | const keyData = new Uint8Array([ 66 | 1, 111, 248, 82, 88, 255, 144, 7, 193, 187, 122, 192, 179, 225, 244, 241, 169, 215, 155, 221, 71, 168, 123, 161, 82, 74, 117, 207, 48, 72, 78, 187 67 | ]) 68 | 69 | tap.test('ciphers', async g => { 70 | alice.privateKey = await wcb.importPrivateKeyPem(alice.privateKeyPem) 71 | alice.publicKey = await wcb.importPublicKeyPem(alice.publicKeyPem) 72 | bob.privateKey = await wcb.importPrivateKeyPem(bob.privateKeyPem) 73 | bob.publicKey = await wcb.importPublicKeyPem(bob.publicKeyPem) 74 | const key = await crypto.subtle.importKey( 75 | 'raw', 76 | keyData, 77 | { 78 | name: `AES-CBC`, 79 | length: 256 80 | }, 81 | true, 82 | ['encrypt', 'decrypt'] 83 | ) 84 | 85 | g.test('key generation', async t => { 86 | t.test('generateKeyPair', async t => { 87 | const { publicKey, privateKey } = await wcb.generateKeyPair() 88 | t.type(publicKey, 'CryptoKey') 89 | t.type(privateKey, 'CryptoKey') 90 | }) 91 | 92 | t.test('generateKey', async t => { 93 | const key = await wcb.generateKey() 94 | t.type(key, 'CryptoKey') 95 | }) 96 | }) 97 | 98 | g.test('key derivation', async t => { 99 | t.test('deriveKey for alice', async t => { 100 | const derivedKey = await wcb.deriveKey({ privateKey: alice.privateKey, publicKey: bob.publicKey }) 101 | await t.sameKey(derivedKey, key) 102 | }) 103 | 104 | t.test('deriveKey for bob', async t => { 105 | const derivedKey = await wcb.deriveKey({ privateKey: bob.privateKey, publicKey: alice.publicKey }) 106 | await t.sameKey(derivedKey, key) 107 | }) 108 | 109 | t.test('deriveBits for alice', async t => { 110 | const derivedBits = await wcb.deriveBits({ length: 16, privateKey: alice.privateKey, publicKey: alice.publicKey }) 111 | t.same(new Uint8Array(derivedBits), alice.derivedBits) 112 | }) 113 | 114 | t.test('deriveBits for bob', async t => { 115 | const derivedBits = await wcb.deriveBits({ length: 16, privateKey: bob.privateKey, publicKey: bob.publicKey }) 116 | t.same(new Uint8Array(derivedBits), bob.derivedBits) 117 | }) 118 | 119 | t.test('getPublicKey for alice', async t => { 120 | const publicKey = await wcb.getPublicKey(alice.privateKey) 121 | await t.sameKey(publicKey, alice.publicKey) 122 | }) 123 | 124 | t.test('getPublicKey for bob', async t => { 125 | const publicKey = await wcb.getPublicKey(bob.privateKey) 126 | await t.sameKey(publicKey, bob.publicKey) 127 | }) 128 | }) 129 | 130 | g.test('key export & import', async t => { 131 | t.test('importPublicKeyPem for alice', async t => { 132 | const pem = await wcb.exportPublicKeyPem(alice.publicKey) 133 | t.equal(pem, alice.publicKeyPem) 134 | }) 135 | 136 | t.test('importPublicKeyPem for bob', async t => { 137 | const pem = await wcb.exportPublicKeyPem(bob.publicKey) 138 | t.equal(pem, bob.publicKeyPem) 139 | }) 140 | 141 | t.test('exportPrivateKeyPem for alice', async t => { 142 | const pem = await wcb.exportPrivateKeyPem(alice.privateKey) 143 | t.equal(pem, alice.privateKeyPem) 144 | }) 145 | 146 | t.test('exportPrivateKeyPem for bob', async t => { 147 | const pem = await wcb.exportPrivateKeyPem(bob.privateKey) 148 | t.equal(pem, bob.privateKeyPem) 149 | }) 150 | 151 | t.test('exportEncryptedPrivateKeyPem and importEncryptedPrivateKeyPem', async t => { 152 | const passphrase = wcb.decodeText('secure') 153 | const pem = await wcb.exportEncryptedPrivateKeyPem({ key: alice.privateKey, passphrase }) 154 | const alicePrivateKey = await wcb.importEncryptedPrivateKeyPem({ pem, passphrase }) 155 | await t.sameKey(alicePrivateKey, alice.privateKey) 156 | }) 157 | 158 | t.test('exportEncryptedPrivateKeyPemTo bob and importEncryptedPrivateKeyPemFrom alice', async t => { 159 | const pem = await wcb.exportEncryptedPrivateKeyPemTo({ 160 | key: alice.privateKey, 161 | privateKey: alice.privateKey, 162 | publicKey: bob.publicKey 163 | }) 164 | const alicePrivateKey = await wcb.importEncryptedPrivateKeyPemFrom({ 165 | pem, 166 | privateKey: bob.privateKey, 167 | publicKey: alice.publicKey 168 | }) 169 | await t.sameKey(alicePrivateKey, alice.privateKey) 170 | }) 171 | }) 172 | 173 | g.test('sha256Fingerprint for alice', async t => { 174 | const fingerprint = await wcb.sha256Fingerprint(alice.publicKey) 175 | t.equal(wcb.encodeHex(fingerprint), alice.sha256Fingerprint) 176 | }) 177 | 178 | g.test('sha256Fingerprint for bob', async t => { 179 | const fingerprint = await wcb.sha256Fingerprint(bob.publicKey) 180 | t.equal(wcb.encodeHex(fingerprint), bob.sha256Fingerprint) 181 | }) 182 | 183 | g.test('sha1Fingerprint for alice', async t => { 184 | const fingerprint = await wcb.sha1Fingerprint(alice.publicKey) 185 | t.equal(wcb.encodeHex(fingerprint), alice.sha1Fingerprint) 186 | }) 187 | 188 | g.test('sha1Fingerprint for bob', async t => { 189 | const fingerprint = await wcb.sha1Fingerprint(bob.publicKey) 190 | t.equal(wcb.encodeHex(fingerprint), bob.sha1Fingerprint) 191 | }) 192 | 193 | g.test('encryption and decryption', async t => { 194 | t.test('encrypt and decrypt', async t => { 195 | const box = await wcb.encrypt({ message, key }) 196 | const data = await wcb.decrypt({ box, key }) 197 | t.same(data, message) 198 | }) 199 | 200 | t.test('encryptTo and decryptFrom', async t => { 201 | const box = await wcb.encryptTo({ privateKey: alice.privateKey, publicKey: bob.publicKey, message }) 202 | const data = await wcb.decryptFrom({ privateKey: bob.privateKey, publicKey: alice.publicKey, box }) 203 | t.same(data, message) 204 | }) 205 | }) 206 | }) 207 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Webcryptobox 5 | 27 | 28 | 29 |

Webcryptobox

30 |

Demo for https://github.com/jo/webcryptobox-js showing asymetric encryption ECDH P-521 AES 256 CBC.

31 | 32 |
33 |
34 |

Alice

35 |

36 | 37 | 38 |

39 |

40 | 41 | 42 |

43 |

44 | 45 | 46 |

47 |

48 | 49 | 50 |

51 |

52 | 53 | 54 |

55 |

56 | 57 | 58 |

59 |
60 |
61 |

Bob

62 |

63 | 64 | 65 |

66 |

67 | 68 | 69 |

70 |

71 | 72 | 73 |

74 |

75 | 76 | 77 |

78 |

79 | 80 | 81 |

82 |

83 | 84 | 85 |

86 |
87 |
88 | 263 | -------------------------------------------------------------------------------- /webcryptobox.js: -------------------------------------------------------------------------------- 1 | // load the webcrypto api either from `window.crypto` when in browser 2 | // or import from crypto standard lib when in node 3 | const crypto = typeof window === 'undefined' ? (await import('crypto')).webcrypto : window.crypto 4 | 5 | 6 | // crypto config 7 | const EC_PARAMS = { 8 | name: 'ECDH', 9 | namedCurve: 'P-521' 10 | } 11 | const AES_PARAMS = { 12 | name: `AES-CBC`, 13 | length: 256 14 | } 15 | const IV_LENGTH = 16 16 | const PBKDF2_PARAMS = { 17 | name: 'PBKDF2', 18 | iterations: 64000, 19 | hash: 'SHA-256' 20 | } 21 | const OIDS = { 22 | pbkdf2: '06092a864886f70d01050c', // PBKDF2 23 | pbes2: '06092a864886f70d01050d', // PBES2 24 | hash: '06082a864886f70d02090500', // SHA-256 25 | cipher: '060960864801650304012a' // AES-256-CBC 26 | } 27 | 28 | export const cipher = `${EC_PARAMS.name}-${EC_PARAMS.namedCurve}-${AES_PARAMS.name.split('-')[0]}-${AES_PARAMS.length}-${AES_PARAMS.name.split('-')[1]}` 29 | 30 | 31 | // utils 32 | 33 | // decode text message 34 | export const decodeText = text => { 35 | const enc = new TextEncoder() 36 | return enc.encode(text) 37 | } 38 | 39 | // encode data as text 40 | export const encodeText = data => { 41 | const dec = new TextDecoder() 42 | return dec.decode(data) 43 | } 44 | 45 | 46 | // decode hex string 47 | export const decodeHex = hex => new Uint8Array( 48 | hex.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16)) 49 | ) 50 | 51 | // encode data as hex string 52 | export const encodeHex = data => Array.from(new Uint8Array(data)) 53 | .map(x => ('00' + x.toString(16)).slice(-2)) 54 | .join('') 55 | 56 | 57 | // decode base64 string 58 | export const decodeBase64 = base64 => { 59 | var i, d = atob(base64.trim()), b = new Uint8Array(d.length) 60 | for (i = 0; i < d.length; i++) b[i] = d.charCodeAt(i) 61 | return b 62 | } 63 | 64 | // encode data as base64 string 65 | export const encodeBase64 = data => btoa(String.fromCharCode(...new Uint8Array(data))) 66 | 67 | 68 | // pem 69 | 70 | // decode public ecdh key pem 71 | const decodePublicKeyPem = pem => decodeBase64(pem.trim().slice(27, -25).replace(/\n/g, '')) 72 | 73 | // encode public ecdh key as pem 74 | const encodePublicKeyPem = data => `-----BEGIN PUBLIC KEY----- 75 | ${encodeBase64(data).match(/.{1,64}/g).join('\n')} 76 | -----END PUBLIC KEY-----` 77 | 78 | // decode private key pem 79 | const decodePrivateKeyPem = pem => decodeBase64(pem.trim().slice(28, -26).replace(/\n/g, '')) 80 | 81 | // encode private key as pem 82 | const encodePrivateKeyPem = data => `-----BEGIN PRIVATE KEY----- 83 | ${encodeBase64(data).match(/.{1,64}/g).join('\n')} 84 | -----END PRIVATE KEY-----` 85 | 86 | 87 | // encrypted pem encoding 88 | 89 | // utilities 90 | const decimalToHex = d => { 91 | const h = (d).toString(16) 92 | return h.length % 2 ? '0' + h : h 93 | } 94 | const hexLength = d => { 95 | const h = (d.length / 2).toString(16) 96 | return h.length % 2 ? '0' + h : h 97 | } 98 | 99 | // encode encrypted private key as pem 100 | const encodeEncryptedPrivateKeyPem = ({ wrappedKey, iv, salt }) => { 101 | const wrappedKeyHex = encodeHex(wrappedKey) 102 | const saltHex = encodeHex(salt) 103 | const ivHex = encodeHex(iv) 104 | const iter = '00' + decimalToHex(PBKDF2_PARAMS.iterations) 105 | const iterInteger = '02' + decimalToHex(iter.length / 2) + iter 106 | const saltOctet = '04' + hexLength(saltHex) + saltHex 107 | const ivOctet = '04' + hexLength(ivHex) + ivHex 108 | const keyOctetPadding = hexLength(wrappedKeyHex).length / 2 === 2 ? '82' : '81' 109 | const keyOctet = '04' + keyOctetPadding + hexLength(wrappedKeyHex) + wrappedKeyHex 110 | const aesContainer = '30' + hexLength(OIDS.cipher + ivOctet) 111 | const hashContainer = '30' + hexLength(OIDS.hash) 112 | const pbkdf2InnerParameters = saltOctet + iterInteger + hashContainer + OIDS.hash 113 | const pbkdf2InnerContainer = '30' + hexLength(pbkdf2InnerParameters) 114 | const pbkdf2Parameters = OIDS.pbkdf2 + pbkdf2InnerContainer + pbkdf2InnerParameters 115 | const pbkdf2Container = '30' + hexLength(pbkdf2Parameters) 116 | const pbes2InnerParameters = pbkdf2Container + pbkdf2Parameters + aesContainer + OIDS.cipher + ivOctet 117 | const pbes2InnerContainer = '30' + hexLength(pbes2InnerParameters) 118 | const sequenceParameters = OIDS.pbes2 + pbes2InnerContainer + pbes2InnerParameters 119 | const sequenceContainer = '30' + hexLength(sequenceParameters) + sequenceParameters 120 | const sequenceLength = hexLength(sequenceContainer + keyOctet) 121 | const sequencePadding = sequenceLength.length / 2 === 2 ? '82' : '81' 122 | const sequence = '30' + sequencePadding + sequenceLength + sequenceContainer + keyOctet 123 | 124 | const asnKey = decodeHex(sequence) 125 | 126 | return `-----BEGIN ENCRYPTED PRIVATE KEY----- 127 | ${encodeBase64(asnKey).match(/.{1,64}/g).join('\n')} 128 | -----END ENCRYPTED PRIVATE KEY-----` 129 | } 130 | 131 | const decodeEncryptedPrivateKeyPem = pem => { 132 | const pemData = decodeBase64(pem.trim().slice(38, -36).replace(/\n/g, '')) 133 | const hex = encodeHex(pemData) 134 | 135 | if (!hex.includes(OIDS.pbkdf2) || !hex.includes(OIDS.pbes2)) { 136 | throw(new Error('Invalid pem: no PBKDF2 or PBES2 header')) 137 | } 138 | if (!hex.includes(OIDS.cipher)) { 139 | throw(new Error('Invalid pem: unsupported cipher')) 140 | } 141 | if (!hex.includes(OIDS.hash)) { 142 | throw(new Error('Invalid pem: unsupported hash')) 143 | } 144 | 145 | const saltBegin = hex.indexOf(OIDS.pbkdf2) + 28 146 | const ivBegin = hex.indexOf(OIDS.cipher) + 24 147 | const saltLength = parseInt(hex.substr(saltBegin, 2), 16) 148 | const ivLength = parseInt(hex.substr(ivBegin, 2), 16) 149 | const saltHex = hex.substr(saltBegin + 2, saltLength * 2) 150 | const ivHex = hex.substr(ivBegin + 2, ivLength * 2) 151 | const iterBegin = saltBegin + 4 + (saltLength * 2) 152 | const iterLength = parseInt(hex.substr(iterBegin, 2), 16) 153 | const iter = parseInt(hex.substr(iterBegin + 2, iterLength * 2), 16) 154 | const sequencePadding = hex.substr(2, 2) === '81' ? 8 : 10 155 | const parametersPadding = hex.substr(2, 2) === '81' ? 12 : 16 156 | const sequenceLength = parseInt(hex.substr(sequencePadding, 2), 16) 157 | const encryptedDataBegin = parametersPadding + (sequenceLength * 2) 158 | const encryptedDataPadding = hex.substr(encryptedDataBegin - 2, 2) === '81' ? 2 : 4 159 | const encryptedDataLength = parseInt(hex.substr(encryptedDataBegin, 6), 16) 160 | const encryptedData = hex.substr(encryptedDataBegin + encryptedDataPadding, (encryptedDataLength * 2)) 161 | 162 | const salt = decodeHex(saltHex) 163 | const iv = decodeHex(ivHex) 164 | const wrappedKey = decodeHex(encryptedData) 165 | 166 | return { 167 | wrappedKey, 168 | iv, 169 | salt 170 | } 171 | } 172 | 173 | 174 | // key generation 175 | 176 | // generate ecdh key pair pair 177 | export const generateKeyPair = () => crypto.subtle.generateKey( 178 | EC_PARAMS, 179 | true, 180 | ['deriveKey', 'deriveBits'] 181 | ) 182 | 183 | // generate aes key 184 | export const generateKey = () => crypto.subtle.generateKey( 185 | AES_PARAMS, 186 | true, 187 | ['encrypt', 'decrypt'] 188 | ) 189 | 190 | // derive aes encryption key from ecdh public and private key 191 | export const deriveKey = ({ publicKey, privateKey }) => crypto.subtle.deriveKey( 192 | { 193 | ...EC_PARAMS, 194 | public: publicKey 195 | }, 196 | privateKey, 197 | AES_PARAMS, 198 | true, 199 | ['encrypt', 'decrypt'] 200 | ) 201 | 202 | // derive bits from ecdh key pair 203 | export const deriveBits = ({ length, publicKey, privateKey }) => crypto.subtle.deriveBits( 204 | { 205 | ...EC_PARAMS, 206 | public: publicKey 207 | }, 208 | privateKey, 209 | length * 8 210 | ) 211 | 212 | // derive wrapping key from passphrase with pkdf2 213 | const deriveWrappingKey = async ({ salt, passphrase }) => { 214 | const baseKey = await crypto.subtle.importKey( 215 | 'raw', 216 | passphrase, 217 | { 218 | name: 'PBKDF2' 219 | }, 220 | false, 221 | ['deriveKey'] 222 | ) 223 | 224 | return crypto.subtle.deriveKey( 225 | { 226 | ...PBKDF2_PARAMS, 227 | salt 228 | }, 229 | baseKey, 230 | AES_PARAMS, 231 | false, 232 | ['wrapKey', 'unwrapKey'] 233 | ) 234 | } 235 | 236 | // wrap a key in pkcs8 encrypted with wrappingKey 237 | const wrapKey = async ({ key, wrappingKey }) => { 238 | const iv = await crypto.getRandomValues(new Uint8Array(16)) 239 | const wrappedKey = await crypto.subtle.wrapKey( 240 | 'pkcs8', 241 | key, 242 | wrappingKey, 243 | { 244 | ...AES_PARAMS, 245 | iv: iv 246 | } 247 | ) 248 | 249 | return { 250 | wrappedKey, 251 | iv 252 | } 253 | } 254 | 255 | // unwrap a key from pkcs8 encrypted with wrappingKey 256 | const unwrapKey = async ({ wrappedKey, wrappingKey, iv }) => { 257 | return crypto.subtle.unwrapKey( 258 | 'pkcs8', 259 | wrappedKey, 260 | wrappingKey, 261 | { 262 | ...AES_PARAMS, 263 | iv: iv 264 | }, 265 | EC_PARAMS, 266 | true, 267 | ['deriveKey', 'deriveBits'] 268 | ) 269 | } 270 | 271 | 272 | // derive password from private and public key 273 | export const derivePassword = async ({ privateKey, publicKey, length }) => { 274 | const aes_length = AES_PARAMS.length / 8 275 | const bits = await deriveBits({ privateKey, publicKey, length: length + aes_length }) 276 | return bits.slice(aes_length) 277 | } 278 | 279 | // derive public key from private key 280 | export const getPublicKey = async privateKey => { 281 | const jwk = await crypto.subtle.exportKey( 282 | 'jwk', 283 | privateKey 284 | ) 285 | delete jwk.d 286 | return crypto.subtle.importKey( 287 | 'jwk', 288 | jwk, 289 | EC_PARAMS, 290 | true, 291 | [] 292 | ) 293 | } 294 | 295 | 296 | // fingerprinting 297 | 298 | // calculate a sha256 fingerprint of a key 299 | export const sha256Fingerprint = async key => { 300 | const keyBits = await crypto.subtle.exportKey('spki', key) 301 | return crypto.subtle.digest('SHA-256', keyBits) 302 | } 303 | 304 | // calculate a sha1 fingerprint of a key 305 | export const sha1Fingerprint = async key => { 306 | const keyBits = await crypto.subtle.exportKey('spki', key) 307 | return crypto.subtle.digest('SHA-1', keyBits) 308 | } 309 | 310 | 311 | // import & export keys 312 | 313 | // import a raw aes key 314 | export const importKey = data => crypto.subtle.importKey( 315 | 'raw', 316 | data, 317 | AES_PARAMS, 318 | true, 319 | ['encrypt', 'decrypt'] 320 | ) 321 | 322 | // export an aes key as raw bytes 323 | export const exportKey = key => crypto.subtle.exportKey( 324 | 'raw', 325 | key 326 | ) 327 | 328 | // internal functions 329 | 330 | // export a public ecdh key as spki 331 | const exportPublicKey = publicKey => crypto.subtle.exportKey( 332 | 'spki', 333 | publicKey 334 | ) 335 | 336 | // export a private ecdh key as pkcs8 337 | const exportPrivateKey = privateKey => crypto.subtle.exportKey( 338 | 'pkcs8', 339 | privateKey 340 | ) 341 | 342 | // import a public ecdh key spki 343 | const importPublicKey = data => crypto.subtle.importKey( 344 | 'spki', 345 | data, 346 | EC_PARAMS, 347 | true, 348 | [] 349 | ) 350 | 351 | // import a private ecdh key pkcs8 352 | const importPrivateKey = data => crypto.subtle.importKey( 353 | 'pkcs8', 354 | data, 355 | EC_PARAMS, 356 | true, 357 | ['deriveKey', 'deriveBits'] 358 | ) 359 | 360 | 361 | // export public key as pem 362 | export const exportPublicKeyPem = async key => { 363 | const data = await exportPublicKey(key) 364 | return encodePublicKeyPem(data) 365 | } 366 | 367 | // import public key pem 368 | export const importPublicKeyPem = pem => { 369 | const data = decodePublicKeyPem(pem) 370 | return importPublicKey(data) 371 | } 372 | 373 | // export private key as pem 374 | export const exportPrivateKeyPem = async key => { 375 | const data = await exportPrivateKey(key) 376 | return encodePrivateKeyPem(data) 377 | } 378 | 379 | // import private key pem 380 | export const importPrivateKeyPem = pem => { 381 | const data = decodePrivateKeyPem(pem) 382 | return importPrivateKey(data) 383 | } 384 | 385 | // export encrypted private key as pem 386 | export const exportEncryptedPrivateKeyPem = async ({ key, passphrase }) => { 387 | const salt = await crypto.getRandomValues(new Uint8Array(16)) 388 | const wrappingKey = await deriveWrappingKey({ salt, passphrase }) 389 | const { wrappedKey, iv } = await wrapKey({ key, wrappingKey }) 390 | return encodeEncryptedPrivateKeyPem({ wrappedKey, iv, salt }) 391 | } 392 | 393 | // import encrypted private key pem 394 | export const importEncryptedPrivateKeyPem = async ({ pem, passphrase }) => { 395 | const { wrappedKey, iv, salt } = decodeEncryptedPrivateKeyPem(pem) 396 | const wrappingKey = await deriveWrappingKey({ salt, passphrase }) 397 | return unwrapKey({ wrappedKey, wrappingKey, iv }) 398 | } 399 | 400 | 401 | // derive wrapping key from peer 402 | const deriveWrappingKeyFrom = async ({ salt, privateKey, publicKey }) => { 403 | const passphraseBits = await derivePassword({ privateKey, publicKey, length: 32 }) 404 | const passphraseHex = encodeHex(passphraseBits) 405 | const passphrase = decodeText(passphraseHex) 406 | return deriveWrappingKey({ salt, passphrase }) 407 | } 408 | 409 | // export encrypted private key as pem to peer 410 | export const exportEncryptedPrivateKeyPemTo = async ({ key, privateKey, publicKey }) => { 411 | const salt = await crypto.getRandomValues(new Uint8Array(16)) 412 | const wrappingKey = await deriveWrappingKeyFrom({ salt, privateKey, publicKey }) 413 | const { wrappedKey, iv } = await wrapKey({ key, wrappingKey }) 414 | return encodeEncryptedPrivateKeyPem({ wrappedKey, iv, salt }) 415 | } 416 | 417 | // import encrypted private key pem from peer 418 | export const importEncryptedPrivateKeyPemFrom = async ({ pem, privateKey, publicKey }) => { 419 | const { wrappedKey, iv, salt } = decodeEncryptedPrivateKeyPem(pem) 420 | const wrappingKey = await deriveWrappingKeyFrom({ salt, privateKey, publicKey }) 421 | return unwrapKey({ wrappedKey, wrappingKey, iv }) 422 | } 423 | 424 | 425 | // encryption & decryption 426 | 427 | // internal: generate a initialization vector (aka nonce) 428 | const generateIv = () => crypto.getRandomValues(new Uint8Array(IV_LENGTH)) 429 | 430 | 431 | // symmetric encryption 432 | 433 | // encrypt a message with aes key 434 | export const encrypt = async ({ message, key }) => { 435 | const iv = generateIv() 436 | const encrypted = await crypto.subtle.encrypt( 437 | { 438 | ...AES_PARAMS, 439 | iv 440 | }, 441 | key, 442 | message 443 | ) 444 | const box = new Uint8Array(IV_LENGTH + encrypted.byteLength) 445 | box.set(iv, 0) 446 | box.set(new Uint8Array(encrypted), IV_LENGTH) 447 | return box 448 | } 449 | 450 | // decrypt a message with aes key 451 | export const decrypt = async ({ box, key }) => { 452 | const iv = box.slice(0, IV_LENGTH) 453 | const encrypted = box.slice(IV_LENGTH) 454 | const message = await crypto.subtle.decrypt( 455 | { 456 | ...AES_PARAMS, 457 | iv 458 | }, 459 | key, 460 | encrypted 461 | ) 462 | return new Uint8Array(message) 463 | } 464 | 465 | 466 | // asymmetric encryption 467 | 468 | // encrypt with private key and public public peer key 469 | export const encryptTo = async ({ message, privateKey, publicKey }) => { 470 | const key = await deriveKey({ privateKey, publicKey }) 471 | return encrypt({ message, key }) 472 | } 473 | 474 | // decrypt with private key and public peer key 475 | export const decryptFrom = async ({ box, privateKey, publicKey }) => { 476 | const key = await deriveKey({ privateKey, publicKey }) 477 | return decrypt({ box, key }) 478 | } 479 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Webcryptobox 2 | WebCrypto compatible encryption with Bash and OpenSSL. 3 | 4 | This package implements the [Webcryptobox](https://github.com/jo/webcryptobox) encryption API. 5 | 6 | Compatible packages: 7 | * [Webcryptobox Rust](https://github.com/jo/webcryptobox-rs) 8 | * [Webcryptobox Bash](https://github.com/jo/webcryptobox-sh) 9 | 10 | There is also a CLI tool: [wcb.sh](https://github.com/jo/wcb-js) 11 | 12 | Demo: https://jo.github.io/webcryptobox-js/ 13 | 14 | This library provides easy to use and convenient wrappers around the WebCrypto primitives, as well as some helpers for encoding/decoding and a few sugar functions, with zero dependencies. 15 | 16 | It works directly in the browser and in latest Node.js versions (via [the experimental WebCrypto API](https://nodejs.org/api/webcrypto.html)). 17 | 18 | 19 | ## Usage 20 | In Node, you can use the lib as usual: 21 | ```sh 22 | npm install webcryptobox 23 | ``` 24 | 25 | and then 26 | ```js 27 | import * as wcb from 'webcryptobox' 28 | ``` 29 | 30 | In modern browser which support es6 modules, just include [the file](./webcryptobox.js) directly: 31 | ```html 32 | 35 | ``` 36 | 37 | Now you can dance with Webcryptobox like this: 38 | ```js 39 | const alice = await wcb.generateKeyPair() 40 | const bob = await wcb.generateKeyPair() 41 | const text = 'Test message' 42 | const message = wcb.decodeText(text) 43 | const box = await wcb.encryptTo({ message, privateKey: alice.privateKey, publicKey: bob.publicKey }) 44 | const decryptedBox = await wcb.decryptFrom({ box, privateKey: bob.privateKey, publicKey: alice.publicKey }) 45 | const decryptedText = wcb.encodeText(decryptedBox) 46 | ``` 47 | 48 | 49 | ## API 50 | This lib is written with some ECMAScript 6 features, mainly modules, dynamic import, async, destructuring and object spreading. 51 | 52 | Most of the functions return promises, except for the encoding/decoding utilities. 53 | 54 | 55 | #### `cipher` 56 | Returns a cipher identifier: 57 | 58 | ```js 59 | wcb.cipher 60 | // 'ECDH-P-521-AES-256-CBC' 61 | ``` 62 | 63 | 64 | ### Utils for Encoding & Decoding 65 | Webcryptobox provides utility functions to convert between several text representations and the internally used `Uint8Array`s. 66 | 67 | #### `decodeText` 68 | Takes an unicode string and encodes it to an Uint8Array: 69 | 70 | ```js 71 | const data = wcb.decodeText('my message') 72 | // Uint8Array(10) [ 73 | // 109, 121, 32, 109, 74 | // 101, 115, 115, 97, 75 | // 103, 101 76 | // ] 77 | ``` 78 | 79 | #### `encodeText` 80 | Given a Uint8Array, encodes the data as unicode string: 81 | 82 | ```js 83 | const text = wcb.encodeText(new Uint8Array([ 84 | 109, 121, 32, 109, 85 | 101, 115, 115, 97, 86 | 103, 101 87 | ])) 88 | // my message 89 | ``` 90 | 91 | #### `decodeHex` 92 | Takes a hex string and encodes it to an Uint8Array: 93 | 94 | ```js 95 | const data = wcb.decodeHex('6d79206d657373616765') 96 | // Uint8Array(10) [ 97 | // 109, 121, 32, 109, 98 | // 101, 115, 115, 97, 99 | // 103, 101 100 | // ] 101 | ``` 102 | 103 | #### `encodeHex` 104 | Given a Uint8Array, encodes the data as hex string: 105 | 106 | ```js 107 | const hex = wcb.encodeHex(new Uint8Array([ 108 | 109, 121, 32, 109, 109 | 101, 115, 115, 97, 110 | 103, 101 111 | ])) 112 | // 6d79206d657373616765 113 | ``` 114 | 115 | #### `decodeBase64` 116 | Takes a base64 string and encodes it to an Uint8Array: 117 | 118 | ```js 119 | const data = wcb.decodeBase64('bXkgbWVzc2FnZQ==') 120 | // Uint8Array(10) [ 121 | // 109, 121, 32, 109, 122 | // 101, 115, 115, 97, 123 | // 103, 101 124 | // ] 125 | ``` 126 | 127 | #### `encodeBase64` 128 | Given a Uint8Array, encodes the data as base64 string: 129 | 130 | ```js 131 | const base64 = wcb.encodeBase64(new Uint8Array([ 132 | 109, 121, 32, 109, 133 | 101, 115, 115, 97, 134 | 103, 101 135 | ])) 136 | // bXkgbWVzc2FnZQ== 137 | ``` 138 | 139 | 140 | ### Key Generation and Derivation 141 | Functions for generating a ecdh and aes-cbc keys, for deriving an aes-cbc key or the public key from a private one and for generating a sha-256 fingerprint of a key. 142 | 143 | #### `generateKey` 144 | Generate aes-cbc key with a length of 256. The key will be extractable and can be used for encryption and decryption. 145 | 146 | ```js 147 | const key = await wcb.generateKey() 148 | // CryptoKey { 149 | // type: 'secret', 150 | // extractable: true, 151 | // algorithm: { name: 'AES-GCM', length: 256 }, 152 | // usages: [ 'encrypt', 'decrypt' ] 153 | // } 154 | ``` 155 | #### `generateKeyPair` 156 | Generates ecdh key pair with curve `P-521`. The private key will be extractable, and can be used to derive a key. 157 | 158 | ```js 159 | const keyPair = await wcb.generateKeyPair() 160 | // { 161 | // publicKey: CryptoKey { 162 | // type: 'public', 163 | // extractable: true, 164 | // algorithm: { name: 'ECDH', namedCurve: 'P-521' }, 165 | // usages: [] 166 | // }, 167 | // privateKey: CryptoKey { 168 | // type: 'private', 169 | // extractable: true, 170 | // algorithm: { name: 'ECDH', namedCurve: 'P-521' }, 171 | // usages: [ 'deriveKey' ] 172 | // } 173 | // } 174 | ``` 175 | 176 | #### `getPublicKey` 177 | Given a private key, returns its corresponding public key. As there is no direct API for this in WebCrypto, this utilizes import and export of the key (in `jwk` format), while removing the private key parts. 178 | 179 | ```js 180 | const { privateKey } = await wcb.generateKeyPair() 181 | const publicKey = await wcb.getPublicKey(privateKey) 182 | // CryptoKey { 183 | // type: 'public', 184 | // extractable: true, 185 | // algorithm: { name: 'ECDH', namedCurve: 'P-521' }, 186 | // usages: [] 187 | // } 188 | ``` 189 | 190 | #### `deriveKey` 191 | Given a private and a public key, this function derives an aes-cbc key. For two key pairs `A` and `B`, the derived key will be the same for 192 | `deriveKey({ privateKey: A.privateKey, publicKey: B.publicKey })` and `deriveKey({ privateKey: B.privateKey, publicKey: A.publicKey })`. 193 | 194 | ```js 195 | const { privateKey } = await wcb.generateKeyPair() 196 | const { publicKey } = await wcb.generateKeyPair() 197 | const key = await wcb.deriveKey({ privateKey, publicKey }) 198 | // CryptoKey { 199 | // type: 'secret', 200 | // extractable: true, 201 | // algorithm: { name: 'AES-GCM', length: 256 }, 202 | // usages: [ 'encrypt', 'decrypt' ] 203 | // } 204 | ``` 205 | 206 | #### `deriveBits` 207 | Given a key pair, this function derives 16 bytes in an ArrayBuffer. 208 | 209 | ```js 210 | const { privateKey, publicKey } = await wcb.generateKeyPair() 211 | const key = await wcb.deriveBits({ privateKey, publicKey }) 212 | // ArrayBuffer { 213 | // [Uint8Contents]: , 214 | // byteLength: 16 215 | // } 216 | ``` 217 | 218 | #### `derivePassword` 219 | Given a key pair, this function derives a password of given length as an ArrayBuffer. 220 | 221 | ```js 222 | const { privateKey, publicKey } = await wcb.generateKeyPair() 223 | const key = await wcb.derivePassword({ privateKey, publicKey, length: 16 }) 224 | // ArrayBuffer { 225 | // [Uint8Contents]: , 226 | // byteLength: 16 227 | // } 228 | ``` 229 | 230 | 231 | ### Fingerprinting 232 | Methods for calculating fingerprints of public keys. Note that the fingerprints differ from the fingerprints from `ssh-keygen`. 233 | 234 | #### `sha256Fingerprint` 235 | Calculate a SHA-256 fingerprint of a key. It has a length of 64 hex chars. 236 | 237 | ```js 238 | const { publicKey } = await wcb.generateKeyPair() 239 | const fingerprintBits = await wcb.sha256Fingerprint(publicKey) 240 | const fingerprint = wcb.encodeHex(fingerprintBits) 241 | // aca8f766cdef8346177987a86b0f04b14fd4060b0e2478f941adc91982d6668c 242 | ``` 243 | 244 | #### `sha1Fingerprint` 245 | Calculate a SHA-1 fingerprint of a key. It has a length of 40 hex chars. 246 | 247 | ```js 248 | const { publicKey } = await wcb.generateKeyPair() 249 | const fingerprintBits = await wcb.sha1Fingerprint(publicKey) 250 | const fingerprint = wcb.encodeHex(fingerprintBits) 251 | // d04f73b7eb0b865a8d4711b5a379273a27c65581 252 | ``` 253 | 254 | 255 | ### Key Import and Export 256 | Tools for exchanging keys. Also comes with convenient helpers to deal with PEM formatted keys. 257 | 258 | #### `exportKey` 259 | Exports aes key data as ArrayBuffer: 260 | 261 | ```js 262 | const key = await wcb.generateKey() 263 | const data = await wcb.exportKey(key) 264 | // ArrayBuffer { 265 | // [Uint8Contents]: , 266 | // byteLength: 32 267 | // } 268 | ``` 269 | 270 | #### `importKey` 271 | Import aes key data, returns CryptoKey: 272 | 273 | ```js 274 | const data = new Uint8Array([ 275 | 210, 29, 179, 47, 204, 90, 109, 111, 95, 64, 50, 48, 192, 105, 44, 236, 276 | 74, 120, 2, 193, 83, 122, 22, 99, 202, 73, 20, 23, 187, 160, 140, 112 277 | ]) 278 | const key = await wcb.importKey(data) 279 | // CryptoKey { 280 | // type: 'secret', 281 | // extractable: true, 282 | // algorithm: { name: 'AES-GCM', length: 256 }, 283 | // usages: [ 'encrypt', 'decrypt' ] 284 | // } 285 | ``` 286 | 287 | #### `exportPublicKeyPem` 288 | Utility function to export a public key as pem: 289 | 290 | ```js 291 | const { publicKey } = await wcb.generateKeyPair() 292 | const pem = await wcb.exportPublicKeyPem(publicKey) 293 | // -----BEGIN PUBLIC KEY----- 294 | // MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBdjbvVyj7NglRaHLqgn2l+Rcw1Lev 295 | // 50/9xL6qvIwYSv84jU6xLOMJGY7nouU0tuWmm1+ojHsd3raDfxjsNSmKSOEAbSFJ 296 | // 1rnRvBU/DflEh0i/RofX4vmKH3quCKPQ8T1NQoQijyKEOjkQDFqDgpPW03SMusqs 297 | // d9/kSDNOMLm+EAIA6C0= 298 | // -----END PUBLIC KEY----- 299 | ``` 300 | 301 | #### `importPublicKeyPem` 302 | Given a pem of a public key, returns the CryptoKey: 303 | 304 | ```js 305 | const pem = `-----BEGIN PUBLIC KEY----- 306 | MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBdjbvVyj7NglRaHLqgn2l+Rcw1Lev 307 | 50/9xL6qvIwYSv84jU6xLOMJGY7nouU0tuWmm1+ojHsd3raDfxjsNSmKSOEAbSFJ 308 | 1rnRvBU/DflEh0i/RofX4vmKH3quCKPQ8T1NQoQijyKEOjkQDFqDgpPW03SMusqs 309 | d9/kSDNOMLm+EAIA6C0= 310 | -----END PUBLIC KEY-----` 311 | const publicKey = await wcb.importPublicKeyPem(pem) 312 | // CryptoKey { 313 | // type: 'public', 314 | // extractable: true, 315 | // algorithm: { name: 'ECDH', namedCurve: 'P-521' }, 316 | // usages: [] 317 | // } 318 | ``` 319 | 320 | #### `exportPrivateKeyPem` 321 | Utility function to export a private key as pem: 322 | 323 | ```js 324 | const { privateKey } = await wcb.generateKeyPair() 325 | const pem = await wcb.exportPrivateKeyPem(privateKey) 326 | // -----BEGIN PRIVATE KEY----- 327 | // MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBcf8zEjlssqn4aTEB 328 | // RR43ofwH/4BAXDAAd83Kz1Dyd+Ko0pit4ESgqSu/bJMdnDrpiGYuz0Klarwip8LD 329 | // rYd9mEahgYkDgYYABAF2Nu9XKPs2CVFocuqCfaX5FzDUt6/nT/3Evqq8jBhK/ziN 330 | // TrEs4wkZjuei5TS25aabX6iMex3etoN/GOw1KYpI4QBtIUnWudG8FT8N+USHSL9G 331 | // h9fi+Yofeq4Io9DxPU1ChCKPIoQ6ORAMWoOCk9bTdIy6yqx33+RIM04wub4QAgDo 332 | // LQ== 333 | // -----END PRIVATE KEY----- 334 | ``` 335 | 336 | #### `importPrivateKeyPem` 337 | Given a pem of a private key, returns the CryptoKey: 338 | 339 | ```js 340 | const pem = `-----BEGIN PRIVATE KEY----- 341 | MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBcf8zEjlssqn4aTEB 342 | RR43ofwH/4BAXDAAd83Kz1Dyd+Ko0pit4ESgqSu/bJMdnDrpiGYuz0Klarwip8LD 343 | rYd9mEahgYkDgYYABAF2Nu9XKPs2CVFocuqCfaX5FzDUt6/nT/3Evqq8jBhK/ziN 344 | TrEs4wkZjuei5TS25aabX6iMex3etoN/GOw1KYpI4QBtIUnWudG8FT8N+USHSL9G 345 | h9fi+Yofeq4Io9DxPU1ChCKPIoQ6ORAMWoOCk9bTdIy6yqx33+RIM04wub4QAgDo 346 | LQ== 347 | -----END PRIVATE KEY-----` 348 | const privateKey = await wcb.importPrivateKeyPem(pem) 349 | // CryptoKey { 350 | // type: 'private', 351 | // extractable: true, 352 | // algorithm: { name: 'ECDH', namedCurve: 'P-521' }, 353 | // usages: [ 'deriveKey' ] 354 | // } 355 | ``` 356 | 357 | #### `exportEncryptedPrivateKeyPem` 358 | Encrypt a private key with passphrase and export as PEM: 359 | 360 | ```js 361 | const { privateKey } = await wcb.generateKeyPair() 362 | const passphrase = 'secure' 363 | const pem = await wcb.exportEncryptedPrivateKeyPem({ key: privateKey, passphrase }) 364 | // -----BEGIN ENCRYPTED PRIVATE KEY----- 365 | // MIIBZjBgBgkqhkiG9w0BBQ0wUzAyBgkqhkiG9w0BBQwwJQQQi9FqU3dish14EV99 366 | // Bz3tugIDAPoAMAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBK8mQ193fArwsz 367 | // PKt2SvCkBIIBAOKBG4NQBWNDUUxSTJAMy5XnOU6nnX+Sisb9uu8/bAhxRtn3ItTo 368 | // vGCs2MxtTKQhBRC5WdjU7oEe5rZAsWfoYdb567hPnl19QRaf2cneTNHT5qDdzRF+ 369 | // PrLRyw2+XUDEeeU5vhC4E29LZgigeYdSd2r8fOOcJxrKmMzkyFJCrYFhoqpdw2IS 370 | // 4FQgJ9axQ2AncSaTqbuhBFQcoIFMrJ21ncVeEtTHS3428RHQJF1czNb/qnj/uIg7 371 | // ta5OqDeXseEgnF+StrDcnSjkSuqMqXVeKsduZd/5JZz25sbHgLBzRgiqf/1jyrrl 372 | // j8CC5qXX2PQH6RKBTys/1fRY++y3OVALhb8= 373 | // -----END ENCRYPTED PRIVATE KEY----- 374 | ``` 375 | 376 | #### `importEncryptedPrivateKeyPem` 377 | Decrypt passphrase encrypted private key pem and import the key: 378 | 379 | ```js 380 | const pem = `-----BEGIN ENCRYPTED PRIVATE KEY----- 381 | MIIBZjBgBgkqhkiG9w0BBQ0wUzAyBgkqhkiG9w0BBQwwJQQQi9FqU3dish14EV99 382 | Bz3tugIDAPoAMAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBK8mQ193fArwsz 383 | PKt2SvCkBIIBAOKBG4NQBWNDUUxSTJAMy5XnOU6nnX+Sisb9uu8/bAhxRtn3ItTo 384 | vGCs2MxtTKQhBRC5WdjU7oEe5rZAsWfoYdb567hPnl19QRaf2cneTNHT5qDdzRF+ 385 | PrLRyw2+XUDEeeU5vhC4E29LZgigeYdSd2r8fOOcJxrKmMzkyFJCrYFhoqpdw2IS 386 | 4FQgJ9axQ2AncSaTqbuhBFQcoIFMrJ21ncVeEtTHS3428RHQJF1czNb/qnj/uIg7 387 | ta5OqDeXseEgnF+StrDcnSjkSuqMqXVeKsduZd/5JZz25sbHgLBzRgiqf/1jyrrl 388 | j8CC5qXX2PQH6RKBTys/1fRY++y3OVALhb8= 389 | -----END ENCRYPTED PRIVATE KEY-----` 390 | const passphrase = 'secure' 391 | const privateKey = await wcb.importEncryptedPrivateKeyPem({ pem, passphrase }) 392 | // CryptoKey { 393 | // type: 'private', 394 | // extractable: true, 395 | // algorithm: { name: 'ECDH', namedCurve: 'P-521' }, 396 | // usages: [ 'deriveKey' ] 397 | // } 398 | ``` 399 | 400 | #### `exportEncryptedPrivateKeyPemTo` 401 | Encrypt a private key with key pair and export as PEM: 402 | 403 | ```js 404 | const share = await wcb.generateKeyPair() 405 | const alice = await wcb.generateKeyPair() 406 | const bob = await wcb.generateKeyPair() 407 | const pem = await wcb.exportEncryptedPrivateKeyPem({ 408 | key: share.privateKey, 409 | privateKey: alice.privateKey, 410 | publicKey: bob.publicKey 411 | }) 412 | // -----BEGIN ENCRYPTED PRIVATE KEY----- 413 | // MIIBZjBgBgkqhkiG9w0BBQ0wUzAyBgkqhkiG9w0BBQwwJQQQPjQ7/lIPfbHQQHGi 414 | // QqXaJgIDAPoAMAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBCI371u15dp+o/N 415 | // Iqq+O3DQBIIBAJ+hnDVdydYcKYTmmhhUmwybqNkWEWi9pG4un5Xf7bEtm2A2qzoi 416 | // 73XnmHPfXW+435RgMbLRtJOxqDa519kRvedO1nNIw1Iycs9GynTar+D+fBE/tFmJ 417 | // 66XwlhKcKe0zMtSoi4FnkeyueEMYpJ7UDx+zVABqwwZdSFUrraeg6g/ljL1SGslg 418 | // xDhTEyULoLyYV4G1+2t+rRXdzr408v6AAi+fJh/iBiwqd6clc+oNW0iXxHsi5/nH 419 | // kETXf3RmXRonS7Ema+zhe3hMGlGGV1OMixaUZHGiIB6zc4fHHyzb5ippXEDkZ6e3 420 | // U+l5Po65rsFkaAcDupfN18Ez9FRAyYbNkz8= 421 | // -----END ENCRYPTED PRIVATE KEY----- 422 | ``` 423 | 424 | #### `importEncryptedPrivateKeyPemFrom` 425 | Decrypts encrypted private key PEM with key pair: 426 | 427 | ```js 428 | // alice and bob from above 429 | const privateKey = await wcb.importEncryptedPrivateKeyPemFrom({ 430 | pem, 431 | privateKey: bob.privateKey, 432 | publicKey: alice.publicKey 433 | }) 434 | // CryptoKey { 435 | // type: 'private', 436 | // extractable: true, 437 | // algorithm: { name: 'ECDH', namedCurve: 'P-521' }, 438 | // usages: [ 'deriveKey' ] 439 | // } 440 | ``` 441 | 442 | 443 | ### Encryption and Decryption 444 | Encrypt and decrypt a message with aes-cbc, and with ECDH key pairs. 445 | 446 | #### `encrypt` 447 | Encrypts a message with aes-cbc: 448 | 449 | ```js 450 | const key = await wcb.generateKey() 451 | const text = 'my message' 452 | const message = decodeText(text) 453 | const data = await wcb.encrypt({ message, key }) 454 | // ArrayBuffer { 455 | // [Uint8Contents]: <95 e1 e9 d4 72 74 27 6b b3 e3 e3 79 9e c3 dd f0 8a ... more bytes>, 456 | // byteLength: 26 457 | // } 458 | ``` 459 | 460 | #### `decrypt` 461 | Decrypts a message: 462 | 463 | ```js 464 | const key = await wcb.generateKey() 465 | 466 | const text = 'my message' 467 | const message = decodeText(text) 468 | const box = await wcb.encrypt({ message, key }) 469 | 470 | const data = await wcb.decrypt({ box, key }) 471 | // ArrayBuffer { 472 | // [Uint8Contents]: <6d 79 20 6d 65 73 73 61 67 65>, 473 | // byteLength: 10 474 | // } 475 | ``` 476 | 477 | #### `encryptoTo` 478 | Encrypts a message with aes-cbc for given private and public ecdh key: 479 | 480 | ```js 481 | const { privateKey, publicKey } = await wcb.generateKeyPair() 482 | const text = 'my message' 483 | const message = decodeText(text) 484 | const data = await wcb.encryptoTo({ message, privateKey, publicKey }) 485 | // ArrayBuffer { 486 | // [Uint8Contents]: , 487 | // byteLength: 26 488 | // } 489 | ``` 490 | 491 | #### `decryptFrom` 492 | Decrypts a message for given private and public ecdh key: 493 | 494 | ```js 495 | const { privateKey, publicKey } = await wcb.generateKeyPair() 496 | 497 | const text = 'my message' 498 | const message = decodeText(text) 499 | const box = await wcb.encryptoTo({ message, privateKey, publicKey }) 500 | 501 | const data = await wcb.decryptFrom({ box, privateKey, publicKey }) 502 | // ArrayBuffer { 503 | // [Uint8Contents]: <6d 79 20 6d 65 73 73 61 67 65>, 504 | // byteLength: 10 505 | // } 506 | ``` 507 | 508 | 509 | ## Test 510 | There's a little test suite which ensures the lib works as expected. You can run it either directly: 511 | ```sh 512 | node test/utils.js 513 | node test/webcryptobox.js 514 | ``` 515 | 516 | or via npm: 517 | ```sh 518 | npm test 519 | ``` 520 | 521 | 522 | ## License 523 | This package is licensed under the [Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0). 524 | 525 | © 2022 Johannes J. Schmidt 526 | --------------------------------------------------------------------------------