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 }