Browse Source

Merge branch 'development'

Kieran Gibb 2 years ago
parent
commit
5d37bab35c
No known key found for this signature in database
3 changed files with 132 additions and 7 deletions
  1. 20
    0
      README.md
  2. 79
    5
      index.js
  3. 33
    2
      test/index.test.js

+ 20
- 0
README.md View File

@@ -64,3 +64,23 @@ feed.ready(() => {
64 64
   })
65 65
 })
66 66
 ```
67
+
68
+```js
69
+const { publicKey, secretKey } = boxKeypair(seed)
70
+```
71
+
72
+```js
73
+const boxed = box(publicKey, message, [contextMessage])
74
+```
75
+Encrypts a message to a given public key and returns it as a buffer
76
+- `publicKey` buffer or hex encoded string
77
+- `message` buffer or hex encoded string of any length
78
+- `contextMessage`, if passed, will be hashed in to the shared secret. Should be a buffer or hex encoded string.
79
+
80
+```js
81
+const unboxed = unbox(cipherText, keypair, [contextMessage])
82
+```
83
+Decrypts a message using the given keypair.
84
+- `cipherText` the encrypted message given as a buffer.
85
+- `keypair` an object of the form `{ publicKey, secretKey }` both of which should be buffers or hex encoded strings.
86
+- `contextMessage`, if given, will be hashed into the shared secret. Should be a buffer or hex encoded string.

+ 79
- 5
index.js View File

@@ -2,8 +2,13 @@ const sodium = require('sodium-native')
2 2
 const crypto = require('hypercore-crypto')
3 3
 const assert = require('assert')
4 4
 const bip39 = require('bip39')
5
+const zero = sodium.sodium_memzero
5 6
 
6 7
 class Crypto {
8
+  constructor () {
9
+    this.PACKEDLENGTH = sodium.crypto_sign_PUBLICKEYBYTES + sodium.crypto_secretbox_KEYBYTES
10
+  }
11
+
7 12
   randomBytes (length) {
8 13
     return crypto.randomBytes(length)
9 14
   }
@@ -21,16 +26,17 @@ class Crypto {
21 26
   encryptionKey (encryptionKey) {
22 27
     var key = sodium.sodium_malloc(sodium.crypto_secretbox_KEYBYTES)
23 28
     if (encryptionKey && Buffer.isBuffer(encryptionKey)) key.copy(encryptionKey)
24
-    else if (encryptionKey && typeof encryptionKey === 'string') key.write(encryptionKey)
29
+    else if (encryptionKey && typeof encryptionKey === 'string') key.write(encryptionKey, 'hex')
25 30
     else sodium.randombytes_buf(key)
26 31
     return key
27 32
   }
28 33
 
29
-  keyPair (masterKey, id, ctxt = 'cobox') {
34
+  keyPair (masterKey, id, ctxt = 'coboxcobox') {
30 35
     const context = sodium.sodium_malloc(sodium.crypto_hash_sha256_BYTES)
31 36
     sodium.crypto_hash_sha256(context, Buffer.from(ctxt))
32 37
     const seed = sodium.sodium_malloc(sodium.crypto_kdf_KEYBYTES)
33 38
     sodium.crypto_kdf_derive_from_key(seed, id, context, masterKey)
39
+    zero(context)
34 40
     return crypto.keyPair(seed)
35 41
   }
36 42
 
@@ -48,14 +54,14 @@ class Crypto {
48 54
   pack (address, encryptionKey) {
49 55
     address = this.toBuffer(address, sodium.crypto_sign_PUBLICKEYBYTES)
50 56
     encryptionKey = this.toBuffer(encryptionKey, sodium.crypto_secretbox_KEYBYTES)
51
-    const accessKey = sodium.sodium_malloc(sodium.crypto_sign_PUBLICKEYBYTES + sodium.crypto_secretbox_KEYBYTES)
57
+    const accessKey = sodium.sodium_malloc(this.PACKEDLENGTH)
52 58
     address.copy(accessKey)
53 59
     encryptionKey.copy(accessKey, sodium.crypto_secretbox_KEYBYTES)
54 60
     return accessKey
55 61
   }
56 62
 
57 63
   unpack (key) {
58
-    key = this.toBuffer(key, [sodium.crypto_sign_PUBLICKEYBYTES, sodium.crypto_sign_PUBLICKEYBYTES + sodium.crypto_secretbox_KEYBYTES])
64
+    key = this.toBuffer(key, [sodium.crypto_sign_PUBLICKEYBYTES, this.PACKEDLENGTH])
59 65
 
60 66
     if (key.length === sodium.crypto_sign_PUBLICKEYBYTES) return { address: key }
61 67
 
@@ -112,6 +118,7 @@ class Crypto {
112 118
         var nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES)
113 119
         sodium.randombytes_buf(nonce)
114 120
         sodium.crypto_secretbox_easy(ciphertext, message, nonce, encryptionKey)
121
+        zero(message)
115 122
         return Buffer.concat([nonce, ciphertext])
116 123
       },
117 124
 
@@ -133,6 +140,74 @@ class Crypto {
133 140
     }
134 141
   }
135 142
 
143
+  boxKeypair (seed) {
144
+    const publicKey = sodium.sodium_malloc(sodium.crypto_box_PUBLICKEYBYTES)
145
+    const secretKey = sodium.sodium_malloc(sodium.crypto_box_SECRETKEYBYTES)
146
+    if (seed) {
147
+      seed = this.toBuffer(seed, sodium.crypto_box_SEEDBYTES)
148
+      sodium.crypto_box_seed_keypair(publicKey, secretKey, seed)
149
+      zero(seed)
150
+    } else {
151
+      sodium.crypto_box_keypair(publicKey, secretKey)
152
+    }
153
+    return { publicKey, secretKey }
154
+  }
155
+
156
+  box (pubKey, messageBuffer, contextMessage = Buffer.from('cobox')) {
157
+    pubKey = this.toBuffer(pubKey, sodium.crypto_box_PUBLICKEYBYTES)
158
+    if (typeof messageBuffer === 'string') messageBuffer = Buffer.from(messageBuffer)
159
+    if (typeof contextMessage === 'string') contextMessage = Buffer.from(contextMessage)
160
+    var boxed = Buffer.alloc(messageBuffer.length + sodium.crypto_secretbox_MACBYTES)
161
+    const ephKeypair = this.boxKeypair()
162
+    const nonce = this.randomBytes(sodium.crypto_secretbox_NONCEBYTES)
163
+    var sharedSecret = this.genericHash(
164
+      Buffer.concat([ephKeypair.publicKey, pubKey, contextMessage]),
165
+      this.genericHash(this.scalarMult(ephKeypair.secretKey, pubKey)))
166
+
167
+    sodium.crypto_secretbox_easy(boxed, messageBuffer, nonce, sharedSecret)
168
+
169
+    zero(sharedSecret)
170
+    zero(ephKeypair.secretKey)
171
+    return Buffer.concat([nonce, ephKeypair.publicKey, boxed])
172
+  }
173
+
174
+  unbox (cipherText, keypair, contextMessage = Buffer.from('cobox')) {
175
+    keypair.publicKey = this.toBuffer(keypair.publicKey, sodium.crypto_box_PUBLICKEYBYTES)
176
+    keypair.secretKey = this.toBuffer(keypair.secretKey, sodium.crypto_box_SECRETKEYBYTES)
177
+    if (typeof contextMessage === 'string') contextMessage = Buffer.from(contextMessage)
178
+    const NONCEBYTES = sodium.crypto_secretbox_NONCEBYTES
179
+    const KEYBYTES = sodium.crypto_secretbox_KEYBYTES
180
+    try {
181
+      var nonce = cipherText.slice(0, NONCEBYTES)
182
+      var pubKey = cipherText.slice(NONCEBYTES, NONCEBYTES + KEYBYTES)
183
+      var box = cipherText.slice(NONCEBYTES + KEYBYTES, cipherText.length)
184
+      var unboxed = Buffer.alloc(box.length - sodium.crypto_secretbox_MACBYTES)
185
+    } catch (err) {
186
+      return false
187
+    }
188
+    const sharedSecret = this.genericHash(
189
+      Buffer.concat([pubKey, keypair.publicKey, contextMessage]),
190
+      this.genericHash(this.scalarMult(keypair.secretKey, pubKey)))
191
+
192
+    const success = sodium.crypto_secretbox_open_easy(unboxed, box, nonce, sharedSecret)
193
+    zero(sharedSecret)
194
+    zero(keypair.secretKey)
195
+    zero(keypair.publicKey)
196
+    return success ? unboxed : false
197
+  }
198
+
199
+  genericHash (msg, key) {
200
+    var hash = sodium.sodium_malloc(sodium.crypto_generichash_BYTES_MAX)
201
+    sodium.crypto_generichash(hash, msg, key)
202
+    return hash
203
+  }
204
+
205
+  scalarMult (sk, pk) {
206
+    var result = sodium.sodium_malloc(sodium.crypto_scalarmult_BYTES)
207
+    sodium.crypto_scalarmult(result, sk, pk)
208
+    return result
209
+  }
210
+
136 211
   _resolveStringEncoder (encoder) {
137 212
     if (encoder === 'json') return {
138 213
       encode: (msg) => Buffer.from(JSON.stringify(msg)),
@@ -148,4 +223,3 @@ class Crypto {
148 223
 }
149 224
 
150 225
 module.exports = new Crypto()
151
-

+ 33
- 2
test/index.test.js View File

@@ -2,6 +2,7 @@ const { describe } = require('tape-plus')
2 2
 const hypercore = require('hypercore')
3 3
 const path = require('path')
4 4
 const fs = require('fs')
5
+const sodium = require('sodium-native')
5 6
 
6 7
 const crypto = require('../')
7 8
 
@@ -31,16 +32,20 @@ describe('key generation', (context) => {
31 32
   })
32 33
 
33 34
   context('encryptionKey(string)', (assert, next) => {
34
-    const key = crypto.encryptionKey(crypto.randomBytes(32).toString('hex'))
35
+    const bytes = crypto.randomBytes(32).toString('hex')
36
+    const key = crypto.encryptionKey(bytes)
35 37
     assert.ok(key, 'Key successfully generated')
36 38
     assert.ok(key instanceof Buffer, 'key is a secure buffer')
39
+    assert.same(bytes, key.toString('hex'), 'keys match')
37 40
     next()
38 41
   })
39 42
 
40 43
   context('encryptionKey(buffer)', (assert, next) => {
41
-    const key = crypto.encryptionKey(crypto.randomBytes(32))
44
+    const bytes = crypto.randomBytes(32)
45
+    const key = crypto.encryptionKey(bytes)
42 46
     assert.ok(key, 'Key successfully generated')
43 47
     assert.ok(key instanceof Buffer, 'key is a secure buffer')
48
+    assert.same(bytes.toString('hex'), key.toString('hex'), 'keys match')
44 49
     next()
45 50
   })
46 51
 
@@ -189,3 +194,29 @@ describe('hypercore', (context) => {
189 194
     })
190 195
   })
191 196
 })
197
+
198
+describe('box and unbox', (context) => {
199
+  context.beforeEach((c) => {
200
+  })
201
+
202
+  context('boxkeypair', (assert, next) => {
203
+    const { publicKey, secretKey } = crypto.boxKeypair()
204
+    assert.ok(Buffer.isBuffer(publicKey), 'public key generated')
205
+    assert.ok(Buffer.isBuffer(secretKey), 'secret key generated')
206
+    const seed = crypto.randomBytes(sodium.crypto_box_SEEDBYTES)
207
+    const seedKeypair = crypto.boxKeypair(seed)
208
+    assert.ok(Buffer.isBuffer(seedKeypair.publicKey), 'public key generated from seed')
209
+    assert.ok(Buffer.isBuffer(seedKeypair.secretKey), 'secret key generated from seed')
210
+    next()
211
+  })
212
+
213
+  context('box and unbox', (assert, next) => {
214
+    const { publicKey, secretKey } = crypto.boxKeypair()
215
+    const msg = 'beep boop'
216
+    const boxedMsg = crypto.box(publicKey, Buffer.from(msg))
217
+    assert.true(boxedMsg.toString('hex') !== msg.toString('hex'), 'boxed message !== message')
218
+    const unboxedMsg = crypto.unbox(boxedMsg, { publicKey, secretKey })
219
+    assert.same(unboxedMsg.toString(), msg, 'message decrypted successfully')
220
+    next()
221
+  })
222
+})