github.com/zntrio/harp/v2@v2.0.9/pkg/sdk/security/crypto/paseto/v4/helpers.go (about) 1 // Licensed to Elasticsearch B.V. under one or more contributor 2 // license agreements. See the NOTICE file distributed with 3 // this work for additional information regarding copyright 4 // ownership. Elasticsearch B.V. licenses this file to you under 5 // the Apache License, Version 2.0 (the "License"); you may 6 // not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, 12 // software distributed under the License is distributed on an 13 // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 // KIND, either express or implied. See the License for the 15 // specific language governing permissions and limitations 16 // under the License. 17 18 package v4 19 20 import ( 21 "bytes" 22 "crypto/ed25519" 23 "encoding/base64" 24 "encoding/binary" 25 "errors" 26 "fmt" 27 "io" 28 29 "github.com/zntrio/harp/v2/pkg/sdk/security" 30 31 "golang.org/x/crypto/blake2b" 32 "golang.org/x/crypto/chacha20" 33 ) 34 35 const ( 36 // KeyLength is the requested encryption key size. 37 KeyLength = 32 38 nonceLength = 32 39 macLength = 32 40 encryptionKDFLength = 56 41 authenticationKeyLength = 32 42 v4LocalPrefix = "v4.local." 43 v4PublicPrefix = "v4.public." 44 ) 45 46 // PASETO v4 symmetric encryption primitive. 47 // https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md#encrypt 48 func Encrypt(r io.Reader, key, m []byte, f, i string) ([]byte, error) { 49 // Create random seed 50 var n [nonceLength]byte 51 if _, err := io.ReadFull(r, n[:]); err != nil { 52 return nil, fmt.Errorf("paseto: unable to generate random seed: %w", err) 53 } 54 55 // Delegate to primitive 56 return encrypt(key, n[:], m, f, i) 57 } 58 59 // PASETO v4 symmetric decryption primitive 60 // https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md#decrypt 61 func Decrypt(key, input []byte, f, i string) ([]byte, error) { 62 // Check arguments 63 if key == nil { 64 return nil, errors.New("paseto: key is nil") 65 } 66 if len(key) != KeyLength { 67 return nil, fmt.Errorf("paseto: invalid key length, it must be %d bytes long", KeyLength) 68 } 69 if input == nil { 70 return nil, errors.New("paseto: input is nil") 71 } 72 73 // Check token header 74 if !bytes.HasPrefix(input, []byte(v4LocalPrefix)) { 75 return nil, errors.New("paseto: invalid token") 76 } 77 78 // Trim prefix 79 input = input[len(v4LocalPrefix):] 80 81 // Check footer usage 82 if f != "" { 83 // Split the footer and the body 84 parts := bytes.SplitN(input, []byte("."), 2) 85 if len(parts) != 2 { 86 return nil, errors.New("paseto: invalid token, footer is missing but expected") 87 } 88 89 // Decode footer 90 footer := make([]byte, base64.RawURLEncoding.DecodedLen(len(parts[1]))) 91 if _, err := base64.RawURLEncoding.Decode(footer, parts[1]); err != nil { 92 return nil, fmt.Errorf("paseto: invalid token, footer has invalid encoding: %w", err) 93 } 94 95 // Compare footer 96 if !security.SecureCompare([]byte(f), footer) { 97 return nil, errors.New("paseto: invalid token, footer mismatch") 98 } 99 100 // Continue without footer 101 input = parts[0] 102 } 103 104 // Decode token 105 raw := make([]byte, base64.RawURLEncoding.DecodedLen(len(input))) 106 if _, err := base64.RawURLEncoding.Decode(raw, input); err != nil { 107 return nil, fmt.Errorf("paseto: invalid token body: %w", err) 108 } 109 110 // Extract components 111 n := raw[:nonceLength] 112 t := raw[len(raw)-macLength:] 113 c := raw[macLength : len(raw)-macLength] 114 115 // Derive keys from seed and secret key 116 ek, n2, ak, err := kdf(key, n) 117 if err != nil { 118 return nil, fmt.Errorf("paseto: unable to derive keys from seed: %w", err) 119 } 120 121 // Compute MAC 122 t2, err := mac(ak, v4LocalPrefix, n, c, f, i) 123 if err != nil { 124 return nil, fmt.Errorf("paseto: unable to compute MAC: %w", err) 125 } 126 127 // Time-constant compare MAC 128 if !security.SecureCompare(t, t2) { 129 return nil, errors.New("paseto: invalid pre-authentication header") 130 } 131 132 // Prepare XChaCha20 stream cipher 133 ciph, err := chacha20.NewUnauthenticatedCipher(ek, n2) 134 if err != nil { 135 return nil, fmt.Errorf("paseto: unable to initialize XChaCha20 cipher: %w", err) 136 } 137 138 // Encrypt the payload 139 m := make([]byte, len(c)) 140 ciph.XORKeyStream(m, c) 141 142 // No error 143 return m, nil 144 } 145 146 // PASETO v4 public signature primitive. 147 // https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md#sign 148 func Sign(m []byte, sk ed25519.PrivateKey, f, i string) ([]byte, error) { 149 // Compute protected content 150 m2, err := pae([]byte(v4PublicPrefix), m, []byte(f), []byte(i)) 151 if err != nil { 152 return nil, fmt.Errorf("unable to prepare protected content: %w", err) 153 } 154 155 // Sign protected content 156 sig := ed25519.Sign(sk, m2) 157 158 // Prepare content 159 body := append([]byte{}, m...) 160 body = append(body, sig...) 161 162 // Encode body as RawURLBase64 163 encodedBody := make([]byte, base64.RawURLEncoding.EncodedLen(len(body))) 164 base64.RawURLEncoding.Encode(encodedBody, body) 165 166 // Assemble final token 167 final := append([]byte(v4PublicPrefix), encodedBody...) 168 if f != "" { 169 // Encode footer as RawURLBase64 170 encodedFooter := make([]byte, base64.RawURLEncoding.EncodedLen(len(f))) 171 base64.RawURLEncoding.Encode(encodedFooter, []byte(f)) 172 173 // Assemble body and footer 174 final = append(final, append([]byte("."), encodedFooter...)...) 175 } 176 177 // No error 178 return final, nil 179 } 180 181 // PASETO v4 signature verification primitive. 182 // https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md#verify 183 func Verify(sm []byte, pk ed25519.PublicKey, f, i string) ([]byte, error) { 184 // Check token header 185 if !bytes.HasPrefix(sm, []byte(v4PublicPrefix)) { 186 return nil, errors.New("paseto: invalid token") 187 } 188 189 // Trim prefix 190 sm = sm[len(v4PublicPrefix):] 191 192 // Check footer usage 193 if f != "" { 194 // Split the footer and the body 195 parts := bytes.SplitN(sm, []byte("."), 2) 196 if len(parts) != 2 { 197 return nil, errors.New("paseto: invalid token, footer is missing but expected") 198 } 199 200 // Decode footer 201 footer := make([]byte, base64.RawURLEncoding.DecodedLen(len(parts[1]))) 202 if _, err := base64.RawURLEncoding.Decode(footer, parts[1]); err != nil { 203 return nil, fmt.Errorf("paseto: invalid token, footer has invalid encoding: %w", err) 204 } 205 206 // Compare footer 207 if !security.SecureCompare([]byte(f), footer) { 208 return nil, errors.New("paseto: invalid token, footer mismatch") 209 } 210 211 // Continue without footer 212 sm = parts[0] 213 } 214 215 // Decode token 216 raw := make([]byte, base64.RawURLEncoding.DecodedLen(len(sm))) 217 if _, err := base64.RawURLEncoding.Decode(raw, sm); err != nil { 218 return nil, fmt.Errorf("paseto: invalid token body: %w", err) 219 } 220 221 // Extract components 222 m := raw[:len(raw)-ed25519.SignatureSize] 223 s := raw[len(raw)-ed25519.SignatureSize:] 224 225 // Compute protected content 226 m2, err := pae([]byte(v4PublicPrefix), m, []byte(f), []byte(i)) 227 if err != nil { 228 return nil, fmt.Errorf("unable to prepare protected content: %w", err) 229 } 230 231 // Check signature 232 if !ed25519.Verify(pk, m2, s) { 233 return nil, errors.New("paseto: invalid token signature") 234 } 235 236 // No error 237 return m, nil 238 } 239 240 // ----------------------------------------------------------------------------- 241 242 func encrypt(key, n, m []byte, f, i string) ([]byte, error) { 243 // Check arguments 244 if len(key) != KeyLength { 245 return nil, fmt.Errorf("paseto: invalid key length, it must be %d bytes long", KeyLength) 246 } 247 if len(n) != nonceLength { 248 return nil, fmt.Errorf("paseto: invalid nonce length, it must be %d bytes long", nonceLength) 249 } 250 251 // Derive keys from seed and secret key 252 ek, n2, ak, err := kdf(key, n) 253 if err != nil { 254 return nil, fmt.Errorf("paseto: unable to derive keys from seed: %w", err) 255 } 256 257 // Prepare XChaCha20 stream cipher (nonce > 24bytes => XChacha) 258 ciph, err := chacha20.NewUnauthenticatedCipher(ek, n2) 259 if err != nil { 260 return nil, fmt.Errorf("paseto: unable to initialize XChaCha20 cipher: %w", err) 261 } 262 263 // Encrypt the payload 264 c := make([]byte, len(m)) 265 ciph.XORKeyStream(c, m) 266 267 // Compute MAC 268 t, err := mac(ak, v4LocalPrefix, n, c, f, i) 269 if err != nil { 270 return nil, fmt.Errorf("paseto: unable to compute MAC: %w", err) 271 } 272 273 // Serialize final token 274 // h || base64url(n || c || t) 275 body := append([]byte{}, n...) 276 body = append(body, c...) 277 body = append(body, t...) 278 279 // Encode body as RawURLBase64 280 encodedBody := make([]byte, base64.RawURLEncoding.EncodedLen(len(body))) 281 base64.RawURLEncoding.Encode(encodedBody, body) 282 283 // Assemble final token 284 final := append([]byte(v4LocalPrefix), encodedBody...) 285 if f != "" { 286 // Encode footer as RawURLBase64 287 encodedFooter := make([]byte, base64.RawURLEncoding.EncodedLen(len(f))) 288 base64.RawURLEncoding.Encode(encodedFooter, []byte(f)) 289 290 // Assemble body and footer 291 final = append(final, append([]byte("."), encodedFooter...)...) 292 } 293 294 // No error 295 return final, nil 296 } 297 298 func kdf(key, n []byte) (ek, n2, ak []byte, err error) { 299 // Derive encryption key 300 encKDF, err := blake2b.New(encryptionKDFLength, key) 301 if err != nil { 302 return nil, nil, nil, fmt.Errorf("unable to initialize encryption kdf: %w", err) 303 } 304 305 // Domain separation (we use the same seed for 2 different purposes) 306 encKDF.Write([]byte("paseto-encryption-key")) 307 encKDF.Write(n) 308 tmp := encKDF.Sum(nil) 309 310 // Split encryption key (Ek) and nonce (n2) 311 ek = tmp[:KeyLength] 312 n2 = tmp[KeyLength:] 313 314 // Derive authentication key 315 authKDF, err := blake2b.New(authenticationKeyLength, key) 316 if err != nil { 317 return nil, nil, nil, fmt.Errorf("unable to initialize authentication kdf: %w", err) 318 } 319 320 // Domain separation (we use the same seed for 2 different purposes) 321 authKDF.Write([]byte("paseto-auth-key-for-aead")) 322 authKDF.Write(n) 323 ak = authKDF.Sum(nil) 324 325 // No error 326 return ek, n2, ak, nil 327 } 328 329 func mac(ak []byte, h string, n, c []byte, f, i string) ([]byte, error) { 330 // Compute pre-authentication message 331 preAuth, err := pae([]byte(h), n, c, []byte(f), []byte(i)) 332 if err != nil { 333 return nil, fmt.Errorf("unable to compute pre-authentication content: %w", err) 334 } 335 336 // Compute MAC 337 mac, err := blake2b.New(macLength, ak) 338 if err != nil { 339 return nil, fmt.Errorf("unable to in initialize MAC kdf: %w", err) 340 } 341 342 // Hash pre-authentication content 343 mac.Write(preAuth) 344 345 // No error 346 return mac.Sum(nil), nil 347 } 348 349 // https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Common.md#authentication-padding 350 func pae(pieces ...[]byte) ([]byte, error) { 351 output := &bytes.Buffer{} 352 353 // Encode piece count 354 count := len(pieces) 355 if err := binary.Write(output, binary.LittleEndian, uint64(count)); err != nil { 356 return nil, err 357 } 358 359 // For each element 360 for i := range pieces { 361 // Encode size 362 if err := binary.Write(output, binary.LittleEndian, uint64(len(pieces[i]))); err != nil { 363 return nil, err 364 } 365 366 // Encode data 367 if _, err := output.Write(pieces[i]); err != nil { 368 return nil, err 369 } 370 } 371 372 // No error 373 return output.Bytes(), nil 374 }