Browse Source

major refactor -- only storing author, hash, and signatures on messages and opening signed messages with nacl.sign.open

english
Ev Bogue 3 years ago
parent
commit
0d07708fe1
  1. 437
      app.js
  2. 1
      bogs/@218Fd2bCrmXe4gwnMg5Gcb9qVZrjXquym2AlelbkBro=
  3. 35
      css/style.css
  4. 9
      index.html
  5. 311
      lib.js
  6. 0
      lib/localforage.min.js
  7. 0
      lib/marked.min.js
  8. 0
      lib/nacl-util.min.js
  9. 0
      lib/nacl.min.js
  10. 12
      package-lock.json
  11. 4
      package.json
  12. 23
      readme.md
  13. 136
      render.js
  14. 47
      server.js

437
app.js

@ -1,322 +1,237 @@
var screen = h('div', {id: 'screen'})
document.body.appendChild(screen)
function compose (keys) {
var message = h('div', {classList: 'message'})
var scroller = document.getElementById('scroller')
scroller.insertBefore(message, scroller.firstChild)
var textarea = h('textarea', {placeholder: 'Write a new bog post'})
message.appendChild(textarea)
var composer = h('div', [
h('button', {
onclick: function () {
if (textarea.value) {
var content = {
author: keys.publicKey,
type: 'post',
text: textarea.value,
timestamp: Date.now()
}
textarea.value = ''
publish(content, keys)
}
}
}, ['Publish'])
])
message.appendChild(composer)
}
localforage.getItem('id', function (err, value) {
// the navbar has a dual purpose of generating a key if you don't already have one
if (value) {
var keys = value
var navbar = h('div', {classList: 'navbar'}, [
h('div', {classList: 'internal'}, [
h('li', [h('a', {href: '/'}, ['Home'])]),
h('li', [h('a', {href: '#' + keys.publicKey}, [getName(keys.publicKey)])]),
h('li', [h('a', {href: '/#key'}, ['Key'])])
])
])
document.body.appendChild(navbar)
} else if (value == null) {
var genkey = nacl.sign.keyPair()
if (genkey) {
var keys = {
publicKey: '@' + nacl.util.encodeBase64(genkey.publicKey),
privateKey: nacl.util.encodeBase64(genkey.secretKey)
}
// when we get our next round of funding, let's figure out how to do this without a page reload
if (keys.publicKey.includes('/')) {
console.log('TRYING AGAIN')
setTimeout(function () {
window.location.reload()
}, 10)
} else {
localforage.setItem('id', keys)
window.location.reload()
function keyPage (keys) {
var scroller = document.getElementById('scroller')
var message = h('div', {classList: 'message'})
message.appendChild(h('p', {innerHTML: marked('This is your ed25519 public/private keypair. It was generated using [Tweetnacl.js](https://tweetnacl.js.org/#/). Your public key is your identity when using [Bogbook](http://bogbook.com/), save your key in a safe place so that you can continue to use the same identity.')}))
// print stringified keypair
message.appendChild(h('pre', {style: 'width: 80%'}, [h('code', [JSON.stringify(keys)])]))
// delete key button
message.appendChild(h('button', {
onclick: function () {
localStorage['id'] = ''
location.reload()
}
}, ['Delete Key']))
var textarea = h('textarea', {placeholder: 'Import your existing ed25519 keypair'})
message.appendChild(textarea)
message.appendChild(h('button', {
onclick: function () {
if (textarea.value) {
localforage.setItem('id', JSON.parse(textarea.value))
location.reload()
}
}
}
})
}, ['Import Key']))
if (!localStorage['subscribees']) {
var subscribees = ['@218Fd2bCrmXe4gwnMg5Gcb9qVZrjXquym2AlelbkBro=']
localStorage['subscribees'] = JSON.stringify(subscribees)
scroller.appendChild(message)
}
if (!localStorage['pubs']) {
var pubs = ['ws://bogbook.com/', 'ws://localhost:8080/']
localStorage['pubs'] = JSON.stringify(pubs)
function profilePage (src, keys) {
var scroller = document.getElementById('scroller')
localforage.getItem(src, function (err, log) {
if (log) {
for (var i=0; i < log.length; i++) {
var post = log[i]
scroller.appendChild(renderMessage(post))
}
}
})
}
function compose (keys, opts) {
localforage.getItem('id', function (err, keys) {
if (keys) {
var header = h('div', {classList: 'message'})
var scroller = document.getElementById('scroller')
scroller.insertBefore(header, scroller.firstChild)
var textarea = h('textarea', {placeholder: 'Write a new bog post'})
header.appendChild(textarea)
var composer = h('div', [
h('button', {
onclick: function () {
if (textarea.value) {
var content = {
author: keys.publicKey,
type: 'post',
text: textarea.value,
timestamp: Date.now()
}
textarea.value = ''
publish(content, keys)
}
}
}, ['Publish'])
])
header.appendChild(composer)
function threadPage (src, keys) {
var scroller = document.getElementById('scroller')
localforage.getItem('log', function (err, log) {
for (var i = log.length - 1; i >= 0; --i) {
if (log[i].key === src) {
var post = log[i]
scroller.appendChild(renderMessage(post))
}
}
})
}
function publicPage (keys) {
compose(keys)
localforage.getItem('log', function (err, log) {
if (log) {
for (var i=0; i < log.length; i++) {
var post = log[i]
scroller.appendChild(renderMessage(post))
}
var newLog = log.sort(function (a, b) {
return b.content.timestamp - a.content.timestamp
})
if (newLog) {
localforage.setItem('log', log)
}
}
})
}
function route () {
localforage.getItem('id', function (err, keys) {
src = window.location.hash.substring(1)
var scroller = h('div', {id: 'scroller'})
var screen = document.getElementById('screen')
screen.appendChild(scroller)
if (src === 'key') {
var keyMessage = h('div', {classList: 'message'})
keyMessage.appendChild(h('p', {innerHTML: marked('This is your ed25519 public/private keypair. It was generated using [Tweetnacl.js](https://tweetnacl.js.org/#/). Your public key is your identiy when using [Bogbook](http://bogbook.com/), save your key in a safe place so that you can continue to use the same identity.')}))
// print stringified keypair
keyMessage.appendChild(h('pre', {style: 'width: 80%'}, [h('code', [JSON.stringify(keys)])]))
// delete key button
keyMessage.appendChild(h('button', {
onclick: function () {
localStorage['id'] = ''
location.reload()
}
}, ['Delete Key']))
var textarea = h('textarea', {placeholder: 'Import your existing ed25519 keypair'})
keyMessage.appendChild(textarea)
keyMessage.appendChild(h('button', {
onclick: function () {
if (textarea.value) {
localforage.setItem('id', JSON.parse(textarea.value))
location.reload()
}
}
}, ['Import Key']))
scroller.appendChild(keyMessage)
keyPage(keys)
} else if (src[0] === '@') {
profilePage(src, keys)
} else if (src[0] === '%') {
threadPage(src, keys)
} else {
publicPage(keys)
}
})
}
if (localStorage['id']) {
localforage.getItem('id', function (err, keys) {
if (keys) {
var oldKey = h('div', {classlist: 'message'})
var navbar = h('div', {classList: 'navbar'}, [
h('div', {classList: 'internal'}, [
h('li', [h('a', {href: '/'}, ['Home'])]),
h('li', [h('a', {href: '#' + keys.publicKey}, [getName(keys.publicKey)])]),
h('li', [h('a', {href: '/#key'}, ['Key'])])
])
])
oldKey.appendChild(h('p', ['You had a key in localStorage. Import it to the new database by pasting it into the box above.']))
oldKey.appendChild(h('pre', {style: 'width: 80%'}, [h('code', [localStorage['id']])]))
document.body.appendChild(navbar)
scroller.appendChild(oldKey)
route()
} else {
var genkey = nacl.sign.keyPair()
if (genkey) {
var keys = {
publicKey: '@' + nacl.util.encodeBase64(genkey.publicKey),
privateKey: nacl.util.encodeBase64(genkey.secretKey)
}
var pubMessage = h('div', {classList: 'message'})
var newPub = h('input', {placeholder: 'Add a new pub. Ex: ws://bogbook.com/'})
var pubs = JSON.parse(localStorage['pubs'])
pubMessage.appendChild(h('div', [
h('p', {innerHTML: marked('These are your bogbook pubs. These servers will sync data when you publish a new post, when you subscribe to new feeds, and when you click on feed ids.')}),
newPub,
h('button', {
onclick: function () {
if (newPub.value) {
pubs.push(newPub.value)
localStorage['pubs'] = JSON.stringify(pubs)
location.reload()
}
}
}, ['Add Pub'])
]))
function removeButton (pubName) {
var button = h('button', {
onclick: function () {
console.log('removing' + pubName)
for (var i = pubs.length; i--;) {
if (pubs[i] === pubName) {
pubs.splice(i, 1);
localStorage['pubs'] = JSON.stringify(pubs)
window.location.reload()
}
}
}
}, ['Remove Pub'])
return button
}
if (keys.publicKey.includes('/')) {
console.log('TRYING AGAIN')
setTimeout(function () {
location.reload()
}, 10)
} else {
var scroller = h('div', {id: 'scroller'})
screen.appendChild(scroller)
for (i = 0; i < pubs.length; i++) {
var pubName = pubs[i]
pubMessage.appendChild(h('p', [
pubName,
removeButton(pubName)
]))
}
var message = h('div', {classList: 'message'})
scroller.appendChild(pubMessage)
}
scroller.appendChild(message)
message.appendChild(h('h1', ['Welcome to Bogbook']))
message.appendChild(h('p', ['Bogbook is a distributed blogging network of signed append-only feeds. To avoid confusion, we call them "bogs".']))
else if (src[0] === '@') {
var profile = h('div', {classList: 'message'})
scroller.appendChild(profile)
message.appendChild(h('p', ['Please note: Bogbook is experimental software, not for use in producton environments. Expect bugs and breaking changes. Pull-requests are needed.']))
if (src == keys.publicKey) {
var nameInput = h('input', {placeholder: 'Publish a new name'})
message.appendChild(h('p', {innerHTML: marked('View the code: [http://github.com/bogbook/bog](http://github.com/bogbook/bog). Questions? [ev@evbogue.com](mailto:ev@evbogue.com).')}))
var namePublisher = h('div',[
nameInput,
h('button', {
onclick: function () {
if (nameInput.value) {
message.appendChild(h('hr'))
message.appendChild(h('h3', ['Get started']))
var content = {
author: keys.publicKey,
type: 'name',
text: nameInput.value,
timestamp: Date.now()
}
message.appendChild(h('p', {innerHTML: marked('This is an ed25519 public/private signing keypair. It was generated using [TweetNaCl.js](https://tweetnacl.js.org/#/)')}))
message.appendChild(h('pre', [JSON.stringify(keys)]))
publish(content, keys)
}
}
}, ['Publish'])
])
message.appendChild(h('p', ['Right now, this keypair exists only in memory. When you leave this page, the keypair will vanish forever. If you refresh this page you\'ll receive a new keypair.']))
profile.appendChild(namePublisher)
message.appendChild(h('p', {innerHTML: marked('To save this keypair, identify with handle below. Once you identify, your public/private keypair will be stored in your browser using [localForage.js](https://localforage.github.io/localForage). Save your keypair somewhere safe to preserve your identity.')}))
readFile()
message.appendChild(h('hr'))
message.appendChild(h('h3', ['Identify']))
var imageInput = h('span', [
h('input', {id: 'inp', type:'file'}),
h('span', {id: 'b64'}),
h('img', {id: 'img'})
])
var identify = h('input', {placeholder: 'Your Name'})
var imagePublisher = h('div', [
imageInput,
h('button', {
onclick: function () {
message.appendChild(h('div', [
identify,
h('button', {onclick: function () {
if (identify.value) {
var content = {
author: keys.publicKey,
type: 'image',
image: document.getElementById("img").src,
type: 'name',
naming: keys.publicKey,
name: identify.value,
timestamp: Date.now()
}
identify.value = ''
publish(content, keys)
}
}, ['Publish'])
])
profile.appendChild(imagePublisher)
document.getElementById("inp").addEventListener("change", readFile);
} else {
var subscribees = JSON.parse(localStorage['subscribees'])
if (subscribees.includes(src)) {
profile.appendChild(h('button', {
onclick: function () {
for (var i = subscribees.length; i--;) {
if (subscribees[i] === src) {
subscribees.splice(i, 1);
localStorage['subscribees'] = JSON.stringify(subscribees)
window.location.reload()
localforage.setItem('id', keys, function (err, published) {
if (published) {
location.reload()
}
}
}
// remove subscribee
}, ['UNSUBSCRIBE']))
} else {
profile.appendChild(h('button', {
onclick: function () {
subscribees.push(src)
localStorage['subscribees'] = JSON.stringify(subscribees)
window.location.reload()
})
}
}, ['SUBSCRIBE']))
}
}
var pubs = JSON.parse(localStorage['pubs'])
}}, ['Identify'])
]))
for (i = 0; i < pubs.length; i++) {
requestFeed(src, pubs[i])
}
localforage.getItem(src, function (err, log) {
if (log) {
for (var i=0; i < log.length; i++) {
var post = log[i]
scroller.appendChild(renderMessage(post))
}
}
})
}
message.appendChild(h('p', ['When you click [Identify], you will post your first message to your append-only bog, your ed25519 keypair will be saved in your browser, and the page will reload. Don\'t forget to back up your key! and happy bogging.']))
message.appendChild(h('hr'))
message.appendChild(h('h3', ['Already have a key?']))
else if (src[0] === '%') {
message.appendChild(h('p', ['Import it here. Make sure to sync your existing feed from a Bogbook \'pub\' before posting a message.']))
localforage.getItem('log', function (err, log) {
for (var i = log.length - 1; i >= 0; --i) {
if (log[i].key === src) {
var post = log[i]
scroller.appendChild(renderMessage(post))
var textarea = h('textarea', {placeholder: 'Import your existing ed25519 keypair'})
message.appendChild(textarea)
message.appendChild(h('button', {
onclick: function () {
if (textarea.value) {
localforage.setItem('id', JSON.parse(textarea.value))
location.reload()
}
}
}
})
}, ['Import Key']))
}
}
else {
compose(keys)
var subscribees = JSON.parse(localStorage['subscribees'])
console.log(subscribees)
for (i = 0; i < subscribees.length; i++) {
var pubs = JSON.parse(localStorage['pubs'])
for (n = 0; n < pubs.length; n++) {
requestFeed(subscribees[i], pubs[n])
}
}
localforage.getItem('log', function (err, log) {
for (var i=0; i < log.length; i++) {
var post = log[i]
scroller.appendChild(renderMessage(post))
}
var newLog = log.sort(function (a, b) {
return b.content.timestamp - a.content.timestamp
})
if (newLog) {
localforage.setItem('log', log)
}
})
}
})
}
}
})
route()
window.onhashchange = function () {
var oldscreen = document.getElementById('screen')

1
bogs/@218Fd2bCrmXe4gwnMg5Gcb9qVZrjXquym2AlelbkBro=

File diff suppressed because one or more lines are too long

35
css/style.css

@ -5,14 +5,33 @@ body {
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
line-height: 1.25em;
}
p {
margin-top: 5px;
margin-bottom: 5px;
margin-top: 1ex;
margin-bottom: 1ex;
font-size: 1em;
}
h1, h2, h3, h4, h5, h6 { margin-top: 5px;}
h1 { font-size: 1.4em; }
h2 { font-size: 1.3em; }
h3 { font-size: 1.25em; }
h4 { font-size: 1.2em; }
h5 { font-size: 1.15em; }
h6 { font-size: 1.1em; }
hr {
border: solid #222;
clear: both;
border-width: 1px 0 0;
height: 0;
margin-bottom: .9em;
}
#screen {
position: absolute;
top: 35px;
@ -32,7 +51,7 @@ p {
.message {
background: #333;
margin-top: .5em;
padding: .5em;
padding: .3em .5em;
border-radius: 5px;
}
@ -84,6 +103,8 @@ code, pre {
}
textarea, input {
font-family: 'Source Sans Pro';
font-size: 1em;
background: #222;
padding: .5em;
color: #f5f5f5;
@ -149,10 +170,10 @@ textarea {
button {
display: inline-block;
padding: 2px 6px;
margin: .2em .2em .2em 0em;
font-size: 14px;
line-height: 20px;
padding: .25em .5em;
margin: .15em;
font-size: 1em;
line-height: 1.2em;
color: #d5d5d5;
text-align: center;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.75);

9
index.html

@ -6,11 +6,12 @@
<link rel='stylesheet' href='./css/style.css' />
</head>
<body>
<script src="nacl.min.js"></script>
<script src="nacl-util.min.js"></script>
<script src="localforage.min.js"></script>
<script src="marked.min.js"></script>
<script src="./lib/nacl.min.js"></script>
<script src="./lib/nacl-util.min.js"></script>
<script src="./lib/localforage.min.js"></script>
<script src="./lib/marked.min.js"></script>
<script src="lib.js"></script>
<script src="render.js"></script>
<script src="app.js"></script>
</body>
</html>

311
lib.js

@ -1,89 +1,45 @@
// generate a public.private keypair with TweetNaCl.js
function getKeys () {
localforage.getItem('id', function(err, value) {
if (value) {
var keys = value
return keys
} else if (value == null) {
var genkey = nacl.sign.keyPair()
if (genkey) {
var keys = {
publicKey: '@' + nacl.util.encodeBase64(genkey.publicKey),
privateKey: nacl.util.encodeBase64(genkey.secretKey)
}
// when we get our next round of funding, let's figure out how to do this without a page reload
if (keys.publicKey.includes('/')) {
console.log('TRYING AGAIN')
setTimeout(function () {
window.location.reload()
}, 10)
} else {
localforage.setItem('id', keys)
return keys
}
}
}
})
return keys
}
function requestFeed (src, server) {
var ws = new WebSocket(server + src)
localforage.getItem(src, function (err, log) {
if (log) {
// update the log
console.log('LOG DOES EXIST, asking')
ws.onopen = function () {
// req feed
var clientLog = {
publicKey: src,
log: log
}
console.log(clientLog)
var clientLog = { publicKey: src, log: log }
ws.send(JSON.stringify(clientLog))
}
ws.onmessage = function (ev) {
console.log(ev.data)
var serverLog = JSON.parse(ev.data)
if (serverLog.log.length > log.length) {
// update the log of the id
localforage.setItem(src, serverLog.log)
// concat new items from the log onto the client's public log
localforage.getItem('log', function (err, feed) {
if (feed) {
var num = serverLog.log.length - log.length
localforage.getItem('log', function (err, publicLog) {
if (publicLog) {
var num = serverLog.log.length - publicLog.length
var diff = serverLog.log.slice(0, num)
oldLog = feed
newLog = diff.concat(oldLog)
localforage.setItem('log', newLog)
}
newLog = diff.concat(publicLog)
localforage.setItem('log', newLog)
} else {
localforage.setItem('log', serverLog.log)
}
})
}
}
}
} else {
ws.onopen = function () {
// req feed
var clientLog = {
publicKey: src,
log: []
}
ws.send(JSON.stringify(clientLog))
}
// request the log (because we don't have it)
console.log('LOG DOES NOT EXIST, asking')
ws.onmessage = function (ev) {
serverLog = JSON.parse(ev.data)
localforage.setItem(src, serverLog.log)
// concat new items from the log onto the client's public log
localforage.getItem('log', function (err, feed) {
if (feed) {
newLog = serverLog.log.concat(feed)
localforage.getItem('log', function (err, publicLog) {
if (publicLog) {
newLog = serverLog.log.concat(publicLog)
localforage.setItem('log', newLog)
} else {
localforage.setItem('log', serverLog.log)
@ -102,27 +58,26 @@ function publish (content, keys) {
localforage.getItem(keys.publicKey, function (err, log) {
if (log) {
var lastPost = log[0]
var seq = lastPost.content.sequence
var pubkey = nacl.util.decodeBase64(keys.publicKey.substring(1))
var sig = nacl.util.decodeBase64(lastPost.signature)
var opened = JSON.parse(nacl.util.encodeUTF8(nacl.sign.open(sig, pubkey)))
console.log(opened)
var seq = opened.sequence
content.sequence = ++seq
content.previous = nacl.util.encodeBase64(nacl.hash(nacl.util.decodeUTF8(JSON.stringify(log[0]))))
var post = {
content: content,
author: keys.publicKey,
key: '%' + nacl.util.encodeBase64(nacl.hash(nacl.util.decodeUTF8(JSON.stringify(content)))),
signature: nacl.util.encodeBase64(nacl.sign(nacl.util.decodeUTF8(JSON.stringify(content)), nacl.util.decodeBase64(keys.privateKey)))
}
// add key (which is a hash of the stringified object post)
post.key = '%' + nacl.util.encodeBase64(nacl.hash(nacl.util.decodeUTF8(JSON.stringify(post))))
}
// update the log
updateLog(keys.publicKey, post)
var pubs = JSON.parse(localStorage['pubs'])
for (i = 0; i < pubs.length; i++) {
requestFeed(keys.publicKey, pubs[i])
}
var scroller = document.getElementById('scroller')
if (scroller.firstChild) {
scroller.insertBefore(renderMessage(post), scroller.childNodes[1])
@ -132,30 +87,21 @@ function publish (content, keys) {
} else {
content.sequence = 0
var post = {
content: content,
author: keys.publicKey,
key: '%' + nacl.util.encodeBase64(nacl.hash(nacl.util.decodeUTF8(JSON.stringify(content)))),
signature: nacl.util.encodeBase64(nacl.sign(nacl.util.decodeUTF8(JSON.stringify(content)), nacl.util.decodeBase64(keys.privateKey)))
}
// add key (which is a hash of the stringified object post)
post.key = '%' + nacl.util.encodeBase64(nacl.hash(nacl.util.decodeUTF8(JSON.stringify(post))))
}
// update the log
updateLog(keys.publicKey, post)
var pubs = JSON.parse(localStorage['pubs'])
for (i = 0; i < pubs.length; i++) {
requestFeed(keys.publicKey, pubs[i])
}
var scroller = document.getElementById('scroller')
if (scroller.firstChild) {
scroller.insertBefore(renderMessage(post), scroller.childNodes[1])
} else {
scroller.appendChild(renderMessage(post))
}
}
})
}
@ -190,169 +136,6 @@ function updateLog (feed, post) {
})
}
// file uploaders for user images
function readFile () {
if (this.files && this.files[0]) {
var fr = new FileReader();
fr.addEventListener("load", function(e) {
var image = e.target.result
document.getElementById("img").src = e.target.result;
document.getElementById("img").style = 'width: 75px; height: 75px';
document.getElementById("b64").innerHTML = e.target.result;
});
fr.readAsDataURL( this.files[0] );
}
}
// render messages
function renderMessage (post) {
var messageDiv = h('messageDiv', {id: post.key})
var message = h('div', {classList: 'message'})
if (post.content.type == 'name') {
var mini = h('span', [
' identified as ',
post.content.text
])
message.appendChild(getHeader(post, mini))
//message.appendChild(h('pre', [JSON.stringify(post)]))
}
if (post.content.type == 'image') {
var mini = h('span', [
' identified as ',
h('img', {classList: 'small', src:post.content.image})
])
message.appendChild(getHeader(post, mini))
//message.appendChild(h('pre', [JSON.stringify(post)]))
}
if (post.content.type == 'post') {
localforage.getItem('log', function (err, log) {
if (log) {
for (var i = log.length - 1; i >= 0; --i) {
if (log[i].content.reply == post.key) {
var nextPost = log[i]
var messageExists = (document.getElementById(nextPost.key) !== null);
if (!messageExists) {
messageDiv.appendChild(h('div', {classList: 'submessage'}, [
renderMessage(nextPost)
]))
}
}
}
}
})
var renderer = new marked.Renderer();
renderer.link = function(href, title, text) {
if ((href[0] == '@') || (href[0] == '%')) {
href = '#' + href
}
var link = marked.Renderer.prototype.link.call(this, href, title, text);
return link
}
marked.setOptions({
renderer: renderer
});
message.appendChild(getHeader(post))
if (post.content.reply) {
message.appendChild(h('span', [
're: ',
h('a', {href: '#' + post.content.reply}, [post.content.reply.substring(0, 10) + '...'])
]))
}
message.appendChild(h('div', {innerHTML: marked(post.content.text)}))
message.appendChild(h('span', {id: post.key + 'src', classList: 'right'}, [
h('a', {
onclick: function () {
message.appendChild(h('pre', [JSON.stringify(post)]))
var span = document.getElementById(post.key + 'src')
span.parentNode.removeChild(span)
}
}, ['[src]'])
]))
var gotName = getName(post.content.author)
localforage.getItem('id', function (err, keys) {
var publishButton = h('button', {
onclick: function () {
if (textarea.value) {
var content = {
author: keys.publicKey,
type: 'post',
text: textarea.value,
reply: post.key,
timestamp: Date.now()
}
publish(content, keys)
message.removeChild(textarea)
message.removeChild(publishButton)
}
}
}, ['Publish'])
var textarea = h('textarea', {placeholder: 'Reply to this bog post'}, ['['+ gotName.textContent + '](' + post.content.author + ')'])
var replyButton = h('button', {
classList: 'replyButton:' + post.key,
onclick: function () {
message.removeChild(replyButton)
message.appendChild(textarea)
message.appendChild(publishButton)
}
}, ['Reply'])
message.appendChild(replyButton)
})
messageDiv.appendChild(message)
}
return messageDiv
}
function getImage (id) {
var image = h('span')
//image.appendChild(h('img', {classList: 'small'}))
localforage.getItem(id, function (err, log) {
if (log) {
for (var i=0; i < log.length; i++) {
var imagePost = log[i]
if (imagePost.content.type == 'image') {
image.appendChild(h('img', {classList: 'small', src: imagePost.content.image}))
return
}
}
}
})
return image
}
function getName (id) {
var name = h('span')
name.textContent = id.substring(0, 10) + '...'
@ -360,38 +143,24 @@ function getName (id) {
localforage.getItem(id, function (err, log) {
if (log) {
for (var i=0; i < log.length; i++) {
var namePost = log[i]
if (namePost.content.type == 'name') {
name.textContent = '@' + namePost.content.text
//return name
}
var post = log[i]
var pubkey = nacl.util.decodeBase64(post.author.substring(1))
var sig = nacl.util.decodeBase64(post.signature)
post.content = JSON.parse(nacl.util.encodeUTF8(nacl.sign.open(sig, pubkey)))
//if (post.content) {
if (post.content.type == 'name') {
console.log(post.content.name)
name.textContent = '@' + post.content.name
}
//}
}
}
})
return name
}
function getHeader (post, mini) {
var inner
if (mini) {
var inner = mini
}
var head = h('span', [
h('a', {href: '#' + post.key}, [
h('p', {classList: 'right'}, [human(new Date(post.content.timestamp))]),
]),
h('p', [
h('a', {href: '#' + post.content.author}, [
getImage(post.content.author),
getName(post.content.author)
]),
inner
])
])
return head
}
// human-time by Dave Eddy https://github.com/bahamas10/human
function human(seconds) {

0
localforage.min.js → lib/localforage.min.js vendored

0
marked.min.js → lib/marked.min.js vendored

0
nacl-util.min.js → lib/nacl-util.min.js vendored

0
nacl.min.js → lib/nacl.min.js vendored

12
package-lock.json generated

@ -1,6 +1,6 @@
{
"name": "bogbook",
"version": "1.0.0",
"version": "1.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -48,6 +48,16 @@
"is-wsl": "^1.1.0"
}
},
"tweetnacl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.1.tgz",
"integrity": "sha512-kcoMoKTPYnoeS50tzoqjPY3Uv9axeuuFAZY9M/9zFnhoVvRfxz9K29IMPD7jGmt2c8SW7i3gT9WqDl2+nV7p4A=="
},
"tweetnacl-util": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.0.tgz",
"integrity": "sha1-RXbBzuXi1j0gf+5S8boCgZSAvHU="
},
"url-join": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz",

4
package.json

@ -1,6 +1,6 @@
{
"name": "bogbook",
"version": "1.1.0",
"version": "1.2.0",
"description": "secure blockchain logging (blogging, without the l) -- bogging",
"main": "server.js",
"scripts": {
@ -12,6 +12,8 @@
"dependencies": {
"ecstatic": "^3.3.1",
"opn": "^6.0.0",
"tweetnacl": "^1.0.1",
"tweetnacl-util": "^0.15.0",
"ws": "^6.2.1"
}
}

23
readme.md

@ -12,7 +12,7 @@ but nothing will load into the browser unless you request a public key, so try
### What?
bogbook is a distributed social networking application using [TweetNaCl.js](https://tweetnacl.js.org/#/) to publish signed append-only logs to your browser's localStorage.
bogbook is a distributed social networking application using [TweetNaCl.js](https://tweetnacl.js.org/#/) to publish signed append-only logs to your browser's IndexedDB using [localForage](https://localforage.github.io/localForage).
The bogs are then gossiped between your bog client and bog 'pub' servers using websockets. You're responsible for syncing your messages between different bog 'pub' servers. Bog 'pubs' themselves don't talk to each other, instead they only talk to clients.
@ -44,20 +44,31 @@ Bogbook should launch in your browser. If it doesn't, navigate to http://localho
All of the bogbook cryptography is produced using [TweetNaCl.js](https://tweetnacl.js.org/#/) which is a port of [TweetNaCl](https://tweetnacl.cr.yp.to/), a cryptography library written in 100 Tweets.
bogbook generates an ed25519 public/private keypair on load using `nacl.sign.keyPair()`, which is then stored in localStorage at `localStorage['id']` as a JSON object with the public/private keypairs base64-encoded.
bogbook generates an ed25519 public/private keypair on load using `nacl.sign.keyPair()`, which is then stored in localForage at `localStorage['id']` as a JSON object with the public/private keypairs base64-encoded.
When you post a new message, bogbook will
+ iterate up the message sequence number
+ hash the contents of the previous message using sha512
+ sign the contents of the current message with your ed25519 private key
+ hash the new message (including the signature) using sha512
+ generate a hash of the contents of the current message using sha512
+ generate a signature of the contents of the current message with your ed25519 private key
before appending the feed to the log stored in your browser.
Then you publish a message containing
```
{
"author": <your publickey>,
"key": <sha512 hash of content>,
"signature": <signature of content>
}
To view the message, you use `nacl.sign.open` passing Uint8Arrays of the signature and publickey as paramaters.
---
Please note: All logs are append-only, public, and plain text at the current time. While you _can_ moderate your local database and pub servers by deleting logs associated with public keys, it can be difficult to unsay something, so don't drink and bog, people.
Some browsers clear localStorage upon exit, others will clear it if you wipe your browser cache. Remember to save your public/private keypair somewhere, because no one can regenerate it for you.
Some browsers clear stored data upon exit, others will clear it if you wipe your browser cache. Remember to save your public/private keypair somewhere, because no one can regenerate it for you.
### contributing

136
render.js

@ -0,0 +1,136 @@
function renderMessage (post) {
var messageDiv = h('messageDiv', {id: post.key})
var message = h('div', {classList: 'message'})
var pubkey = nacl.util.decodeBase64(post.author.substring(1))
var sig = nacl.util.decodeBase64(post.signature)
post.content = JSON.parse(nacl.util.encodeUTF8(nacl.sign.open(sig, pubkey)))
if (post.content.type == 'name') {
var mini = h('span', [
' identified as ',
post.content.name
])
message.appendChild(getHeader(post, mini))
messageDiv.appendChild(message)
}
if (post.content.type == 'post') {
localforage.getItem('log', function (err, log) {
if (log) {
for (var i = log.length - 1; i >= 0; --i) {
if (log[i].content.reply == post.key) {
var nextPost = log[i]
var messageExists = (document.getElementById(nextPost.key) !== null);
if (!messageExists) {
messageDiv.appendChild(h('div', {classList: 'submessage'}, [
renderMessage(nextPost)
]))
}
}
}
}
})
var renderer = new marked.Renderer();
renderer.link = function(href, title, text) {
if ((href[0] == '@') || (href[0] == '%')) {
href = '#' + href
}
var link = marked.Renderer.prototype.link.call(this, href, title, text);
return link
}
marked.setOptions({
renderer: renderer
});
message.appendChild(getHeader(post))
if (post.content.reply) {
message.appendChild(h('span', [
're: ',
h('a', {href: '#' + post.content.reply}, [post.content.reply.substring(0, 10) + '...'])
]))
}
message.appendChild(h('div', {innerHTML: marked(post.content.text)}))
message.appendChild(h('span', {id: post.key + 'src', classList: 'right'}, [
h('a', {
onclick: function () {
message.appendChild(h('pre', [JSON.stringify(post)]))
var span = document.getElementById(post.key + 'src')
span.parentNode.removeChild(span)
}
}, ['[src]'])
]))
var gotName = getName(post.content.author)
localforage.getItem('id', function (err, keys) {
var publishButton = h('button', {
onclick: function () {
if (textarea.value) {
var content = {
author: keys.publicKey,
type: 'post',
text: textarea.value,
reply: post.key,
timestamp: Date.now()
}
publish(content, keys)
message.removeChild(textarea)
message.removeChild(publishButton)
}
}
}, ['Publish'])
var textarea = h('textarea', {placeholder: 'Reply to this bog post'}, ['['+ gotName.textContent + '](' + post.content.author + ')'])
var replyButton = h('button', {
classList: 'replyButton:' + post.key,
onclick: function () {
message.removeChild(replyButton)
message.appendChild(textarea)
message.appendChild(publishButton)
}
}, ['Reply'])
message.appendChild(replyButton)
})
messageDiv.appendChild(message)
}
return messageDiv
}
function getHeader (post, mini) {
var inner
if (mini) {
var inner = mini
}
var head = h('span', [
h('a', {href: '#' + post.key}, [
h('p', {classList: 'right'}, [human(new Date(post.content.timestamp))]),
]),
h('p', [
h('a', {href: '#' + post.content.author}, [
getName(post.content.author)
]),
inner
])
])
return head
}

47
server.js

@ -8,55 +8,20 @@ http.createServer(
serve({ root: __dirname})
).listen(8089)
//opn('http://localhost:8089')
opn('http://localhost:8089')
// websocket server (8080)
var WebSocket = require('ws')
var fs = require('fs')
var nacl = require('tweetnacl')
nacl.util = require('tweetnacl-util')
var wss = new WebSocket.Server({ port: 8080 })
var wserver = new WebSocket.Server({ port: 8080 })
wss.on('connection', function (ws) {
wserver.on('connection', function (ws) {
ws.on('message', function (message) {
var receivedLog = JSON.parse(message)
if (receivedLog.publicKey) {
var publicKey = receivedLog.publicKey
var clientLog = receivedLog.log
// check to see if log is on server
if (fs.existsSync(__dirname + '/bogs/' + publicKey)) {
var serverLog = JSON.parse(fs.readFileSync(__dirname + '/bogs/' + receivedLog.publicKey))
// if the server log has more entries than the log in the client, send the server log to the client
if (serverLog.length > clientLog.length) {
sendingLog = {
publicKey: publicKey,
log: serverLog
}
ws.send(JSON.stringify(sendingLog))
console.log('SENT ' + publicKey)
}
// if server log has less entries than the log sent by the client, write it to the server
if (serverLog.length < clientLog.length) {
fs.writeFile(__dirname + '/bogs/' + publicKey, JSON.stringify(clientLog), function (err) {
if (err) throw err
console.log('SAVED ' + publicKey)
})
}
// if logs are identical, do nothing
if (serverLog.length == clientLog.length) {
console.log('SAME ' + publicKey)
}
// if log doesn't already exist, write it
} else {
fs.writeFile(__dirname + '/bogs/' + publicKey, JSON.stringify(clientLog), function (err) {
if (err) throw err
console.log('SAVED ' + publicKey)
})
}
}
console.log(message)
})
})

Loading…
Cancel
Save