2018-09-24 18:07:34 +00:00
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" )
2018-10-16 10:31:05 +00:00
// If we have no bundles, we use a constant so that the message can reach any device
const noInstallationID = "none"
2018-09-24 18:07:34 +00:00
// 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
}
2018-10-16 10:31:05 +00:00
type IdentityAndIDPair [ 2 ] string
2018-09-24 18:07:34 +00:00
// 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
}
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 ( )
response := make ( [ ] IdentityAndIDPair , len ( signedPreKeys ) )
if err = s . persistence . AddPublicBundle ( b ) ; err != nil {
return nil , err
}
index := 0
for installationID := range signedPreKeys {
response [ index ] = IdentityAndIDPair { identity , installationID }
index ++
}
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 ( )
msg := msgs [ s . installationID ]
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 ( ) ) ,
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
}
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 )
// 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 ) ) {
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 ( ) {
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
2018-10-16 10:31:05 +00:00
continue
2018-09-24 18:07:34 +00:00
}
// 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
}