From 3535a4fbf894df33a88cf03bbb70f10a7974bbdc Mon Sep 17 00:00:00 2001 From: Oleg Kovalov Date: Sun, 14 Apr 2024 16:33:27 +0200 Subject: [PATCH] Rework v1.local (#9) --- common.go | 9 ++- common_test.go | 2 +- v1loc.go | 146 ++++++++++++++++++++++++++++++++----------------- v1loc_test.go | 6 +- v2loc.go | 2 +- 5 files changed, 107 insertions(+), 58 deletions(-) diff --git a/common.go b/common.go index 8750704..d7b3d26 100644 --- a/common.go +++ b/common.go @@ -2,6 +2,7 @@ package paseto import ( "bytes" + "crypto/subtle" "encoding/base64" "encoding/binary" "encoding/json" @@ -50,7 +51,7 @@ func fromBytes(data []byte, x any) error { *f = append(*f, data...) default: if err := json.Unmarshal(data, x); err != nil { - return fmt.Errorf("%v: %w", err, ErrDataUnmarshal) + return fmt.Errorf("%w: %v", ErrDataUnmarshal, err) } } return nil @@ -89,7 +90,7 @@ func splitToken(token, header string) ([]byte, []byte, error) { return payload, footer, nil } -func buildToken(header string, body, footer []byte) string { +func buildToken(header, body, footer []byte) string { size := len(header) + b64EncodedLen(len(body)) if len(footer) > 0 { size += 1 + b64EncodedLen(len(footer)) @@ -124,3 +125,7 @@ func b64Encode(dst, src []byte) { func b64EncodedLen(n int) int { return base64.RawURLEncoding.EncodedLen(n) } + +func constTimeEq(x, y int32) bool { + return subtle.ConstantTimeEq(x, y) == 1 +} diff --git a/common_test.go b/common_test.go index 9cd37b3..3bada53 100644 --- a/common_test.go +++ b/common_test.go @@ -43,7 +43,7 @@ func BenchmarkPAE(b *testing.B) { var footerBytes []byte pieces := [][]byte{ - []byte(v1LocHeader), + []byte(v1locHeader), nonce[:], encryptedPayload[:], footerBytes, diff --git a/v1loc.go b/v1loc.go index ea01e39..30f3413 100644 --- a/v1loc.go +++ b/v1loc.go @@ -6,27 +6,23 @@ import ( "crypto/hmac" "crypto/rand" "crypto/sha512" + "errors" "fmt" "io" + "strings" "golang.org/x/crypto/hkdf" ) const ( - v1LocHeader = "v1.local." - v1LocNonceSize = 32 - v1LocNonceHalf = v1LocNonceSize / 2 - v1LocMacSize = 48 // const for crypty.SHA384.Size() + v1locHeader = "v1.local." + v1locKey = 32 + v1locNonce = 32 + v1locNonceH = v1locNonce / 2 + v1locMac = 48 // const for crypto.SHA384.Size() ) func V1Encrypt(key []byte, payload, footer any, randBytes []byte) (string, error) { - if randBytes == nil { - randBytes = make([]byte, v1LocNonceSize) - if _, err := io.ReadFull(rand.Reader, randBytes); err != nil { - return "", fmt.Errorf("read from crypto/rand.Reader: %w", err) - } - } - payloadBytes, err := toBytes(payload) if err != nil { return "", fmt.Errorf("encode payload: %w", err) @@ -37,76 +33,124 @@ func V1Encrypt(key []byte, payload, footer any, randBytes []byte) (string, error return "", fmt.Errorf("encode footer: %w", err) } - macN := hmac.New(sha512.New384, randBytes) - if _, err := macN.Write(payloadBytes); err != nil { - return "", fmt.Errorf("hash payload: %w", err) + m := payloadBytes + k := key + f := footerBytes + + // step 1. + if !constTimeEq(int32(len(k)), v1locKey) { + return "", errors.New("bad key") } - nonce := macN.Sum(nil)[:v1LocNonceSize] - encKey, authKey, err := v1locSplitKey(key, nonce[:v1LocNonceHalf]) + // step 2. + h := []byte(v1locHeader) + + // step 3. + b := randBytes + if b == nil { + b = make([]byte, v1locNonce) + if _, err := io.ReadFull(rand.Reader, b); err != nil { + return "", fmt.Errorf("read from crypto/rand.Reader: %w", err) + } + } + + // step 4. + hash := hmac.New(sha512.New384, b) + hash.Write(m) + n := hash.Sum(nil)[:v1locNonce] + + // step 5. + ek, ak, err := v1locSplitKey(k, n[:v1locNonceH]) if err != nil { return "", fmt.Errorf("create enc and auth keys: %w", err) } - block, err := aes.NewCipher(encKey) + // step 6. + block, err := aes.NewCipher(ek) if err != nil { return "", fmt.Errorf("create aes cipher: %w", err) } + c := make([]byte, len(m)) + ciph := cipher.NewCTR(block, n[v1locNonceH:]) + ciph.XORKeyStream(c, m) - encryptedPayload := make([]byte, len(payloadBytes)) - cipher.NewCTR(block, nonce[v1LocNonceHalf:]). - XORKeyStream(encryptedPayload, payloadBytes) + // step 7. + preAuth := pae(h, n, c, f) - h := hmac.New(sha512.New384, authKey) - if _, err := h.Write(pae([]byte(v1LocHeader), nonce, encryptedPayload, footerBytes)); err != nil { - return "", fmt.Errorf("create signature: %w", err) - } - mac := h.Sum(nil) + // step 8. + hasher := hmac.New(sha512.New384, ak) + hasher.Write(preAuth) + t := hasher.Sum(nil) - body := make([]byte, 0, len(nonce)+len(encryptedPayload)+len(mac)) - body = append(body, nonce...) - body = append(body, encryptedPayload...) - body = append(body, mac...) + // step 9. + body := make([]byte, 0, len(n)+len(c)+len(t)) + body = append(body, n...) + body = append(body, c...) + body = append(body, t...) - return buildToken(v1LocHeader, body, footerBytes), nil + return buildToken(h, body, f), nil } func V1Decrypt(token string, key []byte, payload, footer any) error { - data, footerBytes, err := splitToken(token, v1LocHeader) + // step 0. + k := key + + // step 1. + if !constTimeEq(int32(len(k)), v1locKey) { + return errors.New("bad key") + } + + // step 2. + // TODO: ? + + // step 3. + if !strings.HasPrefix(token, v1locHeader) { + return ErrIncorrectTokenFormat + } + h := []byte(v1locHeader) + + // step 4. + data, footerBytes, err := splitToken(token, v1locHeader) if err != nil { return fmt.Errorf("decode token: %w", err) } - if len(data) < v1LocNonceSize+v1LocMacSize { + if len(data) < v1locNonce+v1locMac { return ErrIncorrectTokenFormat } + f := footerBytes - pivot := len(data) - v1LocMacSize - nonce := data[:v1LocNonceSize] - encryptedPayload, mac := data[v1LocNonceSize:pivot], data[pivot:] + pivot := len(data) - v1locMac + n := data[:v1locNonce] + c, t := data[v1locNonce:pivot], data[pivot:] - encKey, authKey, err := v1locSplitKey(key, nonce[:v1LocNonceHalf]) + // step 5. + ek, ak, err := v1locSplitKey(k, n[:v1locNonceH]) if err != nil { return fmt.Errorf("create enc and auth keys: %w", err) } - body := pae([]byte(v1LocHeader), nonce, encryptedPayload, footerBytes) - h := hmac.New(sha512.New384, authKey) - if _, err := h.Write(body); err != nil { - return fmt.Errorf("create signature: %w", err) - } + // step 6. + preAuth := pae(h, n, c, f) + + // step 7. + hasher := hmac.New(sha512.New384, ak) + hasher.Write(preAuth) + t2 := hasher.Sum(nil) - if !hmac.Equal(h.Sum(nil), mac) { + // step 8. + if !hmac.Equal(t2, t) { return ErrInvalidTokenAuth } - block, err := aes.NewCipher(encKey) + // step 9. + block, err := aes.NewCipher(ek) if err != nil { return fmt.Errorf("create aes cipher: %w", err) } - decryptedPayload := make([]byte, len(encryptedPayload)) - cipher.NewCTR(block, nonce[v1LocNonceHalf:]). - XORKeyStream(decryptedPayload, encryptedPayload) + decryptedPayload := make([]byte, len(c)) + ciph := cipher.NewCTR(block, n[v1locNonceH:]) + ciph.XORKeyStream(decryptedPayload, c) if payload != nil { if err := fromBytes(decryptedPayload, payload); err != nil { @@ -126,14 +170,14 @@ func v1locSplitKey(key, salt []byte) ([]byte, []byte, error) { eReader := hkdf.New(sha512.New384, key, salt, []byte("paseto-encryption-key")) aReader := hkdf.New(sha512.New384, key, salt, []byte("paseto-auth-key-for-aead")) - encKey := make([]byte, 32) - authKey := make([]byte, 32) + ek := make([]byte, 32) + ak := make([]byte, 32) - if _, err := io.ReadFull(eReader, encKey); err != nil { + if _, err := io.ReadFull(eReader, ek); err != nil { return nil, nil, err } - if _, err := io.ReadFull(aReader, authKey); err != nil { + if _, err := io.ReadFull(aReader, ak); err != nil { return nil, nil, err } - return encKey, authKey, nil + return ek, ak, nil } diff --git a/v1loc_test.go b/v1loc_test.go index 25f9398..80530ca 100644 --- a/v1loc_test.go +++ b/v1loc_test.go @@ -10,7 +10,7 @@ func TestV1Loc_Encrypt(t *testing.T) { testCases := loadGoldenFile("testdata/v1.json") for _, tc := range testCases.Tests { - if tc.Key == "" || !strings.HasPrefix(tc.Token, v1LocHeader) { + if tc.Key == "" || !strings.HasPrefix(tc.Token, v1locHeader) { continue } @@ -30,8 +30,8 @@ func TestV1Loc_Encrypt(t *testing.T) { func TestV1Loc_Decrypt(t *testing.T) { testCases := loadGoldenFile("testdata/v1.json") - for _, tc := range testCases.Tests[:] { - if tc.Key == "" || !strings.HasPrefix(tc.Token, v1LocHeader) { + for _, tc := range testCases.Tests { + if tc.Key == "" || !strings.HasPrefix(tc.Token, v1locHeader) { continue } diff --git a/v2loc.go b/v2loc.go index 5583959..ae7db90 100644 --- a/v2loc.go +++ b/v2loc.go @@ -56,7 +56,7 @@ func V2Encrypt(key []byte, payload, footer any, randBytes []byte) (string, error ) body := append(nonce, encryptedPayload...) - return buildToken(v2LocHeader, body, footerBytes), nil + return buildToken([]byte(v2LocHeader), body, footerBytes), nil } func V2Decrypt(token string, key []byte, payload, footer any) error {