/////////////////////////////////////////////////////////////////////////////// // Copyright © 2020 xx network SEZC // // // // Use of this source code is governed by a license that can be found in the // // LICENSE file // /////////////////////////////////////////////////////////////////////////////// package api import ( "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/auth" "gitlab.com/elixxir/client/interfaces" "gitlab.com/elixxir/client/interfaces/params" "gitlab.com/elixxir/client/interfaces/user" "gitlab.com/elixxir/client/keyExchange" "gitlab.com/elixxir/client/network" "gitlab.com/elixxir/client/registration" "gitlab.com/elixxir/client/stoppable" "gitlab.com/elixxir/client/storage" "gitlab.com/elixxir/client/switchboard" "gitlab.com/elixxir/comms/client" "gitlab.com/elixxir/crypto/cyclic" "gitlab.com/elixxir/crypto/fastRNG" "gitlab.com/elixxir/primitives/version" "gitlab.com/xx_network/comms/connect" "gitlab.com/xx_network/crypto/csprng" "gitlab.com/xx_network/crypto/large" "gitlab.com/xx_network/crypto/signature/rsa" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/ndf" "gitlab.com/xx_network/primitives/region" "math" "time" ) const followerStoppableName = "client" type Client struct { //generic RNG for client rng *fastRNG.StreamGenerator // the storage session securely stores data to disk and memoizes as is // appropriate storage *storage.Session //the switchboard is used for inter-process signaling about received messages switchboard *switchboard.Switchboard //object used for communications comms *client.Comms // Network parameters parameters params.Network // note that the manager has a pointer to the context in many cases, but // this interface allows it to be mocked for easy testing without the // loop network interfaces.NetworkManager //object used to register and communicate with permissioning permissioning *registration.Registration //object containing auth interactions auth *auth.Manager //services system to track running threads followerServices *services clientErrorChannel chan interfaces.ClientError } // NewClient creates client storage, generates keys, connects, and registers // with the network. Note that this does not register a username/identity, but // merely creates a new cryptographic identity for adding such information // at a later date. func NewClient(ndfJSON, storageDir string, password []byte, registrationCode string) error { jww.INFO.Printf("NewClient(dir: %s)", storageDir) // Use fastRNG for RNG ops (AES fortuna based RNG using system RNG) rngStreamGen := fastRNG.NewStreamGenerator(12, 3, csprng.NewSystemRNG) // Parse the NDF def, err := parseNDF(ndfJSON) if err != nil { return err } cmixGrp, e2eGrp := decodeGroups(def) start := time.Now() protoUser := createNewUser(rngStreamGen, cmixGrp, e2eGrp) jww.DEBUG.Printf("User generation took: %s", time.Now().Sub(start)) err = checkVersionAndSetupStorage(def, storageDir, password, protoUser, cmixGrp, e2eGrp, rngStreamGen, false, registrationCode) if err != nil { return err } //TODO: close the session return nil } // NewPrecannedClient creates an insecure user with predetermined keys with nodes // It creates client storage, generates keys, connects, and registers // with the network. Note that this does not register a username/identity, but // merely creates a new cryptographic identity for adding such information // at a later date. func NewPrecannedClient(precannedID uint, defJSON, storageDir string, password []byte) error { jww.INFO.Printf("NewPrecannedClient()") // Use fastRNG for RNG ops (AES fortuna based RNG using system RNG) rngStreamGen := fastRNG.NewStreamGenerator(12, 3, csprng.NewSystemRNG) rngStream := rngStreamGen.GetStream() // Parse the NDF def, err := parseNDF(defJSON) if err != nil { return err } cmixGrp, e2eGrp := decodeGroups(def) protoUser := createPrecannedUser(precannedID, rngStream, cmixGrp, e2eGrp) err = checkVersionAndSetupStorage(def, storageDir, password, protoUser, cmixGrp, e2eGrp, rngStreamGen, true, "") if err != nil { return err } //TODO: close the session return nil } // NewVanityClient creates a user with a receptionID that starts with the supplied prefix // It creates client storage, generates keys, connects, and registers // with the network. Note that this does not register a username/identity, but // merely creates a new cryptographic identity for adding such information // at a later date. func NewVanityClient(ndfJSON, storageDir string, password []byte, registrationCode string, userIdPrefix string) error { jww.INFO.Printf("NewVanityClient()") // Use fastRNG for RNG ops (AES fortuna based RNG using system RNG) rngStreamGen := fastRNG.NewStreamGenerator(12, 3, csprng.NewSystemRNG) rngStream := rngStreamGen.GetStream() // Parse the NDF def, err := parseNDF(ndfJSON) if err != nil { return err } cmixGrp, e2eGrp := decodeGroups(def) protoUser := createNewVanityUser(rngStream, cmixGrp, e2eGrp, userIdPrefix) err = checkVersionAndSetupStorage(def, storageDir, password, protoUser, cmixGrp, e2eGrp, rngStreamGen, false, registrationCode) if err != nil { return err } //TODO: close the session return 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()") // Use fastRNG for RNG ops (AES fortuna based RNG using system RNG) rngStreamGen := fastRNG.NewStreamGenerator(12, 3, csprng.NewSystemRNG) // Get current client version currentVersion, err := version.ParseVersion(SEMVER) if err != nil { return nil, errors.WithMessage(err, "Could not parse version string.") } // Load Storage passwordStr := string(password) storageSess, err := storage.Load(storageDir, passwordStr, currentVersion, rngStreamGen) if err != nil { return nil, err } // Set up a new context c := &Client{ storage: storageSess, switchboard: switchboard.New(), rng: rngStreamGen, comms: nil, network: nil, followerServices: newServices(), parameters: parameters, clientErrorChannel: make(chan interfaces.ClientError, 1000), } // Add all processes to the followerServices err = c.registerFollower() if err != nil { return nil, err } return c, nil } // Login initializes a client object from existing storage. func Login(storageDir string, password []byte, parameters params.Network) (*Client, error) { jww.INFO.Printf("Login()") //Open the client c, err := OpenClient(storageDir, password, parameters) if err != nil { return nil, err } u := c.storage.GetUser() jww.INFO.Printf("Client Logged in: \n\tTransmisstionID: %s "+ "\n\tReceptionID: %s", u.TransmissionID, u.ReceptionID) // initialize comms err = c.initComms() if err != nil { return nil, err } //get the NDF to pass into registration and the network manager def := c.storage.GetNDF() //initialize registration if def.Registration.Address != "" { err = c.initPermissioning(def) if err != nil { return nil, err } } else { jww.WARN.Printf("Registration with permissioning skipped due to " + "blank permissioning address. Client will not be able to register " + "or track network.") } if def.Notification.Address != "" { hp := connect.GetDefaultHostParams() // Client will not send KeepAlive packets hp.KaClientOpts.Time = time.Duration(math.MaxInt64) hp.AuthEnabled = false hp.MaxRetries = 5 _, err = c.comms.AddHost(&id.NotificationBot, def.Notification.Address, []byte(def.Notification.TlsCertificate), hp) if err != nil { jww.WARN.Printf("Failed adding host for notifications: %+v", err) } } // Initialize network and link it to context c.network, err = network.NewManager(c.storage, c.switchboard, c.rng, c.comms, parameters, def) if err != nil { return nil, err } // initialize the auth tracker c.auth = auth.NewManager(c.switchboard, c.storage, c.network) return c, nil } // LoginWithNewBaseNDF_UNSAFE initializes a client object from existing storage // while replacing the base NDF. This is designed for some specific deployment // procedures and is generally unsafe. func LoginWithNewBaseNDF_UNSAFE(storageDir string, password []byte, newBaseNdf string, parameters params.Network) (*Client, error) { jww.INFO.Printf("LoginWithNewBaseNDF_UNSAFE()") // Parse the NDF def, err := parseNDF(newBaseNdf) if err != nil { return nil, err } //Open the client c, err := OpenClient(storageDir, password, parameters) if err != nil { return nil, err } //initialize comms err = c.initComms() if err != nil { return nil, err } //store the updated base NDF c.storage.SetNDF(def) //initialize registration if def.Registration.Address != "" { err = c.initPermissioning(def) if err != nil { return nil, err } } else { jww.WARN.Printf("Registration with permissioning skipped due to " + "blank permissionign address. Client will not be able to register " + "or track network.") } // Initialize network and link it to context c.network, err = network.NewManager(c.storage, c.switchboard, c.rng, c.comms, parameters, def) if err != nil { return nil, err } // initialize the auth tracker c.auth = auth.NewManager(c.switchboard, c.storage, c.network) return c, nil } func (c *Client) initComms() error { var err error //get the user from session u := c.storage.User() cryptoUser := u.GetCryptographicIdentity() //start comms c.comms, err = client.NewClientComms(cryptoUser.GetTransmissionID(), rsa.CreatePublicKeyPem(cryptoUser.GetTransmissionRSA().GetPublic()), rsa.CreatePrivateKeyPem(cryptoUser.GetTransmissionRSA()), cryptoUser.GetTransmissionSalt()) if err != nil { return errors.WithMessage(err, "failed to load client") } return nil } func (c *Client) initPermissioning(def *ndf.NetworkDefinition) error { var err error //initialize registration c.permissioning, err = registration.Init(c.comms, def) if err != nil { return errors.WithMessage(err, "failed to init "+ "permissioning handler") } //register with registration if necessary if c.storage.GetRegistrationStatus() == storage.KeyGenComplete { jww.INFO.Printf("Client has not registered yet, attempting registration") err = c.registerWithPermissioning() if err != nil { jww.ERROR.Printf("Client has failed registration: %s", err) return errors.WithMessage(err, "failed to load client") } jww.INFO.Printf("Client successfully registered with the network") } return nil } // registerFollower adds the follower processes to the client's follower service list. // This should only ever be called once func (c *Client) registerFollower() error { //build the error callback cer := func(source, message, trace string) { select { case c.clientErrorChannel <- interfaces.ClientError{ Source: source, Message: message, Trace: trace, }: default: jww.WARN.Printf("Failed to notify about ClientError from %s: %s", source, message) } } //register the core follower service err := c.followerServices.add(func() (stoppable.Stoppable, error) { return c.network.Follow(cer) }) if err != nil { return errors.WithMessage(err, "Failed to start following "+ "the network") } //register the incremental key upgrade service err = c.followerServices.add(c.auth.StartProcesses) if err != nil { return errors.WithMessage(err, "Failed to start following "+ "the network") } //register the key exchange service keyXchange := func() (stoppable.Stoppable, error) { return keyExchange.Start(c.switchboard, c.storage, c.network, c.parameters.Rekey) } err = c.followerServices.add(keyXchange) return nil } // ----- Client Functions ----- // GetErrorsChannel returns a channel which passess errors from the // long running threads controlled by StartNetworkFollower and StopNetworkFollower func (c *Client) GetErrorsChannel() <-chan interfaces.ClientError { return c.clientErrorChannel } // StartNetworkFollower kicks off the tracking of the network. It starts // long running network client threads and returns an object for checking // state and stopping those threads. // Call this when returning from sleep and close when going back to // sleep. // These threads may become a significant drain on battery when offline, ensure // they are stopped if there is no internet access // Threads Started: // - Network Follower (/network/follow.go) // tracks the network events and hands them off to workers for handling // - Historical Round Retrieval (/network/rounds/historical.go) // Retrieves data about rounds which are too old to be stored by the client // - Message Retrieval Worker Group (/network/rounds/retrieve.go) // Requests all messages in a given round from the gateway of the last node // - Message Handling Worker Group (/network/message/handle.go) // Decrypts and partitions messages when signals via the Switchboard // - Health Tracker (/network/health) // Via the network instance tracks the state of the network // - Garbled Messages (/network/message/garbled.go) // Can be signaled to check all recent messages which could be be decoded // Uses a message store on disk for persistence // - Critical Messages (/network/message/critical.go) // Ensures all protocol layer mandatory messages are sent // Uses a message store on disk for persistence // - KeyExchange Trigger (/keyExchange/trigger.go) // Responds to sent rekeys and executes them // - KeyExchange Confirm (/keyExchange/confirm.go) // Responds to confirmations of successful rekey operations // - Auth Callback (/auth/callback.go) // Handles both auth confirm and requests func (c *Client) StartNetworkFollower(timeout time.Duration) error { u := c.GetUser() jww.INFO.Printf("StartNetworkFollower() \n\tTransmisstionID: %s "+ "\n\tReceptionID: %s", u.TransmissionID, u.ReceptionID) return c.followerServices.start(timeout) } // StopNetworkFollower stops the network follower if it is running. // It returns errors if the Follower is in the wrong state to stop or if it // fails to stop it. // if the network follower is running and this fails, the client object will // most likely be in an unrecoverable state and need to be trashed. func (c *Client) StopNetworkFollower() error { jww.INFO.Printf("StopNetworkFollower()") return c.followerServices.stop() } // NetworkFollowerStatus Gets the state of the network follower. Returns: // Stopped - 0 // Starting - 1000 // Running - 2000 // Stopping - 3000 func (c *Client) NetworkFollowerStatus() Status { jww.INFO.Printf("NetworkFollowerStatus()") return c.followerServices.status() } // Returns the health tracker for registration and polling func (c *Client) GetHealth() interfaces.HealthTracker { jww.INFO.Printf("GetHealth()") return c.network.GetHealthTracker() } // Returns the switchboard for Registration func (c *Client) GetSwitchboard() interfaces.Switchboard { jww.INFO.Printf("GetSwitchboard()") return c.switchboard } // RegisterRoundEventsCb registers a callback for round // events. func (c *Client) GetRoundEvents() interfaces.RoundEvents { jww.INFO.Printf("GetRoundEvents()") jww.WARN.Printf("GetRoundEvents does not handle Client Errors edge case!") return c.network.GetInstance().GetRoundEvents() } // AddService adds a service ot be controlled by the client thread control, // these will be started and stopped with the network follower func (c *Client) AddService(sp Service) error { return c.followerServices.add(sp) } // GetUser returns the current user Identity for this client. This // can be serialized into a byte stream for out-of-band sharing. func (c *Client) GetUser() user.User { jww.INFO.Printf("GetUser()") return c.storage.GetUser() } // GetComms returns the client comms object func (c *Client) GetComms() *client.Comms { return c.comms } // GetRng returns the client rng object func (c *Client) GetRng() *fastRNG.StreamGenerator { return c.rng } // GetStorage returns the client storage object func (c *Client) GetStorage() *storage.Session { return c.storage } // GetNetworkInterface returns the client Network Interface func (c *Client) GetNetworkInterface() interfaces.NetworkManager { return c.network } // GetNodeRegistrationStatus gets the current state of node registration. It // returns the the total number of nodes in the NDF and the number of those // which are currently registers with. An error is returned if the network is // not healthy. func (c *Client) GetNodeRegistrationStatus() (int, int, error) { // Return an error if the network is not healthy if !c.GetHealth().IsHealthy() { return 0, 0, errors.New("Cannot get number of node registrations when " + "network is not healthy") } nodes := c.GetNetworkInterface().GetInstance().GetPartialNdf().Get().Nodes cmixStore := c.storage.Cmix() var numRegistered int for i, n := range nodes { nid, err := id.Unmarshal(n.ID) if err != nil { return 0, 0, errors.Errorf("Failed to unmarshal node ID %v "+ "(#%d): %s", n.ID, i, err.Error()) } if cmixStore.Has(nid) { numRegistered++ } } // Get the number of in progress node registrations return numRegistered, len(nodes), nil } // 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) if err := c.storage.E2e().DeletePartner(partnerId); err != nil { return err } if err := c.storage.Auth().Delete(partnerId); err != nil { return err } c.storage.Conversations().Delete(partnerId) return nil } // SetProxiedBins updates the host pool filter that filters out gateways that // are not in one of the specified bins. func (c *Client) SetProxiedBins(binStrings []string) error { // Convert each region string into a region.GeoBin and place in a map for // easy lookup bins := make(map[region.GeoBin]bool, len(binStrings)) for i, binStr := range binStrings { bin, err := region.GetRegion(binStr) if err != nil { return errors.Errorf("failed to parse geographic bin #%d: %+v", i, err) } bins[bin] = true } // Create filter func f := func(m map[id.ID]int, netDef *ndf.NetworkDefinition) map[id.ID]int { prunedList := make(map[id.ID]int, len(m)) for gwID, i := range m { if bins[netDef.Gateways[i].Bin] { prunedList[gwID] = i } } return prunedList } c.network.SetPoolFilter(f) return nil } // GetPreferredBins returns the geographic bin or bins that the provided two // character country code is a part of. func (c *Client) GetPreferredBins(countryCode string) ([]string, error) { // Get the bin that the country is in bin, exists := region.GetCountryBin(countryCode) if !exists { return nil, errors.Errorf("failed to find geographic bin for country %q", countryCode) } // Add bin to list of geographic bins bins := []string{bin.String()} // Add additional bins in special cases switch bin { case region.Africa: bins = append(bins, region.WesternEurope.String()) case region.MiddleEast: bins = append(bins, region.EasternEurope.String()) } return bins, nil } // ----- Utility Functions ----- // parseNDF parses the initial ndf string for the client. do not check the // signature, it is deprecated. func parseNDF(ndfString string) (*ndf.NetworkDefinition, error) { if ndfString == "" { return nil, errors.New("ndf file empty") } netDef, err := ndf.Unmarshal([]byte(ndfString)) if err != nil { return nil, err } return netDef, nil } // decodeGroups returns the e2e and cmix groups from the ndf func decodeGroups(ndf *ndf.NetworkDefinition) (cmixGrp, e2eGrp *cyclic.Group) { largeIntBits := 16 //Generate the cmix group cmixGrp = cyclic.NewGroup( large.NewIntFromString(ndf.CMIX.Prime, largeIntBits), large.NewIntFromString(ndf.CMIX.Generator, largeIntBits)) //Generate the e2e group e2eGrp = cyclic.NewGroup( large.NewIntFromString(ndf.E2E.Prime, largeIntBits), large.NewIntFromString(ndf.E2E.Generator, largeIntBits)) return cmixGrp, e2eGrp } // checkVersionAndSetupStorage is common code shared by NewClient, NewPrecannedClient and NewVanityClient // it checks client version and creates a new storage for user data func checkVersionAndSetupStorage(def *ndf.NetworkDefinition, storageDir string, password []byte, protoUser user.User, cmixGrp, e2eGrp *cyclic.Group, rngStreamGen *fastRNG.StreamGenerator, isPrecanned bool, registrationCode string) error { // Get current client version currentVersion, err := version.ParseVersion(SEMVER) if err != nil { return errors.WithMessage(err, "Could not parse version string.") } // Create Storage passwordStr := string(password) storageSess, err := storage.New(storageDir, passwordStr, protoUser, currentVersion, cmixGrp, e2eGrp, rngStreamGen) if err != nil { return err } // Save NDF to be used in the future storageSess.SetNDF(def) if !isPrecanned { //store the registration code for later use storageSess.SetRegCode(registrationCode) //move the registration state to keys generated err = storageSess.ForwardRegistrationStatus(storage.KeyGenComplete) } else { //move the registration state to indicate registered with registration err = storageSess.ForwardRegistrationStatus(storage.PermissioningComplete) } if err != nil { return errors.WithMessage(err, "Failed to denote state "+ "change in session") } return nil }