Commit d62aabe95512e6a3e72bd54fe3021aa77a550401
progress, but something is broken
Dominic Tarr committed on 12/27/2018, 11:23:22 AMParent: 86f3b92eaac8f991e0ec002cb08d0b0120608e21
Files changed
index.js | changed |
package.json | changed |
test/index.js | added |
util.js | added |
index.js | |||
---|---|---|---|
@@ -1,18 +1,12 @@ | |||
1 | -function isBox2(ctxt) { | ||
2 | - //we can just do a fairly lax check here, don't check the content | ||
3 | - //is canonical base64, because that check has already been done. | ||
4 | - return 'string' == typeof ctxt && /\.box2$/.test(ctxt) | ||
5 | -} | ||
1 … | +var AtomicFile = require('atomic-file') | ||
2 … | +var path = require('path') | ||
3 … | +var Reduce = require('flumeview-reduce') | ||
4 … | +var group_box = require('group-box') | ||
5 … | +var mkdirp = require('mkdirp') | ||
6 … | +var u = require('./util') | ||
7 … | +var cl = require('chloride') | ||
6 | 8 … | ||
7 | -function ctxtToBuffer(ctxt) { | ||
8 | - return isBox2(ctxt) && Buffer.from(ctxt.substring(ctxt.indexOf('.')), 'base64') | ||
9 | -} | ||
10 | - | ||
11 | -function idToBuffer (id) { | ||
12 | - return Buffer.from(id.substring(1, id.indexOf('.')), 'base64') | ||
13 | -} | ||
14 | - | ||
15 | 9 … | //by deriving the message key from the group id (the founding | |
16 | 10 … | //message id) and the unbox key for that message, this ensures | |
17 | 11 … | //someone can't decrypt the message without knowing the founding | |
18 | 12 … | //message, therefore avoiding surruptious sharing the group. | |
@@ -26,20 +20,29 @@ | |||
26 | 20 … | function getGroupMsgKey(previous, group) { | |
27 | 21 … | return hmac(Buffer.concat([previous, group.id]), group.unbox) | |
28 | 22 … | } | |
29 | 23 … | ||
30 | -exports.init = function () { | ||
24 … | +exports.name = 'private-groups' | ||
31 | 25 … | ||
32 | - var af = AtomicFile( | ||
33 | - path.join(config.path, 'private-groups/local-keys.json') | ||
34 | - ) | ||
26 … | +exports.init = function (sbot, config) { | ||
27 … | + | ||
28 … | + var dir = path.join(config.path, 'private-groups') | ||
29 … | + | ||
30 … | + var af = AtomicFile(path.join(dir, 'local-keys.json')) | ||
35 | 31 … | var ready = false, waiting = [] | |
36 | - af.get(function (err, data) { | ||
37 | - keyState = data || {msgKeys: [], groupKeys: []} | ||
38 | - ready = true | ||
39 | - while(waiting.length) waiting.shift()() | ||
32 … | + mkdirp(dir, function () { | ||
33 … | + af.get(function (err, data) { | ||
34 … | + keyState = data || {msgKeys: [], groupKeys: []} | ||
35 … | + ready = true | ||
36 … | + while(waiting.length) waiting.shift()() | ||
37 … | + }) | ||
40 | 38 … | }) | |
41 | 39 … | ||
40 … | + function onReady (fn) { | ||
41 … | + if(ready) fn() | ||
42 … | + else waiting.push(fn) | ||
43 … | + } | ||
44 … | + | ||
42 | 45 … | //state: | |
43 | 46 … | /* | |
44 | 47 … | { | |
45 | 48 … | <author>: { | |
@@ -53,24 +56,25 @@ | |||
53 | 56 … | //cache: {<author>: [scalar_mult(msg_keys[i], <author's latest privacy key>)] | |
54 | 57 … | var cache = {} | |
55 | 58 … | ||
56 | 59 … | ||
57 | - sbot._flumeUse('private-groups/remote-keys', Reduce(1, function (acc, msg) { | ||
60 … | + sbot._flumeUse('private-groups/remote-keys', Reduce(1, function (acc, data) { | ||
58 | 61 … | state = acc = acc || {} | |
62 … | + var msg = data.value | ||
59 | 63 … | if(msg.content.type === 'private-msg-key') { | |
60 | 64 … | acc[msg.author] = [{sequence: msg.sequence, key: msg.content.key}] | |
61 | 65 … | cache[msg.author] = null | |
62 | 66 … | } | |
63 | - }) | ||
67 … | + })) | ||
64 | 68 … | ||
65 | 69 … | //sbot._flumeUse('private-groups/old-remote-keys', Level(1, function (data) { | |
66 | 70 … | // if(msg.content.type === 'private-msg-key') { | |
67 | 71 … | // return [msg.author, msg.sequence, msg.content.type] | |
68 | 72 … | // } | |
69 | 73 … | //}) | |
70 | 74 … | ||
71 | 75 … | sbot.addMap(function (data, cb) { | |
72 | - if(!isBox2(data.value.content)) return cb(null, data) | ||
76 … | + if(!u.isBox2(data.value.content)) return cb(null, data) | ||
73 | 77 … | //the views and keyState have not been loaded | |
74 | 78 … | //delay processing any box2 messages until they are. | |
75 | 79 … | if(ready) cb(null, data) | |
76 | 80 … | else waiting.push(function () { | |
@@ -78,39 +82,77 @@ | |||
78 | 82 … | }) | |
79 | 83 … | }) | |
80 | 84 … | ||
81 | 85 … | sbot.addUnboxer({ | |
82 | - key: | ||
83 | - function (content, value) { | ||
84 | - if(!isBox2(content)) return | ||
85 | - var a_state = state[value.author] | ||
86 | - if(!a_state) return | ||
86 … | + key: function (content, value) { | ||
87 … | + if(!u.isBox2(content)) return | ||
88 … | + //a_state is reverse chrono list of author's private-msg-keys | ||
89 … | + //take the latest key that has sequence less than message | ||
90 … | + //to decrypt | ||
91 … | + var a_state = state[value.author] | ||
92 … | + if(!a_state) return | ||
87 | 93 … | ||
88 | - var keys_to_try = cache[value.author] | ||
89 | - if(!keys_to_try) { | ||
90 | - keys_to_try = cache[value.author] = keyState.msgKeys.map(function (key) { | ||
91 | - return scalarmult(a_state.key, curve.private) | ||
92 | - }) | ||
94 … | + var keys_to_try = cache[value.author] | ||
95 … | + var a_key | ||
96 … | + for(var i = 0; i < a_state.length; i++) { | ||
97 … | + if(a_state[i].sequence < value.sequence) { | ||
98 … | + a_key = a_state[i].key | ||
99 … | + break; | ||
100 … | + } | ||
101 … | + } | ||
102 … | + if(!a_key) return | ||
93 | 103 … | ||
94 | - var ctxt = ctxtToBuffer(content), nonce = idToBuffer(value.previous) | ||
95 | - var key = groupbox.unboxKey( //direct recipients | ||
96 | - ctxt, nonce, keys_to_try, 8 | ||
97 | - ) | ||
98 | - if(key) return key | ||
104 … | + if(!keys_to_try) | ||
105 … | + keys_to_try = cache[value.author] = keyState.msgKeys.map(function (curve) { | ||
106 … | + console.log("A_KEY", a_key, curve) | ||
107 … | + return cl.crypto_scalarmult( | ||
108 … | + Buffer.from(curve.private, 'base64'), | ||
109 … | + Buffer.from(a_key, 'base64') | ||
110 … | + ) | ||
111 … | + }) | ||
99 | 112 … | ||
100 | - var group_keys = [] | ||
101 | - for(var id in keyState.groupKeys) | ||
102 | - group_keys.push(getGroupMsgKey(nonce, keyState.groupKeys[id]) | ||
103 | - //note: if we only allow groups in the first 4 slots | ||
104 | - //that means better sort them before any individuals | ||
105 | - key = groupbox.unboxKey( //groups we are in | ||
106 | - ctxt, nonce, group_keys, 4 | ||
113 … | + var ctxt = u.ctxt2Buffer(content), nonce = u.id2Buffer(value.previous) | ||
114 … | + // console.log('-CTXT', ctxt) | ||
115 … | + // console.log('-NONCE', nonce) | ||
116 … | +// console.log('-KEYS', keys_to_try) | ||
117 … | + var key = group_box.unboxKey( //direct recipients | ||
118 … | + ctxt, nonce, keys_to_try, 8 | ||
119 … | + ) | ||
120 … | + if(key) return key | ||
121 … | + | ||
122 … | + var group_keys = [] | ||
123 … | + for(var id in keyState.groupKeys) | ||
124 … | + group_keys.push(getGroupMsgKey(nonce, keyState.groupKeys[id])) | ||
125 … | + //note: if we only allow groups in the first 4 slots | ||
126 … | + //that means better sort them before any individuals | ||
127 … | + key = group_box.unboxKey( //groups we are in | ||
128 … | + ctxt, nonce, group_keys, 4 | ||
129 … | + ) | ||
130 … | + if(key) return key | ||
131 … | + }, | ||
132 … | + value: function (content, key, value) { | ||
133 … | + if(!u.isBox2(content)) return | ||
134 … | + console.log() | ||
135 … | + console.log('-------------:', value) | ||
136 … | + var ctxt = u.ctxt2Buffer(content) | ||
137 … | + var nonce = u.id2Buffer(value.previous) | ||
138 … | + console.log("INPUT", { | ||
139 … | + ctxt: ctxt.toString('hex'), | ||
140 … | + nonce: nonce, | ||
141 … | + key: key | ||
142 … | + }) | ||
143 … | + console.log( | ||
144 … | + 'INPUT_VALUE', | ||
145 … | + cl.crypto_hash_sha256(Buffer.concat([ctxt, nonce, key])), | ||
146 … | + group_box.unboxBody(ctxt, nonce, key) | ||
107 | 147 … | ) | |
108 | - if(key) return key | ||
109 | - }, | ||
110 | - value: function (content, key) { | ||
111 | - if(!isBox2(content)) return | ||
112 | - return groupbox.unboxBody(content, key) | ||
148 … | + var ptxt = group_box.unboxBody(ctxt, nonce, key) | ||
149 … | + if(ptxt) { | ||
150 … | + try { | ||
151 … | + console.log("CONTENT", JSON.parse(ptxt.toString())) | ||
152 … | + return JSON.parse(ptxt.toString()) | ||
153 … | + } catch (_) {} | ||
154 … | + } | ||
113 | 155 … | } | |
114 | 156 … | }) | |
115 | 157 … | ||
116 | 158 … | return { | |
@@ -120,15 +162,18 @@ | |||
120 | 162 … | // af.set(keys, cb) | |
121 | 163 … | // }) | |
122 | 164 … | // }, | |
123 | 165 … | addCurvePair: function (curve_keys, cb) { | |
124 | - if(!isCurvePair(curve_keys)) return cb(new Error('expected a pair of curve25519 keys') | ||
125 | - keyState.msgKeys.push(curve_keys) | ||
126 | - cache = {} //clear cache, so it's regenerated up to date. | ||
127 | - //NOTE: identiy adding this key must publish | ||
128 | - // a message advertising this receive key, or no one | ||
129 | - // will send them messages! | ||
130 | - af.set(msg_keys, cb) | ||
166 … | + onReady(function () { | ||
167 … | + if(!u.isCurvePair(curve_keys)) | ||
168 … | + return cb(new Error('expected a pair of curve25519 keys')) | ||
169 … | + keyState.msgKeys.push(curve_keys) | ||
170 … | + cache = {} //clear cache, so it's regenerated up to date. | ||
171 … | + //NOTE: identiy adding this key must publish | ||
172 … | + // a message advertising this receive key, or no one | ||
173 … | + // will send them messages! | ||
174 … | + af.set(keyState, cb) | ||
175 … | + }) | ||
131 | 176 … | }, | |
132 | 177 … | //forgetting old keys. crude basis for forward secrecy. | |
133 | 178 … | //you will no longer be able to decrypt messages sent to curve_pk | |
134 | 179 … | forget: function (curve_pk, cb) { | |
@@ -147,5 +192,4 @@ | |||
147 | 192 … | ||
148 | 193 … | ||
149 | 194 … | ||
150 | 195 … | ||
151 | - |
package.json | ||
---|---|---|
@@ -7,13 +7,16 @@ | ||
7 | 7 … | "type": "git", |
8 | 8 … | "url": "git://github.com/dominictarr/ssb-private-groups.git" |
9 | 9 … | }, |
10 | 10 … | "dependencies": { |
11 … | + "atomic-file": "^1.1.5" | |
11 | 12 … | }, |
12 | 13 … | "devDependencies": { |
14 … | + "ssb-server": "^13.4.0", | |
15 … | + "tape": "^4.9.1" | |
13 | 16 … | }, |
14 | 17 … | "scripts": { |
15 | 18 … | "test": "set -e; for t in test/*.js; do node $t; done" |
16 | 19 … | }, |
17 | 20 … | "author": "Dominic Tarr <dominic.tarr@gmail.com> (http://dominictarr.com)", |
18 | - "license": "MIT" | |
21 … | + "license": "MIT" | |
19 | 22 … | } |
test/index.js | ||
---|---|---|
@@ -1,0 +1,117 @@ | ||
1 … | +var chloride = require('chloride') | |
2 … | +var tape = require('tape') | |
3 … | +var group_box = require('group-box') | |
4 … | +var u = require('../util') | |
5 … | +var Scuttlebot = require('ssb-server') | |
6 … | + .use(require('../')) | |
7 … | + | |
8 … | +var alice = Scuttlebot({ | |
9 … | + temp: true | |
10 … | +}) | |
11 … | + | |
12 … | +var bob = alice.createFeed() | |
13 … | + | |
14 … | +function generate (seed) { | |
15 … | + var keys = chloride.crypto_box_seed_keypair(seed) | |
16 … | + return { | |
17 … | + public: keys.publicKey.toString('base64')+'.curve25519', | |
18 … | + private: keys.secretKey.toString('base64') | |
19 … | + } | |
20 … | +} | |
21 … | + | |
22 … | +function toBuffer(s) { | |
23 … | + return Buffer.isBuffer(s) ? s : Buffer.from(s, 'base64') | |
24 … | +} | |
25 … | + | |
26 … | +function scalarmult (pk,sk) { | |
27 … | + return chloride.crypto_scalarmult(toBuffer(pk), toBuffer(sk)) | |
28 … | +} | |
29 … | +function hash (s) { | |
30 … | + return chloride.crypto_hash_sha256(Buffer.from(s, 'utf8')) | |
31 … | +} | |
32 … | +var alice_keys = generate(hash('alice_secret')) | |
33 … | +var bob_keys = generate(hash('bob_secret')) | |
34 … | + | |
35 … | +tape('create a private-msg-key', function (t) { | |
36 … | + alice.privateGroups.addCurvePair(alice_keys, function (err) { | |
37 … | + if(err) throw err | |
38 … | + alice.publish({ | |
39 … | + type: 'private-msg-key', | |
40 … | + key: alice_keys.public | |
41 … | + }, function (err, msg) { | |
42 … | + if(err) throw err | |
43 … | + | |
44 … | + //bob doesn't call addCurvePair because bob is remote. | |
45 … | + //(we are just adding his feed directly so we don't | |
46 … | + // need to bother with replication) | |
47 … | + bob.publish({ | |
48 … | + type: 'private-msg-key', | |
49 … | + key: bob_keys.public | |
50 … | + }, function (err, data) { | |
51 … | + if(err) throw err | |
52 … | + console.log(data) | |
53 … | + t.ok(data.key) | |
54 … | + | |
55 … | + var content = { type: 'private', text: 'hello, alice' } | |
56 … | + var ptxt = Buffer.from(JSON.stringify(content)) | |
57 … | + var nonce = u.id2Buffer(data.key) | |
58 … | + var keys = [bob_keys, alice_keys].map(function (key) { | |
59 … | + return scalarmult(bob_keys.private, key.public) | |
60 … | + }) | |
61 … | + var keys2 = [bob_keys].map(function (key) { | |
62 … | + return scalarmult(alice_keys.private, key.public) | |
63 … | + }) | |
64 … | + console.log('plaintext.length', ptxt.length) | |
65 … | + | |
66 … | + var ctxt = group_box.box( | |
67 … | + ptxt, | |
68 … | + nonce, | |
69 … | + keys | |
70 … | + ) | |
71 … | +// console.log("CTXT", ctxt.toString('base64')) | |
72 … | +// console.log("NONCE", nonce) | |
73 … | +// console.log("KEYS", keys) | |
74 … | +// console.log("KEYS2", keys2) | |
75 … | + var _key = group_box.unboxKey(ctxt, nonce, keys, 8) | |
76 … | + console.log("INPUT", { | |
77 … | + ctxt: ctxt.toString('hex'), | |
78 … | + nonce: nonce, | |
79 … | + key: _key | |
80 … | + }) | |
81 … | + console.log( | |
82 … | + 'INPUT_TEST', | |
83 … | + chloride.crypto_hash_sha256(Buffer.concat([ctxt, nonce, _key])), | |
84 … | +group_box.unboxBody(ctxt, nonce, _key).toString() | |
85 … | + ) | |
86 … | + console.log('d.ptxt', group_box.unboxBody(ctxt, nonce, _key).toString()) | |
87 … | + | |
88 … | +// console.log('ctxt.length', ctxt.length) | |
89 … | + bob.publish( | |
90 … | + ctxt.toString('base64')+'.box2', | |
91 … | + function (err, data) { | |
92 … | + if(err) throw err | |
93 … | + t.ok(data) | |
94 … | +// console.log(data) | |
95 … | +// alice.privateGroups.get(function () { | |
96 … | + alice.get({id: data.key, private: true}, function (err, msg) { | |
97 … | + if(err) throw err | |
98 … | + t.deepEqual(msg.content, content) | |
99 … | + t.end() | |
100 … | + | |
101 … | + }) | |
102 … | + // }) | |
103 … | + } | |
104 … | + ) | |
105 … | + }) | |
106 … | + }) | |
107 … | + }) | |
108 … | +}) | |
109 … | + | |
110 … | +tape('cleanup', function (t) { | |
111 … | + alice.close() | |
112 … | + t.end() | |
113 … | +}) | |
114 … | + | |
115 … | + | |
116 … | + | |
117 … | + |
util.js | ||
---|---|---|
@@ -1,0 +1,19 @@ | ||
1 … | + | |
2 … | +exports.id2Buffer = function (id) { | |
3 … | + return Buffer.from(id.substring(1, id.indexOf('.')), 'base64') | |
4 … | +} | |
5 … | + | |
6 … | +exports.isBox2 = function (ctxt) { | |
7 … | + //we can just do a fairly lax check here, don't check the content | |
8 … | + //is canonical base64, because that check has already been done. | |
9 … | + return 'string' == typeof ctxt && /\.box2$/.test(ctxt) | |
10 … | +} | |
11 … | + | |
12 … | +exports.isCurvePair = function (keys) { | |
13 … | + return keys.public && keys.private | |
14 … | +} | |
15 … | + | |
16 … | +exports.ctxt2Buffer = function (ctxt) { | |
17 … | + return exports.isBox2(ctxt) && Buffer.from(ctxt.substring(0, ctxt.indexOf('.')), 'base64') | |
18 … | +} | |
19 … | + |
Built with git-ssb-web