-
Notifications
You must be signed in to change notification settings - Fork 249
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
Conversation
services/shhext/api.go
Outdated
// 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() |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks!
There was a problem hiding this comment.
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?
services/shhext/api.go
Outdated
} | ||
|
||
// This needs to be pushed down in the protocol message | ||
publicKey, err := crypto.UnmarshalPubkey(msg.Sig) |
There was a problem hiding this comment.
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.
services/shhext/api.go
Outdated
|
||
payload, err := api.service.protocol.HandleMessage(privateKey, publicKey, msg.Payload) | ||
|
||
// Ignore errors for now |
There was a problem hiding this comment.
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.
There was a problem hiding this 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 |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
services/shhext/api.go
Outdated
// 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() |
There was a problem hiding this comment.
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?
services/shhext/api.go
Outdated
return nil, err | ||
} | ||
|
||
privateKey, err := api.service.w.GetPrivateKey(string(keyBytes)) |
There was a problem hiding this comment.
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.
services/shhext/chat/encryption.go
Outdated
return key, bundle.GetSignedPreKey(), ephemeralKey, nil | ||
} | ||
|
||
func (s *EncryptionService) keyFromDH(pk *ecdsa.PublicKey, privateKey *ecdsa.PrivateKey, payload []byte) ([]byte, *ecdsa.PublicKey, error) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
services/shhext/chat/encryption.go
Outdated
// If not there try with a bundle and store the key | ||
if symmetricKey == nil { | ||
encryptionType = EncryptionTypeX3DH | ||
symmetricKey, bundleID, ourEphemeralKey, err = s.keyFromX3DH(theirIdentityKey, myIdentityKey) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
err
should be checked.
services/shhext/chat/persistence.go
Outdated
|
||
var publicKeyPrefix = []byte{0x10, 0x11} | ||
var privateBundleKeyPrefix = []byte{0x10, 0x12} | ||
var symmetricKeyKeyPrefix = []byte{0x10, 0x13} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/symmetricKeyKeyPrefix/symmetricKeyPrefix
services/shhext/chat/protocol.go
Outdated
x3dhKey := msg.GetBundleKey() | ||
bundleID := msg.GetBundleId() | ||
if x3dhKey != nil { | ||
decompressedKey, err := crypto.DecompressPubkey(symKeyID) |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
correct, thanks!
@adambabik thanks for the comments!
Correct, contacts but also they are shared in public chats
It's ok to use the same bundle, although we will want to rotate it every
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. |
@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
|
@adambabik 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:
Does it clarify a bit? |
@cammellos yes, perfectly clear now. Thanks! |
services/shhext/api.go
Outdated
return nil, err | ||
} | ||
|
||
privateKey, err = api.service.w.GetPrivateKey(string(keyBytes)) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
services/shhext/api.go
Outdated
var privateKey *ecdsa.PrivateKey | ||
var publicKey *ecdsa.PublicKey | ||
|
||
// Msg.Dst is empty is a public message, nothing to do |
There was a problem hiding this comment.
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
) ?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!!
e25b49d
to
7016ff4
Compare
Can one of the admins verify this patch? |
6dd4f35
to
f696396
Compare
a45683d
to
2aaa38f
Compare
e0d6366
to
5575895
Compare
7a2d751
to
048096c
Compare
0daf45a
to
6c73968
Compare
services/shhext/chat/encryption.go
Outdated
@@ -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] |
There was a problem hiding this comment.
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?
189025d
to
a357aff
Compare
8e26c60
to
1699d72
Compare
1c5a67c
to
9c9930e
Compare
There was a problem hiding this 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?
services/shhext/service.go
Outdated
@@ -67,6 +79,36 @@ func (s *Service) Protocols() []p2p.Protocol { | |||
return []p2p.Protocol{} | |||
} | |||
|
|||
func (s *Service) InitProtocol(address string, password string) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Public method missing comment
services/shhext/service.go
Outdated
|
||
func (s *Service) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, bundle *chat.Bundle) error { | ||
if s.protocol == nil { | ||
return errors.New("Procotol is not initialized") |
There was a problem hiding this comment.
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.
services/shhext/service.go
Outdated
// 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 { |
There was a problem hiding this comment.
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.
services/shhext/api.go
Outdated
func (api *PublicAPI) SendDirectMessage(ctx context.Context, msg chat.SendDirectMessageRPC) ([]hexutil.Bytes, error) { | ||
|
||
if !api.service.pfsEnabled { | ||
return nil, errors.New("PFS not enabled") |
There was a problem hiding this comment.
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
services/shhext/api.go
Outdated
|
||
// SendDirectMessage sends a 1:1 chat message to the underlying transport | ||
func (api *PublicAPI) SendDirectMessage(ctx context.Context, msg chat.SendDirectMessageRPC) ([]hexutil.Bytes, error) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty line
services/shhext/service.go
Outdated
|
||
func (s *Service) GetBundle(myIdentityKey *ecdsa.PrivateKey) (*chat.Bundle, error) { | ||
if s.protocol == nil { | ||
return nil, errors.New("Procotol is not initialized") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lowercase message
signal/events_shhext.go
Outdated
@@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of date CouldNotDecrypt
?
signal/events_shhext.go
Outdated
@@ -34,6 +37,11 @@ type MailServerResponseSignal struct { | |||
Cursor string `json:"cursor"` | |||
} | |||
|
|||
// DecryptFailedSignal holds the sender of the message that could not be decrypted |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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)?
1bf8c84
to
5df33a8
Compare
5df33a8
to
8230f9f
Compare
@@ -1,6 +1,7 @@ | |||
{ | |||
"NetworkId": 777, | |||
"DataDir": "/data/ethereumtest/status", | |||
"NoBackupDataDir": "/data/ethereumtest/status", |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
).
_assets/scripts/install_deps.sh
Outdated
apt install -y protobuf-compiler | ||
fi | ||
|
||
if [ -x "$(command -v pacman)" ]; then |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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.
services/shhext/chat/encryption.go
Outdated
log log.Logger | ||
persistence PersistenceServiceInterface | ||
installationID string | ||
mutex *sync.Mutex |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
services/shhext/chat/encryption.go
Outdated
} | ||
|
||
// 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) { |
There was a problem hiding this comment.
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. map
s are passed by a pointer.
services/shhext/chat/persistence.go
Outdated
} | ||
|
||
// PersistenceServiceInterface defines the interface for a storage service | ||
type PersistenceServiceInterface interface { |
There was a problem hiding this comment.
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(?, ?, ?, ?, ?)") |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
1beb6fb
to
826b5e5
Compare
b0b11b6
to
16773d6
Compare
16773d6
to
1ac9dd9
Compare
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 inwhisper
.Currently
persistence.go
usesleveldb
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.