Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add x3dh key exchange #1127

Merged
merged 4 commits into from
Sep 24, 2018
Merged

[WIP] Add x3dh key exchange #1127

merged 4 commits into from
Sep 24, 2018

Conversation

cammellos
Copy link
Contributor

This PR adds x3dh key exchange to the protocol.

It adds a new layer sitting between the transport (whisper) and the higher level protocol (chat).
This layer only understand 3 type of messages, public/broadcast (not encrypted) so noops, direct messages and eventually multicast encrypted (not implemented in this iteration).

To keep compatibility it will need to "fingerprint" messages and decrypt the new ones, leaving the old ones unchanged, for now it just naively tries to decrypt them.

crypto.go is the same encryption used in whisper.

Currently persistence.go uses leveldb and stores keys not encrypted on the filesystem, I made it an interface and the next step is to replace it using a database that supports file encryption.

@cammellos cammellos self-assigned this Jul 30, 2018
// Msg.Dst is empty is a public message, nothing to do
if msg.Dst != nil {
// There's probably a better way to do this
keyBytes, err := hexutil.Bytes(msg.Dst).MarshalText()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a better way for this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fmt.Sprintf()? It should call MarshalText internally.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hexutil.Bytes() has also String() method. fmt.Sprintf() will probably call String() otherwise it would also need to return err. Btw. MarshalText() will prefix the output with 0x. Is that what you want?

}

// This needs to be pushed down in the protocol message
publicKey, err := crypto.UnmarshalPubkey(msg.Sig)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now we rely on whisper to ensure authentication of the message (whisper verifies the signature), eventually we want to move the authentication layer in the encryption layer, but for now this step relies on whisper/transport.


payload, err := api.service.protocol.HandleMessage(privateKey, publicKey, msg.Payload)

// Ignore errors for now
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We ignore errors for now, we should probably have a better fingerprint for old messages, so we don't even attempt to decrypt, I will look into this.

Copy link
Contributor

@adambabik adambabik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First-round :) Overall, looks great!

I have some questions about bundles. We have two types of bundles: public, private and symmetric key in the PersistenceService.

Public bundles are bundles obtained from other contacts, correct?

Private bundles are my bundles? GetAnyPrivateBundle method allows me to get or create any private bundle. Why is it ok to return any bundle? Can a single bundle be used multiple times for different contacts?

Symmetric keys are used for the old protocol encryption?

api/backend.go Outdated
func (b *StatusBackend) CreateX3DHBundle() (string, error) {
selectedAccount, err := b.AccountManager().SelectedAccount()
if selectedAccount == nil || err == account.ErrNoAccountSelected {
return "", nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we return account.ErrNoAccountSelected as error?

api/backend.go Outdated
return "", err
}

jsonBundle, err := bundle.ToJSON()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be implemented with json.Marshaler and you can then call it like this: jsonBundle, err := json.Marshal(bundle)

lib/library.go Outdated
@@ -61,6 +62,20 @@ func StopNode() *C.char {
return makeJSONResponse(nil)
}

// Create an X3DH bundle
//export CreateX3DHBundle
func CreateX3DHBundle() *C.char {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also have func (b *StatusBackend) CreateX3DHBundle() (string, error). Do we need both?

lib/library.go Outdated

cstr := C.CString(bundle)

defer C.free(unsafe.Pointer(cstr))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might crash the program. defer is called before the context is switched to the caller and when it is, the memory pointed by cstr is already gone so the caller won't have a chance to read it. Or my thinking is wrong here and it does work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adambabik I actually wanted to check, is not currently used this method, it will be used only to share the bundle with your contact code, this part needs to be looked up a bit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely gonna blow up.

Copy link
Contributor

@pedropombeiro pedropombeiro Sep 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adambabik This has been a recurring issue in many PRs. Is it something that'll get solved soon with gomobile? Otherwise, maybe we should do a PR that exposes a method to the caller (status-react) to free memory.

// Msg.Dst is empty is a public message, nothing to do
if msg.Dst != nil {
// There's probably a better way to do this
keyBytes, err := hexutil.Bytes(msg.Dst).MarshalText()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hexutil.Bytes() has also String() method. fmt.Sprintf() will probably call String() otherwise it would also need to return err. Btw. MarshalText() will prefix the output with 0x. Is that what you want?

return nil, err
}

privateKey, err := api.service.w.GetPrivateKey(string(keyBytes))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string() type conversion not needed probably.

return key, bundle.GetSignedPreKey(), ephemeralKey, nil
}

func (s *EncryptionService) keyFromDH(pk *ecdsa.PublicKey, privateKey *ecdsa.PrivateKey, payload []byte) ([]byte, *ecdsa.PublicKey, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this method needed? It seems like only the first argument is required.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adambabik no, good spot, I have changed method signature at some point and didn't update the code

// If not there try with a bundle and store the key
if symmetricKey == nil {
encryptionType = EncryptionTypeX3DH
symmetricKey, bundleID, ourEphemeralKey, err = s.keyFromX3DH(theirIdentityKey, myIdentityKey)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

err should be checked.


var publicKeyPrefix = []byte{0x10, 0x11}
var privateBundleKeyPrefix = []byte{0x10, 0x12}
var symmetricKeyKeyPrefix = []byte{0x10, 0x13}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/symmetricKeyKeyPrefix/symmetricKeyPrefix

x3dhKey := msg.GetBundleKey()
bundleID := msg.GetBundleId()
if x3dhKey != nil {
decompressedKey, err := crypto.DecompressPubkey(symKeyID)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't symKeyID be x3dhKey?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct, thanks!

@cammellos
Copy link
Contributor Author

cammellos commented Jul 30, 2018

@adambabik thanks for the comments!

Public bundles are bundles obtained from other contacts, correct?

Correct, contacts but also they are shared in public chats

Private bundles are my bundles? GetAnyPrivateBundle method allows me to get or create any private bundle. Why is it ok to return any bundle? Can a single bundle be used multiple times for different contacts?

It's ok to use the same bundle, although we will want to rotate it every x, but that won't be in the first iteration. Eventually we will have bundle specific to each contact. So if you send to a public chat you share your "shared" bundle, if you send a one-to-one you will propagate a specific bundle (with one-time pre keys), but again that will be not in this iteration.

Symmetric keys are used for the old protocol encryption?

Every time a successful x3dh key exchange is performed we store the sym key generated, which is the one we will use for any successive communication, when we add a ratchet it will be the shared secret to boot the ratchet.

@adambabik
Copy link
Contributor

@cammellos thanks for the clarification. Totally makes sense with x3dh and symmetric keys.

I still don't get why it's ok to call:

func (s *EncryptionService) CreateBundle(privateKey *ecdsa.PrivateKey) (*Bundle, error) {
  // If there is any bundle, it will be always returned here
  bundle, err := s.persistence.GetAnyPrivateBundle()
  // ...  
}

Is this ok because of

It's ok to use the same bundle
and the fact that we want to change that in the next iteration?

@cammellos
Copy link
Contributor Author

@adambabik
basically you can re-use bundles as you will always generate a different sym-key as x3dh depends on the other user identity key & ephemeral key (which always changes).

It is recommend though to refresh the bundle every week/month and use one time pre-keys.

So for the first iteration we will use the same bundle and no one-time pre-keys (which still provides the same guarantees, but not in case of key compromise), just to make sure poc works and everything is ok.

Then the next iteration is to:

  1. Rotate the bundle at a time interval
  2. Use one-time pre keys for contacts

Does it clarify a bit?

@adambabik
Copy link
Contributor

@cammellos yes, perfectly clear now. Thanks!

return nil, err
}

privateKey, err = api.service.w.GetPrivateKey(string(keyBytes))
Copy link
Member

@gravityblast gravityblast Jul 31, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cammellos as I understand keyBytes is the public key of the recipient, can you call GetPrivateKey with that? I thought you had to pass the id but maybe I'm missing something

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah I found this in the docs:

// Originally, public keys
// were used as keys, now random keys are being used. And in order to
// make it easier to consume, we now allow both random IDs and public
// keys to be passed.
func toDeterministicID(id string, expectedLen int) (string, error) {

but does it work with public key for a key created previously?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to be working for older clients ( I have them successfully communicate over public and 1-to-1 chats), but what do you mean exactly created previously? How long ago? (Status-react only stores the key as pk, not the ID)

var privateKey *ecdsa.PrivateKey
var publicKey *ecdsa.PublicKey

// Msg.Dst is empty is a public message, nothing to do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we going to always use the recipient public key in the message (msg.Dst) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So:

  • Messages to public chat will not have msg.Dst as not sent to a particular user
  • One to one messages will have it for now as we use asym encryption in whisper, eventually msg.Dst will be moved inside the encryption layer so we don't rely anymore on it being passed at the transport layer, and the encryption layer is self-sufficient, but that will come in a future iteration ( We need to make it play well with plausible deniability)

Does it answer your question?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok I understand so after this we are not using the topic anymore for one to one chat right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already stopped using topics for one to one ( we disabled them on beta), all the messages go through the discovery topic, the whisper layer will be unchanged after this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok perfect then thank you!!

@status-im-bot
Copy link

Can one of the admins verify this patch?

@cammellos cammellos force-pushed the features/x3dh branch 2 times, most recently from a45683d to 2aaa38f Compare August 21, 2018 08:34
@pedropombeiro pedropombeiro force-pushed the features/x3dh branch 2 times, most recently from e0d6366 to 5575895 Compare August 21, 2018 13:37
@cammellos cammellos force-pushed the features/x3dh branch 7 times, most recently from 7a2d751 to 048096c Compare August 27, 2018 10:33
@pedropombeiro pedropombeiro force-pushed the features/x3dh branch 4 times, most recently from 0daf45a to 6c73968 Compare August 29, 2018 16:17
@@ -130,7 +130,15 @@ func (s *EncryptionService) ProcessPublicBundle(b *Bundle) error {
}

// 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, msg *DirectMessageProtocol) ([]byte, error) {
func (s *EncryptionService) DecryptPayload(myIdentityKey *ecdsa.PrivateKey, theirIdentityKey *ecdsa.PublicKey, msgs *map[string]*DirectMessageProtocol) ([]byte, error) {
msg := (*msgs)[s.installationID]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cammellos what should happen if msgs is nil?

Copy link
Contributor

@pedropombeiro pedropombeiro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some comments. Also, do we want to add some logic in NodeConfig.Validate to return an error if PFS is enabled but InstallationID isn't valid?

@@ -67,6 +79,36 @@ func (s *Service) Protocols() []p2p.Protocol {
return []p2p.Protocol{}
}

func (s *Service) InitProtocol(address string, password string) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public method missing comment


func (s *Service) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, bundle *chat.Bundle) error {
if s.protocol == nil {
return errors.New("Procotol is not initialized")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error could be extracted to a variable. Error message should start with lowercase.

// New returns a new Service.
func New(w *whisper.Whisper, handler EnvelopeEventsHandler, db *leveldb.DB, debug bool) *Service {
// New returns a new Service. dataDir is a folder path to a network-independent location
func New(w *whisper.Whisper, handler EnvelopeEventsHandler, db *leveldb.DB, dataDir string, installationID string, debug bool, pfsEnabled bool) *Service {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be better to pass at least some of these arguments in a struct? The code that calls this function is not very readable currently.

func (api *PublicAPI) SendDirectMessage(ctx context.Context, msg chat.SendDirectMessageRPC) ([]hexutil.Bytes, error) {

if !api.service.pfsEnabled {
return nil, errors.New("PFS not enabled")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be extracted to a var


// SendDirectMessage sends a 1:1 chat message to the underlying transport
func (api *PublicAPI) SendDirectMessage(ctx context.Context, msg chat.SendDirectMessageRPC) ([]hexutil.Bytes, error) {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty line


func (s *Service) GetBundle(myIdentityKey *ecdsa.PrivateKey) (*chat.Bundle, error) {
if s.protocol == nil {
return nil, errors.New("Procotol is not initialized")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lowercase message

@@ -20,6 +20,9 @@ const (

// EventEnodeDiscovered is tiggered when enode has been discovered.
EventEnodeDiscovered = "enode.discovered"

// CouldNotDecrypt is triggered when we receive a message from a bundle we don't have
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of date CouldNotDecrypt?

@@ -34,6 +37,11 @@ type MailServerResponseSignal struct {
Cursor string `json:"cursor"`
}

// DecryptFailedSignal holds the sender of the message that could not be decrypted
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of date DecryptFailedSignal?

Makefile Outdated
@@ -188,6 +188,9 @@ endif
docker push $(BOOTNODE_IMAGE_NAME):latest
docker push $(DOCKER_IMAGE_NAME):latest

protoc-install:
apt install -y protobuf-compiler
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should move this to a script in _assets/scripts so that we can deal more easily with multiple host platforms.

api/backend.go Outdated

func (b *StatusBackend) ProcessContactCode(contactCode string) error {
selectedAccount, err := b.AccountManager().SelectedAccount()
if selectedAccount == nil || err == account.ErrNoAccountSelected {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this code correct for the case where selectedAccount is nil (what error do we want to return)?

@@ -1,6 +1,7 @@
{
"NetworkId": 777,
"DataDir": "/data/ethereumtest/status",
"NoBackupDataDir": "/data/ethereumtest/status",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does NoBackupDataDir stand for? Why is it prefixed with No?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stands for no as is dont. Because we don't want to save in directories that have the automatic backup by default (in android you can disable it, but they provide a no_backup directory, in ios I think it's Library).

apt install -y protobuf-compiler
fi

if [ -x "$(command -v pacman)" ]; then
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add macOS support. I believe it's this package https://formulae.brew.sh/formula/protobuf.

if [ -x "$(command -v brew)" ]; then
	brew install protobuf
fi

api/backend.go Outdated
return err
}

err = st.InitProtocol(address, password)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit tricky for anyone extending this method.

if err := st.InitProtocol(address, password); err != nil {
  return err
}

api/backend.go Outdated
@@ -431,3 +443,58 @@ func appendIf(condition bool, services []gethnode.ServiceConstructor, service ge
}
return append(services, service)
}

func (b *StatusBackend) CreateContactCode() (string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment what this method does.

api/backend.go Outdated
return "", err
}

base64Bundle, err := bundle.ToBase64()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just return bundle.ToBase64() will work.

lib/library.go Outdated

cstr := C.CString(bundle)

defer C.free(unsafe.Pointer(cstr))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely gonna blow up.

log log.Logger
persistence PersistenceServiceInterface
installationID string
mutex *sync.Mutex
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mutex sync.Mutex is fine, no need to make it a pointer. A default value is unlocked.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it is used btw.

}

// 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

msgs map[string]*DirectMessageProtocol will be fine. maps are passed by a pointer.

}

// PersistenceServiceInterface defines the interface for a storage service
type PersistenceServiceInterface interface {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: Interfaces usually are called without Interface suffix.

}

for installationID, signedPreKey := range b.GetBundle().GetSignedPreKeys() {
stmt, err := tx.Prepare("insert into bundles(identity, private_key, signed_pre_key, installation_id, timestamp) values(?, ?, ?, ?, ?)")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the queries below, SQL keywords are uppercase.

}

// NewSQLLiteKeysStorage creates a new SQLLiteKeysStorage instance associated with the specified database
func NewSQLLiteKeysStorage(db *sql.DB) *SQLLiteKeysStorage {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constructors, struct declarations and methods are a bit mixed. It would be great to group them per type.

@cammellos cammellos force-pushed the features/x3dh branch 4 times, most recently from 1beb6fb to 826b5e5 Compare September 18, 2018 12:23
@cammellos cammellos force-pushed the features/x3dh branch 4 times, most recently from b0b11b6 to 16773d6 Compare September 24, 2018 17:17
@cammellos cammellos merged commit dcaf8ca into develop Sep 24, 2018
@adambabik adambabik deleted the features/x3dh branch December 27, 2019 10:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants