Skip to content

Instantly share code, notes, and snippets.

@aynik
Last active January 15, 2018 14:30
Show Gist options
  • Save aynik/e38c74209755f668849d6658b4226afa to your computer and use it in GitHub Desktop.
Save aynik/e38c74209755f668849d6658b4226afa to your computer and use it in GitHub Desktop.
Secure peer to peer coin flip over WebRTC
#!/usr/bin/env node
// dependencies: npm install --global wrtc
// usage: node p2p-coinflip.js [offer?]
const crypto = require('crypto')
const readline = require('readline')
const zlib = require('zlib')
const {
RTCPeerConnection,
RTCSessionDescription
} = require('/usr/local/lib/node_modules/wrtc')
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
const iceServers = [
{ url: 'stun:stun.l.google.com:19302' }
]
let pc, channel
const pcSettings = [
{ iceServers },
{ optional: [{ DtlsSrtpKeyAgreement: false }] }
]
let isOffering = false
const state = {
choice: crypto.randomBytes(32),
hash: null
}
const makeOffer = () => {
isOffering = true
pc = new RTCPeerConnection(pcSettings)
channel = pc.createDataChannel('p2choice')
handleDataChannel()
pc.createOffer((description) => {
setLocalDescription(description)
}, errorHandler)
pc.onicecandidate = (({ candidate }) => {
if (!candidate) {
const offerPacket = zlib.gzipSync(JSON.stringify(pc.localDescription))
console.log('offer:', offerPacket.toString('hex'))
rl.question('answer: ', (answerPacket) => {
const answer = zlib.gunzipSync(new Buffer(answerPacket, 'hex'))
setAnswer(answer)
})
}
})
}
const setOffer = (offerJSON) => {
const offer = new RTCSessionDescription(JSON.parse(offerJSON))
pc = new RTCPeerConnection(pcSettings)
pc.onicecandidate = ({ candidate }) => {
if (!candidate) {
const answerPacket = zlib.gzipSync(JSON.stringify(pc.localDescription))
console.log('answer:', answerPacket.toString('hex'))
}
}
pc.ondatachannel = (event) => {
channel = event.channel
channel.binaryType = 'arraybuffer'
handleDataChannel()
}
pc.setRemoteDescription(offer, handleCreateAnswer, errorHandler)
}
const setLocalDescription = (description) => {
pc.setLocalDescription(description, noHandler, errorHandler)
}
const setAnswer = (answerJSON) => {
pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(answerJSON)))
}
const handleCreateAnswer = () => {
pc.createAnswer(setLocalDescription, errorHandler)
}
const handleDataChannel = () => {
channel.onopen = () => {
//setTimeout(() => win('timeout'), 1000)
if (isOffering) {
sendChoiceHash()
}
}
channel.onclose = () => win('remote closed')
channel.onmessage = messageHandler
channel.onerror = errorHandler
}
const noHandler = () => null
const messageHandler = ({ data }) => {
if (!state.hash) {
state.hash = new Buffer(data)
if (!isOffering) sendChoiceHash()
else sendChoice()
} else {
if (!isOffering) sendChoice()
checkHash(data)
}
}
const errorHandler = (err) => {
console.error(err)
process.exit(1)
}
const sendChoiceHash = () => {
const hash = crypto.createHash('sha256')
hash.update(state.choice)
channel.send(hash.digest())
}
const sendChoice = () => {
channel.send(state.choice)
}
const sendReplayChoiceHash = () => {
channel.send(state.hash)
}
const sendReplayChoice = (data) => {
state.choice = data
channel.send(data)
}
const checkHash = (choice) => {
const hash = crypto.createHash('sha256')
choice = new Buffer(choice)
hash.update(choice)
const digest = hash.digest()
if (digest.equals(state.hash)) {
const ours = toBitArray(state.choice)
.reduce((r, b) => r ^ b, 0)
const theirs = toBitArray(choice)
.reduce((r, b) => r ^ b, 0)
if (isOffering) {
if (ours === theirs) win()
else lose()
} else {
if (ours !== theirs) win()
else lose()
}
} else {
console.log('remote cheated:',
`sha256(${choice.toString('hex')}) should be`,
`${digest.toString('hex')},`,
`instead it was ${state.hash.toString('hex')}`)
win()
}
}
const toBitArray = (x) => {
let bits = []
let tmp = x
if (typeof x === 'number') {
while (tmp > 0) {
bits.push(tmp % 2)
tmp = Math.floor(tmp / 2)
}
return bits.reverse()
}
for (var i = 0; i < x.length; i++) {
bits = bits.concat(toBitArray(x[i]))
}
return bits
}
const win = (info) => {
console.log(`${info ? info + ', ' : ''}you won`)
process.exit(0)
}
const lose = (info) => {
console.log(`${info ? info + ', ' : ''}you lost`)
process.exit(1)
}
if (!process.argv[2]) {
makeOffer()
} else {
const offer = zlib.gunzipSync(new Buffer(process.argv[2], 'hex'))
setOffer(offer)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment