Skip to content
This repository has been archived by the owner on Aug 9, 2021. It is now read-only.

Commit

Permalink
Merge pull request #727 from 3box/release/v1.17.0
Browse files Browse the repository at this point in the history
release v1.17.0
  • Loading branch information
zachferland authored Feb 14, 2020
2 parents f002cf8 + f837c4b commit 2b50f49
Show file tree
Hide file tree
Showing 14 changed files with 1,017 additions and 273 deletions.
5 changes: 5 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Release Notes

## v1.17.0 - 2020-02-13
* feat: confidential threads 🔒📫

Confidential threads are encrypted member only threads, used for private dms, group messages, etc

## v1.16.3 - 2020-02-13
* fix: getIPFS called same time or from different closures in browser
* fix: ghost chat member list, with no auth opens
Expand Down
14 changes: 14 additions & 0 deletions example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ <h3> Threads: </h3>
<input type="radio" name="ghostCheck" id='ghostCheck' value="true"> </br>
<button id="openThread" type="button" class="btn btn btn-primary" disabled=true>Open thread</button> </br></br>

<div id="confidentialThreads" style="display: none;">
<h3> Confidential Threads: </h3>
<h6> Create: </h6>
<input type="text" id="confThreadName" placeholder="Name of thread"></br>
<button id="createConfThread" type="button" class="btn btn btn-primary">Create thread</button> </br></br>
<h6> Join By Address: </h6>
<input type="text" id="confThreadAddress" placeholder="Address of thread"></br>
<button id="joinConfThread" type="button" class="btn btn btn-primary" >Join thread</button> </br></br>
</div>

<div id="threadModeration" style="display: none;">
<input type="text" id="threadMod" placeholder="Thread Moderator">
<button id="addThreadMod" type="button" class="btn btn btn-primary" >Add Thread Moderator</button></br>
Expand All @@ -137,6 +147,10 @@ <h5> Members: </h5>
<p>
<span id="threadACError"></span>
</p>
<h6> Thread Address: </h6>
<p>
<span id="threadAddress"></span>
</p>

<div id="posts" style="display: none;">
<h4> Posts: </h4>
Expand Down
58 changes: 42 additions & 16 deletions example/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ bauth.addEventListener('click', event => {
spacePub.innerHTML = `Public data in ${name}:`
spacePriv.innerHTML = `Private data in ${name}:`
spaceCtrl.style.display = 'block'
confidentialThreads.style.display = 'block'
updateSpaceData()
})
})
Expand Down Expand Up @@ -116,25 +117,29 @@ openThread.addEventListener('click', () => {
const membersBool = members.checked
const ghostBool = ghostCheck.checked
console.log('ghost?', ghostBool);
posts.style.display = 'block'
threadModeration.style.display = 'block'
if (members.checked) threadMembers.style.display = 'block'
displayThread(false)
if (ghostCheck.checked) {
addThreadMod.disabled = true
}
box.openThread(space, name, {firstModerator, members: membersBool, ghost: ghostBool}).then(thread => {
window.currentThread = thread
thread.onUpdate(() => {
updateThreadData()
})
thread.onNewCapabilities(() => {
updateThreadCapabilities()
})
if (window.currentThread._room == undefined) {
updateThreadData()
updateThreadCapabilities()
}
}).catch(updateThreadError)
box.openThread(space, name, {firstModerator, members: membersBool, ghost: ghostBool})
.then(registerThreadEvents)
.catch(updateThreadError)
})

joinConfThread.addEventListener('click', () => {
const address = confThreadAddress.value
displayThread(true)
box.spaces[window.currentSpace].joinThreadByAddress(address)
.then(registerThreadEvents)
.catch(updateThreadError)
})

createConfThread.addEventListener('click', () => {
const name = confThreadName.value
displayThread(true)
box.spaces[window.currentSpace].createConfidentialThread(name)
.then(registerThreadEvents)
.catch(updateThreadError)
})

addThreadMod.addEventListener('click', () => {
Expand All @@ -157,12 +162,33 @@ window.deletePost = (el) => {
}).catch(updateThreadError)
}

const registerThreadEvents = (thread) => {
window.currentThread = thread
thread.onUpdate(() => {
updateThreadData()
})
thread.onNewCapabilities(() => {
updateThreadCapabilities()
})
if (window.currentThread._room == undefined) {
updateThreadData()
updateThreadCapabilities()
}
}

const displayThread = (members) => {
posts.style.display = 'block'
threadModeration.style.display = 'block'
if (members) threadMembers.style.display = 'block'
}

const updateThreadError = (e = '') => {
threadACError.innerHTML = e
}

const updateThreadData = () => {
threadData.innerHTML = ''
threadAddress.innerHTML = window.currentThread.address
updateThreadError()
window.currentThread.getPosts().then(posts => {
posts.map(post => {
Expand Down
8 changes: 5 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "3box",
"version": "1.16.3",
"version": "1.17.0",
"description": "Interact with user data",
"main": "lib/3box.js",
"directories": {
Expand Down Expand Up @@ -43,7 +43,7 @@
},
"homepage": "https://github.com/3box/3box-js#readme",
"dependencies": {
"3box-orbitdb-plugins": "3box/3box-orbitdb-plugins#feat/orbit-23",
"3box-orbitdb-plugins": "^1.1.0",
"3id-blockchain-utils": "^0.3.2",
"3id-resolver": "^0.0.5",
"@babel/runtime": "^7.4.5",
Expand Down
50 changes: 38 additions & 12 deletions readme-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,13 @@ const config = await space.private.get('dapp-config')
```

## Threads API (Messaging)
### Add message threads to your app
Threads are a shared datastore that enable decentralized communication between users, by allowing one or more users to post messages in a sequence. This functionality is great for adding commenting, chat, messaging, feed, and stream features to your application. Threads are saved within a space and users that join a thread (with the same name, in the same space, and same moderation configs) will be able to communicate in that thread.
### Add public and confidential message threads to your app
Threads are a shared datastore that enable decentralized communication between users, by allowing one or more users to post messages in a sequence. This functionality is great for adding commenting, chat, messaging, feed, and stream features to your application. Threads are saved within a space and users that join a thread (with the same name, space, moderation configs, and access configs) will be able to communicate in that thread.

For the fully detailed spec, view the [documentation](https://github.com/3box/3box/blob/master/3IPs/3ip-2.md).

#### Viewing a Thread
You can get all posts made in a thread without opening a space. This is great for allowing visitors of your site view comments made by other users. This is achieved by calling the `getThread` method on the Box object. A thread can be referenced by all its configuration options or by its address.
#### Viewing a Public Thread
You can get all posts made in a public thread without opening a space. This is great for allowing visitors of your site view comments made by other users. This is achieved by calling the `getThread` method on the Box object. A thread can be referenced by all its configuration options or by its address.
```js
const posts = await Box.getThread(spaceName, threadName, firstModerator, membersThread)
console.log(posts)
Expand All @@ -191,12 +191,14 @@ Threads can also be viewed without opening space, or authenticating by calling t
const posts = await Box.getThreadByAddress(threadAddress)
console.log(posts)
```
However if applications want to add interactivity to the thread, such as allowing the user to post in a thread or follow updates in a thread, you will need to open their space to enable additional functionality.
However if applications want to add interactivity to the thread, such as allowing the user to post in a thread or follow updates in a thread, you will need to open their space to enable additional functionality. Same is true for a confidential thread, which requires you autheticate to get access to view the posts in a confidential thread.

#### Interacting with a Thread

##### 1. Joining a thread
To post in a thread, a user must first join the thread. This will implicitly use the moderation options where the current user is the `firstModerator` and `members` is false.
##### 1.a Creating a Public Thread

To create and join a public thread, you can simply join the thread. This will implicitly use the moderation options where the current user is the `firstModerator` and `members` is false.

```js
const thread = await space.joinThread('myThread')
```
Expand All @@ -207,30 +209,54 @@ A thread can also be given the moderation options when joining. You can pass `fi
const thread = await space.joinThread('myThread', { firstModerator: 'some3ID', members: true })
```

Lastly a thread can be joined by its address.
##### 1.b Creating a Confidential Thread

To create and join a confidential thread.

```js
const thread = await space.createConfidentialThread('myConfThread')
```

At creation you will likely want to add other members so that they can read and write messages to the thread, as shown below.

##### 2. Joining a Thread

An existing public or confidential thread can be joined by its address. Confidential threads are best referenced by their address.

```js
const thread = await space.joinThreadByAddress('/orbitdb/zdpuAp5QpBKR4BBVTvqe3KXVcNgo4z8Rkp9C5eK38iuEZj3jq/3box.thread.testSpace.testThread')
```

##### 2. Posting to a thread
While public threads can be joined by address or by passing known configs (same as above).

```js
const publicThread = await space.joinThread('myThread', { firstModerator: 'some3ID', members: true })
```

An address of a thread can be found as follows once joined.

```js
const threadAddress = thread.address
```

##### 3. Posting to a thread
This allows the user to add a message to the thread. The author of the message will be the user's 3Box DID. When a user posts in a thread, they are automatically subscribed to the thread and it is saved in the space used by the application under the key `thread-threadName`.
```js
await thread.post('hello world')
```
##### 3. Getting all posts in a thread
##### 4. Getting all posts in a thread
This allows applications to get the posts in a thread.
```js
const posts = await thread.getPosts()
console.log(posts)
```
##### 4. Listening for updates in thread
##### 5. Listening for updates in thread
This allows applications to listen for new posts in the thread, and perform an action when this occurs, such as adding the new message to the application's UI.
```js
thread.onUpdate(myCallbackFunction)
```

##### 5. Handling moderation and capabilities
##### 6. Handling moderation and capabilities

Add a moderator and list all existing moderators
```js
Expand Down
12 changes: 7 additions & 5 deletions src/3id/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Identities.addIdentityProvider(OdbIdentityProvider)
const utils = require('../utils/index')
const Keyring = require('./keyring')
const config = require('../config.js')
const nacl = require('tweetnacl')
const { randomNonce } = require('./utils')

const DID_METHOD_NAME = '3'
const STORAGE_KEY = 'serialized3id_'
Expand Down Expand Up @@ -233,7 +235,7 @@ class ThreeId {
return utils.callRpc(this._provider, '3id_encrypt', { message, space, to })
} else {
const keyring = this._keyringBySpace(space)
let paddedMsg = utils.pad(message)
let paddedMsg = typeof message === 'string' ? utils.pad(message) : message
if (to) {
return keyring.asymEncrypt(paddedMsg, to)
} else {
Expand All @@ -242,18 +244,18 @@ class ThreeId {
}
}

async decrypt (encObj, space) {
async decrypt (encObj, space, toBuffer) {
if (this._has3idProv) {
return utils.callRpc(this._provider, '3id_decrypt', { ...encObj, space })
} else {
const keyring = this._keyringBySpace(space)
let paddedMsg
if (encObj.ephemeralFrom) {
paddedMsg = keyring.asymDecrypt(encObj.ciphertext, encObj.ephemeralFrom, encObj.nonce)
paddedMsg = keyring.asymDecrypt(encObj.ciphertext, encObj.ephemeralFrom, encObj.nonce, toBuffer)
} else {
paddedMsg = keyring.symDecrypt(encObj.ciphertext, encObj.nonce)
paddedMsg = keyring.symDecrypt(encObj.ciphertext, encObj.nonce, toBuffer)
}
return utils.unpad(paddedMsg)
return toBuffer ? paddedMsg : utils.unpad(paddedMsg)
}
}

Expand Down
27 changes: 1 addition & 26 deletions src/3id/keyring.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const SimpleSigner = require('did-jwt').SimpleSigner
const { sha256 } = require('../utils/index')
const EC = require('elliptic').ec
const ec = new EC('secp256k1')
const { randomNonce, symEncryptBase, symDecryptBase } = require('./utils')

const BASE_PATH = "m/7696500'/0'/0'"
const MM_PATH = "m/44'/60'/0'/0"
Expand Down Expand Up @@ -88,30 +89,4 @@ class Keyring {
}
}

const randomNonce = () => {
return nacl.randomBytes(24)
}

const symEncryptBase = (msg, symKey, nonce) => {
nonce = nonce || randomNonce()
if (typeof msg === 'string') {
msg = nacl.util.decodeUTF8(msg)
}
const ciphertext = nacl.secretbox(msg, nonce, symKey)
return {
nonce: nacl.util.encodeBase64(nonce),
ciphertext: nacl.util.encodeBase64(ciphertext)
}
}

const symDecryptBase = (ciphertext, symKey, nonce, toBuffer) => {
ciphertext = nacl.util.decodeBase64(ciphertext)
nonce = nacl.util.decodeBase64(nonce)
const cleartext = nacl.secretbox.open(ciphertext, nonce, symKey)
if (toBuffer) {
return cleartext ? Buffer.from(cleartext) : null
}
return cleartext ? nacl.util.encodeUTF8(cleartext) : null
}

module.exports = Keyring
35 changes: 35 additions & 0 deletions src/3id/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const nacl = require('tweetnacl')
nacl.util = require('tweetnacl-util')

const randomNonce = () => {
return nacl.randomBytes(24)
}

const symEncryptBase = (msg, symKey, nonce) => {
nonce = nonce || randomNonce()
if (typeof msg === 'string') {
msg = nacl.util.decodeUTF8(msg)
}
const ciphertext = nacl.secretbox(msg, nonce, symKey)
return {
nonce: nacl.util.encodeBase64(nonce),
ciphertext: nacl.util.encodeBase64(ciphertext)
}
}

const symDecryptBase = (ciphertext, symKey, nonce, toBuffer) => {
ciphertext = nacl.util.decodeBase64(ciphertext)
nonce = nacl.util.decodeBase64(nonce)
const cleartext = nacl.secretbox.open(ciphertext, nonce, symKey)
if (toBuffer) {
return cleartext ? Buffer.from(cleartext) : null
}
return cleartext ? nacl.util.encodeUTF8(cleartext) : null
}

const newSymKey = () => {
return nacl.randomBytes(32)
}


module.exports = { randomNonce, symEncryptBase, symDecryptBase, newSymKey }
Loading

0 comments on commit 2b50f49

Please sign in to comment.