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 }