Skip to content

Commit

Permalink
[do-not-merge] Bug/allow returning an error in dh (#11)
Browse files Browse the repository at this point in the history
* Remove encryted header version

* Return error from DH & Encrypt

* Use slices instead of fixed array for keys

In order to support multiple types of curves, this commits changes the
type of `Key` from `[32]byte` to `[]byte`. This is useful as most of the
eliptic keys have a size of a compressed key of `33 bytes` instead of
`32 bytes`, which the default implemetation uses (curve25519).
  • Loading branch information
cammellos authored Oct 31, 2019
1 parent d4e8261 commit 1e33002
Show file tree
Hide file tree
Showing 12 changed files with 145 additions and 609 deletions.
92 changes: 0 additions & 92 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ Let me know if you face any problems or have any questions or suggestions.
1. Skipped messages from a single ratchet step are deleted after 100 ratchet steps.
1. Both parties' sending and receiving chains are initialized with the shared key so that both
of them could message each other from the very beginning.
1. Both plain and encrypted header versions are implemented.

### Cryptographic primitives

Expand Down Expand Up @@ -131,97 +130,6 @@ doubleratchet.New(
)
```

### Header encryption

If you don't want anybody to see message ordering and your ratchet keys, you can utilize
header encryption. It makes your communication even more secure in a sense that an eavesdropper
can only see ciphertexts and nothing else. However, it adds more complexity to the implementation,
namely:

1. Parties should agree on 2 more secret keys for encrypting headers before the double ratchet
session.
1. When a recipient receives a message she must first associate the message with its relevant
Double Ratchet session (assuming she has different sessions with different parties).
How this is done is outside of the scope of this library, although [the Pond protocol](https://github.com/agl/pond) offers some
ideas as stated in the Double Ratchet specification.
1. Header encryption makes messages 48 bytes longer. For example, if you're sending message
`how are you?` in a version without header encryption, it will be encrypted into
`iv + len(pt) + signature = 16 + 12 + 32 = 60` bytes plus a header `rk + pn + n = 32 + 4 + 4 = 40` bytes
with 100 bytes in total. In case of the header encryption modification the header will also
be encrypted which will add 48 more bytes with the total of 148 bytes. Note that the longer
your message, the more resulting length it takes.
1. It does a bit more computations especially for skipped messages and will work more slowly.

#### Example

In order to create a header-encrypted session, parties should agree upon 3 different shared keys
and Alice should know Bob's public key:

```go
package main

import (
"fmt"
"log"

"github.com/status-im/doubleratchet"
)

func main() {
// Shared keys both parties have already agreed upon before the communication.
var (
// The key for message keys derivation.
sk = [32]byte{
0xeb, 0x8, 0x10, 0x7c, 0x33, 0x54, 0x0, 0x20,
0xe9, 0x4f, 0x6c, 0x84, 0xe4, 0x39, 0x50, 0x5a,
0x2f, 0x60, 0xbe, 0x81, 0xa, 0x78, 0x8b, 0xeb,
0x1e, 0x2c, 0x9, 0x8d, 0x4b, 0x4d, 0xc1, 0x40,
}

// Header encryption keys.
sharedHka = [32]byte{
0xbd, 0x29, 0x18, 0xcb, 0x18, 0x6c, 0x26, 0x32,
0xd5, 0x82, 0x41, 0x2d, 0x11, 0xa4, 0x55, 0x87,
0x1e, 0x5b, 0xa3, 0xb5, 0x5a, 0x6d, 0xe1, 0x97,
0xde, 0xf7, 0x5e, 0xc3, 0xf2, 0xec, 0x1d, 0xd,
}
sharedNhkb = [32]byte{
0x32, 0x89, 0x3a, 0xed, 0x4b, 0xf0, 0xbf, 0xc1,
0xa5, 0xa9, 0x53, 0x73, 0x5b, 0xf9, 0x76, 0xce,
0x70, 0x8e, 0xe1, 0xa, 0xed, 0x98, 0x1d, 0xe3,
0xb4, 0xe9, 0xa9, 0x88, 0x54, 0x94, 0xaf, 0x23,
}
)

keyPair, err := doubleratchet.DefaultCrypto{}.GenerateDH()
if err != nil {
log.Fatal(err)
}

// Bob MUST be created with the shared secret, shared header keys and a DH key pair.
bob, err := doubleratchet.NewHE(sk, sharedHka, sharedNhkb, keyPair)
if err != nil {
log.Fatal(err)
}

// Alic MUST be created with the shared secret, shared header keys and Bob's public key.
alice, err := doubleratchet.NewHEWithRemoteKey(sk, sharedHka, sharedNhkb, keyPair.PublicKey())
if err != nil {
log.Fatal(err)
}

// Encryption and decryption is done the same way as in the basic version.
m := alice.RatchetEncrypt([]byte("Hi Bob!"), nil)

plaintext, err := bob.RatchetDecrypt(m, nil)
if err != nil {
log.Fatal(err)
}

fmt.Println(string(plaintext))
}
```

## License

MIT
8 changes: 4 additions & 4 deletions crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ type Crypto interface {

// 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.
DH(dhPair DHPair, dhPub Key) Key
DH(dhPair DHPair, dhPub Key) (Key, error)

// Encrypt returns an AEAD encryption of plaintext with message key mk. The associated_data
// is authenticated but is not included in the ciphertext. The AEAD nonce may be set to a constant.
Encrypt(mk Key, plaintext, ad []byte) (authCiphertext []byte)
Encrypt(mk Key, plaintext, ad []byte) (authCiphertext []byte, err error)

// Decrypt returns the AEAD decryption of ciphertext with message key mk.
Decrypt(mk Key, ciphertext, ad []byte) (plaintext []byte, err error)
Expand All @@ -27,8 +27,8 @@ type DHPair interface {
PublicKey() Key
}

// Key is any 32-byte key. It's created for the possibility of pretty hex output.
type Key [32]byte
// Key is any byte representation of a key.
type Key []byte

// Stringer interface compliance.
func (k Key) String() string {
Expand Down
51 changes: 37 additions & 14 deletions default_crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,48 +32,66 @@ func (c DefaultCrypto) GenerateDH() (DHPair, error) {
var pubKey [32]byte
curve25519.ScalarBaseMult(&pubKey, &privKey)
return dhPair{
privateKey: privKey,
publicKey: pubKey,
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 {
func (c DefaultCrypto) DH(dhPair DHPair, dhPub Key) (Key, error) {
var (
dhOut [32]byte
privKey [32]byte = dhPair.PrivateKey()
pubKey [32]byte = dhPub
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
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) (rootKey, chainKey, headerKey Key) {
func (c DefaultCrypto) KdfRK(rk, dhOut Key) (Key, Key, Key) {
var (
r = hkdf.New(sha256.New, dhOut[:], rk[:], []byte("rsZUpEuXUqqwXBvSy3EcievAh4cMj6QL"))
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
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) (chainKey Key, msgKey Key) {
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})
Expand All @@ -89,7 +107,7 @@ func (c DefaultCrypto) KdfCK(ck Key) (chainKey Key, msgKey Key) {
// 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 {
func (c DefaultCrypto) Encrypt(mk Key, plaintext, ad []byte) ([]byte, error) {
encKey, authKey, iv := c.deriveEncKeys(mk)

ciphertext := make([]byte, aes.BlockSize+len(plaintext))
Expand All @@ -101,7 +119,7 @@ func (c DefaultCrypto) Encrypt(mk Key, plaintext, ad []byte) []byte {
)
stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)

return append(ciphertext, c.computeSignature(authKey[:], ciphertext, ad)...)
return append(ciphertext, c.computeSignature(authKey[:], ciphertext, ad)...), nil
}

// Decrypt returns the AEAD decryption of ciphertext with message key mk.
Expand Down Expand Up @@ -131,7 +149,7 @@ func (c DefaultCrypto) Decrypt(mk Key, authCiphertext, ad []byte) ([]byte, error
}

// deriveEncKeys derive keys for message encryption and decryption. Returns (encKey, authKey, iv, err).
func (c DefaultCrypto) deriveEncKeys(mk Key) (encKey Key, authKey Key, iv [16]byte) {
func (c DefaultCrypto) deriveEncKeys(mk Key) (Key, Key, [16]byte) {
// First, derive encryption and authentication key out of mk.
salt := make([]byte, 32)
var (
Expand All @@ -142,10 +160,15 @@ func (c DefaultCrypto) deriveEncKeys(mk Key) (encKey Key, authKey Key, iv [16]by
// 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

return encKey, authKey, iv
}

func (c DefaultCrypto) computeSignature(authKey, ciphertext, associatedData []byte) []byte {
Expand Down
Loading

0 comments on commit 1e33002

Please sign in to comment.