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

     1  package bundle
     2  
     3  import (
     4  	"crypto/hmac"
     5  	"crypto/sha256"
     6  	"encoding/base64"
     7  	"errors"
     8  	"fmt"
     9  
    10  	"github.com/keybase/client/go/libkb"
    11  	"github.com/keybase/client/go/msgpack"
    12  	"github.com/keybase/client/go/protocol/keybase1"
    13  	"github.com/keybase/client/go/protocol/stellar1"
    14  	"golang.org/x/crypto/nacl/secretbox"
    15  )
    16  
    17  // BoxedEncoded is the result of boxing and encoding a Bundle object.
    18  type BoxedEncoded struct {
    19  	EncParent           stellar1.EncryptedBundle
    20  	EncParentB64        string // base64 msgpacked Enc
    21  	VisParentB64        string
    22  	FormatVersionParent stellar1.BundleVersion
    23  	AcctBundles         map[stellar1.AccountID]AcctBoxedEncoded
    24  }
    25  
    26  func newBoxedEncoded() *BoxedEncoded {
    27  	return &BoxedEncoded{
    28  		FormatVersionParent: stellar1.BundleVersion_V2,
    29  		AcctBundles:         make(map[stellar1.AccountID]AcctBoxedEncoded),
    30  	}
    31  }
    32  
    33  func newVisibleParent(a *stellar1.Bundle, accountsVisible []stellar1.BundleVisibleEntryV2) stellar1.BundleVisibleV2 {
    34  	return stellar1.BundleVisibleV2{
    35  		Revision: a.Revision,
    36  		Prev:     a.Prev,
    37  		Accounts: accountsVisible,
    38  	}
    39  }
    40  
    41  func (b BoxedEncoded) toBundleEncodedB64() BundleEncoded {
    42  	benc := BundleEncoded{
    43  		EncParent:   b.EncParentB64,
    44  		VisParent:   b.VisParentB64,
    45  		AcctBundles: make(map[stellar1.AccountID]string),
    46  	}
    47  
    48  	for acctID, acctBundle := range b.AcctBundles {
    49  		benc.AcctBundles[acctID] = acctBundle.EncB64
    50  	}
    51  
    52  	return benc
    53  }
    54  
    55  // BundleEncoded contains all the encoded fields for communicating
    56  // with the api server to post and get account bundles.
    57  type BundleEncoded struct {
    58  	EncParent           string                        `json:"encrypted_parent"` // base64 msgpacked Enc
    59  	VisParent           string                        `json:"visible_parent"`
    60  	FormatVersionParent stellar1.BundleVersion        `json:"version_parent"`
    61  	AcctBundles         map[stellar1.AccountID]string `json:"account_bundles"`
    62  }
    63  
    64  // BoxAndEncode encrypts and encodes a Bundle object.
    65  func BoxAndEncode(a *stellar1.Bundle, pukGen keybase1.PerUserKeyGeneration, puk libkb.PerUserKeySeed) (*BoxedEncoded, error) {
    66  	err := a.CheckInvariants()
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  
    71  	accountsVisible, accountsSecret := visibilitySplit(a)
    72  
    73  	// visible portion parent
    74  	visibleV2 := newVisibleParent(a, accountsVisible)
    75  
    76  	boxed := newBoxedEncoded()
    77  
    78  	// encrypted account bundles
    79  	for i, acctEntry := range visibleV2.Accounts {
    80  		secret, ok := a.AccountBundles[acctEntry.AccountID]
    81  		if !ok {
    82  			continue
    83  		}
    84  		ab, err := accountBoxAndEncode(acctEntry.AccountID, secret, pukGen, puk)
    85  		if err != nil {
    86  			return nil, err
    87  		}
    88  		boxed.AcctBundles[acctEntry.AccountID] = *ab
    89  
    90  		visibleV2.Accounts[i].EncAcctBundleHash = ab.EncHash
    91  	}
    92  
    93  	// have to do this after to get hashes of encrypted account bundles
    94  	visiblePack, err := msgpack.Encode(visibleV2)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  	visibleHash := sha256.Sum256(visiblePack)
    99  	boxed.VisParentB64 = base64.StdEncoding.EncodeToString(visiblePack)
   100  
   101  	// secret portion parent
   102  	versionedSecret := stellar1.NewBundleSecretVersionedWithV2(stellar1.BundleSecretV2{
   103  		VisibleHash: visibleHash[:],
   104  		Accounts:    accountsSecret,
   105  	})
   106  	boxed.EncParent, boxed.EncParentB64, err = parentBoxAndEncode(versionedSecret, pukGen, puk)
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  
   111  	return boxed, nil
   112  }
   113  
   114  func visibilitySplit(a *stellar1.Bundle) ([]stellar1.BundleVisibleEntryV2, []stellar1.BundleSecretEntryV2) {
   115  	vis := make([]stellar1.BundleVisibleEntryV2, len(a.Accounts))
   116  	sec := make([]stellar1.BundleSecretEntryV2, len(a.Accounts))
   117  	for i, acct := range a.Accounts {
   118  		vis[i] = stellar1.BundleVisibleEntryV2{
   119  			AccountID:          acct.AccountID,
   120  			Mode:               acct.Mode,
   121  			IsPrimary:          acct.IsPrimary,
   122  			AcctBundleRevision: acct.AcctBundleRevision,
   123  			EncAcctBundleHash:  acct.EncAcctBundleHash,
   124  		}
   125  		sec[i] = stellar1.BundleSecretEntryV2{
   126  			AccountID: acct.AccountID,
   127  			Name:      acct.Name,
   128  		}
   129  	}
   130  	return vis, sec
   131  }
   132  
   133  func parentBoxAndEncode(bundle stellar1.BundleSecretVersioned, pukGen keybase1.PerUserKeyGeneration, puk libkb.PerUserKeySeed) (stellar1.EncryptedBundle, string, error) {
   134  	// Msgpack (inner)
   135  	clearpack, err := msgpack.Encode(bundle)
   136  	if err != nil {
   137  		return stellar1.EncryptedBundle{}, "", err
   138  	}
   139  
   140  	// Derive key
   141  	symmetricKey, err := puk.DeriveSymmetricKey(libkb.DeriveReasonPUKStellarBundle)
   142  	if err != nil {
   143  		return stellar1.EncryptedBundle{}, "", err
   144  	}
   145  
   146  	// Secretbox
   147  	var nonce [libkb.NaclDHNonceSize]byte
   148  	nonce, err = libkb.RandomNaclDHNonce()
   149  	if err != nil {
   150  		return stellar1.EncryptedBundle{}, "", err
   151  	}
   152  	secbox := secretbox.Seal(nil, clearpack, &nonce, (*[libkb.NaclSecretBoxKeySize]byte)(&symmetricKey))
   153  
   154  	// Annotate
   155  	res := stellar1.EncryptedBundle{
   156  		V:   2,
   157  		E:   secbox,
   158  		N:   nonce,
   159  		Gen: pukGen,
   160  	}
   161  
   162  	// Msgpack (outer) + b64
   163  	cipherpack, err := msgpack.Encode(res)
   164  	if err != nil {
   165  		return stellar1.EncryptedBundle{}, "", err
   166  	}
   167  	resB64 := base64.StdEncoding.EncodeToString(cipherpack)
   168  	return res, resB64, nil
   169  }
   170  
   171  // AcctBoxedEncoded is the result of boxing and encoding the per-account secrets.
   172  type AcctBoxedEncoded struct {
   173  	Enc           stellar1.EncryptedAccountBundle
   174  	EncHash       stellar1.Hash
   175  	EncB64        string // base64 msgpacked Enc
   176  	FormatVersion stellar1.AccountBundleVersion
   177  }
   178  
   179  func accountBoxAndEncode(accountID stellar1.AccountID, accountBundle stellar1.AccountBundle, pukGen keybase1.PerUserKeyGeneration, puk libkb.PerUserKeySeed) (*AcctBoxedEncoded, error) {
   180  	versionedSecret := stellar1.NewAccountBundleSecretVersionedWithV1(stellar1.AccountBundleSecretV1{
   181  		AccountID: accountID,
   182  		Signers:   accountBundle.Signers,
   183  	})
   184  
   185  	encBundle, b64, err := accountEncrypt(versionedSecret, pukGen, puk)
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  
   190  	encPack, err := msgpack.Encode(encBundle)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  	encHash := sha256.Sum256(encPack)
   195  
   196  	res := AcctBoxedEncoded{Enc: encBundle, EncHash: encHash[:], EncB64: b64, FormatVersion: 1}
   197  
   198  	return &res, nil
   199  }
   200  
   201  // PukFinder helps this package find puks.
   202  type PukFinder interface {
   203  	SeedByGeneration(m libkb.MetaContext, generation keybase1.PerUserKeyGeneration) (libkb.PerUserKeySeed, error)
   204  }
   205  
   206  type AccountPukGens map[stellar1.AccountID](keybase1.PerUserKeyGeneration)
   207  
   208  // DecodeAndUnbox decodes the encrypted and visible encoded bundles and unboxes
   209  // the encrypted bundle using PukFinder to find the correct puk. It combines
   210  // the results into a stellar1.Bundle and also returns additional information
   211  // about the bundle: its version, pukGen, and the pukGens of each of the
   212  // decrypted account secrets.
   213  func DecodeAndUnbox(m libkb.MetaContext, finder PukFinder, encodedBundle BundleEncoded) (*stellar1.Bundle, stellar1.BundleVersion, keybase1.PerUserKeyGeneration, AccountPukGens, error) {
   214  	accountPukGens := make(AccountPukGens)
   215  	encBundle, hash, err := decodeParent(encodedBundle.EncParent)
   216  	if err != nil {
   217  		return nil, 0, 0, accountPukGens, err
   218  	}
   219  
   220  	puk, err := finder.SeedByGeneration(m, encBundle.Gen)
   221  	if err != nil {
   222  		return nil, 0, 0, accountPukGens, err
   223  	}
   224  
   225  	parent, parentVersion, err := unboxParent(encBundle, hash, encodedBundle.VisParent, puk)
   226  	if err != nil {
   227  		return nil, 0, 0, accountPukGens, err
   228  	}
   229  	parent.AccountBundles = make(map[stellar1.AccountID]stellar1.AccountBundle)
   230  	for _, parentEntry := range parent.Accounts {
   231  		if acctEncB64, ok := encodedBundle.AcctBundles[parentEntry.AccountID]; ok {
   232  			acctBundle, acctGen, err := decodeAndUnboxAcctBundle(m, finder, acctEncB64, parentEntry)
   233  			accountPukGens[parentEntry.AccountID] = acctGen
   234  			if err != nil {
   235  				return nil, 0, 0, accountPukGens, err
   236  			}
   237  			if acctBundle == nil {
   238  				return nil, 0, 0, accountPukGens, fmt.Errorf("error unboxing account bundle: missing for account %s", parentEntry.AccountID)
   239  			}
   240  
   241  			parent.AccountBundles[parentEntry.AccountID] = *acctBundle
   242  		}
   243  	}
   244  	if err = parent.CheckInvariants(); err != nil {
   245  		return nil, 0, 0, accountPukGens, err
   246  	}
   247  	return parent, parentVersion, encBundle.Gen, accountPukGens, nil
   248  }
   249  
   250  func decodeAndUnboxAcctBundle(m libkb.MetaContext, finder PukFinder, encB64 string, parentEntry stellar1.BundleEntry) (*stellar1.AccountBundle, keybase1.PerUserKeyGeneration, error) {
   251  	eab, hash, err := decode(encB64)
   252  	if err != nil {
   253  		return nil, 0, err
   254  	}
   255  
   256  	if !libkb.SecureByteArrayEq(hash, parentEntry.EncAcctBundleHash) {
   257  		return nil, 0, errors.New("account bundle and parent entry hash mismatch")
   258  	}
   259  
   260  	puk, err := finder.SeedByGeneration(m, eab.Gen)
   261  	if err != nil {
   262  		return nil, 0, err
   263  	}
   264  	ab, _, err := unbox(eab, hash, puk)
   265  	if err != nil {
   266  		return nil, 0, err
   267  	}
   268  	if ab.AccountID != parentEntry.AccountID {
   269  		return nil, 0, errors.New("account bundle and parent entry account ID mismatch")
   270  	}
   271  	return ab, eab.Gen, nil
   272  }
   273  
   274  // accountEncrypt encrypts the stellar account key bundle for the PUK.
   275  // Returns the encrypted struct and a base64 encoding for posting to the server.
   276  // Does not check invariants.
   277  func accountEncrypt(bundle stellar1.AccountBundleSecretVersioned, pukGen keybase1.PerUserKeyGeneration, puk libkb.PerUserKeySeed) (res stellar1.EncryptedAccountBundle, resB64 string, err error) {
   278  	// Msgpack (inner)
   279  	clearpack, err := msgpack.Encode(bundle)
   280  	if err != nil {
   281  		return res, resB64, err
   282  	}
   283  
   284  	// Derive key
   285  	symmetricKey, err := puk.DeriveSymmetricKey(libkb.DeriveReasonPUKStellarAcctBundle)
   286  	if err != nil {
   287  		return res, resB64, err
   288  	}
   289  
   290  	// Secretbox
   291  	var nonce [libkb.NaclDHNonceSize]byte
   292  	nonce, err = libkb.RandomNaclDHNonce()
   293  	if err != nil {
   294  		return res, resB64, err
   295  	}
   296  	secbox := secretbox.Seal(nil, clearpack, &nonce, (*[libkb.NaclSecretBoxKeySize]byte)(&symmetricKey))
   297  
   298  	// Annotate
   299  	res = stellar1.EncryptedAccountBundle{
   300  		V:   1,
   301  		E:   secbox,
   302  		N:   nonce,
   303  		Gen: pukGen,
   304  	}
   305  
   306  	// Msgpack (outer) + b64
   307  	cipherpack, err := msgpack.Encode(res)
   308  	if err != nil {
   309  		return res, resB64, err
   310  	}
   311  	resB64 = base64.StdEncoding.EncodeToString(cipherpack)
   312  	return res, resB64, nil
   313  }
   314  
   315  // decodeParent decodes a base64-encoded encrypted parent bundle.
   316  func decodeParent(encryptedB64 string) (stellar1.EncryptedBundle, stellar1.Hash, error) {
   317  	cipherpack, err := base64.StdEncoding.DecodeString(encryptedB64)
   318  	if err != nil {
   319  		return stellar1.EncryptedBundle{}, stellar1.Hash{}, err
   320  	}
   321  	encHash := sha256.Sum256(cipherpack)
   322  	var enc stellar1.EncryptedBundle
   323  	if err = msgpack.Decode(&enc, cipherpack); err != nil {
   324  		return stellar1.EncryptedBundle{}, stellar1.Hash{}, err
   325  	}
   326  	return enc, encHash[:], nil
   327  }
   328  
   329  // unboxParent unboxes an encrypted parent bundle and decodes the visual portion of the bundle.
   330  // It validates the visible hash in the secret portion.
   331  func unboxParent(encBundle stellar1.EncryptedBundle, hash stellar1.Hash, visB64 string, puk libkb.PerUserKeySeed) (*stellar1.Bundle, stellar1.BundleVersion, error) {
   332  	versioned, err := decryptParent(encBundle, puk)
   333  	if err != nil {
   334  		return nil, 0, err
   335  	}
   336  	version, err := versioned.Version()
   337  	if err != nil {
   338  		return nil, 0, err
   339  	}
   340  
   341  	var bundleOut stellar1.Bundle
   342  	switch version {
   343  	case stellar1.BundleVersion_V2:
   344  		bundleOut, err = unboxParentV2(versioned, visB64)
   345  		if err != nil {
   346  			return nil, 0, err
   347  		}
   348  	default:
   349  		return nil, 0, fmt.Errorf("unsupported parent bundle version: %d", version)
   350  	}
   351  
   352  	bundleOut.OwnHash = hash
   353  	if len(bundleOut.OwnHash) == 0 {
   354  		return nil, 0, errors.New("stellar account bundle missing own hash")
   355  	}
   356  
   357  	return &bundleOut, version, nil
   358  }
   359  
   360  func unboxParentV2(versioned stellar1.BundleSecretVersioned, visB64 string) (stellar1.Bundle, error) {
   361  	var empty stellar1.Bundle
   362  	visiblePack, err := base64.StdEncoding.DecodeString(visB64)
   363  	if err != nil {
   364  		return empty, err
   365  	}
   366  	visibleHash := sha256.Sum256(visiblePack)
   367  	secretV2 := versioned.V2()
   368  	if !hmac.Equal(visibleHash[:], secretV2.VisibleHash) {
   369  		return empty, errors.New("corrupted bundle: visible hash mismatch")
   370  	}
   371  	var visibleV2 stellar1.BundleVisibleV2
   372  	err = msgpack.Decode(&visibleV2, visiblePack)
   373  	if err != nil {
   374  		return empty, err
   375  	}
   376  	return merge(secretV2, visibleV2)
   377  }
   378  
   379  // decryptParent decrypts an encrypted parent bundle with the provided puk.
   380  func decryptParent(encBundle stellar1.EncryptedBundle, puk libkb.PerUserKeySeed) (res stellar1.BundleSecretVersioned, err error) {
   381  	switch encBundle.V {
   382  	case 1:
   383  		// CORE-8135
   384  		return res, fmt.Errorf("stellar secret bundle encryption version 1 has been retired")
   385  	case 2:
   386  	default:
   387  		return res, fmt.Errorf("unsupported stellar secret bundle encryption version: %v", encBundle.V)
   388  	}
   389  
   390  	// Derive key
   391  	reason := libkb.DeriveReasonPUKStellarBundle
   392  	symmetricKey, err := puk.DeriveSymmetricKey(reason)
   393  	if err != nil {
   394  		return res, err
   395  	}
   396  
   397  	// Secretbox
   398  	clearpack, ok := secretbox.Open(nil, encBundle.E,
   399  		(*[libkb.NaclDHNonceSize]byte)(&encBundle.N),
   400  		(*[libkb.NaclSecretBoxKeySize]byte)(&symmetricKey))
   401  	if !ok {
   402  		return res, errors.New("stellar bundle secret box open failed")
   403  	}
   404  
   405  	// Msgpack (inner)
   406  	err = msgpack.Decode(&res, clearpack)
   407  	return res, err
   408  }
   409  
   410  // decode decodes a base64-encoded encrypted account bundle.
   411  func decode(encryptedB64 string) (stellar1.EncryptedAccountBundle, stellar1.Hash, error) {
   412  	cipherpack, err := base64.StdEncoding.DecodeString(encryptedB64)
   413  	if err != nil {
   414  		return stellar1.EncryptedAccountBundle{}, stellar1.Hash{}, err
   415  	}
   416  	encHash := sha256.Sum256(cipherpack)
   417  	var enc stellar1.EncryptedAccountBundle
   418  	if err = msgpack.Decode(&enc, cipherpack); err != nil {
   419  		return stellar1.EncryptedAccountBundle{}, stellar1.Hash{}, err
   420  	}
   421  	return enc, encHash[:], nil
   422  }
   423  
   424  // unbox unboxes an encrypted account bundle and decodes the visual portion of the bundle.
   425  // It validates the visible hash in the secret portion.
   426  func unbox(encBundle stellar1.EncryptedAccountBundle, hash stellar1.Hash /* visB64 string, */, puk libkb.PerUserKeySeed) (*stellar1.AccountBundle, stellar1.AccountBundleVersion, error) {
   427  	versioned, err := decrypt(encBundle, puk)
   428  	if err != nil {
   429  		return nil, 0, err
   430  	}
   431  	version, err := versioned.Version()
   432  	if err != nil {
   433  		return nil, 0, err
   434  	}
   435  
   436  	var bundleOut stellar1.AccountBundle
   437  	switch version {
   438  	case stellar1.AccountBundleVersion_V1:
   439  		secretV1 := versioned.V1()
   440  		bundleOut = stellar1.AccountBundle{
   441  			AccountID: secretV1.AccountID,
   442  			Signers:   secretV1.Signers,
   443  		}
   444  	case stellar1.AccountBundleVersion_V2,
   445  		stellar1.AccountBundleVersion_V3,
   446  		stellar1.AccountBundleVersion_V4,
   447  		stellar1.AccountBundleVersion_V5,
   448  		stellar1.AccountBundleVersion_V6,
   449  		stellar1.AccountBundleVersion_V7,
   450  		stellar1.AccountBundleVersion_V8,
   451  		stellar1.AccountBundleVersion_V9,
   452  		stellar1.AccountBundleVersion_V10:
   453  		return nil, 0, errors.New("unsupported AccountBundleSecret version")
   454  	default:
   455  		return nil, 0, errors.New("invalid AccountBundle version")
   456  	}
   457  
   458  	bundleOut.OwnHash = hash
   459  	if len(bundleOut.OwnHash) == 0 {
   460  		return nil, 0, errors.New("stellar account bundle missing own hash")
   461  	}
   462  
   463  	return &bundleOut, version, nil
   464  }
   465  
   466  // decrypt decrypts an encrypted account bundle with the provided puk.
   467  func decrypt(encBundle stellar1.EncryptedAccountBundle, puk libkb.PerUserKeySeed) (stellar1.AccountBundleSecretVersioned, error) {
   468  	var empty stellar1.AccountBundleSecretVersioned
   469  	if encBundle.V != 1 {
   470  		return empty, errors.New("invalid stellar secret account bundle encryption version")
   471  	}
   472  
   473  	// Derive key
   474  	reason := libkb.DeriveReasonPUKStellarAcctBundle
   475  	symmetricKey, err := puk.DeriveSymmetricKey(reason)
   476  	if err != nil {
   477  		return empty, err
   478  	}
   479  
   480  	// Secretbox
   481  	clearpack, ok := secretbox.Open(nil, encBundle.E,
   482  		(*[libkb.NaclDHNonceSize]byte)(&encBundle.N),
   483  		(*[libkb.NaclSecretBoxKeySize]byte)(&symmetricKey))
   484  	if !ok {
   485  		return empty, errors.New("stellar bundle secret box open failed")
   486  	}
   487  
   488  	// Msgpack (inner)
   489  	var bver stellar1.AccountBundleSecretVersioned
   490  	err = msgpack.Decode(&bver, clearpack)
   491  	if err != nil {
   492  		return empty, err
   493  	}
   494  	return bver, nil
   495  }
   496  func convertVisibleAccounts(in []stellar1.BundleVisibleEntryV2) []stellar1.BundleEntry {
   497  	out := make([]stellar1.BundleEntry, len(in))
   498  	for i, e := range in {
   499  		out[i] = stellar1.BundleEntry{
   500  			AccountID:          e.AccountID,
   501  			Mode:               e.Mode,
   502  			IsPrimary:          e.IsPrimary,
   503  			AcctBundleRevision: e.AcctBundleRevision,
   504  			EncAcctBundleHash:  e.EncAcctBundleHash,
   505  		}
   506  	}
   507  	return out
   508  }
   509  
   510  // merge combines the versioned secret account bundle and the visible account bundle into
   511  // a stellar1.AccountBundle for local use.
   512  func merge(secret stellar1.BundleSecretV2, visible stellar1.BundleVisibleV2) (stellar1.Bundle, error) {
   513  	if len(secret.Accounts) != len(visible.Accounts) {
   514  		return stellar1.Bundle{}, errors.New("invalid bundle, mismatched number of visible and secret accounts")
   515  	}
   516  	accounts := convertVisibleAccounts(visible.Accounts)
   517  
   518  	// these should be in the same order
   519  	for i, secretAccount := range secret.Accounts {
   520  		if accounts[i].AccountID != secretAccount.AccountID {
   521  			return stellar1.Bundle{}, errors.New("invalid bundle, mismatched order of visible and secret accounts")
   522  		}
   523  		accounts[i].Name = secretAccount.Name
   524  	}
   525  	return stellar1.Bundle{
   526  		Revision: visible.Revision,
   527  		Prev:     visible.Prev,
   528  		Accounts: accounts,
   529  	}, nil
   530  }