diff --git a/api/client.go b/api/client.go index e70241ec757547aa7c3c81291cad52dd70aec947..ce24670a9d5057f118c32445c3637e7474098bf0 100644 --- a/api/client.go +++ b/api/client.go @@ -84,33 +84,12 @@ func NewClient(ndfJSON, storageDir string, password []byte, registrationCode str protoUser := createNewUser(rngStream, cmixGrp, e2eGrp) - // 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) + err = checkVersionAndSetupStorage(def, storageDir, password, protoUser, + cmixGrp, e2eGrp, rngStreamGen, false, registrationCode) if err != nil { return err } - // Save NDF to be used in the future - storageSess.SetBaseNDF(def) - - //store the registration code for later use - storageSess.SetRegCode(registrationCode) - - //move the registration state to keys generated - err = storageSess.ForwardRegistrationStatus(storage.KeyGenComplete) - if err != nil { - return errors.WithMessage(err, "Failed to denote state "+ - "change in session") - } - //TODO: close the session return nil } @@ -135,29 +114,39 @@ func NewPrecannedClient(precannedID uint, defJSON, storageDir string, password [ protoUser := createPrecannedUser(precannedID, rngStream, cmixGrp, e2eGrp) - // Get current client version - currentVersion, err := version.ParseVersion(SEMVER) + err = checkVersionAndSetupStorage(def, storageDir, password, protoUser, + cmixGrp, e2eGrp, rngStreamGen, true, "") if err != nil { - return errors.WithMessage(err, "Could not parse version string.") + return err } + //TODO: close the session + return nil +} - // Create Storage - passwordStr := string(password) - storageSess, err := storage.New(storageDir, passwordStr, protoUser, - currentVersion, cmixGrp, e2eGrp, rngStreamGen) +// 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) - // Save NDF to be used in the future - storageSess.SetBaseNDF(def) + protoUser := createNewVanityUser(rngStream, cmixGrp, e2eGrp, userIdPrefix) - //move the registration state to indicate registered with permissioning - err = storageSess.ForwardRegistrationStatus( - storage.PermissioningComplete) + err = checkVersionAndSetupStorage(def, storageDir, password, protoUser, + cmixGrp, e2eGrp, rngStreamGen, false, registrationCode) if err != nil { - return errors.WithMessage(err, "Failed to denote state "+ - "change in session") + return err } //TODO: close the session @@ -583,3 +572,43 @@ func decodeGroups(ndf *ndf.NetworkDefinition) (cmixGrp, e2eGrp *cyclic.Group) { 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.SetBaseNDF(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 permissioning + err = storageSess.ForwardRegistrationStatus(storage.PermissioningComplete) + } + + if err != nil { + return errors.WithMessage(err, "Failed to denote state "+ + "change in session") + } + + return nil +} diff --git a/api/user.go b/api/user.go index 892d84c63c8fb0e29001774c6c645f351b3d00b8..48d7f992dfa32b48a3905752c55459ddc710e701 100644 --- a/api/user.go +++ b/api/user.go @@ -17,6 +17,10 @@ import ( "gitlab.com/xx_network/crypto/xx" "gitlab.com/xx_network/primitives/id" "math/rand" + "regexp" + "runtime" + "strings" + "sync" ) const ( @@ -134,3 +138,126 @@ func createPrecannedUser(precannedID uint, rng csprng.Source, cmix, e2e *cyclic. ReceptionRSA: rsaKey, } } + +// 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 + // decent security -- cite this. Why valid for BOTH e2e and cmix? + e2eKeyBytes, err := csprng.GenerateInGroup(e2e.GetPBytes(), 256, rng) + if err != nil { + jww.FATAL.Panicf(err.Error()) + } + + // RSA Keygen (4096 bit defaults) + transmissionRsaKey, err := rsa.GenerateKey(rng, rsa.DefaultRSABitLen) + if err != nil { + jww.FATAL.Panicf(err.Error()) + } + + // Salt, UID, etc gen + transmissionSalt := make([]byte, SaltSize) + n, err := csprng.NewSystemRNG().Read(transmissionSalt) + if err != nil { + jww.FATAL.Panicf(err.Error()) + } + if n != SaltSize { + jww.FATAL.Panicf("transmissionSalt size too small: %d", n) + } + transmissionID, err := xx.NewID(transmissionRsaKey.GetPublic(), transmissionSalt, id.User) + if err != nil { + jww.FATAL.Panicf(err.Error()) + } + + receptionRsaKey, err := rsa.GenerateKey(rng, rsa.DefaultRSABitLen) + if err != nil { + jww.FATAL.Panicf(err.Error()) + } + + var mu sync.Mutex // just in case more than one go routine tries to access receptionSalt and receptionID + done := make(chan struct{}) + found:= make(chan bool) + wg:= &sync.WaitGroup{} + cores := runtime.NumCPU() + + var receptionSalt []byte + var receptionID *id.ID + + pref := prefix + ignoreCase := false + // check if case-insensitivity is enabled + if strings.HasPrefix(prefix, "(?i)") { + pref = strings.ToLower(pref[4:]) + ignoreCase = true + } + // Check if prefix contains valid Base64 characters + match, _ := regexp.MatchString("^[A-Za-z0-9+/]+$", pref) + if match == false { + jww.FATAL.Panicf("Prefix contains non-Base64 characters") + } + jww.INFO.Printf("Vanity userID generation started. Prefix: %s Ignore-Case: %v NumCPU: %d", pref, ignoreCase, cores) + for w := 0; w < cores; w++{ + wg.Add(1) + go func() { + rSalt := make([]byte, SaltSize) + for { + select { + case <- done: + defer wg.Done() + return + default: + n, err = csprng.NewSystemRNG().Read(rSalt) + if err != nil { + jww.FATAL.Panicf(err.Error()) + } + if n != SaltSize { + jww.FATAL.Panicf("receptionSalt size too small: %d", n) + } + rID, err := xx.NewID(receptionRsaKey.GetPublic(), rSalt, id.User) + if err != nil { + jww.FATAL.Panicf(err.Error()) + } + id := rID.String() + if ignoreCase { + id = strings.ToLower(id) + } + if strings.HasPrefix(id, pref) { + mu.Lock() + receptionID = rID + receptionSalt = rSalt + mu.Unlock() + found <- true + defer wg.Done() + return + } + } + } + }() + } + // wait for a solution then close the done channel to signal the workers to exit + <- found + close(done) + wg.Wait() + return user.User{ + TransmissionID: transmissionID.DeepCopy(), + TransmissionSalt: transmissionSalt, + TransmissionRSA: transmissionRsaKey, + ReceptionID: receptionID.DeepCopy(), + ReceptionSalt: receptionSalt, + ReceptionRSA: receptionRsaKey, + Precanned: false, + CmixDhPrivateKey: cmix.NewIntFromBytes(cMixKeyBytes), + E2eDhPrivateKey: e2e.NewIntFromBytes(e2eKeyBytes), + } +} diff --git a/cmd/init.go b/cmd/init.go index cf19521f066d1e9e878524a7ee75fd3471cf6b7c..b00f215475bfba0c513752b1d2fd01fd13cea5c8 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -11,6 +11,7 @@ package cmd import ( "fmt" "github.com/spf13/cobra" + "github.com/spf13/viper" jww "github.com/spf13/jwalterweatherman" ) @@ -29,5 +30,9 @@ var initCmd = &cobra.Command{ } func init() { + initCmd.Flags().StringP("userid-prefix", "", "", + "Desired prefix of userID to brute force when running init command. Prepend (?i) for case-insensitive. Only Base64 characters are valid.") + _ = viper.BindPFlag("userid-prefix", initCmd.Flags().Lookup("userid-prefix")) + rootCmd.AddCommand(initCmd) } diff --git a/cmd/root.go b/cmd/root.go index c3f915d186e3a6884bcc2263cf08f78b2051a670..5497f066e231c3e1252ef4584b0d9cf490c417e4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -306,7 +306,7 @@ func createClient() *api.Client { storeDir := viper.GetString("session") regCode := viper.GetString("regcode") precannedID := viper.GetUint("sendid") - + userIDprefix := viper.GetString("userid-prefix") //create a new client if none exist if _, err := os.Stat(storeDir); os.IsNotExist(err) { // Load NDF @@ -320,8 +320,14 @@ func createClient() *api.Client { err = api.NewPrecannedClient(precannedID, string(ndfJSON), storeDir, []byte(pass)) } else { - err = api.NewClient(string(ndfJSON), storeDir, + if userIDprefix != "" { + err = api.NewVanityClient(string(ndfJSON), storeDir, + []byte(pass), regCode, userIDprefix) + } else { + err = api.NewClient(string(ndfJSON), storeDir, []byte(pass), regCode) + } + } if err != nil {