- #############################
- # NOT PRODUCTION READY #
- #############################
This package has not undergone a security audit. Please DO NOT use this for mission critical comms just yet.
- finish docs
- complete unit test
- security audit
- iterate API based on how spice is used
- Big Data
- P2P
The encryption library used is NaCl, which leverages Curve25519, XSalsa20 and Poly1305. The implementation is from the high quality golang crypto package (golang.org/x/crypto/nacl).
Why NaCl?
- Super fast encryption/decryption
- Super fast signatures
- Authentication at the chunk level (signature verfication)
- Aysemmetric Encryption (Ideal for P2P)
- Large Nonce
NaCl allows each chunk of data to be authenticated by the receiver, and encrypted with the recepient's public key. This enables true end-to-end encryption with guranteed authenticity.
Chunked encryption is succeptable to chunk-level replay attacks. Jack O'Connor explains this well:
- Bob sends two messages to Sarah. Let's say the first message comes in chunks A1 and A2, and the second message comes in chunks B1 and B2.
- Mallory intercepts both messages. She then constructs the new message A1+B2.
- Mallory sends the forged message to Sarah. The boxes open with Bob's key, and the sequence numbers look good.
In order to mitigate chunk-level replay, some groups have proposed using a incremented nonce. This ensures that each block must be decrypted in the correct order or else the decryption will not be verfied.
Instead of using a counter, spice links chunks of data by using the first 24 bytes of the last encrypted block. This is analogous to how a blockchain references the previous hash. Chunk referencing is also very efficient.
A CRYPTOGRAPHICALLY RANDOM 24 BYTE ARRAY MUST BE GENERATED FOR THE FIRST NONCE!
Rationale:
When sending data over the network, chunking is pretty much a given. TLS has a maximum record size of 16KB and this fits neatly with authenticated encryption APIs which all operate on an entire message at once.
- The whole message needs to be held in memory to be processed.
- Using large messages pressures implementations on small machines to decrypt and process plaintext before authenticating it. This is very dangerous, and this API does not allow it, but a protocol that uses excessive message sizes might present some implementations with no other choice.
- Fixed overheads will be sufficiently amortised by messages as small as 8KB.
- Performance may be improved by working with messages that fit into data caches.
Each data block is chunked in 16368 byte sections and sealed with a 16 byte signature. This results in encrypted chunks that are exactly 16kb. Optimal for web transmission and safe for small computers.
RUN THIS EXAMPLE (from spice root directory)!
1st terminal: go run examples/network/server.go
2nd terminal: go run examples/network/client.go
# server.go
package main
import (
"fmt"
"net"
"os"
"github.com/tensortask/spice"
)
const (
connHost = "localhost"
connPort = "6666"
connType = "tcp"
)
var (
nonce = [24]byte{58, 230, 79, 44, 187, 45, 107, 226, 245, 53, 169, 118, 218, 116, 235, 95, 132, 127, 166, 200, 203, 141, 251, 51}
sharedKey = &[32]byte{199, 156, 103, 110, 157, 5, 107, 139, 94, 138, 53, 214, 74, 100, 211, 97, 106, 48, 11, 179, 200, 19, 244, 108, 138, 167, 49, 163, 156, 176, 66, 64}
)
func main() {
// Listen for incoming connections.
listener, err := net.Listen(connType, connHost+":"+connPort)
if err != nil {
panic(err)
}
// Close the listener when the application closes.
defer listener.Close()
fmt.Println("Listening on " + connHost + ":" + connPort)
for {
// Listen for an incoming connection.
conn, err := listener.Accept()
if err != nil {
panic(err)
}
// Handle connections in a new goroutine.
go handleRequest(conn)
}
}
// Handles incoming requests.
func handleRequest(conn net.Conn) {
decryptor := spice.NewDecryptor(os.Stdout, nonce, sharedKey)
err := decryptor.DecryptFromReader(conn)
if err != nil {
panic(err)
}
conn.Close()
}
# client.go
package main
import (
"net"
"github.com/tensortask/spice"
)
const (
connHost = "localhost"
connPort = "6666"
connType = "tcp"
fileName = "testing/a_midsummers_night's_dream.txt"
)
var (
nonce = [24]byte{58, 230, 79, 44, 187, 45, 107, 226, 245, 53, 169, 118, 218, 116, 235, 95, 132, 127, 166, 200, 203, 141, 251, 51}
sharedKey = &[32]byte{199, 156, 103, 110, 157, 5, 107, 139, 94, 138, 53, 214, 74, 100, 211, 97, 106, 48, 11, 179, 200, 19, 244, 108, 138, 167, 49, 163, 156, 176, 66, 64}
)
func main() {
// connect to this socket
conn, err := net.Dial(connType, connHost+":"+connPort)
if err != nil {
panic(err)
}
encryptor := spice.NewEncryptor(conn, nonce, sharedKey)
encryptor.EncryptFile(fileName)
}
RUN THIS EXAMPLE (from spice root directory)!
1st terminal: go run examples/file/main.go
# main.go
package main
import (
"os"
"github.com/tensortask/spice"
)
func main() {
hostKeys, err := spice.RandomKeyPair()
if err != nil {
panic(err)
}
peerKeys, err := spice.RandomKeyPair()
if err != nil {
panic(err)
}
sharedKey := hostKeys.SharedKey(peerKeys)
nonce, err := spice.RandomNonce()
if err != nil {
panic(err)
}
encryptedFile, err := os.Create("testing/encrypted_shakespeare.txt")
if err != nil {
panic(err)
}
encryptor := spice.NewEncryptor(encryptedFile, nonce, sharedKey)
err = encryptor.EncryptFile("testing/a_midsummers_night's_dream.txt")
if err != nil {
panic(err)
}
encryptedFile.Close()
decryptedFile, err := os.Create("testing/decrypted_shakespeare.txt")
if err != nil {
panic(err)
}
decryptor := spice.NewDecryptor(decryptedFile, nonce, sharedKey)
err = decryptor.DecryptFile("testing/encrypted_shakespeare.txt")
if err != nil {
panic(err)
}
decryptedFile.Close()
}
NOTE: this is a very old metric... recently spice has been clocking in much hotter. Full benchmarks to come.
2017 Macbook, 1.3Ghz i5, 8GB ram = 146.69 MB/s
Well above average consumer internet connection speeds ✅