status-protocol-go/crypto/ethereum_crypto.go

198 lines
4.7 KiB
Go

package crypto
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/sha256"
"fmt"
"io"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/ecies"
dr "github.com/status-im/doubleratchet"
"golang.org/x/crypto/hkdf"
)
// EthereumCrypto 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 EthereumCrypto struct{}
// See the Crypto interface.
func (c EthereumCrypto) GenerateDH() (dr.DHPair, error) {
keys, err := crypto.GenerateKey()
if err != nil {
return nil, err
}
return DHPair{
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, error) {
tmpKey := dhPair.PrivateKey()
privateKey, err := crypto.ToECDSA(tmpKey)
if err != nil {
return nil, err
}
eciesPrivate := ecies.ImportECDSA(privateKey)
publicKey, err := crypto.DecompressPubkey(dhPub)
if err != nil {
return nil, err
}
eciesPublic := ecies.ImportECDSAPublic(publicKey)
key, err := eciesPrivate.GenerateShared(
eciesPublic,
16,
16,
)
if err != nil {
return nil, err
}
return key, nil
}
// See the Crypto interface.
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"))
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 rootKey, chainKey, headerKey
}
// See the Crypto interface.
func (c EthereumCrypto) KdfCK(ck dr.Key) (dr.Key, dr.Key) {
const (
ckInput = 15
mkInput = 16
)
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))
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 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[:])
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)...), nil
}
// See the Crypto interface.
func (c EthereumCrypto) Decrypt(mk dr.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.
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) (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"))
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(iv[:], buf[64:80])
return encKey, authKey, iv
}
func (c EthereumCrypto) 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 {
PrvKey dr.Key
PubKey dr.Key
}
func (p DHPair) PrivateKey() dr.Key {
return p.PrvKey
}
func (p DHPair) PublicKey() dr.Key {
return p.PubKey
}