status-go/services/shhext/chat/encryption.go
Andrea Maria Piana 38e5335e18
Allow messages to be decrypted from previous bundles (#1400)
Currently we only decrypt messages if received on the current bundle.
This changes the behavior so that messages can be decrypted if sent to
previous bundles as well, as otherwise is a bit restrictive
2019-02-28 13:09:43 +01:00

630 lines
18 KiB
Go

package chat
import (
"bytes"
"crypto/ecdsa"
"encoding/hex"
"errors"
"fmt"
"sync"
"time"
ecrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/ecies"
"github.com/ethereum/go-ethereum/log"
dr "github.com/status-im/doubleratchet"
"github.com/status-im/status-go/services/shhext/chat/crypto"
)
var ErrSessionNotFound = errors.New("session not found")
var ErrDeviceNotFound = errors.New("device not found")
// If we have no bundles, we use a constant so that the message can reach any device.
const noInstallationID = "none"
type ConfirmationData struct {
header *dr.MessageHeader
drInfo *RatchetInfo
}
// EncryptionService defines a service that is responsible for the encryption aspect of the protocol.
type EncryptionService struct {
log log.Logger
persistence PersistenceService
config EncryptionServiceConfig
messageIDs map[string]*ConfirmationData
mutex sync.Mutex
}
type EncryptionServiceConfig struct {
InstallationID string
// Max number of installations we keep synchronized.
MaxInstallations int
// How many consecutive messages can be skipped in the receiving chain.
MaxSkip int
// Any message with seqNo <= currentSeq - maxKeep will be deleted.
MaxKeep int
// How many keys do we store in total per session.
MaxMessageKeysPerSession int
// How long before we refresh the interval in milliseconds
BundleRefreshInterval int64
}
type IdentityAndIDPair [2]string
// DefaultEncryptionServiceConfig returns the default values used by the encryption service
func DefaultEncryptionServiceConfig(installationID string) EncryptionServiceConfig {
return EncryptionServiceConfig{
MaxInstallations: 3,
MaxSkip: 1000,
MaxKeep: 3000,
MaxMessageKeysPerSession: 2000,
BundleRefreshInterval: 24 * 60 * 60 * 1000,
InstallationID: installationID,
}
}
// NewEncryptionService creates a new EncryptionService instance.
func NewEncryptionService(p PersistenceService, config EncryptionServiceConfig) *EncryptionService {
logger := log.New("package", "status-go/services/sshext.chat")
logger.Info("Initialized encryption service", "installationID", config.InstallationID)
return &EncryptionService{
log: logger,
persistence: p,
config: config,
mutex: sync.Mutex{},
messageIDs: make(map[string]*ConfirmationData),
}
}
func (s *EncryptionService) keyFromActiveX3DH(theirIdentityKey []byte, theirSignedPreKey []byte, myIdentityKey *ecdsa.PrivateKey) ([]byte, *ecdsa.PublicKey, error) {
sharedKey, ephemeralPubKey, err := PerformActiveX3DH(theirIdentityKey, theirSignedPreKey, myIdentityKey)
if err != nil {
return nil, nil, err
}
return sharedKey, ephemeralPubKey, nil
}
func (s *EncryptionService) getDRSession(id []byte) (dr.Session, error) {
sessionStorage := s.persistence.GetSessionStorage()
return dr.Load(
id,
sessionStorage,
dr.WithKeysStorage(s.persistence.GetKeysStorage()),
dr.WithMaxSkip(s.config.MaxSkip),
dr.WithMaxKeep(s.config.MaxKeep),
dr.WithMaxMessageKeysPerSession(s.config.MaxMessageKeysPerSession),
dr.WithCrypto(crypto.EthereumCrypto{}),
)
}
func confirmationIDString(id []byte) string {
return hex.EncodeToString(id)
}
// ConfirmMessagesProcessed confirms and deletes message keys for the given messages
func (s *EncryptionService) ConfirmMessagesProcessed(messageIDs [][]byte) error {
s.mutex.Lock()
defer s.mutex.Unlock()
for _, idByte := range messageIDs {
id := confirmationIDString(idByte)
confirmationData, ok := s.messageIDs[id]
if !ok {
s.log.Debug("Could not confirm message", "messageID", id)
continue
}
// Load session from store first
session, err := s.getDRSession(confirmationData.drInfo.ID)
if err != nil {
return err
}
if err := session.DeleteMk(confirmationData.header.DH, confirmationData.header.N); err != nil {
return err
}
}
return nil
}
// CreateBundle retrieves or creates an X3DH bundle given a private key
func (s *EncryptionService) CreateBundle(privateKey *ecdsa.PrivateKey) (*Bundle, error) {
ourIdentityKeyC := ecrypto.CompressPubkey(&privateKey.PublicKey)
installationIDs, err := s.persistence.GetActiveInstallations(s.config.MaxInstallations-1, ourIdentityKeyC)
if err != nil {
return nil, err
}
installationIDs = append(installationIDs, s.config.InstallationID)
bundleContainer, err := s.persistence.GetAnyPrivateBundle(ourIdentityKeyC, installationIDs)
if err != nil {
return nil, err
}
// If the bundle has expired we create a new one
if bundleContainer != nil && bundleContainer.GetBundle().Timestamp < time.Now().Add(-1*time.Duration(s.config.BundleRefreshInterval)*time.Millisecond).UnixNano() {
// Mark sessions has expired
if err := s.persistence.MarkBundleExpired(bundleContainer.GetBundle().GetIdentity()); err != nil {
return nil, err
}
} else if bundleContainer != nil {
err = SignBundle(privateKey, bundleContainer)
if err != nil {
return nil, err
}
return bundleContainer.GetBundle(), nil
}
// needs transaction/mutex to avoid creating multiple bundles
// although not a problem
bundleContainer, err = NewBundleContainer(privateKey, s.config.InstallationID)
if err != nil {
return nil, err
}
if err = s.persistence.AddPrivateBundle(bundleContainer); err != nil {
return nil, err
}
return s.CreateBundle(privateKey)
}
// DecryptWithDH decrypts message sent with a DH key exchange, and throws away the key after decryption
func (s *EncryptionService) DecryptWithDH(myIdentityKey *ecdsa.PrivateKey, theirEphemeralKey *ecdsa.PublicKey, payload []byte) ([]byte, error) {
key, err := PerformDH(
ecies.ImportECDSA(myIdentityKey),
ecies.ImportECDSAPublic(theirEphemeralKey),
)
if err != nil {
return nil, err
}
return crypto.DecryptSymmetric(key, payload)
}
// keyFromPassiveX3DH decrypts message sent with a X3DH key exchange, storing the key for future exchanges
func (s *EncryptionService) keyFromPassiveX3DH(myIdentityKey *ecdsa.PrivateKey, theirIdentityKey *ecdsa.PublicKey, theirEphemeralKey *ecdsa.PublicKey, ourBundleID []byte) ([]byte, error) {
bundlePrivateKey, err := s.persistence.GetPrivateKeyBundle(ourBundleID)
if err != nil {
s.log.Error("Could not get private bundle", "err", err)
return nil, err
}
if bundlePrivateKey == nil {
return nil, ErrSessionNotFound
}
signedPreKey, err := ecrypto.ToECDSA(bundlePrivateKey)
if err != nil {
s.log.Error("Could not convert to ecdsa", "err", err)
return nil, err
}
key, err := PerformPassiveX3DH(
theirIdentityKey,
signedPreKey,
theirEphemeralKey,
myIdentityKey,
)
if err != nil {
s.log.Error("Could not perform passive x3dh", "err", err)
return nil, err
}
return key, nil
}
func (s *EncryptionService) EnableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error {
myIdentityKeyC := ecrypto.CompressPubkey(myIdentityKey)
return s.persistence.EnableInstallation(myIdentityKeyC, installationID)
}
func (s *EncryptionService) DisableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error {
myIdentityKeyC := ecrypto.CompressPubkey(myIdentityKey)
return s.persistence.DisableInstallation(myIdentityKeyC, installationID)
}
// ProcessPublicBundle persists a bundle and returns a list of tuples identity/installationID
func (s *EncryptionService) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, b *Bundle) ([]IdentityAndIDPair, error) {
// Make sure the bundle belongs to who signed it
identity, err := ExtractIdentity(b)
if err != nil {
return nil, err
}
signedPreKeys := b.GetSignedPreKeys()
var response []IdentityAndIDPair
var installationIDs []string
myIdentityStr := fmt.Sprintf("0x%x", ecrypto.FromECDSAPub(&myIdentityKey.PublicKey))
// Any device from other peers will be considered enabled, ours needs to
// be explicitly enabled
fromOurIdentity := identity != myIdentityStr
for installationID := range signedPreKeys {
if installationID != s.config.InstallationID {
installationIDs = append(installationIDs, installationID)
response = append(response, IdentityAndIDPair{identity, installationID})
}
}
if err = s.persistence.AddInstallations(b.GetIdentity(), b.GetTimestamp(), installationIDs, fromOurIdentity); err != nil {
return nil, err
}
if err = s.persistence.AddPublicBundle(b); err != nil {
return nil, err
}
return response, nil
}
// DecryptPayload decrypts the payload of a DirectMessageProtocol, given an identity private key and the sender's public key
func (s *EncryptionService) DecryptPayload(myIdentityKey *ecdsa.PrivateKey, theirIdentityKey *ecdsa.PublicKey, theirInstallationID string, msgs map[string]*DirectMessageProtocol, messageID []byte) ([]byte, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
msg := msgs[s.config.InstallationID]
if msg == nil {
msg = msgs[noInstallationID]
}
// We should not be sending a signal if it's coming from us, as we receive our own messages
if msg == nil && *theirIdentityKey != myIdentityKey.PublicKey {
return nil, ErrDeviceNotFound
}
payload := msg.GetPayload()
if x3dhHeader := msg.GetX3DHHeader(); x3dhHeader != nil {
bundleID := x3dhHeader.GetId()
theirEphemeralKey, err := ecrypto.DecompressPubkey(x3dhHeader.GetKey())
if err != nil {
return nil, err
}
symmetricKey, err := s.keyFromPassiveX3DH(myIdentityKey, theirIdentityKey, theirEphemeralKey, bundleID)
if err != nil {
return nil, err
}
theirIdentityKeyC := ecrypto.CompressPubkey(theirIdentityKey)
err = s.persistence.AddRatchetInfo(symmetricKey, theirIdentityKeyC, bundleID, nil, theirInstallationID)
if err != nil {
return nil, err
}
}
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,
},
Ciphertext: msg.GetPayload(),
}
theirIdentityKeyC := ecrypto.CompressPubkey(theirIdentityKey)
drInfo, err := s.persistence.GetRatchetInfo(drHeader.GetId(), theirIdentityKeyC, theirInstallationID)
if err != nil {
s.log.Error("Could not get ratchet info", "err", err)
return nil, err
}
// Add installations with a timestamp of 0, as we don't have bundle informations
if err = s.persistence.AddInstallations(theirIdentityKeyC, 0, []string{theirInstallationID}, true); err != nil {
return nil, err
}
// We mark the exchange as successful so we stop sending x3dh header
if err = s.persistence.RatchetInfoConfirmed(drHeader.GetId(), theirIdentityKeyC, theirInstallationID); err != nil {
s.log.Error("Could not confirm ratchet info", "err", err)
return nil, err
}
if drInfo == nil {
s.log.Error("Could not find a session")
return nil, ErrSessionNotFound
}
confirmationData := &ConfirmationData{
header: &drMessage.Header,
drInfo: drInfo,
}
s.messageIDs[confirmationIDString(messageID)] = confirmationData
return s.decryptUsingDR(theirIdentityKey, drInfo, drMessage)
}
// Try DH
if header := msg.GetDHHeader(); header != nil {
decompressedKey, err := ecrypto.DecompressPubkey(header.GetKey())
if err != nil {
return nil, err
}
return s.DecryptWithDH(myIdentityKey, decompressedKey, payload)
}
return nil, errors.New("no key specified")
}
func (s *EncryptionService) createNewSession(drInfo *RatchetInfo, sk [32]byte, keyPair crypto.DHPair) (dr.Session, error) {
var err error
var session dr.Session
if drInfo.PrivateKey != nil {
session, err = dr.New(
drInfo.ID,
sk,
keyPair,
s.persistence.GetSessionStorage(),
dr.WithKeysStorage(s.persistence.GetKeysStorage()),
dr.WithMaxSkip(s.config.MaxSkip),
dr.WithMaxKeep(s.config.MaxKeep),
dr.WithMaxMessageKeysPerSession(s.config.MaxMessageKeysPerSession),
dr.WithCrypto(crypto.EthereumCrypto{}))
} else {
session, err = dr.NewWithRemoteKey(
drInfo.ID,
sk,
keyPair.PubKey,
s.persistence.GetSessionStorage(),
dr.WithKeysStorage(s.persistence.GetKeysStorage()),
dr.WithMaxSkip(s.config.MaxSkip),
dr.WithMaxKeep(s.config.MaxKeep),
dr.WithMaxMessageKeysPerSession(s.config.MaxMessageKeysPerSession),
dr.WithCrypto(crypto.EthereumCrypto{}))
}
return session, err
}
func (s *EncryptionService) encryptUsingDR(theirIdentityKey *ecdsa.PublicKey, drInfo *RatchetInfo, payload []byte) ([]byte, *DRHeader, error) {
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,
}
// Load session from store first
session, err = s.getDRSession(drInfo.ID)
if err != nil {
return nil, nil, err
}
// Create a new one
if session == nil {
session, err = s.createNewSession(drInfo, sk, keyPair)
if err != nil {
return nil, nil, err
}
}
response, err := session.RatchetEncrypt(payload, nil)
if err != nil {
return nil, nil, err
}
header := &DRHeader{
Id: drInfo.BundleID,
Key: response.Header.DH[:],
N: response.Header.N,
Pn: response.Header.PN,
}
return response.Ciphertext, header, nil
}
func (s *EncryptionService) decryptUsingDR(theirIdentityKey *ecdsa.PublicKey, drInfo *RatchetInfo, payload *dr.Message) ([]byte, error) {
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,
}
session, err = s.getDRSession(drInfo.ID)
if err != nil {
return nil, err
}
if session == nil {
session, err = s.createNewSession(drInfo, sk, keyPair)
if err != nil {
return nil, err
}
}
plaintext, err := session.RatchetDecrypt(*payload, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}
func (s *EncryptionService) encryptWithDH(theirIdentityKey *ecdsa.PublicKey, payload []byte) (*DirectMessageProtocol, error) {
symmetricKey, ourEphemeralKey, err := PerformActiveDH(theirIdentityKey)
if err != nil {
return nil, err
}
encryptedPayload, err := crypto.EncryptSymmetric(symmetricKey, payload)
if err != nil {
return nil, err
}
return &DirectMessageProtocol{
DHHeader: &DHHeader{
Key: ecrypto.CompressPubkey(ourEphemeralKey),
},
Payload: encryptedPayload,
}, nil
}
func (s *EncryptionService) EncryptPayloadWithDH(theirIdentityKey *ecdsa.PublicKey, payload []byte) (map[string]*DirectMessageProtocol, error) {
response := make(map[string]*DirectMessageProtocol)
dmp, err := s.encryptWithDH(theirIdentityKey, payload)
if err != nil {
return nil, err
}
response[noInstallationID] = dmp
return response, nil
}
// GetPublicBundle returns the active installations bundles for a given user
func (s *EncryptionService) GetPublicBundle(theirIdentityKey *ecdsa.PublicKey) (*Bundle, error) {
theirIdentityKeyC := ecrypto.CompressPubkey(theirIdentityKey)
installationIDs, err := s.persistence.GetActiveInstallations(s.config.MaxInstallations, theirIdentityKeyC)
if err != nil {
return nil, err
}
return s.persistence.GetPublicBundle(theirIdentityKey, installationIDs)
}
// EncryptPayload returns a new DirectMessageProtocol with a given payload encrypted, given a recipient's public key and the sender private identity key
// TODO: refactor this
// nolint: gocyclo
func (s *EncryptionService) EncryptPayload(theirIdentityKey *ecdsa.PublicKey, myIdentityKey *ecdsa.PrivateKey, payload []byte) (map[string]*DirectMessageProtocol, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.log.Debug("Sending message", "theirKey", theirIdentityKey)
theirIdentityKeyC := ecrypto.CompressPubkey(theirIdentityKey)
// Get their installationIds
installationIds, err := s.persistence.GetActiveInstallations(s.config.MaxInstallations, theirIdentityKeyC)
if err != nil {
return nil, err
}
// We don't have any, send a message with DH
if installationIds == nil && !bytes.Equal(theirIdentityKeyC, ecrypto.CompressPubkey(&myIdentityKey.PublicKey)) {
return s.EncryptPayloadWithDH(theirIdentityKey, payload)
}
response := make(map[string]*DirectMessageProtocol)
for _, installationID := range installationIds {
s.log.Debug("Processing installation", "installationID", installationID)
if s.config.InstallationID == installationID {
continue
}
bundle, err := s.persistence.GetPublicBundle(theirIdentityKey, []string{installationID})
if err != nil {
return nil, err
}
// See if a session is there already
drInfo, err := s.persistence.GetAnyRatchetInfo(theirIdentityKeyC, installationID)
if err != nil {
return nil, err
}
if drInfo != nil {
s.log.Debug("Found DR info", "installationID", installationID)
encryptedPayload, drHeader, err := s.encryptUsingDR(theirIdentityKey, drInfo, payload)
if err != nil {
return nil, err
}
dmp := DirectMessageProtocol{
Payload: encryptedPayload,
DRHeader: drHeader,
}
if drInfo.EphemeralKey != nil {
dmp.X3DHHeader = &X3DHHeader{
Key: drInfo.EphemeralKey,
Id: drInfo.BundleID,
}
}
response[drInfo.InstallationID] = &dmp
continue
}
theirSignedPreKeyContainer := bundle.GetSignedPreKeys()[installationID]
// This should not be nil at this point
if theirSignedPreKeyContainer == nil {
s.log.Warn("Could not find either a ratchet info or a bundle for installationId", "installationID", installationID)
continue
}
s.log.Debug("DR info not found, using bundle", "installationID", installationID)
theirSignedPreKey := theirSignedPreKeyContainer.GetSignedPreKey()
sharedKey, ourEphemeralKey, err := s.keyFromActiveX3DH(theirIdentityKeyC, theirSignedPreKey, myIdentityKey)
if err != nil {
return nil, err
}
theirIdentityKeyC := ecrypto.CompressPubkey(theirIdentityKey)
ourEphemeralKeyC := ecrypto.CompressPubkey(ourEphemeralKey)
err = s.persistence.AddRatchetInfo(sharedKey, theirIdentityKeyC, theirSignedPreKey, ourEphemeralKeyC, installationID)
if err != nil {
return nil, err
}
x3dhHeader := &X3DHHeader{
Key: ourEphemeralKeyC,
Id: theirSignedPreKey,
}
drInfo, err = s.persistence.GetRatchetInfo(theirSignedPreKey, theirIdentityKeyC, installationID)
if err != nil {
return nil, err
}
if drInfo != nil {
encryptedPayload, drHeader, err := s.encryptUsingDR(theirIdentityKey, drInfo, payload)
if err != nil {
return nil, err
}
dmp := &DirectMessageProtocol{
Payload: encryptedPayload,
X3DHHeader: x3dhHeader,
DRHeader: drHeader,
}
response[drInfo.InstallationID] = dmp
}
}
s.log.Debug("Built message", "theirKey", theirIdentityKey)
return response, nil
}