Handle DH double ratchet errors

Errors when generating a DH secret where silently ignored, resulting in
invalid key material being used.
This commit fixes the issue by upgrading the double ratchet library
 and implementing the updated interface, which uses now slices instead of fixed bytes keys.
This commit is contained in:
Andrea Maria Piana 2019-10-07 20:46:15 +02:00
parent 143453d660
commit e3b3ba39f3
No known key found for this signature in database
GPG Key ID: AA6CCA6DE0E06424
15 changed files with 185 additions and 390 deletions

View File

@ -27,32 +27,26 @@ func (c EthereumCrypto) GenerateDH() (dr.DHPair, error) {
return nil, err
}
var publicKey [32]byte
copy(publicKey[:], crypto.CompressPubkey(&keys.PublicKey)[:32])
var privateKey [32]byte
copy(privateKey[:], crypto.FromECDSA(keys))
return DHPair{
PrvKey: privateKey,
PubKey: publicKey,
PubKey: crypto.CompressPubkey(&keys.PublicKey),
PrvKey: crypto.FromECDSA(keys),
}, nil
}
// See the Crypto interface.
func (c EthereumCrypto) DH(dhPair dr.DHPair, dhPub dr.Key) dr.Key {
func (c EthereumCrypto) DH(dhPair dr.DHPair, dhPub dr.Key) (dr.Key, error) {
tmpKey := dhPair.PrivateKey()
privateKey, err := crypto.ToECDSA(tmpKey[:])
eciesPrivate := ecies.ImportECDSA(privateKey)
var a [32]byte
privateKey, err := crypto.ToECDSA(tmpKey)
if err != nil {
return a
return nil, err
}
publicKey, err := crypto.DecompressPubkey(dhPub[:])
eciesPrivate := ecies.ImportECDSA(privateKey)
publicKey, err := crypto.DecompressPubkey(dhPub)
if err != nil {
return a
return nil, err
}
eciesPublic := ecies.ImportECDSAPublic(publicKey)
@ -61,48 +55,52 @@ func (c EthereumCrypto) DH(dhPair dr.DHPair, dhPub dr.Key) dr.Key {
16,
16,
)
if err != nil {
return a
return nil, err
}
copy(a[:], key)
return a
return key, nil
}
// See the Crypto interface.
func (c EthereumCrypto) KdfRK(rk, dhOut dr.Key) (rootKey, chainKey, headerKey dr.Key) {
func (c EthereumCrypto) KdfRK(rk, dhOut dr.Key) (dr.Key, dr.Key, dr.Key) {
var (
// We can use a non-secret constant as the last argument
r = hkdf.New(sha256.New, dhOut[:], rk[:], []byte("rsZUpEuXUqqwXBvSy3EcievAh4cMj6QL"))
r = hkdf.New(sha256.New, dhOut, rk, []byte("rsZUpEuXUqqwXBvSy3EcievAh4cMj6QL"))
buf = make([]byte, 96)
)
rootKey := make(dr.Key, 32)
chainKey := make(dr.Key, 32)
headerKey := make(dr.Key, 32)
// The only error here is an entropy limit which won't be reached for such a short buffer.
_, _ = io.ReadFull(r, buf)
copy(rootKey[:], buf[:32])
copy(chainKey[:], buf[32:64])
copy(headerKey[:], buf[64:96])
return
copy(rootKey, buf[:32])
copy(chainKey, buf[32:64])
copy(headerKey, buf[64:96])
return rootKey, chainKey, headerKey
}
// See the Crypto interface.
func (c EthereumCrypto) KdfCK(ck dr.Key) (chainKey dr.Key, msgKey dr.Key) {
func (c EthereumCrypto) KdfCK(ck dr.Key) (dr.Key, dr.Key) {
const (
ckInput = 15
mkInput = 16
)
h := hmac.New(sha256.New, ck[:])
chainKey := make(dr.Key, 32)
msgKey := make(dr.Key, 32)
h := hmac.New(sha256.New, ck)
_, _ = h.Write([]byte{ckInput})
copy(chainKey[:], h.Sum(nil))
copy(chainKey, h.Sum(nil))
h.Reset()
_, _ = h.Write([]byte{mkInput})
copy(msgKey[:], h.Sum(nil))
copy(msgKey, h.Sum(nil))
return chainKey, msgKey
}
@ -110,19 +108,21 @@ func (c EthereumCrypto) KdfCK(ck dr.Key) (chainKey dr.Key, msgKey dr.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 EthereumCrypto) Encrypt(mk dr.Key, plaintext, ad []byte) []byte {
func (c EthereumCrypto) Encrypt(mk dr.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[:])
)
block, err := aes.NewCipher(encKey)
if err != nil {
return nil, err
}
stream := cipher.NewCTR(block, iv[:])
stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)
return append(ciphertext, c.computeSignature(authKey[:], ciphertext, ad)...)
return append(ciphertext, c.computeSignature(authKey, ciphertext, ad)...), nil
}
// See the Crypto interface.
@ -136,37 +136,44 @@ func (c EthereumCrypto) Decrypt(mk dr.Key, authCiphertext, ad []byte) ([]byte, e
// Check the signature.
encKey, authKey, _ := c.deriveEncKeys(mk)
if s := c.computeSignature(authKey[:], ciphertext, ad); !bytes.Equal(s, signature) {
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:]))
)
block, err := aes.NewCipher(encKey)
if err != nil {
return nil, err
}
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 EthereumCrypto) deriveEncKeys(mk dr.Key) (encKey dr.Key, authKey dr.Key, iv [16]byte) {
func (c EthereumCrypto) deriveEncKeys(mk dr.Key) (dr.Key, dr.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"))
r = hkdf.New(sha256.New, mk, salt, []byte("pcwSByyx2CRdryCffXJwy7xgVZWtW5Sh"))
buf = make([]byte, 80)
)
encKey := make(dr.Key, 32)
authKey := make(dr.Key, 32)
var iv [16]byte
// The only error here is an entropy limit which won't be reached for such a short buffer.
_, _ = io.ReadFull(r, buf)
copy(encKey[:], buf[0:32])
copy(authKey[:], buf[32:64])
copy(encKey, buf[0:32])
copy(authKey, buf[32:64])
copy(iv[:], buf[64:80])
return
return encKey, authKey, iv
}
func (c EthereumCrypto) computeSignature(authKey, ciphertext, associatedData []byte) []byte {

View File

@ -272,14 +272,11 @@ func (s *encryptor) DecryptPayload(myIdentityKey *ecdsa.PrivateKey, theirIdentit
}
if drHeader := msg.GetDRHeader(); drHeader != nil {
var dh [32]byte
copy(dh[:], drHeader.GetKey())
drMessage := &dr.Message{
Header: dr.MessageHeader{
N: drHeader.GetN(),
PN: drHeader.GetPn(),
DH: dh,
DH: drHeader.GetKey(),
},
Ciphertext: msg.GetPayload(),
}
@ -324,7 +321,7 @@ func (s *encryptor) DecryptPayload(myIdentityKey *ecdsa.PrivateKey, theirIdentit
return nil, errors.New("no key specified")
}
func (s *encryptor) createNewSession(drInfo *RatchetInfo, sk [32]byte, keyPair crypto.DHPair) (dr.Session, error) {
func (s *encryptor) createNewSession(drInfo *RatchetInfo, sk []byte, keyPair crypto.DHPair) (dr.Session, error) {
var err error
var session dr.Session
@ -359,14 +356,10 @@ func (s *encryptor) encryptUsingDR(theirIdentityKey *ecdsa.PublicKey, drInfo *Ra
var err error
var session dr.Session
var sk, publicKey, privateKey [32]byte
copy(sk[:], drInfo.Sk)
copy(publicKey[:], drInfo.PublicKey[:32])
copy(privateKey[:], drInfo.PrivateKey[:])
keyPair := crypto.DHPair{
PrvKey: privateKey,
PubKey: publicKey,
PrvKey: drInfo.PrivateKey,
PubKey: drInfo.PublicKey,
}
// Load session from store first
@ -378,7 +371,7 @@ func (s *encryptor) encryptUsingDR(theirIdentityKey *ecdsa.PublicKey, drInfo *Ra
// Create a new one
if session == nil {
session, err = s.createNewSession(drInfo, sk, keyPair)
session, err = s.createNewSession(drInfo, drInfo.Sk, keyPair)
if err != nil {
return nil, nil, err
}
@ -403,14 +396,10 @@ func (s *encryptor) decryptUsingDR(theirIdentityKey *ecdsa.PublicKey, drInfo *Ra
var err error
var session dr.Session
var sk, publicKey, privateKey [32]byte
copy(sk[:], drInfo.Sk)
copy(publicKey[:], drInfo.PublicKey[:32])
copy(privateKey[:], drInfo.PrivateKey[:])
keyPair := crypto.DHPair{
PrvKey: privateKey,
PubKey: publicKey,
PrvKey: drInfo.PrivateKey,
PubKey: drInfo.PublicKey,
}
session, err = s.getDRSession(drInfo.ID)
@ -419,7 +408,7 @@ func (s *encryptor) decryptUsingDR(theirIdentityKey *ecdsa.PublicKey, drInfo *Ra
}
if session == nil {
session, err = s.createNewSession(drInfo, sk, keyPair)
session, err = s.createNewSession(drInfo, drInfo.Sk, keyPair)
if err != nil {
return nil, err
}

View File

@ -471,8 +471,7 @@ func newSQLiteKeysStorage(db *sql.DB) *sqliteKeysStorage {
// Get retrieves the message key for a specified public key and message number
func (s *sqliteKeysStorage) Get(pubKey dr.Key, msgNum uint) (dr.Key, bool, error) {
var keyBytes []byte
var key [32]byte
var key []byte
stmt, err := s.db.Prepare(`SELECT message_key
FROM keys
WHERE public_key = ? AND msg_num = ?
@ -483,12 +482,11 @@ func (s *sqliteKeysStorage) Get(pubKey dr.Key, msgNum uint) (dr.Key, bool, error
}
defer stmt.Close()
err = stmt.QueryRow(pubKey[:], msgNum).Scan(&keyBytes)
err = stmt.QueryRow(pubKey, msgNum).Scan(&key)
switch err {
case sql.ErrNoRows:
return key, false, nil
case nil:
copy(key[:], keyBytes)
return key, true, nil
default:
return key, false, err
@ -506,9 +504,9 @@ func (s *sqliteKeysStorage) Put(sessionID []byte, pubKey dr.Key, msgNum uint, mk
_, err = stmt.Exec(
sessionID,
pubKey[:],
pubKey,
msgNum,
mk[:],
mk,
seqNum,
)
@ -561,7 +559,7 @@ func (s *sqliteKeysStorage) DeleteMk(pubKey dr.Key, msgNum uint) error {
defer stmt.Close()
_, err = stmt.Exec(
pubKey[:],
pubKey,
msgNum,
)
@ -579,7 +577,7 @@ func (s *sqliteKeysStorage) Count(pubKey dr.Key) (uint, error) {
defer stmt.Close()
var count uint
err = stmt.QueryRow(pubKey[:]).Scan(&count)
err = stmt.QueryRow(pubKey).Scan(&count)
if err != nil {
return 0, err
}
@ -606,7 +604,7 @@ func (s *sqliteKeysStorage) CountAll() (uint, error) {
}
// All returns nil
func (s *sqliteKeysStorage) All() (map[dr.Key]map[uint]dr.Key, error) {
func (s *sqliteKeysStorage) All() (map[string]map[uint]dr.Key, error) {
return nil, nil
}
@ -622,7 +620,7 @@ func newSQLiteSessionStorage(db *sql.DB) *sqliteSessionStorage {
// Save persists the specified double ratchet state
func (s *sqliteSessionStorage) Save(id []byte, state *dr.State) error {
dhr := state.DHr[:]
dhr := state.DHr
dhs := state.DHs
dhsPublic := dhs.PublicKey()
dhsPrivate := dhs.PrivateKey()
@ -630,12 +628,12 @@ func (s *sqliteSessionStorage) Save(id []byte, state *dr.State) error {
step := state.Step
keysCount := state.KeysCount
rootChainKey := state.RootCh.CK[:]
rootChainKey := state.RootCh.CK
sendChainKey := state.SendCh.CK[:]
sendChainKey := state.SendCh.CK
sendChainN := state.SendCh.N
recvChainKey := state.RecvCh.CK[:]
recvChainKey := state.RecvCh.CK
recvChainN := state.RecvCh.N
stmt, err := s.db.Prepare(`INSERT INTO sessions(id, dhr, dhs_public, dhs_private, root_chain_key, send_chain_key, send_chain_n, recv_chain_key, recv_chain_n, pn, step, keys_count)
@ -648,8 +646,8 @@ func (s *sqliteSessionStorage) Save(id []byte, state *dr.State) error {
_, err = stmt.Exec(
id,
dhr,
dhsPublic[:],
dhsPrivate[:],
dhsPublic,
dhsPrivate,
rootChainKey,
sendChainKey,
sendChainN,
@ -705,23 +703,23 @@ func (s *sqliteSessionStorage) Load(id []byte) (*dr.State, error) {
case sql.ErrNoRows:
return nil, nil
case nil:
state := dr.DefaultState(toKey(rootChainKey))
state := dr.DefaultState(rootChainKey)
state.PN = uint32(pn)
state.Step = step
state.KeysCount = keysCount
state.DHs = ecrypto.DHPair{
PrvKey: toKey(dhsPrivate),
PubKey: toKey(dhsPublic),
PrvKey: dhsPrivate,
PubKey: dhsPublic,
}
state.DHr = toKey(dhr)
state.DHr = dhr
state.SendCh.CK = toKey(sendChainKey)
state.SendCh.CK = sendChainKey
state.SendCh.N = uint32(sendChainN)
state.RecvCh.CK = toKey(recvChainKey)
state.RecvCh.CK = recvChainKey
state.RecvCh.N = uint32(recvChainN)
return &state, nil
@ -729,9 +727,3 @@ func (s *sqliteSessionStorage) Load(id []byte) (*dr.State, error) {
return nil, err
}
}
func toKey(a []byte) dr.Key {
var k [32]byte
copy(k[:], a)
return k
}

2
go.mod
View File

@ -15,7 +15,7 @@ require (
github.com/russolsen/ohyeah v0.0.0-20160324131710-f4938c005315 // indirect
github.com/russolsen/same v0.0.0-20160222130632-f089df61f51d // indirect
github.com/russolsen/transit v0.0.0-20180705123435-0794b4c4505a
github.com/status-im/doubleratchet v2.0.0+incompatible
github.com/status-im/doubleratchet v3.0.0+incompatible
github.com/status-im/migrate/v4 v4.6.2-status.2
github.com/status-im/whisper v1.5.1
github.com/stretchr/testify v1.3.1-0.20190712000136-221dbe5ed467

4
go.sum
View File

@ -249,8 +249,8 @@ github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9Nz
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/status-im/doubleratchet v2.0.0+incompatible h1:s77lF1lDubK0RKftxN2vH8G9gwtVVp13ggWfyY4O1q4=
github.com/status-im/doubleratchet v2.0.0+incompatible/go.mod h1:1sqR0+yhiM/bd+wrdX79AOt2csZuJOni0nUDzKNuqOU=
github.com/status-im/doubleratchet v3.0.0+incompatible h1:aJ1ejcSERpSzmWZBgtfYtiU2nF0Q8ZkGyuEPYETXkCY=
github.com/status-im/doubleratchet v3.0.0+incompatible/go.mod h1:1sqR0+yhiM/bd+wrdX79AOt2csZuJOni0nUDzKNuqOU=
github.com/status-im/go-ethereum v1.9.5-status.4 h1:F5VrxH9LmTxWl4qwQjs0TI5TgG9dVuZKqGmdwHJ0cWk=
github.com/status-im/go-ethereum v1.9.5-status.4/go.mod h1:Ulij8LMpMvXnbnPcmDqrpI+iXoXSjxItuY/wmbasTZU=
github.com/status-im/migrate/v4 v4.6.2-status.2 h1:SdC+sMDl/aI7vUlwD2qj2p7KsK4T60IS9z4/rYCCbI8=

View File

@ -1,9 +1,9 @@
# doubleratchet
[![Go Report Card](https://goreportcard.com/badge/github.com/tiabc/doubleratchet)](https://goreportcard.com/report/github.com/tiabc/doubleratchet)
[![Build Status](https://travis-ci.org/tiabc/doubleratchet.svg?branch=master)](https://travis-ci.org/tiabc/doubleratchet)
[![Coverage Status](https://coveralls.io/repos/github/tiabc/doubleratchet/badge.svg?branch=master)](https://coveralls.io/github/tiabc/doubleratchet?branch=master)
[![GoDoc](https://godoc.org/github.com/tiabc/doubleratchet?status.svg)](https://godoc.org/github.com/tiabc/doubleratchet)
[![Go Report Card](https://goreportcard.com/badge/github.com/status-im/doubleratchet)](https://goreportcard.com/report/github.com/status-im/doubleratchet)
[![Build Status](https://travis-ci.org/status-im/doubleratchet.svg?branch=master)](https://travis-ci.org/status-im/doubleratchet)
[![Coverage Status](https://coveralls.io/repos/github/status-im/doubleratchet/badge.svg?branch=master)](https://coveralls.io/github/status-im/doubleratchet?branch=master)
[![GoDoc](https://godoc.org/github.com/status-im/doubleratchet?status.svg)](https://godoc.org/github.com/status-im/doubleratchet)
[The Double Ratchet Algorithm](https://whispersystems.org/docs/specifications/doubleratchet) is used
by two parties to exchange encrypted messages based on a shared secret key. Typically the parties
@ -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
@ -40,7 +39,7 @@ of them could message each other from the very beginning.
## Installation
go get github.com/tiabc/doubleratchet
go get github.com/status-im/doubleratchet
then `cd` into the project directory and install dependencies:
@ -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/tiabc/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

View File

@ -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)
@ -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 {

View File

@ -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})
@ -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))
@ -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.
@ -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 (
@ -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 {

View File

@ -1,4 +1,4 @@
package: github.com/tiabc/doubleratchet
package: github.com/status-im/doubleratchet
import:
- package: golang.org/x/crypto
subpackages:

View File

@ -2,6 +2,7 @@ package doubleratchet
import (
"bytes"
"fmt"
"sort"
)
@ -26,20 +27,21 @@ type KeysStorage interface {
Count(k Key) (uint, error)
// All returns all the keys
All() (map[Key]map[uint]Key, error)
All() (map[string]map[uint]Key, error)
}
// KeysStorageInMemory is an in-memory message keys storage.
type KeysStorageInMemory struct {
keys map[Key]map[uint]InMemoryKey
keys map[string]map[uint]InMemoryKey
}
// Get returns a message key by the given key and message number.
func (s *KeysStorageInMemory) Get(pubKey Key, msgNum uint) (Key, bool, error) {
index := fmt.Sprintf("%x", pubKey)
if s.keys == nil {
return Key{}, false, nil
}
msgs, ok := s.keys[pubKey]
msgs, ok := s.keys[index]
if !ok {
return Key{}, false, nil
}
@ -58,13 +60,15 @@ type InMemoryKey struct {
// Put saves the given mk under the specified key and msgNum.
func (s *KeysStorageInMemory) Put(sessionID []byte, pubKey Key, msgNum uint, mk Key, seqNum uint) error {
index := fmt.Sprintf("%x", pubKey)
if s.keys == nil {
s.keys = make(map[Key]map[uint]InMemoryKey)
s.keys = make(map[string]map[uint]InMemoryKey)
}
if _, ok := s.keys[pubKey]; !ok {
s.keys[pubKey] = make(map[uint]InMemoryKey)
if _, ok := s.keys[index]; !ok {
s.keys[index] = make(map[uint]InMemoryKey)
}
s.keys[pubKey][msgNum] = InMemoryKey{
s.keys[index][msgNum] = InMemoryKey{
sessionID: sessionID,
messageKey: mk,
seqNum: seqNum,
@ -74,18 +78,20 @@ func (s *KeysStorageInMemory) Put(sessionID []byte, pubKey Key, msgNum uint, mk
// DeleteMk ensures there's no message key under the specified key and msgNum.
func (s *KeysStorageInMemory) DeleteMk(pubKey Key, msgNum uint) error {
index := fmt.Sprintf("%x", pubKey)
if s.keys == nil {
return nil
}
if _, ok := s.keys[pubKey]; !ok {
if _, ok := s.keys[index]; !ok {
return nil
}
if _, ok := s.keys[pubKey][msgNum]; !ok {
if _, ok := s.keys[index][msgNum]; !ok {
return nil
}
delete(s.keys[pubKey], msgNum)
if len(s.keys[pubKey]) == 0 {
delete(s.keys, pubKey)
delete(s.keys[index], msgNum)
if len(s.keys[index]) == 0 {
delete(s.keys, index)
}
return nil
}
@ -143,15 +149,16 @@ func (s *KeysStorageInMemory) DeleteOldMks(sessionID []byte, deleteUntilSeqKey u
// Count returns number of message keys stored under the specified key.
func (s *KeysStorageInMemory) Count(pubKey Key) (uint, error) {
index := fmt.Sprintf("%x", pubKey)
if s.keys == nil {
return 0, nil
}
return uint(len(s.keys[pubKey])), nil
return uint(len(s.keys[index])), nil
}
// All returns all the keys
func (s *KeysStorageInMemory) All() (map[Key]map[uint]Key, error) {
response := make(map[Key]map[uint]Key)
func (s *KeysStorageInMemory) All() (map[string]map[uint]Key, error) {
response := make(map[string]map[uint]Key)
for pubKey, keys := range s.keys {
response[pubKey] = make(map[uint]Key)

View File

@ -46,7 +46,7 @@ func (mh MessageEncHeader) Decode() (MessageHeader, error) {
if len(mh) != 40 {
return MessageHeader{}, fmt.Errorf("encoded message header must be 40 bytes, %d given", len(mh))
}
var dh Key
var dh Key = make(Key, 32)
copy(dh[:], mh[8:40])
return MessageHeader{
DH: dh,

View File

@ -1,6 +1,9 @@
package doubleratchet
import "fmt"
import (
"bytes"
"fmt"
)
// Session of the party involved in the Double Ratchet Algorithm.
type Session interface {
@ -45,7 +48,12 @@ func NewWithRemoteKey(id []byte, sharedKey, remoteKey Key, storage SessionStorag
return nil, fmt.Errorf("can't generate key pair: %s", err)
}
state.DHr = remoteKey
state.SendCh, _ = state.RootCh.step(state.Crypto.DH(state.DHs, state.DHr))
secret, err := state.Crypto.DH(state.DHs, state.DHr)
if err != nil {
return nil, fmt.Errorf("can't generate dh secret: %s", err)
}
state.SendCh, _ = state.RootCh.step(secret)
session := &sessionState{id: id, State: state, storage: storage}
@ -94,7 +102,10 @@ func (s *sessionState) RatchetEncrypt(plaintext, ad []byte) (Message, error) {
}
mk = s.SendCh.step()
)
ct := s.Crypto.Encrypt(mk, plaintext, append(ad, h.Encode()...))
ct, err := s.Crypto.Encrypt(mk, plaintext, append(ad, h.Encode()...))
if err != nil {
return Message{}, err
}
// Store state
if err := s.store(); err != nil {
@ -137,7 +148,7 @@ func (s *sessionState) RatchetDecrypt(m Message, ad []byte) ([]byte, error) {
)
// Is there a new ratchet key?
if m.Header.DH != sc.DHr {
if !bytes.Equal(m.Header.DH, sc.DHr) {
if skippedKeys1, err = sc.skipMessageKeys(sc.DHr, uint(m.Header.PN)); err != nil {
return nil, fmt.Errorf("can't skip previous chain message keys: %s", err)
}

View File

@ -1,153 +0,0 @@
package doubleratchet
import "fmt"
// SessionHE is the session of the party involved the Double Ratchet Algorithm with encrypted header modification.
type SessionHE interface {
// RatchetEncrypt performs a symmetric-key ratchet step, then AEAD-encrypts
// the header-encrypted message with the resulting message key.
RatchetEncrypt(plaintext, associatedData []byte) MessageHE
// RatchetDecrypt is called to AEAD-decrypt header-encrypted messages.
RatchetDecrypt(m MessageHE, associatedData []byte) ([]byte, error)
}
type sessionHE struct {
State
}
// NewHE creates session with the shared keys.
func NewHE(sharedKey, sharedHka, sharedNhkb Key, keyPair DHPair, opts ...option) (SessionHE, error) {
state, err := newState(sharedKey, opts...)
if err != nil {
return nil, err
}
state.DHs = keyPair
state.NHKs = sharedNhkb
state.HKs = sharedHka
state.NHKr = sharedHka
return &sessionHE{state}, nil
}
// NewHEWithRemoteKey creates session with the shared keys and public key of the other party.
func NewHEWithRemoteKey(sharedKey, sharedHka, sharedNhkb, remoteKey Key, opts ...option) (SessionHE, error) {
state, err := newState(sharedKey, opts...)
if err != nil {
return nil, err
}
state.DHs, err = state.Crypto.GenerateDH()
if err != nil {
return nil, fmt.Errorf("can't generate key pair: %s", err)
}
state.DHr = remoteKey
state.SendCh, state.NHKs = state.RootCh.step(state.Crypto.DH(state.DHs, state.DHr))
state.HKs = sharedHka
state.NHKr = sharedNhkb
state.HKr = sharedHka
return &sessionHE{state}, nil
}
// RatchetEncrypt performs a symmetric-key ratchet step, then encrypts the header with
// the corresponding header key and the message with resulting message key.
func (s *sessionHE) RatchetEncrypt(plaintext, ad []byte) MessageHE {
var (
h = MessageHeader{
DH: s.DHs.PublicKey(),
N: s.SendCh.N,
PN: s.PN,
}
mk = s.SendCh.step()
hEnc = s.Crypto.Encrypt(s.HKs, h.Encode(), nil)
)
return MessageHE{
Header: hEnc,
Ciphertext: s.Crypto.Encrypt(mk, plaintext, append(ad, hEnc...)),
}
}
// RatchetDecrypt is called to AEAD-decrypt header-encrypted messages.
func (s *sessionHE) RatchetDecrypt(m MessageHE, ad []byte) ([]byte, error) {
// Is the message one of the skipped?
if plaintext, err := s.trySkippedMessages(m, ad); err != nil || plaintext != nil {
return plaintext, err
}
h, step, err := s.decryptHeader(m.Header)
if err != nil {
return nil, fmt.Errorf("can't decrypt header: %s", err)
}
var (
// All changes must be applied on a different session object, so that this session won't be modified nor left in a dirty session.
sc = s.State
skippedKeys1 []skippedKey
skippedKeys2 []skippedKey
)
if step {
if skippedKeys1, err = sc.skipMessageKeys(sc.HKr, uint(h.PN)); err != nil {
return nil, fmt.Errorf("can't skip previous chain message keys: %s", err)
}
if err = sc.dhRatchet(h); err != nil {
return nil, fmt.Errorf("can't perform ratchet step: %s", err)
}
}
// After all, update the current chain.
if skippedKeys2, err = sc.skipMessageKeys(sc.HKr, uint(h.N)); err != nil {
return nil, fmt.Errorf("can't skip current chain message keys: %s", err)
}
mk := sc.RecvCh.step()
plaintext, err := s.Crypto.Decrypt(mk, m.Ciphertext, append(ad, m.Header...))
if err != nil {
return nil, fmt.Errorf("can't decrypt: %s", err)
}
if err = s.applyChanges(sc, []byte("FIXME"), append(skippedKeys1, skippedKeys2...)); err != nil {
return nil, fmt.Errorf("failed to apply changes: %s", err)
}
return plaintext, nil
}
func (s *sessionHE) decryptHeader(encHeader []byte) (MessageHeader, bool, error) {
if encoded, err := s.Crypto.Decrypt(s.HKr, encHeader, nil); err == nil {
h, err := MessageEncHeader(encoded).Decode()
return h, false, err
}
if encoded, err := s.Crypto.Decrypt(s.NHKr, encHeader, nil); err == nil {
h, err := MessageEncHeader(encoded).Decode()
return h, true, err
}
return MessageHeader{}, false, fmt.Errorf("invalid header")
}
func (s *sessionHE) trySkippedMessages(m MessageHE, ad []byte) ([]byte, error) {
allMessages, err := s.MkSkipped.All()
if err != nil {
return nil, err
}
for hk, keys := range allMessages {
for n, mk := range keys {
hEnc, err := s.Crypto.Decrypt(hk, m.Header, nil)
if err != nil {
continue
}
h, err := MessageEncHeader(hEnc).Decode()
if err != nil {
return nil, fmt.Errorf("can't decode header %s for skipped message key under (%s, %d)", hEnc, hk, n)
}
if uint(h.N) != n {
continue
}
plaintext, err := s.Crypto.Decrypt(mk, m.Ciphertext, append(ad, m.Header...))
if err != nil {
return nil, fmt.Errorf("can't decrypt skipped message: %s", err)
}
_ = s.MkSkipped.DeleteMk(hk, n)
return plaintext, nil
}
}
return nil, nil
}

View File

@ -85,7 +85,7 @@ func (s *State) applyOptions(opts []option) error {
}
func newState(sharedKey Key, opts ...option) (State, error) {
if sharedKey == [32]byte{} {
if sharedKey == nil {
return State{}, fmt.Errorf("sharedKey mustn't be empty")
}
@ -103,13 +103,24 @@ func (s *State) dhRatchet(m MessageHeader) error {
s.DHr = m.DH
s.HKs = s.NHKs
s.HKr = s.NHKr
s.RecvCh, s.NHKr = s.RootCh.step(s.Crypto.DH(s.DHs, s.DHr))
var err error
recvSecret, err := s.Crypto.DH(s.DHs, s.DHr)
if err != nil {
return fmt.Errorf("failed to generate dh recieve ratchet secret: %s", err)
}
s.RecvCh, s.NHKr = s.RootCh.step(recvSecret)
s.DHs, err = s.Crypto.GenerateDH()
if err != nil {
return fmt.Errorf("failed to generate dh pair: %s", err)
}
s.SendCh, s.NHKs = s.RootCh.step(s.Crypto.DH(s.DHs, s.DHr))
sendSecret, err := s.Crypto.DH(s.DHs, s.DHr)
if err != nil {
return fmt.Errorf("failed to generate dh send ratchet secret: %s", err)
}
s.SendCh, s.NHKs = s.RootCh.step(sendSecret)
return nil
}

2
vendor/modules.txt vendored
View File

@ -84,7 +84,7 @@ github.com/rs/cors
github.com/russolsen/transit
# github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24
github.com/shopspring/decimal
# github.com/status-im/doubleratchet v2.0.0+incompatible
# github.com/status-im/doubleratchet v3.0.0+incompatible
github.com/status-im/doubleratchet
# github.com/status-im/migrate/v4 v4.6.2-status.2
github.com/status-im/migrate/v4