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

     1  package stellar
     2  
     3  import (
     4  	"context"
     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/box"
    15  	"golang.org/x/crypto/nacl/secretbox"
    16  )
    17  
    18  type noteBuildSecret struct {
    19  	symmetricKey libkb.NaclSecretBoxKey
    20  	sender       stellar1.NoteRecipient
    21  	recipient    *stellar1.NoteRecipient
    22  }
    23  
    24  // noteSymmetricKey returns a symmetric key to be used in encrypting a note.
    25  // The key is available to the current user and to the optional `other` recipient.
    26  // If `other` is nil the key is derived from the latest PUK seed.
    27  // If `other` is non-nil the key is derived from the nacl shared key of both users' latest PUK encryption keys.
    28  func noteSymmetricKey(mctx libkb.MetaContext, other *keybase1.UserVersion) (res noteBuildSecret, err error) {
    29  	meUV, err := mctx.G().GetMeUV(mctx.Ctx())
    30  	if err != nil {
    31  		return res, err
    32  	}
    33  	puk1Gen, puk1Seed, err := loadOwnLatestPuk(mctx)
    34  	if err != nil {
    35  		return res, err
    36  	}
    37  	symmetricKey, err := puk1Seed.DeriveSymmetricKey(libkb.DeriveReasonPUKStellarNoteSelf)
    38  	if err != nil {
    39  		return res, err
    40  	}
    41  	var recipient *stellar1.NoteRecipient
    42  	if other != nil && !other.Eq(meUV) {
    43  		u2, err := loadUvUpk(mctx, *other)
    44  		if err != nil {
    45  			return res, fmt.Errorf("error loading recipient: %v", err)
    46  		}
    47  		puk2 := u2.GetLatestPerUserKey()
    48  		if puk2 == nil {
    49  			return res, fmt.Errorf("recipient has no per-user key")
    50  		}
    51  		// Overwite symmetricKey with the shared key.
    52  		symmetricKey, err = noteMixKeys(mctx, puk1Seed, puk2.EncKID)
    53  		if err != nil {
    54  			return res, err
    55  		}
    56  		recipient = &stellar1.NoteRecipient{
    57  			User:   *other,
    58  			PukGen: keybase1.PerUserKeyGeneration(puk2.Gen),
    59  		}
    60  	}
    61  	return noteBuildSecret{
    62  		symmetricKey: symmetricKey,
    63  		sender: stellar1.NoteRecipient{
    64  			User:   meUV,
    65  			PukGen: puk1Gen,
    66  		},
    67  		recipient: recipient,
    68  	}, nil
    69  }
    70  
    71  func noteSymmetricKeyForDecryption(mctx libkb.MetaContext, encNote stellar1.EncryptedNote) (res libkb.NaclSecretBoxKey, err error) {
    72  	meUV, err := mctx.G().GetMeUV(mctx.Ctx())
    73  	if err != nil {
    74  		return res, err
    75  	}
    76  	var mePukGen keybase1.PerUserKeyGeneration
    77  	var them *stellar1.NoteRecipient
    78  	if encNote.Sender.User.Eq(meUV) {
    79  		mePukGen = encNote.Sender.PukGen
    80  		them = encNote.Recipient
    81  	}
    82  	if encNote.Recipient != nil && encNote.Recipient.User.Eq(meUV) {
    83  		mePukGen = encNote.Recipient.PukGen
    84  		them = &encNote.Sender
    85  	}
    86  	if mePukGen == 0 {
    87  		return res, fmt.Errorf("note not encrypted for logged-in user")
    88  	}
    89  	pukring, err := mctx.G().GetPerUserKeyring(mctx.Ctx())
    90  	if err != nil {
    91  		return res, err
    92  	}
    93  	pukSeed, err := pukring.GetSeedByGenerationOrSync(mctx, mePukGen)
    94  	if err != nil {
    95  		return res, err
    96  	}
    97  	if them == nil {
    98  		return pukSeed.DeriveSymmetricKey(libkb.DeriveReasonPUKStellarNoteSelf)
    99  	}
   100  	u2, err := loadUvUpk(mctx, them.User)
   101  	if err != nil {
   102  		return res, err
   103  	}
   104  	puk2 := u2.GetPerUserKeyByGen(them.PukGen)
   105  	if puk2 == nil {
   106  		return res, fmt.Errorf("could not find other user's key: %v %v", them.User.String(), them.PukGen)
   107  	}
   108  	return noteMixKeys(mctx, pukSeed, puk2.EncKID)
   109  }
   110  
   111  // noteMixKeys derives a shared symmetric key for two DH keys.
   112  // The key is the last 32 bytes of the nacl box of 32 zeros with a use-specific nonce.
   113  func noteMixKeys(mctx libkb.MetaContext, puk1 libkb.PerUserKeySeed, puk2EncKID keybase1.KID) (res libkb.NaclSecretBoxKey, err error) {
   114  	puk1Enc, err := puk1.DeriveDHKey()
   115  	if err != nil {
   116  		return res, err
   117  	}
   118  	puk2EncGeneric, err := libkb.ImportKeypairFromKID(puk2EncKID)
   119  	if err != nil {
   120  		return res, err
   121  	}
   122  	puk2Enc, ok := puk2EncGeneric.(libkb.NaclDHKeyPair)
   123  	if !ok {
   124  		return res, fmt.Errorf("recipient per-user key was not a DH key")
   125  	}
   126  	var zeros [32]byte
   127  	// This is a constant nonce used for key derivation.
   128  	// The derived key will be used with one-time random nonces for the actual encryption/decryption.
   129  	nonce := noteMixPukNonce()
   130  	sharedSecretBox := box.Seal(nil, zeros[:], &nonce, (*[32]byte)(&puk2Enc.Public), (*[32]byte)(puk1Enc.Private))
   131  	return libkb.MakeByte32Soft(sharedSecretBox[len(sharedSecretBox)-32:])
   132  }
   133  
   134  // noteMixPukNonce is a nonce used in key derivation for shared notes.
   135  // 24-byte prefix of the sha256 hash of a constant string.
   136  func noteMixPukNonce() (res [24]byte) {
   137  	reasonHash := sha256.Sum256([]byte(libkb.DeriveReasonPUKStellarNoteShared))
   138  	copy(res[:], reasonHash[:])
   139  	return res
   140  }
   141  
   142  func NoteEncryptB64(mctx libkb.MetaContext, note stellar1.NoteContents, other *keybase1.UserVersion) (noteB64 string, err error) {
   143  	if len(note.Note) > libkb.MaxStellarPaymentNoteLength {
   144  		return "", fmt.Errorf("Note of size %d bytes exceeds the maximum length of %d bytes",
   145  			len(note.Note), libkb.MaxStellarPaymentNoteLength)
   146  	}
   147  	obj, err := noteEncrypt(mctx, note, other)
   148  	if err != nil {
   149  		return "", err
   150  	}
   151  	pack, err := msgpack.Encode(obj)
   152  	if err != nil {
   153  		return "", err
   154  	}
   155  	noteB64 = base64.StdEncoding.EncodeToString(pack)
   156  	if len(noteB64) > libkb.MaxStellarPaymentBoxedNoteLength {
   157  		return "", fmt.Errorf("Encrypted note of size %d bytes exceeds the maximum length of %d bytes",
   158  			len(noteB64), libkb.MaxStellarPaymentBoxedNoteLength)
   159  	}
   160  	return noteB64, nil
   161  }
   162  
   163  // noteEncrypt encrypts a note for the logged-in user as well as optionally for `other`.
   164  func noteEncrypt(mctx libkb.MetaContext, note stellar1.NoteContents, other *keybase1.UserVersion) (res stellar1.EncryptedNote, err error) {
   165  	nbs, err := noteSymmetricKey(mctx, other)
   166  	if err != nil {
   167  		return res, fmt.Errorf("error getting encryption key for note: %v", err)
   168  	}
   169  	if nbs.symmetricKey.IsZero() {
   170  		// This should never happen
   171  		return res, fmt.Errorf("unexpected zero key")
   172  	}
   173  	res, err = noteEncryptHelper(mctx.Ctx(), note, nbs.symmetricKey)
   174  	if err != nil {
   175  		return res, err
   176  	}
   177  	res.Sender = nbs.sender
   178  	res.Recipient = nbs.recipient
   179  	return res, nil
   180  }
   181  
   182  // noteEncryptHelper does the encryption part and returns a partially populated result.
   183  func noteEncryptHelper(ctx context.Context, note stellar1.NoteContents, symmetricKey libkb.NaclSecretBoxKey) (res stellar1.EncryptedNote, err error) {
   184  	// Msgpack
   185  	clearpack, err := msgpack.Encode(note)
   186  	if err != nil {
   187  		return res, err
   188  	}
   189  
   190  	// Secretbox
   191  	var nonce [libkb.NaclDHNonceSize]byte
   192  	nonce, err = libkb.RandomNaclDHNonce()
   193  	if err != nil {
   194  		return res, err
   195  	}
   196  	secbox := secretbox.Seal(nil, clearpack, &nonce, (*[libkb.NaclSecretBoxKeySize]byte)(&symmetricKey))
   197  
   198  	return stellar1.EncryptedNote{
   199  		V: 1,
   200  		E: secbox,
   201  		N: nonce,
   202  	}, nil
   203  }
   204  
   205  func NoteDecryptB64(mctx libkb.MetaContext, noteB64 string) (res stellar1.NoteContents, err error) {
   206  	pack, err := base64.StdEncoding.DecodeString(noteB64)
   207  	if err != nil {
   208  		return res, err
   209  	}
   210  	var obj stellar1.EncryptedNote
   211  	err = msgpack.Decode(&obj, pack)
   212  	if err != nil {
   213  		return res, err
   214  	}
   215  	return noteDecrypt(mctx, obj)
   216  }
   217  
   218  func noteDecrypt(mctx libkb.MetaContext, encNote stellar1.EncryptedNote) (res stellar1.NoteContents, err error) {
   219  	if encNote.V != 1 {
   220  		return res, fmt.Errorf("unsupported note version: %v", encNote.V)
   221  	}
   222  	symmetricKey, err := noteSymmetricKeyForDecryption(mctx, encNote)
   223  	if err != nil {
   224  		return res, err
   225  	}
   226  	return noteDecryptHelper(mctx.Ctx(), encNote, symmetricKey)
   227  }
   228  
   229  func noteDecryptHelper(ctx context.Context, encNote stellar1.EncryptedNote, symmetricKey libkb.NaclSecretBoxKey) (res stellar1.NoteContents, err error) {
   230  	// Secretbox
   231  	clearpack, ok := secretbox.Open(nil, encNote.E,
   232  		(*[libkb.NaclDHNonceSize]byte)(&encNote.N),
   233  		(*[libkb.NaclSecretBoxKeySize]byte)(&symmetricKey))
   234  	if !ok {
   235  		return res, errors.New("could not decrypt note secretbox")
   236  	}
   237  
   238  	// Msgpack
   239  	err = msgpack.Decode(&res, clearpack)
   240  	return res, err
   241  }