Unverified Commit 08048700 authored by Aral Balkan's avatar Aral Balkan
Browse files

Make WebRTC spike its own stand-alone app

parent 31ffc689
//
// Hypha client
// Hypha basic WebRTC node
//
// Initial key generation
const session25519 = require('session25519')
const generateEFFDicewarePassphrase = require('eff-diceware-passphrase')
// Hypercore
const { Buffer } = require('buffer')
const ram = require('random-access-memory')
......@@ -15,8 +11,6 @@ const hypercore = require('hypercore')
const webSocketStream = require('websocket-stream')
const pump = require('pump')
const nextId = require('monotonic-timestamp-base36')
// From libsodium.
function to_hex(input) {
// Disable input checking for this simple spike.
......@@ -37,17 +31,12 @@ function to_hex(input) {
}
// HTML elements.
const setupForm = document.getElementById('setupForm')
const changeButton = document.getElementById('change')
const passphraseTextField = document.getElementById('passphrase')
const form = document.getElementById('connectionForm')
const hyphalinkTextField = document.getElementById('hyphalink')
const connectButton = document.getElementById('connect')
const indeterminateProgressIndicator = document.getElementById('indeterminateProgressIndicator')
const generatedTextField = document.getElementById('generated')
const hypercoreContentsTextArea = document.getElementById('hypercoreContents')
const errorsTextArea = document.getElementById('errors')
const publicSigningKeyTextField = document.getElementById('publicSigningKey')
const privateSigningKeyTextArea = document.getElementById('privateSigningKey')
const publicEncryptionKeyTextField = document.getElementById('publicEncryptionKey')
const privateEncryptionKeyTextField = document.getElementById('privateEncryptionKey')
const signals = ['ready', 'data', 'error', 'append', 'download', 'upload', 'sync', 'close']
......@@ -82,265 +71,26 @@ function blinkSignal(signal) {
}
function resetForm() {
passphraseTextField.value = ''
publicSigningKeyTextField.value = ''
generatedTextField.value = 'No'
hyphalinkTextField.value = ''
resetSignals()
hypercoreContentsTextArea.value = ''
errorsTextArea.value = ''
privateSigningKeyTextArea.value = ''
publicEncryptionKeyTextField.value = ''
privateEncryptionKeyTextField.value = ''
}
function logError(error) {
errorsTextArea.value += error
}
function generatePassphrase () {
resetForm()
showProgressIndicator()
// On next tick, so the interface has a chance to update.
setTimeout(() => {
const passphrase = generateEFFDicewarePassphrase.entropy(100)
setupForm.elements.passphrase.value = passphrase.join(' ')
generateKeys()
}, 0)
}
function showProgressIndicator() {
changeButton.style.display = 'none';
connectButton.style.display = 'none';
indeterminateProgressIndicator.style.display = 'block';
}
function hideProgressIndicator() {
changeButton.style.display = 'block';
connectButton.style.display = 'block';
indeterminateProgressIndicator.style.display = 'none';
}
function clearOutputFields() {
publicSigningKeyTextField.value = ''
privateSigningKeyTextArea.value = ''
publicEncryptionKeyTextField.value = ''
privateEncryptionKeyTextField.value = ''
}
function generateKeys() {
const passphrase = setupForm.elements.passphrase.value
const domain = setupForm.elements.domain.value
session25519(domain, passphrase, (error, keys) => {
hideProgressIndicator()
if (error) {
logError(error.message)
return
}
//
// Convert the keys first to ArrayBuffer and then to
// Node’s implementation of Buffer, which is what
// hypercore expected.
//
// If you try to pass an ArrayBuffer instead, you get
// the following error:
//
// Error: key must be at least 16, was given undefined
//
console.log(`Creating new hypercore with read key ${to_hex(keys.publicSignKey)} and write key ${to_hex(keys.secretSignKey)}`)
const hypercoreReadKey = Buffer.from(keys.publicSignKey.buffer)
const hypercoreWriteKey = Buffer.from(keys.secretSignKey.buffer)
let feed = null
let stream = null
let updateInterval = null
// Create a new hypercore using the newly-generated key material.
feed = hypercore((filename) => ram(filename), hypercoreReadKey, {
createIfMissing: false,
overwrite: false,
valueEncoding: 'json',
secretKey: hypercoreWriteKey,
storeSecretKey: false,
onwrite: (index, data, peer, next) => {
console.log(`Feed: [onWrite] index = ${index}, peer = ${peer}, data:`)
console.log(data)
next()
}
})
feed.on('ready', () => {
const feedKey = feed.key
const feedKeyInHex = to_hex(feedKey)
console.log(`Feed: [Ready] ${feedKeyInHex}`)
blinkSignal('ready')
generatedTextField.value = 'Yes'
if (!feed.writable) {
generatedTextField.value = 'Yes (warning: but feed is not writable)'
return
}
// Hypercore feed is ready: connect to web socket and start replicating.
const remoteStream = webSocketStream(`wss://localhost/hypha/${feedKeyInHex}`)
const localStream = feed.replicate({
encrypt: false,
live: true
})
// Create a duplex stream.
//
// What’s actually happening:
//
// remoteStream.write -> localStream.read
// localStream.write -> remoteStream.read
pump(
remoteStream,
localStream,
remoteStream,
(error) => {
console.log(`Pipe closed for ${feedKeyInHex}`, error && error.message)
logError(error.message)
}
)
//
// Note: the order of execution for an append appears to be:
//
// 1. onWrite handler (execution stops unless next() is called)
// 2. feed’s on('append') handler
// 3. feed.append callback function
// 4. readStream’s on('data') handler
//
// Create a read stream
stream = feed.createReadStream({live:true})
stream.on('data', (data) => {
blinkSignal('data')
console.log('Feed [read stream, on data]' , data)
// New data is available on the feed. Display it on the page.
for (let [key, value] of Object.entries(data)) {
hypercoreContentsTextArea.value += `${key}: ${value}\n`
}
})
//
// TEST
//
const NUMBER_TO_APPEND = 3
let counter = 0
const intervalToUpdateInMS = 500
updateInterval = setInterval(() => {
counter++
if (counter === NUMBER_TO_APPEND) {
console.log(`Reached max number of items to append (${NUMBER_TO_APPEND}). Will not add any more.`)
clearInterval(updateInterval)
updateInterval = null
}
const key = nextId()
const value = Math.random()*1000000000000000000 // simple random number
let obj = {}
obj[key] = value
feed.append(obj, (error, sequence) => {
console.log('Append callback')
if (error) {
logError(error)
return
}
console.log(' Sequence', sequence)
})
}, intervalToUpdateInMS)
})
feed.on('error', (error) => {
console.log(`Feed [Error] ${error}`)
blinkSignal('error')
logError(error)
})
feed.on('download', (index, data) => {
blinkSignal('download')
console.log(`Feed [Download] index = ${index}, data = ${data}`)
})
feed.on('upload', (index, data) => {
blinkSignal('upload')
console.log(`Feed [Upload] index = ${index}, data = ${data}`)
})
feed.on('append', () => {
blinkSignal('append')
console.log('Feed [Append]')
})
feed.on('sync', () => {
blinkSignal('sync')
console.log('Feed [Sync]')
})
feed.on('close', () => {
blinkSignal('close')
console.log('Feed [Close]')
})
// Update the passphrase (and keys) when the change button is pressed.
function onChangeButtonPress (event) {
console.log('((( onChangeButtonPress )))')
// Let’s remove ourselves as a listener as we will be
// re-added on the next refresh.
setupForm.removeEventListener('submit', onChangeButtonPress)
if (updateInterval !== null) {
clearInterval(updateInterval)
updateInterval = null
}
if (stream !== null) {
stream.destroy()
stream = null
}
// If a feed exists, close it and then generate the new keys/feed.
if (feed !== null) {
feed.close((error) => {
console.log(">>> Feed is closed. <<<")
feed = null
// Feed is closed. Error is not really an error.
generatePassphrase()
})
event.preventDefault()
return
}
// Otherwise, just go ahead and generate the keys now.
generatePassphrase()
event.preventDefault()
}
setupForm.addEventListener('submit', onChangeButtonPress)
// Display the keys.
publicSigningKeyTextField.value = to_hex(keys.publicSignKey)
privateSigningKeyTextArea.value = to_hex(keys.secretSignKey)
publicEncryptionKeyTextField.value = to_hex(keys.publicKey)
privateEncryptionKeyTextField.value = to_hex(keys.secretKey)
})
}
// Main
document.addEventListener('DOMContentLoaded', () => {
......@@ -349,6 +99,4 @@ document.addEventListener('DOMContentLoaded', () => {
// Hide the progress indicator
hideProgressIndicator()
// Generate a passphrase at start
generatePassphrase()
})
......@@ -4,104 +4,68 @@
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<meta http-equiv='X-UA-Compatible' content='ie=edge'>
<title>Hypha Spike: DAT 1</title>
<title>Hypha Spike: WebRTC 1</title>
<link rel='stylesheet' href='style.css'>
</head>
<body>
<h1>Hypha Spike: DAT 1</h1>
<form id='setupForm'>
<fieldset>
<legend>Public details</legend>
<div>
<label for='domain'>Domain name:</label>
<input type='text' id='domain' value='ar.al' readonly>
</div>
<div>
<label>Passphrase:</label>
<input type='text' id='passphrase' readonly>
<div style='position: relative;'>
<!-- Courtesy: http://tobiasahlin.com/spinkit/ -->
<button id='change' type='submit'>Change</button>
<div id='indeterminateProgressIndicator' class="spinner">
<div class="rect1"></div>
<div class="rect2"></div>
<div class="rect3"></div>
<div class="rect4"></div>
<div class="rect5"></div>
</div>
<h1>Hypha Spike: WebRTC 1</h1>
<form id='connectionForm'>
<div>
<label>Hyphalink:</label>
<input type='text' id='hyphalink'>
<div style='position: relative;'>
<!-- Courtesy: http://tobiasahlin.com/spinkit/ -->
<button id='connect' type='submit'>Connect</button>
<div id='indeterminateProgressIndicator' class="spinner">
<div class="rect1"></div>
<div class="rect2"></div>
<div class="rect3"></div>
<div class="rect4"></div>
<div class="rect5"></div>
</div>
</div>
<div>
<label for='publicSigningKey'>Hyphalink:</label>
<input type='text' id='publicSigningKey'>
</div>
<div>
<label for='generated'>Hypercore created?</label>
<input type='text' id='generated' value='No'>
</div>
<div>
<label>Hypercore signals:</label>
<div>
<span id='readySignal' class='signal'><span class='off'></span><span class='on'></span> Ready</span>
<span id='dataSignal' class='signal'><span class='off'></span><span class='on'></span> Data</span>
<span id='errorSignal' class='signal'><span class='off'></span><span class='on'></span> Error</span>
<span id='appendSignal' class='signal'><span class='off'></span><span class='on'></span> Append</span>
<span id='downloadSignal' class='signal'><span class='off'></span><span class='on'></span></span>
<span id='uploadSignal' class='signal'><span class='off'></span><span class='on'></span></span>
<span id='syncSignal' class='signal'><span class='off'></span><span class='on'></span> Sync</span>
<span id='closeSignal' class='signal'><span class='off'></span><span class='on'></span> Close</span>
</div>
</div>
</div>
<div>
<label for='hypercoreContents'>Hypercore contents</label>
<textarea id='hypercoreContents'></textarea>
</div>
<div>
<label for='generated'>Hypercore created?</label>
<input type='text' id='generated' value='No'>
</div>
<div>
<label>Hypercore signals:</label>
<div>
<label for='errors'>Errors:</label>
<textarea id='errors'>None</textarea>
<span id='readySignal' class='signal'><span class='off'></span><span class='on'></span> Ready</span>
<span id='dataSignal' class='signal'><span class='off'></span><span class='on'></span> Data</span>
<span id='errorSignal' class='signal'><span class='off'></span><span class='on'></span> Error</span>
<span id='appendSignal' class='signal'><span class='off'></span><span class='on'></span> Append</span>
<span id='downloadSignal' class='signal'><span class='off'></span><span class='on'></span></span>
<span id='uploadSignal' class='signal'><span class='off'></span><span class='on'></span></span>
<span id='syncSignal' class='signal'><span class='off'></span><span class='on'></span> Sync</span>
<span id='closeSignal' class='signal'><span class='off'></span><span class='on'></span> Close</span>
</div>
</div>
<h3>Notes</h3>
<p>Your domain name and <em>hyphalink</em> are two ways for other people to find your Hypha. The difference is that your hyphalink is decentralised and resilient to censorship. If your domain registrar confiscates or blocks your domain, people will still be able to reach your Hypha as long as there is at least one replica of it on the Internet.</p>
<div>
<label for='hypercoreContents'>Hypercore contents</label>
<textarea id='hypercoreContents'></textarea>
</div>
<p>Your passphrase is the only thing that protects your Hypha. It is randomly chosen for you using <a href='https://www.eff.org/dice'>a method called diceware</a> that would take the most sophisticated computers today hundreds of millions of years to crack<sup>1</sup>. If your passphrase is compromised, your Hypha is compromised. If you lose it, you can no longer write to your Hypha. There is no recovery process. Please save your passphrase in a password manager like <a href='https://www.passwordstore.org/'>pass</a> or <a href='https://1password.com/'>1password</a> and/or make a physical copy of it and store it in a safe place (like a safe, not on a post-it note attached to your monitor). Ideally, also try to memorise it.</p>
<p><em>1: The passphrase selection process has 100 bits of entropy.</em></p>
</fieldset>
<fieldset>
<legend>Internal details</legend>
<div>
<label for='privateSigningKey'>Private signing key:</label>
<textarea id='privateSigningKey'></textarea>
</div>
<div>
<label for='publicEncryptionKey'>Public encryption key:</label>
<input type='text' id='publicEncryptionKey'>
</div>
<div>
<label for='privateEncryptionKey'>Private encryption key:</label>
<input type='text' id='privateEncryptionKey'>
</div>
<div>
<label for='errors'>Errors:</label>
<textarea id='errors'>None</textarea>
</div>
<h3>Notes</h3>
<h3>Instructions</h3>
<p>The generated keys are Ed25519 (signing) and Curve25519 (encryption). The <em>hyphalink</em> is the public signing key.</em></p>
<ul>
<li>Run the Hypha DAT-1 Spike in a separate browser and note the hyphalink.</li>
<li>Run a local instance of signalhub</li>
<li>Enter the hyphalink from the DAT-1 Spike</li>
</ul>
</fieldset>
<p>The hypercore from the DAT-1 Spike should replicate – browser to browser ­– via WebRTC.</p>
</form>
......
......@@ -24,10 +24,6 @@ form {
width: 100%;
}
fieldset {
border: 0;
}
legend {
font-size: 1.25em;
font-weight: 600;
......@@ -38,7 +34,7 @@ legend {
width: 100%;
}
fieldset > div {
form > div {
display: grid;
grid-template-columns: 200px 1fr 100px;
border: 0;
......@@ -56,7 +52,7 @@ form h3 {
padding-left: 200px;
}
form p {
form p, ul {
padding-left: 200px;
padding-right: 100px;
font-size: 0.9em;
......@@ -85,7 +81,7 @@ footer {
border-top: 1px solid black;
}
#change {
#connect {
position: absolute;
top: 0;
left: 0.5em;
......
//
// Hypha: A native client for testing replication of a single hypercore.
//
const hypercore = require('hypercore')
const ram = require('random-access-memory')
const hyperswarm = require('@hyperswarm/network')
const { pipeline } = require('stream')
const { discoveryKey } = require('hypercore/lib/crypto')
const swarm = hyperswarm()
// Basic argument validation.
if (process.argv.length !== 3) {
console.log(`Usage: node index.js <read key to replicate>`)
process.exit()
}
const readKeyInHex = process.argv[2]
console.log(`\nAttempting to find and replicate hypercore with read key:\n${readKeyInHex}\n`)
const readKeyBuffer = Buffer.from(readKeyInHex, 'hex')
const discoveryKeyBuffer = discoveryKey(readKeyBuffer)
const discoveryKeyInHex = discoveryKeyBuffer.toString('hex')
// Create the local hypercore instance
const localCore = hypercore((filename) => ram(filename), readKeyBuffer, {
createIfMissing: false,
overwrite: false,
valueEncoding: 'json',
onwrite: (index, data, peer, next) => {
// console.log(`Feed: [onWrite] index = ${index}, peer = ${peer}, data:`)
// console.log(data)
next()
}
})
const localReadStream = localCore.createReadStream({live: true})
localReadStream.on('data', (data) => {
console.log('[Replicate]', data)
})
localCore.on('ready', () => {
console.log('Local core ready.')
// HACK: Just for now, make sure we only connect once
let connected = false
//
// Join the swarm
//
swarm.join(discoveryKeyBuffer, {
lookup: true, // find and connect to peers.
announce: true // optional: announce self as a connection target.
})
swarm.on('connection', (remoteNativeStream, details) => {
// HACK: only handle first connection
if (connected) return
connected = true
console.log(`Joined swarm for read key ${readKeyInHex}, discovery key ${discoveryKeyInHex}`)
// Replicate!
console.log('About to replicate!')
// Create the local replication stream.
const localReplicationStream = localCore.replicate({
// TODO: why is Jim’s shopping list example setting encrypt to false?
// The encryption of __what__ does this affect?
// (I haven’t even tested this yet with it set to true to limit the variables.)
encrypt: false,
live: true
})
pipeline(
remoteNativeStream,
localReplicationStream,
remoteNativeStream,
(error) => {
console.log(`Pipe closed for ${readKeyInHex}`, error && error.message)
}
)
})
})
//
// Hypha server.
// Hypha WebRTC node.
//
const fs = require('fs')
const https = require('https')
const { pipeline } = require('stream')
const express = require('express')
const expressWebSocket = require('express-ws')
const websocketStream = require('websocket-stream/stream')
const ram = require('random-access-memory')
const hypercore = require('hypercore')
const hyperswarm = require('@hyperswarm/network')
const budo = require('budo')
const babelify = require('babelify')
const router = express.Router()
const hypercores = {}
const server = budo('client/index.js', {