197 lines
5.4 KiB
Go
197 lines
5.4 KiB
Go
package doubleratchet
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"io"
|
|
|
|
"golang.org/x/crypto/curve25519"
|
|
"golang.org/x/crypto/hkdf"
|
|
)
|
|
|
|
// DefaultCrypto is an implementation of Crypto with cryptographic primitives recommended
|
|
// by the Double Ratchet Algorithm specification. However, some details are different,
|
|
// see function comments for details.
|
|
type DefaultCrypto struct{}
|
|
|
|
// GenerateDH creates a new Diffie-Hellman key pair.
|
|
func (c DefaultCrypto) GenerateDH() (DHPair, error) {
|
|
var privKey [32]byte
|
|
if _, err := io.ReadFull(rand.Reader, privKey[:]); err != nil {
|
|
return dhPair{}, fmt.Errorf("couldn't generate privKey: %s", err)
|
|
}
|
|
privKey[0] &= 248
|
|
privKey[31] &= 127
|
|
privKey[31] |= 64
|
|
|
|
var pubKey [32]byte
|
|
curve25519.ScalarBaseMult(&pubKey, &privKey)
|
|
return dhPair{
|
|
privateKey: privKey[:],
|
|
publicKey: pubKey[:],
|
|
}, nil
|
|
}
|
|
|
|
// DH returns the output from the Diffie-Hellman calculation between
|
|
// the private key from the DH key pair dhPair and the DH public key dbPub.
|
|
func (c DefaultCrypto) DH(dhPair DHPair, dhPub Key) (Key, error) {
|
|
var (
|
|
dhOut [32]byte
|
|
privKey [32]byte
|
|
pubKey [32]byte
|
|
)
|
|
if len(dhPair.PrivateKey()) != 32 {
|
|
return nil, fmt.Errorf("Invalid private key length: %d", len(dhPair.PrivateKey()))
|
|
}
|
|
|
|
if len(dhPub) != 32 {
|
|
return nil, fmt.Errorf("Invalid private key length: %d", len(dhPair.PrivateKey()))
|
|
}
|
|
|
|
copy(privKey[:], dhPair.PrivateKey()[:32])
|
|
copy(pubKey[:], dhPub[:32])
|
|
|
|
curve25519.ScalarMult(&dhOut, &privKey, &pubKey)
|
|
return dhOut[:], nil
|
|
}
|
|
|
|
// KdfRK returns a pair (32-byte root key, 32-byte chain key) as the output of applying
|
|
// a KDF keyed by a 32-byte root key rk to a Diffie-Hellman output dhOut.
|
|
func (c DefaultCrypto) KdfRK(rk, dhOut Key) (Key, Key, Key) {
|
|
var (
|
|
r = hkdf.New(sha256.New, dhOut, rk, []byte("rsZUpEuXUqqwXBvSy3EcievAh4cMj6QL"))
|
|
buf = make([]byte, 96)
|
|
)
|
|
|
|
// The only error here is an entropy limit which won't be reached for such a short buffer.
|
|
_, _ = io.ReadFull(r, buf)
|
|
|
|
rootKey := make(Key, 32)
|
|
headerKey := make(Key, 32)
|
|
chainKey := make(Key, 32)
|
|
|
|
copy(rootKey[:], buf[:32])
|
|
copy(chainKey[:], buf[32:64])
|
|
copy(headerKey[:], buf[64:96])
|
|
return rootKey, chainKey, headerKey
|
|
}
|
|
|
|
// KdfCK returns a pair (32-byte chain key, 32-byte message key) as the output of applying
|
|
// a KDF keyed by a 32-byte chain key ck to some constant.
|
|
func (c DefaultCrypto) KdfCK(ck Key) (Key, Key) {
|
|
const (
|
|
ckInput = 15
|
|
mkInput = 16
|
|
)
|
|
|
|
chainKey := make(Key, 32)
|
|
msgKey := make(Key, 32)
|
|
|
|
h := hmac.New(sha256.New, ck[:])
|
|
|
|
_, _ = h.Write([]byte{ckInput})
|
|
copy(chainKey[:], h.Sum(nil))
|
|
h.Reset()
|
|
|
|
_, _ = h.Write([]byte{mkInput})
|
|
copy(msgKey[:], h.Sum(nil))
|
|
|
|
return chainKey, msgKey
|
|
}
|
|
|
|
// Encrypt uses a slightly different approach than in the algorithm specification:
|
|
// it uses AES-256-CTR instead of AES-256-CBC for security, ciphertext length and implementation
|
|
// complexity considerations.
|
|
func (c DefaultCrypto) Encrypt(mk Key, plaintext, ad []byte) ([]byte, error) {
|
|
encKey, authKey, iv := c.deriveEncKeys(mk)
|
|
|
|
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
|
|
copy(ciphertext, iv[:])
|
|
|
|
var (
|
|
block, _ = aes.NewCipher(encKey[:]) // No error will occur here as encKey is guaranteed to be 32 bytes.
|
|
stream = cipher.NewCTR(block, iv[:])
|
|
)
|
|
stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)
|
|
|
|
return append(ciphertext, c.computeSignature(authKey[:], ciphertext, ad)...), nil
|
|
}
|
|
|
|
// Decrypt returns the AEAD decryption of ciphertext with message key mk.
|
|
func (c DefaultCrypto) Decrypt(mk Key, authCiphertext, ad []byte) ([]byte, error) {
|
|
var (
|
|
l = len(authCiphertext)
|
|
ciphertext = authCiphertext[:l-sha256.Size]
|
|
signature = authCiphertext[l-sha256.Size:]
|
|
)
|
|
|
|
// Check the signature.
|
|
encKey, authKey, _ := c.deriveEncKeys(mk)
|
|
|
|
if s := c.computeSignature(authKey[:], ciphertext, ad); !bytes.Equal(s, signature) {
|
|
return nil, fmt.Errorf("invalid signature")
|
|
}
|
|
|
|
// Decrypt.
|
|
var (
|
|
block, _ = aes.NewCipher(encKey[:]) // No error will occur here as encKey is guaranteed to be 32 bytes.
|
|
stream = cipher.NewCTR(block, ciphertext[:aes.BlockSize])
|
|
plaintext = make([]byte, len(ciphertext[aes.BlockSize:]))
|
|
)
|
|
stream.XORKeyStream(plaintext, ciphertext[aes.BlockSize:])
|
|
|
|
return plaintext, nil
|
|
}
|
|
|
|
// deriveEncKeys derive keys for message encryption and decryption. Returns (encKey, authKey, iv, err).
|
|
func (c DefaultCrypto) deriveEncKeys(mk Key) (Key, Key, [16]byte) {
|
|
// First, derive encryption and authentication key out of mk.
|
|
salt := make([]byte, 32)
|
|
var (
|
|
r = hkdf.New(sha256.New, mk[:], salt, []byte("pcwSByyx2CRdryCffXJwy7xgVZWtW5Sh"))
|
|
buf = make([]byte, 80)
|
|
)
|
|
|
|
// The only error here is an entropy limit which won't be reached for such a short buffer.
|
|
_, _ = io.ReadFull(r, buf)
|
|
|
|
var encKey Key = make(Key, 32)
|
|
var authKey Key = make(Key, 32)
|
|
var iv [16]byte
|
|
|
|
copy(encKey[:], buf[0:32])
|
|
copy(authKey[:], buf[32:64])
|
|
copy(iv[:], buf[64:80])
|
|
|
|
return encKey, authKey, iv
|
|
}
|
|
|
|
func (c DefaultCrypto) computeSignature(authKey, ciphertext, associatedData []byte) []byte {
|
|
h := hmac.New(sha256.New, authKey)
|
|
_, _ = h.Write(associatedData)
|
|
_, _ = h.Write(ciphertext)
|
|
return h.Sum(nil)
|
|
}
|
|
|
|
type dhPair struct {
|
|
privateKey Key
|
|
publicKey Key
|
|
}
|
|
|
|
func (p dhPair) PrivateKey() Key {
|
|
return p.privateKey
|
|
}
|
|
|
|
func (p dhPair) PublicKey() Key {
|
|
return p.publicKey
|
|
}
|
|
|
|
func (p dhPair) String() string {
|
|
return fmt.Sprintf("{privateKey: %s publicKey: %s}", p.privateKey, p.publicKey)
|
|
}
|