2018-09-24 18:07:34 +00:00
package chat
import (
"bytes"
"crypto/ecdsa"
"errors"
2018-11-06 08:05:32 +00:00
"fmt"
2018-09-24 18:07:34 +00:00
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" )
2018-11-06 08:05:32 +00:00
// If we have no bundles, we use a constant so that the message can reach any device.
2018-10-16 10:31:05 +00:00
const noInstallationID = "none"
2018-11-06 08:05:32 +00:00
// EncryptionService defines a service that is responsible for the encryption aspect of the protocol.
2018-09-24 18:07:34 +00:00
type EncryptionService struct {
2018-11-27 08:54:20 +00:00
log log . Logger
persistence PersistenceService
config EncryptionServiceConfig
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
2018-11-28 11:34:39 +00:00
// How long before we refresh the interval in milliseconds
BundleRefreshInterval int64
2018-09-24 18:07:34 +00:00
}
2018-10-16 10:31:05 +00:00
type IdentityAndIDPair [ 2 ] string
2018-11-27 08:54:20 +00:00
// DefaultEncryptionServiceConfig returns the default values used by the encryption service
func DefaultEncryptionServiceConfig ( installationID string ) EncryptionServiceConfig {
return EncryptionServiceConfig {
MaxInstallations : 5 ,
MaxSkip : 1000 ,
MaxKeep : 3000 ,
MaxMessageKeysPerSession : 2000 ,
2018-11-28 11:34:39 +00:00
BundleRefreshInterval : 14 * 24 * 60 * 60 * 1000 ,
2018-11-27 08:54:20 +00:00
InstallationID : installationID ,
}
}
2018-11-06 08:05:32 +00:00
// NewEncryptionService creates a new EncryptionService instance.
2018-11-27 08:54:20 +00:00
func NewEncryptionService ( p PersistenceService , config EncryptionServiceConfig ) * EncryptionService {
2018-09-24 18:07:34 +00:00
logger := log . New ( "package" , "status-go/services/sshext.chat" )
2018-11-27 08:54:20 +00:00
logger . Info ( "Initialized encryption service" , "installationID" , config . InstallationID )
2018-09-24 18:07:34 +00:00
return & EncryptionService {
2018-11-27 08:54:20 +00:00
log : logger ,
persistence : p ,
config : config ,
mutex : sync . Mutex { } ,
2018-09-24 18:07:34 +00:00
}
}
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
}
2018-11-27 08:54:20 +00:00
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 { } ) ,
)
}
2018-09-24 18:07:34 +00:00
// 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 )
2018-11-06 08:05:32 +00:00
2018-11-27 08:54:20 +00:00
installationIDs , err := s . persistence . GetActiveInstallations ( s . config . MaxInstallations - 1 , ourIdentityKeyC )
2018-11-06 08:05:32 +00:00
if err != nil {
return nil , err
}
2018-11-27 08:54:20 +00:00
installationIDs = append ( installationIDs , s . config . InstallationID )
2018-11-06 08:05:32 +00:00
bundleContainer , err := s . persistence . GetAnyPrivateBundle ( ourIdentityKeyC , installationIDs )
2018-09-24 18:07:34 +00:00
if err != nil {
return nil , err
}
// If the bundle has expired we create a new one
2018-11-28 11:34:39 +00:00
if bundleContainer != nil && bundleContainer . GetBundle ( ) . Timestamp < time . Now ( ) . Add ( - 1 * time . Duration ( s . config . BundleRefreshInterval ) * time . Millisecond ) . UnixNano ( ) {
2018-09-24 18:07:34 +00:00
// 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
2018-11-27 08:54:20 +00:00
bundleContainer , err = NewBundleContainer ( privateKey , s . config . InstallationID )
2018-09-24 18:07:34 +00:00
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
}
2018-11-06 08:05:32 +00:00
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 )
}
2018-10-16 10:31:05 +00:00
// ProcessPublicBundle persists a bundle and returns a list of tuples identity/installationID
func ( s * EncryptionService ) ProcessPublicBundle ( myIdentityKey * ecdsa . PrivateKey , b * Bundle ) ( [ ] IdentityAndIDPair , error ) {
2018-09-24 18:07:34 +00:00
// Make sure the bundle belongs to who signed it
2018-10-16 10:31:05 +00:00
identity , err := ExtractIdentity ( b )
2018-09-24 18:07:34 +00:00
if err != nil {
2018-10-16 10:31:05 +00:00
return nil , err
2018-09-24 18:07:34 +00:00
}
2018-10-16 10:31:05 +00:00
signedPreKeys := b . GetSignedPreKeys ( )
2018-11-06 08:05:32 +00:00
var response [ ] IdentityAndIDPair
var installationIDs [ ] string
myIdentityStr := fmt . Sprintf ( "0x%x" , ecrypto . FromECDSAPub ( & myIdentityKey . PublicKey ) )
2018-10-16 10:31:05 +00:00
2018-11-06 08:05:32 +00:00
// Any device from other peers will be considered enabled, ours needs to
// be explicitly enabled
fromOurIdentity := identity != myIdentityStr
for installationID := range signedPreKeys {
2018-11-27 08:54:20 +00:00
if installationID != s . config . InstallationID {
2018-11-06 08:05:32 +00:00
installationIDs = append ( installationIDs , installationID )
response = append ( response , IdentityAndIDPair { identity , installationID } )
}
}
if err = s . persistence . AddInstallations ( b . GetIdentity ( ) , b . GetTimestamp ( ) , installationIDs , fromOurIdentity ) ; err != nil {
2018-10-16 10:31:05 +00:00
return nil , err
}
2018-11-06 08:05:32 +00:00
if err = s . persistence . AddPublicBundle ( b ) ; err != nil {
return nil , err
2018-10-16 10:31:05 +00:00
}
return response , nil
2018-09-24 18:07:34 +00:00
}
// DecryptPayload decrypts the payload of a DirectMessageProtocol, given an identity private key and the sender's public key
2018-10-16 10:31:05 +00:00
func ( s * EncryptionService ) DecryptPayload ( myIdentityKey * ecdsa . PrivateKey , theirIdentityKey * ecdsa . PublicKey , theirInstallationID string , msgs map [ string ] * DirectMessageProtocol ) ( [ ] byte , error ) {
2018-09-24 18:07:34 +00:00
s . mutex . Lock ( )
defer s . mutex . Unlock ( )
2018-11-27 08:54:20 +00:00
msg := msgs [ s . config . InstallationID ]
2018-09-24 18:07:34 +00:00
if msg == nil {
2018-10-16 10:31:05 +00:00
msg = msgs [ noInstallationID ]
2018-09-24 18:07:34 +00:00
}
2018-10-16 10:31:05 +00:00
// We should not be sending a signal if it's coming from us, as we receive our own messages
2018-09-24 18:07:34 +00:00
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 )
2018-10-16 10:31:05 +00:00
err = s . persistence . AddRatchetInfo ( symmetricKey , theirIdentityKeyC , bundleID , nil , theirInstallationID )
2018-09-24 18:07:34 +00:00
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 )
2018-10-16 10:31:05 +00:00
drInfo , err := s . persistence . GetRatchetInfo ( drHeader . GetId ( ) , theirIdentityKeyC , theirInstallationID )
2018-09-24 18:07:34 +00:00
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
2018-10-16 10:31:05 +00:00
if err = s . persistence . RatchetInfoConfirmed ( drHeader . GetId ( ) , theirIdentityKeyC , theirInstallationID ) ; err != nil {
2018-09-24 18:07:34 +00:00
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 ( ) ) ,
2018-11-27 08:54:20 +00:00
dr . WithMaxSkip ( s . config . MaxSkip ) ,
dr . WithMaxKeep ( s . config . MaxKeep ) ,
dr . WithMaxMessageKeysPerSession ( s . config . MaxMessageKeysPerSession ) ,
2018-09-24 18:07:34 +00:00
dr . WithCrypto ( crypto . EthereumCrypto { } ) )
} else {
session , err = dr . NewWithRemoteKey (
drInfo . ID ,
sk ,
keyPair . PubKey ,
s . persistence . GetSessionStorage ( ) ,
dr . WithKeysStorage ( s . persistence . GetKeysStorage ( ) ) ,
2018-11-27 08:54:20 +00:00
dr . WithMaxSkip ( s . config . MaxSkip ) ,
dr . WithMaxKeep ( s . config . MaxKeep ) ,
dr . WithMaxMessageKeysPerSession ( s . config . MaxMessageKeysPerSession ) ,
2018-09-24 18:07:34 +00:00
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
2018-11-27 08:54:20 +00:00
session , err = s . getDRSession ( drInfo . ID )
2018-09-24 18:07:34 +00:00
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 ,
}
2018-11-27 08:54:20 +00:00
session , err = s . getDRSession ( drInfo . ID )
2018-09-24 18:07:34 +00:00
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
}
2018-10-16 10:31:05 +00:00
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
}
2018-09-24 18:07:34 +00:00
// 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 ( )
theirIdentityKeyC := ecrypto . CompressPubkey ( theirIdentityKey )
2018-11-27 08:54:20 +00:00
installationIDs , err := s . persistence . GetActiveInstallations ( s . config . MaxInstallations , theirIdentityKeyC )
2018-11-06 08:05:32 +00:00
if err != nil {
return nil , err
}
2018-09-24 18:07:34 +00:00
// Get their latest bundle
2018-11-06 08:05:32 +00:00
theirBundle , err := s . persistence . GetPublicBundle ( theirIdentityKey , installationIDs )
2018-09-24 18:07:34 +00:00
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 ) ) {
2018-10-16 10:31:05 +00:00
return s . EncryptPayloadWithDH ( theirIdentityKey , payload )
2018-09-24 18:07:34 +00:00
}
2018-10-16 10:31:05 +00:00
response := make ( map [ string ] * DirectMessageProtocol )
2018-09-24 18:07:34 +00:00
for installationID , signedPreKeyContainer := range theirBundle . GetSignedPreKeys ( ) {
2018-11-27 08:54:20 +00:00
if s . config . InstallationID == installationID {
2018-09-24 18:07:34 +00:00
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 {
2018-11-06 08:05:32 +00:00
Key : drInfo . EphemeralKey ,
Id : drInfo . BundleID ,
2018-09-24 18:07:34 +00:00
}
}
response [ drInfo . InstallationID ] = & dmp
2018-10-16 10:31:05 +00:00
continue
2018-09-24 18:07:34 +00:00
}
2018-11-06 08:05:32 +00:00
sharedKey , ourEphemeralKey , err := s . keyFromActiveX3DH ( theirIdentityKeyC , theirSignedPreKey , myIdentityKey )
2018-09-24 18:07:34 +00:00
if err != nil {
return nil , err
}
2018-11-06 08:05:32 +00:00
theirIdentityKeyC := ecrypto . CompressPubkey ( theirIdentityKey )
ourEphemeralKeyC := ecrypto . CompressPubkey ( ourEphemeralKey )
2018-09-24 18:07:34 +00:00
2018-11-06 08:05:32 +00:00
err = s . persistence . AddRatchetInfo ( sharedKey , theirIdentityKeyC , theirSignedPreKey , ourEphemeralKeyC , installationID )
if err != nil {
return nil , err
}
2018-09-24 18:07:34 +00:00
2018-11-06 08:05:32 +00:00
x3dhHeader := & X3DHHeader {
Key : ourEphemeralKeyC ,
Id : theirSignedPreKey ,
}
2018-09-24 18:07:34 +00:00
2018-11-27 08:54:20 +00:00
drInfo , err = s . persistence . GetRatchetInfo ( theirSignedPreKey , theirIdentityKeyC , installationID )
2018-11-06 08:05:32 +00:00
if err != nil {
return nil , err
}
2018-09-24 18:07:34 +00:00
2018-11-06 08:05:32 +00:00
if drInfo != nil {
encryptedPayload , drHeader , err := s . encryptUsingDR ( theirIdentityKey , drInfo , payload )
2018-09-24 18:07:34 +00:00
if err != nil {
return nil , err
}
2018-11-06 08:05:32 +00:00
dmp := & DirectMessageProtocol {
Payload : encryptedPayload ,
X3DHHeader : x3dhHeader ,
DRHeader : drHeader ,
2018-09-24 18:07:34 +00:00
}
2018-11-06 08:05:32 +00:00
response [ drInfo . InstallationID ] = dmp
2018-09-24 18:07:34 +00:00
}
}
return response , nil
}