diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0e232306e01c4b38cea29bb638bcdd9f96243e7e..bebba190b73078af76aaeff8b18f6637043ad385 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -52,6 +52,7 @@ build: except: - tags script: + - go mod vendor -v - make version - mkdir -p release # - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' ./... @@ -83,8 +84,11 @@ bindings-ios: - ios script: - export PATH=$PATH:$HOME/go/bin + - go mod tidy + - rm -rf vendor/ + - go build ./... + - go get golang.org/x/mobile/bind - go install golang.org/x/mobile/cmd/gomobile@latest - - go get golang.org/x/mobile/cmd/gobind - gomobile init - gomobile bind -target ios gitlab.com/elixxir/client/bindings - ls @@ -103,10 +107,12 @@ bindings-android: script: - export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin:/android-sdk/platform-tools/ - export ANDROID_HOME=/android-sdk - # Build the bindings + - go mod tidy + - rm -rf vendor/ + - go build ./... + - go get golang.org/x/mobile/bind - go install golang.org/x/mobile/cmd/gomobile@latest - - go get golang.org/x/mobile/cmd/gobind - gomobile init - gomobile bind -target android -androidapi 21 gitlab.com/elixxir/client/bindings artifacts: diff --git a/README.md b/README.md index 0d0938ebbe404dc1cefd87833031017d3d9c6564..b7b5a89cb479a9bfb2433dffd6b5f8857a6050ac 100644 --- a/README.md +++ b/README.md @@ -3,28 +3,30 @@ [](https://gitlab.com/elixxir/client/commits/master) [](https://gitlab.com/elixxir/client/commits/master) -The xx network client is a library and related command line tool -that facilitate making full-featured xx clients for all platforms. The -command line tool can be built for any platform supported by -golang. The libraries are built for iOS and Android using -[gomobile](https://godoc.org/golang.org/x/mobile/cmd/gomobile). + +The client is a library and related command-line tool +that facilitates making full-featured xx clients for all platforms. It interfaces with the cMix system, enabling access +to all xx network messaging features, including end-to-end encryption and metadata protection. This repository contains everything necessary to implement all of the -xx network messaging features. These include the end-to-end encryption -and metadata protection. It also contains features to extend the base +xx network messaging features. It also contains features to extend the base messaging protocols. +The command-line tool accompanying the client library can be built for any platform supported by +golang. The libraries are built for iOS and Android using +[gomobile](https://godoc.org/golang.org/x/mobile/cmd/gomobile). + For library writers, the client requires a writable folder to store data, functions for receiving and approving requests for creating secure end-to-end messaging channels, for discovering users, and for receiving different types of messages. Details for implementing these -features are in the Library Overview section below. +features are in the [Library Overview section](#library-overview) below. -The client is open source software released under the simplified BSD License. +The client is open-source software released under the simplified BSD License. ## Command Line Usage -The command line tool is intended for testing xx network functionality and not +The command-line tool is intended for testing xx network functionality and not for regular user use. These instructions assume that you have [Go 1.17.X installed](https://go.dev/doc/install), and GCC installed for Cgo (such as `build-essential` on Debian or Ubuntu). @@ -47,31 +49,36 @@ GOOS=windows GOARCH=386 CGO_ENABLED=0 go build -ldflags '-w -s' -o release/clien GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' -o release/client.darwin64 main.go ``` -To get an NDF from a network gateway and the permissioning server, use the `getndf` subcommand. The `getndf` subcommand allows command line users to poll the NDF from both a gateway and the permissioning server without any pre-established client connection. It requires an IP address, port, and ssl certificate. You can download an ssl cert with: +#### Fetching an NDF + +All actions performed with the client require a current [NDF](https://xxdk-dev.xx.network/technical-glossary#network-definition-file-ndf). The NDF is downloadable from the command line or [via an access point](https://xxdk-dev.xx.network/quick-reference#func-downloadandverifysignedndfwithurl) in the Client API. + +Use the `getndf` command to fetch the NDF via the command line. `getndf` enables command-line users to poll the NDF from a network gateway without any pre-established client connection. + +First, you'll want to download an SSL certificate: + ``` -openssl s_client -showcerts -connect permissioning.prod.cmix.rip:11420 < /dev/null 2>&1 | openssl x509 -outform PEM > certfile.pem +// Assumes you are running a gateway locally +openssl s_client -showcerts -connect localhost:8440 < /dev/null 2>&1 | openssl x509 -outform PEM > certfile.pem ``` -Example usage for Gateways: +Now you can fetch the NDF: ``` -$ go run main.go getndf --gwhost localhost:8440 --cert ~/integration/keys/cmix.rip.crt | jq . | head -{ - "Timestamp": "2021-01-29T01:19:49.227246827Z", - "Gateways": [ - { - "Id": "BRM+Iotl6ujIGhjRddZMBdauapS7Z6jL0FJGq7IkUdYB", - "Address": ":8440", - "Tls_certificate": "-----BEGIN CERTIFICATE-----\nMIIDbDCCAlSgAwIBAgIJAOUNtZneIYECMA0GCSqGSIb3DQEBBQUAMGgxCzAJBgNV\nBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQx\nGzAZBgNVBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJp\ncDAeFw0xOTAzMDUxODM1NDNaFw0yOTAzMDIxODM1NDNaMGgxCzAJBgNVBAYTAlVT\nMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQxGzAZBgNV\nBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJpcDCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPP0WyVkfZA/CEd2DgKpcudn0oDh\nDwsjmx8LBDWsUgQzyLrFiVigfUmUefknUH3dTJjmiJtGqLsayCnWdqWLHPJYvFfs\nWYW0IGF93UG/4N5UAWO4okC3CYgKSi4ekpfw2zgZq0gmbzTnXcHF9gfmQ7jJUKSE\ntJPSNzXq+PZeJTC9zJAb4Lj8QzH18rDM8DaL2y1ns0Y2Hu0edBFn/OqavBJKb/uA\nm3AEjqeOhC7EQUjVamWlTBPt40+B/6aFJX5BYm2JFkRsGBIyBVL46MvC02MgzTT9\nbJIJfwqmBaTruwemNgzGu7Jk03hqqS1TUEvSI6/x8bVoba3orcKkf9HsDjECAwEA\nAaMZMBcwFQYDVR0RBA4wDIIKKi5jbWl4LnJpcDANBgkqhkiG9w0BAQUFAAOCAQEA\nneUocN4AbcQAC1+b3To8u5UGdaGxhcGyZBlAoenRVdjXK3lTjsMdMWb4QctgNfIf\nU/zuUn2mxTmF/ekP0gCCgtleZr9+DYKU5hlXk8K10uKxGD6EvoiXZzlfeUuotgp2\nqvI3ysOm/hvCfyEkqhfHtbxjV7j7v7eQFPbvNaXbLa0yr4C4vMK/Z09Ui9JrZ/Z4\ncyIkxfC6/rOqAirSdIp09EGiw7GM8guHyggE4IiZrDslT8V3xIl985cbCxSxeW1R\ntgH4rdEXuVe9+31oJhmXOE9ux2jCop9tEJMgWg7HStrJ5plPbb+HmjoX3nBO04E5\n6m52PyzMNV+2N21IPppKwA==\n-----END CERTIFICATE-----\n" - }, - { - "Id": "JCBd9mAQb2BW8hc8H9avy1ubcjUAa7MHrPp0dBU/VqQB", +// Example usage for Gateways, assumes you are running a gateway locally +$ go run main.go getndf --gwhost localhost:8440 --cert certfile.pem | jq . >ndf.json ``` -Example usage for the Permissioning server: +You can also download an NDF directly for different environments by using the `--env` flag: + +```go +$ go run main.go getndf --env mainnet | jq . >ndf.json +// Or, run via the binary (assuming 64-bit Windows): +$ ./client.win64 getndf --env mainnet | jq . >ndf.json +``` +Sample content of `ndf.json`: ``` -$ go run main.go getndf --permhost localhost:18000 --cert ~/integration/keys/cmix.rip.crt | jq . | head { "Timestamp": "2021-01-29T01:19:49.227246827Z", "Gateways": [ @@ -82,61 +89,121 @@ $ go run main.go getndf --permhost localhost:18000 --cert ~/integration/keys/cmi }, { "Id": "JCBd9mAQb2BW8hc8H9avy1ubcjUAa7MHrPp0dBU/VqQB", + ..... ``` -Basic command line usage, sending unsafe, unencrypted messages to yourself: +#### Sending Safe Messages Between Two (2) Users + +**Note:** For information on receiving messages and troubleshooting authenticated channel requests, see [Receiving Messages](#receiving-messages) and [Confirming authenticated channel requests](#confirming-authenticated-channel-requests). + +To send messages with end-to-end encryption, you must first establish a connection +or [authenticated channel](https://xxdk-dev.xx.network/technical-glossary#authenticated-channel) with the other user. See below for example commands for sending or confirming authenticated channel requests, as well as for sending E2E messages: ``` -client --password user-password --ndf ndf.json -l client.log -s session-directory --writeContact user-contact.json --unsafe -m \"Hello World, without E2E Encryption\" +# Get user contact jsons for each client +$ client --password user1-password --ndf ndf.json -l client1.log -s user1session --writeContact user1-contact.json --unsafe -m "Hi to me, without E2E Encryption" +$ client --password user2-password --ndf ndf.json -l client2.log -s user2session --writeContact user2-contact.json --unsafe -m "Hi to me, without E2E Encryption" + +# Request authenticated channel from another client. Note that the receiving client +# is expected to confirm the request before any specified timeout (default 120s) +$ client --password password --ndf ndf.json -l client.log -s session-directory --destfile user2-contact.json --waitTimeout 360 --unsafe-channel-creation --send-auth-request +WARNING: unsafe channel creation enabled +Adding authenticated channel for: Qm40C5hRUm7uhp5aATVWhSL6Mt+Z4JVBQrsEDvMORh4D +Message received: +Sending to Qm40C5hRUm7uhp5aATVWhSL6Mt+Z4JVBQrsEDvMORh4D: +Received 1 + +# Accept/Confirm an authenticated channel request implicitly +# (should be within the timeout window of requesting client, or the request will need to be re-sent): +$ client --password "password" --ndf ndf.json -l client.log -s session-directory --destfile user2-contact.json --unsafe-channel-creation --waitTimeout 200 +Authentication channel request from: o+QpswTmnsuZve/QRz0j0RYNWqjgx4R5pACfO00Pe0cD +Sending to o+QpswTmnsuZve/QRz0j0RYNWqjgx4R5pACfO00Pe0cD: +Message received: +Received 1 + +# Send E2E Messages +$ client --password user1-password --ndf ndf.json -l client1.log -s user1session --destfile user2-contact.json -m "Hi User 2, from User 1 with E2E Encryption" +Sending to Qm40C5hRUm7uhp5aATVWhSL6Mt+Z4JVBQrsEDvMORh4D: Hi User 2, from User 1 with E2E Encryption +Timed out! +Received 0 + +$ client --password user2-password --ndf ndf.json -l client1.log -s user2session --destfile user1-contact.json -m "Hi User 1, from User 2 with E2E Encryption" +Sending to o+QpswTmnsuZve/QRz0j0RYNWqjgx4R5pACfO00Pe0cD: Hi User 1, from User 2 with E2E Encryption +Timed out! +Received 0 +``` + +* `--password`: The password used to encrypt and load the session. +* `--ndf`: The network definition file. +* `-l`: The file to write logs (user messages are still printed to stdout). +* `-s`: The storage directory for client session data. +* `--writeContact`: Output the user's contact information to this file. +* `--destfile` is used to specify the recipient. You can also use + `--destid b64:...` using the user's base64 id, which is printed in the logs. +* `--unsafe`: Send message without encryption (necessary whenever you have not + already established an e2e channel). +* `--unsafe-channel-creation` Auto-create and auto-accept channel requests. +* `-m`: The message to send. + +Note that the client defaults to sending to itself when a destination is not supplied. +This is why we've used the `--unsafe` flag when creating the user contact jsons. +However, when sending between users, it is dropped in exchange for `--unsafe-channel-creation`. + +For the authenticated channel creation to be considered "safe", the user should be prompted. You can do this by explicitly accepting the channel creation +when sending a request with `--send-auth-request` (while excluding the `--unsafe-channel-creation` flag) or explicitly accepting a request with `--accept-channel`: + +``` +$ client --password user-password --ndf ndf.json -l client.log -s session-directory --destfile user-contact.json --accept-channel +Authentication channel request from: yYAztmoCoAH2VIr00zPxnj/ZRvdiDdURjdDWys0KYI4D +Sending to yYAztmoCoAH2VIr00zPxnj/ZRvdiDdURjdDWys0KYI4D: +Message received: +Received 1 ``` -* `--password` is the password used to encrypt and load the session. -* `--ndf` is the network definition file, downloadable from the xx network - website when available. -* `-l` the file to write logs (user messages are still printed to stdout) -* `--writeContact` Output the user's contact information to this file. -* `--unsafe` Send message without encryption (necessary whenever you have not - already established an e2e channel) -* `-m` The message to send +#### Receiving Messages -The client defaults to sending to itself when not supplied. +There is no explicit command for receiving messages. Instead, the client will attempt to fetch pending messages on each run. -Sending unsafe messages between 2 users: +You can use the `--receiveCount` flag to limit the number of messages the client waits for before a timeout occurs: ``` -# Get user contact jsons -client --password user1-password --ndf ndf.json -l client1.log -s user1session --writeContact user1-contact.json --unsafe -m "Hi" -client --password user2-password --ndf ndf.json -l client2.log -s user2session --writeContact user2-contact.json --unsafe -m "Hi" - -# Send messages to each other, run them in the background so they both receive -# each other's messages -client --password user1-password --ndf ndf.json -l client1.log -s user1session --destfile user2-contact.json --unsafe -m "Hi User 2, from User 1 without E2E Encryption" & -client --password user2-password --ndf ndf.json -l client2.log -s user2session --destfile user1-contact.json --unsafe -m "Hi User 1, from User 2 without E2E Encryption" & +$ client --password <password> --ndf <NDF JSON file> -l client.log -s <session directory> --destfile <contact JSON file> --receiveCount <integer count> ``` -* `--destfile` is used to specify the recipient. You can also use - `--destid b64:...` using the user's base64 id which is printed in the logs. +#### Sending Authenticated Channel Requests + +See [Sending Safe Messages Between Two (2) Users](#sending-safe-messages-between-two-2-users) + +#### Confirming Authenticated Channel Requests -To send with end to end encryption, you must first establish a connection -with the other user: +Setting up an authenticated channel between clients is a back-and-forth process that happens in sequence. One client sends a request and waits for the other to accept it. + +See the previous section, [Sending safe messages between 2 users](#sending-safe-messages-between-2-users), for example commands showing how to set up an end-to-end connection between clients before sending messages. + +As with received messages, there is no command for checking for authenticated channel requests; you'll be notified of any pending requests whenever the client is run. ``` -# Get user contact jsons -client --password user1-password --ndf ndf.json -l client1.log -s user1session --writeContact user1-contact.json --unsafe -m "Hi" -client --password user2-password --ndf ndf.json -l client2.log -s user2session --writeContact user2-contact.json --unsafe -m "Hi" +$ ./client.win64 --password password --ndf ndf.json -l client.log -s session-directory --destfile user-contact8.json --waitTimeout 120 -m "Hi User 7, from User 8 with E2E Encryption" +Authentication channel request from: 8zAWY69UUK/FkMBGY3ViR5MMfcp1GoKn6Y3c/64NYNYD +Sending to yYAztmoCoAH2VIr00zPxnj/ZRvdiDdURjdDWys0KYI4D: Hi User 7, from User 8 with E2E Encryption +Timed out! +Received 0 -# Send E2E Messages -client --password user1-password --ndf ndf.json -l client1.log -s user1session --destfile user1-contact.json --unsafe-channel-creation -m "Hi User 2, from User 1 with E2E Encryption" & -client --password user2-password --ndf ndf.json -l client2.log -s user2session --destfile user1-contact.json --unsafe-channel-creation -m "Hi User 1, from User 2 with E2E Encryption" & ``` -Note that we have dropped the `--unsafe` in exchange for: -* `--unsafe-channel-creation` Auto-create and auto-accept channel requests. +##### Troubleshooting -To be considered "safe" the user should be prompted. You can do this -with the command line by explicitly accepting the channel creation -when sending and/or explicitly accepting a request with -`--accept-channel`. +**`panic: Could not confirm authentication channel for ...`** + +Suppose the receiving client does not confirm the authentication channel before the requesting client reaches a timeout (default 120s). In that case, the request eventually terminates in a `panic: Could not confirm authentication channel for ...` error. + +Retrying the request should fix this. If necessary, you may increase the time the client waits to confirm the channel before timeout using the `--auth-timeout` flag (default 120s). + +This error will also occur with the receiving client if it received the request but failed to confirm it before the requesting client reached a timeout. In this case, the request needs to be resent while the other client reattempts to confirm the channel. + +**`panic: Received request not found`** + +You may also run into the `panic: Received request not found` error when attempting to confirm an authenticated channel request. This means your client has not received the request. If one has been sent, simply retrying should fix this. Full usage of client can be found with `client --help`: @@ -163,6 +230,9 @@ Flags: --accept-channel Accept the channel request for the corresponding recipient ID --auth-timeout uint The number of seconds to wait for an authentication channelto confirm (default 120) --delete-all-requests Delete the all contact requests, both sent and received. + --backupIn string Path to load backup client from + --backupOut string Path to output backup client. + --backupPass string Passphrase to encrypt/decrypt backup --delete-channel Delete the channel information for the corresponding recipient ID --delete-receive-requests Delete the all received contact requests. --delete-sent-requests Delete the all sent contact requests. @@ -201,7 +271,7 @@ Flags: Use "client [command] --help" for more information about a command. ``` -Note that the client cannot be used on the betanet with precanned user ids. +**Note:** The client cannot be used on the betanet with precanned user ids. ## Library Overview @@ -216,175 +286,13 @@ platforms. Clients need to perform the same actions *in the same order* as shown in `cmd/root.go`. Specifically, certain handlers need to be registered and -set up before starting network threads (i.e., before StartNetworkFollowers --- #2 below) and you cannot perform certain actions until the network -connection reaches the "healthy" state. Below are relevant code listings for -how to do these actions. - -the ndf is the network definition file, downloadable from the xx network -website when available. - -1. Creating and/or Loading a client: -``` - //create a new client if none exist - if _, err := os.Stat(storeDir); os.IsNotExist(err) { - // Load NDF - ndfPath := viper.GetString("ndf") - ndfJSON, err := ioutil.ReadFile(ndfPath) - if err != nil { - jww.FATAL.Panicf(err.Error()) - } - err = api.NewClient(string(ndfJSON), storeDir, - []byte(pass), regCode) - } - - if err != nil { - jww.FATAL.Panicf("%+v", err) - } - } - - //load the client - client, err := api.Login(storeDir, []byte(pass)) - if err != nil { - jww.FATAL.Panicf("%+v", err) - } -``` -2. Set up registration, authorization request handlers -``` - user := client.GetUser() - - // Set up reception handler - swboard := client.GetSwitchboard() - recvCh := make(chan message.Receive, 10000) // Needs to be large - // Note the name below is arbitrary - listenerID := swboard.RegisterChannel("DefaultCLIReceiver", - switchboard.AnyUser(), message.Text, recvCh) - jww.INFO.Printf("Message ListenerID: %v", listenerID) - - // Set up auth request handler, which simply prints the - // user id of the requestor. - authMgr := client.GetAuthRegistrar() - authMgr.AddGeneralRequestCallback(printChanRequest) -... -func printChanRequest(requestor contact.Contact, message string) { - msg := fmt.Sprintf("Authentication channel request from: %s\n", - requestor.ID) - jww.INFO.Printf(msg) - fmt.Printf(msg) - msg = fmt.Sprintf("Authentication channel request message: %s\n", message) - jww.INFO.Printf(msg) - fmt.Printf(msg) - // Or you can auto confirm with: - // err := client.ConfirmAuthenticatedChannel( - // requestor) - -} -``` - -3. Start network threads and wait until network is healthy: -``` - err = client.StartNetworkFollower() - if err != nil { - jww.FATAL.Panicf("%+v", err) - } - - // Wait until connected or crash on timeout - connected := make(chan bool, 10) - client.GetHealth().AddChannel(connected) - waitUntilConnected(connected) -... -func waitUntilConnected(connected chan bool) { - waitTimeout := time.Duration(viper.GetUint("waitTimeout")) - timeoutTimer := time.NewTimer(waitTimeout * time.Second) - isConnected := false - //Wait until we connect or panic if we can't by a timeout - for !isConnected { - select { - case isConnected = <-connected: - jww.INFO.Printf("Network Status: %v\n", - isConnected) - break - case <-timeoutTimer.C: - jww.FATAL.Panic("timeout on connection") - } - } -} -``` - -4. Adding authenticated channels (if we haven't done it yet) -``` - if client.HasAuthenticatedChannel(recipientID) { - jww.INFO.Printf("Authenticated channel already in place for %s", - recipientID) - return - } - // Check if a channel exists for this recipientID - recipientContact, err := client.GetAuthenticatedChannelRequest( - recipientID) - if err == nil { - jww.INFO.Printf("Accepting existing channel request for %s", - recipientID) - err := client.ConfirmAuthenticatedChannel(recipientContact) - if err != nil { - jww.FATAL.Panicf("%+v", err) - } - return - } else { - recipientContact = recipient - } - - me := client.GetUser().GetContact() - jww.INFO.Printf("Requesting auth channel from: %s", - recipientID) - err := client.RequestAuthenticatedChannel(recipientContact, - me, msg) - if err != nil { - jww.FATAL.Panicf("%+v", err) - } -``` +set up before starting network threads. Additionally, you cannot perform certain actions until the network connection reaches a "healthy" state. -5. Sending E2E and Unsafe Messages -``` - msg := message.Send{ - Recipient: recipientID, - Payload: []byte(msgBody), - MessageType: message.Text, - } - paramsE2E := params.GetDefaultE2E() - paramsUnsafe := params.GetDefaultUnsafe() - - fmt.Printf("Sending to %s: %s\n", recipientID, msgBody) - var roundIDs []id.Round - if unsafe { - roundIDs, err = client.SendUnsafe(msg, - paramsUnsafe) - } else { - roundIDs, _, err = client.SendE2E(msg, - paramsE2E) - } - if err != nil { - jww.FATAL.Panicf("%+v", err) - } - jww.INFO.Printf("RoundIDs: %+v\n", roundIDs) -``` -The "RoundIDs" are the rounds in which your message parts were sent. After those -rounds have completed on the network, you can assume that the message has "sent" -successfully. See the client interface section for info on how to access round -state changes. +See [main.go](https://git.xx.network/elixxir/xxdk-examples/-/blob/sample-messaging-app/sample-messaging-app/main.go) for relevant code listings on when and how to perform these actions. +The [Getting Started](https://xxdk-dev.xx.network/getting-started) guide provides further detail. -6. Receiving Messages (assuming you set the receiver above in step 2) -``` - timeoutTimer := time.NewTimer(waitTimeout * time.Second) - select { - case <-timeoutTimer.C: - fmt.Println("Timed out!") - break - case m := <-recvCh: - fmt.Printf("Message received: %s\n", string( - m.Payload)) - break - } -``` +You can also visit the [API Quick Reference](https://xxdk-dev.xx.network/quick-reference) +for information on the types and functions exposed by the Client API. The main entry point for developing with the client is `api/client` (or `bindings/client`). We recommend using go doc to explore: @@ -489,4 +397,4 @@ parts of the roadmap that are intended for the client: * Efficiency improvements - mechanisms for message pickup and network tracking * will evolve to allow tradeoffs and options for use -We also are always looking at how to simplify and improve the library interface. +We are also always looking at simplifying and improving the library interface. diff --git a/api/authenticatedChannel.go b/api/authenticatedChannel.go index 93446db2b9bee15a5755f49d62ce42bcedf15794..47228cff42516de89903c7090d6fd33eb8e74054 100644 --- a/api/authenticatedChannel.go +++ b/api/authenticatedChannel.go @@ -9,6 +9,8 @@ package api import ( "encoding/binary" + "math/rand" + "github.com/cloudflare/circl/dh/sidh" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" @@ -20,7 +22,6 @@ import ( "gitlab.com/elixxir/crypto/contact" "gitlab.com/elixxir/primitives/fact" "gitlab.com/xx_network/primitives/id" - "math/rand" ) // RequestAuthenticatedChannel sends a request to another party to establish an @@ -44,6 +45,20 @@ func (c *Client) RequestAuthenticatedChannel(recipient, me contact.Contact, c.storage, c.network) } +// ResetSession resets an authenticate channel that already exists +func (c *Client) ResetSession(recipient, me contact.Contact, + message string) (id.Round, error) { + jww.INFO.Printf("ResetSession(%s)", recipient.ID) + + if !c.network.GetHealthTracker().IsHealthy() { + return 0, errors.New("Cannot request authenticated channel " + + "creation when the network is not healthy") + } + + return auth.ResetSession(recipient, me, c.rng.GetStream(), + c.storage, c.network) +} + // GetAuthRegistrar gets the object which allows the registration of auth // callbacks func (c *Client) GetAuthRegistrar() interfaces.Auth { @@ -76,8 +91,7 @@ func (c *Client) ConfirmAuthenticatedChannel(recipient contact.Contact) (id.Roun "creation when the network is not healthy") } - return auth.ConfirmRequestAuth(recipient, c.rng.GetStream(), - c.storage, c.network) + return c.auth.ConfirmRequestAuth(recipient) } // VerifyOwnership checks if the ownership proof on a passed contact matches the @@ -144,7 +158,7 @@ func (c *Client) MakePrecannedAuthenticatedChannel(precannedID uint) (contact.Co Source: precan.ID[:], }, me) - //slient (rekey) + // slient (rekey) c.storage.GetEdge().Add(edge.Preimage{ Data: sessionPartner.GetSilentPreimage(), Type: preimage.Silent, @@ -165,7 +179,6 @@ func (c *Client) MakePrecannedAuthenticatedChannel(precannedID uint) (contact.Co Source: precan.ID[:], }, me) - return precan, err } diff --git a/api/client.go b/api/client.go index a289fea1e030118346df4011ef5601e49438fe6d..41c211d0c299d814465360d965b0393fb67c6a2c 100644 --- a/api/client.go +++ b/api/client.go @@ -24,6 +24,7 @@ import ( "gitlab.com/elixxir/client/storage/edge" "gitlab.com/elixxir/client/switchboard" "gitlab.com/elixxir/comms/client" + "gitlab.com/elixxir/crypto/backup" "gitlab.com/elixxir/crypto/cyclic" "gitlab.com/elixxir/crypto/fastRNG" "gitlab.com/elixxir/primitives/version" @@ -69,6 +70,9 @@ type Client struct { // Event reporting in event.go events *eventManager + + // Handles the triggering and delivery of backups + backup *interfaces.BackupContainer } // NewClient creates client storage, generates keys, connects, and registers @@ -92,8 +96,7 @@ func NewClient(ndfJSON, storageDir string, password []byte, protoUser := createNewUser(rngStreamGen, cmixGrp, e2eGrp) jww.DEBUG.Printf("User generation took: %s", time.Now().Sub(start)) - _, err = checkVersionAndSetupStorage(def, storageDir, password, protoUser, - cmixGrp, e2eGrp, rngStreamGen, false, registrationCode) + _, err = checkVersionAndSetupStorage(def, storageDir, password, protoUser, cmixGrp, e2eGrp, rngStreamGen, false, registrationCode) if err != nil { return err } @@ -123,8 +126,7 @@ func NewPrecannedClient(precannedID uint, defJSON, storageDir string, protoUser := createPrecannedUser(precannedID, rngStream, cmixGrp, e2eGrp) - _, err = checkVersionAndSetupStorage(def, storageDir, password, protoUser, - cmixGrp, e2eGrp, rngStreamGen, true, "") + _, err = checkVersionAndSetupStorage(def, storageDir, password, protoUser, cmixGrp, e2eGrp, rngStreamGen, true, "") if err != nil { return err } @@ -153,8 +155,7 @@ func NewVanityClient(ndfJSON, storageDir string, password []byte, protoUser := createNewVanityUser(rngStream, cmixGrp, e2eGrp, userIdPrefix) - _, err = checkVersionAndSetupStorage(def, storageDir, password, protoUser, - cmixGrp, e2eGrp, rngStreamGen, false, registrationCode) + _, err = checkVersionAndSetupStorage(def, storageDir, password, protoUser, cmixGrp, e2eGrp, rngStreamGen, false, registrationCode) if err != nil { return err } @@ -163,6 +164,55 @@ func NewVanityClient(ndfJSON, storageDir string, password []byte, return nil } +// NewClientFromBackup constructs a new Client from an encrypted backup. +// The backup is decrypted using the backupPassphrase. On success a +// successful client creation, the function will return a JSON encoded +// list of the E2E partners contained in the backup and a json-encoded +//string containing parameters stored in the backup. +func NewClientFromBackup(ndfJSON, storageDir string, sessionPassword, + backupPassphrase []byte, backupFileContents []byte) ([]*id.ID, string, error) { + + backUp := &backup.Backup{} + err := backUp.Decrypt(string(backupPassphrase), backupFileContents) + if err != nil { + return nil, "", errors.WithMessage(err, "Failed to "+ + "unmarshal decrypted client contents.") + } + + usr := user.NewUserFromBackup(backUp) + + // Parse the NDF + def, err := parseNDF(ndfJSON) + if err != nil { + return nil, "", err + } + + cmixGrp, e2eGrp := decodeGroups(def) + + // Use fastRNG for RNG ops (AES fortuna based RNG using system RNG) + rngStreamGen := fastRNG.NewStreamGenerator(12, 3, csprng.NewSystemRNG) + + // Create storage object. + // Note we do not need registration + storageSess, err := checkVersionAndSetupStorage(def, storageDir, []byte(sessionPassword), usr, cmixGrp, e2eGrp, rngStreamGen, false, backUp.RegistrationCode) + + // Set registration values in storage + storageSess.User().SetReceptionRegistrationValidationSignature(backUp. + ReceptionIdentity.RegistrarSignature) + storageSess.User().SetTransmissionRegistrationValidationSignature(backUp. + TransmissionIdentity.RegistrarSignature) + storageSess.User().SetRegistrationTimestamp(backUp.RegistrationTimestamp) + + //move the registration state to indicate registered with registration + //on proto client + err = storageSess.ForwardRegistrationStatus(storage.PermissioningComplete) + if err != nil { + return nil, "", err + } + + return backUp.Contacts.Identities, backUp.JSONParams, nil +} + // OpenClient session, but don't connect to the network or log in func OpenClient(storageDir string, password []byte, parameters params.Network) (*Client, error) { jww.INFO.Printf("OpenClient()") @@ -194,6 +244,7 @@ func OpenClient(storageDir string, password []byte, parameters params.Network) ( parameters: parameters, clientErrorChannel: make(chan interfaces.ClientError, 1000), events: newEventManager(), + backup: &interfaces.BackupContainer{}, } return c, nil @@ -228,8 +279,7 @@ func NewProtoClient_Unsafe(ndfJSON, storageDir string, password, usr := user.NewUserFromProto(protoUser) // Set up storage - storageSess, err := checkVersionAndSetupStorage(def, storageDir, password, usr, - cmixGrp, e2eGrp, rngStreamGen, false, protoUser.RegCode) + storageSess, err := checkVersionAndSetupStorage(def, storageDir, password, usr, cmixGrp, e2eGrp, rngStreamGen, false, protoUser.RegCode) if err != nil { return err } @@ -304,7 +354,8 @@ func Login(storageDir string, password []byte, parameters params.Network) (*Clie } // initialize the auth tracker - c.auth = auth.NewManager(c.switchboard, c.storage, c.network, parameters.ReplayRequests) + c.auth = auth.NewManager(c.switchboard, c.storage, c.network, c.rng, + c.backup.TriggerBackup, parameters.ReplayRequests) // Add all processes to the followerServices err = c.registerFollower() @@ -363,7 +414,8 @@ func LoginWithNewBaseNDF_UNSAFE(storageDir string, password []byte, } // initialize the auth tracker - c.auth = auth.NewManager(c.switchboard, c.storage, c.network, parameters.ReplayRequests) + c.auth = auth.NewManager(c.switchboard, c.storage, c.network, c.rng, + c.backup.TriggerBackup, parameters.ReplayRequests) err = c.registerFollower() if err != nil { @@ -420,7 +472,8 @@ func LoginWithProtoClient(storageDir string, password []byte, protoClientJSON [] } // initialize the auth tracker - c.auth = auth.NewManager(c.switchboard, c.storage, c.network, parameters.ReplayRequests) + c.auth = auth.NewManager(c.switchboard, c.storage, c.network, c.rng, + c.backup.TriggerBackup, parameters.ReplayRequests) err = c.registerFollower() if err != nil { @@ -641,6 +694,12 @@ func (c *Client) GetNetworkInterface() interfaces.NetworkManager { return c.network } +// GetBackup returns a pointer to the backup container so that the backup can be +// set and triggered. +func (c *Client) GetBackup() *interfaces.BackupContainer { + return c.backup +} + // GetRateLimitParams retrieves the rate limiting parameters. func (c *Client) GetRateLimitParams() (uint32, uint32, int64) { rateLimitParams := c.storage.GetBucketParams().Get() @@ -684,6 +743,14 @@ func (c *Client) GetNodeRegistrationStatus() (int, int, error) { return numRegistered, len(nodes) - numStale, nil } +// DeleteRequest will delete a request, agnostic of request type +// for the given partner ID. If no request exists for this +// partner ID an error will be returned. +func (c *Client) DeleteRequest(partnerId *id.ID) error { + jww.DEBUG.Printf("Deleting request for partner ID: %s", partnerId) + return c.GetStorage().Auth().DeleteRequest(partnerId) +} + // DeleteAllRequests clears all requests from client's auth storage. func (c *Client) DeleteAllRequests() error { jww.DEBUG.Printf("Deleting all requests") @@ -705,7 +772,7 @@ func (c *Client) DeleteReceiveRequests() error { // DeleteContact is a function which removes a partner from Client's storage func (c *Client) DeleteContact(partnerId *id.ID) error { jww.DEBUG.Printf("Deleting contact with ID %s", partnerId) - //get the partner so they can be removed from preiamge store + // get the partner so that they can be removed from preimage store partner, err := c.storage.E2e().GetPartner(partnerId) if err != nil { return errors.WithMessagef(err, "Could not delete %s because "+ @@ -720,6 +787,10 @@ func (c *Client) DeleteContact(partnerId *id.ID) error { if err = c.storage.E2e().DeletePartner(partnerId); err != nil { return err } + + // Trigger backup + c.backup.TriggerBackup("contact deleted") + //delete the preimages if err = c.storage.GetEdge().Remove(edge.Preimage{ Data: e2ePreimage, @@ -868,8 +939,7 @@ func decodeGroups(ndf *ndf.NetworkDefinition) (cmixGrp, e2eGrp *cyclic.Group) { // checkVersionAndSetupStorage is common code shared by NewClient, NewPrecannedClient and NewVanityClient // it checks client version and creates a new storage for user data func checkVersionAndSetupStorage(def *ndf.NetworkDefinition, - storageDir string, password []byte, - protoUser user.User, + storageDir string, password []byte, protoUser user.User, cmixGrp, e2eGrp *cyclic.Group, rngStreamGen *fastRNG.StreamGenerator, isPrecanned bool, registrationCode string) (*storage.Session, error) { // Get current client version @@ -906,6 +976,12 @@ func checkVersionAndSetupStorage(def *ndf.NetworkDefinition, Source: protoUser.ReceptionID[:], }, protoUser.ReceptionID) + storageSess.GetEdge().Add(edge.Preimage{ + Data: preimage.GenerateReset(protoUser.ReceptionID), + Type: preimage.Reset, + Source: protoUser.ReceptionID[:], + }, protoUser.ReceptionID) + if err != nil { return nil, errors.WithMessage(err, "Failed to denote state "+ "change in session") diff --git a/api/permissioning.go b/api/permissioning.go index 85aac34b93f333f4994c49b1dac1ec868965807a..2dd62ec87dd8a95f43bce9f93df31db105edc4ca 100644 --- a/api/permissioning.go +++ b/api/permissioning.go @@ -73,8 +73,6 @@ func (c *Client) ConstructProtoUerFile() ([]byte, error) { RegCode: regCode, TransmissionRegValidationSig: c.storage.User().GetTransmissionRegistrationValidationSignature(), ReceptionRegValidationSig: c.storage.User().GetReceptionRegistrationValidationSignature(), - CmixDhPrivateKey: c.GetStorage().Cmix().GetDHPrivateKey(), - CmixDhPublicKey: c.GetStorage().Cmix().GetDHPublicKey(), E2eDhPrivateKey: c.GetStorage().E2e().GetDHPrivateKey(), E2eDhPublicKey: c.GetStorage().E2e().GetDHPublicKey(), } diff --git a/api/results.go b/api/results.go index 04e91e15e7c6e3407ecfab759dd9eb1f598d9885..eb0403dee92b93be7ad90cc3ab2b4cafff28e8f8 100644 --- a/api/results.go +++ b/api/results.go @@ -4,6 +4,7 @@ // Use of this source code is governed by a license that can be found in the // // LICENSE file // /////////////////////////////////////////////////////////////////////////////// + package api import ( @@ -18,7 +19,7 @@ import ( "gitlab.com/xx_network/primitives/id" ) -// Enum of possible round results to pass back +// RoundResult is the enum of possible round results to pass back type RoundResult uint const ( @@ -40,7 +41,7 @@ func (rr RoundResult) String() string { } } -// Callback interface which reports the requested rounds. +// RoundEventCallback interface which reports the requested rounds. // Designed such that the caller may decide how much detail they need. // allRoundsSucceeded: // Returns false if any rounds in the round map were unsuccessful. @@ -60,7 +61,7 @@ type historicalRoundsComm interface { GetHost(hostId *id.ID) (*connect.Host, bool) } -// Adjudicates on the rounds requested. Checks if they are +// GetRoundResults adjudicates on the rounds requested. Checks if they are // older rounds or in progress rounds. func (c *Client) GetRoundResults(roundList []id.Round, timeout time.Duration, roundCallback RoundEventCallback) error { @@ -168,7 +169,7 @@ func (c *Client) getRoundResults(roundList []id.Round, timeout time.Duration, roundsResults[roundId] = Failed allRoundsSucceeded = false } - return + continue } allRoundsSucceeded = false anyRoundTimedOut = true diff --git a/api/user.go b/api/user.go index 1b22a0877235636c55bbd2be922cddfcf4fe2b39..4377f1b0333cb41ca3766cafa4c86185240836d7 100644 --- a/api/user.go +++ b/api/user.go @@ -34,10 +34,10 @@ func createNewUser(rng *fastRNG.StreamGenerator, cmix, e2e *cyclic.Group) user.U // CMIX Keygen var transmissionRsaKey, receptionRsaKey *rsa.PrivateKey - var cMixKeyBytes, e2eKeyBytes, transmissionSalt, receptionSalt []byte + var e2eKeyBytes, transmissionSalt, receptionSalt []byte - cMixKeyBytes, e2eKeyBytes, transmissionSalt, receptionSalt, - transmissionRsaKey, receptionRsaKey = createDhKeys(rng, cmix, e2e) + e2eKeyBytes, transmissionSalt, receptionSalt, + transmissionRsaKey, receptionRsaKey = createDhKeys(rng, e2e) // Salt, UID, etc gen stream := rng.GetStream() @@ -83,32 +83,17 @@ func createNewUser(rng *fastRNG.StreamGenerator, cmix, e2e *cyclic.Group) user.U ReceptionSalt: receptionSalt, ReceptionRSA: receptionRsaKey, Precanned: false, - CmixDhPrivateKey: cmix.NewIntFromBytes(cMixKeyBytes), E2eDhPrivateKey: e2e.NewIntFromBytes(e2eKeyBytes), } } func createDhKeys(rng *fastRNG.StreamGenerator, - cmix, e2e *cyclic.Group) (cMixKeyBytes, e2eKeyBytes, + e2e *cyclic.Group) (e2eKeyBytes, transmissionSalt, receptionSalt []byte, transmissionRsaKey, receptionRsaKey *rsa.PrivateKey) { wg := sync.WaitGroup{} - wg.Add(4) - - go func() { - defer wg.Done() - var err error - // FIXME: Why 256 bits? -- this is spec but not explained, it has - // to do with optimizing operations on one side and still preserves - // decent security -- cite this. - stream := rng.GetStream() - cMixKeyBytes, err = csprng.GenerateInGroup(cmix.GetPBytes(), 256, stream) - stream.Close() - if err != nil { - jww.FATAL.Panicf(err.Error()) - } - }() + wg.Add(3) go func() { defer wg.Done() @@ -186,8 +171,6 @@ func createPrecannedUser(precannedID uint, rng csprng.Source, cmix, e2e *cyclic. ReceptionSalt: salt, Precanned: true, E2eDhPrivateKey: e2e.NewIntFromBytes(e2eKeyBytes), - // NOTE: These are dummy/not used - CmixDhPrivateKey: cmix.NewInt(1), TransmissionRSA: rsaKey, ReceptionRSA: rsaKey, } @@ -196,15 +179,6 @@ func createPrecannedUser(precannedID uint, rng csprng.Source, cmix, e2e *cyclic. // createNewVanityUser generates an identity for cMix // The identity's ReceptionID is not random but starts with the supplied prefix func createNewVanityUser(rng csprng.Source, cmix, e2e *cyclic.Group, prefix string) user.User { - // CMIX Keygen - // FIXME: Why 256 bits? -- this is spec but not explained, it has - // to do with optimizing operations on one side and still preserves - // decent security -- cite this. - cMixKeyBytes, err := csprng.GenerateInGroup(cmix.GetPBytes(), 256, rng) - if err != nil { - jww.FATAL.Panicf(err.Error()) - } - // DH Keygen // FIXME: Why 256 bits? -- this is spec but not explained, it has // to do with optimizing operations on one side and still preserves @@ -311,7 +285,6 @@ func createNewVanityUser(rng csprng.Source, cmix, e2e *cyclic.Group, prefix stri ReceptionSalt: receptionSalt, ReceptionRSA: receptionRsaKey, Precanned: false, - CmixDhPrivateKey: cmix.NewIntFromBytes(cMixKeyBytes), E2eDhPrivateKey: e2e.NewIntFromBytes(e2eKeyBytes), } } diff --git a/api/utils.go b/api/utils.go index 2486d4d66143bb7fe6b660a9d23da26e70828652..799e3abf5a2410d15a1299c6d682ef801fdd439c 100644 --- a/api/utils.go +++ b/api/utils.go @@ -20,9 +20,9 @@ const ( // Maximum input image size (in bytes) maxSize int64 = 12000000 // Desired number of pixels in output image - desiredSize = 640*480 + desiredSize = 640 * 480 // Desired number of pixels in output image for preview - desiredPreviewSize = 32*24 + desiredPreviewSize = 32 * 24 ) // CompressJpeg takes a JPEG image in byte format @@ -76,7 +76,6 @@ func CompressJpeg(imgBytes []byte) ([]byte, error) { return newImgBuf.Bytes(), nil } - // CompressJpeg takes a JPEG image in byte format // and compresses it based on desired output size func CompressJpegForPreview(imgBytes []byte) ([]byte, error) { diff --git a/api/version_vars.go b/api/version_vars.go index 9ef72f196cc77927fd45ecad2a70d4a28d5ce7ae..5524c3b726ba6013ddb5c22e23282134f7f37bd1 100644 --- a/api/version_vars.go +++ b/api/version_vars.go @@ -1,16 +1,16 @@ // Code generated by go generate; DO NOT EDIT. // This file was generated by robots at -// 2022-02-15 12:19:19.667352 -0600 CST m=+0.033427370 +// 2022-06-06 11:33:31.114383 -0500 CDT m=+0.028284450 package api -const GITVERSION = `d8832766 made splitSends default to false` -const SEMVER = "4.0.0" +const GITVERSION = `6850ced3 log cleanup` +const SEMVER = "4.1.0" const DEPENDENCIES = `module gitlab.com/elixxir/client go 1.17 require ( - github.com/cloudflare/circl v1.0.1-0.20211008185751-59b49bc148ce + github.com/cloudflare/circl v1.1.0 github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 github.com/golang/protobuf v1.5.2 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 @@ -19,13 +19,13 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 github.com/spf13/viper v1.7.1 gitlab.com/elixxir/bloomfilter v0.0.0-20200930191214-10e9ac31b228 - gitlab.com/elixxir/comms v0.0.4-0.20220214214811-4a1bd320aa45 - gitlab.com/elixxir/crypto v0.0.7-0.20220211185439-4a6d9f41f8ab + gitlab.com/elixxir/comms v0.0.4-0.20220603231314-e47e4af13326 + gitlab.com/elixxir/crypto v0.0.7-0.20220414225314-6f3eb9c073a5 gitlab.com/elixxir/ekv v0.1.6 - gitlab.com/elixxir/primitives v0.0.3-0.20220211185255-f9bc3df21e1d - gitlab.com/xx_network/comms v0.0.4-0.20220211184526-00dc9cfe8e2e - gitlab.com/xx_network/crypto v0.0.5-0.20220211184244-5803ecaafd59 - gitlab.com/xx_network/primitives v0.0.4-0.20220211183913-d6f5fd114a2a + gitlab.com/elixxir/primitives v0.0.3-0.20220323183834-b98f255361b8 + gitlab.com/xx_network/comms v0.0.4-0.20220315161313-76acb14429ac + gitlab.com/xx_network/crypto v0.0.5-0.20220317171841-084640957d71 + gitlab.com/xx_network/primitives v0.0.4-0.20220324193139-b292d1ae6e7e golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 google.golang.org/grpc v1.42.0 @@ -53,8 +53,8 @@ require ( github.com/ttacon/libphonenumber v1.2.1 // indirect github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/zeebo/blake3 v0.1.1 // indirect - gitlab.com/xx_network/ring v0.0.3-0.20210527191221-ce3f170aabd5 // indirect - golang.org/x/sys v0.0.0-20210902050250-f475640dd07b // indirect + gitlab.com/xx_network/ring v0.0.3-0.20220222211904-da613960ad93 // indirect + golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect golang.org/x/text v0.3.6 // indirect google.golang.org/genproto v0.0.0-20210105202744-fe13368bc0e1 // indirect gopkg.in/ini.v1 v1.62.0 // indirect diff --git a/auth/callback.go b/auth/callback.go index 7ae57d144bfb2a3c6e0b7b8cd02cb610d4694553..6da4fd2fbd3ad612a7942cb2f7aac11337d6ee74 100644 --- a/auth/callback.go +++ b/auth/callback.go @@ -24,10 +24,9 @@ import ( "gitlab.com/elixxir/crypto/cyclic" "gitlab.com/elixxir/crypto/diffieHellman" cAuth "gitlab.com/elixxir/crypto/e2e/auth" - "gitlab.com/elixxir/crypto/fastRNG" "gitlab.com/elixxir/primitives/fact" "gitlab.com/elixxir/primitives/format" - "gitlab.com/xx_network/crypto/csprng" + "gitlab.com/xx_network/primitives/id" ) func (m *Manager) StartProcesses() (stoppable.Stoppable, error) { @@ -108,10 +107,19 @@ func (m *Manager) handleRequest(cmixMsg format.Message, jww.TRACE.Printf("handleRequest ECRPAYLOAD: %v", baseFmt.GetEcrPayload()) jww.TRACE.Printf("handleRequest MAC: %v", cmixMsg.GetMac()) + ecrPayload := baseFmt.GetEcrPayload() success, payload := cAuth.Decrypt(myHistoricalPrivKey, - partnerPubKey, baseFmt.GetEcrPayload(), + partnerPubKey, ecrPayload, cmixMsg.GetMac(), grp) + if !success { + jww.WARN.Printf("Attempting to decrypt old request packet...") + ecrPayload = append(ecrPayload, baseFmt.GetVersion()) + success, payload = cAuth.Decrypt(myHistoricalPrivKey, + partnerPubKey, ecrPayload, + cmixMsg.GetMac(), grp) + } + if !success { jww.WARN.Printf("Received auth request failed " + "its mac check") @@ -153,6 +161,68 @@ func (m *Manager) handleRequest(cmixMsg format.Message, events.Report(1, "Auth", "RequestReceived", em) /*do state edge checks*/ + // Check if this is a reset, which are valid as of version 1 + // Resets happen when our fingerprint is new AND we are + // the latest fingerprint to be added to the list and we already have + // a negotiation or authenticated channel in progress + fp := cAuth.CreateNegotiationFingerprint(partnerPubKey, + partnerSIDHPubKey) + newFP, latest := m.storage.Auth().AddIfNew(partnerID, fp) + resetSession := false + autoConfirm := false + if baseFmt.GetVersion() >= 1 && newFP && latest { + // If we had an existing session and it's new, then yes, we + // want to reset + if _, err := m.storage.E2e().GetPartner(partnerID); err == nil { + jww.INFO.Printf("Resetting session for %s", partnerID) + resetSession = true + // Most likely, we got 2 reset sessions at once, so this + // is a non-fatal error but we will record a warning + // just in case. + err = m.storage.E2e().DeletePartner(partnerID) + if err != nil { + jww.WARN.Printf("Unable to delete channel: %+v", + err) + } + // Also delete any existing request, sent or received + m.storage.Auth().Delete(partnerID) + } + // If we had an existing negotiation open, then it depends + + // If we've only received, then user has not confirmed, treat as + // a non-duplicate request, so delete the old one (to cause new + // callback to be called) + rType, _, _, err := m.storage.Auth().GetRequest(partnerID) + if err != nil && rType == auth.Receive { + m.storage.Auth().Delete(partnerID) + } + + // If we've already Sent and are now receiving, + // then we attempt auto-confirm as below + // This poses a potential problem if it is truly a session + // reset by the other user, because we may not actually + // autoconfirm based on our public key compared to theirs. + // This could result in a permanently broken association, as + // the other side has attempted to reset it's session and + // can no longer detect a sent request collision, so this side + // cannot ever successfully resend. + // We prevent this by stopping session resets if they + // are called when the other side is in the "Sent" state. + // If the other side is in the "received" state we also block, + // but we could autoconfirm. + // Note that you can still get into this state by one side + // deleting requests. In that case, both sides need to clear + // out all requests and retry negotiation from scratch. + // NOTE: This protocol part could use an overhaul/second look, + // there's got to be a way to do this with far less state + // but this is the spec so we're sticking with it for now. + + // If not an existing request, we do nothing. + } else { + jww.WARN.Printf("Version: %d, newFP: %v, latest: %v", baseFmt.GetVersion(), + newFP, latest) + } + // check if a relationship already exists. // if it does and the keys used are the same as we have, send a // confirmation in case there are state issues. @@ -232,57 +302,13 @@ func (m *Manager) handleRequest(cmixMsg format.Message, // If I do, delete my request on disk m.storage.Auth().Delete(partnerID) - //process the inner payload - facts, _, err := fact.UnstringifyFactList( - string(requestFmt.msgPayload)) - if err != nil { - em := fmt.Sprintf("failed to parse facts and message "+ - "from Auth Request: %s", err) - jww.WARN.Print(em) - events.Report(10, "Auth", "RequestError", em) - return - } - + // Do the normal, fall out of this if block and // create the contact, note that we use the data // sent in the request and not any data we had // already - partnerContact := contact.Contact{ - ID: partnerID, - DhPubKey: partnerPubKey, - OwnershipProof: copySlice(ownership), - Facts: facts, - } - // add a confirmation to disk - if err = m.storage.Auth().AddReceived(partnerContact, - partnerSIDHPubKey); err != nil { - em := fmt.Sprintf("failed to store contact Auth "+ - "Request: %s", err) - jww.WARN.Print(em) - events.Report(10, "Auth", "RequestError", em) - } - - // Call ConfirmRequestAuth to send confirmation - rngGen := fastRNG.NewStreamGenerator(1, 1, - csprng.NewSystemRNG) - rng := rngGen.GetStream() - rndNum, err := ConfirmRequestAuth(partnerContact, - rng, m.storage, m.net) - if err != nil { - jww.ERROR.Printf("Could not ConfirmRequestAuth: %+v", - err) - return - } + autoConfirm = true - jww.INFO.Printf("ConfirmRequestAuth to %s on round %d", - partnerID, rndNum) - c := partnerContact - cbList := m.confirmCallbacks.Get(c.ID) - for _, cb := range cbList { - ccb := cb.(interfaces.ConfirmCallback) - go ccb(c) - } - return } } } @@ -300,8 +326,8 @@ func (m *Manager) handleRequest(cmixMsg format.Message, //create the contact, note that no facts are sent in the payload c := contact.Contact{ - ID: partnerID, - DhPubKey: partnerPubKey, + ID: partnerID.DeepCopy(), + DhPubKey: partnerPubKey.DeepCopy(), OwnershipProof: copySlice(ecrFmt.ownership), Facts: facts, } @@ -317,12 +343,44 @@ func (m *Manager) handleRequest(cmixMsg format.Message, return } - // fixme: if a crash occurs before or during the calls, the notification - // will never be sent. - cbList := m.requestCallbacks.Get(c.ID) - for _, cb := range cbList { - rcb := cb.(interfaces.RequestCallback) - go rcb(c) + // We autoconfirm anytime we had already sent a request OR we are + // resetting an existing session + var rndNum id.Round + if autoConfirm || resetSession { + // Call ConfirmRequestAuth to send confirmation + rndNum, err = m.confirmRequestAuth(c, true) + if err != nil { + jww.ERROR.Printf("Could not ConfirmRequestAuth: %+v", + err) + return + } + + if autoConfirm { + jww.INFO.Printf("ConfirmRequestAuth to %s on round %d", + partnerID, rndNum) + cbList := m.confirmCallbacks.Get(c.ID) + for _, cb := range cbList { + ccb := cb.(interfaces.ConfirmCallback) + go ccb(c) + } + } + if resetSession { + jww.INFO.Printf("Reset Auth %s on round %d", + partnerID, rndNum) + cbList := m.resetCallbacks.Get(c.ID) + for _, cb := range cbList { + ccb := cb.(interfaces.ResetNotificationCallback) + go ccb(c) + } + } + } else { + // fixme: if a crash occurs before or during the calls, the notification + // will never be sent. + cbList := m.requestCallbacks.Get(c.ID) + for _, cb := range cbList { + rcb := cb.(interfaces.RequestCallback) + go rcb(c) + } } return } @@ -427,6 +485,8 @@ func (m *Manager) doConfirm(sr *auth.SentRequest, grp *cyclic.Group, sr.GetPartner(), err) } + m.backupTrigger("received confirmation from request") + //remove the confirm fingerprint fp := sr.GetFingerprint() if err := m.storage.GetEdge().Remove(edge.Preimage{ @@ -438,40 +498,7 @@ func (m *Manager) doConfirm(sr *auth.SentRequest, grp *cyclic.Group, sr.GetPartner(), err) } - //add the e2e and rekey firngeprints - //e2e - sessionPartner, err := m.storage.E2e().GetPartner(sr.GetPartner()) - if err != nil { - jww.FATAL.Panicf("Cannot find %s right after creating: %+v", sr.GetPartner(), err) - } - me := m.storage.GetUser().ReceptionID - - m.storage.GetEdge().Add(edge.Preimage{ - Data: sessionPartner.GetE2EPreimage(), - Type: preimage.E2e, - Source: sr.GetPartner()[:], - }, me) - - //silent (rekey) - m.storage.GetEdge().Add(edge.Preimage{ - Data: sessionPartner.GetSilentPreimage(), - Type: preimage.Silent, - Source: sr.GetPartner()[:], - }, me) - - // File transfer end - m.storage.GetEdge().Add(edge.Preimage{ - Data: sessionPartner.GetFileTransferPreimage(), - Type: preimage.EndFT, - Source: sr.GetPartner()[:], - }, me) - - //group Request - m.storage.GetEdge().Add(edge.Preimage{ - Data: sessionPartner.GetGroupRequestPreimage(), - Type: preimage.GroupRq, - Source: sr.GetPartner()[:], - }, me) + addPreimages(sr.GetPartner(), m.storage) // delete the in progress negotiation // this undoes the request lock @@ -513,7 +540,7 @@ func handleBaseFormat(cmixMsg format.Message, grp *cyclic.Group) (baseFormat, baseFmt, err := unmarshalBaseFormat(cmixMsg.GetContents(), grp.GetP().ByteLen()) - if err != nil { + if err != nil && baseFmt == nil { return baseFormat{}, nil, errors.WithMessage(err, "Failed to"+ " unmarshal auth") } @@ -524,5 +551,5 @@ func handleBaseFormat(cmixMsg format.Message, grp *cyclic.Group) (baseFormat, } partnerPubKey := grp.NewIntFromBytes(baseFmt.pubkey) - return baseFmt, partnerPubKey, nil + return *baseFmt, partnerPubKey, nil } diff --git a/auth/cmix.go b/auth/cmix.go new file mode 100644 index 0000000000000000000000000000000000000000..54ef0650ae66503562168931f7b7e1a088571716 --- /dev/null +++ b/auth/cmix.go @@ -0,0 +1,66 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +// cmix.go cMix functions for the auth module + +package auth + +import ( + "fmt" + + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/interfaces" + "gitlab.com/elixxir/client/interfaces/params" + "gitlab.com/elixxir/client/interfaces/preimage" + "gitlab.com/elixxir/primitives/format" + "gitlab.com/xx_network/primitives/id" +) + +// getMixPayloadSize calculates the payload size of a cMix Message based on the +// total message size. +// TODO: Maybe move this to primitives and export it? +// FIXME: This can only vary per cMix network target, and it could be scoped +// to a Client instance. +func getMixPayloadSize(primeSize int) int { + return 2*primeSize - format.AssociatedDataSize - 1 +} + +// sendAuthRequest is a helper to send the cMix Message after the request +// is created. +func sendAuthRequest(recipient *id.ID, contents, mac []byte, primeSize int, + fingerprint format.Fingerprint, net interfaces.NetworkManager, + cMixParams params.CMIX, reset bool) (id.Round, error) { + cmixMsg := format.NewMessage(primeSize) + cmixMsg.SetKeyFP(fingerprint) + cmixMsg.SetMac(mac) + cmixMsg.SetContents(contents) + + jww.INFO.Printf("Requesting Auth with %s, msgDigest: %s", + recipient, cmixMsg.Digest()) + if reset { + cMixParams.IdentityPreimage = preimage.GenerateReset(recipient) + } else { + cMixParams.IdentityPreimage = preimage.GenerateRequest(recipient) + } + + cMixParams.DebugTag = "auth.Request" + round, _, err := net.SendCMIX(cmixMsg, recipient, cMixParams) + if err != nil { + // if the send fails just set it to failed, it will + // but automatically retried + return 0, errors.WithMessagef(err, "Auth Request with %s "+ + "(msgDigest: %s) failed to transmit: %+v", recipient, + cmixMsg.Digest(), err) + } + + em := fmt.Sprintf("Auth Request with %s (msgDigest: %s) sent"+ + " on round %d", recipient, cmixMsg.Digest(), round) + jww.INFO.Print(em) + net.GetEventManager().Report(1, "Auth", "RequestSent", em) + return round, nil +} diff --git a/auth/confirm.go b/auth/confirm.go index f0f7ba84975ae9f8e261c68014fabe8f347bc2d7..40ddc527dc1f43da6b5f5071d0670d1702369d16 100644 --- a/auth/confirm.go +++ b/auth/confirm.go @@ -9,108 +9,130 @@ package auth import ( "fmt" + "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" - "gitlab.com/elixxir/client/interfaces" "gitlab.com/elixxir/client/interfaces/params" "gitlab.com/elixxir/client/interfaces/preimage" "gitlab.com/elixxir/client/storage" "gitlab.com/elixxir/client/storage/edge" util "gitlab.com/elixxir/client/storage/utility" "gitlab.com/elixxir/crypto/contact" - "gitlab.com/elixxir/crypto/diffieHellman" cAuth "gitlab.com/elixxir/crypto/e2e/auth" "gitlab.com/elixxir/primitives/format" "gitlab.com/xx_network/primitives/id" - "io" ) -func ConfirmRequestAuth(partner contact.Contact, rng io.Reader, - storage *storage.Session, net interfaces.NetworkManager) (id.Round, error) { +func (m *Manager) ConfirmRequestAuth(partner contact.Contact) (id.Round, error) { /*edge checking*/ // check that messages can be sent over the network - if !net.GetHealthTracker().IsHealthy() { + if !m.net.GetHealthTracker().IsHealthy() { return 0, errors.New("Cannot confirm authenticated message " + "when the network is not healthy") } + return m.confirmRequestAuth(partner, false) +} + +func (m *Manager) confirmRequestAuth(partner contact.Contact, critical bool) (id.Round, + error) { + + // Cannot confirm already established channels + if _, err := m.storage.E2e().GetPartner(partner.ID); err == nil { + em := fmt.Sprintf("Cannot ConfirmRequestAuth for %s, "+ + "channel already exists. Ignoring", partner.ID) + jww.WARN.Print(em) + m.net.GetEventManager().Report(5, "Auth", + "ConfirmRequestAuthIgnored", em) + //exit + return 0, errors.New(em) + } // check if the partner has an auth in progress // this takes the lock, from this point forward any errors need to // release the lock - storedContact, theirSidhKey, err := storage.Auth().GetReceivedRequest( + storedContact, theirSidhKey, err := m.storage.Auth().GetReceivedRequest( partner.ID) if err != nil { return 0, errors.Errorf( "failed to find a pending Auth Request: %s", err) } - defer storage.Auth().Done(partner.ID) + defer m.storage.Auth().Done(partner.ID) // verify the passed contact matches what is stored if storedContact.DhPubKey.Cmp(partner.DhPubKey) != 0 { - storage.Auth().Done(partner.ID) return 0, errors.WithMessage(err, "Pending Auth Request has different pubkey than stored") } - grp := storage.E2e().GetGroup() + grp := m.storage.E2e().GetGroup() /*cryptographic generation*/ - //generate ownership proof - ownership := cAuth.MakeOwnershipProof(storage.E2e().GetDHPrivateKey(), - partner.DhPubKey, storage.E2e().GetGroup()) + // generate ownership proof + ownership := cAuth.MakeOwnershipProof(m.storage.E2e().GetDHPrivateKey(), + partner.DhPubKey, m.storage.E2e().GetGroup()) - //generate new keypair - newPrivKey := diffieHellman.GeneratePrivateKey(256, grp, rng) - newPubKey := diffieHellman.GeneratePublicKey(newPrivKey, grp) + rng := m.rng.GetStream() + // generate new keypair + dhGrp := grp + dhPriv, dhPub := genDHKeys(dhGrp, rng) sidhVariant := util.GetCompatibleSIDHVariant(theirSidhKey.Variant()) - newSIDHPrivKey := util.NewSIDHPrivateKey(sidhVariant) - newSIDHPubKey := util.NewSIDHPublicKey(sidhVariant) - newSIDHPrivKey.Generate(rng) - newSIDHPrivKey.GeneratePublicKey(newSIDHPubKey) + sidhPriv, sidhPub := util.GenerateSIDHKeyPair(sidhVariant, rng) + + rng.Close() /*construct message*/ // we build the payload before we save because it is technically fallible // which can get into a bricked state if it fails - cmixMsg := format.NewMessage(storage.Cmix().GetGroup().GetP().ByteLen()) + cmixMsg := format.NewMessage(m.storage.Cmix().GetGroup().GetP().ByteLen()) baseFmt := newBaseFormat(cmixMsg.ContentsSize(), grp.GetP().ByteLen()) ecrFmt := newEcrFormat(baseFmt.GetEcrPayloadLen()) // setup the encrypted payload ecrFmt.SetOwnership(ownership) - ecrFmt.SetSidHPubKey(newSIDHPubKey) + ecrFmt.SetSidHPubKey(sidhPub) // confirmation has no custom payload - //encrypt the payload - ecrPayload, mac := cAuth.Encrypt(newPrivKey, partner.DhPubKey, + // encrypt the payload + ecrPayload, mac := cAuth.Encrypt(dhPriv, partner.DhPubKey, ecrFmt.data, grp) - //get the fingerprint from the old ownership proof + // get the fingerprint from the old ownership proof fp := cAuth.MakeOwnershipProofFP(storedContact.OwnershipProof) preimg := preimage.Generate(fp[:], preimage.Confirm) - //final construction + // final construction baseFmt.SetEcrPayload(ecrPayload) - baseFmt.SetPubKey(newPubKey) + baseFmt.SetPubKey(dhPub) cmixMsg.SetKeyFP(fp) cmixMsg.SetMac(mac) cmixMsg.SetContents(baseFmt.Marshal()) + jww.TRACE.Printf("SendConfirm cMixMsg contents: %v", + cmixMsg.GetContents()) + + jww.TRACE.Printf("SendConfirm PARTNERPUBKEY: %v", + partner.DhPubKey.Bytes()) + jww.TRACE.Printf("SendConfirm MYPUBKEY: %v", dhPub.Bytes()) + + jww.TRACE.Printf("SendConfirm ECRPAYLOAD: %v", baseFmt.GetEcrPayload()) + jww.TRACE.Printf("SendConfirm MAC: %v", mac) + // fixme: channel can get into a bricked state if the first save occurs and // the second does not or the two occur and the storage into critical // messages does not occur - events := net.GetEventManager() + events := m.net.GetEventManager() - //create local relationship - p := storage.E2e().GetE2ESessionParams() - if err := storage.E2e().AddPartner(partner.ID, partner.DhPubKey, - newPrivKey, theirSidhKey, newSIDHPrivKey, + // create local relationship + p := m.storage.E2e().GetE2ESessionParams() + if err := m.storage.E2e().AddPartner(partner.ID, partner.DhPubKey, + dhPriv, theirSidhKey, sidhPriv, p, p); err != nil { em := fmt.Sprintf("Failed to create channel with partner (%s) "+ "on confirmation, this is likley a replay: %s", @@ -119,44 +141,13 @@ func ConfirmRequestAuth(partner contact.Contact, rng io.Reader, events.Report(10, "Auth", "SendConfirmError", em) } - //add the preimages - sessionPartner, err := storage.E2e().GetPartner(partner.ID) - if err != nil { - jww.FATAL.Panicf("Cannot find %s right after creating: %+v", partner.ID, err) - } - me := storage.GetUser().ReceptionID - - //e2e - storage.GetEdge().Add(edge.Preimage{ - Data: sessionPartner.GetE2EPreimage(), - Type: preimage.E2e, - Source: partner.ID[:], - }, me) - - //slient (rekey) - storage.GetEdge().Add(edge.Preimage{ - Data: sessionPartner.GetSilentPreimage(), - Type: preimage.Silent, - Source: partner.ID[:], - }, me) - - // File transfer end - storage.GetEdge().Add(edge.Preimage{ - Data: sessionPartner.GetFileTransferPreimage(), - Type: preimage.EndFT, - Source: partner.ID[:], - }, me) + m.backupTrigger("confirmed authenticated channel") - //group Request - storage.GetEdge().Add(edge.Preimage{ - Data: sessionPartner.GetGroupRequestPreimage(), - Type: preimage.GroupRq, - Source: partner.ID[:], - }, me) + addPreimages(partner.ID, m.storage) // delete the in progress negotiation // this unlocks the request lock - //fixme - do these deletes at a later date + // fixme - do these deletes at a later date /*if err := storage.Auth().Delete(partner.ID); err != nil { return 0, errors.Errorf("UNRECOVERABLE! Failed to delete in "+ "progress negotiation with partner (%s) after creating confirmation: %+v", @@ -170,15 +161,28 @@ func ConfirmRequestAuth(partner contact.Contact, rng io.Reader, param.IdentityPreimage = preimg param.DebugTag = "auth.Confirm" /*send message*/ - round, _, err := net.SendCMIX(cmixMsg, partner.ID, param) + if critical { + m.storage.GetCriticalRawMessages().AddProcessing(cmixMsg, + partner.ID) + } + round, _, err := m.net.SendCMIX(cmixMsg, partner.ID, param) if err != nil { // if the send fails just set it to failed, it will but automatically // retried jww.INFO.Printf("Auth Confirm with %s (msgDigest: %s) failed "+ "to transmit: %+v", partner.ID, cmixMsg.Digest(), err) + if critical { + m.storage.GetCriticalRawMessages().Failed(cmixMsg, + partner.ID) + } return 0, errors.WithMessage(err, "Auth Confirm Failed to transmit") } + if critical { + m.storage.GetCriticalRawMessages().Succeeded(cmixMsg, + partner.ID) + } + em := fmt.Sprintf("Confirm Request with %s (msgDigest: %s) sent on round %d", partner.ID, cmixMsg.Digest(), round) jww.INFO.Print(em) @@ -186,3 +190,65 @@ func ConfirmRequestAuth(partner contact.Contact, rng io.Reader, return round, nil } + +func addPreimages(partner *id.ID, store *storage.Session) { + // add the preimages + sessionPartner, err := store.E2e().GetPartner(partner) + if err != nil { + jww.FATAL.Panicf("Cannot find %s right after creating: %+v", + partner, err) + } + + // Delete any known pre-existing edges for this partner + existingEdges, _ := store.GetEdge().Get(partner) + for i := range existingEdges { + delete := true + switch existingEdges[i].Type { + case preimage.E2e: + case preimage.Silent: + case preimage.EndFT: + case preimage.GroupRq: + default: + delete = false + } + + if delete { + err = store.GetEdge().Remove(existingEdges[i], partner) + if err != nil { + jww.ERROR.Printf( + "Unable to delete %s edge for %s: %v", + existingEdges[i].Type, partner, err) + } + } + } + + me := store.GetUser().ReceptionID + + // e2e + store.GetEdge().Add(edge.Preimage{ + Data: sessionPartner.GetE2EPreimage(), + Type: preimage.E2e, + Source: partner[:], + }, me) + + // silent (rekey) + store.GetEdge().Add(edge.Preimage{ + Data: sessionPartner.GetSilentPreimage(), + Type: preimage.Silent, + Source: partner[:], + }, me) + + // File transfer end + store.GetEdge().Add(edge.Preimage{ + Data: sessionPartner.GetFileTransferPreimage(), + Type: preimage.EndFT, + Source: partner[:], + }, me) + + // group Request + store.GetEdge().Add(edge.Preimage{ + Data: sessionPartner.GetGroupRequestPreimage(), + Type: preimage.GroupRq, + Source: partner[:], + }, me) +} diff --git a/auth/fmt.go b/auth/fmt.go index 335867f41044eb65a5b5c3db27af669c72e60cb8..cf38aaed44967d8a825a27451783bf5318038544 100644 --- a/auth/fmt.go +++ b/auth/fmt.go @@ -17,26 +17,33 @@ import ( "gitlab.com/xx_network/primitives/id" ) +const requestFmtVersion = 1 + //Basic Format////////////////////////////////////////////////////////////////// type baseFormat struct { data []byte pubkey []byte ecrPayload []byte + version []byte } func newBaseFormat(payloadSize, pubkeySize int) baseFormat { - total := pubkeySize + sidhinterface.PubKeyByteSize + 1 + total := pubkeySize + // Size of sidh pubkey + total += sidhinterface.PubKeyByteSize + 1 + // Size of version + total += 1 if payloadSize < total { jww.FATAL.Panicf("Size of baseFormat is too small (%d), must be big "+ "enough to contain public key (%d) and sidh key (%d)"+ - "which totals to %d", payloadSize, pubkeySize, - sidhinterface.PubKeyByteSize+1, total) + "and version which totals to %d", payloadSize, + pubkeySize, sidhinterface.PubKeyByteSize+1, total) } jww.INFO.Printf("Empty Space RequestAuth: %d", payloadSize-total) f := buildBaseFormat(make([]byte, payloadSize), pubkeySize) - + f.version[0] = requestFmtVersion return f } @@ -47,25 +54,39 @@ func buildBaseFormat(data []byte, pubkeySize int) baseFormat { start := 0 end := pubkeySize - f.pubkey = f.data[:end] + f.pubkey = f.data[start:end] start = end - f.ecrPayload = f.data[start:] + end = len(f.data) - 1 + f.ecrPayload = f.data[start:end] + + f.version = f.data[end:] + return f } -func unmarshalBaseFormat(b []byte, pubkeySize int) (baseFormat, error) { +func unmarshalBaseFormat(b []byte, pubkeySize int) (*baseFormat, error) { if len(b) < pubkeySize { - return baseFormat{}, errors.New("Received baseFormat too small") + return nil, errors.New("Received baseFormat too small") + } + bfmt := buildBaseFormat(b, pubkeySize) + version := bfmt.GetVersion() + if version != requestFmtVersion { + return &bfmt, errors.Errorf( + "Unknown baseFormat version: %d", version) } - return buildBaseFormat(b, pubkeySize), nil + return &bfmt, nil } func (f baseFormat) Marshal() []byte { return f.data } +func (f baseFormat) GetVersion() byte { + return f.version[0] +} + func (f baseFormat) GetPubKey(grp *cyclic.Group) *cyclic.Int { return grp.NewIntFromBytes(f.pubkey) } diff --git a/auth/fmt_test.go b/auth/fmt_test.go index 1178fc0b705b3c6dc3ee57bacfe6ada38de54933..30580ec1bef400eb05409d3033076e9f0b00b4cd 100644 --- a/auth/fmt_test.go +++ b/auth/fmt_test.go @@ -9,20 +9,26 @@ package auth import ( "bytes" - sidhinterface "gitlab.com/elixxir/client/interfaces/sidh" - "gitlab.com/xx_network/primitives/id" "math/rand" "reflect" "testing" + + sidhinterface "gitlab.com/elixxir/client/interfaces/sidh" + "gitlab.com/xx_network/primitives/id" ) // Tests newBaseFormat func TestNewBaseFormat(t *testing.T) { // Construct message pubKeySize := 256 - payloadSize := pubKeySize + sidhinterface.PubKeyByteSize + 1 + payloadSize := pubKeySize + sidhinterface.PubKeyByteSize + 2 baseMsg := newBaseFormat(payloadSize, pubKeySize) + if baseMsg.GetVersion() != requestFmtVersion { + t.Errorf("Incorrect version: %d, expect %d", + baseMsg.GetVersion(), requestFmtVersion) + } + // Check that the base format was constructed properly if !bytes.Equal(baseMsg.pubkey, make([]byte, pubKeySize)) { t.Errorf("NewBaseFormat error: "+ @@ -31,7 +37,7 @@ func TestNewBaseFormat(t *testing.T) { "\n\tReceived: %v", make([]byte, pubKeySize), baseMsg.pubkey) } - expectedEcrPayloadSize := payloadSize - (pubKeySize) + expectedEcrPayloadSize := payloadSize - (pubKeySize) - 1 if !bytes.Equal(baseMsg.ecrPayload, make([]byte, expectedEcrPayloadSize)) { t.Errorf("NewBaseFormat error: "+ "Unexpected payload field in base format."+ @@ -56,7 +62,7 @@ func TestNewBaseFormat(t *testing.T) { func TestBaseFormat_SetGetPubKey(t *testing.T) { // Construct message pubKeySize := 256 - payloadSize := pubKeySize + sidhinterface.PubKeyByteSize + 1 + payloadSize := pubKeySize + sidhinterface.PubKeyByteSize + 2 baseMsg := newBaseFormat(payloadSize, pubKeySize) // Test setter @@ -88,7 +94,7 @@ func TestBaseFormat_SetGetEcrPayload(t *testing.T) { baseMsg := newBaseFormat(payloadSize, pubKeySize) // Test setter - ecrPayloadSize := payloadSize - (pubKeySize) + ecrPayloadSize := payloadSize - (pubKeySize) - 1 ecrPayload := newPayload(ecrPayloadSize, "ecrPayload") baseMsg.SetEcrPayload(ecrPayload) if !bytes.Equal(ecrPayload, baseMsg.ecrPayload) { @@ -123,7 +129,7 @@ func TestBaseFormat_MarshalUnmarshal(t *testing.T) { pubKeySize := 256 payloadSize := (pubKeySize + sidhinterface.PubKeyByteSize) * 2 baseMsg := newBaseFormat(payloadSize, pubKeySize) - ecrPayloadSize := payloadSize - (pubKeySize) + ecrPayloadSize := payloadSize - (pubKeySize) - 1 ecrPayload := newPayload(ecrPayloadSize, "ecrPayload") baseMsg.SetEcrPayload(ecrPayload) grp := getGroup() @@ -145,10 +151,10 @@ func TestBaseFormat_MarshalUnmarshal(t *testing.T) { "Could not unmarshal into baseFormat: %v", err) } - if !reflect.DeepEqual(newMsg, baseMsg) { + if !reflect.DeepEqual(*newMsg, baseMsg) { t.Errorf("unmarshalBaseFormat() error: "+ "Unmarshalled message does not match originally marshalled message."+ - "\n\tExpected: %v\n\tRecieved: %v", baseMsg, newMsg) + "\n\tExpected: %v\n\tRecieved: %v", baseMsg, *newMsg) } // Unmarshal error test: Invalid size parameter diff --git a/auth/manager.go b/auth/manager.go index 20b341c3891212edf308ec9c7afcf43a5f1588f5..a2ba826141ccbcf3a25bbae0c1df00afbda42d79 100644 --- a/auth/manager.go +++ b/auth/manager.go @@ -12,29 +12,37 @@ import ( "gitlab.com/elixxir/client/interfaces/message" "gitlab.com/elixxir/client/storage" "gitlab.com/elixxir/client/switchboard" + "gitlab.com/elixxir/crypto/fastRNG" "gitlab.com/xx_network/primitives/id" ) type Manager struct { requestCallbacks *callbackMap confirmCallbacks *callbackMap + resetCallbacks *callbackMap rawMessages chan message.Receive - storage *storage.Session - net interfaces.NetworkManager + storage *storage.Session + net interfaces.NetworkManager + rng *fastRNG.StreamGenerator + backupTrigger interfaces.TriggerBackup replayRequests bool } func NewManager(sw interfaces.Switchboard, storage *storage.Session, - net interfaces.NetworkManager, replayRequests bool) *Manager { + net interfaces.NetworkManager, rng *fastRNG.StreamGenerator, + backupTrigger interfaces.TriggerBackup, replayRequests bool) *Manager { m := &Manager{ requestCallbacks: newCallbackMap(), confirmCallbacks: newCallbackMap(), + resetCallbacks: newCallbackMap(), rawMessages: make(chan message.Receive, 1000), storage: storage, net: net, + rng: rng, + backupTrigger: backupTrigger, replayRequests: replayRequests, } @@ -93,6 +101,11 @@ func (m *Manager) RemoveSpecificConfirmCallback(id *id.ID) { m.confirmCallbacks.RemoveSpecific(id) } +// Adds a general callback to be used on auth session renegotiations. +func (m *Manager) AddResetNotificationCallback(cb interfaces.ResetNotificationCallback) { + m.resetCallbacks.AddOverride(cb) +} + // ReplayRequests will iterate through all pending contact requests and resend them // to the desired contact. func (m *Manager) ReplayRequests() { diff --git a/auth/request.go b/auth/request.go index 7148cf5a4b0dc1d171da6eb62178cd08c60620b7..8f39fc9db835f30e41e51e6a6fe5db377aef666f 100644 --- a/auth/request.go +++ b/auth/request.go @@ -8,7 +8,9 @@ package auth import ( - "fmt" + "io" + "strings" + "github.com/cloudflare/circl/dh/sidh" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" @@ -24,25 +26,50 @@ import ( "gitlab.com/elixxir/crypto/cyclic" "gitlab.com/elixxir/crypto/diffieHellman" cAuth "gitlab.com/elixxir/crypto/e2e/auth" - "gitlab.com/elixxir/primitives/format" "gitlab.com/xx_network/primitives/id" - "io" - "strings" ) const terminator = ";" func RequestAuth(partner, me contact.Contact, rng io.Reader, storage *storage.Session, net interfaces.NetworkManager) (id.Round, error) { - /*edge checks generation*/ - - // check that an authenticated channel does not already exists + // check that an authenticated channel does not already exist if _, err := storage.E2e().GetPartner(partner.ID); err == nil || !strings.Contains(err.Error(), e2e.NoPartnerErrorStr) { return 0, errors.Errorf("Authenticated channel already " + "established with partner") } + return requestAuth(partner, me, rng, false, storage, net) +} + +func ResetSession(partner, me contact.Contact, rng io.Reader, + storage *storage.Session, net interfaces.NetworkManager) (id.Round, error) { + + // Delete authenticated channel if it exists. + if err := storage.E2e().DeletePartner(partner.ID); err != nil { + jww.WARN.Printf("Unable to delete partner when "+ + "resetting session: %+v", err) + } else { + // Delete any stored sent/received requests + storage.Auth().Delete(partner.ID) + } + + rqType, _, _, err := storage.Auth().GetRequest(partner.ID) + if err == nil && rqType == auth.Sent { + return 0, errors.New("Cannot reset a session after " + + "sending request, caller must resend request instead") + } + + // Try to initiate a clean session request + return requestAuth(partner, me, rng, true, storage, net) +} + +// requestAuth internal helper +func requestAuth(partner, me contact.Contact, rng io.Reader, reset bool, + storage *storage.Session, net interfaces.NetworkManager) (id.Round, error) { + + /*edge checks generation*/ // check that the request is being sent from the proper ID if !me.ID.Cmp(storage.GetUser().ReceptionID) { return 0, errors.Errorf("Authenticated channel request " + @@ -54,98 +81,81 @@ func RequestAuth(partner, me contact.Contact, rng io.Reader, //lookup if an ongoing request is occurring rqType, sr, _, err := storage.Auth().GetRequest(partner.ID) - - if err == nil { - if rqType == auth.Receive { - return 0, errors.Errorf("Cannot send a request after " + - "receiving a request") - } else if rqType == auth.Sent { - resend = true - } else { - return 0, errors.Errorf("Cannot send a request after "+ - " a stored request with unknown rqType: %d", rqType) - } - } else if !strings.Contains(err.Error(), auth.NoRequest) { + if err != nil && !strings.Contains(err.Error(), auth.NoRequest) { return 0, errors.WithMessage(err, "Cannot send a request after receiving unknown error "+ "on requesting contact status") + } else if err == nil { + switch rqType { + case auth.Receive: + if reset { + storage.Auth().DeleteRequest(partner.ID) + } else { + return 0, errors.Errorf("Cannot send a " + + "request after receiving a request") + } + case auth.Sent: + resend = true + default: + return 0, errors.Errorf("Cannot send a request after "+ + "a stored request with unknown rqType: %d", + rqType) + } } - grp := storage.E2e().GetGroup() - - /*generate embedded message structures and check payload*/ - cmixMsg := format.NewMessage(storage.Cmix().GetGroup().GetP().ByteLen()) - baseFmt := newBaseFormat(cmixMsg.ContentsSize(), grp.GetP().ByteLen()) - ecrFmt := newEcrFormat(baseFmt.GetEcrPayloadLen()) - requestFmt, err := newRequestFormat(ecrFmt) - if err != nil { - return 0, errors.Errorf("failed to make request format: %+v", err) - } - - //check the payload fits - facts := me.Facts.Stringify() - msgPayload := facts + terminator - msgPayloadBytes := []byte(msgPayload) - /*cryptographic generation*/ - var newPrivKey, newPubKey *cyclic.Int - var sidHPrivKeyA *sidh.PrivateKey - var sidHPubKeyA *sidh.PublicKey + var dhPriv, dhPub *cyclic.Int + var sidhPriv *sidh.PrivateKey + var sidhPub *sidh.PublicKey - // in this case we have an ongoing request so we can resend the extant - // request - if resend { - newPrivKey = sr.GetMyPrivKey() - newPubKey = sr.GetMyPubKey() - sidHPrivKeyA = sr.GetMySIDHPrivKey() - sidHPubKeyA = sr.GetMySIDHPubKey() - //in this case it is a new request and we must generate new keys - } else { - //generate new keypair - newPrivKey = diffieHellman.GeneratePrivateKey(256, grp, rng) - newPubKey = diffieHellman.GeneratePublicKey(newPrivKey, grp) + // NOTE: E2E group is the group used for DH key exchange, not cMix + dhGrp := storage.E2e().GetGroup() + // origin DH Priv key is the DH Key corresponding to the public key + // registered with user discovery + originDHPrivKey := storage.E2e().GetDHPrivateKey() - sidHPrivKeyA = util.NewSIDHPrivateKey(sidh.KeyVariantSidhA) - sidHPubKeyA = util.NewSIDHPublicKey(sidh.KeyVariantSidhA) - - if err = sidHPrivKeyA.Generate(rng); err != nil { - return 0, errors.WithMessagef(err, "RequestAuth: "+ - "could not generate SIDH private key") - } - sidHPrivKeyA.GeneratePublicKey(sidHPubKeyA) + // If we are resending (valid sent request), reuse those keys + if resend { + dhPriv = sr.GetMyPrivKey() + dhPub = sr.GetMyPubKey() + sidhPriv = sr.GetMySIDHPrivKey() + sidhPub = sr.GetMySIDHPubKey() + } else { + dhPriv, dhPub = genDHKeys(dhGrp, rng) + sidhPriv, sidhPub = util.GenerateSIDHKeyPair( + sidh.KeyVariantSidhA, rng) } - if len(msgPayloadBytes) > requestFmt.MsgPayloadLen() { - return 0, errors.Errorf("Combined message longer than space "+ - "available in payload; available: %v, length: %v", - requestFmt.MsgPayloadLen(), len(msgPayloadBytes)) - } + jww.TRACE.Printf("RequestAuth MYPUBKEY: %v", dhPub.Bytes()) + jww.TRACE.Printf("RequestAuth THEIRPUBKEY: %v", + partner.DhPubKey.Bytes()) - //generate ownership proof - ownership := cAuth.MakeOwnershipProof(storage.E2e().GetDHPrivateKey(), - partner.DhPubKey, storage.E2e().GetGroup()) + cMixPrimeSize := storage.Cmix().GetGroup().GetP().ByteLen() + cMixPayloadSize := getMixPayloadSize(cMixPrimeSize) - jww.TRACE.Printf("RequestAuth MYPUBKEY: %v", newPubKey.Bytes()) - jww.TRACE.Printf("RequestAuth THEIRPUBKEY: %v", partner.DhPubKey.Bytes()) + sender := storage.GetUser().ReceptionID - /*encrypt payload*/ - requestFmt.SetID(storage.GetUser().ReceptionID) - requestFmt.SetMsgPayload(msgPayloadBytes) - ecrFmt.SetOwnership(ownership) - ecrFmt.SetSidHPubKey(sidHPubKeyA) - ecrPayload, mac := cAuth.Encrypt(newPrivKey, partner.DhPubKey, - ecrFmt.data, grp) + //generate ownership proof + ownership := cAuth.MakeOwnershipProof(originDHPrivKey, partner.DhPubKey, + dhGrp) confirmFp := cAuth.MakeOwnershipProofFP(ownership) + + // cMix fingerprint so the recipient can recognize this is a + // request message. requestfp := cAuth.MakeRequestFingerprint(partner.DhPubKey) - /*construct message*/ - baseFmt.SetEcrPayload(ecrPayload) - baseFmt.SetPubKey(newPubKey) + // My fact data so we can display in the interface. + msgPayload := []byte(me.Facts.Stringify() + terminator) - cmixMsg.SetKeyFP(requestfp) - cmixMsg.SetMac(mac) - cmixMsg.SetContents(baseFmt.Marshal()) + // Create the request packet. + request, mac, err := createRequestAuth(sender, msgPayload, ownership, + dhPriv, dhPub, partner.DhPubKey, sidhPub, + dhGrp, cMixPayloadSize) + if err != nil { + return 0, err + } + contents := request.Marshal() storage.GetEdge().Add(edge.Preimage{ Data: preimage.Generate(confirmFp[:], preimage.Confirm), @@ -153,40 +163,77 @@ func RequestAuth(partner, me contact.Contact, rng io.Reader, Source: partner.ID[:], }, me.ID) - jww.TRACE.Printf("RequestAuth ECRPAYLOAD: %v", baseFmt.GetEcrPayload()) + jww.TRACE.Printf("RequestAuth ECRPAYLOAD: %v", request.GetEcrPayload()) jww.TRACE.Printf("RequestAuth MAC: %v", mac) /*store state*/ - //fixme: channel is bricked if the first store succedes but the second fails - //store the in progress auth + //fixme: channel is bricked if the first store succedes but the second + // fails + //store the in progress auth if this is not a resend. if !resend { - err = storage.Auth().AddSent(partner.ID, partner.DhPubKey, newPrivKey, - newPubKey, sidHPrivKeyA, sidHPubKeyA, confirmFp) + err = storage.Auth().AddSent(partner.ID, partner.DhPubKey, + dhPriv, dhPub, sidhPriv, sidhPub, confirmFp) if err != nil { - return 0, errors.Errorf("Failed to store auth request: %s", err) + return 0, errors.Errorf( + "Failed to store auth request: %s", err) } } - jww.INFO.Printf("Requesting Auth with %s, msgDigest: %s", - partner.ID, cmixMsg.Digest()) + cMixParams := params.GetDefaultCMIX() + rndID, err := sendAuthRequest(partner.ID, contents, mac, cMixPrimeSize, + requestfp, net, cMixParams, reset) + return rndID, err +} + +// genDHKeys is a short helper to generate a Diffie-Helman Keypair +func genDHKeys(dhGrp *cyclic.Group, csprng io.Reader) (priv, pub *cyclic.Int) { + numBytes := len(dhGrp.GetPBytes()) + newPrivKey := diffieHellman.GeneratePrivateKey(numBytes, dhGrp, csprng) + newPubKey := diffieHellman.GeneratePublicKey(newPrivKey, dhGrp) + return newPrivKey, newPubKey +} - /*send message*/ - p := params.GetDefaultCMIX() - p.IdentityPreimage = preimage.GenerateRequest(partner.ID) - p.DebugTag = "auth.Request" - round, _, err := net.SendCMIX(cmixMsg, partner.ID, p) +// createRequestAuth Creates the request packet, including encrypting the +// required parts of it. +func createRequestAuth(sender *id.ID, payload, ownership []byte, myDHPriv, + myDHPub, theirDHPub *cyclic.Int, mySIDHPub *sidh.PublicKey, + dhGrp *cyclic.Group, cMixSize int) (*baseFormat, []byte, error) { + /*generate embedded message structures and check payload*/ + dhPrimeSize := dhGrp.GetP().ByteLen() + + // FIXME: This base -> ecr -> request structure is a little wonky. + // We should refactor so that is is more direct. + // I recommend we move to a request object that takes: + // sender, dhPub, sidhPub, ownershipProof, payload + // with a Marshal/Unmarshal that takes the Dh/grp needed to gen + // the session key and encrypt or decrypt. + + // baseFmt wraps ecrFmt. ecrFmt is encrypted + baseFmt := newBaseFormat(cMixSize, dhPrimeSize) + // ecrFmt wraps requestFmt + ecrFmt := newEcrFormat(baseFmt.GetEcrPayloadLen()) + requestFmt, err := newRequestFormat(ecrFmt) if err != nil { - // if the send fails just set it to failed, it will - // but automatically retried - return 0, errors.WithMessagef(err, "Auth Request with %s "+ - "(msgDigest: %s) failed to transmit: %+v", partner.ID, - cmixMsg.Digest(), err) + return nil, nil, errors.Errorf("failed to make request format: %+v", err) + } + + if len(payload) > requestFmt.MsgPayloadLen() { + return nil, nil, errors.Errorf( + "Combined message longer than space "+ + "available in payload; available: %v, length: %v", + requestFmt.MsgPayloadLen(), len(payload)) } - em := fmt.Sprintf("Auth Request with %s (msgDigest: %s) sent"+ - " on round %d", partner.ID, cmixMsg.Digest(), round) - jww.INFO.Print(em) - net.GetEventManager().Report(1, "Auth", "RequestSent", em) + /*encrypt payload*/ + requestFmt.SetID(sender) + requestFmt.SetMsgPayload(payload) + ecrFmt.SetOwnership(ownership) + ecrFmt.SetSidHPubKey(mySIDHPub) + ecrPayload, mac := cAuth.Encrypt(myDHPriv, theirDHPub, ecrFmt.data, + dhGrp) + /*construct message*/ + baseFmt.SetEcrPayload(ecrPayload) + baseFmt.SetPubKey(myDHPub) - return round, nil + return &baseFmt, mac, nil } diff --git a/backup/backup.go b/backup/backup.go new file mode 100644 index 0000000000000000000000000000000000000000..795ec2f9ab6a9141493c1e384a2e37cf6984a936 --- /dev/null +++ b/backup/backup.go @@ -0,0 +1,324 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package backup + +import ( + "sync" + + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/api" + "gitlab.com/elixxir/client/interfaces" + "gitlab.com/elixxir/client/storage" + "gitlab.com/elixxir/crypto/backup" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/xx_network/primitives/id" +) + +// Error messages. +const ( + // initializeBackup + errSavePassword = "failed to save password: %+v" + errSaveKeySaltParams = "failed to save key, salt, and params: %+v" + + // Backup.StopBackup + errDeletePassword = "failed to delete password: %+v" + errDeleteCrypto = "failed to delete key, salt, and parameters: %+v" +) + +// Backup stores the user's key and backup callback used to encrypt and transmit +// the backup data. +type Backup struct { + // Callback that is called with the encrypted backup when triggered + updateBackupCb UpdateBackupFn + + mux sync.RWMutex + + // Client structures + client *api.Client + store *storage.Session + backupContainer *interfaces.BackupContainer + rng *fastRNG.StreamGenerator + + jsonParams string +} + +// UpdateBackupFn is the callback that encrypted backup data is returned on +type UpdateBackupFn func(encryptedBackup []byte) + +// InitializeBackup creates a new Backup object with the callback to return +// backups when triggered. On initialization, 32-bit key is derived from the +// user's password via Argon2 and a 16-bit salt is generated. Both are saved to +// storage along with the parameters used in Argon2 to be used when encrypting +// new backups. +// Call this to turn on backups for the first time or to replace the user's +// password. +func InitializeBackup(password string, updateBackupCb UpdateBackupFn, + c *api.Client) (*Backup, error) { + return initializeBackup( + password, updateBackupCb, c, c.GetStorage(), c.GetBackup(), c.GetRng()) +} + +// initializeBackup is a helper function that takes in all the fields for Backup +// as parameters for easier testing. +func initializeBackup(password string, updateBackupCb UpdateBackupFn, + c *api.Client, store *storage.Session, + backupContainer *interfaces.BackupContainer, rng *fastRNG.StreamGenerator) ( + *Backup, error) { + b := &Backup{ + updateBackupCb: updateBackupCb, + client: c, + store: store, + backupContainer: backupContainer, + rng: rng, + } + + // Derive key and get generated salt and parameters + rand := b.rng.GetStream() + salt, err := backup.MakeSalt(rand) + if err != nil { + return nil, err + } + rand.Close() + + params := backup.DefaultParams() + params.Memory = 64 * 1024 // 64 MiB + params.Threads = 1 + params.Time = 5 + key := backup.DeriveKey(password, salt, params) + + // Save key, salt, and parameters to storage + err = saveBackup(key, salt, params, b.store.GetKV()) + if err != nil { + return nil, errors.Errorf(errSaveKeySaltParams, err) + } + + // Setting backup trigger in client + b.backupContainer.SetBackup(b.TriggerBackup) + + b.TriggerBackup("initializeBackup") + jww.INFO.Print("Initialized backup with new user key.") + + return b, nil +} + +// ResumeBackup resumes a backup by restoring the Backup object and registering +// a new callback. Call this to resume backups that have already been +// initialized. Returns an error if backups have not already been initialized. +func ResumeBackup(updateBackupCb UpdateBackupFn, c *api.Client) (*Backup, error) { + return resumeBackup( + updateBackupCb, c, c.GetStorage(), c.GetBackup(), c.GetRng()) +} + +// resumeBackup is a helper function that takes in all the fields for Backup as +// parameters for easier testing. +func resumeBackup(updateBackupCb UpdateBackupFn, c *api.Client, + store *storage.Session, backupContainer *interfaces.BackupContainer, + rng *fastRNG.StreamGenerator) (*Backup, error) { + _, _, _, err := loadBackup(store.GetKV()) + if err != nil { + return nil, err + } + + b := &Backup{ + updateBackupCb: updateBackupCb, + client: c, + store: store, + backupContainer: backupContainer, + rng: rng, + jsonParams: loadJson(store.GetKV()), + } + + // Setting backup trigger in client + b.backupContainer.SetBackup(b.TriggerBackup) + + jww.INFO.Print("resumed backup with password loaded from storage.") + + return b, nil +} + +// getKeySaltParams derives a key from the user's password, a generated salt, +// and the default parameters and return all three. +func (b *Backup) getKeySaltParams(password string) ( + key, salt []byte, params backup.Params, err error) { + rand := b.rng.GetStream() + salt, err = backup.MakeSalt(rand) + if err != nil { + return + } + rand.Close() + + params = backup.DefaultParams() + key = backup.DeriveKey(password, salt, params) + + return +} + +// TriggerBackup assembles the backup and calls it on the registered backup +// callback. Does nothing if no encryption key or backup callback is registered. +// The passed in reason will be printed to the log when the backup is sent. It +// should be in the past tense. For example, if a contact is deleted, the +// reason can be "contact deleted" and the log will show: +// Triggering backup: contact deleted +func (b *Backup) TriggerBackup(reason string) { + b.mux.RLock() + defer b.mux.RUnlock() + + key, salt, params, err := loadBackup(b.store.GetKV()) + if err != nil { + jww.ERROR.Printf("Backup Failed: could not load key, salt, and "+ + "parameters for encrypting backup from storage: %+v", err) + return + } + + // Grab backup data + collatedBackup := b.assembleBackup() + + // Encrypt backup data with user key + rand := b.rng.GetStream() + encryptedBackup, err := collatedBackup.Encrypt(rand, key, salt, params) + if err != nil { + jww.FATAL.Panicf("Failed to encrypt backup: %+v", err) + } + rand.Close() + + jww.INFO.Printf("Backup triggered: %s", reason) + + // Send backup on callback + b.mux.RLock() + defer b.mux.RUnlock() + if b.updateBackupCb != nil { + go b.updateBackupCb(encryptedBackup) + } else { + jww.WARN.Printf("could not call backup callback, stopped...") + } +} + +func (b *Backup) AddJson(newJson string) { + b.mux.Lock() + defer b.mux.Unlock() + + if newJson != b.jsonParams { + b.jsonParams = newJson + if err := storeJson(newJson, b.store.GetKV()); err != nil { + jww.FATAL.Panicf("Failed to store json: %+v", err) + } + go b.TriggerBackup("New Json") + } +} + +// StopBackup stops the backup processes and deletes the user's password, key, +// salt, and parameters from storage. +func (b *Backup) StopBackup() error { + b.mux.Lock() + defer b.mux.Unlock() + b.updateBackupCb = nil + + err := deleteBackup(b.store.GetKV()) + if err != nil { + return errors.Errorf(errDeleteCrypto, err) + } + + jww.INFO.Print("Stopped backups.") + + return nil +} + +// IsBackupRunning returns true if the backup has been initialized and is +// running. Returns false if it has been stopped. +func (b *Backup) IsBackupRunning() bool { + b.mux.RLock() + defer b.mux.RUnlock() + return b.updateBackupCb != nil +} + +// assembleBackup gathers all the contents of the backup and stores them in a +// backup.Backup. This backup contains: +// 1. Cryptographic information for the transmission identity +// 2. Cryptographic information for the reception identity +// 3. User's UD facts (username, email, phone number) +// 4. Contact list +func (b *Backup) assembleBackup() backup.Backup { + bu := backup.Backup{ + TransmissionIdentity: backup.TransmissionIdentity{}, + ReceptionIdentity: backup.ReceptionIdentity{}, + UserDiscoveryRegistration: backup.UserDiscoveryRegistration{}, + Contacts: backup.Contacts{}, + } + + // Get user and storage user + u := b.store.GetUser() + su := b.store.User() + + // Get registration timestamp + bu.RegistrationTimestamp = u.RegistrationTimestamp + + // Get registration code; ignore the error because if there is no + // registration, then an empty string is returned + bu.RegistrationCode, _ = b.store.GetRegCode() + + // Get transmission identity + bu.TransmissionIdentity = backup.TransmissionIdentity{ + RSASigningPrivateKey: u.TransmissionRSA, + RegistrarSignature: su.GetTransmissionRegistrationValidationSignature(), + Salt: u.TransmissionSalt, + ComputedID: u.TransmissionID, + } + + // Get reception identity + bu.ReceptionIdentity = backup.ReceptionIdentity{ + RSASigningPrivateKey: u.ReceptionRSA, + RegistrarSignature: su.GetReceptionRegistrationValidationSignature(), + Salt: u.ReceptionSalt, + ComputedID: u.ReceptionID, + DHPrivateKey: u.E2eDhPrivateKey, + DHPublicKey: u.E2eDhPublicKey, + } + + // Get facts + bu.UserDiscoveryRegistration.FactList = b.store.GetUd().GetFacts() + + // Get contacts + bu.Contacts.Identities = b.store.E2e().GetPartners() + // Get pending auth requests + // NOTE: Received requests don't matter here, as those are either + // not yet noticed by user OR explicitly rejected. + bu.Contacts.Identities = append(bu.Contacts.Identities, + b.store.Auth().GetAllSentIDs()...) + jww.INFO.Printf("backup saw %d contacts", len(bu.Contacts.Identities)) + jww.DEBUG.Printf("contacts in backup list: %+v", bu.Contacts.Identities) + //deduplicate list + bu.Contacts.Identities = deduplicate(bu.Contacts.Identities) + + jww.INFO.Printf("backup saved %d contacts after deduplication", + len(bu.Contacts.Identities)) + + // Add the memoized JSON params + bu.JSONParams = b.jsonParams + + return bu +} + +func deduplicate(list []*id.ID) []*id.ID { + entryMap := make(map[id.ID]bool) + newList := make([]*id.ID, 0) + for i, _ := range list { + if _, value := entryMap[*list[i]]; !value { + entryMap[*list[i]] = true + newList = append(newList, list[i]) + } + } + return newList +} diff --git a/backup/backup_test.go b/backup/backup_test.go new file mode 100644 index 0000000000000000000000000000000000000000..47a42c12764eb2c5579940b265a6f4c6291c3c26 --- /dev/null +++ b/backup/backup_test.go @@ -0,0 +1,443 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package backup + +import ( + "bytes" + "reflect" + "sort" + "strings" + "testing" + "time" + + "github.com/cloudflare/circl/dh/sidh" + "gitlab.com/elixxir/client/interfaces/params" + util "gitlab.com/elixxir/client/storage/utility" + "gitlab.com/elixxir/crypto/diffieHellman" + "gitlab.com/xx_network/primitives/id" + + "gitlab.com/elixxir/client/interfaces" + "gitlab.com/elixxir/client/storage" + "gitlab.com/elixxir/crypto/backup" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/xx_network/crypto/csprng" +) + +// Tests that Backup.initializeBackup returns a new Backup with a copy of the +// key and the callback. +func Test_initializeBackup(t *testing.T) { + cbChan := make(chan []byte, 2) + cb := func(encryptedBackup []byte) { cbChan <- encryptedBackup } + expectedPassword := "MySuperSecurePassword" + b, err := initializeBackup(expectedPassword, cb, nil, + storage.InitTestingSession(t), &interfaces.BackupContainer{}, + fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG)) + if err != nil { + t.Errorf("initializeBackup returned an error: %+v", err) + } + + select { + case <-cbChan: + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback.") + } + + // Check that the key, salt, and params were saved to storage + key, salt, _, err := loadBackup(b.store.GetKV()) + if err != nil { + t.Errorf("Failed to load key, salt, and params: %+v", err) + } + if len(key) != keyLen || bytes.Equal(key, make([]byte, keyLen)) { + t.Errorf("Invalid key: %v", key) + } + if len(salt) != saltLen || bytes.Equal(salt, make([]byte, saltLen)) { + t.Errorf("Invalid salt: %v", salt) + } + // if !reflect.DeepEqual(p, backup.DefaultParams()) { + // t.Errorf("Invalid params.\nexpected: %+v\nreceived: %+v", + // backup.DefaultParams(), p) + // } + + encryptedBackup := []byte("encryptedBackup") + go b.updateBackupCb(encryptedBackup) + + select { + case r := <-cbChan: + if !bytes.Equal(encryptedBackup, r) { + t.Errorf("Callback has unexepected data."+ + "\nexpected: %q\nreceived: %q", encryptedBackup, r) + } + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback.") + } +} + +// Initialises a new backup and then tests that Backup.resumeBackup overwrites +// the callback but keeps the password. +func Test_resumeBackup(t *testing.T) { + // Start the first backup + cbChan1 := make(chan []byte) + cb1 := func(encryptedBackup []byte) { cbChan1 <- encryptedBackup } + s := storage.InitTestingSession(t) + expectedPassword := "MySuperSecurePassword" + b, err := initializeBackup(expectedPassword, cb1, nil, s, + &interfaces.BackupContainer{}, + fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG)) + if err != nil { + t.Errorf("Failed to initialize new Backup: %+v", err) + } + + select { + case <-cbChan1: + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback.") + } + + // Get key and salt to compare to later + key1, salt1, _, err := loadBackup(b.store.GetKV()) + if err != nil { + t.Errorf("Failed to load key, salt, and params from newly "+ + "initialized backup: %+v", err) + } + + // Resume the backup with a new callback + cbChan2 := make(chan []byte) + cb2 := func(encryptedBackup []byte) { cbChan2 <- encryptedBackup } + b2, err := resumeBackup(cb2, nil, s, &interfaces.BackupContainer{}, + fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG)) + if err != nil { + t.Errorf("resumeBackup returned an error: %+v", err) + } + + // Get key, salt, and parameters of resumed backup + key2, salt2, _, err := loadBackup(b.store.GetKV()) + if err != nil { + t.Errorf("Failed to load key, salt, and params from resumed "+ + "backup: %+v", err) + } + + // Check that the loaded key and salt are the same + if !bytes.Equal(key1, key2) { + t.Errorf("New key does not match old key.\nold: %v\nnew: %v", key1, key2) + } + if !bytes.Equal(salt1, salt2) { + t.Errorf("New salt does not match old salt.\nold: %v\nnew: %v", salt1, salt2) + } + + encryptedBackup := []byte("encryptedBackup") + go b2.updateBackupCb(encryptedBackup) + + select { + case r := <-cbChan1: + t.Errorf("Callback of first Backup called: %q", r) + case r := <-cbChan2: + if !bytes.Equal(encryptedBackup, r) { + t.Errorf("Callback has unexepected data."+ + "\nexpected: %q\nreceived: %q", encryptedBackup, r) + } + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback.") + } +} + +// Error path: Tests that Backup.resumeBackup returns an error if no password is +// present in storage. +func Test_resumeBackup_NoKeyError(t *testing.T) { + expectedErr := "object not found" + s := storage.InitTestingSession(t) + _, err := resumeBackup(nil, nil, s, &interfaces.BackupContainer{}, nil) + if err == nil || !strings.Contains(err.Error(), expectedErr) { + t.Errorf("resumeBackup did not return the expected error when no "+ + "password is present.\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + +// Tests that Backup.TriggerBackup triggers the callback and that the data +// received can be decrypted. +func TestBackup_TriggerBackup(t *testing.T) { + cbChan := make(chan []byte) + cb := func(encryptedBackup []byte) { cbChan <- encryptedBackup } + password := "MySuperSecurePassword" + b := newTestBackup(password, cb, t) + + collatedBackup := b.assembleBackup() + + b.TriggerBackup("") + + select { + case r := <-cbChan: + receivedCollatedBackup := backup.Backup{} + err := receivedCollatedBackup.Decrypt(password, r) + if err != nil { + t.Errorf("Failed to decrypt collated backup: %+v", err) + } else if !reflect.DeepEqual(collatedBackup, receivedCollatedBackup) { + t.Errorf("Unexpected decrypted collated backup."+ + "\nexpected: %#v\nreceived: %#v", + collatedBackup, receivedCollatedBackup) + } + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback.") + } +} + +// Tests that Backup.TriggerBackup does not call the callback if there is no +// key, salt, and params in storage. +func TestBackup_TriggerBackup_NoKey(t *testing.T) { + cbChan := make(chan []byte) + cb := func(encryptedBackup []byte) { cbChan <- encryptedBackup } + b := newTestBackup("MySuperSecurePassword", cb, t) + select { + case <-cbChan: + case <-time.After(10 * time.Millisecond): + t.Errorf("backup not called") + } + + err := deleteBackup(b.store.GetKV()) + if err != nil { + t.Errorf("Failed to delete key, salt, and params: %+v", err) + } + + b.TriggerBackup("") + + select { + case r := <-cbChan: + t.Errorf("Callback received when it should not have been called: %q", r) + case <-time.After(10 * time.Millisecond): + } + +} + +// Tests that Backup.StopBackup prevents the callback from triggering and that +// the password, key, salt, and parameters were deleted. +func TestBackup_StopBackup(t *testing.T) { + cbChan := make(chan []byte) + cb := func(encryptedBackup []byte) { cbChan <- encryptedBackup } + b := newTestBackup("MySuperSecurePassword", cb, t) + select { + case <-cbChan: + case <-time.After(1000 * time.Millisecond): + t.Errorf("backup not called") + } + + err := b.StopBackup() + if err != nil { + t.Errorf("StopBackup returned an error: %+v", err) + } + + if b.updateBackupCb != nil { + t.Error("Callback not cleared.") + } + + b.TriggerBackup("") + + select { + case r := <-cbChan: + t.Errorf("Callback received when it should not have been called: %q", r) + case <-time.After(10 * time.Millisecond): + } + + // Make sure key, salt, and params are deleted + key, salt, p, err := loadBackup(b.store.GetKV()) + if err == nil || len(key) != 0 || len(salt) != 0 || p != (backup.Params{}) { + t.Errorf("Loaded key, salt, and params that should be deleted.") + } +} + +func TestBackup_IsBackupRunning(t *testing.T) { + cbChan := make(chan []byte) + cb := func(encryptedBackup []byte) { cbChan <- encryptedBackup } + b := newTestBackup("MySuperSecurePassword", cb, t) + + // Check that the backup is running after being initialized + if !b.IsBackupRunning() { + t.Error("Backup is not running after initialization.") + } + + // Stop the backup + err := b.StopBackup() + if err != nil { + t.Errorf("Failed to stop backup: %+v", err) + } + + // Check that the backup is stopped + if b.IsBackupRunning() { + t.Error("Backup is running after being stopped.") + } +} + +func TestBackup_AddJson(t *testing.T) { + b := newTestBackup("MySuperSecurePassword", nil, t) + s := b.store + json := "{'data': {'one': 1}}" + + expectedCollatedBackup := backup.Backup{ + RegistrationTimestamp: s.GetUser().RegistrationTimestamp, + TransmissionIdentity: backup.TransmissionIdentity{ + RSASigningPrivateKey: s.GetUser().TransmissionRSA, + RegistrarSignature: s.User().GetTransmissionRegistrationValidationSignature(), + Salt: s.GetUser().TransmissionSalt, + ComputedID: s.GetUser().TransmissionID, + }, + ReceptionIdentity: backup.ReceptionIdentity{ + RSASigningPrivateKey: s.GetUser().ReceptionRSA, + RegistrarSignature: s.User().GetReceptionRegistrationValidationSignature(), + Salt: s.GetUser().ReceptionSalt, + ComputedID: s.GetUser().ReceptionID, + DHPrivateKey: s.GetUser().E2eDhPrivateKey, + DHPublicKey: s.GetUser().E2eDhPublicKey, + }, + UserDiscoveryRegistration: backup.UserDiscoveryRegistration{ + FactList: s.GetUd().GetFacts(), + }, + Contacts: backup.Contacts{Identities: s.E2e().GetPartners()}, + JSONParams: json, + } + + b.AddJson(json) + + collatedBackup := b.assembleBackup() + if !reflect.DeepEqual(expectedCollatedBackup, collatedBackup) { + t.Errorf("Collated backup does not match expected."+ + "\nexpected: %+v\nreceived: %+v", + expectedCollatedBackup, collatedBackup) + } +} + +func TestBackup_AddJson_badJson(t *testing.T) { + b := newTestBackup("MySuperSecurePassword", nil, t) + s := b.store + json := "abc{'i'm a bad json: 'one': 1'''}}" + + expectedCollatedBackup := backup.Backup{ + RegistrationTimestamp: s.GetUser().RegistrationTimestamp, + TransmissionIdentity: backup.TransmissionIdentity{ + RSASigningPrivateKey: s.GetUser().TransmissionRSA, + RegistrarSignature: s.User().GetTransmissionRegistrationValidationSignature(), + Salt: s.GetUser().TransmissionSalt, + ComputedID: s.GetUser().TransmissionID, + }, + ReceptionIdentity: backup.ReceptionIdentity{ + RSASigningPrivateKey: s.GetUser().ReceptionRSA, + RegistrarSignature: s.User().GetReceptionRegistrationValidationSignature(), + Salt: s.GetUser().ReceptionSalt, + ComputedID: s.GetUser().ReceptionID, + DHPrivateKey: s.GetUser().E2eDhPrivateKey, + DHPublicKey: s.GetUser().E2eDhPublicKey, + }, + UserDiscoveryRegistration: backup.UserDiscoveryRegistration{ + FactList: s.GetUd().GetFacts(), + }, + Contacts: backup.Contacts{Identities: s.E2e().GetPartners()}, + JSONParams: json, + } + + b.AddJson(json) + + collatedBackup := b.assembleBackup() + if !reflect.DeepEqual(expectedCollatedBackup, collatedBackup) { + t.Errorf("Collated backup does not match expected."+ + "\nexpected: %+v\nreceived: %+v", + expectedCollatedBackup, collatedBackup) + } +} + +// Tests that Backup.assembleBackup returns the backup.Backup with the expected +// results. +func TestBackup_assembleBackup(t *testing.T) { + b := newTestBackup("MySuperSecurePassword", nil, t) + s := b.store + + rng := csprng.NewSystemRNG() + for i := 0; i < 10; i++ { + recipient, _ := id.NewRandomID(rng, id.User) + dhKey := s.E2e().GetGroup().NewInt(int64(i + 10)) + pubKey := diffieHellman.GeneratePublicKey(dhKey, s.E2e().GetGroup()) + _, mySidhPriv := util.GenerateSIDHKeyPair(sidh.KeyVariantSidhA, rng) + theirSidhPub, _ := util.GenerateSIDHKeyPair(sidh.KeyVariantSidhB, rng) + p := params.GetDefaultE2ESessionParams() + + err := s.E2e().AddPartner( + recipient, pubKey, dhKey, mySidhPriv, theirSidhPub, p, p) + if err != nil { + t.Errorf("Failed to add partner %s: %+v", recipient, err) + } + } + + expectedCollatedBackup := backup.Backup{ + RegistrationTimestamp: s.GetUser().RegistrationTimestamp, + TransmissionIdentity: backup.TransmissionIdentity{ + RSASigningPrivateKey: s.GetUser().TransmissionRSA, + RegistrarSignature: s.User().GetTransmissionRegistrationValidationSignature(), + Salt: s.GetUser().TransmissionSalt, + ComputedID: s.GetUser().TransmissionID, + }, + ReceptionIdentity: backup.ReceptionIdentity{ + RSASigningPrivateKey: s.GetUser().ReceptionRSA, + RegistrarSignature: s.User().GetReceptionRegistrationValidationSignature(), + Salt: s.GetUser().ReceptionSalt, + ComputedID: s.GetUser().ReceptionID, + DHPrivateKey: s.GetUser().E2eDhPrivateKey, + DHPublicKey: s.GetUser().E2eDhPublicKey, + }, + UserDiscoveryRegistration: backup.UserDiscoveryRegistration{ + FactList: s.GetUd().GetFacts(), + }, + Contacts: backup.Contacts{Identities: s.E2e().GetPartners()}, + } + + collatedBackup := b.assembleBackup() + + sort.Slice(expectedCollatedBackup.Contacts.Identities, func(i, j int) bool { + return bytes.Compare(expectedCollatedBackup.Contacts.Identities[i].Bytes(), + expectedCollatedBackup.Contacts.Identities[j].Bytes()) == -1 + }) + + sort.Slice(collatedBackup.Contacts.Identities, func(i, j int) bool { + return bytes.Compare(collatedBackup.Contacts.Identities[i].Bytes(), + collatedBackup.Contacts.Identities[j].Bytes()) == -1 + }) + + if !reflect.DeepEqual(expectedCollatedBackup, collatedBackup) { + t.Errorf("Collated backup does not match expected."+ + "\nexpected: %+v\nreceived: %+v", + expectedCollatedBackup, collatedBackup) + } +} + +// newTestBackup creates a new Backup for testing. +func newTestBackup(password string, cb UpdateBackupFn, t *testing.T) *Backup { + b, err := initializeBackup( + password, + cb, + nil, + storage.InitTestingSession(t), + &interfaces.BackupContainer{}, + fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG), + ) + if err != nil { + t.Fatalf("Failed to initialize backup: %+v", err) + } + + return b +} + +// Tests that Backup.InitializeBackup returns a new Backup with a copy of the +// key and the callback. +func Benchmark_InitializeBackup(t *testing.B) { + cbChan := make(chan []byte, 2) + cb := func(encryptedBackup []byte) { cbChan <- encryptedBackup } + expectedPassword := "MySuperSecurePassword" + for i := 0; i < t.N; i++ { + _, err := initializeBackup(expectedPassword, cb, nil, + storage.InitTestingSession(t), &interfaces.BackupContainer{}, + fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG)) + if err != nil { + t.Errorf("InitializeBackup returned an error: %+v", err) + } + } +} diff --git a/backup/jsonStorage.go b/backup/jsonStorage.go new file mode 100644 index 0000000000000000000000000000000000000000..8ce778b56aff9c76847caf1953451d7cb6b39d5d --- /dev/null +++ b/backup/jsonStorage.go @@ -0,0 +1,30 @@ +package backup + +import ( + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/xx_network/primitives/netTime" +) + +const ( + jsonStorageVersion = 0 + jsonStorageKey = "JsonStorage" +) + +func storeJson(json string, kv *versioned.KV) error { + obj := &versioned.Object{ + Version: jsonStorageVersion, + Timestamp: netTime.Now(), + Data: []byte(json), + } + + return kv.Set(jsonStorageKey, jsonStorageVersion, obj) +} + +func loadJson(kv *versioned.KV) string { + obj, err := kv.Get(jsonStorageKey, jsonStorageVersion) + if err != nil { + return "" + } + + return string(obj.Data) +} diff --git a/backup/jsonStorage_test.go b/backup/jsonStorage_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d0207d910d76ffd73b5dfa1ff623996bf14c916b --- /dev/null +++ b/backup/jsonStorage_test.go @@ -0,0 +1,22 @@ +package backup + +import ( + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/ekv" + "testing" +) + +func Test_storeJson_loadJson(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + json := "{'data': {'one': 1}}" + + err := storeJson(json, kv) + if err != nil { + t.Errorf("Failed to store JSON: %+v", err) + } + + loaded := loadJson(kv) + if loaded != json { + t.Errorf("Did not receive expected data from KV.\n\tExpected: %s, Received: %s\n", json, loaded) + } +} diff --git a/backup/keyStorage.go b/backup/keyStorage.go new file mode 100644 index 0000000000000000000000000000000000000000..6a06dc463fd83f49aae67733f38ccc62e0ab2940 --- /dev/null +++ b/backup/keyStorage.go @@ -0,0 +1,103 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package backup + +import ( + "bytes" + + "github.com/pkg/errors" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/crypto/backup" + "gitlab.com/xx_network/primitives/netTime" +) + +const ( + passwordStorageVersion = 0 + passwordStorageKey = "BackupPassword" + cryptoStorageVersion = 0 + cryptoStorageKey = "BackupCryptoInfo" +) + +// Length of marshalled fields. +const ( + keyLen = backup.KeyLen + saltLen = backup.SaltLen + paramsLen = backup.ParamsLen +) + +// saveBackup saves the key, salt, and params to storage. +func saveBackup(key, salt []byte, params backup.Params, kv *versioned.KV) error { + + obj := &versioned.Object{ + Version: cryptoStorageVersion, + Timestamp: netTime.Now(), + Data: marshalBackup(key, salt, params), + } + + return kv.Set(cryptoStorageKey, cryptoStorageVersion, obj) +} + +// loadBackup loads the key, salt, and params from storage. +func loadBackup(kv *versioned.KV) (key, salt []byte, params backup.Params, err error) { + obj, err := kv.Get(cryptoStorageKey, cryptoStorageVersion) + if err != nil { + return + } + + return unmarshalBackup(obj.Data) +} + +// deleteBackup deletes the key, salt, and params from storage. +func deleteBackup(kv *versioned.KV) error { + return kv.Delete(cryptoStorageKey, cryptoStorageVersion) +} + +// marshalBackup marshals the backup's key, salt, and params into a byte slice. +func marshalBackup(key, salt []byte, params backup.Params) []byte { + buff := bytes.NewBuffer(nil) + buff.Grow(keyLen + saltLen + paramsLen) + + // Write key to buffer + buff.Write(key) + + // Write salt to buffer + buff.Write(salt) + + // Write marshalled params to buffer + buff.Write(params.Marshal()) + + return buff.Bytes() +} + +// unmarshalBackup unmarshalls the byte slice into a key, salt, and params. +func unmarshalBackup(buf []byte) (key, salt []byte, params backup.Params, err error) { + buff := bytes.NewBuffer(buf) + // Get key + key = make([]byte, keyLen) + n, err := buff.Read(key) + if err != nil || n != keyLen { + err = errors.Errorf("reading key failed: %+v", err) + return + } + + // Get salt + salt = make([]byte, saltLen) + n, err = buff.Read(salt) + if err != nil || n != saltLen { + err = errors.Errorf("reading salt failed: %+v", err) + return + } + + // Get params from remaining bytes + err = params.Unmarshal(buff.Bytes()) + if err != nil { + err = errors.Errorf("reading params failed: %+v", err) + } + + return +} diff --git a/bindings/authenticatedChannels.go b/bindings/authenticatedChannels.go index 36888a604177ae36b37d80f97ad61b01700cb51a..af246b5408736224ec081ae7e45806b25b4a0a60 100644 --- a/bindings/authenticatedChannels.go +++ b/bindings/authenticatedChannels.go @@ -8,8 +8,8 @@ package bindings import ( - "errors" "fmt" + "gitlab.com/elixxir/crypto/contact" "gitlab.com/xx_network/primitives/id" ) @@ -18,8 +18,8 @@ import ( func (c *Client) MakePrecannedAuthenticatedChannel(precannedID int) (*Contact, error) { precannedContact, err := c.api.MakePrecannedAuthenticatedChannel(uint(precannedID)) if err != nil { - return nil, errors.New(fmt.Sprintf("Failed to "+ - "MakePrecannedAuthenticatedChannel: %+v", err)) + return nil, fmt.Errorf("Failed to "+ + "MakePrecannedAuthenticatedChannel: %+v", err) } return &Contact{c: &precannedContact}, nil } @@ -41,16 +41,16 @@ func (c *Client) RequestAuthenticatedChannel(recipientMarshaled, recipent, err := contact.Unmarshal(recipientMarshaled) if err != nil { - return 0, errors.New(fmt.Sprintf("Failed to "+ + return 0, fmt.Errorf("Failed to "+ "RequestAuthenticatedChannel: Failed to Unmarshal Recipent: "+ - "%+v", err)) + "%+v", err) } me, err := contact.Unmarshal(meMarshaled) if err != nil { - return 0, errors.New(fmt.Sprintf("Failed to "+ - "RequestAuthenticatedChannel: Failed to Unmarshal Me: %+v", err)) + return 0, fmt.Errorf("Failed to "+ + "RequestAuthenticatedChannel: Failed to Unmarshal Me: %+v", err) } rid, err := c.api.RequestAuthenticatedChannel(recipent, me, message) @@ -58,16 +58,44 @@ func (c *Client) RequestAuthenticatedChannel(recipientMarshaled, return int(rid), err } -// RegisterAuthCallbacks registers both callbacks for authenticated channels. +// ResetSession resets an authenticated channel that already exists +func (c *Client) ResetSession(recipientMarshaled, + meMarshaled []byte, message string) (int, error) { + recipent, err := contact.Unmarshal(recipientMarshaled) + + if err != nil { + return 0, fmt.Errorf("failed to "+ + "ResetSession: failed to Unmarshal Recipent: "+ + "%+v", err) + } + + me, err := contact.Unmarshal(meMarshaled) + + if err != nil { + return 0, fmt.Errorf("failed to "+ + "ResetSession: Failed to Unmarshal Me: %+v", err) + } + + rid, err := c.api.ResetSession(recipent, me, message) + + return int(rid), err +} + +// RegisterAuthCallbacks registers all callbacks for authenticated channels. // This can only be called once func (c *Client) RegisterAuthCallbacks(request AuthRequestCallback, - confirm AuthConfirmCallback) { + confirm AuthConfirmCallback, reset AuthResetNotificationCallback) { requestFunc := func(requestor contact.Contact) { requestorBind := &Contact{c: &requestor} request.Callback(requestorBind) } + resetFunc := func(resetor contact.Contact) { + resetorBind := &Contact{c: &resetor} + reset.Callback(resetorBind) + } + confirmFunc := func(partner contact.Contact) { partnerBind := &Contact{c: &partner} confirm.Callback(partnerBind) @@ -75,8 +103,7 @@ func (c *Client) RegisterAuthCallbacks(request AuthRequestCallback, c.api.GetAuthRegistrar().AddGeneralConfirmCallback(confirmFunc) c.api.GetAuthRegistrar().AddGeneralRequestCallback(requestFunc) - - return + c.api.GetAuthRegistrar().AddResetNotificationCallback(resetFunc) } // ConfirmAuthenticatedChannel creates an authenticated channel out of a valid @@ -95,9 +122,9 @@ func (c *Client) ConfirmAuthenticatedChannel(recipientMarshaled []byte) (int, er recipent, err := contact.Unmarshal(recipientMarshaled) if err != nil { - return 0, errors.New(fmt.Sprintf("Failed to "+ + return 0, fmt.Errorf("Failed to "+ "ConfirmAuthenticatedChannel: Failed to Unmarshal Recipient: "+ - "%+v", err)) + "%+v", err) } rid, err := c.api.ConfirmAuthenticatedChannel(recipent) @@ -111,15 +138,15 @@ func (c *Client) VerifyOwnership(receivedMarshaled, verifiedMarshaled []byte) (b received, err := contact.Unmarshal(receivedMarshaled) if err != nil { - return false, errors.New(fmt.Sprintf("Failed to "+ - "VerifyOwnership: Failed to Unmarshal Received: %+v", err)) + return false, fmt.Errorf("Failed to "+ + "VerifyOwnership: Failed to Unmarshal Received: %+v", err) } verified, err := contact.Unmarshal(verifiedMarshaled) if err != nil { - return false, errors.New(fmt.Sprintf("Failed to "+ - "VerifyOwnership: Failed to Unmarshal Verified: %+v", err)) + return false, fmt.Errorf("Failed to "+ + "VerifyOwnership: Failed to Unmarshal Verified: %+v", err) } return c.api.VerifyOwnership(received, verified), nil @@ -138,6 +165,6 @@ func (c *Client) GetRelationshipFingerprint(partnerID []byte) (string, error) { } // ReplayRequests Resends all pending requests over the normal callbacks -func (c *Client) ReplayRequests() () { +func (c *Client) ReplayRequests() { c.api.GetAuthRegistrar().ReplayRequests() -} \ No newline at end of file +} diff --git a/bindings/backup.go b/bindings/backup.go new file mode 100644 index 0000000000000000000000000000000000000000..955839e8cf326ecda83e36fd35f2ba68fdb657f9 --- /dev/null +++ b/bindings/backup.go @@ -0,0 +1,72 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package bindings + +import ( + "gitlab.com/elixxir/client/backup" +) + +type Backup struct { + b *backup.Backup +} + +// UpdateBackupFunc contains a function callback that returns new backups. +type UpdateBackupFunc interface { + UpdateBackup(encryptedBackup []byte) +} + +// InitializeBackup starts the backup processes that returns backup updates when +// they occur. Any time an event occurs that changes the contents of the backup, +// such as adding or deleting a contact, the backup is triggered and an +// encrypted backup is generated and returned on the updateBackupCb callback. +// Call this function only when enabling backup if it has not already been +// initialized or when the user wants to change their password. +// To resume backup process on app recovery, use ResumeBackup. +func InitializeBackup( + password string, updateBackupCb UpdateBackupFunc, c *Client) (*Backup, error) { + b, err := backup.InitializeBackup( + password, updateBackupCb.UpdateBackup, &c.api) + if err != nil { + return nil, err + } + + return &Backup{b}, nil +} + +// ResumeBackup starts the backup processes back up with a new callback after it +// has been initialized. +// Call this function only when resuming a backup that has already been +// initialized or to replace the callback. +// To start the backup for the first time or to use a new password, use +// InitializeBackup. +func ResumeBackup(cb UpdateBackupFunc, c *Client) ( + *Backup, error) { + b, err := backup.ResumeBackup(cb.UpdateBackup, &c.api) + if err != nil { + return nil, err + } + + return &Backup{b}, nil +} + +// StopBackup stops the backup processes and deletes the user's password from +// storage. To enable backups again, call InitializeBackup. +func (b *Backup) StopBackup() error { + return b.b.StopBackup() +} + +// IsBackupRunning returns true if the backup has been initialized and is +// running. Returns false if it has been stopped. +func (b *Backup) IsBackupRunning() bool { + return b.b.IsBackupRunning() +} + +// AddJson stores a passed in json string in the backup structure +func (b *Backup) AddJson(json string) { + b.b.AddJson(json) +} diff --git a/bindings/callback.go b/bindings/callback.go index fc64afe758e1c9a410f07503d473de69adc7de4d..4ad87c94ae91e1dc5d688900d75d0b404f9a1765 100644 --- a/bindings/callback.go +++ b/bindings/callback.go @@ -60,6 +60,12 @@ type AuthConfirmCallback interface { Callback(partner *Contact) } +// AuthRequestCallback notifies the register whenever they receive an auth +// request +type AuthResetNotificationCallback interface { + Callback(requestor *Contact) +} + // Generic Unregister - a generic return used for all callbacks which can be // unregistered // Interface which allows the un-registration of a listener diff --git a/bindings/client.go b/bindings/client.go index 1996507cbc0a005ae20006763f6596fe1ca25834..edda4a31dfa78239b4f7593f0a3db71af0a2021d 100644 --- a/bindings/client.go +++ b/bindings/client.go @@ -10,8 +10,14 @@ package bindings import ( "bytes" "encoding/csv" + "encoding/json" "errors" "fmt" + "runtime/pprof" + "strings" + "sync" + "time" + jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/api" "gitlab.com/elixxir/client/interfaces/message" @@ -23,13 +29,9 @@ import ( "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/netTime" "google.golang.org/grpc/grpclog" - "runtime/pprof" - "strings" - "sync" - "time" ) -var extantClient bool +var extantClient bool = false var loginMux sync.Mutex var clientSingleton *Client @@ -81,6 +83,32 @@ func NewPrecannedClient(precannedID int, network, storageDir string, password [] return nil } +type BackupReport struct { + RestoredContacts []*id.ID + Params string +} + +// NewClientFromBackup constructs a new Client from an encrypted backup. The backup +// is decrypted using the backupPassphrase. On success a successful client creation, +// the function will return a JSON encoded list of the E2E partners +// contained in the backup and a json-encoded string of the parameters stored in the backup +func NewClientFromBackup(ndfJSON, storageDir string, sessionPassword, + backupPassphrase, backupFileContents []byte) ([]byte, error) { + backupPartnerIds, jsonParams, err := api.NewClientFromBackup(ndfJSON, storageDir, + sessionPassword, backupPassphrase, backupFileContents) + if err != nil { + return nil, errors.New(fmt.Sprintf("Failed to create new "+ + "client from backup: %+v", err)) + } + + report := BackupReport{ + RestoredContacts: backupPartnerIds, + Params: jsonParams, + } + + return json.Marshal(report) +} + // Login will load an existing client from the storageDir // using the password. This will fail if the client doesn't exist or // the password is incorrect. @@ -450,6 +478,19 @@ func (c *Client) GetNodeRegistrationStatus() (*NodeRegistrationsStatus, error) { return &NodeRegistrationsStatus{registered, total}, err } +// DeleteRequest will delete a request, agnostic of request type +// for the given partner ID. If no request exists for this +// partner ID an error will be returned. +func (c *Client) DeleteRequest(requesterUserId []byte) error { + requesterId, err := id.Unmarshal(requesterUserId) + if err != nil { + return err + } + + jww.DEBUG.Printf("Deleting request for partner ID: %s", requesterId) + return c.api.DeleteRequest(requesterId) +} + // DeleteAllRequests clears all requests from Client's auth storage. func (c *Client) DeleteAllRequests() error { return c.api.DeleteAllRequests() @@ -555,6 +596,16 @@ func (c *Client) getSingle() (*single.Manager, error) { return c.single, nil } +// GetInternalClient returns a reference to the client api. This is for internal +// use only and should not be called by bindings clients. +func (c *Client) GetInternalClient() api.Client { + return c.api +} + +func WrapAPIClient(c *api.Client) *Client { + return &Client{api: *c} +} + // DumpStack returns a string with the stack trace of every running thread. func DumpStack() (string, error) { buf := new(bytes.Buffer) diff --git a/bindings/contact.go b/bindings/contact.go index 47a7a7290e4fec3684523d54312d359643fc3dbb..c02aa2cbfd8b276aa399ee1f7059b0564b7929ba 100644 --- a/bindings/contact.go +++ b/bindings/contact.go @@ -73,3 +73,8 @@ func (c *Contact) GetFactList() *FactList { func (c *Contact) Marshal() ([]byte, error) { return c.c.Marshal(), nil } + +// GetAPIContact returns the api contact object. Not exported to bindings. +func (c *Contact) GetAPIContact() *contact.Contact { + return c.c +} diff --git a/bindings/fileTransfer.go b/bindings/fileTransfer.go index 099cecc8496f254704267cb1c286b34a3355361b..74d911483de71b990ae05ee5b3b0a281ecc528f1 100644 --- a/bindings/fileTransfer.go +++ b/bindings/fileTransfer.go @@ -163,12 +163,13 @@ func (f *FileTransfer) RegisterSendProgressCallback(transferID []byte, // Resend resends a file if sending fails. This function should only be called // if the interfaces.SentProgressCallback returns an error. -func (f *FileTransfer) Resend(transferID []byte) error { +// Resend is not currently implemented. +/*func (f *FileTransfer) Resend(transferID []byte) error { // Unmarshal transfer ID tid := ftCrypto.UnmarshalTransferID(transferID) return f.m.Resend(tid) -} +}*/ // CloseSend deletes a sent file transfer from the sent transfer map and from // storage once a transfer has completed or reached the retry limit. Returns an diff --git a/bindings/notifications.go b/bindings/notifications.go index 7f393cf7271c253cf5e07771f9f570d89b69a3de..57972fbe45babeda42022a2cdd7751f0efc3c179 100644 --- a/bindings/notifications.go +++ b/bindings/notifications.go @@ -54,6 +54,7 @@ func (mnfmr *ManyNotificationForMeReport) Len() int { // TYPE SOURCE DESCRIPTION // "default" recipient user ID A message with no association // "request" sender user ID A channel request has been received +// "reset" sender user ID A channel reset has been received // "confirm" sender user ID A channel request has been accepted // "silent" sender user ID A message which should not be notified on // "e2e" sender user ID reception of an E2E message diff --git a/bindings/restoreContacts.go b/bindings/restoreContacts.go new file mode 100644 index 0000000000000000000000000000000000000000..aeead16fd7a7c94e4e4148d1291f1b6404f0a690 --- /dev/null +++ b/bindings/restoreContacts.go @@ -0,0 +1,111 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package bindings + +import ( + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/xxmutils" + "gitlab.com/elixxir/crypto/contact" + "gitlab.com/xx_network/primitives/id" +) + +// RestoreContactsUpdater interface provides a callback function +// for receiving update information from RestoreContactsFromBackup. +type RestoreContactsUpdater interface { + // RestoreContactsCallback is called to report the current # of contacts + // that have been found and how many have been restored + // against the total number that need to be + // processed. If an error occurs it it set on the err variable as a + // plain string. + RestoreContactsCallback(numFound, numRestored, total int, err string) +} + +// RestoreContactsReport is a gomobile friendly report structure +// for determining which IDs restored, which failed, and why. +type RestoreContactsReport struct { + restored []*id.ID + failed []*id.ID + errs []error + restErr error +} + +// LenRestored returns the length of ID's restored. +func (r *RestoreContactsReport) LenRestored() int { + return len(r.restored) +} + +// LenFailed returns the length of the ID's failed. +func (r *RestoreContactsReport) LenFailed() int { + return len(r.failed) +} + +// GetRestoredAt returns the restored ID at index +func (r *RestoreContactsReport) GetRestoredAt(index int) []byte { + return r.restored[index].Bytes() +} + +// GetFailedAt returns the failed ID at index +func (r *RestoreContactsReport) GetFailedAt(index int) []byte { + return r.failed[index].Bytes() +} + +// GetErrorAt returns the error string at index +func (r *RestoreContactsReport) GetErrorAt(index int) string { + return r.errs[index].Error() +} + +// GetRestoreContactsError returns an error string. Empty if no error. +func (r *RestoreContactsReport) GetRestoreContactsError() string { + if r.restErr == nil { + return "" + } + return r.restErr.Error() +} + +// RestoreContactsFromBackup takes as input the jason output of the +// `NewClientFromBackup` function, unmarshals it into IDs, looks up +// each ID in user discovery, and initiates a session reset request. +// This function will not return until every id in the list has been sent a +// request. It should be called again and again until it completes. +// xxDK users should not use this function. This function is used by +// the mobile phone apps and are not intended to be part of the xxDK. It +// should be treated as internal functions specific to the phone apps. +func RestoreContactsFromBackup(backupPartnerIDs []byte, client *Client, + udManager *UserDiscovery, lookupCB LookupCallback, + updatesCb RestoreContactsUpdater) *RestoreContactsReport { + + extLookupCB := func(c contact.Contact, myErr error) { + jww.INFO.Printf("extLookupCB triggered: %v, %v", c, myErr) + bindingsContact := &Contact{c: &c} + errStr := "" + if myErr != nil { + jww.WARN.Printf("restore err on lookup: %+v", + myErr) + errStr = myErr.Error() + } + if lookupCB != nil { + jww.INFO.Printf("Calling lookupCB(%+v, %+v)", + bindingsContact, errStr) + lookupCB.Callback(bindingsContact, errStr) + } else { + jww.WARN.Printf("nil external lookup callback") + } + } + + restored, failed, errs, err := xxmutils.RestoreContactsFromBackup( + backupPartnerIDs, &client.api, udManager.ud, extLookupCB, + updatesCb) + + return &RestoreContactsReport{ + restored: restored, + failed: failed, + errs: errs, + restErr: err, + } + +} diff --git a/bindings/ud.go b/bindings/ud.go index f9a12eb39972f94743a833e9ec4e081139b5ac40..cc3c5164f761469acbc60c07d5e876950bd4f6d6 100644 --- a/bindings/ud.go +++ b/bindings/ud.go @@ -9,12 +9,13 @@ package bindings import ( "fmt" + "time" + "github.com/pkg/errors" "gitlab.com/elixxir/client/ud" "gitlab.com/elixxir/crypto/contact" "gitlab.com/elixxir/primitives/fact" "gitlab.com/xx_network/primitives/id" - "time" ) // This package wraps the user discovery system @@ -45,6 +46,55 @@ func NewUserDiscovery(client *Client) (*UserDiscovery, error) { } } +// NewUserDiscoveryFromBackup returns a new user discovery object. It +// wil set up the manager with the backup data. Pass into it the backed up +// facts, one email and phone number each. This will add the registered facts +// to the backed Store. Any one of these fields may be empty, +// however both fields being empty will cause an error. Any other fact that is not +// an email or phone number will return an error. You may only add a fact for the +// accepted types once each. If you attempt to back up a fact type that has already +// been backed up, an error will be returned. Anytime an error is returned, it means +// the backup was not successful. +// NOTE: Do not use this as a direct store operation. This feature is intended to add facts +// to a backend store that have ALREADY BEEN REGISTERED on the account. +// THIS IS NOT FOR ADDING NEWLY REGISTERED FACTS. That is handled on the backend. +// Only call this once. It must be called after StartNetworkFollower +// is called and will fail if the network has never been contacted. +// This function technically has a memory leak because it causes both sides of +// the bindings to think the other is in charge of the client object. +// In general this is not an issue because the client object should exist +// for the life of the program. +// This must be called while start network follower is running. +func NewUserDiscoveryFromBackup(client *Client, + email, phone string) (*UserDiscovery, error) { + single, err := client.getSingle() + if err != nil { + return nil, errors.WithMessage(err, "Failed to create User Discovery Manager") + } + + var emailFact, phoneFact fact.Fact + if len(email) > 2 { + emailFact, err = fact.UnstringifyFact(email) + if err != nil { + return nil, errors.WithMessagef(err, "Failed to parse malformed email fact: %s", email) + } + } + + if len(phone) > 2 { + phoneFact, err = fact.UnstringifyFact(phone) + if err != nil { + return nil, errors.WithMessagef(err, "Failed to parse malformed phone fact: %s", phone) + } + } + + m, err := ud.NewManagerFromBackup(&client.api, single, emailFact, phoneFact) + if err != nil { + return nil, errors.WithMessage(err, "Failed to create User Discovery Manager") + } else { + return &UserDiscovery{ud: m}, nil + } +} + // Register registers a user with user discovery. Will return an error if the // network signatures are malformed or if the username is taken. Usernames // cannot be changed after registration at this time. Will fail if the user is @@ -307,3 +357,7 @@ func (ud *UserDiscovery) SetAlternativeUserDiscovery(address, cert, contactFile func (ud *UserDiscovery) UnsetAlternativeUserDiscovery() error { return ud.ud.UnsetAlternativeUserDiscovery() } + +func WrapUserDiscovery(ud *ud.Manager) *UserDiscovery { + return &UserDiscovery{ud: ud} +} diff --git a/bindings/url.go b/bindings/url.go index 0322b6c3dd05318bbce6a1d9dee8ed9caeebf7da..f561540f3ecf89fb1b67e139549208884fe46779 100644 --- a/bindings/url.go +++ b/bindings/url.go @@ -7,10 +7,9 @@ package bindings - import ( - "gitlab.com/xx_network/primitives/id" "fmt" + "gitlab.com/xx_network/primitives/id" ) const dashboardBaseURL = "https://dashboard.xx.network" diff --git a/bindings/user.go b/bindings/user.go index 722fe6af8dd48a3c80ef102750a3110ec48ff54d..f05245daabd7a71b23468424d89d9662216574c6 100644 --- a/bindings/user.go +++ b/bindings/user.go @@ -52,14 +52,6 @@ func (u *User) IsPrecanned() bool { return u.u.Precanned } -func (u *User) GetCmixDhPrivateKey() []byte { - return u.u.CmixDhPrivateKey.Bytes() -} - -func (u *User) GetCmixDhPublicKey() []byte { - return u.u.CmixDhPublicKey.Bytes() -} - func (u *User) GetE2EDhPrivateKey() []byte { return u.u.E2eDhPrivateKey.Bytes() } diff --git a/cmd/fileTransfer.go b/cmd/fileTransfer.go index 932abff2a8cc1a99574fb4128c6762ba6f84a535..6892a9cb9538673ee7351bcd0c74ed439d11bbf1 100644 --- a/cmd/fileTransfer.go +++ b/cmd/fileTransfer.go @@ -18,6 +18,7 @@ import ( "gitlab.com/elixxir/crypto/contact" ftCrypto "gitlab.com/elixxir/crypto/fileTransfer" "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/netTime" "gitlab.com/xx_network/primitives/utils" "io/ioutil" "time" @@ -90,17 +91,13 @@ var ftCmd = &cobra.Command{ // Wait until either the file finishes sending or the file finishes // being received, stop the receiving thread, and exit - for done := false; !done; { - select { - case <-sendDone: - jww.INFO.Printf("[FT] Finished sending file. Stopping " + - "threads and network follower.") - done = true - case <-receiveDone: - jww.INFO.Printf("[FT] Finished receiving file. Stopping " + - "threads and network follower.") - done = true - } + select { + case <-sendDone: + jww.INFO.Printf("[FT] Finished sending file. Stopping threads " + + "and network follower.") + case <-receiveDone: + jww.INFO.Printf("[FT] Finished receiving file. Stopping threads " + + "and network follower.") } // Stop reception thread @@ -200,6 +197,8 @@ func sendFile(filePath, fileType, filePreviewPath, filePreviewString, fileName, recipient.ID, fileType, len(fileData), retry, filePath, filePreviewPath, filePreviewData) + var sendStart time.Time + // Create sent progress callback that prints the results progressCB := func(completed bool, sent, arrived, total uint16, t interfaces.FilePartTracker, err error) { @@ -214,14 +213,24 @@ func sendFile(filePath, fileType, filePreviewPath, filePreviewString, } if completed { + fileSize := len(fileData) + sendTime := netTime.Since(sendStart) + fileSizeKb := float32(fileSize) * .001 + speed := fileSizeKb * float32(time.Second) / (float32(sendTime)) + jww.INFO.Printf("[FT] Completed sending file %q in %s (%.2f kb @ %.2f kb/s).", + fileName, sendTime, fileSizeKb, speed) fmt.Printf("Completed sending file.\n") done <- struct{}{} } else if err != nil { + jww.ERROR.Printf("[FT] Failed sending file %q in %s: %+v", + fileName, netTime.Since(sendStart), err) fmt.Printf("Failed sending file: %+v\n", err) done <- struct{}{} } } + sendStart = netTime.Now() + // Send the file tid, err := m.Send(fileName, fileType, fileData, recipient.ID, retry, filePreviewData, progressCB, callbackPeriod) @@ -248,13 +257,14 @@ func receiveNewFileTransfers(receive chan receivedFtResults, done, "E2E message.") return case r := <-receive: + receiveStart := netTime.Now() jww.INFO.Printf("[FT] Received new file %q transfer %s of type "+ "%q from %s of size %d bytes with preview: %q", r.fileName, r.tid, r.fileType, r.sender, r.size, r.preview) fmt.Printf("Received new file transfer %q of size %d "+ "bytes with preview: %q\n", r.fileName, r.size, r.preview) - cb := newReceiveProgressCB(r.tid, done, m) + cb := newReceiveProgressCB(r.tid, r.fileName, done, receiveStart, m) err := m.RegisterReceivedProgressCallback(r.tid, cb, callbackPeriod) if err != nil { jww.FATAL.Panicf("[FT] Failed to register new receive "+ @@ -266,7 +276,8 @@ func receiveNewFileTransfers(receive chan receivedFtResults, done, // newReceiveProgressCB creates a new reception progress callback that prints // the results to the log. -func newReceiveProgressCB(tid ftCrypto.TransferID, done chan struct{}, +func newReceiveProgressCB(tid ftCrypto.TransferID, fileName string, + done chan struct{}, receiveStart time.Time, m *ft.Manager) interfaces.ReceivedProgressCallback { return func(completed bool, received, total uint16, t interfaces.FilePartTracker, err error) { @@ -286,9 +297,13 @@ func newReceiveProgressCB(tid ftCrypto.TransferID, done chan struct{}, jww.FATAL.Panicf( "[FT] Failed to receive file %s: %+v", tid, err) } + jww.INFO.Printf("[FT] Completed receiving file %q in %s.", + fileName, netTime.Since(receiveStart)) fmt.Printf("Completed receiving file:\n%s\n", receivedFile) done <- struct{}{} } else if err != nil { + jww.INFO.Printf("[FT] Failed receiving file %q in %s.", + fileName, netTime.Since(receiveStart)) fmt.Printf("Failed sending file: %+v\n", err) done <- struct{}{} } diff --git a/cmd/root.go b/cmd/root.go index c9f094a1883304107ea6cbdf5cd7d0c51fc6a7a4..d7bb3d6f5c65399751fb00a454752cc103c70902 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,26 +12,30 @@ import ( "encoding/base64" "encoding/binary" "encoding/hex" + "encoding/json" "fmt" + "io/ioutil" + "log" + "os" + "runtime/pprof" + "strconv" + "strings" + "sync" + "time" + "github.com/spf13/cobra" jww "github.com/spf13/jwalterweatherman" "github.com/spf13/viper" "gitlab.com/elixxir/client/api" + "gitlab.com/elixxir/client/backup" "gitlab.com/elixxir/client/interfaces/message" "gitlab.com/elixxir/client/interfaces/params" "gitlab.com/elixxir/client/switchboard" + backupCrypto "gitlab.com/elixxir/crypto/backup" "gitlab.com/elixxir/crypto/contact" "gitlab.com/elixxir/primitives/excludedRounds" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/utils" - "io/ioutil" - "log" - "os" - "runtime/pprof" - "strconv" - "strings" - "sync" - "time" ) // Deployment environment constants for the download-ndf code path @@ -247,6 +251,8 @@ var rootCmd = &cobra.Command{ numReg, total) } + client.GetBackup().TriggerBackup("Integration test.") + // Send Messages msgBody := viper.GetString("message") @@ -278,6 +284,12 @@ var rootCmd = &cobra.Command{ addPrecanAuthenticatedChannel(client, recipientID, recipientContact) authConfirmed = true + } else if !unsafe && authConfirmed && !isPrecanPartner && + sendAuthReq { + jww.WARN.Printf("Resetting negotiated auth channel") + resetAuthenticatedChannel(client, recipientID, + recipientContact) + authConfirmed = false } if !unsafe && !authConfirmed { @@ -316,6 +328,10 @@ var rootCmd = &cobra.Command{ client.DeleteAllRequests() } + if viper.GetBool("delete-request") { + client.DeleteRequest(recipientID) + } + msg := message.Send{ Recipient: recipientID, Payload: []byte(msgBody), @@ -428,7 +444,12 @@ var rootCmd = &cobra.Command{ // wait an extra 5 seconds to make sure no messages were missed done = false - timer := time.NewTimer(5 * time.Second) + waitTime := time.Duration(5 * time.Second) + if expectedCnt == 0 { + // Wait longer if we didn't expect to receive anything + waitTime = time.Duration(15 * time.Second) + } + timer := time.NewTimer(waitTime) for !done { select { case <-timer.C: @@ -499,57 +520,19 @@ func initClientCallbacks(client *api.Client) (chan *id.ID, return authConfirmed, recvCh } -// Helper function which prints the round resuls -func printRoundResults(allRoundsSucceeded, timedOut bool, - rounds map[id.Round]api.RoundResult, roundIDs []id.Round, msg message.Send) { - - // Done as string slices for easy and human readable printing - successfulRounds := make([]string, 0) - failedRounds := make([]string, 0) - timedOutRounds := make([]string, 0) - - for _, r := range roundIDs { - // Group all round reports into a category based on their - // result (successful, failed, or timed out) - if result, exists := rounds[r]; exists { - if result == api.Succeeded { - successfulRounds = append(successfulRounds, strconv.Itoa(int(r))) - } else if result == api.Failed { - failedRounds = append(failedRounds, strconv.Itoa(int(r))) - } else { - timedOutRounds = append(timedOutRounds, strconv.Itoa(int(r))) - } - } - } - - jww.INFO.Printf("Result of sending message \"%s\" to \"%v\":", - msg.Payload, msg.Recipient) - - // Print out all rounds results, if they are populated - if len(successfulRounds) > 0 { - jww.INFO.Printf("\tRound(s) %v successful", strings.Join(successfulRounds, ",")) - } - if len(failedRounds) > 0 { - jww.ERROR.Printf("\tRound(s) %v failed", strings.Join(failedRounds, ",")) - } - if len(timedOutRounds) > 0 { - jww.ERROR.Printf("\tRound(s) %v timed "+ - "\n\tout (no network resolution could be found)", strings.Join(timedOutRounds, ",")) - } - -} - func createClient() *api.Client { logLevel := viper.GetUint("logLevel") initLog(logLevel, viper.GetString("log")) jww.INFO.Printf(Version()) - pass := viper.GetString("password") + pass := parsePassword(viper.GetString("password")) storeDir := viper.GetString("session") regCode := viper.GetString("regcode") precannedID := viper.GetUint("sendid") userIDprefix := viper.GetString("userid-prefix") protoUserPath := viper.GetString("protoUserPath") + backupPath := viper.GetString("backupIn") + backupPass := []byte(viper.GetString("backupPass")) // create a new client if none exist if _, err := os.Stat(storeDir); os.IsNotExist(err) { @@ -561,20 +544,56 @@ func createClient() *api.Client { if precannedID != 0 { err = api.NewPrecannedClient(precannedID, - string(ndfJSON), storeDir, []byte(pass)) + string(ndfJSON), storeDir, pass) } else if protoUserPath != "" { protoUserJson, err := utils.ReadFile(protoUserPath) if err != nil { jww.FATAL.Panicf("%v", err) } err = api.NewProtoClient_Unsafe(string(ndfJSON), storeDir, - []byte(pass), protoUserJson) + pass, protoUserJson) } else if userIDprefix != "" { err = api.NewVanityClient(string(ndfJSON), storeDir, - []byte(pass), regCode, userIDprefix) + pass, regCode, userIDprefix) + } else if backupPath != "" { + + b, backupFile := loadBackup(backupPath, string(backupPass)) + + // Marshal the backup object in JSON + backupJson, err := json.Marshal(b) + if err != nil { + jww.ERROR.Printf("Failed to JSON Marshal backup: %+v", err) + } + + // Write the backup JSON to file + err = utils.WriteFileDef(viper.GetString("backupJsonOut"), backupJson) + if err != nil { + jww.FATAL.Panicf("Failed to write backup to file: %+v", err) + } + + // Construct client from backup data + backupIdList, _, err := api.NewClientFromBackup(string(ndfJSON), storeDir, + pass, backupPass, backupFile) + + backupIdListPath := viper.GetString("backupIdList") + if backupIdListPath != "" { + // Marshal backed up ID list to JSON + backedUpIdListJson, err := json.Marshal(backupIdList) + if err != nil { + jww.ERROR.Printf("Failed to JSON Marshal backed up IDs: %+v", err) + } + + // Write backed up ID list to file + err = utils.WriteFileDef(backupIdListPath, backedUpIdListJson) + if err != nil { + jww.FATAL.Panicf("Failed to write backed up IDs to file %q: %+v", + backupIdListPath, err) + } + } + } else { err = api.NewClient(string(ndfJSON), storeDir, - []byte(pass), regCode) + pass, regCode) } if err != nil { @@ -593,7 +612,7 @@ func createClient() *api.Client { netParams.ForceMessagePickupRetry = viper.GetBool("forceMessagePickupRetry") netParams.VerboseRoundTracking = viper.GetBool("verboseRoundTracking") - client, err := api.OpenClient(storeDir, []byte(pass), netParams) + client, err := api.OpenClient(storeDir, pass, netParams) if err != nil { jww.FATAL.Panicf("%+v", err) } @@ -603,7 +622,7 @@ func createClient() *api.Client { func initClient() *api.Client { createClient() - pass := viper.GetString("password") + pass := parsePassword(viper.GetString("password")) storeDir := viper.GetString("session") jww.DEBUG.Printf("sessionDur: %v", storeDir) netParams := params.GetDefaultNetwork() @@ -623,7 +642,7 @@ func initClient() *api.Client { netParams.VerboseRoundTracking = viper.GetBool("verboseRoundTracking") // load the client - client, err := api.Login(storeDir, []byte(pass), netParams) + client, err := api.Login(storeDir, pass, netParams) if err != nil { jww.FATAL.Panicf("%+v", err) } @@ -642,35 +661,44 @@ func initClient() *api.Client { } - return client -} + if backupOut := viper.GetString("backupOut"); backupOut != "" { + backupPass := viper.GetString("backupPass") + updateBackupCb := func(encryptedBackup []byte) { + jww.INFO.Printf("Backup update received, size %d", len(encryptedBackup)) + fmt.Println("Backup update received.") + err = utils.WriteFileDef(backupOut, encryptedBackup) + if err != nil { + jww.FATAL.Panicf("Failed to write backup to file: %+v", err) + } -func writeContact(c contact.Contact) { - outfilePath := viper.GetString("writeContact") - if outfilePath == "" { - return - } - err := ioutil.WriteFile(outfilePath, c.Marshal(), 0644) - if err != nil { - jww.FATAL.Panicf("%+v", err) - } -} + backupJsonPath := viper.GetString("backupJsonOut") -func readContact() contact.Contact { - inputFilePath := viper.GetString("destfile") - if inputFilePath == "" { - return contact.Contact{} - } - data, err := ioutil.ReadFile(inputFilePath) - jww.INFO.Printf("Contact file size read in: %d", len(data)) - if err != nil { - jww.FATAL.Panicf("Failed to read contact file: %+v", err) - } - c, err := contact.Unmarshal(data) - if err != nil { - jww.FATAL.Panicf("Failed to unmarshal contact: %+v", err) + if backupJsonPath != "" { + var b backupCrypto.Backup + err = b.Decrypt(backupPass, encryptedBackup) + if err != nil { + jww.ERROR.Printf("Failed to decrypt backup: %+v", err) + } + + backupJson, err := json.Marshal(b) + if err != nil { + jww.ERROR.Printf("Failed to JSON unmarshal backup: %+v", err) + } + + err = utils.WriteFileDef(backupJsonPath, backupJson) + if err != nil { + jww.FATAL.Panicf("Failed to write backup to file: %+v", err) + } + } + } + _, err = backup.InitializeBackup(backupPass, updateBackupCb, client) + if err != nil { + jww.FATAL.Panicf("Failed to initialize backup with key %q: %+v", + backupPass, err) + } } - return c + + return client } func acceptChannel(client *api.Client, recipientID *id.ID) { @@ -693,14 +721,6 @@ func deleteChannel(client *api.Client, partnerId *id.ID) { } } -func printChanRequest(requestor contact.Contact) { - msg := fmt.Sprintf("Authentication channel request from: %s\n", - requestor.ID) - jww.INFO.Printf(msg) - fmt.Printf(msg) - // fmt.Printf(msg) -} - func addPrecanAuthenticatedChannel(client *api.Client, recipientID *id.ID, recipient contact.Contact) { jww.WARN.Printf("Precanned user id detected: %s", recipientID) @@ -757,6 +777,43 @@ func addAuthenticatedChannel(client *api.Client, recipientID *id.ID, } } +func resetAuthenticatedChannel(client *api.Client, recipientID *id.ID, + recipient contact.Contact) { + var allowed bool + if viper.GetBool("unsafe-channel-creation") { + msg := "unsafe channel creation enabled\n" + jww.WARN.Printf(msg) + fmt.Printf("WARNING: %s", msg) + allowed = true + } else { + allowed = askToCreateChannel(recipientID) + } + if !allowed { + jww.FATAL.Panicf("User did not allow channel reset!") + } + + msg := fmt.Sprintf("Resetting authenticated channel for: %s\n", + recipientID) + jww.INFO.Printf(msg) + fmt.Printf(msg) + + recipientContact := recipient + + if recipientContact.ID != nil && recipientContact.DhPubKey != nil { + me := client.GetUser().GetContact() + jww.INFO.Printf("Requesting auth channel from: %s", + recipientID) + _, err := client.ResetSession(recipientContact, + me, msg) + if err != nil { + jww.FATAL.Panicf("%+v", err) + } + } else { + jww.ERROR.Printf("Could not reset auth channel for %s", + recipientID) + } +} + func waitUntilConnected(connected chan bool) { waitTimeout := time.Duration(viper.GetUint("waitTimeout")) timeoutTimer := time.NewTimer(waitTimeout * time.Second) @@ -796,6 +853,16 @@ func getPrecanID(recipientID *id.ID) uint { return uint(recipientID.Bytes()[7]) } +func parsePassword(pwStr string) []byte { + if strings.HasPrefix(pwStr, "0x") { + return getPWFromHexString(pwStr[2:]) + } else if strings.HasPrefix(pwStr, "b64:") { + return getPWFromb64String(pwStr[4:]) + } else { + return []byte(pwStr) + } +} + func parseRecipient(idStr string) (*id.ID, bool) { if idStr == "0" { return nil, false @@ -848,6 +915,23 @@ func getUIDFromb64String(idStr string) *id.ID { return ID } +func getPWFromHexString(pwStr string) []byte { + pwBytes, err := hex.DecodeString(fmt.Sprintf("%0*d%s", + 66-len(pwStr), 0, pwStr)) + if err != nil { + jww.FATAL.Panicf("%+v", err) + } + return pwBytes +} + +func getPWFromb64String(pwStr string) []byte { + pwBytes, err := base64.StdEncoding.DecodeString(pwStr) + if err != nil { + jww.FATAL.Panicf("%+v", err) + } + return pwBytes +} + func getUIDFromString(idStr string) *id.ID { idInt, err := strconv.Atoi(idStr) if err != nil { @@ -1057,6 +1141,12 @@ func init() { viper.BindPFlag("delete-all-requests", rootCmd.PersistentFlags().Lookup("delete-all-requests")) + rootCmd.PersistentFlags().Bool("delete-request", false, + "Delete the request for the specified ID given by the "+ + "destfile flag's contact file.") + viper.BindPFlag("delete-request", + rootCmd.PersistentFlags().Lookup("delete-request")) + rootCmd.Flags().BoolP("send-auth-request", "", false, "Send an auth request to the specified destination and wait"+ "for confirmation") @@ -1117,6 +1207,28 @@ func init() { "will write proto user JSON file") viper.BindPFlag("protoUserOut", rootCmd.Flags().Lookup("protoUserOut")) + // Backup flags + rootCmd.Flags().String("backupOut", "", + "Path to output encrypted client backup. If no path is supplied, the "+ + "backup system is not started.") + viper.BindPFlag("backupOut", rootCmd.Flags().Lookup("backupOut")) + + rootCmd.Flags().String("backupJsonOut", "", + "Path to output unencrypted client JSON backup.") + viper.BindPFlag("backupJsonOut", rootCmd.Flags().Lookup("backupJsonOut")) + + rootCmd.Flags().String("backupIn", "", + "Path to load backup client from") + viper.BindPFlag("backupIn", rootCmd.Flags().Lookup("backupIn")) + + rootCmd.Flags().String("backupPass", "", + "Passphrase to encrypt/decrypt backup") + viper.BindPFlag("backupPass", rootCmd.Flags().Lookup("backupPass")) + + rootCmd.Flags().String("backupIdList", "", + "JSON file containing the backed up partner IDs") + viper.BindPFlag("backupIdList", rootCmd.Flags().Lookup("backupIdList")) + } // initConfig reads in config file and ENV variables if set. diff --git a/cmd/ud.go b/cmd/ud.go index cebbe7371079cbc9619aa7c74c4fc68ab419b0c7..a8f6205a4fe173d62af1d9ef10c1b85574ad63e9 100644 --- a/cmd/ud.go +++ b/cmd/ud.go @@ -10,6 +10,8 @@ package cmd import ( "fmt" + "time" + "github.com/spf13/cobra" jww "github.com/spf13/jwalterweatherman" "github.com/spf13/viper" @@ -17,9 +19,10 @@ import ( "gitlab.com/elixxir/client/single" "gitlab.com/elixxir/client/switchboard" "gitlab.com/elixxir/client/ud" + "gitlab.com/elixxir/client/xxmutils" "gitlab.com/elixxir/crypto/contact" "gitlab.com/elixxir/primitives/fact" - "time" + "gitlab.com/xx_network/primitives/utils" ) // udCmd is the user discovery subcommand, which allows for user lookup, @@ -158,6 +161,26 @@ var udCmd = &cobra.Command{ time.Sleep(31 * time.Second) } + if viper.GetString("batchadd") != "" { + idListFile, err := utils.ReadFile(viper.GetString("batchadd")) + if err != nil { + fmt.Printf("BATCHADD: Couldn't read file: %s\n", + err.Error()) + jww.FATAL.Panicf("BATCHADD: Couldn't read file: %+v", err) + } + restored, _, _, err := xxmutils.RestoreContactsFromBackup( + idListFile, client, userDiscoveryMgr, nil, nil) + if err != nil { + jww.FATAL.Panicf("%+v", err) + } + for i := 0; i < len(restored); i++ { + uid := restored[i] + for !client.HasAuthenticatedChannel(uid) { + time.Sleep(time.Second) + } + jww.INFO.Printf("Authenticated channel established for %s", uid) + } + } usernameSearchStr := viper.GetString("searchusername") emailSearchStr := viper.GetString("searchemail") phoneSearchStr := viper.GetString("searchphone") @@ -270,6 +293,10 @@ func init() { "Search for users with this email address.") _ = viper.BindPFlag("searchphone", udCmd.Flags().Lookup("searchphone")) + udCmd.Flags().String("batchadd", "", + "Path to JSON marshalled slice of partner IDs that will be looked up on UD.") + _ = viper.BindPFlag("batchadd", udCmd.Flags().Lookup("batchadd")) + rootCmd.AddCommand(udCmd) } diff --git a/cmd/utils.go b/cmd/utils.go new file mode 100644 index 0000000000000000000000000000000000000000..9e26e2c762c343432991c5e161a57e9eb1ad0995 --- /dev/null +++ b/cmd/utils.go @@ -0,0 +1,114 @@ +package cmd + +import ( + "fmt" + jww "github.com/spf13/jwalterweatherman" + "github.com/spf13/viper" + "gitlab.com/elixxir/client/api" + "gitlab.com/elixxir/client/interfaces/message" + backupCrypto "gitlab.com/elixxir/crypto/backup" + "gitlab.com/elixxir/crypto/contact" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/utils" + "io/ioutil" + "strconv" + "strings" +) + +// todo: go through cmd package and organize utility functions + +func loadBackup(backupPath, backupPass string) (backupCrypto.Backup, []byte) { + jww.INFO.Printf("Loading backup from path %q with password %q", backupPath, backupPass) + backupFile, err := utils.ReadFile(backupPath) + if err != nil { + jww.FATAL.Panicf("%v", err) + } + + var b backupCrypto.Backup + err = b.Decrypt(backupPass, backupFile) + if err != nil { + jww.ERROR.Printf("Failed to decrypt backup: %+v", err) + } + + return b, backupFile +} + +///////////////////////////////////////////////////////////////// +////////////////// Print functions ///////////////////////////// +///////////////////////////////////////////////////////////////// + +func printChanRequest(requestor contact.Contact) { + msg := fmt.Sprintf("Authentication channel request from: %s\n", + requestor.ID) + jww.INFO.Printf(msg) + fmt.Printf(msg) + // fmt.Printf(msg) +} + +// Helper function which prints the round resuls +func printRoundResults(allRoundsSucceeded, timedOut bool, + rounds map[id.Round]api.RoundResult, roundIDs []id.Round, msg message.Send) { + + // Done as string slices for easy and human readable printing + successfulRounds := make([]string, 0) + failedRounds := make([]string, 0) + timedOutRounds := make([]string, 0) + + for _, r := range roundIDs { + // Group all round reports into a category based on their + // result (successful, failed, or timed out) + if result, exists := rounds[r]; exists { + if result == api.Succeeded { + successfulRounds = append(successfulRounds, strconv.Itoa(int(r))) + } else if result == api.Failed { + failedRounds = append(failedRounds, strconv.Itoa(int(r))) + } else { + timedOutRounds = append(timedOutRounds, strconv.Itoa(int(r))) + } + } + } + + jww.INFO.Printf("Result of sending message \"%s\" to \"%v\":", + msg.Payload, msg.Recipient) + + // Print out all rounds results, if they are populated + if len(successfulRounds) > 0 { + jww.INFO.Printf("\tRound(s) %v successful", strings.Join(successfulRounds, ",")) + } + if len(failedRounds) > 0 { + jww.ERROR.Printf("\tRound(s) %v failed", strings.Join(failedRounds, ",")) + } + if len(timedOutRounds) > 0 { + jww.ERROR.Printf("\tRound(s) %v timed "+ + "\n\tout (no network resolution could be found)", strings.Join(timedOutRounds, ",")) + } + +} + +func writeContact(c contact.Contact) { + outfilePath := viper.GetString("writeContact") + if outfilePath == "" { + return + } + err := ioutil.WriteFile(outfilePath, c.Marshal(), 0644) + if err != nil { + jww.FATAL.Panicf("%+v", err) + } +} + +func readContact() contact.Contact { + inputFilePath := viper.GetString("destfile") + if inputFilePath == "" { + return contact.Contact{} + } + data, err := ioutil.ReadFile(inputFilePath) + jww.INFO.Printf("Contact file size read in: %d", len(data)) + if err != nil { + jww.FATAL.Panicf("Failed to read contact file: %+v", err) + } + c, err := contact.Unmarshal(data) + if err != nil { + jww.FATAL.Panicf("Failed to unmarshal contact: %+v", err) + } + return c +} diff --git a/cmd/version.go b/cmd/version.go index f6443702ed89544cfeda43c6671e75d740bb21fc..78dee4c0b41830b5f6df795bb4e55192a3c037b5 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -18,7 +18,7 @@ import ( ) // Change this value to set the version for this build -const currentVersion = "4.0.0" +const currentVersion = "4.1.0" func Version() string { out := fmt.Sprintf("Elixxir Client v%s -- %s\n\n", api.SEMVER, diff --git a/fileTransfer/manager.go b/fileTransfer/manager.go index b188bcc203fe5e286507a5f7e7de4cb34485a836..713e230c96a3076669fbd718115f62b97b65a8af 100644 --- a/fileTransfer/manager.go +++ b/fileTransfer/manager.go @@ -50,7 +50,7 @@ const ( sendQueueBuffLen = 10_000 // Size of the buffered channel that reports if the network is healthy - networkHealthBuffLen = 100 + networkHealthBuffLen = 10 ) // Error messages. diff --git a/fileTransfer/oldTransferRecovery.go b/fileTransfer/oldTransferRecovery.go index 7fc4d41c61915d070717a3001c4154237cf0f81c..f47e9ba0d004272b8605cf6b5a23332df9e3c67d 100644 --- a/fileTransfer/oldTransferRecovery.go +++ b/fileTransfer/oldTransferRecovery.go @@ -41,7 +41,7 @@ func (m Manager) oldTransferRecovery(healthyChan chan bool, chanID uint64) { // Get list of unsent parts and rounds that parts were sent on unsentParts, sentRounds, err := m.sent.GetUnsentPartsAndSentRounds() - jww.DEBUG.Printf("Adding unsent parts from %d recovered transfers: %v", + jww.DEBUG.Printf("[FT] Adding unsent parts from %d recovered transfers: %v", len(unsentParts), unsentParts) // Add all unsent parts to the queue diff --git a/fileTransfer/receive.go b/fileTransfer/receive.go index 3552fdf5635a819f66cbecfb9945ca01ab529846..9c46cb257fe733ad1731874d4ca8f5db2ee60529 100644 --- a/fileTransfer/receive.go +++ b/fileTransfer/receive.go @@ -40,9 +40,9 @@ func (m *Manager) receive(rawMsgs chan message.Receive, stop *stoppable.Single) // which means this message is not of the correct type and will // be ignored if strings.Contains(err.Error(), "fingerprint") { - jww.TRACE.Printf("[FT] %+v", err) + jww.TRACE.Printf("[FT] %v", err) } else { - jww.WARN.Printf("[FT] %+v", err) + jww.WARN.Printf("[FT] %v", err) } continue } diff --git a/fileTransfer/receive_test.go b/fileTransfer/receive_test.go index 4ea9419b8b96e2125da246d71a216303e3bb0643..cc28fc24f1b910128ceba15d1486b1ef9e120966 100644 --- a/fileTransfer/receive_test.go +++ b/fileTransfer/receive_test.go @@ -97,7 +97,7 @@ func TestManager_receive(t *testing.T) { if err != nil { t.Errorf("Failed to get sent transfer %s: %+v", stID, err) } - cMixMsg, err := m1.newCmixMessage(st, 0, prng) + cMixMsg, err := m1.newCmixMessage(st, 0) if err != nil { t.Errorf("Failed to create new cMix message: %+v", err) } @@ -203,7 +203,7 @@ func TestManager_receive_Stop(t *testing.T) { if err != nil { t.Errorf("Failed to get sent transfer %s: %+v", stID, err) } - cMixMsg, err := m1.newCmixMessage(st, 0, prng) + cMixMsg, err := m1.newCmixMessage(st, 0) if err != nil { t.Errorf("Failed to create new cMix message: %+v", err) } @@ -311,7 +311,7 @@ func TestManager_readMessage(t *testing.T) { if err != nil { t.Errorf("Failed to get sent transfer %s: %+v", stID, err) } - cMixMsg, err := m1.newCmixMessage(st, 0, prng) + cMixMsg, err := m1.newCmixMessage(st, 0) if err != nil { t.Errorf("Failed to create new cMix message: %+v", err) } diff --git a/fileTransfer/send.go b/fileTransfer/send.go index 0ae1bdfae8e58456395d4cc5cbc300fd13f7040a..b42c16d13cb3c589a3b8dc620878e206aa73b65e 100644 --- a/fileTransfer/send.go +++ b/fileTransfer/send.go @@ -51,7 +51,8 @@ const ( // Manager.sendEndE2eMessage endE2eGetPartnerErr = "failed to get file transfer partner %s: %+v" - endE2eSendErr = "failed to send end file transfer message: %+v" + endE2eHealthTimeout = "waiting for network to become healthy timed out after %s." + endE2eSendErr = "failed to send end file transfer message via E2E to recipient %s: %+v" // getRandomNumParts getRandomNumPartsRandPanic = "[FT] Failed to generate random number of file parts to send: %+v" @@ -64,8 +65,15 @@ const ( // Duration to wait for send batch to fill before sending partial batch. pollSleepDuration = 100 * time.Millisecond - // Age when rounds that files were sent from are deleted from the tracker + // Age when rounds that files were sent from are deleted from the tracker. clearSentRoundsAge = 10 * time.Second + + // Duration to wait for network to become healthy to send end E2E message + // before timing out. + sendEndE2eHealthTimeout = 5 * time.Second + + // Tag that prints with cMix sending logs. + cMixDebugTag = "FT.Part" ) // sendThread waits on the sendQueue channel for parts to send. Once its @@ -108,20 +116,27 @@ func (m *Manager) sendThread(stop *stoppable.Single, healthChan chan bool, timer = time.NewTimer(pollSleepDuration) select { case <-stop.Quit(): + timer.Stop() + // Close the thread when the stoppable is triggered m.closeSendThread(partList, stop, healthChanID) return case healthy := <-healthChan: + var wasNotHealthy bool // If the network is unhealthy, wait until it becomes healthy if !healthy { jww.TRACE.Print("[FT] Suspending file part sending thread: " + "network is unhealthy.") + wasNotHealthy = true } for !healthy { healthy = <-healthChan } - jww.TRACE.Print("[FT] File part sending thread: network is healthy.") + if wasNotHealthy { + jww.TRACE.Print("[FT] File part sending thread: " + + "network is healthy.") + } case part := <-m.sendQueue: // When a part is received from the queue, add it to the list of // parts to be sent @@ -141,17 +156,15 @@ func (m *Manager) sendThread(stop *stoppable.Single, healthChan chan bool, quit := m.handleSend( &partList, &lastSend, delay, stop, healthChanID, sentRounds) if quit { + timer.Stop() return } - } else { - timer = time.NewTimer(pollSleepDuration) } case <-timer.C: // If the timeout is reached, send an incomplete batch // Skip if there are no parts to send if len(partList) == 0 { - timer = time.NewTimer(pollSleepDuration) continue } @@ -196,23 +209,33 @@ func (m *Manager) handleSend(partList *[]queuedPart, lastSend *time.Time, // the bandwidth is limited to the maximum throughput if netTime.Since(*lastSend) < delay { waitingTime := delay - netTime.Since(*lastSend) - jww.TRACE.Printf("[FT] Suspending file part sending: "+ - "bandwidth limit reached; waiting %s to send.", waitingTime) + jww.TRACE.Printf("[FT] Suspending file part sending (%d parts): "+ + "bandwidth limit reached; waiting %s to send.", + len(*partList), waitingTime) + + waitingTimer := time.NewTimer(waitingTime) select { case <-stop.Quit(): + waitingTimer.Stop() + // Close the thread when the stoppable is triggered m.closeSendThread(*partList, stop, healthChanID) return true - case <-time.NewTimer(delay - netTime.Since(*lastSend)).C: + case <-waitingTimer.C: + jww.TRACE.Printf("[FT] Resuming file part sending (%d parts) "+ + "after waiting %s for bandwidth limiting.", + len(*partList), waitingTime) } } // Send all the messages - err := m.sendParts(*partList, sentRounds) - if err != nil { - jww.ERROR.Print(err) - } + go func(partList []queuedPart, sentRounds *sentRoundTracker) { + err := m.sendParts(partList, sentRounds) + if err != nil { + jww.ERROR.Print(err) + } + }(copyPartList(*partList), sentRounds) // Update the timestamp of the send *lastSend = netTime.Now() @@ -223,6 +246,13 @@ func (m *Manager) handleSend(partList *[]queuedPart, lastSend *time.Time, return false } +// copyPartList makes a copy of the list of queuedPart. +func copyPartList(partList []queuedPart) []queuedPart { + newPartList := make([]queuedPart, len(partList)) + copy(newPartList, partList) + return newPartList +} + // sendParts handles the composing and sending of a cMix message for each part // in the list. All errors returned are fatal errors. func (m *Manager) sendParts(partList []queuedPart, @@ -247,7 +277,10 @@ func (m *Manager) sendParts(partList []queuedPart, p := params.GetDefaultCMIX() p.SendTimeout = m.p.SendTimeout p.ExcludedRounds = sentRounds - p.DebugTag = "ft.Part" + p.DebugTag = cMixDebugTag + + jww.TRACE.Printf("[FT] Sending %d file parts via SendManyCMIX with "+ + "parameters %+v", len(messages), p) // Send parts rid, _, err := m.net.SendManyCMIX(messages, p) @@ -324,7 +357,7 @@ func (m *Manager) buildMessages(partList []queuedPart) ( } // Generate new cMix message with encrypted file part - cmixMsg, err := m.newCmixMessage(st, part.partNum, rng) + cmixMsg, err := m.newCmixMessage(st, part.partNum) if err == ftStorage.MaxRetriesErr { jww.DEBUG.Printf("[FT] File transfer %s sent to %s ran out of "+ "retries {parts: %d, numFps: %d/%d}", @@ -361,7 +394,7 @@ func (m *Manager) buildMessages(partList []queuedPart) ( // newCmixMessage creates a new cMix message with an encrypted file part, its // MAC, and fingerprint. func (m *Manager) newCmixMessage(transfer *ftStorage.SentTransfer, - partNum uint16, rng csprng.Source) (format.Message, error) { + partNum uint16) (format.Message, error) { // Create new empty cMix message cmixMsg := format.NewMessage(m.store.Cmix().GetGroup().GetP().ByteLen()) @@ -407,6 +440,9 @@ func (m *Manager) makeRoundEventCallback( continue } + // Call progress callback after change in progress + st.CallProgressCB(nil) + // If the transfer is complete, send an E2E message to the // recipient informing them if completed { @@ -424,9 +460,6 @@ func (m *Manager) makeRoundEventCallback( } }(tid, st.GetRecipient()) } - - // Call progress callback after change in progress - st.CallProgressCB(nil) } } else { @@ -478,15 +511,31 @@ func (m *Manager) sendEndE2eMessage(recipient *id.ID) error { // Send the message under file transfer preimage e2eParams := params.GetDefaultE2E() e2eParams.IdentityPreimage = partner.GetFileTransferPreimage() - e2eParams.DebugTag = "ft.End" + e2eParams.DebugTag = "FT.End" // Store the message in the critical messages buffer first to ensure it is // present if the send fails m.store.GetCriticalMessages().AddProcessing(sendMsg, e2eParams) + // Register health channel and wait for network to become healthy + healthChan := make(chan bool, networkHealthBuffLen) + healthChanID := m.net.GetHealthTracker().AddChannel(healthChan) + defer m.net.GetHealthTracker().RemoveChannel(healthChanID) + isHealthy := m.net.GetHealthTracker().IsHealthy() + healthCheckTimer := time.NewTimer(sendEndE2eHealthTimeout) + for !isHealthy { + select { + case isHealthy = <-healthChan: + case <-healthCheckTimer.C: + return errors.Errorf(endE2eHealthTimeout, sendEndE2eHealthTimeout) + } + } + healthCheckTimer.Stop() + + // Send E2E message rounds, e2eMsgID, _, err := m.net.SendE2E(sendMsg, e2eParams, nil) if err != nil { - return errors.Errorf(endE2eSendErr, err) + return errors.Errorf(endE2eSendErr, recipient, err) } // Register the event for all rounds diff --git a/fileTransfer/sendNew.go b/fileTransfer/sendNew.go index b28fe263958c3ff5b95455faee9f3623f44267b1..91c979dc806324d5a4077a902605c239239c9926 100644 --- a/fileTransfer/sendNew.go +++ b/fileTransfer/sendNew.go @@ -43,11 +43,11 @@ func (m *Manager) sendNewFileTransfer(recipient *id.ID, fileName, // Sends as a silent message to avoid a notification p := params.GetDefaultE2E() p.CMIX.IdentityPreimage = relationship.GetSilentPreimage() - p.DebugTag = "ft.New" + p.DebugTag = "FT.New" // Send E2E message - rounds, _, _, err := m.net.SendE2E(sendMsg, p, nil) - if err != nil && len(rounds) == 0 { + _, _, _, err = m.net.SendE2E(sendMsg, p, nil) + if err != nil { return errors.Errorf(newFtSendE2eErr, recipient, err) } diff --git a/fileTransfer/send_test.go b/fileTransfer/send_test.go index 2e3d3d16d8325be32ba975decb14b3af41f57636..d090d4dca6aca58f5fae3a3b6fbfa10d42cf3b2a 100644 --- a/fileTransfer/send_test.go +++ b/fileTransfer/send_test.go @@ -650,7 +650,7 @@ func TestManager_newCmixMessage(t *testing.T) { t.Errorf("Failed to create a new SentTransfer: %+v", err) } - cmixMsg, err := m.newCmixMessage(transfer, 0, prng) + cmixMsg, err := m.newCmixMessage(transfer, 0) if err != nil { t.Errorf("newCmixMessage returned an error: %+v", err) } @@ -663,7 +663,7 @@ func TestManager_newCmixMessage(t *testing.T) { } decrPart, err := ftCrypto.DecryptPart(key, cmixMsg.GetContents(), - cmixMsg.GetMac(), 0,cmixMsg.GetKeyFP()) + cmixMsg.GetMac(), 0, cmixMsg.GetKeyFP()) if err != nil { t.Errorf("Failed to decrypt file part: %+v", err) } @@ -888,13 +888,11 @@ func TestManager_sendEndE2eMessage(t *testing.T) { p := params.GetDefaultE2ESessionParams() rng := csprng.NewSystemRNG() - _, mySidhPriv := util.GenerateSIDHKeyPair(sidh.KeyVariantSidhA, - rng) - theirSidhPub, _ := util.GenerateSIDHKeyPair( - sidh.KeyVariantSidhB, rng) + _, mySidhPriv := util.GenerateSIDHKeyPair(sidh.KeyVariantSidhA, rng) + theirSidhPub, _ := util.GenerateSIDHKeyPair(sidh.KeyVariantSidhB, rng) - err := m.store.E2e().AddPartner(recipient, pubKey, dhKey, mySidhPriv, - theirSidhPub, p, p) + err := m.store.E2e().AddPartner( + recipient, pubKey, dhKey, mySidhPriv, theirSidhPub, p, p) if err != nil { t.Errorf("Failed to add partner %s: %+v", recipient, err) } @@ -1007,7 +1005,7 @@ func TestManager_getPartSize(t *testing.T) { primeByteLen := m.store.Cmix().GetGroup().GetP().ByteLen() cmixMsgUsedLen := format.AssociatedDataSize filePartMsgUsedLen := ftStorage.FmMinSize - expected := 2*primeByteLen - cmixMsgUsedLen - filePartMsgUsedLen-1 + expected := 2*primeByteLen - cmixMsgUsedLen - filePartMsgUsedLen - 1 // Get the part size partSize, err := m.getPartSize() diff --git a/fileTransfer/sentRoundTracker.go b/fileTransfer/sentRoundTracker.go index 80fa1da210befa6d3cc7cec698d197259a903ecb..75a06eb13e593f205b470b30f2dadc76a1c2c869 100644 --- a/fileTransfer/sentRoundTracker.go +++ b/fileTransfer/sentRoundTracker.go @@ -59,13 +59,20 @@ func (srt *sentRoundTracker) Has(rid id.Round) bool { return exists } -// Insert adds the round to the tracker with the current time. -func (srt *sentRoundTracker) Insert(rid id.Round) { +// Insert adds the round to the tracker with the current time. Returns true if +// the round was added. +func (srt *sentRoundTracker) Insert(rid id.Round) bool { timeNow := netTime.Now() srt.mux.Lock() defer srt.mux.Unlock() + _, exists := srt.rounds[rid] + if exists { + return false + } + srt.rounds[rid] = timeNow + return true } // Remove deletes a round ID from the tracker. diff --git a/go.mod b/go.mod index 4a0f6367b73509140ecc2885b5e5b7bd87396bd4..0cfb5fb2a0bc0a292e903bc7781daac4d968d9bc 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module gitlab.com/elixxir/client go 1.17 require ( - github.com/cloudflare/circl v1.0.1-0.20211008185751-59b49bc148ce + github.com/cloudflare/circl v1.1.0 github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 github.com/golang/protobuf v1.5.2 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 @@ -12,13 +12,13 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 github.com/spf13/viper v1.7.1 gitlab.com/elixxir/bloomfilter v0.0.0-20200930191214-10e9ac31b228 - gitlab.com/elixxir/comms v0.0.4-0.20220222212253-41a1a0067369 - gitlab.com/elixxir/crypto v0.0.7-0.20220222212142-d3303373ee78 + gitlab.com/elixxir/comms v0.0.4-0.20220603231314-e47e4af13326 + gitlab.com/elixxir/crypto v0.0.7-0.20220414225314-6f3eb9c073a5 gitlab.com/elixxir/ekv v0.1.6 - gitlab.com/elixxir/primitives v0.0.3-0.20220222212109-d412a6e46623 - gitlab.com/xx_network/comms v0.0.4-0.20220222212058-5a37737af57e - gitlab.com/xx_network/crypto v0.0.5-0.20220222212031-750f7e8a01f4 - gitlab.com/xx_network/primitives v0.0.4-0.20220222211843-901fa4a2d72b + gitlab.com/elixxir/primitives v0.0.3-0.20220323183834-b98f255361b8 + gitlab.com/xx_network/comms v0.0.4-0.20220315161313-76acb14429ac + gitlab.com/xx_network/crypto v0.0.5-0.20220317171841-084640957d71 + gitlab.com/xx_network/primitives v0.0.4-0.20220324193139-b292d1ae6e7e golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 google.golang.org/grpc v1.42.0 @@ -26,13 +26,29 @@ require ( ) require ( + github.com/badoux/checkmail v1.2.1 // indirect + github.com/elliotchance/orderedmap v1.4.0 // indirect + github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/magiconair/properties v1.8.4 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.4.0 // indirect github.com/pelletier/go-toml v1.8.1 // indirect + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/smartystreets/assertions v1.0.1 // indirect github.com/spf13/afero v1.5.1 // indirect github.com/spf13/cast v1.3.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.2.0 // indirect + github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect + github.com/ttacon/libphonenumber v1.2.1 // indirect + github.com/tyler-smith/go-bip39 v1.1.0 // indirect + github.com/zeebo/blake3 v0.1.1 // indirect + gitlab.com/xx_network/ring v0.0.3-0.20220222211904-da613960ad93 // indirect + golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect + golang.org/x/text v0.3.6 // indirect google.golang.org/genproto v0.0.0-20210105202744-fe13368bc0e1 // indirect gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 5cecc075ac7209f3664ffc22c85e679008a5edfb..4447f22a73a5bf4ab23dbb958cd2ea82e2502f0e 100644 --- a/go.sum +++ b/go.sum @@ -31,8 +31,8 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.0.1-0.20211008185751-59b49bc148ce h1:2s+cfEmFVdtV8Z85o6U0QxtNhCXDCMR2OLZKgL39ApI= -github.com/cloudflare/circl v1.0.1-0.20211008185751-59b49bc148ce/go.mod h1:tnEeRn/onb0b4Ew40H00boTlcVMHveaTzi6m+/iMruw= +github.com/cloudflare/circl v1.1.0 h1:bZgT/A+cikZnKIwn7xL2OBj012Bmvho/o6RpRvv3GKY= +github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= @@ -272,32 +272,37 @@ github.com/zeebo/pcg v1.0.0 h1:dt+dx+HvX8g7Un32rY9XWoYnd0NmKmrIzpHF7qiTDj0= github.com/zeebo/pcg v1.0.0/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= gitlab.com/elixxir/bloomfilter v0.0.0-20200930191214-10e9ac31b228 h1:Gi6rj4mAlK0BJIk1HIzBVMjWNjIUfstrsXC2VqLYPcA= gitlab.com/elixxir/bloomfilter v0.0.0-20200930191214-10e9ac31b228/go.mod h1:H6jztdm0k+wEV2QGK/KYA+MY9nj9Zzatux/qIvDDv3k= -gitlab.com/elixxir/comms v0.0.4-0.20220222212253-41a1a0067369 h1:Bk5T3unbs3cjEqzxCirb1IlcTOMAEVQLltgsrVs1/cw= -gitlab.com/elixxir/comms v0.0.4-0.20220222212253-41a1a0067369/go.mod h1:AligJKSltFDPe/rqE2EZBfWCMSrae0zUo7scsXoyMPE= +gitlab.com/elixxir/comms v0.0.4-0.20220603231314-e47e4af13326 h1:Zid8oNHtbOqF6ebrcGIccvIMabFNGh9dzY1b7mgIcF0= +gitlab.com/elixxir/comms v0.0.4-0.20220603231314-e47e4af13326/go.mod h1:tlHSrtSliKWUxsck8z/Ql/VJkMdSONV2BeWaUAAXzgk= gitlab.com/elixxir/crypto v0.0.0-20200804182833-984246dea2c4/go.mod h1:ucm9SFKJo+K0N2GwRRpaNr+tKXMIOVWzmyUD0SbOu2c= gitlab.com/elixxir/crypto v0.0.3/go.mod h1:ZNgBOblhYToR4m8tj4cMvJ9UsJAUKq+p0gCp07WQmhA= -gitlab.com/elixxir/crypto v0.0.7-0.20220222212142-d3303373ee78 h1:MvZ0UwyhCSuNUcmHT905oadu7XYT2WSz+QD3Rjcgg00= -gitlab.com/elixxir/crypto v0.0.7-0.20220222212142-d3303373ee78/go.mod h1:bPD4FmnnaDFLxn+d4YDWZhVnevWXArKwOMMza4MU5uQ= +gitlab.com/elixxir/crypto v0.0.7-0.20220317172048-3de167bd9406/go.mod h1:tD6XjtQh87T2nKZL5I/pYPck5M2wLpkZ1Oz7H/LqO10= +gitlab.com/elixxir/crypto v0.0.7-0.20220414225314-6f3eb9c073a5 h1:yw3G8ZEiWu2eSZWRQmj6nBhiJIYK3Cw2MJzDPkNHYVA= +gitlab.com/elixxir/crypto v0.0.7-0.20220414225314-6f3eb9c073a5/go.mod h1:tD6XjtQh87T2nKZL5I/pYPck5M2wLpkZ1Oz7H/LqO10= gitlab.com/elixxir/ekv v0.1.6 h1:M2hUSNhH/ChxDd+s8xBqSEKgoPtmE6hOEBqQ73KbN6A= gitlab.com/elixxir/ekv v0.1.6/go.mod h1:e6WPUt97taFZe5PFLPb1Dupk7tqmDCTQu1kkstqJvw4= gitlab.com/elixxir/primitives v0.0.0-20200731184040-494269b53b4d/go.mod h1:OQgUZq7SjnE0b+8+iIAT2eqQF+2IFHn73tOo+aV11mg= gitlab.com/elixxir/primitives v0.0.0-20200804170709-a1896d262cd9/go.mod h1:p0VelQda72OzoUckr1O+vPW0AiFe0nyKQ6gYcmFSuF8= gitlab.com/elixxir/primitives v0.0.0-20200804182913-788f47bded40/go.mod h1:tzdFFvb1ESmuTCOl1z6+yf6oAICDxH2NPUemVgoNLxc= gitlab.com/elixxir/primitives v0.0.1/go.mod h1:kNp47yPqja2lHSiS4DddTvFpB/4D9dB2YKnw5c+LJCE= -gitlab.com/elixxir/primitives v0.0.3-0.20220222212109-d412a6e46623 h1:NzJ06KdJd3fVJee0QvGhNr3CO+Ki8Ea1PeakZsm+rZM= gitlab.com/elixxir/primitives v0.0.3-0.20220222212109-d412a6e46623/go.mod h1:MtFIyJUQn9P7djzVlBpEYkPNnnWFTjZvw89swoXY+QM= +gitlab.com/elixxir/primitives v0.0.3-0.20220323183834-b98f255361b8 h1:U3Ahbg2N6QL5uwPyccWWN4ZUBFBLgCsuq5sQxOI2VCw= +gitlab.com/elixxir/primitives v0.0.3-0.20220323183834-b98f255361b8/go.mod h1:MtFIyJUQn9P7djzVlBpEYkPNnnWFTjZvw89swoXY+QM= gitlab.com/xx_network/comms v0.0.0-20200805174823-841427dd5023/go.mod h1:owEcxTRl7gsoM8c3RQ5KAm5GstxrJp5tn+6JfQ4z5Hw= -gitlab.com/xx_network/comms v0.0.4-0.20220222212058-5a37737af57e h1:PrQoTQoA6be4J+Lr/AclebS5Gz0Zm/TYC5b44qWa1PU= -gitlab.com/xx_network/comms v0.0.4-0.20220222212058-5a37737af57e/go.mod h1:isHnwem0v4rTcwwHP455FhVlFyPcHkHiVz+N3s/uCSI= +gitlab.com/xx_network/comms v0.0.4-0.20220315161313-76acb14429ac h1:+ykw0JqLH/qMprPEKazGHNH8gUoHGA78EIr4ienxnw4= +gitlab.com/xx_network/comms v0.0.4-0.20220315161313-76acb14429ac/go.mod h1:isHnwem0v4rTcwwHP455FhVlFyPcHkHiVz+N3s/uCSI= gitlab.com/xx_network/crypto v0.0.3/go.mod h1:DF2HYvvCw9wkBybXcXAgQMzX+MiGbFPjwt3t17VRqRE= gitlab.com/xx_network/crypto v0.0.4/go.mod h1:+lcQEy+Th4eswFgQDwT0EXKp4AXrlubxalwQFH5O0Mk= -gitlab.com/xx_network/crypto v0.0.5-0.20220222212031-750f7e8a01f4 h1:95dZDMn/hpLNwsgZO9eyQgGKaSDyh6F6+WygqZIciww= gitlab.com/xx_network/crypto v0.0.5-0.20220222212031-750f7e8a01f4/go.mod h1:6apvsoHCQJDjO0J4E3uhR3yO9tTz/Mq5be5rjB3tQPU= +gitlab.com/xx_network/crypto v0.0.5-0.20220317171841-084640957d71 h1:N2+Jja4xNg66entu6rGvzRcf3Vc785xgiaHeDPYnBvg= +gitlab.com/xx_network/crypto v0.0.5-0.20220317171841-084640957d71/go.mod h1:/SJf+R75E+QepdTLh0H1/udsovxx2Q5ru34q1v0umKk= gitlab.com/xx_network/primitives v0.0.0-20200803231956-9b192c57ea7c/go.mod h1:wtdCMr7DPePz9qwctNoAUzZtbOSHSedcK++3Df3psjA= gitlab.com/xx_network/primitives v0.0.0-20200804183002-f99f7a7284da/go.mod h1:OK9xevzWCaPO7b1wiluVJGk7R5ZsuC7pHY5hteZFQug= gitlab.com/xx_network/primitives v0.0.2/go.mod h1:cs0QlFpdMDI6lAo61lDRH2JZz+3aVkHy+QogOB6F/qc= -gitlab.com/xx_network/primitives v0.0.4-0.20220222211843-901fa4a2d72b h1:shZZ3xZNNKYVpEp4Mqu/G9+ZR+J8QA8mmPk/4Cit8+Y= gitlab.com/xx_network/primitives v0.0.4-0.20220222211843-901fa4a2d72b/go.mod h1:9imZHvYwNFobxueSvVtHneZLk9wTK7HQTzxPm+zhFhE= +gitlab.com/xx_network/primitives v0.0.4-0.20220317172007-4d2a53e6e669/go.mod h1:AXVVFt7dDAeIUpOGPiStCcUIKsBXLWbmV/BgZ4T+tOo= +gitlab.com/xx_network/primitives v0.0.4-0.20220324193139-b292d1ae6e7e h1:F651DdbU9n5qOLN8rdyAIluXWtm5Wl4jwH5D6ED3bSI= +gitlab.com/xx_network/primitives v0.0.4-0.20220324193139-b292d1ae6e7e/go.mod h1:AXVVFt7dDAeIUpOGPiStCcUIKsBXLWbmV/BgZ4T+tOo= gitlab.com/xx_network/ring v0.0.3-0.20220222211904-da613960ad93 h1:eJZrXqHsMmmejEPWw8gNAt0I8CGAMNO/7C339Zco3TM= gitlab.com/xx_network/ring v0.0.3-0.20220222211904-da613960ad93/go.mod h1:aLzpP2TiZTQut/PVHR40EJAomzugDdHXetbieRClXIM= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -319,7 +324,7 @@ golang.org/x/crypto v0.0.0-20200707235045-ab33eee955e0/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed h1:YoWVYYAfvQ4ddHv3OKmIvX7NCAhFGTj62VP2l2kfBbA= golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -393,8 +398,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210902050250-f475640dd07b h1:S7hKs0Flbq0bbc9xgYt4stIEG1zNDFqyrPwAX2Wj/sE= -golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/groupChat/receive.go b/groupChat/receive.go index 93a149c4e0e480d4d56b98123ce1627cf5962d55..52bed4a06a082087d87ae07d30ba84d2a4952379 100644 --- a/groupChat/receive.go +++ b/groupChat/receive.go @@ -42,11 +42,17 @@ func (m Manager) receive(rawMsgs chan message.Receive, stop *stoppable.Single) { stop.ToStopped() return case receiveMsg := <-rawMsgs: - jww.TRACE.Print("Group message reception received cMix message.") + jww.DEBUG.Printf("Group message reception received cMix message on round %d (%d).", + receiveMsg.RoundId, receiveMsg.RoundTimestamp.Unix()) + + // If given zero time, try to guesstimate roundTimestamp as right now + if receiveMsg.RoundTimestamp.Equal(time.Unix(0, 0)) { + jww.ERROR.Printf("getCryptKey missing roundTimestamp") + receiveMsg.RoundTimestamp = time.Now() + } // Attempt to read the message - g, msgID, timestamp, senderID, msg, noFpMatch, err := - m.readMessage(receiveMsg) + g, msgID, timestamp, senderID, msg, noFpMatch, err := m.readMessage(receiveMsg) if err != nil { if noFpMatch { jww.TRACE.Printf("Received message not for group chat: %+v", @@ -145,6 +151,9 @@ func (m *Manager) decryptMessage(g gs.Group, cMixMsg format.Message, messageID := group.NewMessageID(g.ID, intlMsg.Marshal()) + // Remove from garbled message on success to prevent reprocessing + m.store.GetGarbledMessages().Remove(cMixMsg) + return messageID, intlMsg.GetTimestamp(), senderID, intlMsg.GetPayload(), nil } diff --git a/groupChat/send.go b/groupChat/send.go index 03c513ccc032ab28778dfb94bb44d05c1362355e..f5bf054fb2cb3b318ab61d15912001f1507af23f 100644 --- a/groupChat/send.go +++ b/groupChat/send.go @@ -73,7 +73,7 @@ func (m *Manager) createMessages(groupID *id.ID, msg []byte, timestamp time.Time cmixMsg := format.NewMessage(m.store.Cmix().GetGroup().GetP().ByteLen()) _, intlMsg, err := newMessageParts(cmixMsg.ContentsSize()) if err != nil { - return nil, group.MessageID{},errors.WithMessage(err,"Failed to make message parts for message ID") + return nil, group.MessageID{}, errors.WithMessage(err, "Failed to make message parts for message ID") } messageID := group.NewMessageID(groupID, setInternalPayload(intlMsg, timestamp, m.gs.GetUser().ID, msg)) diff --git a/groupChat/sendRequests.go b/groupChat/sendRequests.go index 555fd2c5e0cfc891ba8cf37877e6dc72353aebf3..6318e6420cf558034c9ac1dfa36f0db715ba677d 100644 --- a/groupChat/sendRequests.go +++ b/groupChat/sendRequests.go @@ -117,10 +117,9 @@ func (m Manager) sendRequest(memberID *id.ID, request []byte) ([]id.Round, error MessageType: message.GroupCreationRequest, } - recipent, err := m.store.E2e().GetPartner(memberID) - if err!=nil{ - return nil, errors.WithMessagef(err,"Failed to send request to %s " + + if err != nil { + return nil, errors.WithMessagef(err, "Failed to send request to %s "+ "because e2e relationship could not be found", memberID) } diff --git a/groupChat/sendRequests_test.go b/groupChat/sendRequests_test.go index 9f2b9c19f2eb2c65dd51d3f66e3dc512cfe991b3..c5ec225a4d4638e518fca793da97bdfabfea5162 100644 --- a/groupChat/sendRequests_test.go +++ b/groupChat/sendRequests_test.go @@ -38,7 +38,7 @@ func TestManager_ResendRequest(t *testing.T) { Created: g.Created.UnixNano(), } - for i := range g.Members{ + for i := range g.Members { grp := m.store.E2e().GetGroup() dhKey := grp.NewInt(int64(i + 42)) pubKey := diffieHellman.GeneratePublicKey(dhKey, grp) @@ -60,7 +60,6 @@ func TestManager_ResendRequest(t *testing.T) { t.Errorf("ResendRequest() returned an error: %+v", err) } - if status != AllSent { t.Errorf("ResendRequest() failed to return the expected status."+ "\nexpected: %s\nreceived: %s", AllSent, status) @@ -135,7 +134,7 @@ func TestManager_sendRequests(t *testing.T) { Created: g.Created.UnixNano(), } - for i := range g.Members{ + for i := range g.Members { grp := m.store.E2e().GetGroup() dhKey := grp.NewInt(int64(i + 42)) pubKey := diffieHellman.GeneratePublicKey(dhKey, grp) @@ -235,7 +234,7 @@ func TestManager_sendRequests_SendPartialSent(t *testing.T) { expectedErr := fmt.Sprintf(sendRequestPartialErr, (len(g.Members)-1)/2, len(g.Members)-1, "") - for i := range g.Members{ + for i := range g.Members { grp := m.store.E2e().GetGroup() dhKey := grp.NewInt(int64(i + 42)) pubKey := diffieHellman.GeneratePublicKey(dhKey, grp) @@ -274,7 +273,7 @@ func TestManager_sendRequest(t *testing.T) { prng := rand.New(rand.NewSource(42)) m, g := newTestManagerWithStore(prng, 10, 0, nil, nil, t) - for i := range g.Members{ + for i := range g.Members { grp := m.store.E2e().GetGroup() dhKey := grp.NewInt(int64(i + 42)) pubKey := diffieHellman.GeneratePublicKey(dhKey, grp) @@ -332,7 +331,6 @@ func TestManager_sendRequest_SendE2eError(t *testing.T) { t.Errorf("Failed to add partner %s: %+v", recipientID, err) } - _, err = m.sendRequest(recipientID, nil) if err == nil || !strings.Contains(err.Error(), expectedErr) { t.Errorf("sendRequest() failed to return the expected error."+ diff --git a/interfaces/auth.go b/interfaces/auth.go index 4ce22fba789b45a5616a640f0409a054f6a05ecb..d82625c723150b433959d22c83241c909fb3781e 100644 --- a/interfaces/auth.go +++ b/interfaces/auth.go @@ -14,6 +14,7 @@ import ( type RequestCallback func(requestor contact.Contact) type ConfirmCallback func(partner contact.Contact) +type ResetNotificationCallback func(partner contact.Contact) type Auth interface { // Adds a general callback to be used on auth requests. This will be preempted @@ -42,6 +43,8 @@ type Auth interface { AddSpecificConfirmCallback(id *id.ID, cb ConfirmCallback) // Removes a specific callback to be used on auth confirm. RemoveSpecificConfirmCallback(id *id.ID) + // Add a callback to receive session renegotiation notifications + AddResetNotificationCallback(cb ResetNotificationCallback) //Replays all pending received requests over tha callbacks ReplayRequests() } diff --git a/interfaces/backup.go b/interfaces/backup.go new file mode 100644 index 0000000000000000000000000000000000000000..559b4b0f8756ee772aba9064757601d2ade417d6 --- /dev/null +++ b/interfaces/backup.go @@ -0,0 +1,40 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package interfaces + +import "sync" + +type TriggerBackup func(reason string) + +// BackupContainer contains the trigger to call to initiate a backup. +type BackupContainer struct { + triggerBackup TriggerBackup + mux sync.RWMutex +} + +// TriggerBackup triggers a backup if a backup trigger has been set. +// The passed in reason will be printed to the log when the backup is sent. It +// should be in the paste tense. For example, if a contact is deleted, the +// reason can be "contact deleted" and the log will show: +// Triggering backup: contact deleted +func (bc *BackupContainer) TriggerBackup(reason string) { + bc.mux.RLock() + defer bc.mux.RUnlock() + if bc.triggerBackup != nil { + bc.triggerBackup(reason) + } +} + +// SetBackup sets the backup trigger function which will cause a backup to start +// on the next event that triggers is. +func (bc *BackupContainer) SetBackup(triggerBackup TriggerBackup) { + bc.mux.Lock() + defer bc.mux.Unlock() + + bc.triggerBackup = triggerBackup +} diff --git a/interfaces/params/CMIX.go b/interfaces/params/CMIX.go index db2bfa655906d3b07d98ca54ea1f1539cd2fb9ed..b7351ce00a4fe641159f8225f71a4e19f02650b5 100644 --- a/interfaces/params/CMIX.go +++ b/interfaces/params/CMIX.go @@ -39,7 +39,7 @@ func GetDefaultCMIX() CMIX { Timeout: 25 * time.Second, RetryDelay: 1 * time.Second, SendTimeout: 3 * time.Second, - DebugTag: "External", + DebugTag: "External", } } diff --git a/interfaces/params/E2E.go b/interfaces/params/E2E.go index 66f5b8f989468ba4988dfeefd7dc00cd5d438510..f3aed87855d884b9d99f8de68af9bac5a6a2ca1d 100644 --- a/interfaces/params/E2E.go +++ b/interfaces/params/E2E.go @@ -63,38 +63,36 @@ func (st SendType) String() string { // Network E2E Params - - type E2ESessionParams struct { // using the DH as a seed, both sides generate a number // of keys to use before they must rekey because // there are no keys to use. - MinKeys uint16 - MaxKeys uint16 + MinKeys uint16 + MaxKeys uint16 // the percent of keys before a rekey is attempted. must be <0 - RekeyThreshold float64 + RekeyThreshold float64 // extra keys generated and reserved for rekey attempts. This // many keys are not allowed to be used for sending messages // in order to ensure there are extras for rekeying. - NumRekeys uint16 + NumRekeys uint16 } // DEFAULT KEY GENERATION PARAMETERS // Hardcoded limits for keys // sets the number of keys very high, but with a low rekey threshold. In this case, if the other party is online, you will read const ( - minKeys uint16 = 1000 - maxKeys uint16 = 2000 - rekeyThrshold float64 = 0.05 - numReKeys uint16 = 16 + minKeys uint16 = 1000 + maxKeys uint16 = 2000 + rekeyThrshold float64 = 0.05 + numReKeys uint16 = 16 ) func GetDefaultE2ESessionParams() E2ESessionParams { return E2ESessionParams{ - MinKeys: minKeys, - MaxKeys: maxKeys, + MinKeys: minKeys, + MaxKeys: maxKeys, RekeyThreshold: rekeyThrshold, - NumRekeys: numReKeys, + NumRekeys: numReKeys, } } diff --git a/interfaces/params/message.go b/interfaces/params/message.go index 27a8ebd7d626445cc4c2ace03772d5dbbb6105ed..66371a7797d1c647c3a7efe96d801f4980e008fd 100644 --- a/interfaces/params/message.go +++ b/interfaces/params/message.go @@ -16,7 +16,7 @@ type Messages struct { MessageReceptionWorkerPoolSize uint MaxChecksGarbledMessage uint GarbledMessageWait time.Duration - RealtimeOnly bool + RealtimeOnly bool } func GetDefaultMessage() Messages { @@ -25,6 +25,6 @@ func GetDefaultMessage() Messages { MessageReceptionWorkerPoolSize: 4, MaxChecksGarbledMessage: 10, GarbledMessageWait: 15 * time.Minute, - RealtimeOnly: false, + RealtimeOnly: false, } } diff --git a/interfaces/params/network.go b/interfaces/params/network.go index 7e3b6f4ab3f72f664af8e5b7e9cc5d226950a37f..a89db50e17a1063d99ef17e26cd12ab6fcc9b24f 100644 --- a/interfaces/params/network.go +++ b/interfaces/params/network.go @@ -71,7 +71,7 @@ func (n Network) Marshal() ([]byte, error) { return json.Marshal(n) } -func (n Network) SetRealtimeOnlyAll()Network { +func (n Network) SetRealtimeOnlyAll() Network { n.RealtimeOnly = true n.Rounds.RealtimeOnly = true n.Messages.RealtimeOnly = true diff --git a/interfaces/params/rounds.go b/interfaces/params/rounds.go index 75e4270987ab2b5d8ea1c4edb6e49efb9f32a863..4cfdbb233326f3e85d35497f85a2b3cd50c899ac 100644 --- a/interfaces/params/rounds.go +++ b/interfaces/params/rounds.go @@ -60,7 +60,7 @@ func GetDefaultRounds() Rounds { MaxHistoricalRoundsRetries: 3, UncheckRoundPeriod: 20 * time.Second, ForceMessagePickupRetry: false, - SendTimeout: 1 * time.Second, - RealtimeOnly: false, + SendTimeout: 3 * time.Second, + RealtimeOnly: false, } } diff --git a/interfaces/preimage/generate.go b/interfaces/preimage/generate.go index 84798d696a8b2311e6e89b01f64c885eb6c2d5b8..28e8a2fc48a528205e7f3cf05377474cb58552b5 100644 --- a/interfaces/preimage/generate.go +++ b/interfaces/preimage/generate.go @@ -28,3 +28,13 @@ func GenerateRequest(recipient *id.ID) []byte { // Base 64 encode hash and truncate return h.Sum(nil) } + +func GenerateReset(recipient *id.ID) []byte { + // Hash fingerprints + h, _ := blake2b.New256(nil) + h.Write(recipient[:]) + h.Write([]byte(Reset)) + + // Base 64 encode hash and truncate + return h.Sum(nil) +} diff --git a/interfaces/preimage/types.go b/interfaces/preimage/types.go index bd6226e623594561ef0338210af8824c1b789f27..7067c87a538f10aea3e778c255146c27277c418d 100644 --- a/interfaces/preimage/types.go +++ b/interfaces/preimage/types.go @@ -3,6 +3,7 @@ package preimage const ( Default = "default" Request = "request" + Reset = "reset" Confirm = "confirm" Silent = "silent" E2e = "e2e" diff --git a/interfaces/restoreContacts.go b/interfaces/restoreContacts.go new file mode 100644 index 0000000000000000000000000000000000000000..defc6877b4310d964425b6f2555df64a2a5a28d9 --- /dev/null +++ b/interfaces/restoreContacts.go @@ -0,0 +1,19 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package interfaces + +// RestoreContactsUpdater interface provides a callback function +// for receiving update information from RestoreContactsFromBackup. +type RestoreContactsUpdater interface { + // RestoreContactsCallback is called to report the current # of contacts + // that have been found and how many have been restored + // against the total number that need to be + // processed. If an error occurs it it set on the err variable as a + // plain string. + RestoreContactsCallback(numFound, numRestored, total int, err string) +} diff --git a/interfaces/user/proto.go b/interfaces/user/proto.go index 6b9cf1e3a6b338a08abd386b92772314b5f499e3..657c2e714d70f06b88c6d0866ec2bbca5015f24e 100644 --- a/interfaces/user/proto.go +++ b/interfaces/user/proto.go @@ -23,10 +23,6 @@ type Proto struct { TransmissionRegValidationSig []byte ReceptionRegValidationSig []byte - //cmix Identity - CmixDhPrivateKey *cyclic.Int - CmixDhPublicKey *cyclic.Int - //e2e Identity E2eDhPrivateKey *cyclic.Int E2eDhPublicKey *cyclic.Int diff --git a/interfaces/user/user.go b/interfaces/user/user.go index 56f260d03527ce2c605c5fe781f3be3935cc106c..5dc559917a7622710f356936c3b3f2f1985651f2 100644 --- a/interfaces/user/user.go +++ b/interfaces/user/user.go @@ -8,6 +8,7 @@ package user import ( + "gitlab.com/elixxir/crypto/backup" "gitlab.com/elixxir/crypto/contact" "gitlab.com/elixxir/crypto/cyclic" "gitlab.com/elixxir/primitives/fact" @@ -27,10 +28,6 @@ type User struct { // Timestamp in which user has registered with the network RegistrationTimestamp int64 - //cmix Identity - CmixDhPrivateKey *cyclic.Int - CmixDhPublicKey *cyclic.Int - //e2e Identity E2eDhPrivateKey *cyclic.Int E2eDhPublicKey *cyclic.Int @@ -54,9 +51,22 @@ func NewUserFromProto(proto *Proto) User { ReceptionRSA: proto.ReceptionRSA, Precanned: proto.Precanned, RegistrationTimestamp: proto.RegistrationTimestamp, - CmixDhPrivateKey: proto.CmixDhPrivateKey, - CmixDhPublicKey: proto.CmixDhPublicKey, E2eDhPrivateKey: proto.E2eDhPrivateKey, E2eDhPublicKey: proto.E2eDhPublicKey, } } + +func NewUserFromBackup(backup *backup.Backup) User { + return User{ + TransmissionID: backup.TransmissionIdentity.ComputedID, + TransmissionSalt: backup.TransmissionIdentity.Salt, + TransmissionRSA: backup.TransmissionIdentity.RSASigningPrivateKey, + ReceptionID: backup.ReceptionIdentity.ComputedID, + ReceptionSalt: backup.ReceptionIdentity.Salt, + ReceptionRSA: backup.ReceptionIdentity.RSASigningPrivateKey, + Precanned: false, + RegistrationTimestamp: backup.RegistrationTimestamp, + E2eDhPrivateKey: backup.ReceptionIdentity.DHPrivateKey, + E2eDhPublicKey: backup.ReceptionIdentity.DHPublicKey, + } +} diff --git a/keyExchange/rekey.go b/keyExchange/rekey.go index 04bb31413af965f621884a01ab0f557c632ec504..ea14e92fa37bda8a2826eca10e43112fc1661530 100644 --- a/keyExchange/rekey.go +++ b/keyExchange/rekey.go @@ -164,8 +164,8 @@ func negotiate(instance *network.Instance, sendE2E interfaces.SendE2E, session, msgID) err = session.TrySetNegotiationStatus(e2e.Sent) if err != nil { - if (session.NegotiationStatus() == e2e.NewSessionTriggered) { - msg := fmt.Sprintf("All channels exhausted for %s, " + + if session.NegotiationStatus() == e2e.NewSessionTriggered { + msg := fmt.Sprintf("All channels exhausted for %s, "+ "rekey impossible.", session) return errors.WithMessage(err, msg) } diff --git a/network/follow.go b/network/follow.go index 6f98a33b50127d78f10e703b1c75081d02357d0f..817ba0effc0c01e744cd430effa7ecc874d746fb 100644 --- a/network/follow.go +++ b/network/follow.go @@ -372,7 +372,7 @@ func (m *manager) follow(report interfaces.ClientErrorReport, rng csprng.Source, var roundsWithMessages2 []id.Round - if !m.param.RealtimeOnly{ + if !m.param.RealtimeOnly { roundsWithMessages2 = identity.UR.Iterate(func(rid id.Round) bool { if gwRoundsState.Checked(rid) { return rounds.Checker(rid, filterList, identity.CR) @@ -381,7 +381,6 @@ func (m *manager) follow(report interfaces.ClientErrorReport, rng csprng.Source, }, roundsUnknown, abandon) } - for _, rid := range roundsWithMessages { //denote that the round has been looked at in the tracking store if identity.CR.Check(rid) { diff --git a/network/gateway/hostPool.go b/network/gateway/hostPool.go index 43ae1481b9dcb01f732b12e6d6234fbc29acf5ff..2f9cfa67b216745c32f3a3f5178f279d2be24b4f 100644 --- a/network/gateway/hostPool.go +++ b/network/gateway/hostPool.go @@ -33,10 +33,18 @@ import ( "time" ) -// List of errors that initiate a Host replacement -var errorsList = []string{context.DeadlineExceeded.Error(), "connection refused", "host disconnected", - "transport is closing", balancer.ErrTransientFailure.Error(), "Last try to connect", - ndf.NO_NDF, "Host is in cool down", grpc.ErrClientConnClosing.Error()} +// List of errors that initiate a Host replacement. +var errorsList = []string{ + context.DeadlineExceeded.Error(), + "connection refused", + "host disconnected", + "transport is closing", + balancer.ErrTransientFailure.Error(), + "Last try to connect", + ndf.NO_NDF, + "Host is in cool down", + grpc.ErrClientConnClosing.Error(), +} // HostManager Interface allowing storage and retrieval of Host objects type HostManager interface { @@ -46,7 +54,7 @@ type HostManager interface { } // Filter filters out IDs from the provided map based on criteria in the NDF. -// The passed in map is a map of the NDF for easier acesss. The map is ID -> index in the NDF +// The passed in map is a map of the NDF for easier access. The map is ID -> index in the NDF // There is no multithreading, the filter function can either edit the passed map or make a new one // and return it. The general pattern is to loop through the map, then look up data about the node // in the ndf to make a filtering decision, then add them to a new map if they are accepted. @@ -74,29 +82,32 @@ type HostPool struct { // PoolParams Allows configuration of HostPool parameters type PoolParams struct { - MaxPoolSize uint32 // Maximum number of Hosts in the HostPool - PoolSize uint32 // Allows override of HostPool size. Set to zero for dynamic size calculation - ProxyAttempts uint32 // How many proxies will be used in event of send failure - MaxPings uint32 // How many gateways to concurrently test when initializing HostPool. Disabled if zero. - HostParams connect.HostParams // Parameters for the creation of new Host objects + MaxPoolSize uint32 // Maximum number of Hosts in the HostPool + PoolSize uint32 // Allows override of HostPool size. Set to zero for dynamic size calculation + ProxyAttempts uint32 // How many proxies will be used in event of send failure + MaxPings uint32 // How many gateways to concurrently test when initializing HostPool. Disabled if zero. + ForceConnection bool // Flag determining whether Host connections are initialized when added to HostPool + HostParams connect.HostParams // Parameters for the creation of new Host objects } // DefaultPoolParams Returns a default set of PoolParams func DefaultPoolParams() PoolParams { p := PoolParams{ - MaxPoolSize: 30, - ProxyAttempts: 5, - PoolSize: 0, - MaxPings: 0, - HostParams: connect.GetDefaultHostParams(), + MaxPoolSize: 30, + ProxyAttempts: 5, + PoolSize: 0, + MaxPings: 0, + ForceConnection: false, + HostParams: connect.GetDefaultHostParams(), } p.HostParams.MaxRetries = 1 + p.HostParams.MaxSendRetries = 1 p.HostParams.AuthEnabled = false p.HostParams.EnableCoolOff = false p.HostParams.NumSendsBeforeCoolOff = 1 p.HostParams.CoolOffTimeout = 5 * time.Minute - p.HostParams.SendTimeout = 2000 * time.Millisecond - p.HostParams.PingTimeout = 1 * time.Second + p.HostParams.SendTimeout = 1000 * time.Millisecond + p.HostParams.PingTimeout = 1000 * time.Millisecond return p } @@ -185,8 +196,16 @@ func (h *HostPool) initialize(startIdx uint32) error { } // Randomly shuffle gateways in NDF - randomGateways := make([]ndf.Gateway, len(h.ndf.Gateways)) - copy(randomGateways, h.ndf.Gateways) + randomGateways := make([]ndf.Gateway, 0, len(h.ndf.Gateways)) + + // Filter out not active gateways + for i := 0; i < len(h.ndf.Gateways); i++ { + if h.ndf.Nodes[i].Status == ndf.Active { + randomGateways = append(randomGateways, h.ndf.Gateways[i]) + } + } + + // Randomize the gateway order var rndBytes [32]byte stream := h.rng.GetStream() _, err := stream.Read(rndBytes[:]) @@ -244,14 +263,15 @@ func (h *HostPool) initialize(startIdx uint32) error { } // Ping the Host latency and send the result - jww.DEBUG.Printf("Testing host %s...", gwId.String()) + jww.DEBUG.Printf("Testing host %s...", gwId) latency, _ := newHost.IsOnline() c <- gatewayDuration{gwId, latency} }() } // Collect ping results - timer := time.NewTimer(2 * h.poolParams.HostParams.PingTimeout) + pingTimeout := 2 * h.poolParams.HostParams.PingTimeout + timer := time.NewTimer(pingTimeout) innerLoop: for { select { @@ -260,16 +280,18 @@ func (h *HostPool) initialize(startIdx uint32) error { if gw.latency > 0 { resultList = append(resultList, gw) jww.DEBUG.Printf("Adding HostPool result %d/%d: %s: %d", - len(resultList), numGatewaysToTry, gw.id.String(), gw.latency) + len(resultList), numGatewaysToTry, gw.id, gw.latency) } // Break if we have all needed slots if uint32(len(resultList)) == numGatewaysToTry { exit = true + timer.Stop() break innerLoop } case <-timer.C: - jww.INFO.Printf("HostPool initialization timed out!") + jww.INFO.Printf("HostPool initialization timed out after %s.", + pingTimeout) break innerLoop } } @@ -302,23 +324,23 @@ func (h *HostPool) initialize(startIdx uint32) error { return nil } -// UpdateNdf Mutates internal ndf to the given ndf +// UpdateNdf mutates internal NDF to the given NDF func (h *HostPool) UpdateNdf(ndf *ndf.NetworkDefinition) { if len(ndf.Gateways) == 0 { jww.WARN.Printf("Unable to UpdateNdf: no gateways available") return } + // Lock order is extremely important here + h.hostMux.Lock() h.ndfMux.Lock() h.ndf = ndf.DeepCopy() - - h.hostMux.Lock() err := h.updateConns() - h.hostMux.Unlock() if err != nil { jww.ERROR.Printf("Unable to updateConns: %+v", err) } h.ndfMux.Unlock() + h.hostMux.Unlock() } // SetFilter sets the filter used to filter gateways from the ID map. @@ -462,7 +484,7 @@ func (h *HostPool) checkReplace(hostId *id.ID, hostErr error) (bool, error) { } h.hostMux.Unlock() } - return doReplace, err + return doReplace && err == nil, err } // Select a viable HostPool candidate from the NDF @@ -486,9 +508,9 @@ func (h *HostPool) selectGateway() *id.ID { nodeId := gwId.DeepCopy() nodeId.SetType(id.Node) nodeNdfIdx := h.ndfMap[*nodeId] - isNodeStale := h.ndf.Nodes[nodeNdfIdx].Status == ndf.Stale - if isNodeStale { - jww.DEBUG.Printf("Ignoring stale node: %s", nodeId.String()) + isNodeIsNotActive := h.ndf.Nodes[nodeNdfIdx].Status != ndf.Active + if isNodeIsNotActive { + jww.DEBUG.Printf("Ignoring stale node: %s", nodeId) continue } @@ -538,7 +560,7 @@ func (h *HostPool) replaceHostNoStore(newId *id.ID, oldPoolIndex uint32) error { // Use the GwId to keep track of the new random Host's index in the hostList h.hostMap[*newId] = oldPoolIndex - // Clean up and move onto next Host + // Clean up and disconnect old Host oldHostIDStr := "unknown" if oldHost != nil { oldHostIDStr = oldHost.GetId().String() @@ -546,9 +568,19 @@ func (h *HostPool) replaceHostNoStore(newId *id.ID, oldPoolIndex uint32) error { go oldHost.Disconnect() } - jww.DEBUG.Printf("Replaced Host at %d [%s] with new Host %s", oldPoolIndex, oldHostIDStr, - newId.String()) + // Manually connect the new Host + if h.poolParams.ForceConnection { + go func() { + err := newHost.Connect() + if err != nil { + jww.WARN.Printf("Unable to initialize Host connection to %s: "+ + "%+v", newId, err) + } + }() + } + jww.DEBUG.Printf("Replaced Host at %d [%s] with new Host %s", + oldPoolIndex, oldHostIDStr, newId) return nil } @@ -652,7 +684,6 @@ func (h *HostPool) addGateway(gwId *id.ID, ndfIndex int) { // Check if the host exists host, ok := h.manager.GetHost(gwId) if !ok { - // Check if gateway ID collides with an existing hard coded ID if id.CollidesWithHardCodedID(gwId) { jww.ERROR.Printf("Gateway ID invalid, collides with a "+ @@ -660,7 +691,9 @@ func (h *HostPool) addGateway(gwId *id.ID, ndfIndex int) { } // Add the new gateway host - _, err := h.manager.AddHost(gwId, gw.Address, []byte(gw.TlsCertificate), h.poolParams.HostParams) + _, err := h.manager.AddHost( + gwId, gw.Address, []byte(gw.TlsCertificate), + h.poolParams.HostParams) if err != nil { jww.ERROR.Printf("Could not add gateway host %s: %+v", gwId, err) } @@ -675,7 +708,8 @@ func (h *HostPool) addGateway(gwId *id.ID, ndfIndex int) { select { case h.addGatewayChan <- ng: default: - jww.WARN.Printf("Unable to send AddGateway event for id %s", gwId.String()) + jww.WARN.Printf( + "Unable to send AddGateway event for id %s", gwId) } } @@ -712,9 +746,9 @@ func readUint32(rng io.Reader) uint32 { // readRangeUint32 reduces an integer from 0, MaxUint32 to the range start, end func readRangeUint32(start, end uint32, rng io.Reader) uint32 { size := end - start - // note we could just do the part inside the () here, but then extra - // can == size which means a little bit of range is wastes, either - // choice seems negligible so we went with the "more correct" + // Note that we could just do the part inside the () here, but then extra + // can == size which means a little range is wasted; either choice seems + // negligible, so we went with the "more correct" extra := (math.MaxUint32%size + 1) % size limit := math.MaxUint32 - extra // Loop until we read something inside the limit diff --git a/network/gateway/hostpool_test.go b/network/gateway/hostpool_test.go index 5e93ba8df80ded94f70034bdc6ba3540ac6d1644..30f3b6a6151626a68e5d2b2fe4d3c282de0aefeb 100644 --- a/network/gateway/hostpool_test.go +++ b/network/gateway/hostpool_test.go @@ -485,11 +485,12 @@ func TestHostPool_UpdateNdf(t *testing.T) { // Construct a manager (bypass business logic in constructor) hostPool := &HostPool{ - manager: manager, - hostList: make([]*connect.Host, newIndex+1), - hostMap: make(map[id.ID]uint32), - ndf: testNdf, - storage: storage.InitTestingSession(t), + manager: manager, + hostList: make([]*connect.Host, newIndex+1), + hostMap: make(map[id.ID]uint32), + ndf: testNdf, + storage: storage.InitTestingSession(t), + poolParams: DefaultPoolParams(), filter: func(m map[id.ID]int, _ *ndf.NetworkDefinition) map[id.ID]int { return m }, @@ -855,6 +856,7 @@ func TestHostPool_AddGateway(t *testing.T) { hostList: make([]*connect.Host, newIndex+1), hostMap: make(map[id.ID]uint32), ndf: testNdf, + poolParams: params, addGatewayChan: make(chan network.NodeGateway), storage: storage.InitTestingSession(t), } @@ -888,6 +890,7 @@ func TestHostPool_RemoveGateway(t *testing.T) { hostList: make([]*connect.Host, newIndex+1), hostMap: make(map[id.ID]uint32), ndf: testNdf, + poolParams: params, addGatewayChan: make(chan network.NodeGateway), storage: storage.InitTestingSession(t), rng: fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG), diff --git a/network/gateway/sender.go b/network/gateway/sender.go index b3b029f369bd91ada4f023f5fd77bb336e682177..dae588f4f355130749f7c16057afd0b301b0b6ca 100644 --- a/network/gateway/sender.go +++ b/network/gateway/sender.go @@ -51,27 +51,26 @@ func (s *Sender) SendToAny(sendFunc func(host *connect.Host) (interface{}, error return nil, errors.Errorf(stoppable.ErrMsg, stop.Name(), "SendToAny") } else if err == nil { return result, nil - } else if strings.Contains(err.Error(), RetryableError) { - // Retry of the proxy could not communicate - jww.INFO.Printf("Unable to SendToAny via %s: non-fatal error received, retrying: %s", - proxies[proxy].GetId().String(), err) - } else if strings.Contains(err.Error(), "unable to connect to target host") || - strings.Contains(err.Error(), "unable to find target host") { - // Retry of the proxy could not communicate - jww.WARN.Printf("Unable to SendToAny via %s: %s,"+ - " proxy could not contact requested host", - proxies[proxy].GetId(), err) - continue - } else if replaced, checkReplaceErr := s.checkReplace(proxies[proxy].GetId(), err); replaced { - if checkReplaceErr != nil { + } else { + // Now we must check whether the Host should be replaced + replaced, checkReplaceErr := s.checkReplace(proxies[proxy].GetId(), err) + if replaced { jww.WARN.Printf("Unable to SendToAny, replaced a proxy %s with error %s", - proxies[proxy].GetId().String(), checkReplaceErr) + proxies[proxy].GetId().String(), err.Error()) } else { - jww.WARN.Printf("Unable to SendToAny, replaced a proxy %s", - proxies[proxy].GetId().String()) + if checkReplaceErr != nil { + jww.WARN.Printf("Unable to SendToAny via %s: %s. Unable to replace host: %+v", + proxies[proxy].GetId().String(), err.Error(), checkReplaceErr) + } else { + jww.WARN.Printf("Unable to SendToAny via %s: %s. Did not replace host.", + proxies[proxy].GetId().String(), err.Error()) + } + } + + // End for non-retryable errors + if !strings.Contains(err.Error(), RetryableError) { + return nil, errors.WithMessage(err, "Received error with SendToAny") } - } else { - return nil, errors.WithMessage(err, "Received error with SendToAny") } } @@ -107,34 +106,26 @@ func (s *Sender) SendToPreferred(targets []*id.ID, sendFunc sendToPreferredFunc, return nil, errors.Errorf(stoppable.ErrMsg, stop.Name(), "SendToPreferred") } else if err == nil { return result, nil - } else if strings.Contains(err.Error(), RetryableError) { - // Retry of the proxy could not communicate - jww.INFO.Printf("Unable to to SendToPreferred first pass %s via %s: non-fatal error received, retrying: %s", - targets[i], targetHosts[i].GetId(), err) - } else if strings.Contains(err.Error(), "unable to connect to target host") || - strings.Contains(err.Error(), "unable to find target host") { - // Retry of the proxy could not communicate - jww.WARN.Printf("Unable to SendToPreferred first pass %s via %s: %s, "+ - "proxy could not contact requested host", - targets[i], targetHosts[i].GetId(), err) - continue - } else if replaced, checkReplaceErr := s.checkReplace(targetHosts[i].GetId(), err); replaced { - if checkReplaceErr != nil { - jww.WARN.Printf("Unable to SendToPreferred first pass %s via %s, "+ - "proxy failed, was replaced with error: %s", - targets[i], targetHosts[i].GetId(), checkReplaceErr) + } else { + // Now we must check whether the Host should be replaced + replaced, checkReplaceErr := s.checkReplace(targetHosts[i].GetId(), err) + if replaced { + jww.WARN.Printf("Unable to SendToPreferred first pass via %s, replaced a proxy %s with error %s", + targets[i], targetHosts[i].GetId(), err.Error()) } else { - jww.WARN.Printf("Unable to SendToPreferred first pass %s via %s, "+ - "proxy failed, was replaced", - targets[i], targetHosts[i].GetId()) + if checkReplaceErr != nil { + jww.WARN.Printf("Unable to SendToPreferred first pass %s via %s: %s. Unable to replace host: %+v", + targets[i], targetHosts[i].GetId(), err.Error(), checkReplaceErr) + } else { + jww.WARN.Printf("Unable to SendToPreferred first pass %s via %s: %s. Did not replace host.", + targets[i], targetHosts[i].GetId(), err.Error()) + } + } + + // End for non-retryable errors + if !strings.Contains(err.Error(), RetryableError) { + return nil, errors.WithMessage(err, "Received error with SendToPreferred") } - jww.WARN.Printf("Unable to SendToPreferred first pass %s via %s: %s, proxy failed, was replaced", - targets[i], targetHosts[i].GetId(), checkReplaceErr) - continue - } else { - jww.WARN.Printf("Unable to SendToPreferred first pass %s via %s: %s, comm returned an error", - targets[i], targetHosts[i].GetId(), err) - return result, err } } @@ -182,29 +173,30 @@ func (s *Sender) SendToPreferred(targets []*id.ID, sendFunc sendToPreferredFunc, // Retry of the proxy could not communicate jww.INFO.Printf("Unable to SendToPreferred second pass %s via %s: non-fatal error received, retrying: %s", target, proxy, err) - } else if strings.Contains(err.Error(), "unable to connect to target host") || - strings.Contains(err.Error(), "unable to find target host") { - // Retry of the proxy could not communicate - jww.WARN.Printf("Unable to SendToPreferred second pass %s via %s: %s,"+ - " proxy could not contact requested host", - target, proxy, err) continue - } else if replaced, checkReplaceErr := s.checkReplace(proxy.GetId(), err); replaced { - if checkReplaceErr != nil { - jww.WARN.Printf("Unable to SendToPreferred second pass %s via %s,"+ - "proxy failed, was replaced with error: %s", target, proxy.GetId(), - checkReplaceErr) + } else if err == nil { + return result, nil + } else { + // Now we must check whether the Host should be replaced + replaced, checkReplaceErr := s.checkReplace(proxy.GetId(), err) + badProxies[proxy.String()] = nil + if replaced { + jww.WARN.Printf("Unable to SendToPreferred second pass via %s, replaced a proxy %s with error %s", + target, proxy.GetId(), err.Error()) } else { - jww.WARN.Printf("Unable to SendToPreferred second pass %s via %s, "+ - "proxy failed, was replaced", target, proxy.GetId()) + if checkReplaceErr != nil { + jww.WARN.Printf("Unable to SendToPreferred second pass %s via %s: %s. Unable to replace host: %+v", + target, proxy.GetId(), err.Error(), checkReplaceErr) + } else { + jww.WARN.Printf("Unable to SendToPreferred second pass %s via %s: %s. Did not replace host.", + target, proxy.GetId(), err.Error()) + } } - badProxies[proxy.String()] = nil - continue - } else { - jww.WARN.Printf("Unable to SendToPreferred second pass %s via %s: %s, comm returned an error", - target, proxy.GetId(), err) - return result, err + // End for non-retryable errors + if !strings.Contains(err.Error(), RetryableError) { + return nil, errors.WithMessage(err, "Received error with SendToPreferred") + } } } } diff --git a/network/manager.go b/network/manager.go index bba264c6b63263a196d4bfc135f1db42649948ec..04b1fcfd15c035057da4efe263fc3b32f535434d 100644 --- a/network/manager.go +++ b/network/manager.go @@ -11,9 +11,12 @@ package network // and intraclient state are accessible through the context object. import ( - "crypto/rand" "encoding/binary" "fmt" + "math" + "sync/atomic" + "time" + "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/interfaces" @@ -34,9 +37,6 @@ import ( "gitlab.com/xx_network/crypto/csprng" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/ndf" - "math" - "sync/atomic" - "time" ) // fakeIdentityRange indicates the range generated between @@ -133,6 +133,7 @@ func NewManager(session *storage.Session, switchboard *switchboard.Switchboard, poolParams.HostParams.KaClientOpts.Time = time.Duration(math.MaxInt64) // Enable optimized HostPool initialization poolParams.MaxPings = 50 + poolParams.ForceConnection = true m.sender, err = gateway.NewSender(poolParams, rng, ndf, comms, session, m.NodeRegistration) if err != nil { @@ -184,7 +185,7 @@ func (m *manager) Follow(report interfaces.ClientErrorReport) (stoppable.Stoppab multi.Add(trackNetworkStopper) // Message reception - multi.Add(m.message.StartProcessies()) + multi.Add(m.message.StartProcesses()) // Round processing multi.Add(m.round.StartProcessors()) @@ -265,10 +266,12 @@ func (m *manager) SetFakeEarliestRound(rnd id.Round) { // GetFakeEarliestRound generates a random earliest round for a fake identity. func (m *manager) GetFakeEarliestRound() id.Round { - b, err := csprng.Generate(8, rand.Reader) + rng := m.Rng.GetStream() + b, err := csprng.Generate(8, rng) if err != nil { jww.FATAL.Panicf("Could not get random number: %v", err) } + rng.Close() rangeVal := binary.LittleEndian.Uint64(b) % 800 diff --git a/network/message/garbled.go b/network/message/garbled.go index b2449ca9f0d79c8d1b5cdd7a8e9cd9db1ae2863f..8d0950f3d2a781e3f5836cb7ffa225f10e01c294 100644 --- a/network/message/garbled.go +++ b/network/message/garbled.go @@ -109,11 +109,11 @@ func (m *Manager) handleGarbledMessages() { MessageType: message.Raw, Sender: &id.ID{}, EphemeralID: ephemeral.Id{}, - Timestamp: time.Time{}, + Timestamp: time.Unix(0, 0), Encryption: message.None, RecipientID: &id.ID{}, RoundId: 0, - RoundTimestamp: time.Time{}, + RoundTimestamp: time.Unix(0, 0), } im := fmt.Sprintf("[GARBLE] RAW Message reprocessed: keyFP: %v, "+ "msgDigest: %s", grbldMsg.GetKeyFP(), grbldMsg.Digest()) diff --git a/network/message/garbled_test.go b/network/message/garbled_test.go index 4a0f7eda9cb50fe7e7ed61aee72040d3756edb10..86f23b84776abbf2d2e9eae52fec21a690540ad9 100644 --- a/network/message/garbled_test.go +++ b/network/message/garbled_test.go @@ -152,7 +152,7 @@ func TestManager_CheckGarbledMessages(t *testing.T) { copy(fmp.Timestamp, ts) msg.SetContents(fmp.Bytes()) encryptedMsg := key.Encrypt(msg) - msg.SetIdentityFP(fingerprint.IdentityFP( msg.GetContents(), preimage.Data)) + msg.SetIdentityFP(fingerprint.IdentityFP(msg.GetContents(), preimage.Data)) i.Session.GetGarbledMessages().Add(encryptedMsg) stop := stoppable.NewSingle("stop") diff --git a/network/message/handler.go b/network/message/handler.go index 7aa63e6ad85293b57d716d9bdf894736a3fd4bed..1c89d9a53c68beffc9f44d0a3b83994949dfdf3f 100644 --- a/network/message/handler.go +++ b/network/message/handler.go @@ -111,15 +111,17 @@ func (m *Manager) handleMessage(ecrMsg format.Message, bundle Bundle, edge *edge // and add it to garbled messages to be handled later msg = ecrMsg raw := message.Receive{ - Payload: msg.Marshal(), - MessageType: message.Raw, - Sender: &id.ID{}, - EphemeralID: identity.EphId, - Timestamp: time.Time{}, - Encryption: message.None, - RecipientID: identity.Source, - RoundId: id.Round(bundle.RoundInfo.ID), - RoundTimestamp: time.Unix(0, int64(bundle.RoundInfo.Timestamps[states.QUEUED])), + Payload: msg.Marshal(), + MessageType: message.Raw, + Sender: &id.ID{}, + EphemeralID: identity.EphId, + Timestamp: time.Time{}, + Encryption: message.None, + RecipientID: identity.Source, + RoundId: id.Round(bundle.RoundInfo.ID), + // We use PRECOMPUTING here because all Rounds have that timestamp available to them + // QUEUED can be missing sometimes and cause a lot of hidden problems further down the line + RoundTimestamp: time.Unix(0, int64(bundle.RoundInfo.Timestamps[states.PRECOMPUTING])), } im := fmt.Sprintf("Received message of type Garbled/RAW: keyFP: %v, round: %d, "+ "msgDigest: %s", msg.GetKeyFP(), bundle.Round, msg.Digest()) diff --git a/network/message/manager.go b/network/message/manager.go index 0ac5eeceb0f892ef6ef7d4b0e0f578904e1901a3..ae51c15520e5cbdd56b494578a9bff60180fc405 100644 --- a/network/message/manager.go +++ b/network/message/manager.go @@ -39,17 +39,19 @@ func NewManager(internal internal.Internal, param params.Network, m := Manager{ param: param, partitioner: parse.NewPartitioner(dummyMessage.ContentsSize(), internal.Session), + Internal: internal, + sender: sender, + blacklistedNodes: make(map[string]interface{}, len(param.BlacklistedNodes)), messageReception: make(chan Bundle, param.MessageReceptionBuffLen), + nodeRegistration: nodeRegistration, networkIsHealthy: make(chan bool, 1), triggerGarbled: make(chan struct{}, 100), - nodeRegistration: nodeRegistration, - sender: sender, - Internal: internal, } for _, nodeId := range param.BlacklistedNodes { decodedId, err := base64.StdEncoding.DecodeString(nodeId) if err != nil { - jww.ERROR.Printf("Unable to decode blacklisted Node ID %s: %+v", decodedId, err) + jww.ERROR.Printf("Unable to decode blacklisted Node ID %s: %+v", + decodedId, err) continue } m.blacklistedNodes[string(decodedId)] = nil @@ -57,29 +59,29 @@ func NewManager(internal internal.Internal, param params.Network, return &m } -//Gets the channel to send received messages on +// GetMessageReceptionChannel gets the channel to send received messages on. func (m *Manager) GetMessageReceptionChannel() chan<- Bundle { return m.messageReception } -//Starts all worker pool -func (m *Manager) StartProcessies() stoppable.Stoppable { +// StartProcesses starts all worker pool. +func (m *Manager) StartProcesses() stoppable.Stoppable { multi := stoppable.NewMulti("MessageReception") - //create the message handler workers + // Create the message handler workers for i := uint(0); i < m.param.MessageReceptionWorkerPoolSize; i++ { stop := stoppable.NewSingle(fmt.Sprintf("MessageReception Worker %v", i)) go m.handleMessages(stop) multi.Add(stop) } - //create the critical messages thread + // Create the critical messages thread critStop := stoppable.NewSingle("CriticalMessages") go m.processCriticalMessages(critStop) m.Health.AddChannel(m.networkIsHealthy) multi.Add(critStop) - //create the garbled messages thread + // Create the garbled messages thread garbledStop := stoppable.NewSingle("GarbledMessages") go m.processGarbledMessages(garbledStop) multi.Add(garbledStop) diff --git a/network/message/sendCmix.go b/network/message/sendCmix.go index a7db6d51f80e38bad648ccff338dc68409d6cb05..82b4d21e56bbb86605fcfcd56458d592a21585ee 100644 --- a/network/message/sendCmix.go +++ b/network/message/sendCmix.go @@ -45,11 +45,9 @@ func (m *Manager) SendCMIX(sender *gateway.Sender, msg format.Message, } func calculateSendTimeout(best *pb.RoundInfo, max time.Duration) time.Duration { - RoundStartTime := time.Unix(0, - int64(best.Timestamps[states.QUEUED])) + RoundStartTime := time.Unix(0, int64(best.Timestamps[states.QUEUED])) // 250ms AFTER the round starts to hear the response. - timeout := RoundStartTime.Sub( - netTime.Now().Add(250 * time.Millisecond)) + timeout := RoundStartTime.Sub(netTime.Now().Add(250 * time.Millisecond)) if timeout > max { timeout = max } @@ -128,9 +126,6 @@ func sendCmixHelper(sender *gateway.Sender, msg format.Message, } jww.TRACE.Printf("[SendCMIX-%s] bestRound: %v", cmixParams.DebugTag, bestRound) - // add the round on to the list of attempted, so it is not tried again - attempted.Insert(bestRound.GetRoundId()) - // Determine whether the selected round contains any Nodes // that are blacklisted by the params.Network object containsBlacklisted := false @@ -184,7 +179,7 @@ func sendCmixHelper(sender *gateway.Sender, msg format.Message, timeout = calculatedTimeout } - //send the message + // send the message result, err := comms.SendPutMessage(host, wrappedMsg, timeout) jww.TRACE.Printf("[SendCMIX-%s]sendFunc %s putmsg", cmixParams.DebugTag, host) diff --git a/network/message/sendManyCmix.go b/network/message/sendManyCmix.go index 29e766edd90738a3e0f89afa8832c48f109b2f68..2e2b67fc716c50907a3dbfb9a14761291491c22d 100644 --- a/network/message/sendManyCmix.go +++ b/network/message/sendManyCmix.go @@ -102,10 +102,6 @@ func sendManyCmixHelper(sender *gateway.Sender, continue } - // Add the round on to the list of attempted rounds so that it is not - // tried again - attempted.Insert(bestRound.GetRoundId()) - // Determine whether the selected round contains any nodes that are // blacklisted by the params.Network object containsBlacklisted := false @@ -203,8 +199,9 @@ func sendManyCmixHelper(sender *gateway.Sender, param.DebugTag, ephemeralIDsString, recipientString, bestRound.ID, err) jww.INFO.Printf("[SendManyCMIX-%s]error received, continuing: %v", param.DebugTag, err) continue + } else { + jww.INFO.Printf("[SendManyCMIX-%s]Error received: %v", param.DebugTag, err) } - jww.INFO.Printf("error received: %v", err) return 0, []ephemeral.Id{}, err } @@ -212,14 +209,14 @@ func sendManyCmixHelper(sender *gateway.Sender, gwSlotResp := result.(*pb.GatewaySlotResponse) if gwSlotResp.Accepted { m := fmt.Sprintf("[SendManyCMIX-%s]Successfully sent to EphIDs %s (sources: [%s]) "+ - "in round %d", param.DebugTag, ephemeralIDsString, recipientString, bestRound.ID) + "in round %d (msgDigest: %s)", param.DebugTag, ephemeralIDsString, recipientString, bestRound.ID, msgDigests) jww.INFO.Print(m) events.Report(1, "MessageSendMany", "Metric", m) onSend(uint32(len(msgs)), session) return id.Round(bestRound.ID), ephemeralIDs, nil } else { - jww.FATAL.Panicf("Gateway %s returned no error, but failed to "+ - "accept message when sending to EphIDs [%s] (%s) on round %d", + jww.FATAL.Panicf("[SendManyCMIX-%s]Gateway %s returned no error, but failed to "+ + "accept message when sending to EphIDs [%s] (%s) on round %d", param.DebugTag, firstGateway, ephemeralIDsString, recipientString, bestRound.ID) } } diff --git a/network/message/utils_test.go b/network/message/utils_test.go index 80b31c9a911a49babe5862078fc864498f3fdb32..e0bf2222d6f36a6934fd3469cfa1222fe257bae1 100644 --- a/network/message/utils_test.go +++ b/network/message/utils_test.go @@ -17,10 +17,10 @@ func (mc *MockSendCMIXComms) GetHost(*id.ID) (*connect.Host, bool) { nid1 := id.NewIdFromString("zezima", id.Node, mc.t) gwID := nid1.DeepCopy() gwID.SetType(id.Gateway) - h, _ := connect.NewHost(gwID, "0.0.0.0", []byte(""), connect.HostParams{ - MaxRetries: 0, - AuthEnabled: false, - }) + p := connect.GetDefaultHostParams() + p.MaxRetries = 0 + p.AuthEnabled = false + h, _ := connect.NewHost(gwID, "0.0.0.0", []byte(""), p) return h, true } diff --git a/network/node/register.go b/network/node/register.go index 4336575dee390d0b948b914595d9bb15cfafd5bd..9bf78dd36886d0be7a8f2b1a5c9d875c53daf165 100644 --- a/network/node/register.go +++ b/network/node/register.go @@ -21,6 +21,7 @@ import ( pb "gitlab.com/elixxir/comms/mixmessages" "gitlab.com/elixxir/comms/network" "gitlab.com/elixxir/crypto/cyclic" + "gitlab.com/elixxir/crypto/diffieHellman" "gitlab.com/elixxir/crypto/fastRNG" "gitlab.com/elixxir/crypto/hash" "gitlab.com/elixxir/crypto/registration" @@ -38,6 +39,10 @@ import ( "time" ) +const maxAttempts = 5 + +var delayTable = [5]time.Duration{0, 5 * time.Second, 30 * time.Second, 60 * time.Second, 120 * time.Second} + type RegisterNodeCommsInterface interface { SendRequestClientKeyMessage(host *connect.Host, message *pb.SignedClientKeyRequest) (*pb.SignedKeyResponse, error) @@ -49,11 +54,14 @@ func StartRegistration(sender *gateway.Sender, session *storage.Session, rngGen multi := stoppable.NewMulti("NodeRegistrations") inProgess := &sync.Map{} + // we are relying on the in progress check to + // ensure there is only a single operator at a time, as a result this is a map of ID -> int + attempts := &sync.Map{} for i := uint(0); i < numParallel; i++ { stop := stoppable.NewSingle(fmt.Sprintf("NodeRegistration %d", i)) - go registerNodes(sender, session, rngGen, comms, stop, c, inProgess) + go registerNodes(sender, session, rngGen, comms, stop, c, inProgess, attempts) multi.Add(stop) } @@ -62,7 +70,7 @@ func StartRegistration(sender *gateway.Sender, session *storage.Session, rngGen func registerNodes(sender *gateway.Sender, session *storage.Session, rngGen *fastRNG.StreamGenerator, comms RegisterNodeCommsInterface, - stop *stoppable.Single, c chan network.NodeGateway, inProgress *sync.Map) { + stop *stoppable.Single, c chan network.NodeGateway, inProgress, attempts *sync.Map) { u := session.User() regSignature := u.GetTransmissionRegistrationValidationSignature() // Timestamp in which user has registered with registration @@ -84,6 +92,14 @@ func registerNodes(sender *gateway.Sender, session *storage.Session, if _, operating := inProgress.LoadOrStore(nidStr, struct{}{}); operating { continue } + + //keep track of how many times this has been attempted + numAttempts := uint(1) + if nunAttemptsInterface, hasValue := attempts.LoadOrStore(nidStr, numAttempts); hasValue { + numAttempts = nunAttemptsInterface.(uint) + attempts.Store(nidStr, numAttempts+1) + } + // No need to register with stale nodes if isStale := gw.Node.Status == ndf.Stale; isStale { jww.DEBUG.Printf("Skipping registration with stale node %s", nidStr) @@ -93,7 +109,15 @@ func registerNodes(sender *gateway.Sender, session *storage.Session, regTimestamp, uci, cmix, rng, stop) inProgress.Delete(nidStr) if err != nil { - jww.ERROR.Printf("Failed to register node: %+v", err) + jww.ERROR.Printf("Failed to register node: %s", err.Error()) + //if we have not reached the attempt limit for this gateway, send it back into the channel to retry + if numAttempts < maxAttempts { + go func() { + //delay the send for a backoff + time.Sleep(delayTable[numAttempts-1]) + c <- gw + }() + } } case <-t.C: } @@ -154,7 +178,19 @@ func requestKey(sender *gateway.Sender, comms RegisterNodeCommsInterface, uci *user.CryptographicIdentity, store *cmix.Store, rng csprng.Source, stop *stoppable.Single) (*cyclic.Int, []byte, uint64, error) { - dhPub := store.GetDHPublicKey().Bytes() + grp := store.GetGroup() + + // FIXME: Why 256 bits? -- this is spec but not explained, it has + // to do with optimizing operations on one side and still preserves + // decent security -- cite this. + dhPrivBytes, err := csprng.GenerateInGroup(store.GetGroup().GetPBytes(), 256, rng) + if err != nil { + return nil, nil, 0, err + } + + dhPriv := grp.NewIntFromBytes(dhPrivBytes) + + dhPub := diffieHellman.GeneratePublicKey(dhPriv, grp) // Reconstruct client confirmation message userPubKeyRSA := rsa.CreatePublicKeyPem(uci.GetTransmissionRSA().GetPublic()) @@ -170,7 +206,7 @@ func requestKey(sender *gateway.Sender, comms RegisterNodeCommsInterface, RegistrarSignature: &messages.RSASignature{Signature: regSig}, ClientRegistrationConfirmation: confirmationSerialized, }, - ClientDHPubKey: dhPub, + ClientDHPubKey: dhPub.Bytes(), RegistrationTimestamp: registrationTimestampNano, RequestTimestamp: netTime.Now().UnixNano(), } @@ -211,7 +247,7 @@ func requestKey(sender *gateway.Sender, comms RegisterNodeCommsInterface, Target: gatewayID.Bytes(), }) if err != nil { - return nil, errors.WithMessage(err, "Register: Failed requesting client key from gateway") + return nil, errors.WithMessage(err, fmt.Sprintf("Register: Failed requesting client key from gateway %s", gatewayID.String())) } if keyResponse.Error != "" { return nil, errors.WithMessage(err, "requestKey: clientKeyResponse error") @@ -262,12 +298,11 @@ func requestKey(sender *gateway.Sender, comms RegisterNodeCommsInterface, h.Reset() // Convert Node DH Public key to a cyclic.Int - grp := store.GetGroup() nodeDHPub := grp.NewIntFromBytes(keyResponse.NodeDHPubKey) // Construct the session key sessionKey := registration.GenerateBaseKey(grp, - nodeDHPub, store.GetDHPrivateKey(), h) + nodeDHPub, dhPriv, h) // Verify the HMAC h.Reset() diff --git a/network/rounds/check.go b/network/rounds/check.go index 295a63f0abdb607660c0e6bc6865e7d026d608f6..39068df86502617f96edd55b07f6614f40db1860 100644 --- a/network/rounds/check.go +++ b/network/rounds/check.go @@ -56,11 +56,6 @@ func serializeRound(roundId id.Round) []byte { func (m *Manager) GetMessagesFromRound(roundID id.Round, identity reception.IdentityUse) { ri, err := m.Instance.GetRound(roundID) if err != nil || m.params.ForceHistoricalRounds { - if m.params.RealtimeOnly { - jww.WARN.Printf("Skipping round %d because it is not in ram and we are realtime only mode", - roundID) - return - } if m.params.ForceHistoricalRounds { jww.WARN.Printf("Forcing use of historical rounds for round ID %d.", roundID) @@ -68,7 +63,7 @@ func (m *Manager) GetMessagesFromRound(roundID id.Round, identity reception.Iden jww.INFO.Printf("Messages found in round %d for %d (%s), looking "+ "up messages via historical lookup", roundID, identity.EphId.Int64(), identity.Source) - //store the round as an unretreived round + //store the round as an unreceived round err = m.Session.UncheckedRounds().AddRound(roundID, nil, identity.Source, identity.EphId) if err != nil { @@ -84,7 +79,7 @@ func (m *Manager) GetMessagesFromRound(roundID id.Round, identity reception.Iden jww.INFO.Printf("Messages found in round %d for %d (%s), looking "+ "up messages via in ram lookup", roundID, identity.EphId.Int64(), identity.Source) - //store the round as an unretreived round + //store the round as an unreceived round if !m.params.RealtimeOnly { err = m.Session.UncheckedRounds().AddRound(roundID, ri, identity.Source, identity.EphId) diff --git a/network/rounds/historical.go b/network/rounds/historical.go index c3b9b4b9b4296ed776dfa7503f01b301ffc9e46d..39fd0b318d19db8613e57d2a9b335cc7e24577a3 100644 --- a/network/rounds/historical.go +++ b/network/rounds/historical.go @@ -120,7 +120,7 @@ func (m *Manager) processHistoricalRounds(comm historicalRoundsComms, stop *stop // process the returned historical roundRequests. for i, roundInfo := range response.Rounds { // The interface has missing returns returned as nil, such roundRequests - // need be be removes as processing so the network follower will + // need to be removes as processing so the network follower will // pick them up in the future. if roundInfo == nil { var errMsg string diff --git a/network/rounds/manager.go b/network/rounds/manager.go index f220583de8de98ab706b48db3a2b24d6a6168078..43d1f5f5a1e9acc4b09fc360d583d16e7e73c51e 100644 --- a/network/rounds/manager.go +++ b/network/rounds/manager.go @@ -58,12 +58,11 @@ func (m *Manager) StartProcessors() stoppable.Stoppable { } // Start the periodic unchecked round worker - if !m.params.RealtimeOnly{ + if !m.params.RealtimeOnly { stopper := stoppable.NewSingle("UncheckRound") go m.processUncheckedRounds(m.params.UncheckRoundPeriod, backOffTable, stopper) multi.Add(stopper) } - return multi } diff --git a/network/rounds/remoteFilters_test.go b/network/rounds/remoteFilters_test.go index 51d26973d9f024142ed89b04e57d8f7117992268..e490432857d5e94bf599d3b7eb3c4b1a66ee8017 100644 --- a/network/rounds/remoteFilters_test.go +++ b/network/rounds/remoteFilters_test.go @@ -20,7 +20,7 @@ import ( ) func TestMain(m *testing.M) { - jww.SetStdoutThreshold(jww.LevelTrace) + jww.SetStdoutThreshold(jww.LevelDebug) connect.TestingOnlyDisableTLS = true os.Exit(m.Run()) } @@ -98,4 +98,4 @@ func TestRemoteFilter_FirstLastRound(t *testing.T) { "\n\tExpected: %v\n\tReceived: %v", receivedLastRound, firstRound+uint64(roundRange)) } -} \ No newline at end of file +} diff --git a/network/rounds/retrieve.go b/network/rounds/retrieve.go index 00f6675375243ef4025ad86f87751b578f185c1c..7151dd92de4d110e8a79f514e0dd4a1190e065a9 100644 --- a/network/rounds/retrieve.go +++ b/network/rounds/retrieve.go @@ -49,7 +49,7 @@ func (m *Manager) processMessageRetrieval(comms messageRetrievalComms, case rl := <-m.lookupRoundMessages: ri := rl.roundInfo jww.DEBUG.Printf("Checking for messages in round %d", ri.ID) - if !m.params.RealtimeOnly{ + if !m.params.RealtimeOnly { err := m.Session.UncheckedRounds().AddRound(id.Round(ri.ID), ri, rl.identity.Source, rl.identity.EphId) if err != nil { @@ -57,7 +57,6 @@ func (m *Manager) processMessageRetrieval(comms messageRetrievalComms, } } - // Convert gateways in round to proper ID format gwIds := make([]*id.ID, len(ri.Topology)) for i, idBytes := range ri.Topology { @@ -132,7 +131,7 @@ func (m *Manager) processMessageRetrieval(comms messageRetrievalComms, m.messageBundles <- bundle jww.DEBUG.Printf("Removing round %d from unchecked store", ri.ID) - if !m.params.RealtimeOnly{ + if !m.params.RealtimeOnly { err = m.Session.UncheckedRounds().Remove(id.Round(ri.ID), rl.identity.Source, rl.identity.EphId) if err != nil { jww.ERROR.Printf("Could not remove round %d "+ @@ -140,7 +139,6 @@ func (m *Manager) processMessageRetrieval(comms messageRetrievalComms, } } - } } @@ -196,14 +194,13 @@ func (m *Manager) getMessagesFromGateway(roundID id.Round, " in round %d. This happening every once in a while is normal,"+ " but can be indicative of a problem if it is consistent", m.TransmissionID, roundID) - if m.params.RealtimeOnly{ + if m.params.RealtimeOnly { err = m.Session.UncheckedRounds().Remove(roundID, identity.Source, identity.EphId) if err != nil { jww.ERROR.Printf("Failed to remove round %d: %+v", roundID, err) } } - return message.Bundle{}, nil } diff --git a/network/rounds/utils_test.go b/network/rounds/utils_test.go index 8779a68acdf4f0c929186ddb59b6a0ad26bc3f05..ea24534930b93b345ef2bff86de0f29f6663507b 100644 --- a/network/rounds/utils_test.go +++ b/network/rounds/utils_test.go @@ -62,10 +62,10 @@ func (mmrc *mockMessageRetrievalComms) RemoveHost(hid *id.ID) { } func (mmrc *mockMessageRetrievalComms) GetHost(hostId *id.ID) (*connect.Host, bool) { - h, _ := connect.NewHost(hostId, "0.0.0.0", []byte(""), connect.HostParams{ - MaxRetries: 0, - AuthEnabled: false, - }) + p := connect.GetDefaultHostParams() + p.MaxRetries = 0 + p.AuthEnabled = false + h, _ := connect.NewHost(hostId, "0.0.0.0", []byte(""), p) return h, true } diff --git a/single/reception.go b/single/reception.go index b4aa68f8341da6dba735639883239fcc1d0a97da..01faada1fa07ada31f779ec25f2eecc973f95f9b 100644 --- a/single/reception.go +++ b/single/reception.go @@ -114,9 +114,8 @@ func (m *Manager) processTransmission(msg format.Message, c := NewContact(payload.GetRID(transmitMsg.GetPubKey(grp)), transmitMsg.GetPubKey(grp), dhKey, payload.GetTagFP(), payload.GetMaxParts()) - jww.INFO.Printf("Generated by singe use receiver reception id for single use: %s, "+ - "ephId: %v, pubkey: %x", - c.partner, "unknown:", transmitMsg.GetPubKey(grp).Bytes()) + jww.INFO.Printf("Generated by singe use receiver reception id for single use. EphId %s, PubKey: %x", + c.partner, transmitMsg.GetPubKey(grp).Bytes()) return payload.GetContents(), c, nil } diff --git a/single/responseMessage.go b/single/responseMessage.go index 72d3bbdb666f4d6153ed59cf20caa9e64b49d7fd..191038dcb7665b17578268c85623af858b1601fb 100644 --- a/single/responseMessage.go +++ b/single/responseMessage.go @@ -14,11 +14,11 @@ import ( ) const ( - partNumLen = 1 - maxPartsLen = 1 - responseMinSize = receptionMessageVersionLen + partNumLen + maxPartsLen + sizeSize + partNumLen = 1 + maxPartsLen = 1 + responseMinSize = receptionMessageVersionLen + partNumLen + maxPartsLen + sizeSize receptionMessageVersion = 0 - receptionMessageVersionLen = 1 + receptionMessageVersionLen = 1 ) /* @@ -60,7 +60,7 @@ func mapResponseMessagePart(data []byte) responseMessagePart { return responseMessagePart{ data: data, version: data[:receptionMessageVersionLen], - partNum: data[receptionMessageVersionLen:receptionMessageVersionLen+partNumLen], + partNum: data[receptionMessageVersionLen : receptionMessageVersionLen+partNumLen], maxParts: data[receptionMessageVersionLen+partNumLen : receptionMessageVersionLen+maxPartsLen+partNumLen], size: data[receptionMessageVersionLen+maxPartsLen+partNumLen : responseMinSize], contents: data[responseMinSize:], diff --git a/single/responseMessage_test.go b/single/responseMessage_test.go index b97aa27b544b03779bc28c32e6a9232722fd815f..f9a78f068fb90021f1a7a9f9571ca02d244b91d1 100644 --- a/single/responseMessage_test.go +++ b/single/responseMessage_test.go @@ -21,7 +21,7 @@ func Test_newResponseMessagePart(t *testing.T) { payloadSize := prng.Intn(2000) expected := responseMessagePart{ data: make([]byte, payloadSize), - version: make([]byte, receptionMessageVersionLen), + version: make([]byte, receptionMessageVersionLen), partNum: make([]byte, partNumLen), maxParts: make([]byte, maxPartsLen), size: make([]byte, sizeSize), diff --git a/single/response_test.go b/single/response_test.go index 2a6c74521b593b490f3ee53f92aeff488f6bba7f..fcacf19a51bb2c15bcdb961b53665e67248837da 100644 --- a/single/response_test.go +++ b/single/response_test.go @@ -23,7 +23,7 @@ import ( func TestManager_GetMaxResponsePayloadSize(t *testing.T) { m := newTestManager(0, false, t) cmixPrimeSize := m.store.Cmix().GetGroup().GetP().ByteLen() - expectedSize := 2*cmixPrimeSize - format.KeyFPLen - format.MacLen - format.RecipientIDLen - responseMinSize-1 + expectedSize := 2*cmixPrimeSize - format.KeyFPLen - format.MacLen - format.RecipientIDLen - responseMinSize - 1 testSize := m.GetMaxResponsePayloadSize() if expectedSize != testSize { diff --git a/single/transmitMessage.go b/single/transmitMessage.go index 6676a3ae5d754efa6bf1434e9ccb263214f23bb6..17ce73acd2a6a5183c5c173f31eb23bd950f41c7 100644 --- a/single/transmitMessage.go +++ b/single/transmitMessage.go @@ -61,7 +61,7 @@ func mapTransmitMessage(data []byte, pubKeySize int) transmitMessage { return transmitMessage{ data: data, version: data[:transmitMessageVersionSize], - pubKey: data[transmitMessageVersionSize:transmitMessageVersionSize+pubKeySize], + pubKey: data[transmitMessageVersionSize : transmitMessageVersionSize+pubKeySize], payload: data[transmitMessageVersionSize+pubKeySize:], } } diff --git a/single/transmitMessage_test.go b/single/transmitMessage_test.go index 5125526ab47e3d98e7f9e4c81306492f129a1772..b6c62d25b71f507d54d1f69a70c0a35e932af628 100644 --- a/single/transmitMessage_test.go +++ b/single/transmitMessage_test.go @@ -140,7 +140,7 @@ func TestTransmitMessage_SetPayload_GetPayload_GetPayloadSize(t *testing.T) { prng := rand.New(rand.NewSource(42)) externalPayloadSize := prng.Intn(2000) pubKeySize := prng.Intn(externalPayloadSize) - payloadSize := externalPayloadSize - pubKeySize-transmitMessageVersionSize + payloadSize := externalPayloadSize - pubKeySize - transmitMessageVersionSize payload := make([]byte, payloadSize) prng.Read(payload) m := newTransmitMessage(externalPayloadSize, pubKeySize) @@ -153,7 +153,6 @@ func TestTransmitMessage_SetPayload_GetPayload_GetPayloadSize(t *testing.T) { "\nexpected: %+v\nreceived: %+v", payload, testPayload) } - if payloadSize != m.GetPayloadSize() { t.Errorf("GetContentsSize() returned incorrect content size."+ "\nexpected: %d\nreceived: %d", payloadSize, m.GetPayloadSize()) diff --git a/storage/auth/confirmation.go b/storage/auth/confirmation.go new file mode 100644 index 0000000000000000000000000000000000000000..b55c79433db9d7caf51d303caee06e00665df011 --- /dev/null +++ b/storage/auth/confirmation.go @@ -0,0 +1,61 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package auth + +import ( + "encoding/base64" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/netTime" +) + +const ( + confirmationKeyPrefix = "Confirmation/" + currentConfirmationVersion = 0 +) + +// StoreConfirmation saves the confirmation to storage for the given partner and +// fingerprint. +func (s *Store) StoreConfirmation( + partner *id.ID, fingerprint, confirmation []byte) error { + obj := &versioned.Object{ + Version: currentConfirmationVersion, + Timestamp: netTime.Now(), + Data: confirmation, + } + + return s.kv.Set(makeConfirmationKey(partner, fingerprint), + currentConfirmationVersion, obj) +} + +// LoadConfirmation loads the confirmation for the given partner and fingerprint +// from storage. +func (s *Store) LoadConfirmation(partner *id.ID, fingerprint []byte) ( + []byte, error) { + obj, err := s.kv.Get( + makeConfirmationKey(partner, fingerprint), currentConfirmationVersion) + if err != nil { + return nil, err + } + + return obj.Data, nil +} + +// deleteConfirmation deletes the confirmation for the given partner and +// fingerprint from storage. +func (s *Store) deleteConfirmation(partner *id.ID, fingerprint []byte) error { + return s.kv.Delete( + makeConfirmationKey(partner, fingerprint), currentConfirmationVersion) +} + +// makeConfirmationKey generates the key used to load and store confirmations +// for the partner and fingerprint. +func makeConfirmationKey(partner *id.ID, fingerprint []byte) string { + return confirmationKeyPrefix + partner.String() + "/" + + base64.StdEncoding.EncodeToString(fingerprint) +} diff --git a/storage/auth/confirmation_test.go b/storage/auth/confirmation_test.go new file mode 100644 index 0000000000000000000000000000000000000000..94f4dad1b27d227e6742da07917f75b1dc79253e --- /dev/null +++ b/storage/auth/confirmation_test.go @@ -0,0 +1,174 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package auth + +import ( + "github.com/cloudflare/circl/dh/sidh" + "gitlab.com/elixxir/client/storage/utility" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/crypto/cyclic" + "gitlab.com/elixxir/crypto/diffieHellman" + "gitlab.com/elixxir/crypto/e2e/auth" + "gitlab.com/elixxir/ekv" + "gitlab.com/xx_network/crypto/large" + "gitlab.com/xx_network/primitives/id" + "math/rand" + "reflect" + "testing" +) + +// Tests that a confirmation for different partners and fingerprints can be +// saved and loaded from storage via Store.StoreConfirmation and +// Store.LoadConfirmation. +func TestStore_StoreConfirmation_LoadConfirmation(t *testing.T) { + s := &Store{kv: versioned.NewKV(make(ekv.Memstore))} + prng := rand.New(rand.NewSource(42)) + grp := cyclic.NewGroup(large.NewInt(173), large.NewInt(2)) + + testValues := make([]struct { + partner *id.ID + fingerprint, confirmation []byte + }, 10) + + partner, _ := id.NewRandomID(prng, id.User) + for i := range testValues { + if i%2 == 0 { + partner, _ = id.NewRandomID(prng, id.User) + } + + // Generate original fingerprint + var fp []byte + if i%2 == 1 { + dhPubKey := diffieHellman.GeneratePublicKey(grp.NewInt(42), grp) + _, sidhPubkey := utility.GenerateSIDHKeyPair(sidh.KeyVariantSidhA, prng) + fp = auth.CreateNegotiationFingerprint(dhPubKey, sidhPubkey) + } + + // Generate confirmation + confirmation := make([]byte, 32) + prng.Read(confirmation) + + testValues[i] = struct { + partner *id.ID + fingerprint, confirmation []byte + }{partner: partner, fingerprint: fp, confirmation: confirmation} + + err := s.StoreConfirmation(partner, fp, confirmation) + if err != nil { + t.Errorf("StoreConfirmation returned an error (%d): %+v", i, err) + } + } + + for i, val := range testValues { + loadedConfirmation, err := s.LoadConfirmation(val.partner, val.fingerprint) + if err != nil { + t.Errorf("LoadConfirmation returned an error (%d): %+v", i, err) + } + + if !reflect.DeepEqual(val.confirmation, loadedConfirmation) { + t.Errorf("Loaded confirmation does not match original (%d)."+ + "\nexpected: %v\nreceived: %v", i, val.confirmation, + loadedConfirmation) + } + } +} + +// Tests that Store.deleteConfirmation deletes the correct confirmation from +// storage and that it cannot be loaded from storage. +func TestStore_deleteConfirmation(t *testing.T) { + s := &Store{kv: versioned.NewKV(make(ekv.Memstore))} + prng := rand.New(rand.NewSource(42)) + grp := cyclic.NewGroup(large.NewInt(173), large.NewInt(2)) + + testValues := make([]struct { + partner *id.ID + fingerprint, confirmation []byte + }, 10) + + partner, _ := id.NewRandomID(prng, id.User) + for i := range testValues { + if i%2 == 0 { + partner, _ = id.NewRandomID(prng, id.User) + } + + // Generate original fingerprint + var fp []byte + if i%2 == 1 { + dhPubKey := diffieHellman.GeneratePublicKey(grp.NewInt(42), grp) + _, sidhPubkey := utility.GenerateSIDHKeyPair(sidh.KeyVariantSidhA, prng) + fp = auth.CreateNegotiationFingerprint(dhPubKey, sidhPubkey) + } + + // Generate confirmation + confirmation := make([]byte, 32) + prng.Read(confirmation) + + testValues[i] = struct { + partner *id.ID + fingerprint, confirmation []byte + }{partner: partner, fingerprint: fp, confirmation: confirmation} + + err := s.StoreConfirmation(partner, fp, confirmation) + if err != nil { + t.Errorf("StoreConfirmation returned an error (%d): %+v", i, err) + } + } + + for i, val := range testValues { + err := s.deleteConfirmation(val.partner, val.fingerprint) + if err != nil { + t.Errorf("deleteConfirmation returned an error (%d): %+v", i, err) + } + + loadedConfirmation, err := s.LoadConfirmation(val.partner, val.fingerprint) + if err == nil || loadedConfirmation != nil { + t.Errorf("LoadConfirmation returned a confirmation for partner "+ + "%s and fingerprint %v (%d)", val.partner, val.fingerprint, i) + } + } +} + +// Consistency test of makeConfirmationKey. +func Test_makeConfirmationKey_Consistency(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + grp := cyclic.NewGroup(large.NewInt(173), large.NewInt(2)) + expectedKeys := []string{ + "Confirmation/U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVID/VzgXG/mlQA68iq1eCgEMoew1rnuVG6mA2x2U34PYiOs=", + "Confirmation/P2HTdbwCtB30+Rkp4Y/anm+C5U50joHnnku9b+NM3LoD/DT1RkZJUbdDqNLQv+Pp+Ilx7ZvOX5zBzl8gseeRLu1w=", + "Confirmation/r66IG4KnURCKQu08kDyqQ0ZaeGIGFpeK7QzjxsTzrnsD/BVkxRTRPx5+16fRHsq5bYkpZDJyVJaon0roLGsOBSmI=", + "Confirmation/otwtwlpbdLLhKXBeJz8FySMmgo4rBW44F2WOEGFJiUcD/jKSgdUKni0rsIDDutHlO1fiss+BiNd1vxSGxJL0u2e8=", + "Confirmation/lk39x56NU0NzZhz9ZtdP7B4biUkatyNuS3UhYpDPK+sD/prNQTXAQjkTRhltOQuhU8XagwwWP0RfwJe6yrtI3aaY=", + "Confirmation/l4KD1KCaNvlsIJQXRuPtTaZGqa6LT6e0/Doguvoade0D/D+xEPt5A44s0BD5u/fz1iiPFoCnOR52PefTFOehdkbU=", + "Confirmation/HPCdo54Okp0CSry8sWk5e7c05+8KbgHxhU3rX+Qk/vcD/cPDqZ3S1mqVxRTQ1p7Gwg7cEc34Xz/fUsIpghGiJygg=", + "Confirmation/Ud9mj4dOLJ8c4JyoYBfn4hdIMD/0HBsj4RxI7RdTnWgD/minVwOqyN3l4zy7A4dvJDQ5ZLUcM2NmNdAWhR5/NTDc=", + "Confirmation/Ximg3KRqw6DVcBM7whVx9fVKZDEFUT/YQpsZSuG6nyoD/dK0ZnuwEmyeXqjQj5mri5f8ChTHOVgTgUKkOGjUfPyQ=", + "Confirmation/ZxkHLWcvYfqgvob0V5Iew3wORgzw1wPQfcX1ZhpFATMD/r0Nylw9Bd+eol1+4UWwWD8SBchPbjtnLYJx1zX1htEo=", + "Confirmation/IpwYPBkzqRZYXhg7twkZLbDmyNcJudc4O5k8aUmZRbAD/eszeUU8yAglf5TrE5U4L8SVqKOPqypt9RbVjworRBbk=", + "Confirmation/Rc0b8Lz8GjRsQ08RzwBBb6YWlbkgLmg2Ohx4f0eE4K4D/jhddD9Kqk6rcSJAB/Jy88cwhozR43M1nL+VTyl34SEk=", + "Confirmation/1ieMn3yHL4QPnZTZ/e2uk9sklXGPWAuMjyvsxqp2w7AD/aaMF2inM08M9FdFOHPfGKMnoqqEJ4MiXxDhY2J84cE8=", + "Confirmation/FER0v9N80ga1Gs4FCrYZnsezltYY/eDhopmabz2fi3oD/TJ5e0/2ji9eZSYa78RIP2ZvDW/PxP685D3xZAqHkGHY=", + "Confirmation/KRnCqHpJlPweQB4RxaScfo6p5l1sxARl/TUvLELsPT4D/mlbwi77z/XUw/LfzX8L67k0/0dAIDHAYicLd2RukYO0=", + "Confirmation/Q9EGMwNtPUa4GRauRv8T1qay+tkHnW3zRAWQKWZ7LrQD/0J3tuOL9xxfZdFQ73YEktXkeoFY6sAJIcgzlyDl3BxQ=", + } + + for i, expected := range expectedKeys { + partner, _ := id.NewRandomID(prng, id.User) + dhPubKey := diffieHellman.GeneratePublicKey(grp.NewInt(42), grp) + _, sidhPubkey := utility.GenerateSIDHKeyPair(sidh.KeyVariantSidhA, prng) + fp := auth.CreateNegotiationFingerprint(dhPubKey, sidhPubkey) + + key := makeConfirmationKey(partner, fp) + if expected != key { + t.Errorf("Confirmation key does not match expected for partner "+ + "%s and fingerprint %v (%d).\nexpected: %q\nreceived: %q", + partner, fp, i, expected, key) + } + + // fmt.Printf("\"%s\",\n", key) + } +} diff --git a/storage/auth/previousNegotiations.go b/storage/auth/previousNegotiations.go new file mode 100644 index 0000000000000000000000000000000000000000..d15bab6bd33a75985ae4b021427f58e59a3ff353 --- /dev/null +++ b/storage/auth/previousNegotiations.go @@ -0,0 +1,281 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package auth + +import ( + "bytes" + "encoding/binary" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/crypto/e2e/auth" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/netTime" + "strings" +) + +const ( + negotiationPartnersKey = "NegotiationPartners" + negotiationPartnersVersion = 0 + negotiationFingerprintsKeyPrefix = "NegotiationFingerprints/" + currentNegotiationFingerprintsVersion = 0 +) + +// AddIfNew adds a new negotiation fingerprint if it is new. +// If the partner does not exist, it will add it and the new fingerprint and +// return newFingerprint = true, latest = true. +// If the partner exists and the fingerprint does not exist, add it adds it as +// the latest fingerprint and returns newFingerprint = true, latest = true +// If the partner exists and the fingerprint exists, return +// newFingerprint = false, latest = false or latest = true if it is the last one +// in the list. +func (s *Store) AddIfNew(partner *id.ID, negotiationFingerprint []byte) ( + newFingerprint, latest bool) { + s.mux.Lock() + defer s.mux.Unlock() + + // If the partner does not exist, add it to the list and store a new + // fingerprint to storage + _, exists := s.previousNegotiations[*partner] + if !exists { + s.previousNegotiations[*partner] = struct{}{} + + // Save fingerprint to storage + err := s.saveNegotiationFingerprints(partner, negotiationFingerprint) + if err != nil { + jww.FATAL.Panicf("Failed to save negotiation fingerprints for "+ + "partner %s: %+v", partner, err) + } + + // Save partner list to storage + err = s.savePreviousNegotiations() + if err != nil { + jww.FATAL.Panicf( + "Failed to save negotiation partners %s: %+v", partner, err) + } + + newFingerprint = true + latest = true + + return + } + + // Get the fingerprint list from storage + fingerprints, err := s.loadNegotiationFingerprints(partner) + if err != nil { + jww.FATAL.Panicf("Failed to load negotiation fingerprints for "+ + "partner %s: %+v", partner, err) + } + + // If the partner does exist and the fingerprint exists, then make no + // changes to the list + for i, fp := range fingerprints { + if bytes.Equal(fp, negotiationFingerprint) { + newFingerprint = false + + // Latest = true if it is the last fingerprint in the list + latest = i == len(fingerprints)-1 + + return + } + } + + // If the partner does exist and the fingerprint does not exist, then add + // the fingerprint to the list as latest + fingerprints = append(fingerprints, negotiationFingerprint) + err = s.saveNegotiationFingerprints(partner, fingerprints...) + if err != nil { + jww.FATAL.Panicf("Failed to save negotiation fingerprints for "+ + "partner %s: %+v", partner, err) + } + + newFingerprint = true + latest = true + + return +} + +// deletePreviousNegotiationPartner removes the partner, its fingerprints, and +// its confirmations from memory and storage. +func (s *Store) deletePreviousNegotiationPartner(partner *id.ID) error { + + // Do nothing if the partner does not exist + if _, exists := s.previousNegotiations[*partner]; !exists { + return nil + } + + // Delete partner from memory + delete(s.previousNegotiations, *partner) + + // Delete partner from storage and return an error + err := s.savePreviousNegotiations() + if err != nil { + return err + } + + // Check if fingerprints exist + fingerprints, err := s.loadNegotiationFingerprints(partner) + + // If fingerprints exist for this partner, delete them from storage and any + // accompanying confirmations + if err == nil { + // Delete the fingerprint list from storage but do not return the error + // until after attempting to delete the confirmations + err = s.kv.Delete(makeNegotiationFingerprintsKey(partner), + currentNegotiationFingerprintsVersion) + + // Delete all confirmations from storage + for _, fp := range fingerprints { + // Ignore the error since confirmations rarely exist + _ = s.deleteConfirmation(partner, fp) + } + } + + // Return any error from loading or deleting fingerprints + return err +} + +// savePreviousNegotiations saves the list of previousNegotiations partners to +// storage. +func (s *Store) savePreviousNegotiations() error { + obj := &versioned.Object{ + Version: negotiationPartnersVersion, + Timestamp: netTime.Now(), + Data: marshalPreviousNegotiations(s.previousNegotiations), + } + + return s.kv.Set(negotiationPartnersKey, negotiationPartnersVersion, obj) +} + +// newOrLoadPreviousNegotiations loads the list of previousNegotiations partners +// from storage. +func (s *Store) newOrLoadPreviousNegotiations() (map[id.ID]struct{}, error) { + obj, err := s.kv.Get(negotiationPartnersKey, negotiationPartnersVersion) + if err != nil { + if strings.Contains(err.Error(), "object not found") || + strings.Contains(err.Error(), "no such file or directory") { + return make(map[id.ID]struct{}), nil + } + return nil, err + } + + return unmarshalPreviousNegotiations(obj.Data), nil +} + +// marshalPreviousNegotiations marshals the list of partners into a byte slice. +func marshalPreviousNegotiations(partners map[id.ID]struct{}) []byte { + buff := bytes.NewBuffer(nil) + buff.Grow(8 + (len(partners) * id.ArrIDLen)) + + // Write number of partners to buffer + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, uint64(len(partners))) + buff.Write(b) + + // Write each partner ID to buffer + for partner := range partners { + buff.Write(partner.Marshal()) + } + + return buff.Bytes() +} + +// unmarshalPreviousNegotiations unmarshalls the marshalled byte slice into a +// list of partner IDs. +func unmarshalPreviousNegotiations(buf []byte) map[id.ID]struct{} { + buff := bytes.NewBuffer(buf) + + numberOfPartners := binary.LittleEndian.Uint64(buff.Next(8)) + partners := make(map[id.ID]struct{}, numberOfPartners) + + for i := uint64(0); i < numberOfPartners; i++ { + partner, err := id.Unmarshal(buff.Next(id.ArrIDLen)) + if err != nil { + jww.FATAL.Panicf( + "Failed to unmarshal negotiation partner ID: %+v", err) + } + + partners[*partner] = struct{}{} + } + + return partners +} + +// saveNegotiationFingerprints saves the list of fingerprints for the given +// partner to storage. +func (s *Store) saveNegotiationFingerprints( + partner *id.ID, fingerprints ...[]byte) error { + + obj := &versioned.Object{ + Version: currentNegotiationFingerprintsVersion, + Timestamp: netTime.Now(), + Data: marshalNegotiationFingerprints(fingerprints...), + } + + return s.kv.Set(makeNegotiationFingerprintsKey(partner), + currentNegotiationFingerprintsVersion, obj) +} + +// loadNegotiationFingerprints loads the list of fingerprints for the given +// partner from storage. +func (s *Store) loadNegotiationFingerprints(partner *id.ID) ([][]byte, error) { + obj, err := s.kv.Get(makeNegotiationFingerprintsKey(partner), + currentNegotiationFingerprintsVersion) + if err != nil { + return nil, err + } + + return unmarshalNegotiationFingerprints(obj.Data), nil +} + +// marshalNegotiationFingerprints marshals the list of fingerprints into a byte +// slice for storage. +func marshalNegotiationFingerprints(fingerprints ...[]byte) []byte { + buff := bytes.NewBuffer(nil) + buff.Grow(8 + (len(fingerprints) * auth.NegotiationFingerprintLen)) + + // Write number of fingerprints to buffer + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, uint64(len(fingerprints))) + buff.Write(b) + + for _, fp := range fingerprints { + // Write fingerprint to buffer + buff.Write(fp[:auth.NegotiationFingerprintLen]) + } + + return buff.Bytes() +} + +// unmarshalNegotiationFingerprints unmarshalls the marshalled byte slice into a +// list of fingerprints. +func unmarshalNegotiationFingerprints(buf []byte) [][]byte { + buff := bytes.NewBuffer(buf) + + listLen := binary.LittleEndian.Uint64(buff.Next(8)) + fingerprints := make([][]byte, listLen) + + for i := range fingerprints { + fingerprints[i] = make([]byte, auth.NegotiationFingerprintLen) + copy(fingerprints[i], buff.Next(auth.NegotiationFingerprintLen)) + } + + return fingerprints +} + +// makeNegotiationFingerprintsKey generates the key used to load and store +// negotiation fingerprints for the partner. +func makeNegotiationFingerprintsKey(partner *id.ID) string { + return negotiationFingerprintsKeyPrefix + partner.String() +} diff --git a/storage/auth/previousNegotiations_test.go b/storage/auth/previousNegotiations_test.go new file mode 100644 index 0000000000000000000000000000000000000000..8d096fc21d054110d0fd048151a8873d90291b4d --- /dev/null +++ b/storage/auth/previousNegotiations_test.go @@ -0,0 +1,451 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package auth + +import ( + "github.com/cloudflare/circl/dh/sidh" + "gitlab.com/elixxir/client/storage/utility" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/crypto/cyclic" + "gitlab.com/elixxir/crypto/diffieHellman" + "gitlab.com/elixxir/crypto/e2e/auth" + "gitlab.com/elixxir/ekv" + "gitlab.com/xx_network/crypto/csprng" + "gitlab.com/xx_network/crypto/large" + "gitlab.com/xx_network/primitives/id" + "math/rand" + "reflect" + "testing" +) + +// Tests the four possible cases of Store.AddIfNew: +// 1. If the partner does not exist, add partner with the new fingerprint. +// Returns newFingerprint = true, latest = true. +// 2. If the partner exists and the fingerprint does not, add the fingerprint. +// Returns newFingerprint = true, latest = true. +// 3. If the partner exists and the fingerprint exists, do nothing. +// Return newFingerprint = false, latest = false. +// 4. If the partner exists, the fingerprint exists, and the fingerprint is the +// latest, do nothing. +// Return newFingerprint = false, latest = true. +func TestStore_AddIfNew(t *testing.T) { + s := &Store{ + kv: versioned.NewKV(make(ekv.Memstore)), + previousNegotiations: make(map[id.ID]struct{}), + } + prng := rand.New(rand.NewSource(42)) + grp := cyclic.NewGroup(large.NewInt(173), large.NewInt(2)) + newPartner := func() *id.ID { + partner, _ := id.NewRandomID(prng, id.User) + return partner + } + newFps := func() []byte { + dhPubKey := diffieHellman.GeneratePublicKey(grp.NewInt(42), grp) + _, sidhPubkey := utility.GenerateSIDHKeyPair(sidh.KeyVariantSidhA, prng) + return auth.CreateNegotiationFingerprint(dhPubKey, sidhPubkey) + } + + type test struct { + name string + + addPartner bool // If true, partner is added to list first + addFp bool // If true, fingerprint is added to list first + latestFp bool // If true, fingerprint is added as latest + otherFps [][]byte // Other fingerprints to add first + + // Inputs + partner *id.ID + fp []byte + + // Expected values + newFingerprint bool + latest bool + } + + tests := []test{ + { + name: "Case 1: partner does not exist", + addPartner: false, + addFp: false, + latestFp: false, + partner: newPartner(), + fp: newFps(), + newFingerprint: true, + latest: true, + }, { + name: "Case 2: partner exists, fingerprint does not", + addPartner: true, + addFp: false, + latestFp: false, + otherFps: [][]byte{newFps(), newFps(), newFps()}, + partner: newPartner(), + fp: newFps(), + newFingerprint: true, + latest: true, + }, { + name: "Case 3: partner and fingerprint exist", + addPartner: true, + addFp: true, + latestFp: false, + otherFps: [][]byte{newFps(), newFps(), newFps()}, + partner: newPartner(), + fp: newFps(), + newFingerprint: false, + latest: false, + }, { + name: "Case 4: partner and fingerprint exist, fingerprint latest", + addPartner: true, + addFp: true, + latestFp: true, + otherFps: [][]byte{newFps(), newFps(), newFps()}, + partner: newPartner(), + fp: newFps(), + newFingerprint: false, + latest: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.addPartner { + s.previousNegotiations[*tt.partner] = struct{}{} + err := s.savePreviousNegotiations() + if err != nil { + t.Errorf( + "savePreviousNegotiations returned an error: %+v", err) + } + + var fps [][]byte + if tt.addFp { + fps, _ = s.loadNegotiationFingerprints(tt.partner) + + for _, fp := range tt.otherFps { + fps = append(fps, fp) + } + + if tt.latestFp { + fps = append(fps, tt.fp) + } else { + fps = append([][]byte{tt.fp}, fps...) + } + } + err = s.saveNegotiationFingerprints(tt.partner, fps...) + if err != nil { + t.Errorf("saveNegotiationFingerprints returned an "+ + "error: %+v", err) + } + } + + newFingerprint, latest := s.AddIfNew(tt.partner, tt.fp) + + if newFingerprint != tt.newFingerprint { + t.Errorf("Unexpected value for newFingerprint."+ + "\nexpected: %t\nreceived: %t", + tt.newFingerprint, newFingerprint) + } + if latest != tt.latest { + t.Errorf("Unexpected value for latest."+ + "\nexpected: %t\nreceived: %t", tt.latest, latest) + } + }) + } +} + +// Tests that Store.deletePreviousNegotiationPartner deletes the partner from +// previousNegotiations in memory, previousNegotiations in storage, fingerprints +// in storage, and any confirmations in storage. +func TestStore_deletePreviousNegotiationPartner(t *testing.T) { + s := &Store{ + kv: versioned.NewKV(make(ekv.Memstore)), + previousNegotiations: make(map[id.ID]struct{}), + } + prng := rand.New(rand.NewSource(42)) + grp := cyclic.NewGroup(large.NewInt(173), large.NewInt(2)) + + type values struct { + partner *id.ID + fps [][]byte + } + + testValues := make([]values, 16) + + for i := range testValues { + partner, _ := id.NewRandomID(prng, id.User) + s.previousNegotiations[*partner] = struct{}{} + + err := s.savePreviousNegotiations() + if err != nil { + t.Errorf("savePreviousNegotiations returned an error (%d): %+v", + i, err) + } + + // Generate fingerprints + fingerprints := make([][]byte, i+1) + for j := range fingerprints { + dhPubKey := diffieHellman.GeneratePublicKey(grp.NewInt(42), grp) + _, sidhPubkey := utility.GenerateSIDHKeyPair(sidh.KeyVariantSidhA, prng) + fingerprints[j] = auth.CreateNegotiationFingerprint(dhPubKey, sidhPubkey) + } + + err = s.saveNegotiationFingerprints(partner, fingerprints...) + if err != nil { + t.Errorf("saveNegotiationFingerprints returned an error (%d): %+v", + i, err) + } + + testValues[i] = values{partner, fingerprints} + + // Generate confirmation + confirmation := make([]byte, 32) + prng.Read(confirmation) + + err = s.StoreConfirmation(partner, fingerprints[0], confirmation) + if err != nil { + t.Errorf("StoreConfirmation returned an error (%d): %+v", i, err) + } + } + + // Add partner that is not in list + partner, _ := id.NewRandomID(prng, id.User) + testValues = append(testValues, values{partner, [][]byte{}}) + + for i, v := range testValues { + err := s.deletePreviousNegotiationPartner(v.partner) + if err != nil { + t.Errorf("deletePreviousNegotiationPartner returned an error "+ + "(%d): %+v", i, err) + } + + // Check previousNegotiations in memory + _, exists := s.previousNegotiations[*v.partner] + if exists { + t.Errorf("Parter %s exists in previousNegotiations (%d).", + v.partner, i) + } + + // Check previousNegotiations in storage + previousNegotiations, err := s.newOrLoadPreviousNegotiations() + if err != nil { + t.Errorf("newOrLoadPreviousNegotiations returned an error (%d): %+v", + i, err) + } + _, exists = previousNegotiations[*v.partner] + if exists { + t.Errorf("Parter %s exists in previousNegotiations in storage (%d).", + v.partner, i) + } + + // Check negotiation fingerprints in storage + fps, err := s.loadNegotiationFingerprints(v.partner) + if err == nil || fps != nil { + t.Errorf("Loaded fingerprints for partner %s (%d): %v", + v.partner, i, fps) + } + + // Check all possible confirmations in storage + for j, fp := range v.fps { + confirmation, err := s.LoadConfirmation(v.partner, fp) + if err == nil || fps != nil { + t.Errorf("Loaded confirmation for partner %s and "+ + "fingerprint %v (%d, %d): %v", + v.partner, fp, i, j, confirmation) + } + } + } +} + +// Tests that Store.previousNegotiations can be saved and loaded from storage +// via Store.savePreviousNegotiations andStore.newOrLoadPreviousNegotiations. +func TestStore_savePreviousNegotiations_newOrLoadPreviousNegotiations(t *testing.T) { + s := &Store{ + kv: versioned.NewKV(make(ekv.Memstore)), + previousNegotiations: make(map[id.ID]struct{}), + } + prng := rand.New(rand.NewSource(42)) + expected := make(map[id.ID]struct{}) + + for i := 0; i < 16; i++ { + partner, _ := id.NewRandomID(prng, id.User) + s.previousNegotiations[*partner] = struct{}{} + expected[*partner] = struct{}{} + + err := s.savePreviousNegotiations() + if err != nil { + t.Errorf("savePreviousNegotiations returned an error (%d): %+v", + i, err) + } + + s.previousNegotiations, err = s.newOrLoadPreviousNegotiations() + if err != nil { + t.Errorf("newOrLoadPreviousNegotiations returned an error (%d): %+v", + i, err) + } + + if !reflect.DeepEqual(expected, s.previousNegotiations) { + t.Errorf("Loaded previousNegotiations does not match expected (%d)."+ + "\nexpected: %v\nreceived: %v", i, expected, s.previousNegotiations) + } + } +} + +// Tests that Store.newOrLoadPreviousNegotiations returns blank negotiations if +// they do not exist. +func TestStore_newOrLoadPreviousNegotiations_noNegotiations(t *testing.T) { + s := &Store{ + kv: versioned.NewKV(make(ekv.Memstore)), + previousNegotiations: make(map[id.ID]struct{}), + } + expected := make(map[id.ID]struct{}) + + blankNegotations, err := s.newOrLoadPreviousNegotiations() + if err != nil { + t.Errorf("newOrLoadPreviousNegotiations returned an error: %+v", err) + } + + if !reflect.DeepEqual(expected, blankNegotations) { + t.Errorf("Loaded previousNegotiations does not match expected."+ + "\nexpected: %v\nreceived: %v", expected, blankNegotations) + } +} + +// Tests that a list of partner IDs that is marshalled and unmarshalled via +// marshalPreviousNegotiations and unmarshalPreviousNegotiations matches the +// original list +func Test_marshalPreviousNegotiations_unmarshalPreviousNegotiations(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + + // Create original map of partner IDs + originalPartners := make(map[id.ID]struct{}, 50) + for i := 0; i < 50; i++ { + partner, _ := id.NewRandomID(prng, id.User) + originalPartners[*partner] = struct{}{} + } + + // Marshal and unmarshal the partner list + marshalledPartners := marshalPreviousNegotiations(originalPartners) + unmarshalledPartners := unmarshalPreviousNegotiations(marshalledPartners) + + // Check that the original matches the unmarshalled + if !reflect.DeepEqual(originalPartners, unmarshalledPartners) { + t.Errorf("Unmarshalled partner list does not match original."+ + "\nexpected: %v\nreceived: %v", + originalPartners, unmarshalledPartners) + } +} + +// Tests that a list of fingerprints for different partners can be saved and +// loaded from storage via Store.saveNegotiationFingerprints and +// Store.loadNegotiationFingerprints. +func TestStore_saveNegotiationFingerprints_loadNegotiationFingerprints(t *testing.T) { + s := &Store{kv: versioned.NewKV(make(ekv.Memstore))} + rng := csprng.NewSystemRNG() + grp := cyclic.NewGroup(large.NewInt(173), large.NewInt(2)) + + testValues := make([]struct { + partner *id.ID + fps [][]byte + }, 10) + + for i := range testValues { + partner, _ := id.NewRandomID(rng, id.User) + + // Generate original fingerprints to marshal + originalFps := make([][]byte, 50) + for j := range originalFps { + dhPubKey := diffieHellman.GeneratePublicKey(grp.NewInt(42), grp) + _, sidhPubkey := utility.GenerateSIDHKeyPair(sidh.KeyVariantSidhA, rng) + originalFps[j] = auth.CreateNegotiationFingerprint(dhPubKey, sidhPubkey) + } + + testValues[i] = struct { + partner *id.ID + fps [][]byte + }{partner: partner, fps: originalFps} + + err := s.saveNegotiationFingerprints(partner, originalFps...) + if err != nil { + t.Errorf("saveNegotiationFingerprints returned an error (%d): %+v", + i, err) + } + } + + for i, val := range testValues { + loadedFps, err := s.loadNegotiationFingerprints(val.partner) + if err != nil { + t.Errorf("loadNegotiationFingerprints returned an error (%d): %+v", + i, err) + } + + if !reflect.DeepEqual(val.fps, loadedFps) { + t.Errorf("Loaded fingerprints do not match original (%d)."+ + "\nexpected: %v\nreceived: %v", i, val.fps, loadedFps) + } + } +} + +// Tests that a list of fingerprints that is marshalled and unmarshalled via +// marshalNegotiationFingerprints and unmarshalNegotiationFingerprints matches +// the original list +func Test_marshalNegotiationFingerprints_unmarshalNegotiationFingerprints(t *testing.T) { + rng := csprng.NewSystemRNG() + grp := cyclic.NewGroup(large.NewInt(173), large.NewInt(2)) + + // Generate original fingerprints to marshal + originalFps := make([][]byte, 50) + for i := range originalFps { + dhPubKey := diffieHellman.GeneratePublicKey(grp.NewInt(42), grp) + _, sidhPubkey := utility.GenerateSIDHKeyPair(sidh.KeyVariantSidhA, rng) + originalFps[i] = auth.CreateNegotiationFingerprint(dhPubKey, sidhPubkey) + } + + // Marshal and unmarshal the fingerprint list + marshalledFingerprints := marshalNegotiationFingerprints(originalFps...) + unmarshalledFps := unmarshalNegotiationFingerprints(marshalledFingerprints) + + // Check that the original matches the unmarshalled + if !reflect.DeepEqual(originalFps, unmarshalledFps) { + t.Errorf("Unmarshalled fingerprints do not match original."+ + "\nexpected: %v\nreceived: %v", originalFps, unmarshalledFps) + } +} + +// Consistency test of makeNegotiationFingerprintsKey. +func Test_makeNegotiationFingerprintsKey_Consistency(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + expectedKeys := []string{ + "NegotiationFingerprints/U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVID", + "NegotiationFingerprints/15tNdkKbYXoMn58NO6VbDMDWFEyIhTWEGsvgcJsHWAgD", + "NegotiationFingerprints/YdN1vAK0HfT5GSnhj9qeb4LlTnSOgeeeS71v40zcuoQD", + "NegotiationFingerprints/6NY+jE/+HOvqVG2PrBPdGqwEzi6ih3xVec+ix44bC68D", + "NegotiationFingerprints/iBuCp1EQikLtPJA8qkNGWnhiBhaXiu0M48bE8657w+AD", + "NegotiationFingerprints/W1cS/v2+DBAoh+EA2s0tiF9pLLYH2gChHBxwceeWotwD", + "NegotiationFingerprints/wlpbdLLhKXBeJz8FySMmgo4rBW44F2WOEGFJiUf980QD", + "NegotiationFingerprints/DtTBFgI/qONXa2/tJ/+JdLrAyv2a0FaSsTYZ5ziWTf0D", + "NegotiationFingerprints/no1TQ3NmHP1m10/sHhuJSRq3I25LdSFikM8r60LDyicD", + "NegotiationFingerprints/hWDxqsBnzqbov0bUqytGgEAsX7KCDohdMmDx3peCg9QD", + "NegotiationFingerprints/mjb5bCCUF0bj7U2mRqmui0+ntPw6ILr6GnXtMnqGuLAD", + "NegotiationFingerprints/mvHP0rO1EhnqeVM6v0SNLEedMmB1M5BZFMjMHPCdo54D", + "NegotiationFingerprints/kp0CSry8sWk5e7c05+8KbgHxhU3rX+Qk/vesIQiR9ZcD", + "NegotiationFingerprints/KSqiuKoEfGHNszNz6+csJ6CYwCGX2ua3MsNR32aPh04D", + "NegotiationFingerprints/nxzgnKhgF+fiF0gwP/QcGyPhHEjtF1OdaF928qeYvGQD", + "NegotiationFingerprints/Dl2yhksq08Js5jgjQnZaE9aW5S33YPbDRl4poNykasMD", + } + + for i, expected := range expectedKeys { + partner, _ := id.NewRandomID(prng, id.User) + + key := makeNegotiationFingerprintsKey(partner) + if expected != key { + t.Errorf("Negotiation fingerprints key does not match expected "+ + "for partner %s (%d).\nexpected: %q\nreceived: %q", partner, i, + expected, key) + } + + // fmt.Printf("\"%s\",\n", key) + } +} diff --git a/storage/auth/store.go b/storage/auth/store.go index fb9aa2b04be8875bae32de4c74752db3dd80ddee..2d81f960cf767702f21b5401e26e780c0ff74603 100644 --- a/storage/auth/store.go +++ b/storage/auth/store.go @@ -9,6 +9,8 @@ package auth import ( "encoding/json" + "sync" + "github.com/cloudflare/circl/dh/sidh" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" @@ -20,7 +22,6 @@ import ( "gitlab.com/elixxir/primitives/format" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/netTime" - "sync" ) const NoRequest = "Request Not Found" @@ -31,11 +32,12 @@ const requestMapKey = "map" const requestMapVersion = 0 type Store struct { - kv *versioned.KV - grp *cyclic.Group - requests map[id.ID]*request - fingerprints map[format.Fingerprint]fingerprint - mux sync.RWMutex + kv *versioned.KV + grp *cyclic.Group + requests map[id.ID]*request + fingerprints map[format.Fingerprint]fingerprint + previousNegotiations map[id.ID]struct{} + mux sync.RWMutex } // NewStore creates a new store. All passed in private keys are added as @@ -43,10 +45,11 @@ type Store struct { func NewStore(kv *versioned.KV, grp *cyclic.Group, privKeys []*cyclic.Int) (*Store, error) { kv = kv.Prefix(storePrefix) s := &Store{ - kv: kv, - grp: grp, - requests: make(map[id.ID]*request), - fingerprints: make(map[format.Fingerprint]fingerprint), + kv: kv, + grp: grp, + requests: make(map[id.ID]*request), + fingerprints: make(map[format.Fingerprint]fingerprint), + previousNegotiations: make(map[id.ID]struct{}), } for _, key := range privKeys { @@ -59,6 +62,12 @@ func NewStore(kv *versioned.KV, grp *cyclic.Group, privKeys []*cyclic.Int) (*Sto } } + err := s.savePreviousNegotiations() + if err != nil { + return nil, errors.Errorf( + "failed to load previousNegotiations partners: %+v", err) + } + return s, s.save() } @@ -72,10 +81,11 @@ func LoadStore(kv *versioned.KV, grp *cyclic.Group, privKeys []*cyclic.Int) (*St } s := &Store{ - kv: kv, - grp: grp, - requests: make(map[id.ID]*request), - fingerprints: make(map[format.Fingerprint]fingerprint), + kv: kv, + grp: grp, + requests: make(map[id.ID]*request), + fingerprints: make(map[format.Fingerprint]fingerprint), + previousNegotiations: make(map[id.ID]struct{}), } for _, key := range privKeys { @@ -141,10 +151,17 @@ func LoadStore(kv *versioned.KV, grp *cyclic.Group, privKeys []*cyclic.Int) (*St jww.FATAL.Panicf("Unknown request type: %d", r.rt) } - //store in the request map + // store in the request map s.requests[*rid] = r } + // Load previous negotiations from storage + s.previousNegotiations, err = s.newOrLoadPreviousNegotiations() + if err != nil { + return nil, errors.Errorf("failed to load list of previouse "+ + "negotation partner IDs: %+v", err) + } + return s, nil } @@ -275,6 +292,20 @@ func (s *Store) GetAllReceived() []contact.Contact { return cList } +// GetAllReceived returns all pending received contact requests from storage. +func (s *Store) GetAllSentIDs() []*id.ID { + s.mux.RLock() + defer s.mux.RUnlock() + cList := make([]*id.ID, 0, len(s.requests)) + for key := range s.requests { + r := s.requests[key] + if r.rt == Sent { + cList = append(cList, r.sent.partner) + } + } + return cList +} + // GetFingerprint can return either a private key or a sentRequest if the // fingerprint is found. If it returns a sentRequest, then it takes the lock to // ensure there is only one operator at a time. The user of the API must release @@ -429,6 +460,11 @@ func (s *Store) Delete(partner *id.ID) error { "deletion: %+v", err) } + err := s.deletePreviousNegotiationPartner(partner) + if err != nil { + jww.FATAL.Panicf("Failed to delete partner negotiations: %+v", err) + } + return nil } @@ -458,6 +494,36 @@ func (s *Store) DeleteAllRequests() error { return nil } +// DeleteRequest deletes a request from Store given a partner ID. +// If the partner ID exists as a request, then the request will be deleted +// and the state stored. If the partner does not exist, then an error will +// be returned. +func (s *Store) DeleteRequest(partnerId *id.ID) error { + s.mux.Lock() + defer s.mux.Unlock() + + req, ok := s.requests[*partnerId] + if !ok { + return errors.Errorf("Request for %s does not exist", partnerId) + } + + switch req.rt { + case Sent: + s.deleteSentRequest(req) + case Receive: + s.deleteReceiveRequest(req) + } + + delete(s.requests, *partnerId) + + if err := s.save(); err != nil { + jww.FATAL.Panicf("Failed to store updated request map after "+ + "deleting receive request for partner %s: %+v", partnerId, err) + } + + return nil +} + // DeleteSentRequests deletes all Sent requests from Store. func (s *Store) DeleteSentRequests() error { s.mux.Lock() diff --git a/storage/auth/store_test.go b/storage/auth/store_test.go index f1f10414836eb6c37596593322d112dbcfcc9394..2963efc611c16360a0aaa0d8411f6ed448ac9acc 100644 --- a/storage/auth/store_test.go +++ b/storage/auth/store_test.go @@ -88,6 +88,9 @@ func TestLoadStore(t *testing.T) { t.Fatalf("AddSent() produced an error: %+v", err) } + s.AddIfNew( + sr.partner, auth.CreateNegotiationFingerprint(privKeys[0], sidhPubKey)) + // Attempt to load the store store, err := LoadStore(kv, s.grp, privKeys) if err != nil { @@ -905,6 +908,28 @@ func TestStore_GetAllReceived_MixSentReceived(t *testing.T) { } +// Error case: Call DeleteRequest on a request that does +// not exist. +func TestStore_DeleteRequest_NonexistantRequest(t *testing.T) { + s, _, _ := makeTestStore(t) + c := contact.Contact{ID: id.NewIdFromUInt(rand.Uint64(), id.User, t)} + rng := csprng.NewSystemRNG() + _, sidhPubKey := genSidhAKeys(rng) + if err := s.AddReceived(c, sidhPubKey); err != nil { + t.Fatalf("AddReceived() returned an error: %+v", err) + } + if _, _, err := s.GetReceivedRequest(c.ID); err != nil { + t.Fatalf("GetReceivedRequest() returned an error: %+v", err) + } + + err := s.DeleteRequest(c.ID) + if err != nil { + t.Errorf("DeleteRequest should return an error " + + "when trying to delete a receive request") + } + +} + // Unit test. func TestStore_DeleteReceiveRequests(t *testing.T) { s, _, _ := makeTestStore(t) diff --git a/storage/cmix/store.go b/storage/cmix/store.go index 32b7d506c7de63b4fa901773ff6ba5551747d293..5947900089ea3ec047c6ce676eb27264cc20dca9 100644 --- a/storage/cmix/store.go +++ b/storage/cmix/store.go @@ -14,7 +14,6 @@ import ( "gitlab.com/elixxir/client/storage/utility" "gitlab.com/elixxir/client/storage/versioned" "gitlab.com/elixxir/crypto/cyclic" - "gitlab.com/elixxir/crypto/diffieHellman" "gitlab.com/xx_network/comms/connect" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/netTime" @@ -24,50 +23,31 @@ import ( const prefix = "cmix" const currentStoreVersion = 0 const ( - storeKey = "KeyStore" - pubKeyKey = "DhPubKey" - privKeyKey = "DhPrivKey" - grpKey = "GroupKey" + storeKey = "KeyStore" + grpKey = "GroupKey" ) type Store struct { - nodes map[id.ID]*key - dhPrivateKey *cyclic.Int - dhPublicKey *cyclic.Int - validUntil uint64 - keyId []byte - grp *cyclic.Group - kv *versioned.KV - mux sync.RWMutex + nodes map[id.ID]*key + validUntil uint64 + keyId []byte + grp *cyclic.Group + kv *versioned.KV + mux sync.RWMutex } // NewStore returns a new cMix storage object. -func NewStore(grp *cyclic.Group, kv *versioned.KV, priv *cyclic.Int) (*Store, error) { +func NewStore(grp *cyclic.Group, kv *versioned.KV) (*Store, error) { // Generate public key - pub := diffieHellman.GeneratePublicKey(priv, grp) kv = kv.Prefix(prefix) s := &Store{ - nodes: make(map[id.ID]*key), - dhPrivateKey: priv, - dhPublicKey: pub, - grp: grp, - kv: kv, - } - - err := utility.StoreCyclicKey(kv, pub, pubKeyKey) - if err != nil { - return nil, - errors.WithMessage(err, "Failed to store cMix DH public key") - } - - err = utility.StoreCyclicKey(kv, priv, privKeyKey) - if err != nil { - return nil, - errors.WithMessage(err, "Failed to store cMix DH private key") + nodes: make(map[id.ID]*key), + grp: grp, + kv: kv, } - err = utility.StoreGroup(kv, grp, grpKey) + err := utility.StoreGroup(kv, grp, grpKey) if err != nil { return nil, errors.WithMessage(err, "Failed to store cMix group") } @@ -172,16 +152,6 @@ func (s *Store) GetRoundKeys(topology *connect.Circuit) (*RoundKeys, []*id.ID) { return rk, missingNodes } -// GetDHPrivateKey returns the diffie hellman private key -func (s *Store) GetDHPrivateKey() *cyclic.Int { - return s.dhPrivateKey -} - -// GetDHPublicKey returns the diffie hellman public key. -func (s *Store) GetDHPublicKey() *cyclic.Int { - return s.dhPublicKey -} - // GetGroup returns the cyclic group used for cMix. func (s *Store) GetGroup() *cyclic.Group { return s.grp @@ -251,16 +221,6 @@ func (s *Store) unmarshal(b []byte) error { s.nodes[nid] = k } - s.dhPrivateKey, err = utility.LoadCyclicKey(s.kv, privKeyKey) - if err != nil { - return errors.WithMessage(err, "Failed to load cMix DH private key") - } - - s.dhPublicKey, err = utility.LoadCyclicKey(s.kv, pubKeyKey) - if err != nil { - return errors.WithMessage(err, "Failed to load cMix DH public key") - } - s.grp, err = utility.LoadGroup(s.kv, grpKey) if err != nil { return errors.WithMessage(err, "Failed to load cMix group") diff --git a/storage/cmix/store_test.go b/storage/cmix/store_test.go index d84b2dff66723020d5c0b16f7b7d2b0d059911d1..6fb614257dc2e23fab664bd73cd8a3055a6a7c06 100644 --- a/storage/cmix/store_test.go +++ b/storage/cmix/store_test.go @@ -11,7 +11,6 @@ import ( "bytes" "gitlab.com/elixxir/client/storage/versioned" "gitlab.com/elixxir/crypto/cyclic" - "gitlab.com/elixxir/crypto/diffieHellman" "gitlab.com/elixxir/ekv" "gitlab.com/xx_network/comms/connect" "gitlab.com/xx_network/crypto/large" @@ -26,10 +25,8 @@ func TestNewStore(t *testing.T) { vkv := versioned.NewKV(kv) grp := cyclic.NewGroup(large.NewInt(173), large.NewInt(2)) - priv := grp.NewInt(2) - pub := diffieHellman.GeneratePublicKey(priv, grp) - store, err := NewStore(grp, vkv, priv) + store, err := NewStore(grp, vkv) if err != nil { t.Fatal(err.Error()) } @@ -37,12 +34,6 @@ func TestNewStore(t *testing.T) { if store.nodes == nil { t.Errorf("Failed to initialize nodes") } - if store.GetDHPrivateKey() == nil || store.GetDHPrivateKey().Cmp(priv) != 0 { - t.Errorf("Failed to set store.dhPrivateKey correctly") - } - if store.GetDHPublicKey() == nil || store.GetDHPublicKey().Cmp(pub) != 0 { - t.Errorf("Failed to set store.dhPublicKey correctly") - } if store.grp == nil { t.Errorf("Failed to set store.grp") } @@ -132,12 +123,6 @@ func TestLoadStore(t *testing.T) { if err != nil { t.Fatalf("Unable to load store: %+v", err) } - if store.GetDHPublicKey().Cmp(testStore.GetDHPublicKey()) != 0 { - t.Errorf("LoadStore failed to load public key") - } - if store.GetDHPrivateKey().Cmp(testStore.GetDHPrivateKey()) != 0 { - t.Errorf("LoadStore failed to load public key") - } if len(store.nodes) != len(testStore.nodes) { t.Errorf("LoadStore failed to load node keys") } @@ -221,7 +206,7 @@ func TestStore_Count(t *testing.T) { vkv := versioned.NewKV(make(ekv.Memstore)) grp := cyclic.NewGroup(large.NewInt(173), large.NewInt(2)) - store, err := NewStore(grp, vkv, grp.NewInt(2)) + store, err := NewStore(grp, vkv) if err != nil { t.Fatalf("Failed to generate new Store: %+v", err) } @@ -249,9 +234,8 @@ func makeTestStore() (*Store, *versioned.KV) { vkv := versioned.NewKV(kv) grp := cyclic.NewGroup(large.NewInt(173), large.NewInt(2)) - priv := grp.NewInt(2) - testStore, _ := NewStore(grp, vkv, priv) + testStore, _ := NewStore(grp, vkv) return testStore, vkv } diff --git a/storage/e2e/manager.go b/storage/e2e/manager.go index f6c000f78ff002e3a3c82db143b8e783ca8a497e..ff76e9985b72ef1c355be64ee0f643e1c07c3205 100644 --- a/storage/e2e/manager.go +++ b/storage/e2e/manager.go @@ -297,4 +297,4 @@ func (m *Manager) GetFileTransferPreimage() []byte { // fingerprint for group requests received from this user. func (m *Manager) GetGroupRequestPreimage() []byte { return preimage.Generate(m.GetRelationshipFingerprintBytes(), preimage.GroupRq) -} \ No newline at end of file +} diff --git a/storage/e2e/session.go b/storage/e2e/session.go index 8fbcae123c8a22ab87504bdac5188e78c9139902..10d7c4faf6ddcdd7e46ecbece5c0447b0ed74c36 100644 --- a/storage/e2e/session.go +++ b/storage/e2e/session.go @@ -639,7 +639,7 @@ func (s *Session) generate(kv *versioned.KV) *versioned.KV { s.baseKey.Bytes(), h).Int64() + int64(p.MinKeys)) // start rekeying when enough keys have been used - s.rekeyThreshold = uint32(math.Ceil(s.e2eParams.RekeyThreshold*float64(numKeys))) + s.rekeyThreshold = uint32(math.Ceil(s.e2eParams.RekeyThreshold * float64(numKeys))) // the total number of keys should be the number of rekeys plus the // number of keys to use diff --git a/storage/e2e/store.go b/storage/e2e/store.go index 9be32c5c490e715dfa424e8b788dfb21f68b47a9..931fb28449809fc398c70abfab12ef0090c3e92c 100644 --- a/storage/e2e/store.go +++ b/storage/e2e/store.go @@ -246,6 +246,22 @@ func (s *Store) GetPartnerContact(partnerID *id.ID) (contact.Contact, error) { return c, nil } +// GetPartners returns a list of all partner IDs that the user has +// an E2E relationship with. +func (s *Store) GetPartners() []*id.ID { + s.mux.RLock() + defer s.mux.RUnlock() + + partnerIds := make([]*id.ID, 0, len(s.managers)) + + for partnerId := range s.managers { + pid := partnerId + partnerIds = append(partnerIds, &pid) + } + + return partnerIds +} + // PopKey pops a key for use based upon its fingerprint. func (s *Store) PopKey(f format.Fingerprint) (*Key, bool) { return s.fingerprints.Pop(f) diff --git a/storage/edge/edge.go b/storage/edge/edge.go index 7e88d35eccbbe19752dccd4de63363e37e5a12f5..e6a25fa821587393fca2e5c236e36111a7bcadd9 100644 --- a/storage/edge/edge.go +++ b/storage/edge/edge.go @@ -2,13 +2,14 @@ package edge import ( "encoding/json" + "sync" + "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/storage/versioned" fingerprint2 "gitlab.com/elixxir/crypto/fingerprint" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/netTime" - "sync" ) // This stores Preimages which can be used with the identity fingerprint system. @@ -65,6 +66,8 @@ func (s *Store) Add(preimage Preimage, identity *id.ID) { // Add to the list if !preimages.add(preimage) { + jww.ERROR.Printf("Preimage already exists for id %s: %v", + identity, preimage) return } diff --git a/storage/fileTransfer/fileMessage.go b/storage/fileTransfer/fileMessage.go index 03269326bbeab891e4678c888994fc35ea1026a6..90fd6eb30345f2aa5eda31d313058aaadd437ee6 100644 --- a/storage/fileTransfer/fileMessage.go +++ b/storage/fileTransfer/fileMessage.go @@ -78,7 +78,7 @@ func UnmarshalPartMessage(b []byte) (PartMessage, error) { // Marshal returns the byte representation of the PartMessage. func (m PartMessage) Marshal() []byte { b := make([]byte, len(m.data)) - copy(b,m.data) + copy(b, m.data) return b } @@ -97,7 +97,7 @@ func (m PartMessage) SetPartNum(num uint16) { // GetPart returns the file part data from the message. func (m PartMessage) GetPart() []byte { b := make([]byte, len(m.part)) - copy(b,m.part) + copy(b, m.part) return b } diff --git a/storage/fileTransfer/receiveFileTransfers.go b/storage/fileTransfer/receiveFileTransfers.go index 0c7bffdc6881e96fd79026deb58c98195b05dfbf..8dcc8649c56f75e4e82727258de0c993fdfe2b39 100644 --- a/storage/fileTransfer/receiveFileTransfers.go +++ b/storage/fileTransfer/receiveFileTransfers.go @@ -29,7 +29,7 @@ const ( const ( saveReceivedTransfersListErr = "failed to save list of received items in transfer map to storage: %+v" loadReceivedTransfersListErr = "failed to load list of received items in transfer map from storage: %+v" - loadReceivedFileTransfersErr = "failed to load received transfers from storage: %+v" + loadReceivedFileTransfersErr = "[FT] Failed to load received transfers from storage: %+v" newReceivedTransferErr = "failed to create new received transfer: %+v" getReceivedTransferErr = "received transfer with ID %s not found" @@ -37,6 +37,10 @@ const ( noFingerprintErr = "no part found with fingerprint %s" addPartErr = "failed to add part to transfer %s: %+v" deleteReceivedTransferErr = "failed to delete received transfer with ID %s from store: %+v" + + // ReceivedFileTransfersStore.load + loadReceivedTransferWarn = "[FT] Failed to load received file transfer %d of %d with ID %s: %v" + loadReceivedTransfersAllErr = "failed to load all %d transfers" ) // ReceivedFileTransfersStore contains information for tracking a received @@ -265,7 +269,8 @@ func NewOrLoadReceivedFileTransfersStore(kv *versioned.KV) ( // Load transfers and fingerprints into the maps err = rft.load(transfersList) if err != nil { - return nil, errors.Errorf(loadReceivedFileTransfersErr, err) + jww.ERROR.Printf(loadReceivedFileTransfersErr, err) + return NewReceivedFileTransfersStore(kv) } return rft, nil @@ -304,12 +309,16 @@ func (rft *ReceivedFileTransfersStore) loadTransfersList() ( // map. Also adds all unused fingerprints in each ReceivedTransfer to the info // map. func (rft *ReceivedFileTransfersStore) load(list []ftCrypto.TransferID) error { + var errCount int + // Load each sentTransfer from storage into the map - for _, tid := range list { + for i, tid := range list { // Load the transfer with the given transfer ID from storage rt, err := loadReceivedTransfer(tid, rft.kv) if err != nil { - return err + jww.WARN.Printf(loadReceivedTransferWarn, i, len(list), tid, err) + errCount++ + continue } // Add transfer to transfer map @@ -329,6 +338,11 @@ func (rft *ReceivedFileTransfersStore) load(list []ftCrypto.TransferID) error { } } + // Return an error if all transfers failed to load + if errCount == len(list) { + return errors.Errorf(loadReceivedTransfersAllErr, len(list)) + } + return nil } diff --git a/storage/fileTransfer/receiveFileTransfers_test.go b/storage/fileTransfer/receiveFileTransfers_test.go index 4fd245ec4d902d99f8389a8a4a1a6393c95d1432..1eee9480f9c28fa0c82dc6c8a46a4e32edee1e6e 100644 --- a/storage/fileTransfer/receiveFileTransfers_test.go +++ b/storage/fileTransfer/receiveFileTransfers_test.go @@ -654,30 +654,6 @@ func TestNewOrLoadReceivedFileTransfersStore_NewReceivedFileTransfersStore(t *te } } -// Error path: tests that NewOrLoadReceivedFileTransfersStore returns the -// expected error when the first transfer loaded from storage does not exist. -func TestNewOrLoadReceivedFileTransfersStore_NoTransferInStorageError(t *testing.T) { - kv := versioned.NewKV(make(ekv.Memstore)) - expectedErr := strings.Split(loadReceivedFileTransfersErr, "%")[0] - - // Save list of one transfer ID to storage - obj := &versioned.Object{ - Version: receivedFileTransfersStoreVersion, - Timestamp: netTime.Now(), - Data: ftCrypto.UnmarshalTransferID([]byte("testID_01")).Bytes(), - } - err := kv.Prefix(receivedFileTransfersStorePrefix).Set( - receivedFileTransfersStoreKey, receivedFileTransfersStoreVersion, obj) - - // Load ReceivedFileTransfersStore from storage - _, err = NewOrLoadReceivedFileTransfersStore(kv) - if err == nil || !strings.Contains(err.Error(), expectedErr) { - t.Errorf("NewOrLoadReceivedFileTransfersStore did not return the "+ - "expected error when there is no transfer saved in storage."+ - "\nexpected: %s\nreceived: %+v", expectedErr, err) - } -} - // Tests that the list saved by ReceivedFileTransfersStore.saveTransfersList // matches the list loaded by ReceivedFileTransfersStore.load. func TestReceivedFileTransfersStore_saveTransfersList_loadTransfersList(t *testing.T) { @@ -802,6 +778,57 @@ func TestReceivedFileTransfersStore_load(t *testing.T) { } } +// Error path: tests that ReceivedFileTransfersStore.load returns an error when +// all file transfers fail to load from storage. +func TestReceivedFileTransfersStore_load_AllFail(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + rft, err := NewReceivedFileTransfersStore(kv) + if err != nil { + t.Fatalf("Failed to create new ReceivedFileTransfersStore: %+v", err) + } + + // Fill map with transfers + idList := make([]ftCrypto.TransferID, 7) + for i := range idList { + prng := NewPrng(int64(i)) + key, _ := ftCrypto.NewTransferKey(prng) + mac := []byte("transferMAC") + + idList[i], err = rft.AddTransfer(key, mac, 256, 16, 24, prng) + if err != nil { + t.Errorf("Failed to add new transfer #%d: %+v", i, err) + } + + err = rft.DeleteTransfer(idList[i]) + if err != nil { + t.Errorf("Failed to delete transfer: %+v", err) + } + } + + // Save the list + err = rft.saveTransfersList() + if err != nil { + t.Errorf("saveTransfersList returned an error: %+v", err) + } + + // Build new ReceivedFileTransfersStore + newRFT := &ReceivedFileTransfersStore{ + transfers: make(map[ftCrypto.TransferID]*ReceivedTransfer), + info: make(map[format.Fingerprint]*partInfo), + kv: kv.Prefix(receivedFileTransfersStorePrefix), + } + + expectedErr := fmt.Sprintf(loadReceivedTransfersAllErr, len(idList)) + + // Load saved transfers from storage + err = newRFT.load(idList) + if err == nil || err.Error() != expectedErr { + t.Errorf("load did not return the expected error when none of the "+ + "transfer could be loaded from storage."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + // Tests that a transfer list marshalled with // ReceivedFileTransfersStore.marshalTransfersList and unmarshalled with // unmarshalTransfersList matches the original. diff --git a/storage/fileTransfer/receiveTransfer_test.go b/storage/fileTransfer/receiveTransfer_test.go index 281bb373822ee0642a6c02c07c814d402cd3d174..0edd3cfe63112c519c45046a009ecce868c3cde3 100644 --- a/storage/fileTransfer/receiveTransfer_test.go +++ b/storage/fileTransfer/receiveTransfer_test.go @@ -557,7 +557,7 @@ func TestReceivedTransfer_AddPart(t *testing.T) { cmixMsg.SetMac(mac) // Add encrypted part - complete, err := rt.AddPart(cmixMsg,fpNum) + complete, err := rt.AddPart(cmixMsg, fpNum) if err != nil { t.Errorf("AddPart returned an error: %+v", err) } @@ -1156,4 +1156,4 @@ func newEmptyReceivedTransfer(numParts, numFps uint16, kv *versioned.KV, } return tid, rt, fileData -} \ No newline at end of file +} diff --git a/storage/fileTransfer/sentFileTransfers.go b/storage/fileTransfer/sentFileTransfers.go index e672ce184fa4674a47b94c89bb47abdc2b420eb8..eb14f0e8b0a33e090e9ebed9810ac9db58a1aa1a 100644 --- a/storage/fileTransfer/sentFileTransfers.go +++ b/storage/fileTransfer/sentFileTransfers.go @@ -32,12 +32,16 @@ const ( const ( saveSentTransfersListErr = "failed to save list of sent items in transfer map to storage: %+v" loadSentTransfersListErr = "failed to load list of sent items in transfer map from storage: %+v" - loadSentTransfersErr = "failed to load sent transfers from storage: %+v" + loadSentTransfersErr = "[FT] Failed to load sent transfers from storage: %+v" newSentTransferErr = "failed to create new sent transfer: %+v" getSentTransferErr = "sent file transfer not found" cancelCallbackErr = "[FT] Transfer with ID %s: %+v" deleteSentTransferErr = "failed to delete sent transfer with ID %s from store: %+v" + + // SentFileTransfersStore.loadTransfers + loadSentTransferWarn = "[FT] Failed to load sent file transfer %d of %d with ID %s: %v" + loadSentTransfersAllErr = "failed to load all %d transfers" ) // SentFileTransfersStore contains information for tracking sent file transfers. @@ -254,8 +258,7 @@ func NewOrLoadSentFileTransfersStore(kv *versioned.KV) (*SentFileTransfersStore, vo, err := sft.kv.Get( sentFileTransfersStoreKey, sentFileTransfersStoreVersion) if err != nil { - newSFT, err := NewSentFileTransfersStore(kv) - return newSFT, err + return NewSentFileTransfersStore(kv) } // Unmarshal data into list of saved transfer IDs @@ -264,7 +267,8 @@ func NewOrLoadSentFileTransfersStore(kv *versioned.KV) (*SentFileTransfersStore, // Load each transfer in the list from storage into the map err = sft.loadTransfers(transfersList) if err != nil { - return nil, errors.Errorf(loadSentTransfersErr, err) + jww.WARN.Printf(loadSentTransfersErr, err) + return NewSentFileTransfersStore(kv) } return sft, nil @@ -304,15 +308,22 @@ func (sft *SentFileTransfersStore) loadTransfersList() ([]ftCrypto.TransferID, // to add them back into the queue. func (sft *SentFileTransfersStore) loadTransfers(list []ftCrypto.TransferID) error { var err error + var errCount int // Load each sentTransfer from storage into the map - for _, tid := range list { + for i, tid := range list { sft.transfers[tid], err = loadSentTransfer(tid, sft.kv) if err != nil { - return err + jww.WARN.Printf(loadSentTransferWarn, i, len(list), tid, err) + errCount++ } } + // Return an error if all transfers failed to load + if errCount == len(list) { + return errors.Errorf(loadSentTransfersAllErr, len(list)) + } + return nil } diff --git a/storage/fileTransfer/sentFileTransfers_test.go b/storage/fileTransfer/sentFileTransfers_test.go index 5fb3bdc2f9df5aaac90ccb7aca70a348977a0371..336cbb4ca17b577a13e0728f3c499fd2a924fdd3 100644 --- a/storage/fileTransfer/sentFileTransfers_test.go +++ b/storage/fileTransfer/sentFileTransfers_test.go @@ -8,6 +8,7 @@ package fileTransfer import ( + "fmt" "gitlab.com/elixxir/client/storage/versioned" ftCrypto "gitlab.com/elixxir/crypto/fileTransfer" "gitlab.com/elixxir/ekv" @@ -582,30 +583,6 @@ func TestNewOrLoadSentFileTransfersStore_NewSentFileTransfersStore(t *testing.T) } } -// Error path: tests that the NewOrLoadSentFileTransfersStore returns the -// expected error when the first transfer loaded from storage does not exist. -func TestNewOrLoadSentFileTransfersStore_NoTransferInStorageError(t *testing.T) { - kv := versioned.NewKV(make(ekv.Memstore)) - expectedErr := strings.Split(loadSentTransfersErr, "%")[0] - - // Save list of one transfer ID to storage - obj := &versioned.Object{ - Version: sentFileTransfersStoreVersion, - Timestamp: netTime.Now(), - Data: ftCrypto.UnmarshalTransferID([]byte("testID_01")).Bytes(), - } - err := kv.Prefix(sentFileTransfersStorePrefix).Set( - sentFileTransfersStoreKey, sentFileTransfersStoreVersion, obj) - - // Load SentFileTransfersStore from storage - _, err = NewOrLoadSentFileTransfersStore(kv) - if err == nil || !strings.Contains(err.Error(), expectedErr) { - t.Errorf("NewOrLoadSentFileTransfersStore did not return the expected "+ - "error when there is no transfer saved in storage."+ - "\nexpected: %s\nreceived: %+v", expectedErr, err) - } -} - // Tests that SentFileTransfersStore.saveTransfersList saves all the transfer // IDs to storage by loading them from storage via // SentFileTransfersStore.loadTransfersList and comparing the list to the list @@ -646,7 +623,7 @@ func TestSentFileTransfersStore_saveTransfersList_loadTransfersList(t *testing.T } // Tests that the transfer loaded by SentFileTransfersStore.loadTransfers from -// storage matches the original in memory +// storage matches the original in memory. func TestSentFileTransfersStore_loadTransfers(t *testing.T) { kv := versioned.NewKV(make(ekv.Memstore)) sft := &SentFileTransfersStore{ @@ -686,6 +663,37 @@ func TestSentFileTransfersStore_loadTransfers(t *testing.T) { } } +// Error path: tests that SentFileTransfersStore.loadTransfers returns an error +// when all file transfers fail to load from storage. +func TestSentFileTransfersStore_loadTransfers_AllErrors(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)).Prefix(sentFileTransfersStorePrefix) + + // Add 10 transfers to map in memory + list := make([]ftCrypto.TransferID, 10) + for i := range list { + tid, st := newRandomSentTransfer(16, 24, kv, t) + list[i] = tid + err := st.delete() + if err != nil { + t.Errorf("Failed to delete transfer: %+v", err) + } + } + + expectedErr := fmt.Sprintf(loadSentTransfersAllErr, len(list)) + + // Load the transfers into a new SentFileTransfersStore + loadedSft := &SentFileTransfersStore{ + transfers: make(map[ftCrypto.TransferID]*SentTransfer), + kv: kv, + } + err := loadedSft.loadTransfers(list) + if err == nil || err.Error() != expectedErr { + t.Errorf("loadTransfers did not return the expected error when none "+ + "of the transfer could be loaded from storage."+ + "\nexpected: %s\nreceived: %+v", expectedErr, err) + } +} + // Tests that a transfer list marshalled with // SentFileTransfersStore.marshalTransfersList and unmarshalled with // unmarshalTransfersList matches the original. diff --git a/storage/fileTransfer/sentTransfer.go b/storage/fileTransfer/sentTransfer.go index cdffaaeca6807c4025b9bf90925130e6be9cbcb5..115eee1de2d8a95ef302edec84d6271e1ddce4ce 100644 --- a/storage/fileTransfer/sentTransfer.go +++ b/storage/fileTransfer/sentTransfer.go @@ -447,7 +447,7 @@ func (st *SentTransfer) GetEncryptedPart(partNum uint16, contentsSize int) (encP errors.Errorf(noPartNumErr, partNum) } - if err = partMsg.SetPart(part); err != nil{ + if err = partMsg.SetPart(part); err != nil { return nil, nil, format.Fingerprint{}, err } diff --git a/storage/fileTransfer/sentTransfer_test.go b/storage/fileTransfer/sentTransfer_test.go index 033992bf84b42aa4bf54431ac50e3c3c0b132998..ea4302b47a857125522b97be4cc13c4503e2d39c 100644 --- a/storage/fileTransfer/sentTransfer_test.go +++ b/storage/fileTransfer/sentTransfer_test.go @@ -850,9 +850,9 @@ func TestSentTransfer_GetEncryptedPart(t *testing.T) { "\nexpected: %+v\nreceived: %+v", partNum, i, expectedPart, partMsg.GetPart()) } - if partMsg.GetPartNum()!=i % st.numParts{ - t.Errorf("Number of part did not match, expected: %d, " + - "received: %d", i % st.numParts, partMsg.GetPartNum()) + if partMsg.GetPartNum() != i%st.numParts { + t.Errorf("Number of part did not match, expected: %d, "+ + "received: %d", i%st.numParts, partMsg.GetPartNum()) } } } diff --git a/storage/session.go b/storage/session.go index d11e11fc6697edd4e52d3550c6de2d6677a37bd1..2ad3042ed7c76758227a42f751f146ae15cc58b4 100644 --- a/storage/session.go +++ b/storage/session.go @@ -13,6 +13,7 @@ import ( "gitlab.com/elixxir/client/storage/edge" "gitlab.com/elixxir/client/storage/hostList" "gitlab.com/elixxir/client/storage/rounds" + "gitlab.com/elixxir/client/storage/ud" "gitlab.com/xx_network/primitives/rateLimiting" "sync" "testing" @@ -74,6 +75,7 @@ type Session struct { hostList *hostList.Store edgeCheck *edge.Store ringBuff *conversation.Buff + ud *ud.Store } // Initialize a new Session object @@ -116,7 +118,7 @@ func New(baseDir, password string, u userInterface.User, } uid := s.user.GetCryptographicIdentity().GetReceptionID() - s.cmix, err = cmix.NewStore(cmixGrp, s.kv, u.CmixDhPrivateKey) + s.cmix, err = cmix.NewStore(cmixGrp, s.kv) if err != nil { return nil, errors.WithMessage(err, "Failed to create cmix store") } @@ -178,6 +180,11 @@ func New(baseDir, password string, u userInterface.User, s.bucketStore = utility.NewStoredBucket(uint32(rateLimitParams.Capacity), uint32(rateLimitParams.LeakedTokens), time.Duration(rateLimitParams.LeakDuration), s.kv) + s.ud, err = ud.NewStore(s.kv) + if err != nil { + return nil, errors.WithMessage(err, "Failed to create ud store") + } + return s, nil } @@ -275,6 +282,11 @@ func Load(baseDir, password string, currentVersion version.Version, "Failed to load bucket store") } + s.ud, err = ud.NewOrLoadStore(s.kv) + if err != nil { + return nil, errors.WithMessage(err, "Failed to load ud store") + } + return s, nil } @@ -364,6 +376,12 @@ func (s *Session) GetEdge() *edge.Store { return s.edgeCheck } +func (s *Session) GetUd() *ud.Store { + s.mux.RLock() + defer s.mux.RUnlock() + return s.ud +} + // GetBucketParams returns the bucket params store. func (s *Session) GetBucketParams() *utility.BucketParamStore { s.mux.RLock() @@ -445,7 +463,7 @@ func InitTestingSession(i interface{}) *Session { "3A10B1C4D203CC76A470A33AFDCBDD92959859ABD8B56E1725252D78EAC66E71"+ "BA9AE3F1DD2487199874393CD4D832186800654760E1E34C09E4D155179F9EC0"+ "DC4473F996BDCE6EED1CABED8B6F116F7AD9CF505DF0F998E34AB27514B0FFE7", 16)) - cmixStore, err := cmix.NewStore(cmixGrp, kv, cmixGrp.NewInt(2)) + cmixStore, err := cmix.NewStore(cmixGrp, kv) if err != nil { jww.FATAL.Panicf("InitTestingSession failed to create dummy cmix session: %+v", err) } @@ -509,5 +527,10 @@ func InitTestingSession(i interface{}) *Session { // jww.FATAL.Panicf("Failed to create ring buffer store: %+v", err) //} + s.ud, err = ud.NewStore(s.kv) + if err != nil { + jww.FATAL.Panicf("Failed to create ud store: %v", err) + } + return s } diff --git a/storage/ud/facts.go b/storage/ud/facts.go new file mode 100644 index 0000000000000000000000000000000000000000..9ebfa59ba45ceaec15c31b909f0bd86ea2c009e6 --- /dev/null +++ b/storage/ud/facts.go @@ -0,0 +1,235 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package ud + +import ( + "fmt" + "github.com/pkg/errors" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/primitives/fact" + "sync" +) + +const ( + factTypeExistsErr = "Fact %v cannot be added as fact type %s has already been stored. Cancelling backup operation!" + backupMissingInvalidFactTypeErr = "BackUpMissingFacts expects input in the order (email, phone). " + + "%s (%s) is non-empty but not an email. Cancelling backup operation" + backupMissingAllZeroesFactErr = "Cannot backup missing facts: Both email and phone facts are empty!" + factNotInStoreErr = "Fact %v does not exist in store" + statefulStoreErr = "cannot overwrite ud store with existing data" +) + +// Store is the storage object for the higher level ud.Manager object. +// This storage implementation is written for client side. +type Store struct { + // confirmedFacts contains facts that have been confirmed + confirmedFacts map[fact.Fact]struct{} + // Stores facts that have been added by UDB but unconfirmed facts. + // Maps confirmID to fact + unconfirmedFacts map[string]fact.Fact + kv *versioned.KV + mux sync.RWMutex +} + +// NewStore creates a new Store object. If we are initializing from a backup, +// the backupFacts fact.FactList will be non-nil and initialize the state +// with the backed up data. +func NewStore(kv *versioned.KV) (*Store, error) { + kv = kv.Prefix(prefix) + s := &Store{ + confirmedFacts: make(map[fact.Fact]struct{}, 0), + unconfirmedFacts: make(map[string]fact.Fact, 0), + kv: kv, + } + + return s, s.save() +} + +// RestoreFromBackUp initializes the confirmedFacts map +// with the backed up fact data. This will error if +// the store is already stateful. +func (s *Store) RestoreFromBackUp(backupData fact.FactList) error { + s.mux.Lock() + defer s.mux.Unlock() + + if len(s.confirmedFacts) != 0 || len(s.unconfirmedFacts) != 0 { + return errors.New(statefulStoreErr) + } + + for _, f := range backupData { + if !isFactZero(f) { + s.confirmedFacts[f] = struct{}{} + } + } + + return s.save() +} + +// StoreUnconfirmedFact stores a fact that has been added to UD but has not been +// confirmed by the user. It is keyed on the confirmation ID given by UD. +func (s *Store) StoreUnconfirmedFact(confirmationId string, f fact.Fact) error { + s.mux.Lock() + defer s.mux.Unlock() + + s.unconfirmedFacts[confirmationId] = f + return s.saveUnconfirmedFacts() +} + +// ConfirmFact will delete the fact from the unconfirmed store and +// add it to the confirmed fact store. The Store will then be saved +func (s *Store) ConfirmFact(confirmationId string) error { + s.mux.Lock() + defer s.mux.Unlock() + + f, exists := s.unconfirmedFacts[confirmationId] + if !exists { + return errors.New(fmt.Sprintf("No fact exists in store "+ + "with confirmation ID %q", confirmationId)) + } + + delete(s.unconfirmedFacts, confirmationId) + s.confirmedFacts[f] = struct{}{} + return s.save() +} + +// BackUpMissingFacts adds a registered fact to the Store object. It can take in both an +// email and a phone number. One or the other may be an empty string, however both is considered +// an error. It checks for each whether that fact type already exists in the structure. If a fact +// type already exists, an error is returned. +// ************************************************************************ +// NOTE: This is done since BackUpMissingFacts is exposed to the +// bindings layer. This prevents front end from using this as the method +// to store facts on their end, which is not its intended use case. It's intended use +// case is to store already registered facts, prior to the creation of this function. +// We handle storage of newly registered internally using Store.ConfirmFact. +// ************************************************************************ +// Any other fact.FactType is not accepted and returns an error and nothing is backed up. +// If you attempt to back up a fact type that has already been backed up, +// an error will be returned and nothing will be backed up. +// Otherwise, it adds the fact and returns whether the Store saved successfully. +func (s *Store) BackUpMissingFacts(email, phone fact.Fact) error { + s.mux.Lock() + defer s.mux.Unlock() + + modifiedEmail, modifiedPhone := false, false + + // Handle email if it is not zero (empty string) + if !isFactZero(email) { + // check if fact is expected type + if email.T != fact.Email { + return errors.New(fmt.Sprintf(backupMissingInvalidFactTypeErr, fact.Email, email.Fact)) + } + + // Check if fact type is already in map. See docstring NOTE for explanation + if isFactTypeInMap(fact.Email, s.confirmedFacts) { + // If an email exists in memory, return an error + return errors.Errorf(factTypeExistsErr, email, fact.Email) + } else { + modifiedEmail = true + } + } + + if !isFactZero(phone) { + // check if fact is expected type + if phone.T != fact.Phone { + return errors.New(fmt.Sprintf(backupMissingInvalidFactTypeErr, fact.Phone, phone.Fact)) + } + + // Check if fact type is already in map. See docstring NOTE for explanation + if isFactTypeInMap(fact.Phone, s.confirmedFacts) { + // If a phone exists in memory, return an error + return errors.Errorf(factTypeExistsErr, phone, fact.Phone) + } else { + modifiedPhone = true + } + } + + if modifiedPhone || modifiedEmail { + if modifiedEmail { + s.confirmedFacts[email] = struct{}{} + } + + if modifiedPhone { + s.confirmedFacts[phone] = struct{}{} + } + + return s.saveConfirmedFacts() + } + + return nil + +} + +// DeleteFact is our internal use function which will delete the registered fact +// from memory and storage. An error is returned if the fact does not exist in +// memory. +func (s *Store) DeleteFact(f fact.Fact) error { + s.mux.Lock() + defer s.mux.Unlock() + + if _, exists := s.confirmedFacts[f]; !exists { + return errors.Errorf(factNotInStoreErr, f) + } + + delete(s.confirmedFacts, f) + return s.saveConfirmedFacts() +} + +// GetStringifiedFacts returns a list of stringified facts from the Store's +// confirmedFacts map. +func (s *Store) GetStringifiedFacts() []string { + s.mux.RLock() + defer s.mux.RUnlock() + + return s.serializeConfirmedFacts() +} + +// GetFacts returns a list of fact.Fact objects that exist within the +// Store's confirmedFacts map. +func (s *Store) GetFacts() []fact.Fact { + s.mux.RLock() + defer s.mux.RUnlock() + + // Flatten the facts into a slice + facts := make([]fact.Fact, 0, len(s.confirmedFacts)) + for f := range s.confirmedFacts { + facts = append(facts, f) + } + + return facts +} + +// serializeConfirmedFacts is a helper function which serializes Store's confirmedFacts +// map into a list of strings. Each string in the list represents +// a fact.Fact that has been Stringified. +func (s *Store) serializeConfirmedFacts() []string { + fStrings := make([]string, 0, len(s.confirmedFacts)) + for f := range s.confirmedFacts { + fStrings = append(fStrings, f.Stringify()) + } + + return fStrings +} + +// fixme: consider this being a method on the fact.Fact object? +// isFactZero tests whether a fact has been uninitialized. +func isFactZero(f fact.Fact) bool { + return f.T == fact.Username && f.Fact == "" +} + +// isFactTypeInMap is a helper function which determines whether a fact type exists within +// the data structure. +func isFactTypeInMap(factType fact.FactType, facts map[fact.Fact]struct{}) bool { + for f := range facts { + if f.T == factType { + return true + } + } + + return false +} diff --git a/storage/ud/facts_test.go b/storage/ud/facts_test.go new file mode 100644 index 0000000000000000000000000000000000000000..52e51979dbb417f2dd584b50c77c68ecd5167a26 --- /dev/null +++ b/storage/ud/facts_test.go @@ -0,0 +1,363 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package ud + +import ( + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/ekv" + "gitlab.com/elixxir/primitives/fact" + "reflect" + "sort" + "testing" +) + +// Smoke test. +func TestNewStore(t *testing.T) { + + kv := versioned.NewKV(make(ekv.Memstore)) + + _, err := NewStore(kv) + if err != nil { + t.Errorf("NewStore() produced an error: %v", err) + } + +} + +// Unit test +func TestStore_RestoreFromBackUp(t *testing.T) { + + kv := versioned.NewKV(make(ekv.Memstore)) + + s, err := NewStore(kv) + if err != nil { + t.Errorf("NewStore() produced an error: %v", err) + } + + expected := fact.Fact{ + Fact: "josh", + T: fact.Username, + } + + fl := fact.FactList{expected} + + err = s.RestoreFromBackUp(fl) + if err != nil { + t.Fatalf("RestoreFromBackup err: %v", err) + } + + _, exists := s.confirmedFacts[expected] + if !exists { + t.Fatalf("Fact %s does not exist in map", expected) + } + +} + +// Error case. +func TestStore_RestoreFromBackUp_StatefulStore(t *testing.T) { + + kv := versioned.NewKV(make(ekv.Memstore)) + + s, err := NewStore(kv) + if err != nil { + t.Errorf("NewStore() produced an error: %v", err) + } + + confirmId := "confirm" + expected := fact.Fact{ + Fact: "josh", + T: fact.Username, + } + + err = s.StoreUnconfirmedFact(confirmId, expected) + if err != nil { + t.Fatalf("StoreUnconfirmedFact error: %v", err) + } + + // Expected error: should error when restoring on + // a stateful store. + fl := fact.FactList{expected} + err = s.RestoreFromBackUp(fl) + if err == nil { + t.Fatalf("RestoreFromBackup err: %v", err) + } + +} + +// Unit test. +func TestStore_ConfirmFact(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + + expectedStore, err := NewStore(kv) + if err != nil { + t.Errorf("NewStore() produced an error: %v", err) + } + + confirmId := "confirm" + + expected := fact.Fact{ + Fact: "josh", + T: fact.Username, + } + + err = expectedStore.StoreUnconfirmedFact(confirmId, expected) + if err != nil { + t.Fatalf("StoreUnconfirmedFact error: %v", err) + } + + err = expectedStore.ConfirmFact(confirmId) + if err != nil { + t.Fatalf("ConfirmFact() produced an error: %v", err) + } + + _, exists := expectedStore.confirmedFacts[expected] + if !exists { + t.Fatalf("Fact %s does not exist in map", expected) + } + + // Check that fact was removed from unconfirmed + _, exists = expectedStore.unconfirmedFacts[confirmId] + if exists { + t.Fatalf("Confirmed fact %v should be removed from unconfirmed"+ + " map", expected) + } +} + +func TestStore_StoreUnconfirmedFact(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + + expectedStore, err := NewStore(kv) + if err != nil { + t.Errorf("NewStore() produced an error: %v", err) + } + + confirmId := "confirm" + + expected := fact.Fact{ + Fact: "josh", + T: fact.Username, + } + + err = expectedStore.StoreUnconfirmedFact(confirmId, expected) + if err != nil { + t.Fatalf("StoreUnconfirmedFact error: %v", err) + } + + // Check that fact exists in unconfirmed + _, exists := expectedStore.unconfirmedFacts[confirmId] + if !exists { + t.Fatalf("Confirmed fact %v should be removed from unconfirmed"+ + " map", expected) + } +} + +// Unit test. +func TestStore_DeleteFact(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + + expectedStore, err := NewStore(kv) + if err != nil { + t.Errorf("NewStore() produced an error: %v", err) + } + + expected := fact.Fact{ + Fact: "josh", + T: fact.Username, + } + + expectedStore.confirmedFacts[expected] = struct{}{} + + _, exists := expectedStore.confirmedFacts[expected] + if !exists { + t.Fatalf("Fact %s does not exist in map", expected) + } + + err = expectedStore.DeleteFact(expected) + if err != nil { + t.Fatalf("DeleteFact() produced an error: %v", err) + } + + err = expectedStore.DeleteFact(expected) + if err == nil { + t.Fatalf("DeleteFact should produce an error when deleting a fact not in store") + } + +} + +// Unit test. +func TestStore_BackUpMissingFacts(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + + expectedStore, err := NewStore(kv) + if err != nil { + t.Errorf("NewStore() produced an error: %v", err) + } + + email := fact.Fact{ + Fact: "josh@elixxir.io", + T: fact.Email, + } + + phone := fact.Fact{ + Fact: "6175555678", + T: fact.Phone, + } + + err = expectedStore.BackUpMissingFacts(email, phone) + if err != nil { + t.Fatalf("BackUpMissingFacts() produced an error: %v", err) + } + + _, exists := expectedStore.confirmedFacts[email] + if !exists { + t.Fatalf("Fact %v not found in store.", email) + } + + _, exists = expectedStore.confirmedFacts[phone] + if !exists { + t.Fatalf("Fact %v not found in store.", phone) + } + +} + +// Error case. +func TestStore_BackUpMissingFacts_DuplicateFactType(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + + expectedStore, err := NewStore(kv) + if err != nil { + t.Errorf("NewStore() produced an error: %v", err) + } + + email := fact.Fact{ + Fact: "josh@elixxir.io", + T: fact.Email, + } + + phone := fact.Fact{ + Fact: "6175555678", + T: fact.Phone, + } + + err = expectedStore.BackUpMissingFacts(email, phone) + if err != nil { + t.Fatalf("BackUpMissingFacts() produced an error: %v", err) + } + + err = expectedStore.BackUpMissingFacts(email, fact.Fact{}) + if err == nil { + t.Fatalf("BackUpMissingFacts() should not allow backing up an "+ + "email when an email has already been backed up: %v", err) + } + + err = expectedStore.BackUpMissingFacts(fact.Fact{}, phone) + if err == nil { + t.Fatalf("BackUpMissingFacts() should not allow backing up a "+ + "phone number when a phone number has already been backed up: %v", err) + } + +} + +// Unit test. +func TestStore_GetFacts(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + + testStore, err := NewStore(kv) + if err != nil { + t.Errorf("NewStore() produced an error: %v", err) + } + + emailFact := fact.Fact{ + Fact: "josh@elixxir.io", + T: fact.Email, + } + + emptyFact := fact.Fact{} + + err = testStore.BackUpMissingFacts(emailFact, emptyFact) + if err != nil { + t.Fatalf("Faild to add fact %v: %v", emailFact, err) + } + + phoneFact := fact.Fact{ + Fact: "6175555212", + T: fact.Phone, + } + + err = testStore.BackUpMissingFacts(emptyFact, phoneFact) + if err != nil { + t.Fatalf("Faild to add fact %v: %v", phoneFact, err) + } + + expectedFacts := []fact.Fact{emailFact, phoneFact} + + receivedFacts := testStore.GetFacts() + + sort.SliceStable(receivedFacts, func(i, j int) bool { + return receivedFacts[i].Fact > receivedFacts[j].Fact + }) + + sort.SliceStable(expectedFacts, func(i, j int) bool { + return expectedFacts[i].Fact > expectedFacts[j].Fact + }) + + if !reflect.DeepEqual(expectedFacts, receivedFacts) { + t.Fatalf("GetFacts() did not return expected fact list."+ + "\nExpected: %v"+ + "\nReceived: %v", expectedFacts, receivedFacts) + } +} + +// Unit test. +func TestStore_GetFactStrings(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + + testStore, err := NewStore(kv) + if err != nil { + t.Errorf("NewStore() produced an error: %v", err) + } + + emailFact := fact.Fact{ + Fact: "josh@elixxir.io", + T: fact.Email, + } + + emptyFact := fact.Fact{} + + err = testStore.BackUpMissingFacts(emailFact, emptyFact) + if err != nil { + t.Fatalf("Faild to add fact %v: %v", emailFact, err) + } + + phoneFact := fact.Fact{ + Fact: "6175555212", + T: fact.Phone, + } + + err = testStore.BackUpMissingFacts(emptyFact, phoneFact) + if err != nil { + t.Fatalf("Faild to add fact %v: %v", phoneFact, err) + } + + expectedFacts := []string{emailFact.Stringify(), phoneFact.Stringify()} + + receivedFacts := testStore.GetStringifiedFacts() + sort.SliceStable(receivedFacts, func(i, j int) bool { + return receivedFacts[i] > receivedFacts[j] + }) + + sort.SliceStable(expectedFacts, func(i, j int) bool { + return expectedFacts[i] > expectedFacts[j] + }) + + if !reflect.DeepEqual(expectedFacts, receivedFacts) { + t.Fatalf("GetStringifiedFacts() did not return expected fact list."+ + "\nExpected: %v"+ + "\nReceived: %v", expectedFacts, receivedFacts) + } + +} diff --git a/storage/ud/store.go b/storage/ud/store.go new file mode 100644 index 0000000000000000000000000000000000000000..64325e317bef67ef2d192ab64e70ba32da2d5365 --- /dev/null +++ b/storage/ud/store.go @@ -0,0 +1,254 @@ +package ud + +// This file handles the storage operations on facts. + +import ( + "encoding/json" + "github.com/pkg/errors" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/primitives/fact" + "gitlab.com/xx_network/primitives/netTime" + "strings" +) + +// Storage constants +const ( + version = 0 + prefix = "udStorePrefix" + unconfirmedFactKey = "unconfirmedFactKey" + confirmedFactKey = "confirmedFactKey" +) + +// Error constants +const ( + malformedFactErr = "Failed to load due to " + + "malformed fact" + loadConfirmedFactErr = "Failed to load confirmed facts" + loadUnconfirmedFactErr = "Failed to load unconfirmed facts" + saveUnconfirmedFactErr = "Failed to save unconfirmed facts" + saveConfirmedFactErr = "Failed to save confirmed facts" +) + +// unconfirmedFactDisk is an object used to store the data of an unconfirmed fact. +// It combines the key (confirmationId) and fact data (stringifiedFact) into a +// single JSON-able object. +type unconfirmedFactDisk struct { + confirmationId string + stringifiedFact string +} + +///////////////////////////////////////////////////////////////// +// SAVE FUNCTIONS +///////////////////////////////////////////////////////////////// + +// save serializes the state within Store into byte data and stores +// that data into storage via the EKV. +func (s *Store) save() error { + + err := s.saveUnconfirmedFacts() + if err != nil { + return errors.WithMessage(err, saveUnconfirmedFactErr) + } + + err = s.saveConfirmedFacts() + if err != nil { + return errors.WithMessage(err, saveConfirmedFactErr) + } + + return nil +} + +// saveConfirmedFacts saves all the data within Store.confirmedFacts into storage. +func (s *Store) saveConfirmedFacts() error { + + data, err := s.marshalConfirmedFacts() + if err != nil { + return err + } + + // Construct versioned object + now := netTime.Now() + obj := versioned.Object{ + Version: version, + Timestamp: now, + Data: data, + } + + // Save to storage + return s.kv.Set(confirmedFactKey, version, &obj) +} + +// saveUnconfirmedFacts saves all data within Store.unconfirmedFacts into storage. +func (s *Store) saveUnconfirmedFacts() error { + data, err := s.marshalUnconfirmedFacts() + if err != nil { + return err + } + + // Construct versioned object + now := netTime.Now() + obj := versioned.Object{ + Version: version, + Timestamp: now, + Data: data, + } + + // Save to storage + return s.kv.Set(unconfirmedFactKey, version, &obj) + +} + +///////////////////////////////////////////////////////////////// +// LOAD FUNCTIONS +///////////////////////////////////////////////////////////////// + +// NewOrLoadStore loads the Store object from the provided versioned.KV. +func NewOrLoadStore(kv *versioned.KV) (*Store, error) { + + s := &Store{ + confirmedFacts: make(map[fact.Fact]struct{}, 0), + unconfirmedFacts: make(map[string]fact.Fact, 0), + kv: kv.Prefix(prefix), + } + + if err := s.load(); err != nil { + if strings.Contains(err.Error(), "object not found") || + strings.Contains(err.Error(), "no such file or directory") { + return s, s.save() + } + } + + return s, nil + +} + +// load is a helper function which loads all data stored in storage from +// the save operation. +func (s *Store) load() error { + + err := s.loadUnconfirmedFacts() + if err != nil { + return errors.WithMessage(err, loadUnconfirmedFactErr) + } + + err = s.loadConfirmedFacts() + if err != nil { + return errors.WithMessage(err, loadConfirmedFactErr) + } + + return nil +} + +// loadConfirmedFacts loads all confirmed facts from storage. +// It is the inverse operation of saveConfirmedFacts. +func (s *Store) loadConfirmedFacts() error { + // Pull data from storage + obj, err := s.kv.Get(confirmedFactKey, version) + if err != nil { + return err + } + + // Place the map in memory + s.confirmedFacts, err = s.unmarshalConfirmedFacts(obj.Data) + if err != nil { + return err + } + + return nil +} + +// loadUnconfirmedFacts loads all unconfirmed facts from storage. +// It is the inverse operation of saveUnconfirmedFacts. +func (s *Store) loadUnconfirmedFacts() error { + // Pull data from storage + obj, err := s.kv.Get(unconfirmedFactKey, version) + if err != nil { + return err + } + + // Place the map in memory + s.unconfirmedFacts, err = s.unmarshalUnconfirmedFacts(obj.Data) + if err != nil { + return err + } + + return nil +} + +///////////////////////////////////////////////////////////////// +// MARSHAL/UNMARSHAL FUNCTIONS +///////////////////////////////////////////////////////////////// + +// marshalConfirmedFacts is a marshaller which serializes the data +//// in the confirmedFacts map into a JSON. +func (s *Store) marshalConfirmedFacts() ([]byte, error) { + // Flatten confirmed facts to a list + fStrings := s.serializeConfirmedFacts() + + // Marshal to JSON + return json.Marshal(&fStrings) +} + +// marshalUnconfirmedFacts is a marshaller which serializes the data +// in the unconfirmedFacts map into a JSON. +func (s *Store) marshalUnconfirmedFacts() ([]byte, error) { + // Flatten unconfirmed facts to a list + ufdList := make([]unconfirmedFactDisk, 0, len(s.unconfirmedFacts)) + for confirmationId, f := range s.unconfirmedFacts { + ufd := unconfirmedFactDisk{ + confirmationId: confirmationId, + stringifiedFact: f.Stringify(), + } + ufdList = append(ufdList, ufd) + } + + return json.Marshal(&ufdList) +} + +// unmarshalConfirmedFacts is a function which deserializes the data from storage +// into a structure matching the confirmedFacts map. +func (s *Store) unmarshalConfirmedFacts(data []byte) (map[fact.Fact]struct{}, error) { + // Unmarshal into list + var fStrings []string + err := json.Unmarshal(data, &fStrings) + if err != nil { + return nil, err + } + + // Deserialize the list into a map + confirmedFacts := make(map[fact.Fact]struct{}, 0) + for _, fStr := range fStrings { + f, err := fact.UnstringifyFact(fStr) + if err != nil { + return nil, errors.WithMessage(err, malformedFactErr) + } + + confirmedFacts[f] = struct{}{} + } + + return confirmedFacts, nil +} + +// unmarshalUnconfirmedFacts is a function which deserializes the data from storage +// into a structure matching the unconfirmedFacts map. +func (s *Store) unmarshalUnconfirmedFacts(data []byte) (map[string]fact.Fact, error) { + // Unmarshal into list + var ufdList []unconfirmedFactDisk + err := json.Unmarshal(data, &ufdList) + if err != nil { + return nil, err + } + + // Deserialize the list into a map + unconfirmedFacts := make(map[string]fact.Fact, 0) + for _, ufd := range ufdList { + f, err := fact.UnstringifyFact(ufd.stringifiedFact) + if err != nil { + return nil, errors.WithMessage(err, malformedFactErr) + } + + unconfirmedFacts[ufd.confirmationId] = f + } + + return unconfirmedFacts, nil +} diff --git a/storage/ud/store_test.go b/storage/ud/store_test.go new file mode 100644 index 0000000000000000000000000000000000000000..977aba8ef641029199bf186d4e9a1af2fc692131 --- /dev/null +++ b/storage/ud/store_test.go @@ -0,0 +1,129 @@ +package ud + +import ( + "bytes" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/ekv" + "gitlab.com/elixxir/primitives/fact" + "reflect" + "testing" +) + +// Test it loads a Store from storage if it exists. +func TestNewOrLoadStore_LoadStore(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + + expectedStore, err := NewStore(kv) + if err != nil { + t.Errorf("NewStore() produced an error: %v", err) + } + + receivedStore, err := NewOrLoadStore(kv) + if err != nil { + t.Fatalf("NewOrLoadStore() produced an error: %v", err) + } + + if !reflect.DeepEqual(expectedStore, receivedStore) { + t.Errorf("NewOrLoadStore() returned incorrect Store."+ + "\nexpected: %#v\nreceived: %#v", expectedStore, + receivedStore) + + } + +} + +// Test that it creates a new store if an old one is not in storage. +func TestNewOrLoadStore_NewStore(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + + receivedStore, err := NewOrLoadStore(kv) + if err != nil { + t.Fatalf("NewOrLoadStore() produced an error: %v", err) + } + + expectedStore := &Store{ + confirmedFacts: make(map[fact.Fact]struct{}, 0), + unconfirmedFacts: make(map[string]fact.Fact, 0), + kv: kv.Prefix(prefix), + } + + if !reflect.DeepEqual(expectedStore, receivedStore) { + t.Errorf("NewOrLoadStore() returned incorrect Store."+ + "\nexpected: %#v\nreceived: %#v", expectedStore, + receivedStore) + + } + +} + +func TestStore_MarshalUnmarshal_ConfirmedFacts(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + + expectedStore, err := NewStore(kv) + if err != nil { + t.Errorf("NewStore() produced an error: %v", err) + } + + data, err := expectedStore.kv.Get(confirmedFactKey, version) + if err != nil { + t.Errorf("Get() error when getting Store from KV: %v", err) + } + + expectedData, err := expectedStore.marshalConfirmedFacts() + if err != nil { + t.Fatalf("marshalConfirmedFact error: %+v", err) + } + + if !bytes.Equal(expectedData, data.Data) { + t.Errorf("NewStore() returned incorrect Store."+ + "\nexpected: %+v\nreceived: %+v", expectedData, + data.Data) + } + + recieved, err := expectedStore.unmarshalConfirmedFacts(data.Data) + if err != nil { + t.Fatalf("unmarshalUnconfirmedFacts error: %v", err) + } + + if !reflect.DeepEqual(recieved, expectedStore.confirmedFacts) { + t.Fatalf("Marshal/Unmarshal did not produce identical data"+ + "\nExpected: %v "+ + "\nReceived: %v", expectedStore.confirmedFacts, recieved) + } +} + +func TestStore_MarshalUnmarshal_UnconfirmedFacts(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + + expectedStore, err := NewStore(kv) + if err != nil { + t.Errorf("NewStore() produced an error: %v", err) + } + + data, err := expectedStore.kv.Get(unconfirmedFactKey, version) + if err != nil { + t.Errorf("Get() error when getting Store from KV: %v", err) + } + + expectedData, err := expectedStore.marshalUnconfirmedFacts() + if err != nil { + t.Fatalf("marshalConfirmedFact error: %+v", err) + } + + if !bytes.Equal(expectedData, data.Data) { + t.Errorf("NewStore() returned incorrect Store."+ + "\nexpected: %+v\nreceived: %+v", expectedData, + data.Data) + } + + recieved, err := expectedStore.unmarshalUnconfirmedFacts(data.Data) + if err != nil { + t.Fatalf("unmarshalUnconfirmedFacts error: %v", err) + } + + if !reflect.DeepEqual(recieved, expectedStore.unconfirmedFacts) { + t.Fatalf("Marshal/Unmarshal did not produce identical data"+ + "\nExpected: %v "+ + "\nReceived: %v", expectedStore.unconfirmedFacts, recieved) + } +} diff --git a/storage/user.go b/storage/user.go index 313741471ee5e3fefa12349d2e9b441352b983e1..e8f89909279f4496ea625b10f865fe4391d90f6c 100644 --- a/storage/user.go +++ b/storage/user.go @@ -22,8 +22,6 @@ func (s *Session) GetUser() user.User { ReceptionSalt: copySlice(ci.GetReceptionSalt()), ReceptionRSA: ci.GetReceptionRSA(), Precanned: ci.IsPrecanned(), - CmixDhPrivateKey: s.cmix.GetDHPrivateKey().DeepCopy(), - CmixDhPublicKey: s.cmix.GetDHPublicKey().DeepCopy(), E2eDhPrivateKey: s.e2e.GetDHPrivateKey().DeepCopy(), E2eDhPublicKey: s.e2e.GetDHPublicKey().DeepCopy(), } diff --git a/storage/utility/messageBuffer.go b/storage/utility/messageBuffer.go index d9c7ad749a3b1011c976fe97387dac281aec7086..f9891edce39ff284406bd1878df8a627f3a7589b 100644 --- a/storage/utility/messageBuffer.go +++ b/storage/utility/messageBuffer.go @@ -270,7 +270,7 @@ func (mb *MessageBuffer) Next() (interface{}, bool) { // Retrieve the message for storage m, err = mb.handler.LoadMessage(mb.kv, makeStoredMessageKey(mb.key, h)) if err != nil { - m=nil + m = nil jww.ERROR.Printf("Failed to load message %s from store, "+ "this may happen on occasion due to replays to increase "+ "reliability: %v", h, err) @@ -292,7 +292,7 @@ func next(msgMap map[MessageHash]struct{}) MessageHash { func (mb *MessageBuffer) Succeeded(m interface{}) { h := mb.handler.HashMessage(m) jww.TRACE.Printf("Critical Messages Succeeded(%s)", - base64.StdEncoding.EncodeToString((h[:]))) + base64.StdEncoding.EncodeToString(h[:])) mb.mux.Lock() defer mb.mux.Unlock() diff --git a/storage/utility/meteredCmixMessageBuffer.go b/storage/utility/meteredCmixMessageBuffer.go index 9060dbb7ea2424f87879032371ee25944ff64851..b0b425800738fba7af41ae7339bafdf6ffa1cd6d 100644 --- a/storage/utility/meteredCmixMessageBuffer.go +++ b/storage/utility/meteredCmixMessageBuffer.go @@ -114,7 +114,7 @@ func LoadMeteredCmixMessageBuffer(kv *versioned.KV, key string) (*MeteredCmixMes } func (mcmb *MeteredCmixMessageBuffer) Add(m format.Message) { - if m.GetPrimeByteLen()==0{ + if m.GetPrimeByteLen() == 0 { jww.FATAL.Panicf("Cannot handle a metered " + "cmix message with a length of 0") } diff --git a/ud/addFact.go b/ud/addFact.go index 1fc1a15f0a24fd327445d4d8220517863cd5eda6..81734a2bdca3e1e0f73c3943b6330fede685228b 100644 --- a/ud/addFact.go +++ b/ud/addFact.go @@ -75,6 +75,10 @@ func (m *Manager) addFact(inFact fact.Fact, uid *id.ID, aFC addFactComms) (strin confirmationID = response.ConfirmationID } + err = m.storage.GetUd().StoreUnconfirmedFact(confirmationID, f) + if err != nil { + return "", errors.WithMessagef(err, "Failed to store unconfirmed fact %v", f.Fact) + } // Return the error return confirmationID, err } diff --git a/ud/addFact_test.go b/ud/addFact_test.go index ba6db2dc18596d476676666e7a4eaed4da0feabc..7fecfc4d6d59a3a15666e662cbb149af52c6795d 100644 --- a/ud/addFact_test.go +++ b/ud/addFact_test.go @@ -2,6 +2,7 @@ package ud import ( jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/storage" "gitlab.com/elixxir/comms/client" pb "gitlab.com/elixxir/comms/mixmessages" "gitlab.com/elixxir/primitives/fact" @@ -50,6 +51,7 @@ func TestAddFact(t *testing.T) { net: newTestNetworkManager(t), privKey: cpk, registered: &isReg, + storage: storage.InitTestingSession(t), } // Create our test fact diff --git a/ud/confirmFact.go b/ud/confirmFact.go index b7c4294e2c026e35da1ae70b7739fe6feea0c0bd..af71eb7a43cc811839e360420dbdca96c7a5e812 100644 --- a/ud/confirmFact.go +++ b/ud/confirmFact.go @@ -40,5 +40,14 @@ func (m *Manager) confirmFact(confirmationID, code string, comm confirmFactComm) Code: code, } _, err = comm.SendConfirmFact(host, msg) - return err + if err != nil { + return err + } + + err = m.storage.GetUd().ConfirmFact(confirmationID) + if err != nil { + return errors.WithMessagef(err, "Failed to confirm fact in storage with confirmation ID: %q", confirmationID) + } + + return nil } diff --git a/ud/confirmFact_test.go b/ud/confirmFact_test.go index 9fe29b0a4491e6df330595d27d570e98d87f7fb9..4b9789b40d07d4ea3689d2caa5c90752ca1fea46 100644 --- a/ud/confirmFact_test.go +++ b/ud/confirmFact_test.go @@ -1,8 +1,10 @@ package ud import ( + "gitlab.com/elixxir/client/storage" "gitlab.com/elixxir/comms/client" pb "gitlab.com/elixxir/comms/mixmessages" + "gitlab.com/elixxir/primitives/fact" "gitlab.com/xx_network/comms/connect" "gitlab.com/xx_network/comms/messages" "reflect" @@ -32,6 +34,7 @@ func TestManager_confirmFact(t *testing.T) { comms: comms, net: newTestNetworkManager(t), registered: &isReg, + storage: storage.InitTestingSession(t), } c := &testComm{} @@ -41,6 +44,12 @@ func TestManager_confirmFact(t *testing.T) { Code: "1234", } + // Set up store for expected state + err = m.storage.GetUd().StoreUnconfirmedFact(expectedRequest.ConfirmationID, fact.Fact{}) + if err != nil { + t.Fatalf("StoreUnconfirmedFact error: %v", err) + } + err = m.confirmFact(expectedRequest.ConfirmationID, expectedRequest.Code, c) if err != nil { t.Errorf("confirmFact() returned an error: %+v", err) diff --git a/ud/lookup.go b/ud/lookup.go index 2dc2dd2a8724227423df2ff028809746aa49cc8f..1769b5f34d5cca735eb96097f34e8a3f2ed70e42 100644 --- a/ud/lookup.go +++ b/ud/lookup.go @@ -23,7 +23,31 @@ type lookupCallback func(contact.Contact, error) // system or returns by the timeout. func (m *Manager) Lookup(uid *id.ID, callback lookupCallback, timeout time.Duration) error { jww.INFO.Printf("ud.Lookup(%s, %s)", uid, timeout) + return m.lookup(uid, callback, timeout) +} + +// BatchLookup performs a Lookup operation on a list of user IDs. +// The lookup performs a callback on each lookup on the returned contact object +// constructed from the response. +func (m *Manager) BatchLookup(uids []*id.ID, callback lookupCallback, timeout time.Duration) { + jww.INFO.Printf("ud.BatchLookup(%s, %s)", uids, timeout) + + for _, uid := range uids { + go func(localUid *id.ID) { + err := m.lookup(localUid, callback, timeout) + if err != nil { + jww.WARN.Printf("Failed batch lookup on user %s: %v", localUid, err) + } + }(uid) + } + + return +} +// lookup is a helper function which sends a lookup request to the user discovery +// service. It will construct a contact object off of the returned public key. +// The callback will be called on that contact object. +func (m *Manager) lookup(uid *id.ID, callback lookupCallback, timeout time.Duration) error { // Build the request and marshal it request := &LookupSend{UserID: uid.Marshal()} requestMarshaled, err := proto.Marshal(request) @@ -50,6 +74,9 @@ func (m *Manager) Lookup(uid *id.ID, callback lookupCallback, timeout time.Durat return nil } +// lookupResponseProcess processes the lookup response. The returned public key +// and the user ID will be constructed into a contact object. The contact object +// will be passed into the callback. func (m *Manager) lookupResponseProcess(uid *id.ID, callback lookupCallback, payload []byte, err error) { if err != nil { diff --git a/ud/manager.go b/ud/manager.go index b271c0d90a635e3927728a9337b2902bb8a58be4..ebf2798236325dd6ff7b30187a16e4789d85b580 100644 --- a/ud/manager.go +++ b/ud/manager.go @@ -12,6 +12,7 @@ import ( "gitlab.com/elixxir/crypto/contact" "gitlab.com/elixxir/crypto/cyclic" "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/elixxir/primitives/fact" "gitlab.com/xx_network/comms/connect" "gitlab.com/xx_network/crypto/signature/rsa" "gitlab.com/xx_network/primitives/id" @@ -105,6 +106,71 @@ func NewManager(client *api.Client, single *single.Manager) (*Manager, error) { return m, nil } +// NewManagerFromBackup builds a new user discover manager from a backup. +// It will construct a manager that is already registered and restore +// already registered facts into store. +func NewManagerFromBackup(client *api.Client, single *single.Manager, + email, phone fact.Fact) (*Manager, error) { + jww.INFO.Println("ud.NewManagerFromBackup()") + if client.NetworkFollowerStatus() != api.Running { + return nil, errors.New( + "cannot start UD Manager when network follower is not running.") + } + + registered := uint32(0) + + m := &Manager{ + client: client, + comms: client.GetComms(), + rng: client.GetRng(), + sw: client.GetSwitchboard(), + storage: client.GetStorage(), + net: client.GetNetworkInterface(), + single: single, + registered: ®istered, + } + + err := m.client.GetStorage().GetUd(). + BackUpMissingFacts(email, phone) + if err != nil { + return nil, errors.WithMessage(err, "Failed to restore UD store "+ + "from backup") + } + + // check that user discovery is available in the NDF + def := m.net.GetInstance().GetPartialNdf().Get() + + if def.UDB.Cert == "" { + return nil, errors.New("NDF does not have User Discovery information, " + + "is there network access?: Cert not present.") + } + + // Create the user discovery host object + hp := connect.GetDefaultHostParams() + // Client will not send KeepAlive packets + hp.KaClientOpts.Time = time.Duration(math.MaxInt64) + hp.MaxRetries = 3 + hp.SendTimeout = 3 * time.Second + hp.AuthEnabled = false + + m.myID = m.storage.User().GetCryptographicIdentity().GetReceptionID() + + // Get the commonly used data from storage + m.privKey = m.storage.GetUser().ReceptionRSA + + // Set as registered. Since it's from a backup, + // the client is already registered + if err = m.setRegistered(); err != nil { + return nil, errors.WithMessage(err, "failed to set client as "+ + "registered with user discovery.") + } + + // Store the pointer to the group locally for easy access + m.grp = m.storage.E2e().GetGroup() + + return m, nil +} + // SetAlternativeUserDiscovery sets the alternativeUd object within manager. // Once set, any user discovery operation will go through the alternative // user discovery service. @@ -150,6 +216,18 @@ func (m *Manager) UnsetAlternativeUserDiscovery() error { return nil } +// GetFacts returns a list of fact.Fact objects that exist within the +// Store's registeredFacts map. +func (m *Manager) GetFacts() []fact.Fact { + return m.storage.GetUd().GetFacts() +} + +// GetStringifiedFacts returns a list of stringified facts from the Store's +// registeredFacts map. +func (m *Manager) GetStringifiedFacts() []string { + return m.storage.GetUd().GetStringifiedFacts() +} + // getHost returns the current UD host for the UD ID found in the NDF. If the // host does not exist, then it is added and returned func (m *Manager) getHost() (*connect.Host, error) { diff --git a/ud/registered.go b/ud/registered.go index d81e615894e6928f5e7a529cf10c2ca288f21e04..c8ae1c8aaddf570f0b7b357f74f5d55b1b879dc7 100644 --- a/ud/registered.go +++ b/ud/registered.go @@ -32,7 +32,7 @@ func (m *Manager) IsRegistered() bool { return atomic.LoadUint32(m.registered) == 1 } -// IsRegistered returns if the client is registered with user discovery +// setRegistered sets the manager's state to registered. func (m *Manager) setRegistered() error { if !atomic.CompareAndSwapUint32(m.registered, 0, 1) { return errors.New("cannot register with User Discovery when " + diff --git a/ud/remove.go b/ud/remove.go index 67f6721773baed94402963fad95726dbe34e9365..85547373fc064da7e424640683af4cc74663ae76 100644 --- a/ud/remove.go +++ b/ud/remove.go @@ -61,9 +61,12 @@ func (m *Manager) removeFact(fact fact.Fact, rFC removeFactComms) error { // Send the message _, err = rFC.SendRemoveFact(host, &remFactMsg) + if err != nil { + return err + } - // Return the error - return err + // Remove from storage + return m.storage.GetUd().DeleteFact(fact) } type removeUserComms interface { diff --git a/ud/remove_test.go b/ud/remove_test.go index 5705a843a4a182b3ad959243f4b94092551803de..ecf0487d68b27047953d245e5ce2710f0ec6cc45 100644 --- a/ud/remove_test.go +++ b/ud/remove_test.go @@ -1,6 +1,7 @@ package ud import ( + "gitlab.com/elixxir/client/storage" "gitlab.com/elixxir/comms/client" pb "gitlab.com/elixxir/comms/mixmessages" "gitlab.com/elixxir/primitives/fact" @@ -39,6 +40,7 @@ func TestRemoveFact(t *testing.T) { net: newTestNetworkManager(t), privKey: cpk, registered: &isReg, + storage: storage.InitTestingSession(t), myID: &id.ID{}, } @@ -47,6 +49,16 @@ func TestRemoveFact(t *testing.T) { T: 2, } + // Set up storage for expected state + confirmId := "test" + if err = m.storage.GetUd().StoreUnconfirmedFact(confirmId, f); err != nil { + t.Fatalf("StoreUnconfirmedFact error: %v", err) + } + + if err = m.storage.GetUd().ConfirmFact(confirmId); err != nil { + t.Fatalf("ConfirmFact error: %v", err) + } + tRFC := testRFC{} err = m.removeFact(f, &tRFC) diff --git a/xxmutils/README.md b/xxmutils/README.md new file mode 100644 index 0000000000000000000000000000000000000000..a817417b4cd7297e090ff556388333b155fc761f --- /dev/null +++ b/xxmutils/README.md @@ -0,0 +1,7 @@ += XX Messenger Utility Functions + +xxDK users should not use these functions. These functions are used by +the mobile phone apps and are not intended to be part of the xxDK. They +should be treated as internal functions specific to the phone apps. + +These functions are subject to change without notice. diff --git a/xxmutils/restoreContacts.go b/xxmutils/restoreContacts.go new file mode 100644 index 0000000000000000000000000000000000000000..5074508c6d70f21057a0dffe3300035f66162b26 --- /dev/null +++ b/xxmutils/restoreContacts.go @@ -0,0 +1,353 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package xxmutils + +import ( + "encoding/json" + "errors" + "fmt" + "math" + "strings" + "sync" + "time" + + jww "github.com/spf13/jwalterweatherman" + + "gitlab.com/elixxir/client/api" + "gitlab.com/elixxir/client/interfaces" + "gitlab.com/elixxir/client/storage" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/client/ud" + "gitlab.com/elixxir/crypto/contact" + "gitlab.com/elixxir/primitives/fact" + "gitlab.com/xx_network/primitives/id" +) + +type LookupCallback func(c contact.Contact, myErr error) + +// RestoreContactsFromBackup takes as input the jason output of the +// `NewClientFromBackup` function, unmarshals it into IDs, looks up +// each ID in user discovery, and initiates a session reset request. +// This function will not return until every id in the list has been sent a +// request. It should be called again and again until it completes. +// xxDK users should not use this function. This function is used by +// the mobile phone apps and are not intended to be part of the xxDK. It +// should be treated as internal functions specific to the phone apps. +func RestoreContactsFromBackup(backupPartnerIDs []byte, client *api.Client, + udManager *ud.Manager, lookupCB LookupCallback, + updatesCb interfaces.RestoreContactsUpdater) ([]*id.ID, []*id.ID, + []error, error) { + + var restored, failed []*id.ID + var errs []error + + // Constants/control settings + numRoutines := 8 + maxChanSize := 10000 + restoreTimeout := time.Duration(30 * time.Second) + + update := func(numFound, numRestored, total int, err string) { + if updatesCb != nil { + updatesCb.RestoreContactsCallback(numFound, numRestored, + total, err) + } + } + + store := stateStore{ + apiStore: client.GetStorage(), + } + + // Unmarshal IDs and then check restore state + var idList []*id.ID + if err := json.Unmarshal(backupPartnerIDs, &idList); err != nil { + return nil, nil, nil, err + } + lookupIDs, resetContacts, restored := checkRestoreState(idList, store) + + jww.INFO.Printf("restoring %d backup partner IDs", len(lookupIDs)) + jww.DEBUG.Printf("backup partner IDs to restore: %+v", lookupIDs) + + // State variables, how many we have looked up successfully + // and how many we have already reset. + totalCnt := len(idList) + lookupCnt := len(resetContacts) + resetCnt := totalCnt - len(resetContacts) - len(lookupIDs) + + // Before we start, report initial state + update(lookupCnt, resetCnt, totalCnt, "") + + // Initialize channels + chanSize := int(math.Min(float64(maxChanSize), float64(len(idList)))) + // Jobs are processed via the following pipeline: + // lookupCh -> foundCh -> resetContactCh -> restoredCh + // foundCh and restoredCh are used to track progress + lookupCh := make(chan *id.ID, chanSize) + foundCh := make(chan *contact.Contact, chanSize) + resetContactCh := make(chan *contact.Contact, chanSize) + restoredCh := make(chan *contact.Contact, chanSize) + failCh := make(chan failure, chanSize) + + // Start routines for processing + lcWg := &sync.WaitGroup{} + lcWg.Add(numRoutines) + rsWg := &sync.WaitGroup{} + rsWg.Add(numRoutines) + for i := 0; i < numRoutines; i++ { + go LookupContacts(lookupCh, foundCh, failCh, udManager, lookupCB, + lcWg) + go ResetSessions(resetContactCh, restoredCh, failCh, *client, + rsWg) + } + + // Load channels based on previous state + go func() { + for i := range lookupIDs { + lookupCh <- lookupIDs[i] + } + }() + go func() { + for i := range resetContacts { + lookupCnt += 1 + resetContactCh <- resetContacts[i] + } + }() + + // Failure processing, done separately (in a single thread) + // because failures should not reset the timer + failWg := sync.WaitGroup{} + failWg.Add(1) + go func() { + defer failWg.Done() + for fail := range failCh { + failed = append(failed, fail.ID) + errs = append(errs, fail.Err) + } + }() + + // Event Processing + done := false + var err error = nil + for !done { + // NOTE: Timer is reset every loop + timeoutTimer := time.NewTimer(restoreTimeout) + select { + case <-timeoutTimer.C: + err = errors.New("restoring accounts timed out") + done = true + case c := <-foundCh: + store.set(c, contactFound) + lookupCnt += 1 + // NOTE: Prevent blocking by using routine here + go func() { resetContactCh <- c }() + case c := <-restoredCh: + store.set(c, contactRestored) + restored = append(restored, c.ID) + resetCnt += 1 + } + if resetCnt == totalCnt { + done = true + } + update(lookupCnt, resetCnt, totalCnt, "") + } + + // Cleanup + // lookupCh -> foundCh -> resetContactCh -> restoredCh + close(lookupCh) + // Now wait for subroutines to close before closing their output chans + lcWg.Wait() + // Close input to reset chan after lookup is done to avoid writes after + // close + close(foundCh) + close(resetContactCh) + rsWg.Wait() + // failCh is closed after exit of the threads to avoid writes after + // close + close(failCh) + close(restoredCh) + failWg.Wait() + + return restored, failed, errs, err +} + +// LookupContacts routine looks up contacts +// xxDK users should not use this function. This function is used by +// the mobile phone apps and are not intended to be part of the xxDK. It +// should be treated as internal functions specific to the phone apps. +func LookupContacts(in chan *id.ID, out chan *contact.Contact, + failCh chan failure, udManager *ud.Manager, extLookupCB LookupCallback, + wg *sync.WaitGroup) { + defer wg.Done() + // Start looking up contacts with user discovery and feed this + // contacts channel. + for lookupID := range in { + c, err := LookupContact(lookupID, udManager, extLookupCB) + if err == nil { + out <- c + continue + } + // If an error, figure out if I should report or retry + errStr := err.Error() + if strings.Contains(errStr, "failed to lookup ID") { + failCh <- failure{ID: lookupID, Err: err} + continue + } + jww.WARN.Printf("could not lookup %s: %v", lookupID, err) + } +} + +// ResetSessions routine reads the in channel, sends a reset session +// request, then marks it done by sending to the out channel. +// xxDK users should not use this function. This function is used by +// the mobile phone apps and are not intended to be part of the xxDK. It +// should be treated as internal functions specific to the phone apps. +func ResetSessions(in, out chan *contact.Contact, failCh chan failure, + client api.Client, wg *sync.WaitGroup) { + defer wg.Done() + me := client.GetUser().GetContact() + msg := "Account reset from backup" + for c := range in { + _, err := client.ResetSession(*c, me, msg) + if err == nil { + out <- c + continue + } + // If an error, figure out if I should report or retry + // Note: Always fail here for now. + jww.WARN.Printf("could not reset %s: %v", c.ID, err) + failCh <- failure{ID: c.ID, Err: err} + } +} + +// LookupContact lookups up a contact using the user discovery manager +// xxDK users should not use this function. This function is used by +// the mobile phone apps and are not intended to be part of the xxDK. It +// should be treated as internal functions specific to the phone apps. +func LookupContact(userID *id.ID, udManager *ud.Manager, + extLookupCB LookupCallback) (*contact.Contact, error) { + // This is a little wonky, but wait until we get called then + // set the result to the contact objects details if there is + // no error + waiter := sync.Mutex{} + var result *contact.Contact + var err error + lookupCB := func(c contact.Contact, myErr error) { + if myErr == nil { + newOwnership := make([]byte, len(c.OwnershipProof)) + copy(newOwnership, c.OwnershipProof) + newFacts, _, _ := fact.UnstringifyFactList( + c.Facts.Stringify()) + result = &contact.Contact{ + ID: c.ID.DeepCopy(), + DhPubKey: c.DhPubKey.DeepCopy(), + OwnershipProof: newOwnership, + Facts: newFacts, + } + } else { + err = myErr + result = nil + } + waiter.Unlock() + extLookupCB(c, myErr) + } + // Take lock once to make sure I will wait + waiter.Lock() + + // in MS, so 90 seconds + timeout := time.Duration(90 * time.Second) + udManager.Lookup(userID, lookupCB, timeout) + + // Now force a wait for callback to exit + waiter.Lock() + defer waiter.Unlock() + + return result, err +} + +// restoreState is the internal state of a contact +type restoreState byte + +const ( + contactNotFound restoreState = iota + contactFound + contactRestored +) + +type failure struct { + ID *id.ID + Err error +} + +//// +// stateStore wraps a kv and stores contact state for the restoration +// TODO: Right now, it uses 1 contact-per-key approach, but it might make sense +// to wrap this in a mutex and load/store a whole list +//// +const stateStoreFmt = "restoreContactsFromBackup/v1/%s" + +type stateStore struct { + apiStore *storage.Session + // TODO: We could put a syncmap or something here instead of + // 1-key-per-id +} + +func (s stateStore) key(id *id.ID) string { + return fmt.Sprintf(stateStoreFmt, id) +} + +func (s stateStore) set(user *contact.Contact, state restoreState) error { + key := s.key(user.ID) + // First byte is state var, second is contact object + data := []byte{byte(state)} + data = append(data, user.Marshal()...) + val := &versioned.Object{ + Version: 0, + Timestamp: time.Now(), + Data: data, + } + return s.apiStore.Set(key, val) +} +func (s stateStore) get(id *id.ID) (restoreState, *contact.Contact, error) { + key := s.key(id) + val, err := s.apiStore.Get(key) + if err != nil { + return contactNotFound, nil, err + } + user, err := contact.Unmarshal(val.Data[1:]) + if err != nil { + return contactFound, nil, err + } + return restoreState(val.Data[0]), &user, nil +} + +// stateStore END + +func checkRestoreState(IDs []*id.ID, store stateStore) ([]*id.ID, + []*contact.Contact, []*id.ID) { + var idsToLookup []*id.ID + var contactsToReset []*contact.Contact + var contactsRestored []*id.ID + for i := range IDs { + id := IDs[i] + idState, user, err := store.get(id) + if err != nil { + // Ignore errors here since they always will result + // in a retry. + jww.WARN.Printf("Error on restore check for %s: %v", + id, err) + } + switch idState { + case contactNotFound: + idsToLookup = append(idsToLookup, id) + case contactFound: + contactsToReset = append(contactsToReset, user) + case contactRestored: + contactsRestored = append(contactsRestored, user.ID) + } + } + return idsToLookup, contactsToReset, contactsRestored +}