github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/teams/seitan_v2.go (about)

     1  package teams
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"crypto/hmac"
     9  	"crypto/rand"
    10  	"crypto/sha512"
    11  	"encoding/base64"
    12  
    13  	"github.com/keybase/client/go/kbcrypto"
    14  	"github.com/keybase/client/go/libkb"
    15  	"github.com/keybase/client/go/msgpack"
    16  	"github.com/keybase/go-crypto/ed25519"
    17  	"golang.org/x/net/context"
    18  
    19  	keybase1 "github.com/keybase/client/go/protocol/keybase1"
    20  )
    21  
    22  // Seitan tokens v2 have a '+' as the sixth character. We use this
    23  // to distinguish from email invite tokens (and team names).
    24  const seitanEncodedIKeyV2PlusOffset = 6
    25  
    26  // "Invite Key Version 2"
    27  type SeitanIKeyV2 string
    28  
    29  func GenerateIKeyV2() (ikey SeitanIKeyV2, err error) {
    30  	str, err := generateIKey(SeitanEncodedIKeyLength, seitanEncodedIKeyV2PlusOffset)
    31  	if err != nil {
    32  		return ikey, err
    33  	}
    34  	return SeitanIKeyV2(str), err
    35  }
    36  
    37  // ParseIKeyV2FromString safely creates SeitanIKey value from
    38  // plaintext string. Only format is checked - any 18-character token
    39  // with '+' character at position 6 can be "Invite Key". Alphabet is
    40  // not checked, as it is only a hint for token generation and it can
    41  // change over time, but we assume that token length stays the same.
    42  func ParseIKeyV2FromString(token string) (ikey SeitanIKeyV2, err error) {
    43  	if len(token) != SeitanEncodedIKeyLength {
    44  		return ikey, fmt.Errorf("invalid token length: expected %d characters, got %d", SeitanEncodedIKeyLength, len(token))
    45  	}
    46  	if token[seitanEncodedIKeyV2PlusOffset] != '+' {
    47  		return ikey, fmt.Errorf("invalid token format: expected %dth character to be '+'", seitanEncodedIKeyV2PlusOffset+1)
    48  	}
    49  
    50  	return SeitanIKeyV2(strings.ToLower(token)), nil
    51  }
    52  
    53  func (ikey SeitanIKeyV2) String() string {
    54  	return strings.ToLower(string(ikey))
    55  }
    56  
    57  // "Stretched Invite Key"
    58  type SeitanSIKeyV2 [SeitanScryptKeylen]byte
    59  
    60  func (ikey SeitanIKeyV2) GenerateSIKey() (sikey SeitanSIKeyV2, err error) {
    61  	buf, err := generateSIKey(ikey.String())
    62  	if err != nil {
    63  		return sikey, err
    64  	}
    65  	copy(sikey[:], buf)
    66  	return sikey, nil
    67  }
    68  
    69  type SeitanVersionedInviteStagePayload struct {
    70  	Stage   string        `codec:"stage" json:"stage"`
    71  	Version SeitanVersion `codec:"version" json:"version"`
    72  }
    73  
    74  func NewSeitanInviteIDPayload(version SeitanVersion) SeitanVersionedInviteStagePayload {
    75  	return SeitanVersionedInviteStagePayload{Stage: "invite_id", Version: version}
    76  }
    77  
    78  func (sikey SeitanSIKeyV2) GenerateTeamInviteID() (id SCTeamInviteID, err error) {
    79  
    80  	payload, err := msgpack.Encode(NewSeitanInviteIDPayload(SeitanVersion2))
    81  	if err != nil {
    82  		return id, err
    83  	}
    84  	return generateTeamInviteID(sikey[:], payload)
    85  }
    86  
    87  func (sikey SeitanSIKeyV2) generateKeyPair() (key libkb.NaclSigningKeyPair, err error) {
    88  	type PrivateKeySeedPayload struct {
    89  		Stage   string        `codec:"stage" json:"stage"`
    90  		Version SeitanVersion `codec:"version" json:"version"`
    91  	}
    92  
    93  	payload, err := msgpack.Encode(PrivateKeySeedPayload{
    94  		Stage:   "eddsa",
    95  		Version: SeitanVersion2,
    96  	})
    97  	if err != nil {
    98  		return key, err
    99  	}
   100  
   101  	mac := hmac.New(sha512.New, sikey[:])
   102  	_, err = mac.Write(payload)
   103  	if err != nil {
   104  		return key, err
   105  	}
   106  
   107  	seed := mac.Sum(nil)
   108  	seed = seed[0:32]
   109  	pub, priv, err := ed25519.GenerateKey(bytes.NewBuffer(seed))
   110  	if err != nil {
   111  		return key, err
   112  	}
   113  
   114  	copy(key.Public[:], pub[:])
   115  	key.Private = &kbcrypto.NaclSigningKeyPrivate{}
   116  	copy(key.Private[:], priv[:])
   117  	return key, nil
   118  }
   119  
   120  func (sikey SeitanSIKeyV2) generatePackedEncryptedKeyWithSecretKey(secretKey keybase1.Bytes32, gen keybase1.PerTeamKeyGeneration, nonce keybase1.BoxNonce, label keybase1.SeitanKeyLabel) (pkey SeitanPKey, encoded string, err error) {
   121  
   122  	keyPair, err := sikey.generateKeyPair()
   123  	if err != nil {
   124  		return pkey, encoded, err
   125  	}
   126  
   127  	var keyAndLabel keybase1.SeitanKeyAndLabelVersion2
   128  	keyAndLabel.K = keybase1.SeitanPubKey(keyPair.GetKID().String())
   129  	keyAndLabel.L = label
   130  
   131  	packedKeyAndLabel, err := msgpack.Encode(keybase1.NewSeitanKeyAndLabelWithV2(keyAndLabel))
   132  	if err != nil {
   133  		return pkey, encoded, err
   134  	}
   135  	return packAndEncryptKeyWithSecretKey(secretKey, gen, nonce, packedKeyAndLabel, SeitanVersion2)
   136  }
   137  
   138  func (sikey SeitanSIKeyV2) GeneratePackedEncryptedKey(ctx context.Context, team *Team, label keybase1.SeitanKeyLabel) (pkey SeitanPKey, encoded string, err error) {
   139  	appKey, err := team.SeitanInviteTokenKeyLatest(ctx)
   140  	if err != nil {
   141  		return pkey, encoded, err
   142  	}
   143  
   144  	var nonce keybase1.BoxNonce
   145  	if _, err = rand.Read(nonce[:]); err != nil {
   146  		return pkey, encoded, err
   147  	}
   148  
   149  	return sikey.generatePackedEncryptedKeyWithSecretKey(appKey.Key, appKey.KeyGeneration, nonce, label)
   150  }
   151  
   152  // "Signature"
   153  type SeitanSig kbcrypto.NaclSignature
   154  type SeitanPubKey kbcrypto.NaclSigningKeyPublic
   155  
   156  func GenerateSeitanSignatureMessage(uid keybase1.UID, eldestSeqno keybase1.Seqno, inviteID SCTeamInviteID, time keybase1.Time) (payload []byte, err error) {
   157  	type SigPayload struct {
   158  		Stage       string         `codec:"stage" json:"stage"`
   159  		UID         keybase1.UID   `codec:"uid" json:"uid"`
   160  		EldestSeqno keybase1.Seqno `codec:"eldest_seqno" json:"eldest_seqno"`
   161  		CTime       keybase1.Time  `codec:"ctime" json:"ctime"`
   162  		InviteID    SCTeamInviteID `codec:"invite_id" json:"invite_id"`
   163  		Version     SeitanVersion  `codec:"version" json:"version"`
   164  	}
   165  
   166  	payload, err = msgpack.Encode(SigPayload{
   167  		Stage:       "accept",
   168  		Version:     SeitanVersion2,
   169  		InviteID:    inviteID,
   170  		UID:         uid,
   171  		EldestSeqno: eldestSeqno,
   172  		CTime:       time,
   173  	})
   174  	return payload, err
   175  }
   176  
   177  func VerifySeitanSignatureMessage(pubKey SeitanPubKey, msg []byte, sig SeitanSig) error {
   178  	naclsig := kbcrypto.NaclSignature(sig)
   179  	valid := kbcrypto.NaclSigningKeyPublic(pubKey).Verify(msg, naclsig)
   180  	if !valid {
   181  		return libkb.KeyCannotVerifyError{}
   182  	}
   183  	return nil
   184  }
   185  
   186  func ImportSeitanPubKey(keyString keybase1.SeitanPubKey) (pubKey SeitanPubKey, err error) {
   187  	keypair, err := libkb.ImportNaclSigningKeyPairFromHex(string(keyString))
   188  	if err != nil {
   189  		return pubKey, err
   190  	}
   191  	return SeitanPubKey(keypair.Public), nil
   192  }
   193  
   194  func (sikey SeitanSIKeyV2) GenerateSignature(uid keybase1.UID, eldestSeqno keybase1.Seqno, inviteID SCTeamInviteID, time keybase1.Time) (sig SeitanSig, encoded string, err error) {
   195  	payload, err := GenerateSeitanSignatureMessage(uid, eldestSeqno, inviteID, time)
   196  	if err != nil {
   197  		return sig, encoded, err
   198  	}
   199  
   200  	keyPair, err := sikey.generateKeyPair()
   201  	if err != nil {
   202  		return sig, encoded, err
   203  	}
   204  
   205  	sig = SeitanSig(keyPair.Private.Sign(payload))
   206  	encoded = base64.StdEncoding.EncodeToString(sig[:])
   207  	return sig, encoded, nil
   208  }