status-go/services/shhext/chat/encryption.go

469 lines
12 KiB
Go

package chat
import (
"bytes"
"crypto/ecdsa"
"errors"
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"
"sync"
"time"
"github.com/status-im/status-go/services/shhext/chat/crypto"
)
var ErrSessionNotFound = errors.New("session not found")
// EncryptionService defines a service that is responsible for the encryption aspect of the protocol
type EncryptionService struct {
log log.Logger
persistence PersistenceService
installationID string
mutex sync.Mutex
}
// NewEncryptionService creates a new EncryptionService instance
func NewEncryptionService(p PersistenceService, installationID string) *EncryptionService {
logger := log.New("package", "status-go/services/sshext.chat")
logger.Info("Initialized encryption service", "installationID", installationID)
return &EncryptionService{
log: logger,
persistence: p,
installationID: installationID,
mutex: sync.Mutex{},
}
}
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
}
// 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)
bundleContainer, err := s.persistence.GetAnyPrivateBundle(ourIdentityKeyC)
if err != nil {
return nil, err
}
// If the bundle has expired we create a new one
if bundleContainer != nil && bundleContainer.Timestamp < time.Now().AddDate(0, 0, -14).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.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
}
// ProcessPublicBundle persists a bundle
func (s *EncryptionService) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, b *Bundle) error {
// Make sure the bundle belongs to who signed it
err := VerifyBundle(b)
if err != nil {
return err
}
return s.persistence.AddPublicBundle(b)
}
// 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, msgs map[string]*DirectMessageProtocol) ([]byte, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
msg := msgs[s.installationID]
if msg == nil {
msg = msgs["none"]
}
if msg == nil {
return nil, ErrSessionNotFound
}
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, x3dhHeader.GetInstallationId())
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)
if err != nil {
s.log.Error("Could not get ratchet info", "err", err)
return nil, err
}
// We mark the exchange as successful so we stop sending x3dh header
if err = s.persistence.RatchetInfoConfirmed(drHeader.GetId(), theirIdentityKeyC); 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
}
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.WithCrypto(crypto.EthereumCrypto{}))
} else {
session, err = dr.NewWithRemoteKey(
drInfo.ID,
sk,
keyPair.PubKey,
s.persistence.GetSessionStorage(),
dr.WithKeysStorage(s.persistence.GetKeysStorage()),
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,
}
sessionStorage := s.persistence.GetSessionStorage()
// Load session from store first
session, err = dr.Load(
drInfo.ID,
sessionStorage,
dr.WithKeysStorage(s.persistence.GetKeysStorage()),
dr.WithCrypto(crypto.EthereumCrypto{}),
)
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,
}
sessionStorage := s.persistence.GetSessionStorage()
session, err = dr.Load(
drInfo.ID,
sessionStorage,
dr.WithKeysStorage(s.persistence.GetKeysStorage()),
dr.WithCrypto(crypto.EthereumCrypto{}),
)
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
}
// 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()
response := make(map[string]*DirectMessageProtocol)
theirIdentityKeyC := ecrypto.CompressPubkey(theirIdentityKey)
// Get their latest bundle
theirBundle, err := s.persistence.GetPublicBundle(theirIdentityKey)
if err != nil {
return nil, err
}
// We don't have any, send a message with DH
if theirBundle == nil && !bytes.Equal(theirIdentityKeyC, ecrypto.CompressPubkey(&myIdentityKey.PublicKey)) {
dmp, err := s.encryptWithDH(theirIdentityKey, payload)
if err != nil {
return nil, err
}
response["none"] = dmp
return response, nil
}
for installationID, signedPreKeyContainer := range theirBundle.GetSignedPreKeys() {
if s.installationID == installationID {
continue
}
theirSignedPreKey := signedPreKeyContainer.GetSignedPreKey()
// See if a session is there already
drInfo, err := s.persistence.GetAnyRatchetInfo(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,
DRHeader: drHeader,
}
if drInfo.EphemeralKey != nil {
dmp.X3DHHeader = &X3DHHeader{
Key: drInfo.EphemeralKey,
Id: drInfo.BundleID,
InstallationId: s.installationID,
}
}
response[drInfo.InstallationID] = &dmp
return response, nil
}
// check if a bundle is there
theirBundle, err := s.persistence.GetPublicBundle(theirIdentityKey)
if err != nil {
return nil, err
}
if theirBundle != nil {
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,
InstallationId: s.installationID,
}
drInfo, err := s.persistence.GetAnyRatchetInfo(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
}
}
}
return response, nil
}