git ssb

1+

Dominic / ssb-private-groups



Commit d62aabe95512e6a3e72bd54fe3021aa77a550401

progress, but something is broken

Dominic Tarr committed on 12/27/2018, 11:23:22 AM
Parent: 86f3b92eaac8f991e0ec002cb08d0b0120608e21

Files changed

index.jschanged
package.jsonchanged
test/index.jsadded
util.jsadded
index.jsView
@@ -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')
68
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-
159 //by deriving the message key from the group id (the founding
1610 //message id) and the unbox key for that message, this ensures
1711 //someone can't decrypt the message without knowing the founding
1812 //message, therefore avoiding surruptious sharing the group.
@@ -26,20 +20,29 @@
2620 function getGroupMsgKey(previous, group) {
2721 return hmac(Buffer.concat([previous, group.id]), group.unbox)
2822 }
2923
30-exports.init = function () {
24 +exports.name = 'private-groups'
3125
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'))
3531 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 + })
4038 })
4139
40 + function onReady (fn) {
41 + if(ready) fn()
42 + else waiting.push(fn)
43 + }
44 +
4245 //state:
4346 /*
4447 {
4548 <author>: {
@@ -53,24 +56,25 @@
5356 //cache: {<author>: [scalar_mult(msg_keys[i], <author's latest privacy key>)]
5457 var cache = {}
5558
5659
57- sbot._flumeUse('private-groups/remote-keys', Reduce(1, function (acc, msg) {
60 + sbot._flumeUse('private-groups/remote-keys', Reduce(1, function (acc, data) {
5861 state = acc = acc || {}
62 + var msg = data.value
5963 if(msg.content.type === 'private-msg-key') {
6064 acc[msg.author] = [{sequence: msg.sequence, key: msg.content.key}]
6165 cache[msg.author] = null
6266 }
63- })
67 + }))
6468
6569 //sbot._flumeUse('private-groups/old-remote-keys', Level(1, function (data) {
6670 // if(msg.content.type === 'private-msg-key') {
6771 // return [msg.author, msg.sequence, msg.content.type]
6872 // }
6973 //})
7074
7175 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)
7377 //the views and keyState have not been loaded
7478 //delay processing any box2 messages until they are.
7579 if(ready) cb(null, data)
7680 else waiting.push(function () {
@@ -78,39 +82,77 @@
7882 })
7983 })
8084
8185 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
8793
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
93103
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 + })
99112
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)
107147 )
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 + }
113155 }
114156 })
115157
116158 return {
@@ -120,15 +162,18 @@
120162 // af.set(keys, cb)
121163 // })
122164 // },
123165 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 + })
131176 },
132177 //forgetting old keys. crude basis for forward secrecy.
133178 //you will no longer be able to decrypt messages sent to curve_pk
134179 forget: function (curve_pk, cb) {
@@ -147,5 +192,4 @@
147192
148193
149194
150195
151-
package.jsonView
@@ -7,13 +7,16 @@
77 "type": "git",
88 "url": "git://github.com/dominictarr/ssb-private-groups.git"
99 },
1010 "dependencies": {
11 + "atomic-file": "^1.1.5"
1112 },
1213 "devDependencies": {
14 + "ssb-server": "^13.4.0",
15 + "tape": "^4.9.1"
1316 },
1417 "scripts": {
1518 "test": "set -e; for t in test/*.js; do node $t; done"
1619 },
1720 "author": "Dominic Tarr <dominic.tarr@gmail.com> (http://dominictarr.com)",
18- "license": "MIT"
21 + "license": "MIT"
1922 }
test/index.jsView
@@ -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.jsView
@@ -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