Last active
January 15, 2018 14:30
-
-
Save aynik/e38c74209755f668849d6658b4226afa to your computer and use it in GitHub Desktop.
Secure peer to peer coin flip over WebRTC
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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