Skip to content

Instantly share code, notes, and snippets.

@Bren2010
Last active October 7, 2023 03:42

Revisions

  1. Brendan Mc revised this gist Apr 14, 2013. 1 changed file with 0 additions and 1 deletion.
    1 change: 0 additions & 1 deletion secure.coffee
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,6 @@
    es = require 'event-stream'
    crypto = require 'crypto'
    msgpack = require 'msgpack'
    readline = require 'readline'
    {BigInteger} = require 'bigdecimal'
    salt = 'vpfSIDPgJ5tBt7gZgnGQeG8G6r0q4fDkDWWtZD7zqAsYWw2Ep81vIjqC3gJM'

  2. Brendan Mc revised this gist Apr 14, 2013. 3 changed files with 71 additions and 59 deletions.
    91 changes: 51 additions & 40 deletions hearsay.coffee
    Original file line number Diff line number Diff line change
    @@ -4,6 +4,7 @@ path = require 'path'
    async = require 'async'
    crypto = require 'crypto'
    readline = require 'readline'
    msgpack = require 'msgpack'
    secure = require './secure'
    {BigInteger} = require 'bigdecimal'

    @@ -46,7 +47,7 @@ server = net.createServer (conn) ->

    else if not isNaN port and not ours
    # Pass on.
    drain.push msg: ['hookup', address, data[2]]
    drain.push cmd: 'hookup', address: address, tracker: data[2]

    when 'mate'
    # If ours, acknowledge. If not, throw away.
    @@ -70,11 +71,13 @@ server.listen ->
    # Peers
    drainer = (task, done) ->
    if peersLength is 0 then return
    cb = task.cb
    delete task.cb

    if (task.msg[0] is 'req' or task.msg[0] is 'res') and task.from?
    if (task.cmd is 'req' or task.cmd is 'res') and task.from?
    # Hash tracker
    c = crypto.createCipher 'aes-256-ctr', routingKey
    c.end task.msg[1], 'hex'
    c.end task.tracker, 'hex'
    tracker = c.read().toString 'hex'

    # Generate lists of peers to reference.
    @@ -92,10 +95,11 @@ drainer = (task, done) ->

    a > b

    if task.msg[0] is res then [first, second] = [second, first]
    if task.cmd is 'res' then [first, second] = [second, first]

    # Choose next.
    n = first.indexOf task.from
    delete task.from
    addr = second[n]
    else
    # Randomly choose peer.
    @@ -104,16 +108,16 @@ drainer = (task, done) ->
    if n is m then break else m++

    # Encrypt.
    peers[addr].writeCipher.write task.msg.join ' '
    ct = peers[addr].writeCipher.read().toString 'hex'
    peers[addr].writeCipher.write msgpack.pack task
    ct = peers[addr].writeCipher.read().toString 'base64'

    # MAC.
    mac = crypto.createHmac 'sha256', peers[addr].writeMACKey
    mac.end ct, 'hex'
    tag = mac.read().toString 'hex'
    mac.end ct, 'base64'
    tag = mac.read().toString 'base64'

    peers[addr].conn.write ct + tag + '\n', 'utf8', (err) ->
    if task.cb? then task.cb err
    if cb? then cb err
    done err

    drain = async.queue drainer, 1
    @@ -135,45 +139,50 @@ peer = (conn, port, init) ->
    delete peers[address]

    conn.on 'line', (data) ->
    if data[0] is 'hookup' and data.length is 3
    if data.cmd is 'hookup' and data.address? and data.tracker?
    want = secure.want peersLength
    know = peers[data[1]]?
    ours = pendingHookups[data[2]]?
    [host, port] = data[1].split ':'
    know = peers[data.address]?
    ours = pendingHookups[data.tracker]?
    [host, port] = data.address.split ':'

    if want and not know and not ours
    # Connect and befriend.
    conn = net.connect port, host, ->
    conn.write 'mate ' + serverPort + ' ' + data[2]
    conn.write 'mate ' + serverPort + ' ' + data.tracker

    conn.once 'data', (res) ->
    if res.toString() is 'ok' then peer conn, port, true

    conn.on 'error', ->

    else if not ours and not secure.drop()
    drain.push msg: data
    drain.push data

    else if data[0] is 'distr' and data.length is 3
    know = chunks[data[1]]?
    else if data.cmd is 'distr' and data.tag? and data.chunk?
    know = chunks[data.tag]?

    if secure.want() and not know
    chunks[data[1]] = data: data[2], own: false
    chunks[data.tag] = data: data.chunk, own: false
    chunksLength++

    if not secure.drop() then drain.push msg: data
    if not secure.drop() then drain.push data

    else if data[0] is 'req' and data.length is 3
    if chunks[data[2]]?
    drain.push msg: ['res', data[1], chunks[data[2]].data]
    else if data.cmd is 'req' and data.tracker? and data.tag?
    if chunks[data.tag]?
    drain.push {
    cmd: 'res',
    tracker: data.tracker,
    chunk: chunks[data.tag].data
    }

    else if not secure.drop()
    drain.push msg: data
    drain.push data

    else if data[0] is 'res' and data.length is 3
    if pendingDownloads[data[1]]?
    pendingDownloads[data[1]](data[2])
    else if data.cmd is 'res' and data.tracker? and data.chunk?
    if pendingDownloads[data.tracker]?
    pendingDownloads[data.tracker](data.chunk)
    else if not secure.drop()
    drain.push msg: data
    drain.push data

    # General maintenance.
    maintenance = ->
    @@ -217,7 +226,7 @@ publish = () ->
    # Publish chunks.
    args = ({tag: tag, chunk: chunk} for tag, chunk of chunks)
    pub = (item, done) ->
    drain.push msg: ['distr', item.tag, item.chunk.data], cb: ->
    drain.push cmd: 'distr', tag: item.tag, chunk: item.chunk.data, cb: ->
    setTimeout done, Math.random() * 50000

    async.mapSeries args, pub, -> setTimeout publish, 10000
    @@ -295,15 +304,15 @@ frontend = ->

    # Encrypt chunk.
    cipher.write buff.slice(0, n)
    chunk = cipher.read().toString 'base64'
    chunk = cipher.read()

    # Hash encrypted chunk.
    h = crypto.createHash 'sha256'
    h.end chunk, 'base64'
    tag = h.read().toString 'hex'
    h.end chunk
    tag = h.read().toString 'base64'

    # Publish
    chunks[tag] = data: chunk, ours: true
    chunks[tag] = data: chunk.toJSON(), ours: true
    chunksLength++

    tb = new Buffer tag
    @@ -325,7 +334,7 @@ frontend = ->
    console.log err
    rl.prompt true
    else
    key = secure.randomHex()
    key = secure.randomBase64()
    kb = new Buffer key
    fs.write fds[1], kb, 0, kb.length, null, ->
    c = crypto.createCipher 'aes-256-ctr', key
    @@ -337,9 +346,9 @@ frontend = ->
    if line.length isnt 1 then return rl.prompt true

    nextLine = (fd, cb) ->
    buff = new Buffer 64
    fs.read fd, buff, 0, 64, null, (err, n, buff) ->
    if err is null and n is 64
    buff = new Buffer 44
    fs.read fd, buff, 0, 44, null, (err, n, buff) ->
    if err is null and n is 44
    cb buff.toString()
    else
    cb()
    @@ -361,24 +370,26 @@ frontend = ->
    delete pendingDownloads[tracker]
    fetchChunk inFd, outFd, cipher, tag
    else
    chunk = new Buffer chunk

    # Hash encrypted chunk to check integrity.
    h = crypto.createHash 'sha256'
    h.end chunk, 'base64'
    candidateTag = h.read().toString 'hex'
    h.end chunk
    candidateTag = h.read().toString 'base64'

    if tag isnt candidateTag then return
    delete pendingDownloads[tracker]

    # Decrypt chunk.
    cipher.write chunk, 'base64'
    cipher.write chunk
    chunk = cipher.read()

    # Output, and get next chunk.
    fs.write outFd, chunk, 0, chunk.length, null, ->
    nextLine inFd, (tag) ->
    fetchChunk inFd, outFd, cipher, tag

    drain.push msg: ['req', tracker, tag]
    drain.push cmd: 'req', tracker: tracker, tag: tag
    setTimeout timeout, 5000, tracker

    # Get files needed, open them, and prepare for downloading.
    6 changes: 4 additions & 2 deletions package.json
    Original file line number Diff line number Diff line change
    @@ -5,6 +5,8 @@
    "dependencies": {
    "coffee-script": "*",
    "bigdecimal": "*",
    "async": "*"
    "async": "*",
    "msgpack": "*",
    "event-stream": "*"
    }
    }
    }
    33 changes: 16 additions & 17 deletions secure.coffee
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,6 @@
    es = require 'event-stream'
    crypto = require 'crypto'
    msgpack = require 'msgpack'
    readline = require 'readline'
    {BigInteger} = require 'bigdecimal'
    salt = 'vpfSIDPgJ5tBt7gZgnGQeG8G6r0q4fDkDWWtZD7zqAsYWw2Ep81vIjqC3gJM'
    @@ -24,29 +26,25 @@ exports.remote = (conn, init) ->
    writeCipher = crypto.createCipher 'aes-256-ctr', writeCKey

    # Begin handling input.
    opts =
    input: conn
    output: conn
    terminal: false

    i = readline.createInterface opts
    i.on 'line', (line) ->
    handle = (line, cb) ->
    # Decrypt and validate.
    ct = line.slice 0, line.length - 64
    tag = line.slice line.length - 64
    ct = line.slice 0, line.length - 44
    tag = line.slice line.length - 44

    readCipher.write ct, 'hex'
    pt = readCipher.read().toString()
    readCipher.write ct, 'base64'
    pt = msgpack.unpack readCipher.read()

    readMAC = crypto.createHmac 'sha256', readMKey
    readMAC.end ct, 'hex'
    candidateTag = readMAC.read().toString 'hex'
    readMAC.end ct, 'base64'
    candidateTag = readMAC.read().toString 'base64'
    if candidateTag != tag then return conn.end()

    # Handle data.
    data = pt.split ' '
    conn.emit 'line', data
    conn.emit 'line', pt
    cb null, null

    p = es.pipeline conn, es.split(), es.map handle
    p.on 'error', ->
    conn.emit 'secure', writeCipher, writeMKey

    # Probabilistically decides if we 'want' something.
    @@ -60,7 +58,7 @@ exports.want = (n) ->

    # Probablistically decides if we should drop a packet.
    exports.drop = ->
    x = exports.random 1000
    x = exports.random 10
    if x is 2 then true else false

    # Choose a random number in the range [0, max)
    @@ -73,4 +71,5 @@ exports.random = (max) ->
    (Number) x

    # Returns a string of random hex.
    exports.randomHex = (n = 32) -> crypto.randomBytes(n).toString 'hex'
    exports.randomHex = (n = 32) -> crypto.randomBytes(n).toString 'hex'
    exports.randomBase64 = (n = 32) -> crypto.randomBytes(n).toString 'base64'
  3. Brendan Mc. revised this gist Apr 9, 2013. 1 changed file with 3 additions and 2 deletions.
    5 changes: 3 additions & 2 deletions HearSay.md
    Original file line number Diff line number Diff line change
    @@ -8,8 +8,9 @@ routing throughout them while maintaining anonymity and semantic security.

    However, lets be honest with ourselves for a second. Don't use this to fight an
    oppressive regime. I can not (and will not try) to 'prove' its security, and I
    have no idea how this system will perform under high stress or in large clusters.
    All of this code is simply the product of basic cryptographic research.
    have no idea how this system will perform under high stress or in large
    clusters. All of this code is simply the product of basic cryptographic research
    and application.

    > Beware of bugs in the [below] code; I have only proved it correct, not tried it. ~ Donald Knuth
  4. Brendan Mc. revised this gist Apr 9, 2013. 1 changed file with 6 additions and 6 deletions.
    12 changes: 6 additions & 6 deletions HearSay.md
    Original file line number Diff line number Diff line change
    @@ -2,14 +2,14 @@ HearSay
    =======
    The HearSay P2P File Sharer; a response to The Copyright Alert System, as well
    as several other internet regulation attempts. The goal of this project is to
    provide semi-anonymous and confidential file sharing to any community that
    pleases. Consists of several proofs of concepts such as the formation of
    ad-hoc mix networks and routing throughout them while maintaining anonymity and
    semantic security.
    prove the viability of semi-anonymous and confidential file sharing. Consists
    of several proofs of concepts such as the formation of ad-hoc mix networks and
    routing throughout them while maintaining anonymity and semantic security.

    *Note:* Lets just be honest with ourselves. Don't use this to fight an
    However, lets be honest with ourselves for a second. Don't use this to fight an
    oppressive regime. I can not (and will not try) to 'prove' its security, and I
    have no idea how this system will perform under high stress or in large clusters.
    have no idea how this system will perform under high stress or in large clusters.
    All of this code is simply the product of basic cryptographic research.

    > Beware of bugs in the [below] code; I have only proved it correct, not tried it. ~ Donald Knuth
  5. Brendan Mc. revised this gist Apr 9, 2013. 1 changed file with 0 additions and 1 deletion.
    1 change: 0 additions & 1 deletion HearSay.md
    Original file line number Diff line number Diff line change
    @@ -33,5 +33,4 @@ To Do
    2. Re-uploads
    3. Limiting number of failed requests before giving up.
    4. Requesting a specific port for the server to listen on.
    5. Add a way for peers to validate their connections with each other through a trusted peer.
    5. Cleaning, perhaps?
  6. Brendan Mc. revised this gist Apr 9, 2013. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions HearSay.md
    Original file line number Diff line number Diff line change
    @@ -33,4 +33,5 @@ To Do
    2. Re-uploads
    3. Limiting number of failed requests before giving up.
    4. Requesting a specific port for the server to listen on.
    5. Add a way for peers to validate their connections with each other through a trusted peer.
    5. Cleaning, perhaps?
  7. Brendan Mc. revised this gist Apr 9, 2013. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion HearSay.md
    Original file line number Diff line number Diff line change
    @@ -23,7 +23,7 @@ Usage
    -----
    1. `chunks` - Lists the hashes of all the chunks you are currently hosting. These consist of the chunks you've uploaded as well as the chunks that you're co-hosting.
    2. `peers` - Lists all of the peers on a network that you're currently connected to.
    3. `enter {host} {port}` - Attempts to enter a cluster through the given node. This requires that your server be publicly available. (Through the use of port forwarding, or a DMZ.)
    3. `enter {host} {port}` - Attempts to enter a cluster through the given node. This requires that your server be publicly available as well. (Through the use of port forwarding, or a DMZ.)
    4. `upload` - Will then prompt you for an input file (the file to be uploaded) and an output file (where the torrent data will be written), and upload the input file to the cluster.
    5. `download` - Will then prompt you for an input file (the file with the torrent data) and an output file (where the file contents will be written), and attempt to download the file.

  8. Brendan Mc. revised this gist Apr 9, 2013. 1 changed file with 3 additions and 2 deletions.
    5 changes: 3 additions & 2 deletions HearSay.md
    Original file line number Diff line number Diff line change
    @@ -23,7 +23,7 @@ Usage
    -----
    1. `chunks` - Lists the hashes of all the chunks you are currently hosting. These consist of the chunks you've uploaded as well as the chunks that you're co-hosting.
    2. `peers` - Lists all of the peers on a network that you're currently connected to.
    3. `enter {host} {port}` - Attempts to enter a cluster through the given node.
    3. `enter {host} {port}` - Attempts to enter a cluster through the given node. This requires that your server be publicly available. (Through the use of port forwarding, or a DMZ.)
    4. `upload` - Will then prompt you for an input file (the file to be uploaded) and an output file (where the torrent data will be written), and upload the input file to the cluster.
    5. `download` - Will then prompt you for an input file (the file with the torrent data) and an output file (where the file contents will be written), and attempt to download the file.

    @@ -32,4 +32,5 @@ To Do
    1. Allow a public organization to sponsor uploads to a network without being legally responsible for them.
    2. Re-uploads
    3. Limiting number of failed requests before giving up.
    4. Cleaning, perhaps?
    4. Requesting a specific port for the server to listen on.
    5. Cleaning, perhaps?
  9. Brendan Mc. revised this gist Apr 9, 2013. 1 changed file with 6 additions and 1 deletion.
    7 changes: 6 additions & 1 deletion HearSay.md
    Original file line number Diff line number Diff line change
    @@ -21,7 +21,12 @@ Setup

    Usage
    -----
    1.
    1. `chunks` - Lists the hashes of all the chunks you are currently hosting. These consist of the chunks you've uploaded as well as the chunks that you're co-hosting.
    2. `peers` - Lists all of the peers on a network that you're currently connected to.
    3. `enter {host} {port}` - Attempts to enter a cluster through the given node.
    4. `upload` - Will then prompt you for an input file (the file to be uploaded) and an output file (where the torrent data will be written), and upload the input file to the cluster.
    5. `download` - Will then prompt you for an input file (the file with the torrent data) and an output file (where the file contents will be written), and attempt to download the file.

    To Do
    -----
    1. Allow a public organization to sponsor uploads to a network without being legally responsible for them.
  10. Brendan Mc. revised this gist Apr 9, 2013. 1 changed file with 1 addition and 2 deletions.
    3 changes: 1 addition & 2 deletions HearSay.md
    Original file line number Diff line number Diff line change
    @@ -11,8 +11,7 @@ semantic security.
    oppressive regime. I can not (and will not try) to 'prove' its security, and I
    have no idea how this system will perform under high stress or in large clusters.

    > Beware of bugs in the [below] code; I have only proved it correct, not tried it.
    ~ Donald Knuth
    > Beware of bugs in the [below] code; I have only proved it correct, not tried it. ~ Donald Knuth
    Setup
    -----
  11. Brendan Mc. revised this gist Apr 9, 2013. 1 changed file with 12 additions and 4 deletions.
    16 changes: 12 additions & 4 deletions HearSay.md
    Original file line number Diff line number Diff line change
    @@ -2,19 +2,27 @@ HearSay
    =======
    The HearSay P2P File Sharer; a response to The Copyright Alert System, as well
    as several other internet regulation attempts. The goal of this project is to
    provide semi-anonymous and confidential file sharing among communities. Consists
    of several proofs of concepts such as the formation of ad-hoc mix networks and
    routing throughout them while maintaining anonymity and semantic security.
    provide semi-anonymous and confidential file sharing to any community that
    pleases. Consists of several proofs of concepts such as the formation of
    ad-hoc mix networks and routing throughout them while maintaining anonymity and
    semantic security.

    *Note:* Lets just be honest with ourselves. Don't use this to fight an
    oppressive regime.
    oppressive regime. I can not (and will not try) to 'prove' its security, and I
    have no idea how this system will perform under high stress or in large clusters.

    > Beware of bugs in the [below] code; I have only proved it correct, not tried it.
    ~ Donald Knuth

    Setup
    -----
    - Dependencies: Node.js, npm
    - Installation: `npm install` (May require sudo.)
    - Start: `coffee hearsay` (Never run with sudo.)

    Usage
    -----
    1.
    To Do
    -----
    1. Allow a public organization to sponsor uploads to a network without being legally responsible for them.
  12. Brendan Mc. revised this gist Apr 9, 2013. 1 changed file with 4 additions and 3 deletions.
    7 changes: 4 additions & 3 deletions HearSay.md
    Original file line number Diff line number Diff line change
    @@ -1,9 +1,10 @@
    HearSay
    =======
    The HearSay P2P File Sharer; a response to The Copyright Alert System, as well
    as several other internet regulation attempts. Consists of several proofs of
    concepts such as the formation of ad-hoc mix networks and routing throughout
    them while maintaining anonymity and semantic security.
    as several other internet regulation attempts. The goal of this project is to
    provide semi-anonymous and confidential file sharing among communities. Consists
    of several proofs of concepts such as the formation of ad-hoc mix networks and
    routing throughout them while maintaining anonymity and semantic security.

    *Note:* Lets just be honest with ourselves. Don't use this to fight an
    oppressive regime.
  13. Brendan Mc revised this gist Apr 8, 2013. 1 changed file with 9 additions and 9 deletions.
    18 changes: 9 additions & 9 deletions hearsay.coffee
    Original file line number Diff line number Diff line change
    @@ -300,14 +300,14 @@ frontend = ->
    # Hash encrypted chunk.
    h = crypto.createHash 'sha256'
    h.end chunk, 'base64'
    tag = h.read()
    tagString = tag.toString 'hex'
    tag = h.read().toString 'hex'

    # Publish
    chunks[tagString] = data: chunk, ours: true
    chunks[tag] = data: chunk, ours: true
    chunksLength++

    fs.write outFd, tag, 0, tag.length, null, ->
    tb = new Buffer tag
    fs.write outFd, tb, 0, tb.length, null, ->
    readChunk inFd, outFd, cipher

    # Get files needed, open them, and prepare for uploading.
    @@ -326,7 +326,7 @@ frontend = ->
    rl.prompt true
    else
    key = secure.randomHex()
    kb = new Buffer key, 'hex'
    kb = new Buffer key
    fs.write fds[1], kb, 0, kb.length, null, ->
    c = crypto.createCipher 'aes-256-ctr', key
    readChunk fds[0], fds[1], c
    @@ -337,10 +337,10 @@ frontend = ->
    if line.length isnt 1 then return rl.prompt true

    nextLine = (fd, cb) ->
    buff = new Buffer 32
    fs.read fd, buff, 0, 32, null, (err, n, buff) ->
    if err is null and n is 32
    cb buff.toString 'hex'
    buff = new Buffer 64
    fs.read fd, buff, 0, 64, null, (err, n, buff) ->
    if err is null and n is 64
    cb buff.toString()
    else
    cb()

  14. Brendan Mc. revised this gist Apr 8, 2013. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion secure.coffee
    Original file line number Diff line number Diff line change
    @@ -60,7 +60,7 @@ exports.want = (n) ->

    # Probablistically decides if we should drop a packet.
    exports.drop = ->
    x = exports.random 20
    x = exports.random 1000
    if x is 2 then true else false

    # Choose a random number in the range [0, max)
  15. Brendan Mc. revised this gist Apr 8, 2013. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions hearsay.coffee
    Original file line number Diff line number Diff line change
    @@ -262,6 +262,7 @@ frontend = ->
    line = line.trim().split ' '

    switch line[0]
    when "" then console.log ""
    when "chunks"
    console.log tag for tag of chunks

  16. Brendan Mc. revised this gist Apr 8, 2013. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion hearsay.coffee
    Original file line number Diff line number Diff line change
    @@ -121,7 +121,7 @@ drain = async.queue drainer, 1
    peer = (conn, port, init) ->
    # Secure the connection.
    secure.remote conn, init
    address = conn.address().address + ':' + port
    address = conn.remoteAddress + ':' + port

    conn.on 'secure', (writeCipher, writeMKey) ->
    peersLength++
  17. Brendan Mc. revised this gist Apr 7, 2013. 2 changed files with 5 additions and 3 deletions.
    4 changes: 3 additions & 1 deletion HearSay.md
    Original file line number Diff line number Diff line change
    @@ -17,4 +17,6 @@ Setup
    To Do
    -----
    1. Allow a public organization to sponsor uploads to a network without being legally responsible for them.
    2. Cleaning, perhaps?
    2. Re-uploads
    3. Limiting number of failed requests before giving up.
    4. Cleaning, perhaps?
    4 changes: 2 additions & 2 deletions hearsay.coffee
    Original file line number Diff line number Diff line change
    @@ -42,7 +42,7 @@ server = net.createServer (conn) ->
    conn.once 'data', (res) ->
    if res.toString() is 'ok' then peer conn, data[1], true

    conn.on 'error', (err) -> console.log err
    conn.on 'error', ->

    else if not isNaN port and not ours
    # Pass on.
    @@ -149,7 +149,7 @@ peer = (conn, port, init) ->
    conn.once 'data', (res) ->
    if res.toString() is 'ok' then peer conn, port, true

    conn.on 'error', (err) -> console.log err
    conn.on 'error', ->

    else if not ours and not secure.drop()
    drain.push msg: data
  18. Brendan Mc. revised this gist Apr 7, 2013. 1 changed file with 5 additions and 0 deletions.
    5 changes: 5 additions & 0 deletions HearSay.md
    Original file line number Diff line number Diff line change
    @@ -13,3 +13,8 @@ Setup
    - Dependencies: Node.js, npm
    - Installation: `npm install` (May require sudo.)
    - Start: `coffee hearsay` (Never run with sudo.)

    To Do
    -----
    1. Allow a public organization to sponsor uploads to a network without being legally responsible for them.
    2. Cleaning, perhaps?
  19. Brendan Mc. created this gist Apr 7, 2013.
    15 changes: 15 additions & 0 deletions HearSay.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,15 @@
    HearSay
    =======
    The HearSay P2P File Sharer; a response to The Copyright Alert System, as well
    as several other internet regulation attempts. Consists of several proofs of
    concepts such as the formation of ad-hoc mix networks and routing throughout
    them while maintaining anonymity and semantic security.

    *Note:* Lets just be honest with ourselves. Don't use this to fight an
    oppressive regime.

    Setup
    -----
    - Dependencies: Node.js, npm
    - Installation: `npm install` (May require sudo.)
    - Start: `coffee hearsay` (Never run with sudo.)
    413 changes: 413 additions & 0 deletions hearsay.coffee
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,413 @@
    fs = require 'fs'
    net = require 'net'
    path = require 'path'
    async = require 'async'
    crypto = require 'crypto'
    readline = require 'readline'
    secure = require './secure'
    {BigInteger} = require 'bigdecimal'

    # Network state
    # Server information
    [peers, pendingHookups, pendingDownloads, chunks] = [{}, {}, {}, {}]
    [peersLength, chunksLength, serverPort] = [0, 0, 0]
    routingKey = secure.randomHex()

    console.log 'Hearsay File Sharer v0.0.1'
    console.log 'Starting up...'

    # Start the server. Handles inter-cluster requests. (Insecure)
    server = net.createServer (conn) ->
    conn.once 'data', (data) =>
    data = data.toString().split ' '
    switch data[0]
    when 'hookup'
    address = conn.remoteAddress + ':' + data[1]
    conn.end()

    # 1. Check that given port is valid.
    # 2. Decide if we want this connection for ourselves.
    # 3. Make sure we don't know this peer.
    # 4. Make sure this isn't one of ours.
    port = parseInt data[1]
    want = secure.want peersLength
    know = peers[address]?
    ours = pendingHookups[data[2]]?

    if not isNaN(port) and want and not know and not ours
    # Connect and befriend.
    conn = net.connect data[1], conn.remoteAddress, ->
    conn.write 'mate ' + serverPort + ' ' + data[2]

    conn.once 'data', (res) ->
    if res.toString() is 'ok' then peer conn, data[1], true

    conn.on 'error', (err) -> console.log err

    else if not isNaN port and not ours
    # Pass on.
    drain.push msg: ['hookup', address, data[2]]

    when 'mate'
    # If ours, acknowledge. If not, throw away.
    if pendingHookups[data[2]]?
    delete pendingHookups[data[2]]
    conn.write 'ok'
    peer conn, data[1], false
    else
    conn.end 'no'

    # So there's never an 'unhandeled' error.
    conn.on 'error', ->

    server.listen ->
    serverPort = server.address().port

    console.log 'Server port:', serverPort
    frontend()


    # Peers
    drainer = (task, done) ->
    if peersLength is 0 then return

    if (task.msg[0] is 'req' or task.msg[0] is 'res') and task.from?
    # Hash tracker
    c = crypto.createCipher 'aes-256-ctr', routingKey
    c.end task.msg[1], 'hex'
    tracker = c.read().toString 'hex'

    # Generate lists of peers to reference.
    first = (addr for addr of peers).sort()
    second = (addr for addr of peers).sort (a, b) ->
    c = crypto.createCipher 'aes-256-ctr', routingKey
    c.write tracker, 'hex'
    c.end a
    a = c.read().toString 'hex'

    c = crypto.createCipher 'aes-256-ctr', routingKey
    c.write tracker, 'hex'
    c.end b
    b = c.read().toString 'hex'

    a > b

    if task.msg[0] is res then [first, second] = [second, first]

    # Choose next.
    n = first.indexOf task.from
    addr = second[n]
    else
    # Randomly choose peer.
    [n, m] = [secure.random(peersLength), 0]
    for addr of peers
    if n is m then break else m++

    # Encrypt.
    peers[addr].writeCipher.write task.msg.join ' '
    ct = peers[addr].writeCipher.read().toString 'hex'

    # MAC.
    mac = crypto.createHmac 'sha256', peers[addr].writeMACKey
    mac.end ct, 'hex'
    tag = mac.read().toString 'hex'

    peers[addr].conn.write ct + tag + '\n', 'utf8', (err) ->
    if task.cb? then task.cb err
    done err

    drain = async.queue drainer, 1

    peer = (conn, port, init) ->
    # Secure the connection.
    secure.remote conn, init
    address = conn.address().address + ':' + port

    conn.on 'secure', (writeCipher, writeMKey) ->
    peersLength++
    peers[address] =
    conn: conn
    writeCipher: writeCipher
    writeMACKey: writeMKey

    conn.on 'end', ->
    peersLength--
    delete peers[address]

    conn.on 'line', (data) ->
    if data[0] is 'hookup' and data.length is 3
    want = secure.want peersLength
    know = peers[data[1]]?
    ours = pendingHookups[data[2]]?
    [host, port] = data[1].split ':'

    if want and not know and not ours
    # Connect and befriend.
    conn = net.connect port, host, ->
    conn.write 'mate ' + serverPort + ' ' + data[2]

    conn.once 'data', (res) ->
    if res.toString() is 'ok' then peer conn, port, true

    conn.on 'error', (err) -> console.log err

    else if not ours and not secure.drop()
    drain.push msg: data

    else if data[0] is 'distr' and data.length is 3
    know = chunks[data[1]]?

    if secure.want() and not know
    chunks[data[1]] = data: data[2], own: false
    chunksLength++

    if not secure.drop() then drain.push msg: data

    else if data[0] is 'req' and data.length is 3
    if chunks[data[2]]?
    drain.push msg: ['res', data[1], chunks[data[2]].data]
    else if not secure.drop()
    drain.push msg: data

    else if data[0] is 'res' and data.length is 3
    if pendingDownloads[data[1]]?
    pendingDownloads[data[1]](data[2])
    else if not secure.drop()
    drain.push msg: data

    # General maintenance.
    maintenance = ->
    # Clean up old hookups.
    d = (new Date()).getTime()

    clean = (tracker) ->
    if d - pendingHookups[tracker] >= 5000
    delete pendingHookups[tracker]

    clean tracker for tracker of pendingHookups

    # Clean out unwanted chunks.
    clean = (tag) ->
    if chunks[tag].own is false and secure.drop()
    delete chunks[tag]

    clean tag for tag of chunks

    # Play matchmaker.
    if peersLength is 0 or peersLength >= 5 then return
    [n, m] = [secure.random(peersLength), 0]
    for addr of peers
    if n is m then break
    m++

    [host, port] = addr.split ':'
    conn = net.connect port, host, ->
    d = new Date()
    tracker = secure.randomHex()
    pendingHookups[tracker] = d.getTime()
    conn.end 'hookup ' + serverPort + ' ' + tracker

    conn.on 'error', ->

    setInterval maintenance, 5000

    publish = () ->
    if peersLength is 0 then return setTimeout publish, 10000

    # Publish chunks.
    args = ({tag: tag, chunk: chunk} for tag, chunk of chunks)
    pub = (item, done) ->
    drain.push msg: ['distr', item.tag, item.chunk.data], cb: ->
    setTimeout done, Math.random() * 50000

    async.mapSeries args, pub, -> setTimeout publish, 10000

    publish()

    # Command Line Interface
    frontend = ->
    # Completion service.
    completer = (line) ->
    [completions, hits] = [[], []]

    line = path.normalize line
    [p, n] = [path.dirname(line), path.basename(line)]
    exists = fs.existsSync p

    completions = if exists then fs.readdirSync p else []

    hits = completions.filter (c) -> c.indexOf(n) is 0

    if hits.length is 1
    h = hits[0]
    hits[0] = p
    hits[0] += if p[p.length - 1] is path.sep then '' else path.sep
    hits[0] += h

    stats = fs.statSync hits[0]
    if stats.isDirectory() then hits[0] += path.sep

    rec = if hits.length > 0 then hits else completions
    return [rec, line]

    # Create interface.
    rl = readline.createInterface {
    input: process.stdin
    output: process.stdout
    completer: completer
    }

    # Parses and handles lines entered by the user.
    rl.on 'line', (line) ->
    line = line.trim().split ' '

    switch line[0]
    when "chunks"
    console.log tag for tag of chunks

    when "peers"
    console.log addr for addr, _ of peers
    if peersLength is 0 then console.log 'No peers.'

    when "enter"
    if line.length isnt 3 then return rl.prompt true
    conn = net.connect line[2], line[1], ->
    d = new Date()
    tracker = secure.randomHex()
    pendingHookups[tracker] = d.getTime()
    conn.end 'hookup ' + serverPort + ' ' + tracker

    end = (error) =>
    if error? then console.log error
    rl.prompt true

    conn.on 'error', end
    conn.on 'end', end

    when "upload"
    if line.length isnt 1 then return rl.prompt true
    readChunk = (inFd, outFd, cipher) ->
    buff = new Buffer 262144
    fs.read inFd, buff, 0, 262144, null, (err, n, buff) ->
    if err isnt null or n is 0
    return rl.prompt true

    # Encrypt chunk.
    cipher.write buff.slice(0, n)
    chunk = cipher.read().toString 'base64'

    # Hash encrypted chunk.
    h = crypto.createHash 'sha256'
    h.end chunk, 'base64'
    tag = h.read()
    tagString = tag.toString 'hex'

    # Publish
    chunks[tagString] = data: chunk, ours: true
    chunksLength++

    fs.write outFd, tag, 0, tag.length, null, ->
    readChunk inFd, outFd, cipher

    # Get files needed, open them, and prepare for uploading.
    questions = [
    {ask: 'Input file? ', mode: 'r'},
    {ask: 'Output file? ', mode: 'w'},
    ]

    openFile = (task, done) ->
    rl.question task.ask, (file) ->
    fs.open file, task.mode, null, done

    async.mapSeries questions, openFile, (err, fds) ->
    if err?
    console.log err
    rl.prompt true
    else
    key = secure.randomHex()
    kb = new Buffer key, 'hex'
    fs.write fds[1], kb, 0, kb.length, null, ->
    c = crypto.createCipher 'aes-256-ctr', key
    readChunk fds[0], fds[1], c

    return

    when "download"
    if line.length isnt 1 then return rl.prompt true

    nextLine = (fd, cb) ->
    buff = new Buffer 32
    fs.read fd, buff, 0, 32, null, (err, n, buff) ->
    if err is null and n is 32
    cb buff.toString 'hex'
    else
    cb()

    timeout = (tracker) ->
    if not pendingDownloads[tracker]? then return
    pendingDownloads[tracker]('timeout')

    fetchChunk = (inFd, outFd, cipher, tag) ->
    if not tag? then return rl.prompt true

    # Choose a random tracker, put it as pending, and send a
    # request with it.
    tracker = secure.randomHex()
    pendingDownloads[tracker] = (chunk) ->
    if chunk is 'timeout'
    # Timed out waiting for this chunk.
    # Add limiters here later.
    delete pendingDownloads[tracker]
    fetchChunk inFd, outFd, cipher, tag
    else
    # Hash encrypted chunk to check integrity.
    h = crypto.createHash 'sha256'
    h.end chunk, 'base64'
    candidateTag = h.read().toString 'hex'

    if tag isnt candidateTag then return
    delete pendingDownloads[tracker]

    # Decrypt chunk.
    cipher.write chunk, 'base64'
    chunk = cipher.read()

    # Output, and get next chunk.
    fs.write outFd, chunk, 0, chunk.length, null, ->
    nextLine inFd, (tag) ->
    fetchChunk inFd, outFd, cipher, tag

    drain.push msg: ['req', tracker, tag]
    setTimeout timeout, 5000, tracker

    # Get files needed, open them, and prepare for downloading.
    questions = [
    {ask: 'Input file? ', mode: 'r'},
    {ask: 'Output file? ', mode: 'w'},
    ]

    openFile = (task, done) ->
    rl.question task.ask, (file) ->
    fs.open file, task.mode, null, done

    async.mapSeries questions, openFile, (err, fds) ->
    if err?
    console.log err
    rl.prompt true
    else
    nextLine fds[0], (key) ->
    c = crypto.createCipher 'aes-256-ctr', key
    nextLine fds[0], (tag) ->
    fetchChunk fds[0], fds[1], c, tag

    return

    else console.log 'Command not known.'

    rl.prompt true

    rl.on 'close', ->
    console.log '\nGoodbye'
    process.exit 0

    rl.prompt true
    10 changes: 10 additions & 0 deletions package.json
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,10 @@
    {
    "name": "HearSay",
    "version": "0.0.1",
    "private": true,
    "dependencies": {
    "coffee-script": "*",
    "bigdecimal": "*",
    "async": "*"
    }
    }
    76 changes: 76 additions & 0 deletions secure.coffee
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,76 @@
    crypto = require 'crypto'
    readline = require 'readline'
    {BigInteger} = require 'bigdecimal'
    salt = 'vpfSIDPgJ5tBt7gZgnGQeG8G6r0q4fDkDWWtZD7zqAsYWw2Ep81vIjqC3gJM'

    exports.remote = (conn, init) ->
    dh = crypto.getDiffieHellman 'modp14'
    dh.generateKeys()

    conn.write dh.getPublicKey 'hex'
    conn.once 'data', (data) ->
    # Generate keys.
    secret = dh.computeSecret data.toString(), 'hex', 'hex'
    key = crypto.pbkdf2Sync secret, salt, 2048, 128

    [readCKey, readMKey] = [key.slice(0, 32), key.slice(32, 64)]
    [writeCKey, writeMKey] = [key.slice(64, 96), key.slice(96, 128)]

    if init
    [readCKey, writeCKey] = [writeCKey, readCKey]
    [readMKey, writeMKey] = [writeMKey, readMKey]

    readCipher = crypto.createDecipher 'aes-256-ctr', readCKey
    writeCipher = crypto.createCipher 'aes-256-ctr', writeCKey

    # Begin handling input.
    opts =
    input: conn
    output: conn
    terminal: false

    i = readline.createInterface opts
    i.on 'line', (line) ->
    # Decrypt and validate.
    ct = line.slice 0, line.length - 64
    tag = line.slice line.length - 64

    readCipher.write ct, 'hex'
    pt = readCipher.read().toString()

    readMAC = crypto.createHmac 'sha256', readMKey
    readMAC.end ct, 'hex'
    candidateTag = readMAC.read().toString 'hex'
    if candidateTag != tag then return conn.end()

    # Handle data.
    data = pt.split ' '
    conn.emit 'line', data

    conn.emit 'secure', writeCipher, writeMKey

    # Probabilistically decides if we 'want' something.
    exports.want = (n) ->
    a = BigInteger('3', 10).pow(n)
    b = BigInteger('4', 10).pow(n)
    r = crypto.randomBytes(2).toString('hex')
    x = BigInteger(r, 16).remainder(b)

    if x.compareTo(a) is 1 then false else true

    # Probablistically decides if we should drop a packet.
    exports.drop = ->
    x = exports.random 20
    if x is 2 then true else false

    # Choose a random number in the range [0, max)
    exports.random = (max) ->
    max = BigInteger max.toString(), 10
    n = Math.ceil(((Math.log(max) / Math.log(2)) + 1) / 8)
    r = crypto.randomBytes(n).toString('hex')
    x = BigInteger(r, 16).remainder(max)

    (Number) x

    # Returns a string of random hex.
    exports.randomHex = (n = 32) -> crypto.randomBytes(n).toString 'hex'