From 606df78fb1f952fb1829386174906adbb05a52ce Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Sun, 30 Jun 2024 23:05:00 -0400 Subject: [PATCH 1/7] Support for key length, Add benchmarks for EncryptCookie middleware --- docs/middleware/encryptcookie.md | 29 +- docs/whats_new.md | 4 + middleware/encryptcookie/config.go | 10 +- .../encryptcookie/encryptcookie_test.go | 400 +++++++++++++++++- middleware/encryptcookie/utils.go | 28 +- 5 files changed, 452 insertions(+), 19 deletions(-) diff --git a/docs/middleware/encryptcookie.md b/docs/middleware/encryptcookie.md index c153b6412d..309c9defc1 100644 --- a/docs/middleware/encryptcookie.md +++ b/docs/middleware/encryptcookie.md @@ -16,8 +16,8 @@ This middleware encrypts cookie values and not the cookie names. // Intitializes the middleware func New(config ...Config) fiber.Handler -// Returns a random 32 character long string -func GenerateKey() string +// Returns a random 16, 24, 32 bytes encoded string +func GenerateKey(length) string ``` ## Examples @@ -55,9 +55,9 @@ app.Post("/", func(c fiber.Ctx) error { ``` :::note -`Key` must be a 32 character string. It's used to encrypt the values, so make sure it is random and keep it secret. -You can run `openssl rand -base64 32` or call `encryptcookie.GenerateKey()` to create a random key for you. -Make sure not to set `Key` to `encryptcookie.GenerateKey()` because that will create a new key every run. +`Key` must be a 16, 24, or 32 bytes encoded string. It's used to encrypt the values, so make sure it is random and keep it secret. +For example, you can run `openssl rand -base64 32` or call `encryptcookie.GenerateKey(32)` to create a random key for you. +Make sure not to set `Key` to `encryptcookie.GenerateKey(32)` because that will create a new key every run of the application. ::: ## Config @@ -99,3 +99,22 @@ app.Use(csrf.New(csrf.Config{ CookieHTTPOnly: false, })) ``` + +## Encryption Algorithms +The default Encryptor and Decryptor functions use `AES-256-GCM` for encryption and decryption. If you need to use `AES-128` or `AES-192` instead, you can do so by changing the length of the key when calling `encryptcookie.GenerateKey(length)` or by providing a key of one of the following lengths: + +- AES-128 requires a 16-byte key. +- AES-192 requires a 24-byte key. +- AES-256 requires a 32-byte key. + +For example, to generate a key for AES-128: + +```go +key := encryptcookie.GenerateKey(16) +``` + +And for AES-192: + +```go +key := encryptcookie.GenerateKey(24) +``` \ No newline at end of file diff --git a/docs/whats_new.md b/docs/whats_new.md index c34ed4bf7b..934d94c7a6 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -275,6 +275,10 @@ We've updated several fields from a single string (containing comma-separated va We've added support for `zstd` compression on top of `gzip`, `deflate`, and `brotli`. +### EncryptCookie + +Added support for specifying Key length when using `encryptcookie.GenerateKey(length)`. This allows the user to generate keys compatible with `AES-128`, `AES-192`, and `AES-256` (Default). + ### Session :::caution diff --git a/middleware/encryptcookie/config.go b/middleware/encryptcookie/config.go index 23c63bcd14..ffa8e10c65 100644 --- a/middleware/encryptcookie/config.go +++ b/middleware/encryptcookie/config.go @@ -18,18 +18,19 @@ type Config struct { // Base64 encoded unique key to encode & decode cookies. // - // Required. Key length should be 32 characters. - // You may use `encryptcookie.GenerateKey()` to generate a new key. + // Required. Key length should be 16, 24, or 32 bytes when decoded + // if using the default EncryptCookie and DecryptCookie functions. + // You may use `encryptcookie.GenerateKey(length)` to generate a new key. Key string // Custom function to encrypt cookies. // - // Optional. Default: EncryptCookie + // Optional. Default: EncryptCookie (using AES-256-GCM) Encryptor func(decryptedString, key string) (string, error) // Custom function to decrypt cookies. // - // Optional. Default: DecryptCookie + // Optional. Default: DecryptCookie (using AES-256-GCM) Decryptor func(encryptedString, key string) (string, error) } @@ -52,7 +53,6 @@ func configDefault(config ...Config) Config { cfg = config[0] // Set default values - if cfg.Next == nil { cfg.Next = ConfigDefault.Next } diff --git a/middleware/encryptcookie/encryptcookie_test.go b/middleware/encryptcookie/encryptcookie_test.go index ee7ca9ae60..ac229e1328 100644 --- a/middleware/encryptcookie/encryptcookie_test.go +++ b/middleware/encryptcookie/encryptcookie_test.go @@ -3,6 +3,7 @@ package encryptcookie import ( "encoding/base64" "net/http/httptest" + "strconv" "testing" "github.com/gofiber/fiber/v3" @@ -10,7 +11,7 @@ import ( "github.com/valyala/fasthttp" ) -var testKey = GenerateKey() +var testKey = GenerateKey(32) func Test_Middleware_Encrypt_Cookie(t *testing.T) { t.Parallel() @@ -189,3 +190,400 @@ func Test_Encrypt_Cookie_Custom_Encryptor(t *testing.T) { require.Equal(t, 200, ctx.Response.StatusCode()) require.Equal(t, "value=SomeThing", string(ctx.Response.Body())) } + +func Test_GenerateKey(t *testing.T) { + t.Parallel() + + tests := []struct { + length int + }{ + {16}, + {24}, + {32}, + } + + decodeBase64 := func(t *testing.T, s string) []byte { + t.Helper() + data, err := base64.StdEncoding.DecodeString(s) + require.NoError(t, err) + return data + } + + for _, tt := range tests { + tt := tt + t.Run(strconv.Itoa(tt.length)+"length", func(t *testing.T) { + t.Parallel() + key := GenerateKey(tt.length) + decodedKey := decodeBase64(t, key) + require.Len(t, decodedKey, tt.length) + }) + } + + t.Run("Invalid Length", func(t *testing.T) { + require.Panics(t, func() { GenerateKey(10) }) + require.Panics(t, func() { GenerateKey(20) }) + }) +} + +func Benchmark_Middleware_Encrypt_Cookie(b *testing.B) { + app := fiber.New() + + app.Use(New(Config{ + Key: testKey, + })) + + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("value=" + c.Cookies("test")) + }) + app.Post("/", func(c fiber.Ctx) error { + c.Cookie(&fiber.Cookie{ + Name: "test", + Value: "SomeThing", + }) + return nil + }) + + h := app.Handler() + + b.Run("Empty Cookie", func(b *testing.B) { + for i := 0; i < b.N; i++ { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + } + }) + + b.Run("Invalid Cookie", func(b *testing.B) { + for i := 0; i < b.N; i++ { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.SetCookie("test", "Invalid") + h(ctx) + } + }) + + b.Run("Valid Cookie", func(b *testing.B) { + for i := 0; i < b.N; i++ { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodPost) + h(ctx) + } + }) +} + +func Benchmark_Encrypt_Cookie_Next(b *testing.B) { + app := fiber.New() + + app.Use(New(Config{ + Key: testKey, + Next: func(_ fiber.Ctx) bool { + return true + }, + })) + + app.Get("/", func(c fiber.Ctx) error { + c.Cookie(&fiber.Cookie{ + Name: "test", + Value: "SomeThing", + }) + return nil + }) + + h := app.Handler() + + b.Run("Encrypt Cookie Next", func(b *testing.B) { + for i := 0; i < b.N; i++ { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.SetRequestURI("/") + h(ctx) + } + }) +} + +func Benchmark_Encrypt_Cookie_Except(b *testing.B) { + app := fiber.New() + + app.Use(New(Config{ + Key: testKey, + Except: []string{ + "test1", + }, + })) + + app.Get("/", func(c fiber.Ctx) error { + c.Cookie(&fiber.Cookie{ + Name: "test1", + Value: "SomeThing", + }) + c.Cookie(&fiber.Cookie{ + Name: "test2", + Value: "SomeThing", + }) + + return nil + }) + + h := app.Handler() + + b.Run("Encrypt Cookie Except", func(b *testing.B) { + for i := 0; i < b.N; i++ { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + } + }) +} + +func Benchmark_Encrypt_Cookie_Custom_Encryptor(b *testing.B) { + app := fiber.New() + + app.Use(New(Config{ + Key: testKey, + Encryptor: func(decryptedString, _ string) (string, error) { + return base64.StdEncoding.EncodeToString([]byte(decryptedString)), nil + }, + Decryptor: func(encryptedString, _ string) (string, error) { + decodedBytes, err := base64.StdEncoding.DecodeString(encryptedString) + return string(decodedBytes), err + }, + })) + + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("value=" + c.Cookies("test")) + }) + app.Post("/", func(c fiber.Ctx) error { + c.Cookie(&fiber.Cookie{ + Name: "test", + Value: "SomeThing", + }) + + return nil + }) + + h := app.Handler() + + b.Run("Custom Encryptor Post", func(b *testing.B) { + for i := 0; i < b.N; i++ { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodPost) + h(ctx) + } + }) + + b.Run("Custom Encryptor Get", func(b *testing.B) { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodPost) + h(ctx) + encryptedCookie := fasthttp.Cookie{} + encryptedCookie.SetKey("test") + require.True(b, ctx.Response.Header.Cookie(&encryptedCookie), "Get cookie value") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.SetCookie("test", string(encryptedCookie.Value())) + h(ctx) + } + }) +} + +func Benchmark_Middleware_Encrypt_Cookie_Parallel(b *testing.B) { + app := fiber.New() + + app.Use(New(Config{ + Key: testKey, + })) + + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("value=" + c.Cookies("test")) + }) + app.Post("/", func(c fiber.Ctx) error { + c.Cookie(&fiber.Cookie{ + Name: "test", + Value: "SomeThing", + }) + return nil + }) + + h := app.Handler() + + b.Run("Empty Cookie Parallel", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + } + }) + }) + + b.Run("Invalid Cookie Parallel", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.SetCookie("test", "Invalid") + h(ctx) + } + }) + }) + + b.Run("Valid Cookie Parallel", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodPost) + h(ctx) + } + }) + }) +} +func Benchmark_Encrypt_Cookie_Next_Parallel(b *testing.B) { + app := fiber.New() + + app.Use(New(Config{ + Key: testKey, + Next: func(_ fiber.Ctx) bool { + return true + }, + })) + + app.Get("/", func(c fiber.Ctx) error { + c.Cookie(&fiber.Cookie{ + Name: "test", + Value: "SomeThing", + }) + return nil + }) + + h := app.Handler() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.SetRequestURI("/") + h(ctx) + } + }) +} + +func Benchmark_Encrypt_Cookie_Except_Parallel(b *testing.B) { + app := fiber.New() + + app.Use(New(Config{ + Key: testKey, + Except: []string{ + "test1", + }, + })) + + app.Get("/", func(c fiber.Ctx) error { + c.Cookie(&fiber.Cookie{ + Name: "test1", + Value: "SomeThing", + }) + c.Cookie(&fiber.Cookie{ + Name: "test2", + Value: "SomeThing", + }) + + return nil + }) + + h := app.Handler() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + } + }) +} + +func Benchmark_Encrypt_Cookie_Custom_Encryptor_Parallel(b *testing.B) { + app := fiber.New() + + app.Use(New(Config{ + Key: testKey, + Encryptor: func(decryptedString, _ string) (string, error) { + return base64.StdEncoding.EncodeToString([]byte(decryptedString)), nil + }, + Decryptor: func(encryptedString, _ string) (string, error) { + decodedBytes, err := base64.StdEncoding.DecodeString(encryptedString) + return string(decodedBytes), err + }, + })) + + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("value=" + c.Cookies("test")) + }) + app.Post("/", func(c fiber.Ctx) error { + c.Cookie(&fiber.Cookie{ + Name: "test", + Value: "SomeThing", + }) + + return nil + }) + + h := app.Handler() + + b.RunParallel(func(pb *testing.PB) { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodPost) + h(ctx) + encryptedCookie := fasthttp.Cookie{} + encryptedCookie.SetKey("test") + require.True(b, ctx.Response.Header.Cookie(&encryptedCookie), "Get cookie value") + + b.ResetTimer() + for pb.Next() { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.SetCookie("test", string(encryptedCookie.Value())) + h(ctx) + } + }) +} + +func Benchmark_GenerateKey(b *testing.B) { + tests := []struct { + length int + }{ + {16}, + {24}, + {32}, + } + + for _, tt := range tests { + b.Run(strconv.Itoa(tt.length), func(b *testing.B) { + for i := 0; i < b.N; i++ { + GenerateKey(tt.length) + } + }) + } +} + +func Benchmark_GenerateKey_Parallel(b *testing.B) { + tests := []struct { + length int + }{ + {16}, + {24}, + {32}, + } + + for _, tt := range tests { + b.Run(strconv.Itoa(tt.length), func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + GenerateKey(tt.length) + } + }) + }) + } +} diff --git a/middleware/encryptcookie/utils.go b/middleware/encryptcookie/utils.go index c35064d954..4897aa3211 100644 --- a/middleware/encryptcookie/utils.go +++ b/middleware/encryptcookie/utils.go @@ -17,6 +17,11 @@ func EncryptCookie(value, key string) (string, error) { return "", fmt.Errorf("failed to base64-decode key: %w", err) } + keyLen := len(keyDecoded) + if keyLen != 16 && keyLen != 24 && keyLen != 32 { + return "", errors.New("encryption key must be 16, 24, or 32 bytes") + } + block, err := aes.NewCipher(keyDecoded) if err != nil { return "", fmt.Errorf("failed to create AES cipher: %w", err) @@ -29,11 +34,10 @@ func EncryptCookie(value, key string) (string, error) { nonce := make([]byte, gcm.NonceSize()) if _, err = io.ReadFull(rand.Reader, nonce); err != nil { - return "", fmt.Errorf("failed to read: %w", err) + return "", fmt.Errorf("failed to read nonce: %w", err) } ciphertext := gcm.Seal(nonce, nonce, []byte(value), nil) - return base64.StdEncoding.EncodeToString(ciphertext), nil } @@ -43,6 +47,12 @@ func DecryptCookie(value, key string) (string, error) { if err != nil { return "", fmt.Errorf("failed to base64-decode key: %w", err) } + + keyLen := len(keyDecoded) + if keyLen != 16 && keyLen != 24 && keyLen != 32 { + return "", errors.New("encryption key must be 16, 24, or 32 bytes") + } + enc, err := base64.StdEncoding.DecodeString(value) if err != nil { return "", fmt.Errorf("failed to base64-decode value: %w", err) @@ -65,7 +75,6 @@ func DecryptCookie(value, key string) (string, error) { } nonce, ciphertext := enc[:nonceSize], enc[nonceSize:] - plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) if err != nil { return "", fmt.Errorf("failed to decrypt ciphertext: %w", err) @@ -75,15 +84,18 @@ func DecryptCookie(value, key string) (string, error) { } // GenerateKey Generates an encryption key -func GenerateKey() string { - const keyLen = 32 - ret := make([]byte, keyLen) +func GenerateKey(length int) string { + if length != 16 && length != 24 && length != 32 { + panic("encryption key length must be 16, 24, or 32 bytes") + } + + key := make([]byte, length) - if _, err := rand.Read(ret); err != nil { + if _, err := rand.Read(key); err != nil { panic(err) } - return base64.StdEncoding.EncodeToString(ret) + return base64.StdEncoding.EncodeToString(key) } // Check given cookie key is disabled for encryption or not From 146c76ffde43f960f0e4589af0b9f07be8758769 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Sun, 30 Jun 2024 23:09:22 -0400 Subject: [PATCH 2/7] Format tests --- middleware/encryptcookie/encryptcookie_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/middleware/encryptcookie/encryptcookie_test.go b/middleware/encryptcookie/encryptcookie_test.go index ac229e1328..ff6158fb6a 100644 --- a/middleware/encryptcookie/encryptcookie_test.go +++ b/middleware/encryptcookie/encryptcookie_test.go @@ -440,6 +440,7 @@ func Benchmark_Middleware_Encrypt_Cookie_Parallel(b *testing.B) { }) }) } + func Benchmark_Encrypt_Cookie_Next_Parallel(b *testing.B) { app := fiber.New() From a4d135c3a92bb05fafdfa4d799d014625a3501d0 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Sun, 30 Jun 2024 23:28:19 -0400 Subject: [PATCH 3/7] Add tests for panics and key check in Encryptor and Decryptor functions --- .../encryptcookie/encryptcookie_test.go | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/middleware/encryptcookie/encryptcookie_test.go b/middleware/encryptcookie/encryptcookie_test.go index ff6158fb6a..a7103fc0f1 100644 --- a/middleware/encryptcookie/encryptcookie_test.go +++ b/middleware/encryptcookie/encryptcookie_test.go @@ -1,6 +1,7 @@ package encryptcookie import ( + "crypto/rand" "encoding/base64" "net/http/httptest" "strconv" @@ -13,6 +14,63 @@ import ( var testKey = GenerateKey(32) +func Test_Middleware_Panics(t *testing.T) { + t.Parallel() + + t.Run("Empty Key", func(t *testing.T) { + t.Parallel() + app := fiber.New() + require.Panics(t, func() { + app.Use(New(Config{ + Key: "", + })) + }) + }) + + t.Run("Invalid Key", func(t *testing.T) { + t.Parallel() + require.Panics(t, func() { + GenerateKey(11) + }) + }) +} + +func Test_Middleware_InvalidKeys(t *testing.T) { + t.Parallel() + tests := []struct { + length int + }{ + {11}, + {25}, + {60}, + } + + for _, tt := range tests { + tt := tt + t.Run(strconv.Itoa(tt.length)+"_length_encrypt", func(t *testing.T) { + t.Parallel() + key := make([]byte, tt.length) + _, err := rand.Read(key) + require.NoError(t, err) + keyString := base64.StdEncoding.EncodeToString(key) + + _, err = EncryptCookie("SomeThing", keyString) + require.Error(t, err) + }) + + t.Run(strconv.Itoa(tt.length)+"_length_decrypt", func(t *testing.T) { + t.Parallel() + key := make([]byte, tt.length) + _, err := rand.Read(key) + require.NoError(t, err) + keyString := base64.StdEncoding.EncodeToString(key) + + _, err = DecryptCookie("SomeThing", keyString) + require.Error(t, err) + }) + } +} + func Test_Middleware_Encrypt_Cookie(t *testing.T) { t.Parallel() app := fiber.New() @@ -211,7 +269,7 @@ func Test_GenerateKey(t *testing.T) { for _, tt := range tests { tt := tt - t.Run(strconv.Itoa(tt.length)+"length", func(t *testing.T) { + t.Run(strconv.Itoa(tt.length)+"_length", func(t *testing.T) { t.Parallel() key := GenerateKey(tt.length) decodedKey := decodeBase64(t, key) From c246453d75aa719dc83ac62563fb71f0173f2336 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Sun, 30 Jun 2024 23:39:32 -0400 Subject: [PATCH 4/7] Add tests for base64 decoding errors --- .../encryptcookie/encryptcookie_test.go | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/middleware/encryptcookie/encryptcookie_test.go b/middleware/encryptcookie/encryptcookie_test.go index a7103fc0f1..881fbcde1a 100644 --- a/middleware/encryptcookie/encryptcookie_test.go +++ b/middleware/encryptcookie/encryptcookie_test.go @@ -71,6 +71,32 @@ func Test_Middleware_InvalidKeys(t *testing.T) { } } +func Test_Middleware_InvalidBase64(t *testing.T) { + t.Parallel() + invalidBase64 := "invalid-base64-string-!@#" + + t.Run("encryptor", func(t *testing.T) { + t.Parallel() + _, err := EncryptCookie("SomeText", invalidBase64) + require.Error(t, err) + require.ErrorContains(t, err, "failed to base64-decode key") + }) + + t.Run("decryptor_key", func(t *testing.T) { + t.Parallel() + _, err := DecryptCookie("SomeText", invalidBase64) + require.Error(t, err) + require.ErrorContains(t, err, "failed to base64-decode key") + }) + + t.Run("decryptor_value", func(t *testing.T) { + t.Parallel() + _, err := DecryptCookie(invalidBase64, GenerateKey(32)) + require.Error(t, err) + require.ErrorContains(t, err, "failed to base64-decode value") + }) +} + func Test_Middleware_Encrypt_Cookie(t *testing.T) { t.Parallel() app := fiber.New() From e92742a061cf32a5b867d80da674a26d92abbc5d Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Mon, 1 Jul 2024 09:33:04 -0400 Subject: [PATCH 5/7] Update docs/middleware/encryptcookie.md Co-authored-by: Jason McNeil --- docs/middleware/encryptcookie.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/middleware/encryptcookie.md b/docs/middleware/encryptcookie.md index 309c9defc1..d7e48f37ad 100644 --- a/docs/middleware/encryptcookie.md +++ b/docs/middleware/encryptcookie.md @@ -16,7 +16,9 @@ This middleware encrypts cookie values and not the cookie names. // Intitializes the middleware func New(config ...Config) fiber.Handler -// Returns a random 16, 24, 32 bytes encoded string +// GenerateKey returns a random string of 16, 24, or 32 bytes. +// The length of the key determines the AES encryption algorithm used: +// 16 bytes for AES-128, 24 bytes for AES-192, and 32 bytes for AES-256-GCM. func GenerateKey(length) string ``` From 8a43e5841bf62e51eeebe0352201a56784c9fb9e Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Mon, 1 Jul 2024 09:44:36 -0400 Subject: [PATCH 6/7] Update middleware/encryptcookie/utils.go Co-authored-by: Jason McNeil --- middleware/encryptcookie/utils.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/middleware/encryptcookie/utils.go b/middleware/encryptcookie/utils.go index 4897aa3211..e9bc41df4d 100644 --- a/middleware/encryptcookie/utils.go +++ b/middleware/encryptcookie/utils.go @@ -83,7 +83,9 @@ func DecryptCookie(value, key string) (string, error) { return string(plaintext), nil } -// GenerateKey Generates an encryption key +// GenerateKey returns a random string of 16, 24, or 32 bytes. +// The length of the key determines the AES encryption algorithm used: +// 16 bytes for AES-128, 24 bytes for AES-192, and 32 bytes for AES-256-GCM. func GenerateKey(length int) string { if length != 16 && length != 24 && length != 32 { panic("encryption key length must be 16, 24, or 32 bytes") From 05ce592394bdedcfbed32c0da1722718b0ca583d Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez Date: Tue, 2 Jul 2024 09:08:48 -0400 Subject: [PATCH 7/7] Add suggestions from code review --- .github/workflows/test.yml | 5 +---- docs/middleware/encryptcookie.md | 10 +++++----- middleware/encryptcookie/config.go | 4 ++-- middleware/encryptcookie/encryptcookie_test.go | 14 ++++++++++++-- middleware/encryptcookie/utils.go | 8 +++++--- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0740d1b0d1..e6706493a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,9 +44,6 @@ jobs: slug: gofiber/fiber repeated: - strategy: - matrix: - go-version: [stable] runs-on: ubuntu-latest steps: - name: Fetch Repository @@ -55,7 +52,7 @@ jobs: - name: Install Go uses: actions/setup-go@v5 with: - go-version: ${{ matrix.go-version }} + go-version: stable - name: Test run: go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=15 -shuffle=on diff --git a/docs/middleware/encryptcookie.md b/docs/middleware/encryptcookie.md index d7e48f37ad..3054becb84 100644 --- a/docs/middleware/encryptcookie.md +++ b/docs/middleware/encryptcookie.md @@ -19,7 +19,7 @@ func New(config ...Config) fiber.Handler // GenerateKey returns a random string of 16, 24, or 32 bytes. // The length of the key determines the AES encryption algorithm used: // 16 bytes for AES-128, 24 bytes for AES-192, and 32 bytes for AES-256-GCM. -func GenerateKey(length) string +func GenerateKey(length int) string ``` ## Examples @@ -57,9 +57,8 @@ app.Post("/", func(c fiber.Ctx) error { ``` :::note -`Key` must be a 16, 24, or 32 bytes encoded string. It's used to encrypt the values, so make sure it is random and keep it secret. -For example, you can run `openssl rand -base64 32` or call `encryptcookie.GenerateKey(32)` to create a random key for you. -Make sure not to set `Key` to `encryptcookie.GenerateKey(32)` because that will create a new key every run of the application. +The `Key` parameter requires an encoded string of 16, 24, or 32 bytes for encryption, corresponding to AES-128, AES-192, and AES-256-GCM standards, respectively. Ensure the key is randomly generated and securely stored. +To generate a 32 char key, use `openssl rand -base64 32` or `encryptcookie.GenerateKey(32)`. Avoid dynamically generating a new `Key` with `encryptcookie.GenerateKey(32)` at each application startup to prevent rendering previously encrypted data inaccessible. ::: ## Config @@ -85,6 +84,7 @@ var ConfigDefault = Config{ ``` ## Usage With Other Middlewares That Reads Or Modify Cookies + Place the `encryptcookie` middleware before any other middleware that reads or modifies cookies. For example, if you are using the CSRF middleware, ensure that the `encryptcookie` middleware is placed before it. Failure to do so may prevent the CSRF middleware from reading the encrypted cookie. You may also choose to exclude certain cookies from encryption. For instance, if you are using the `CSRF` middleware with a frontend framework like Angular, and the framework reads the token from a cookie, you should exclude that cookie from encryption. This can be achieved by adding the cookie name to the Except array in the configuration: @@ -103,8 +103,8 @@ app.Use(csrf.New(csrf.Config{ ``` ## Encryption Algorithms -The default Encryptor and Decryptor functions use `AES-256-GCM` for encryption and decryption. If you need to use `AES-128` or `AES-192` instead, you can do so by changing the length of the key when calling `encryptcookie.GenerateKey(length)` or by providing a key of one of the following lengths: +The default Encryptor and Decryptor functions use `AES-256-GCM` for encryption and decryption. If you need to use `AES-128` or `AES-192` instead, you can do so by changing the length of the key when calling `encryptcookie.GenerateKey(length)` or by providing a key of one of the following lengths: - AES-128 requires a 16-byte key. - AES-192 requires a 24-byte key. - AES-256 requires a 32-byte key. diff --git a/middleware/encryptcookie/config.go b/middleware/encryptcookie/config.go index ffa8e10c65..80db08d997 100644 --- a/middleware/encryptcookie/config.go +++ b/middleware/encryptcookie/config.go @@ -25,12 +25,12 @@ type Config struct { // Custom function to encrypt cookies. // - // Optional. Default: EncryptCookie (using AES-256-GCM) + // Optional. Default: EncryptCookie (using AES-GCM) Encryptor func(decryptedString, key string) (string, error) // Custom function to decrypt cookies. // - // Optional. Default: DecryptCookie (using AES-256-GCM) + // Optional. Default: DecryptCookie (using AES-GCM) Decryptor func(encryptedString, key string) (string, error) } diff --git a/middleware/encryptcookie/encryptcookie_test.go b/middleware/encryptcookie/encryptcookie_test.go index 881fbcde1a..c2f7639bb1 100644 --- a/middleware/encryptcookie/encryptcookie_test.go +++ b/middleware/encryptcookie/encryptcookie_test.go @@ -12,8 +12,6 @@ import ( "github.com/valyala/fasthttp" ) -var testKey = GenerateKey(32) - func Test_Middleware_Panics(t *testing.T) { t.Parallel() @@ -99,6 +97,7 @@ func Test_Middleware_InvalidBase64(t *testing.T) { func Test_Middleware_Encrypt_Cookie(t *testing.T) { t.Parallel() + testKey := GenerateKey(32) app := fiber.New() app.Use(New(Config{ @@ -160,6 +159,7 @@ func Test_Middleware_Encrypt_Cookie(t *testing.T) { func Test_Encrypt_Cookie_Next(t *testing.T) { t.Parallel() + testKey := GenerateKey(32) app := fiber.New() app.Use(New(Config{ @@ -184,6 +184,7 @@ func Test_Encrypt_Cookie_Next(t *testing.T) { func Test_Encrypt_Cookie_Except(t *testing.T) { t.Parallel() + testKey := GenerateKey(32) app := fiber.New() app.Use(New(Config{ @@ -228,6 +229,7 @@ func Test_Encrypt_Cookie_Except(t *testing.T) { func Test_Encrypt_Cookie_Custom_Encryptor(t *testing.T) { t.Parallel() + testKey := GenerateKey(32) app := fiber.New() app.Use(New(Config{ @@ -310,6 +312,7 @@ func Test_GenerateKey(t *testing.T) { } func Benchmark_Middleware_Encrypt_Cookie(b *testing.B) { + testKey := GenerateKey(32) app := fiber.New() app.Use(New(Config{ @@ -356,6 +359,7 @@ func Benchmark_Middleware_Encrypt_Cookie(b *testing.B) { } func Benchmark_Encrypt_Cookie_Next(b *testing.B) { + testKey := GenerateKey(32) app := fiber.New() app.Use(New(Config{ @@ -386,6 +390,7 @@ func Benchmark_Encrypt_Cookie_Next(b *testing.B) { } func Benchmark_Encrypt_Cookie_Except(b *testing.B) { + testKey := GenerateKey(32) app := fiber.New() app.Use(New(Config{ @@ -420,6 +425,7 @@ func Benchmark_Encrypt_Cookie_Except(b *testing.B) { } func Benchmark_Encrypt_Cookie_Custom_Encryptor(b *testing.B) { + testKey := GenerateKey(32) app := fiber.New() app.Use(New(Config{ @@ -474,6 +480,7 @@ func Benchmark_Encrypt_Cookie_Custom_Encryptor(b *testing.B) { } func Benchmark_Middleware_Encrypt_Cookie_Parallel(b *testing.B) { + testKey := GenerateKey(32) app := fiber.New() app.Use(New(Config{ @@ -526,6 +533,7 @@ func Benchmark_Middleware_Encrypt_Cookie_Parallel(b *testing.B) { } func Benchmark_Encrypt_Cookie_Next_Parallel(b *testing.B) { + testKey := GenerateKey(32) app := fiber.New() app.Use(New(Config{ @@ -556,6 +564,7 @@ func Benchmark_Encrypt_Cookie_Next_Parallel(b *testing.B) { } func Benchmark_Encrypt_Cookie_Except_Parallel(b *testing.B) { + testKey := GenerateKey(32) app := fiber.New() app.Use(New(Config{ @@ -590,6 +599,7 @@ func Benchmark_Encrypt_Cookie_Except_Parallel(b *testing.B) { } func Benchmark_Encrypt_Cookie_Custom_Encryptor_Parallel(b *testing.B) { + testKey := GenerateKey(32) app := fiber.New() app.Use(New(Config{ diff --git a/middleware/encryptcookie/utils.go b/middleware/encryptcookie/utils.go index e9bc41df4d..f08b5dadbc 100644 --- a/middleware/encryptcookie/utils.go +++ b/middleware/encryptcookie/utils.go @@ -10,6 +10,8 @@ import ( "io" ) +var ErrInvalidKeyLength = errors.New("encryption key must be 16, 24, or 32 bytes") + // EncryptCookie Encrypts a cookie value with specific encryption key func EncryptCookie(value, key string) (string, error) { keyDecoded, err := base64.StdEncoding.DecodeString(key) @@ -19,7 +21,7 @@ func EncryptCookie(value, key string) (string, error) { keyLen := len(keyDecoded) if keyLen != 16 && keyLen != 24 && keyLen != 32 { - return "", errors.New("encryption key must be 16, 24, or 32 bytes") + return "", ErrInvalidKeyLength } block, err := aes.NewCipher(keyDecoded) @@ -50,7 +52,7 @@ func DecryptCookie(value, key string) (string, error) { keyLen := len(keyDecoded) if keyLen != 16 && keyLen != 24 && keyLen != 32 { - return "", errors.New("encryption key must be 16, 24, or 32 bytes") + return "", ErrInvalidKeyLength } enc, err := base64.StdEncoding.DecodeString(value) @@ -88,7 +90,7 @@ func DecryptCookie(value, key string) (string, error) { // 16 bytes for AES-128, 24 bytes for AES-192, and 32 bytes for AES-256-GCM. func GenerateKey(length int) string { if length != 16 && length != 24 && length != 32 { - panic("encryption key length must be 16, 24, or 32 bytes") + panic(ErrInvalidKeyLength) } key := make([]byte, length)