github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/stellar/bundle/bundle.go (about)

     1  package bundle
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  
     7  	"github.com/keybase/client/go/libkb"
     8  	"github.com/keybase/client/go/protocol/stellar1"
     9  	"github.com/stellar/go/keypair"
    10  )
    11  
    12  // New creates a Bundle from an existing secret key.
    13  func New(secret stellar1.SecretKey, name string) (*stellar1.Bundle, error) {
    14  	secretKey, accountID, _, err := libkb.ParseStellarSecretKey(string(secret))
    15  	if err != nil {
    16  		return nil, err
    17  	}
    18  	return &stellar1.Bundle{
    19  		Revision: 1,
    20  		Accounts: []stellar1.BundleEntry{
    21  			newEntry(accountID, name, false, stellar1.AccountMode_USER),
    22  		},
    23  		AccountBundles: map[stellar1.AccountID]stellar1.AccountBundle{
    24  			accountID: newAccountBundle(accountID, secretKey),
    25  		},
    26  	}, nil
    27  }
    28  
    29  // NewInitial creates a Bundle with a new random secret key.
    30  func NewInitial(name string) (*stellar1.Bundle, error) {
    31  	full, err := keypair.Random()
    32  	if err != nil {
    33  		return nil, err
    34  	}
    35  
    36  	x, err := New(stellar1.SecretKey(full.Seed()), name)
    37  	if err != nil {
    38  		return nil, err
    39  	}
    40  
    41  	x.Accounts[0].IsPrimary = true
    42  
    43  	return x, nil
    44  }
    45  
    46  func newEntry(accountID stellar1.AccountID, name string, isPrimary bool, mode stellar1.AccountMode) stellar1.BundleEntry {
    47  	return stellar1.BundleEntry{
    48  		AccountID:          accountID,
    49  		Name:               name,
    50  		Mode:               mode,
    51  		IsPrimary:          isPrimary,
    52  		AcctBundleRevision: 1,
    53  	}
    54  }
    55  
    56  func newAccountBundle(accountID stellar1.AccountID, secretKey stellar1.SecretKey) stellar1.AccountBundle {
    57  	return stellar1.AccountBundle{
    58  		AccountID: accountID,
    59  		Signers:   []stellar1.SecretKey{secretKey},
    60  	}
    61  }
    62  
    63  // ErrNoChangeNecessary means that any proposed change to a bundle isn't
    64  // actually necessary.
    65  var ErrNoChangeNecessary = errors.New("no account mode change is necessary")
    66  
    67  // MakeMobileOnly transforms an account in a stellar1.Bundle into a mobile-only
    68  // account. This advances the revision of the Bundle.  If it's already mobile-only,
    69  // this function will return ErrNoChangeNecessary.
    70  func MakeMobileOnly(a *stellar1.Bundle, accountID stellar1.AccountID) error {
    71  	var found bool
    72  	for i, account := range a.Accounts {
    73  		if account.AccountID == accountID {
    74  			if account.Mode == stellar1.AccountMode_MOBILE {
    75  				return ErrNoChangeNecessary
    76  			}
    77  			account.Mode = stellar1.AccountMode_MOBILE
    78  			a.Accounts[i] = account
    79  			found = true
    80  			break
    81  		}
    82  	}
    83  	if !found {
    84  		return libkb.NotFoundError{}
    85  	}
    86  	return nil
    87  }
    88  
    89  // MakeAllDevices transforms an account in a stellar1.Bundle into an all-devices
    90  // account. This advances the revision of the Bundle.  If it's already all-devices,
    91  // this function will return ErrNoChangeNecessary.
    92  func MakeAllDevices(a *stellar1.Bundle, accountID stellar1.AccountID) error {
    93  	var found bool
    94  	for i, account := range a.Accounts {
    95  		if account.AccountID == accountID {
    96  			if account.Mode == stellar1.AccountMode_USER {
    97  				return ErrNoChangeNecessary
    98  			}
    99  			account.Mode = stellar1.AccountMode_USER
   100  			a.Accounts[i] = account
   101  			found = true
   102  			break
   103  		}
   104  	}
   105  	if !found {
   106  		return libkb.NotFoundError{}
   107  	}
   108  	return nil
   109  }
   110  
   111  // WithSecret is a convenient summary of an individual account
   112  // that includes the secret keys.
   113  type WithSecret struct {
   114  	AccountID stellar1.AccountID
   115  	Mode      stellar1.AccountMode
   116  	Name      string
   117  	Revision  stellar1.BundleRevision
   118  	Signers   []stellar1.SecretKey
   119  }
   120  
   121  // AccountWithSecret finds an account in bundle and its associated secret
   122  // and extracts them into a convenience type bundle.WithSecret.
   123  // It will return libkb.NotFoundError if it can't find the secret or the
   124  // account in the bundle.
   125  func AccountWithSecret(bundle *stellar1.Bundle, accountID stellar1.AccountID) (*WithSecret, error) {
   126  	secret, ok := bundle.AccountBundles[accountID]
   127  	if !ok {
   128  		return nil, libkb.NotFoundError{}
   129  	}
   130  	// ugh
   131  	var found *stellar1.BundleEntry
   132  	for _, a := range bundle.Accounts {
   133  		if a.AccountID == accountID {
   134  			found = &a
   135  			break
   136  		}
   137  	}
   138  	if found == nil {
   139  		// this is bad: secret found but not visible portion
   140  		return nil, libkb.NotFoundError{}
   141  	}
   142  	return &WithSecret{
   143  		AccountID: found.AccountID,
   144  		Mode:      found.Mode,
   145  		Name:      found.Name,
   146  		Revision:  found.AcctBundleRevision,
   147  		Signers:   secret.Signers,
   148  	}, nil
   149  }
   150  
   151  // AdvanceBundle only advances the revisions and hashes on the Bundle
   152  // and not on the accounts. This is useful for adding and removing accounts
   153  // but not for changing them.
   154  func AdvanceBundle(prevBundle stellar1.Bundle) stellar1.Bundle {
   155  	nextBundle := prevBundle.DeepCopy()
   156  	nextBundle.Prev = nextBundle.OwnHash
   157  	nextBundle.OwnHash = nil
   158  	nextBundle.Revision++
   159  	return nextBundle
   160  }
   161  
   162  // AdvanceAccounts advances the revisions and hashes on the Bundle
   163  // as well as on the specified Accounts. This is useful for mutating one or more
   164  // of the accounts in the bundle, e.g. changing which one is Primary.
   165  func AdvanceAccounts(prevBundle stellar1.Bundle, accountIDs []stellar1.AccountID) stellar1.Bundle {
   166  	nextBundle := prevBundle.DeepCopy()
   167  	nextBundle.Prev = nextBundle.OwnHash
   168  	nextBundle.OwnHash = nil
   169  	nextBundle.Revision++
   170  
   171  	var nextAccounts []stellar1.BundleEntry
   172  	for _, acct := range nextBundle.Accounts {
   173  		copiedAcct := acct.DeepCopy()
   174  		for _, accountID := range accountIDs {
   175  			if copiedAcct.AccountID == accountID {
   176  				copiedAcct.AcctBundleRevision++
   177  			}
   178  		}
   179  		nextAccounts = append(nextAccounts, copiedAcct)
   180  	}
   181  	nextBundle.Accounts = nextAccounts
   182  
   183  	return nextBundle
   184  }
   185  
   186  // AddAccount adds an account to the bundle. Mutates `bundle`.
   187  func AddAccount(bundle *stellar1.Bundle, secretKey stellar1.SecretKey, name string, makePrimary bool) (err error) {
   188  	if bundle == nil {
   189  		return fmt.Errorf("nil bundle")
   190  	}
   191  	secretKey, accountID, _, err := libkb.ParseStellarSecretKey(string(secretKey))
   192  	if err != nil {
   193  		return err
   194  	}
   195  	if name == "" {
   196  		return fmt.Errorf("Name required for new account")
   197  	}
   198  	if makePrimary {
   199  		for i := range bundle.Accounts {
   200  			bundle.Accounts[i].IsPrimary = false
   201  		}
   202  	}
   203  	bundle.Accounts = append(bundle.Accounts, stellar1.BundleEntry{
   204  		AccountID:          accountID,
   205  		Mode:               stellar1.AccountMode_USER,
   206  		IsPrimary:          makePrimary,
   207  		AcctBundleRevision: 1,
   208  		Name:               name,
   209  	})
   210  	bundle.AccountBundles[accountID] = stellar1.AccountBundle{
   211  		AccountID: accountID,
   212  		Signers:   []stellar1.SecretKey{secretKey},
   213  	}
   214  	return bundle.CheckInvariants()
   215  }
   216  
   217  // CreateNewAccount generates a Stellar key pair and adds it to the
   218  // bundle. Mutates `bundle`.
   219  func CreateNewAccount(bundle *stellar1.Bundle, name string, makePrimary bool) (pub stellar1.AccountID, err error) {
   220  	accountID, masterKey, err := randomStellarKeypair()
   221  	if err != nil {
   222  		return pub, err
   223  	}
   224  	if err := AddAccount(bundle, masterKey, name, makePrimary); err != nil {
   225  		return pub, err
   226  	}
   227  	return accountID, nil
   228  }
   229  
   230  func randomStellarKeypair() (pub stellar1.AccountID, sec stellar1.SecretKey, err error) {
   231  	full, err := keypair.Random()
   232  	if err != nil {
   233  		return pub, sec, err
   234  	}
   235  	return stellar1.AccountID(full.Address()), stellar1.SecretKey(full.Seed()), nil
   236  }