ipfs / notes

IPFS Collaborative Notebook for Research
MIT License
402 stars 31 forks source link

Shared Secret constructions for Private Networks #177

Open jbenet opened 7 years ago

jbenet commented 7 years ago

Caveats:


Construction 1:

|| = concat

SS = <shared secret>
conn = <plaintext connection>

N = randomNonce(32) # 256 bits
LK = LocalPublicKey() # use only if remote has it available already. (wouldn't allow connecting to unknown nodes to identify them via secio)
SK = sha2-256(SS || LK || N)  # session key
SC = AESCTR(SK) or CHACHA(SK) # stream cipher

conn.Write(N)
for {
  data := getOutgoingData()
  SC.XORKeyStream(data, data)
  conn.Write(data)
}

Though really:

for {  # AES has a 64GB key limit
  N = randomNonce(32) # 256 bits
  SK = sha2-256(SS || LK || N) # session key

  conn.Write(N)
  SC = AESCTR(SK) or CHACHA(SK)
  SW = cipher.StreamWriter{S: SC, W: conn} # XORs using given cipher

  written := 0
  for written < 63GB { # AES must be re-keyed after 64GB.
    data = getOutgoingData()
    n, _ = SW.Write(data)
    written += n
  }
}

Construction 2:

SS = <shared secret>
conn = <plaintext connection>

N1, N2 := make([]byte, 32) # 256 bits
N1 = randomNonce(32)
conn.Write(N1)
conn.Read(N2)
N = sortAndConcat(N1, N2) # to disregard order.

LK = localPublicKey()
RK = remotePublicKey() # only available if we know it beforehand. this is pre-secio
PKs = sortAndConcat(LK, RK) # to disregard order.
SK = sha2-256(SS || PKs || N)        # session key
SC = AESCTR(SK) or CHACHA(SK) # stream cipher

for {
  data := getOutgoingData()
  SC.XORKeyStream(data, data)
  conn.Write(data)
}

Construction 3:

Kubuxu commented 7 years ago

This is outdated. See: https://github.com/ipfs/notes/issues/177#issuecomment-255588927


My suggestion: It is constriction quite similar to cjdns's:

SS = <shared secret>
N = make([]byte, 24) // 192 bits
copy(N[8:], randomNonce(16))
conn.Write(N[8:])
i = int64(0)

for {
  binary.BigEndian.PutUint64(N, i)
  data := getOutgoingData()
  salsa20.XORKeyStream(data, data, N, SS)
  conn.Write(data)
  i++
}
whyrusleeping commented 7 years ago

why not use varints?

Also, what is the necessity for having the counter? If its an xor'ed keystream it has to be ordered anyways.

Kubuxu commented 7 years ago

why not use varints?

Where you want to put varints, the base of the nonce is sent once per connection. If you mean PutUint64, then I always need 8 bytes.

Also, what is the necessity for having the counter?

Salsa20 is stateless, it can't be resumed (it has internal counter but it isn't exposed), that is why one uses counter as part of the nonce.

Kubuxu commented 7 years ago

The schema had to change a bit as I didn't took into account read re-fragmentation of TCP buffers and no maximum frame length on this level (nor I wanted to introduce one).

Right now it is:

SS = <shared secret>
N = randomNonce(24) // 192bits
conn.Write(N)
S20 = salsa20.NewStream(SS, N)

for {
  data := getOutgoingData()
  S20.XORKeyStream(data, data)
  conn.Write(data)
}

Same benefits apply, most importatnly, there is no need for rekeying as it is with AES. Salsa20 stream is 2^70 bytes long as for the 64bit internal counter, versus AES stream modes using 32bit counters.

The performance of salsa20 is double of AES even on modern architectures with dedicated AES instructions (tested on i7-6800k) and it keeps being double on 32bit ARM. The difference will be even higher one AArch64 becomes more popular.

dominictarr commented 7 years ago

@Kubuxu's last suggestion is fine, you don't need framing or counters, because you have integrity from the next layer. However, you could just encrypt the handshake like this, but not the rest of the session and you'd still preserve your desired properties without another layer of encryption, but then you basically have a integrated design, but this clearly simple to implement as a layer.