2018-12-09 23:45:06 +00:00
|
|
|
package multiplex
|
2018-10-07 17:09:45 +00:00
|
|
|
|
|
|
|
import (
|
2019-08-03 21:05:06 +00:00
|
|
|
"crypto/aes"
|
2019-07-31 23:16:33 +00:00
|
|
|
"crypto/cipher"
|
2018-10-07 17:09:45 +00:00
|
|
|
"encoding/binary"
|
2018-12-09 23:45:06 +00:00
|
|
|
"errors"
|
2019-09-01 19:23:45 +00:00
|
|
|
"fmt"
|
2020-04-14 00:53:28 +00:00
|
|
|
"github.com/cbeuw/Cloak/internal/common"
|
2019-08-03 21:05:06 +00:00
|
|
|
"golang.org/x/crypto/chacha20poly1305"
|
|
|
|
"golang.org/x/crypto/salsa20"
|
2018-10-07 17:09:45 +00:00
|
|
|
)
|
|
|
|
|
2020-04-13 21:48:28 +00:00
|
|
|
type Obfser func(*Frame, []byte, int) (int, error)
|
2020-12-22 14:45:29 +00:00
|
|
|
type Deobfser func(*Frame, []byte) error
|
2018-12-09 23:45:06 +00:00
|
|
|
|
2019-01-06 01:40:27 +00:00
|
|
|
var u32 = binary.BigEndian.Uint32
|
2019-08-27 14:06:28 +00:00
|
|
|
var u64 = binary.BigEndian.Uint64
|
2019-06-14 09:48:59 +00:00
|
|
|
var putU32 = binary.BigEndian.PutUint32
|
2019-08-27 14:06:28 +00:00
|
|
|
var putU64 = binary.BigEndian.PutUint64
|
2019-01-06 01:40:27 +00:00
|
|
|
|
2020-10-21 15:42:24 +00:00
|
|
|
const frameHeaderLength = 14
|
2020-10-18 20:37:41 +00:00
|
|
|
const salsa20NonceSize = 8
|
2019-01-13 21:28:57 +00:00
|
|
|
|
2019-08-16 22:44:40 +00:00
|
|
|
const (
|
2020-10-21 15:42:24 +00:00
|
|
|
EncryptionMethodPlain = iota
|
|
|
|
EncryptionMethodAESGCM
|
|
|
|
EncryptionMethodChaha20Poly1305
|
2019-08-16 22:44:40 +00:00
|
|
|
)
|
|
|
|
|
2020-10-21 15:37:32 +00:00
|
|
|
// Obfuscator is responsible for serialisation, obfuscation, and optional encryption of data frames.
|
2020-04-08 11:18:20 +00:00
|
|
|
type Obfuscator struct {
|
|
|
|
// Used in Stream.Write. Add multiplexing headers, encrypt and add TLS header
|
|
|
|
Obfs Obfser
|
|
|
|
// Remove TLS header, decrypt and unmarshall frames
|
2020-10-15 21:51:36 +00:00
|
|
|
Deobfs Deobfser
|
|
|
|
SessionKey [32]byte
|
|
|
|
|
|
|
|
maxOverhead int
|
2020-04-08 11:18:20 +00:00
|
|
|
}
|
|
|
|
|
2020-10-15 21:51:36 +00:00
|
|
|
// MakeObfs returns a function of type Obfser. An Obfser takes three arguments:
|
|
|
|
// a *Frame with all the field set correctly, a []byte as buffer to put encrypted
|
|
|
|
// message in, and an int called payloadOffsetInBuf to be used when *Frame.payload
|
|
|
|
// is in the byte slice used as buffer (2nd argument). payloadOffsetInBuf specifies
|
|
|
|
// the index at which data belonging to *Frame.Payload starts in the buffer.
|
2020-04-08 19:53:09 +00:00
|
|
|
func MakeObfs(salsaKey [32]byte, payloadCipher cipher.AEAD) Obfser {
|
2020-10-20 00:10:57 +00:00
|
|
|
// The method here is to use the first payloadCipher.NonceSize() bytes of the serialised frame header
|
|
|
|
// as iv/nonce for the AEAD cipher to encrypt the frame payload. Then we use
|
|
|
|
// the authentication tag produced appended to the end of the ciphertext (of size payloadCipher.Overhead())
|
|
|
|
// as nonce for Salsa20 to encrypt the frame header. Both with SessionKey as keys.
|
|
|
|
//
|
|
|
|
// Several cryptographic guarantees we have made here: that payloadCipher, as an AEAD, is given a unique
|
|
|
|
// iv/nonce each time, relative to its key; that the frame header encryptor Salsa20 is given a unique
|
|
|
|
// nonce each time, relative to its key; and that the authenticity of frame header is checked.
|
|
|
|
//
|
|
|
|
// The payloadCipher is given a unique iv/nonce each time because it is derived from the frame header, which
|
|
|
|
// contains the monotonically increasing stream id (uint32) and frame sequence (uint64). There will be a nonce
|
|
|
|
// reuse after 2^64-1 frames sent (sent, not received because frames going different ways are sequenced
|
|
|
|
// independently) by a stream, or after 2^32-1 streams created in a single session. We consider these number
|
|
|
|
// to be large enough that they may never happen in reasonable time frames. Of course, different sessions
|
|
|
|
// will produce the same combination of stream id and frame sequence, but they will have different session keys.
|
|
|
|
//
|
|
|
|
// Salsa20 is assumed to be given a unique nonce each time because we assume the tags produced by payloadCipher
|
|
|
|
// AEAD is unique each time, as payloadCipher itself is given a unique iv/nonce each time due to points made above.
|
|
|
|
// This is relatively a weak guarantee as we are assuming AEADs to produce different tags given different iv/nonces.
|
|
|
|
// This is almost certainly true but I cannot find a source that outright states this.
|
|
|
|
//
|
|
|
|
// Because the frame header, before it being encrypted, is fed into the AEAD, it is also authenticated.
|
|
|
|
// (rfc5116 s.2.1 "The nonce is authenticated internally to the algorithm").
|
|
|
|
//
|
|
|
|
// In case the user chooses to not encrypt the frame payload, payloadCipher will be nil. In this scenario,
|
|
|
|
// we pad the frame payload with random bytes until it reaches Salsa20's nonce size (8 bytes). Then we simply
|
|
|
|
// encrypt the frame header with the last 8 bytes of frame payload as nonce.
|
|
|
|
// If the payload provided by the user is greater than 8 bytes, then we use entirely the user input as nonce.
|
|
|
|
// We can't ensure its uniqueness ourselves, which is why plaintext mode must only be used when the user input
|
|
|
|
// is already random-like. For Cloak it would normally mean that the user is using a proxy protocol that sends
|
|
|
|
// encrypted data.
|
2020-04-13 21:48:28 +00:00
|
|
|
obfs := func(f *Frame, buf []byte, payloadOffsetInBuf int) (int, error) {
|
|
|
|
payloadLen := len(f.Payload)
|
|
|
|
if payloadLen == 0 {
|
|
|
|
return 0, errors.New("payload cannot be empty")
|
|
|
|
}
|
2020-04-12 15:10:48 +00:00
|
|
|
var extraLen int
|
2019-08-04 09:38:49 +00:00
|
|
|
if payloadCipher == nil {
|
2020-10-18 20:37:41 +00:00
|
|
|
extraLen = salsa20NonceSize - payloadLen
|
2020-10-15 21:51:36 +00:00
|
|
|
if extraLen < 0 {
|
|
|
|
// if our payload is already greater than 8 bytes
|
2020-04-12 15:10:48 +00:00
|
|
|
extraLen = 0
|
2019-08-04 09:38:49 +00:00
|
|
|
}
|
|
|
|
} else {
|
2020-04-12 15:10:48 +00:00
|
|
|
extraLen = payloadCipher.Overhead()
|
2020-10-18 20:37:41 +00:00
|
|
|
if extraLen < salsa20NonceSize {
|
2020-04-12 15:10:48 +00:00
|
|
|
return 0, errors.New("AEAD's Overhead cannot be fewer than 8 bytes")
|
|
|
|
}
|
2019-08-04 09:38:49 +00:00
|
|
|
}
|
2020-04-13 21:48:28 +00:00
|
|
|
|
2020-10-21 15:42:24 +00:00
|
|
|
usefulLen := frameHeaderLength + payloadLen + extraLen
|
2020-04-13 21:48:28 +00:00
|
|
|
if len(buf) < usefulLen {
|
|
|
|
return 0, errors.New("obfs buffer too small")
|
2019-08-04 09:38:49 +00:00
|
|
|
}
|
2019-08-20 21:43:04 +00:00
|
|
|
// we do as much in-place as possible to save allocation
|
2020-10-21 15:42:24 +00:00
|
|
|
payload := buf[frameHeaderLength : frameHeaderLength+payloadLen]
|
|
|
|
if payloadOffsetInBuf != frameHeaderLength {
|
2020-04-13 21:48:28 +00:00
|
|
|
// if payload is not at the correct location in buffer
|
|
|
|
copy(payload, f.Payload)
|
|
|
|
}
|
2019-08-20 21:43:04 +00:00
|
|
|
|
2020-10-21 15:42:24 +00:00
|
|
|
header := buf[:frameHeaderLength]
|
2019-06-14 09:48:59 +00:00
|
|
|
putU32(header[0:4], f.StreamID)
|
2019-08-27 14:06:28 +00:00
|
|
|
putU64(header[4:12], f.Seq)
|
|
|
|
header[12] = f.Closing
|
2020-04-12 15:10:48 +00:00
|
|
|
header[13] = byte(extraLen)
|
2019-06-14 09:48:59 +00:00
|
|
|
|
2019-07-31 23:43:33 +00:00
|
|
|
if payloadCipher == nil {
|
2020-04-12 15:10:48 +00:00
|
|
|
if extraLen != 0 { // read nonce
|
2020-04-13 21:48:28 +00:00
|
|
|
extra := buf[usefulLen-extraLen : usefulLen]
|
2020-04-14 00:53:28 +00:00
|
|
|
common.CryptoRandRead(extra)
|
2019-08-04 09:38:49 +00:00
|
|
|
}
|
2019-07-31 23:43:33 +00:00
|
|
|
} else {
|
2020-10-18 20:37:41 +00:00
|
|
|
payloadCipher.Seal(payload[:0], header[:payloadCipher.NonceSize()], payload, nil)
|
2019-06-09 14:03:28 +00:00
|
|
|
}
|
2019-06-09 11:05:41 +00:00
|
|
|
|
2020-10-18 20:37:41 +00:00
|
|
|
nonce := buf[usefulLen-salsa20NonceSize : usefulLen]
|
2019-08-03 21:05:06 +00:00
|
|
|
salsa20.XORKeyStream(header, header, nonce, &salsaKey)
|
2019-06-14 09:48:59 +00:00
|
|
|
|
2019-08-04 09:38:49 +00:00
|
|
|
return usefulLen, nil
|
2018-10-07 17:09:45 +00:00
|
|
|
}
|
|
|
|
return obfs
|
|
|
|
}
|
|
|
|
|
2020-10-15 21:51:36 +00:00
|
|
|
// MakeDeobfs returns a function Deobfser. A Deobfser takes in a single byte slice,
|
|
|
|
// containing the message to be decrypted, and returns a *Frame containing the frame
|
|
|
|
// information and plaintext
|
2020-04-08 19:53:09 +00:00
|
|
|
func MakeDeobfs(salsaKey [32]byte, payloadCipher cipher.AEAD) Deobfser {
|
2020-10-20 23:54:36 +00:00
|
|
|
// frame header length + minimum data size (i.e. nonce size of salsa20)
|
2020-10-21 15:42:24 +00:00
|
|
|
const minInputLen = frameHeaderLength + salsa20NonceSize
|
2020-12-22 14:45:29 +00:00
|
|
|
deobfs := func(f *Frame, in []byte) error {
|
2020-04-08 14:58:46 +00:00
|
|
|
if len(in) < minInputLen {
|
2020-12-22 14:45:29 +00:00
|
|
|
return fmt.Errorf("input size %v, but it cannot be shorter than %v bytes", len(in), minInputLen)
|
2018-12-09 23:45:06 +00:00
|
|
|
}
|
2019-06-14 09:48:59 +00:00
|
|
|
|
2020-10-21 15:42:24 +00:00
|
|
|
header := in[:frameHeaderLength]
|
|
|
|
pldWithOverHead := in[frameHeaderLength:] // payload + potential overhead
|
2019-06-14 09:48:59 +00:00
|
|
|
|
2020-10-18 20:37:41 +00:00
|
|
|
nonce := in[len(in)-salsa20NonceSize:]
|
2019-08-03 21:05:06 +00:00
|
|
|
salsa20.XORKeyStream(header, header, nonce, &salsaKey)
|
2019-06-14 09:48:59 +00:00
|
|
|
|
|
|
|
streamID := u32(header[0:4])
|
2019-08-27 14:06:28 +00:00
|
|
|
seq := u64(header[4:12])
|
|
|
|
closing := header[12]
|
|
|
|
extraLen := header[13]
|
2019-06-14 09:48:59 +00:00
|
|
|
|
2019-08-07 16:53:34 +00:00
|
|
|
usefulPayloadLen := len(pldWithOverHead) - int(extraLen)
|
2020-04-12 15:10:48 +00:00
|
|
|
if usefulPayloadLen < 0 || usefulPayloadLen > len(pldWithOverHead) {
|
2020-12-22 14:45:29 +00:00
|
|
|
return errors.New("extra length is negative or extra length is greater than total pldWithOverHead length")
|
2019-08-07 16:22:40 +00:00
|
|
|
}
|
2019-08-07 16:53:34 +00:00
|
|
|
|
|
|
|
var outputPayload []byte
|
2019-06-09 11:05:41 +00:00
|
|
|
|
2019-07-31 23:43:33 +00:00
|
|
|
if payloadCipher == nil {
|
2019-08-07 16:53:34 +00:00
|
|
|
if extraLen == 0 {
|
|
|
|
outputPayload = pldWithOverHead
|
|
|
|
} else {
|
|
|
|
outputPayload = pldWithOverHead[:usefulPayloadLen]
|
|
|
|
}
|
2019-07-31 23:43:33 +00:00
|
|
|
} else {
|
2020-10-18 20:37:41 +00:00
|
|
|
_, err := payloadCipher.Open(pldWithOverHead[:0], header[:payloadCipher.NonceSize()], pldWithOverHead, nil)
|
2019-07-31 23:43:33 +00:00
|
|
|
if err != nil {
|
2020-12-22 14:45:29 +00:00
|
|
|
return err
|
2019-07-31 23:43:33 +00:00
|
|
|
}
|
2019-08-07 16:53:34 +00:00
|
|
|
outputPayload = pldWithOverHead[:usefulPayloadLen]
|
2019-07-31 23:43:33 +00:00
|
|
|
}
|
2019-06-14 12:28:14 +00:00
|
|
|
|
2020-12-22 14:45:29 +00:00
|
|
|
f.StreamID = streamID
|
|
|
|
f.Seq = seq
|
|
|
|
f.Closing = closing
|
|
|
|
f.Payload = outputPayload
|
|
|
|
return nil
|
2018-10-07 17:09:45 +00:00
|
|
|
}
|
|
|
|
return deobfs
|
|
|
|
}
|
2019-08-03 21:05:06 +00:00
|
|
|
|
2020-04-10 15:09:05 +00:00
|
|
|
func MakeObfuscator(encryptionMethod byte, sessionKey [32]byte) (obfuscator Obfuscator, err error) {
|
|
|
|
obfuscator = Obfuscator{
|
2020-04-09 15:37:46 +00:00
|
|
|
SessionKey: sessionKey,
|
|
|
|
}
|
2019-08-03 21:05:06 +00:00
|
|
|
var payloadCipher cipher.AEAD
|
|
|
|
switch encryptionMethod {
|
2020-10-21 15:42:24 +00:00
|
|
|
case EncryptionMethodPlain:
|
2019-08-03 21:05:06 +00:00
|
|
|
payloadCipher = nil
|
2020-10-18 20:37:41 +00:00
|
|
|
obfuscator.maxOverhead = salsa20NonceSize
|
2020-10-21 15:42:24 +00:00
|
|
|
case EncryptionMethodAESGCM:
|
2019-08-03 21:05:06 +00:00
|
|
|
var c cipher.Block
|
2020-04-07 20:15:28 +00:00
|
|
|
c, err = aes.NewCipher(sessionKey[:])
|
2019-08-03 21:05:06 +00:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
payloadCipher, err = cipher.NewGCM(c)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
2020-10-15 21:51:36 +00:00
|
|
|
obfuscator.maxOverhead = payloadCipher.Overhead()
|
2020-10-21 15:42:24 +00:00
|
|
|
case EncryptionMethodChaha20Poly1305:
|
2020-04-07 20:15:28 +00:00
|
|
|
payloadCipher, err = chacha20poly1305.New(sessionKey[:])
|
2019-08-03 21:05:06 +00:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
2020-10-15 21:51:36 +00:00
|
|
|
obfuscator.maxOverhead = payloadCipher.Overhead()
|
2019-08-03 21:05:06 +00:00
|
|
|
default:
|
2020-04-10 15:09:05 +00:00
|
|
|
return obfuscator, errors.New("Unknown encryption method")
|
2019-08-03 21:05:06 +00:00
|
|
|
}
|
|
|
|
|
2020-10-18 20:41:30 +00:00
|
|
|
if payloadCipher != nil {
|
2020-10-21 15:42:24 +00:00
|
|
|
if payloadCipher.NonceSize() > frameHeaderLength {
|
2020-10-18 20:41:30 +00:00
|
|
|
return obfuscator, errors.New("payload AEAD's nonce size cannot be greater than size of frame header")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-09 15:37:46 +00:00
|
|
|
obfuscator.Obfs = MakeObfs(sessionKey, payloadCipher)
|
|
|
|
obfuscator.Deobfs = MakeDeobfs(sessionKey, payloadCipher)
|
2019-08-03 21:05:06 +00:00
|
|
|
return
|
|
|
|
}
|