diff --git a/README.md b/README.md index 0d0938ebbe404dc1cefd87833031017d3d9c6564..8c29768b63c9fd9e3b119e6832f1931bdbf71b53 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,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. 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..c3ff5cd85229da9bcc54e0d06195b1f353dc3636 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 @@ -163,6 +167,51 @@ 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. +func NewClientFromBackup(ndfJSON, storageDir, sessionPassword, + backupPassphrase string, backupFileContents []byte) ([]*id.ID, error) { + + backUp := &backup.Backup{} + err := backUp.Decrypt(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, 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 +243,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 @@ -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() @@ -705,7 +764,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 +779,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, 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/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/auth/callback.go b/auth/callback.go index 7ae57d144bfb2a3c6e0b7b8cd02cb610d4694553..a437d834ccc7240cdcc5ee18ee85528c6fc99d8a 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) { @@ -153,6 +152,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 +293,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) - } + autoConfirm = true - // 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 - } - - 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 } } } @@ -317,12 +334,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) + 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.ResetCallback) + 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 +476,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 +489,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 diff --git a/auth/cmix.go b/auth/cmix.go new file mode 100644 index 0000000000000000000000000000000000000000..0f76dd7efc1f757613c0cd43897afb851f4bd93a --- /dev/null +++ b/auth/cmix.go @@ -0,0 +1,62 @@ +/////////////////////////////////////////////////////////////////////////////// +// 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) (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()) + + 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..ecc32ac304282ad123125b761916e3ea9af9e697 100644 --- a/auth/confirm.go +++ b/auth/confirm.go @@ -9,108 +9,125 @@ 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") } + // Cannot confirm already established channels + if _, err := m.storage.E2e().GetPartner(partner.ID); err == nil { + em := fmt.Sprintf("Cannot FonritmRequestAuth 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 +136,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 + m.backupTrigger("confirmed authenticated channel") - //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) - - //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,7 +156,7 @@ 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) + 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 @@ -186,3 +172,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..f267945be7934705f7046b948efc409c42f51d6d 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,10 +54,14 @@ 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 } @@ -58,14 +69,24 @@ func unmarshalBaseFormat(b []byte, pubkeySize int) (baseFormat, error) { if len(b) < pubkeySize { return baseFormat{}, errors.New("Received baseFormat too small") } + bfmt := buildBaseFormat(b, pubkeySize) + version := bfmt.GetVersion() + if version != requestFmtVersion { + return baseFormat{}, 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..64785b399abc261400a346fceb68bc4ee0fc59c0 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() diff --git a/auth/manager.go b/auth/manager.go index 20b341c3891212edf308ec9c7afcf43a5f1588f5..6be1c8bd1695af1ff5c5609ee6c58e4effea2b62 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) AddResetCallback(cb interfaces.ResetCallback) { + 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..75459786839b8fd6730b9bc58bb44cca67cbb7c5 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,79 @@ 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 { + 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: + // TODO: We've already received a request, so send a + // confirmation instead? return 0, errors.Errorf("Cannot send a request after " + "receiving a request") - } else if rqType == auth.Sent { + case auth.Sent: resend = true - } else { + default: return 0, errors.Errorf("Cannot send a request after "+ - " a stored request with unknown rqType: %d", rqType) + "a stored request with unknown rqType: %d", + rqType) } - } else if !strings.Contains(err.Error(), auth.NoRequest) { - return 0, errors.WithMessage(err, - "Cannot send a request after receiving unknown error "+ - "on requesting contact status") - } - - 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 +161,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) + 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..47bdd4cca85487d18971117d9bd27bf19f79385a --- /dev/null +++ b/backup/backup.go @@ -0,0 +1,283 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 ( + "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" + "sync" +) + +// Error messages. +const ( + // initializeBackup + errSavePassword = "failed to save password: %+v" + errSaveKeySaltParams = "failed to save key, salt, and params: %+v" + + // resumeBackup + errLoadPassword = "backup not initialized: load user password failed: %+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 +} + +// 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, + } + + // Save password to storage + err := savePassword(password, b.store.GetKV()) + if err != nil { + return nil, errors.Errorf(errSavePassword, err) + } + + // 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() + 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) + + 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 := loadPassword(store.GetKV()) + if err != nil { + return nil, errors.Errorf(errLoadPassword, err) + } + + b := &Backup{ + updateBackupCb: updateBackupCb, + client: c, + store: store, + backupContainer: backupContainer, + rng: rng, + } + + // 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 + go b.updateBackupCb(encryptedBackup) +} + +// 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 := deletePassword(b.store.GetKV()) + if err != nil { + return errors.Errorf(errDeletePassword, err) + } + + 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() + + return bu +} diff --git a/backup/backup_test.go b/backup/backup_test.go new file mode 100644 index 0000000000000000000000000000000000000000..fc80163e64f39fd19882fb563f7a586b1d14c2bf --- /dev/null +++ b/backup/backup_test.go @@ -0,0 +1,325 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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" + "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" + "reflect" + "strings" + "testing" + "time" +) + +// 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) + 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) + } + + // Check that the correct password is in storage + loadedPassword, err := loadPassword(b.store.GetKV()) + if err != nil { + t.Errorf("Failed to load password: %+v", err) + } + if expectedPassword != loadedPassword { + t.Errorf("Loaded invalid key.\nexpected: %q\nreceived: %q", + expectedPassword, loadedPassword) + } + + // Check that the key, salt, and params were saved to storage + key, salt, p, 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) + } + + // 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) + } + + // Check that the correct password is in storage + loadedPassword, err := loadPassword(b.store.GetKV()) + if err != nil { + t.Errorf("Failed to load password: %+v", err) + } + if expectedPassword != loadedPassword { + t.Errorf("Loaded invalid key.\nexpected: %q\nreceived: %q", + expectedPassword, loadedPassword) + } + + // 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 := strings.Split(errLoadPassword, "%")[0] + 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 } + b := newTestBackup("MySuperSecurePassword", cb, t) + + // Get password + password, err := loadPassword(b.store.GetKV()) + if err != nil { + t.Errorf("Failed to load password from storage: %+v", err) + } + + 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) + + 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) + + 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 password is deleted + password, err := loadPassword(b.store.GetKV()) + if err == nil || len(password) != 0 { + t.Errorf("Loaded password that should be deleted: %q", password) + } + + // 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.") + } +} + +// 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 + + 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() + + 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 +} diff --git a/backup/keyStorage.go b/backup/keyStorage.go new file mode 100644 index 0000000000000000000000000000000000000000..c4627396d6c4abb8be19c6d0d0d3c8d1859c57e4 --- /dev/null +++ b/backup/keyStorage.go @@ -0,0 +1,128 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 +} + +// savePassword saves the user's backup password to storage. +func savePassword(password string, kv *versioned.KV) error { + obj := &versioned.Object{ + Version: passwordStorageVersion, + Timestamp: netTime.Now(), + Data: []byte(password), + } + + return kv.Set(passwordStorageKey, passwordStorageVersion, obj) +} + +// loadPassword returns the user's backup password from storage. +func loadPassword(kv *versioned.KV) (string, error) { + obj, err := kv.Get(passwordStorageKey, passwordStorageVersion) + if err != nil { + return "", err + } + + return string(obj.Data), nil +} + +// deletePassword deletes the user's backup password from storage. +func deletePassword(kv *versioned.KV) error { + return kv.Delete(passwordStorageKey, passwordStorageVersion) +} diff --git a/backup/keyStorage_test.go b/backup/keyStorage_test.go new file mode 100644 index 0000000000000000000000000000000000000000..939ca2c632fbf8d5ce41af81b158be0a3be7d4aa --- /dev/null +++ b/backup/keyStorage_test.go @@ -0,0 +1,94 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 ( + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/ekv" + "gitlab.com/xx_network/primitives/netTime" + "testing" +) + +// Tests that savePassword saves the password to storage by loading it and +// comparing it to the original. +func Test_savePassword(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + expectedPassword := "MySuperSecurePassword" + + // Save the password + err := savePassword(expectedPassword, kv) + if err != nil { + t.Errorf("savePassword returned an error: %+v", err) + } + + // Attempt to load the password + obj, err := kv.Get(passwordStorageKey, passwordStorageVersion) + if err != nil { + t.Errorf("Failed to get password from storage: %+v", err) + } + + // Check that the password matches the original + if expectedPassword != string(obj.Data) { + t.Errorf("Loaded password does not match original."+ + "\nexpected: %q\nreceived: %q", expectedPassword, obj.Data) + } +} + +// Tests that loadPassword restores the original password saved to stage and +// compares it to the original. +func Test_loadPassword(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + expectedPassword := "MySuperSecurePassword" + + // Save the password + err := kv.Set(passwordStorageKey, passwordStorageVersion, &versioned.Object{ + Version: passwordStorageVersion, + Timestamp: netTime.Now(), + Data: []byte(expectedPassword), + }) + if err != nil { + t.Errorf("Failed to save password to storage: %+v", err) + } + + // Attempt to load the password + loadedPassword, err := loadPassword(kv) + if err != nil { + t.Errorf("loadPassword returned an error: %+v", err) + } + + // Check that the password matches the original + if expectedPassword != loadedPassword { + t.Errorf("Loaded password does not match original."+ + "\nexpected: %q\nreceived: %q", expectedPassword, loadedPassword) + } +} + +// Tests that deletePassword deletes the password from storage by trying to recover a +// deleted password. +func Test_deletePassword(t *testing.T) { + kv := versioned.NewKV(make(ekv.Memstore)) + expectedPassword := "MySuperSecurePassword" + + // Save the password + err := savePassword(expectedPassword, kv) + if err != nil { + t.Errorf("Failed to save password to storage: %+v", err) + } + + // Delete the password + err = deletePassword(kv) + if err != nil { + t.Errorf("deletePassword returned an error: %+v", err) + } + + // Attempt to load the password + obj, err := loadPassword(kv) + if err == nil || obj != "" { + t.Errorf("Loaded object from storage when it should be deleted: %+v", obj) + } +} diff --git a/bindings/backup.go b/bindings/backup.go new file mode 100644 index 0000000000000000000000000000000000000000..9621fdfe69a2117a3939550490ae25fc3d5562dd --- /dev/null +++ b/bindings/backup.go @@ -0,0 +1,67 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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() +} diff --git a/bindings/client.go b/bindings/client.go index 1996507cbc0a005ae20006763f6596fe1ca25834..e543b80428c9b6ac9a5439c0534142bd8a1d17f1 100644 --- a/bindings/client.go +++ b/bindings/client.go @@ -10,6 +10,7 @@ package bindings import ( "bytes" "encoding/csv" + "encoding/json" "errors" "fmt" jww "github.com/spf13/jwalterweatherman" @@ -81,6 +82,22 @@ func NewPrecannedClient(precannedID int, network, storageDir string, password [] 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. +func NewClientFromBackup(ndfJSON, storageDir, sessionPassword, + backupPassphrase string, backupFileContents []byte) ([]byte, error) { + backupPartnerIds, 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)) + } + + return json.Marshal(backupPartnerIds) +} + // 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. diff --git a/bindings/ud.go b/bindings/ud.go index f9a12eb39972f94743a833e9ec4e081139b5ac40..8c467c810a452847cce7989ed9224d55167a36e1 100644 --- a/bindings/ud.go +++ b/bindings/ud.go @@ -102,6 +102,37 @@ func (ud *UserDiscovery) RemoveUser(fStr string) error { return ud.ud.RemoveUser(f) } +//BackUpMissingFacts adds a registered fact to the Store object and saves +// it to storage. It can take in both an email or a phone number, passed into +// the function in that order. 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. +func (ud *UserDiscovery) BackUpMissingFacts(email, phone string) error { + var emailFact, phoneFact fact.Fact + var err error + if len(email) > 2 { + emailFact, err = fact.UnstringifyFact(email) + if err != nil { + return errors.WithMessagef(err, "Failed to parse malformed email fact: %s", email) + } + } + + if len(phone) > 2 { + phoneFact, err = fact.UnstringifyFact(phone) + if err != nil { + return errors.WithMessagef(err, "Failed to parse malformed phone fact: %s", phone) + } + } + + return ud.ud.BackUpMissingFacts(emailFact, phoneFact) +} + // SearchCallback returns the result of a search type SearchCallback interface { Callback(contacts *ContactList, error string) 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/root.go b/cmd/root.go index c9f094a1883304107ea6cbdf5cd7d0c51fc6a7a4..aa9a1c716608fc5eb87ecadd566a7dad5f592083 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 { @@ -428,7 +440,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,46 +516,6 @@ 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")) @@ -550,6 +527,8 @@ func createClient() *api.Client { precannedID := viper.GetUint("sendid") userIDprefix := viper.GetString("userid-prefix") protoUserPath := viper.GetString("protoUserPath") + backupPath := viper.GetString("backupIn") + backupPass := viper.GetString("backupPass") // create a new client if none exist if _, err := os.Stat(storeDir); os.IsNotExist(err) { @@ -572,6 +551,42 @@ func createClient() *api.Client { } else if userIDprefix != "" { err = api.NewVanityClient(string(ndfJSON), storeDir, []byte(pass), regCode, userIDprefix) + } else if backupPath != "" { + + b, backupFile := loadBackup(backupPath, 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) @@ -642,35 +657,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 +717,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 +773,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) @@ -1117,6 +1170,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..ba6445c30b8b3e837082890ce2da7f7a173ecd0a 100644 --- a/cmd/ud.go +++ b/cmd/ud.go @@ -9,6 +9,7 @@ package cmd import ( + "encoding/json" "fmt" "github.com/spf13/cobra" jww "github.com/spf13/jwalterweatherman" @@ -19,6 +20,8 @@ import ( "gitlab.com/elixxir/client/ud" "gitlab.com/elixxir/crypto/contact" "gitlab.com/elixxir/primitives/fact" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/utils" "time" ) @@ -158,6 +161,44 @@ 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) + } + + var idList []*id.ID + err = json.Unmarshal(idListFile, &idList) + if err != nil { + fmt.Printf("BATCHADD: Couldn't umarshal id list: %s\n", + err.Error()) + jww.FATAL.Panicf("BATCHADD: Couldn't read file: %+v", err) + } + + jww.INFO.Printf("BATCHADD: %d IDs: %v", len(idList), idList) + + cb := func(newContact contact.Contact, err error) { + if err != nil { + jww.WARN.Printf("BATCHADD: %+v", err) + return + } + + jww.INFO.Printf("BATCHADD: contact %s", newContact) + + addAuthenticatedChannel(client, newContact.ID, newContact) + } + + userDiscoveryMgr.BatchLookup(idList, cb, 90*time.Second) + + for _, uid := range idList { + for client.HasAuthenticatedChannel(uid) == false { + 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 +311,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/go.mod b/go.mod index 4a0f6367b73509140ecc2885b5e5b7bd87396bd4..84912eac5c80f77824defbc906025bd1f23584e3 100644 --- a/go.mod +++ b/go.mod @@ -12,8 +12,8 @@ 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.20220214214811-4a1bd320aa45 + gitlab.com/elixxir/crypto v0.0.7-0.20220214184248-2b6499b3c024 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 diff --git a/go.sum b/go.sum index 5cecc075ac7209f3664ffc22c85e679008a5edfb..e3960a188284efb90a562f1b079b3b7e96cce39d 100644 --- a/go.sum +++ b/go.sum @@ -276,8 +276,11 @@ gitlab.com/elixxir/comms v0.0.4-0.20220222212253-41a1a0067369 h1:Bk5T3unbs3cjEqz gitlab.com/elixxir/comms v0.0.4-0.20220222212253-41a1a0067369/go.mod h1:AligJKSltFDPe/rqE2EZBfWCMSrae0zUo7scsXoyMPE= 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.20220110170041-7e42f2e8b062/go.mod h1:qmW0OGPB21GcaGg1Jvt527/qUw7ke6W8DKCiYBfsx48= +gitlab.com/elixxir/crypto v0.0.7-0.20220211185439-4a6d9f41f8ab h1:UKtFrw8qyLQhwZoftn2926Cm02ZL5HQitTLGF4sQ+ys= +gitlab.com/elixxir/crypto v0.0.7-0.20220211185439-4a6d9f41f8ab/go.mod h1:WyLFCxOOgaCHElpH0Ha893tfjxg3HXYU7lSJz2M4JUE= +gitlab.com/elixxir/crypto v0.0.7-0.20220214184248-2b6499b3c024 h1:uF6X1josZiOWt6v4Ym2b4IHN1v+UF7xbSxebiV3NFlk= +gitlab.com/elixxir/crypto v0.0.7-0.20220214184248-2b6499b3c024/go.mod h1:WyLFCxOOgaCHElpH0Ha893tfjxg3HXYU7lSJz2M4JUE= 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= diff --git a/interfaces/auth.go b/interfaces/auth.go index 4ce22fba789b45a5616a640f0409a054f6a05ecb..28443cf800727ad20cd9ff9586dcb5945e0a87e5 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 ResetCallback 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 renegotiations + AddResetCallback(cb ResetCallback) //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/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/network/node/register.go b/network/node/register.go index 4336575dee390d0b948b914595d9bb15cfafd5bd..1576fe89a027cbba1a29ef374a84c809848e0d57 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" @@ -154,7 +155,20 @@ 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 +184,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(), } @@ -262,12 +276,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/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..6a538f51825098f8602a741ea2617825eab4245a --- /dev/null +++ b/storage/auth/previousNegotiations.go @@ -0,0 +1,276 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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" +) + +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) +} + +// loadPreviousNegotiations loads the list of previousNegotiations partners from +// storage. +func (s *Store) loadPreviousNegotiations() (map[id.ID]struct{}, error) { + obj, err := s.kv.Get(negotiationPartnersKey, negotiationPartnersVersion) + if err != 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..94351aac983f8b58439177a927c05bc9df6ee616 --- /dev/null +++ b/storage/auth/previousNegotiations_test.go @@ -0,0 +1,431 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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.loadPreviousNegotiations() + if err != nil { + t.Errorf("loadPreviousNegotiations 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.loadPreviousNegotiations. +func TestStore_savePreviousNegotiations_loadPreviousNegotiations(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.loadPreviousNegotiations() + if err != nil { + t.Errorf("loadPreviousNegotiations 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 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..83a306d9bbb50f43cf15b4091c8b275feff414d1 100644 --- a/storage/auth/store.go +++ b/storage/auth/store.go @@ -31,11 +31,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 +44,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 +61,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 +80,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 +150,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.loadPreviousNegotiations() + if err != nil { + return nil, errors.Errorf("failed to load list of previouse "+ + "negotation partner IDs: %+v", err) + } + return s, nil } @@ -429,6 +445,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 } diff --git a/storage/auth/store_test.go b/storage/auth/store_test.go index f1f10414836eb6c37596593322d112dbcfcc9394..1fc808235b9d48c1c9b59027a32de86feabd535b 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 { diff --git a/storage/cmix/store.go b/storage/cmix/store.go index 32b7d506c7de63b4fa901773ff6ba5551747d293..641261c9d85fb0f2718058c420ecb797cd00aaea 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" @@ -25,15 +24,11 @@ const prefix = "cmix" const currentStoreVersion = 0 const ( storeKey = "KeyStore" - pubKeyKey = "DhPubKey" - privKeyKey = "DhPrivKey" grpKey = "GroupKey" ) type Store struct { nodes map[id.ID]*key - dhPrivateKey *cyclic.Int - dhPublicKey *cyclic.Int validUntil uint64 keyId []byte grp *cyclic.Group @@ -42,32 +37,17 @@ type Store struct { } // 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") - } - - 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..1aea1efe8ddff38327d44fbfc84a07a1f32db458 100644 --- a/storage/e2e/store.go +++ b/storage/e2e/store.go @@ -246,6 +246,21 @@ 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 { + partnerIds = append(partnerIds, &partnerId) + } + + 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/session.go b/storage/session.go index d11e11fc6697edd4e52d3550c6de2d6677a37bd1..1fc0aec0e43ffa46d95ce07db69936e381a4e0db 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.LoadStore(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..9762bc068b915c4d5138226bf827157911508332 --- /dev/null +++ b/storage/ud/facts.go @@ -0,0 +1,217 @@ +/////////////////////////////////////////////////////////////////////////////// +// 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" +) + +// 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, empty Store object. +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() +} + +// 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() + + if isFactZero(email) && isFactZero(phone) { + return errors.New(backupMissingAllZeroesFactErr) + } + + 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..c1f5dfc5c965d8b1bece3ab7fa3b903dc21e2b33 --- /dev/null +++ b/storage/ud/facts_test.go @@ -0,0 +1,296 @@ +/////////////////////////////////////////////////////////////////////////////// +// 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" +) + +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) + } + +} + +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) + } +} + +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") + } + +} + +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) + } + +} + +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) + } + +} + +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) + } +} + +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..33f62113e8218b378df3e6afd2f72ea972a2a6e0 --- /dev/null +++ b/storage/ud/store.go @@ -0,0 +1,241 @@ +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" +) + +// 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 +///////////////////////////////////////////////////////////////// + +// LoadStore loads the Store object from the provided versioned.KV. +func LoadStore(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.load() + +} + +// 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) + + 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) + + 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..135348eed9bfa97db73931db19d81fc1a69b3cff --- /dev/null +++ b/storage/ud/store_test.go @@ -0,0 +1,103 @@ +package ud + +import ( + "bytes" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/ekv" + "reflect" + "testing" +) + +func TestLoadStore(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 := LoadStore(kv) + if err != nil { + t.Fatalf("LoadStore() produced an error: %v", err) + } + + if !reflect.DeepEqual(expectedStore, receivedStore) { + t.Errorf("LoadStore() 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/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..4306743ff91bdf0dd6b2d36ab4e8064735cd75ee 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" @@ -150,6 +151,29 @@ func (m *Manager) UnsetAlternativeUserDiscovery() error { return nil } +// 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 nil, however both is considered +// an error. It checks for the proper fact type for the associated fact. +// 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 (m *Manager) BackUpMissingFacts(email, phone fact.Fact) error { + return m.storage.GetUd().BackUpMissingFacts(email, phone) +} + +// 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/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)