├── .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 |
--------------------------------------------------------------------------------