From e34917bee64b12ede3dcb15c1b2d58945eae9ac8 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Tue, 23 Sep 2025 17:42:26 +0300 Subject: [PATCH 01/36] feat(yabase64): encode/decode --- go.mod | 2 +- yabase64/yabase64.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 yabase64/yabase64.go diff --git a/go.mod b/go.mod index d16bdca..b49de10 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,6 @@ require ( github.com/redis/go-redis/v9 v9.11.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b golang.org/x/net v0.42.0 golang.org/x/text v0.27.0 gorm.io/driver/sqlite v1.6.0 @@ -54,6 +53,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.40.0 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/mod v0.26.0 // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.34.0 // indirect diff --git a/yabase64/yabase64.go b/yabase64/yabase64.go new file mode 100644 index 0000000..2e42ef8 --- /dev/null +++ b/yabase64/yabase64.go @@ -0,0 +1,38 @@ +package yabase64 + +import ( + "bytes" + "encoding/base64" + "encoding/json" +) + +func Encode[T any](v T) ([]byte, error) { + var buf bytes.Buffer + + encoder := base64.NewEncoder(base64.StdEncoding, &buf) + err := json.NewEncoder(encoder).Encode(v) + if err != nil { + return nil, err + } + + if err := encoder.Close(); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func Decode[T any](value []byte) (*T, error) { + decoded, err := base64.StdEncoding.DecodeString(string(value)) + if err != nil { + return nil, err + } + + var result T + err = json.NewDecoder(bytes.NewReader(decoded)).Decode(&result) + if err != nil { + return nil, err + } + + return &result, nil +} From b5f852a75bf69ba5da8e308ba02d41943f0aa504 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Thu, 25 Sep 2025 18:59:36 +0300 Subject: [PATCH 02/36] feat(yagzip): logic zip/unzip bytes --- yagzip/yagzip.go | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 yagzip/yagzip.go diff --git a/yagzip/yagzip.go b/yagzip/yagzip.go new file mode 100644 index 0000000..8c1f307 --- /dev/null +++ b/yagzip/yagzip.go @@ -0,0 +1,40 @@ +package yagzip + +import ( + "bytes" + "compress/gzip" + "io" +) + +func Zip(object []byte) ([]byte, error) { + var buf bytes.Buffer + + w := gzip.NewWriter(&buf) + + _, err := w.Write(object) + if err != nil { + return nil, err + } + + if err := w.Close(); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func Unzip(compressed []byte) ([]byte, error) { + r, err := gzip.NewReader(bytes.NewReader(compressed)) + if err != nil { + return nil, err + } + defer r.Close() + + var out bytes.Buffer + _, err = io.Copy(&out, r) + if err != nil { + return nil, err + } + + return out.Bytes(), nil +} From e8fd9a290e5626b64ef3a760dfa739daf78c518f Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Thu, 25 Sep 2025 19:23:43 +0300 Subject: [PATCH 03/36] feat(yarsa): improve logic with chunks --- yarsa/yarsa.go | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 yarsa/yarsa.go diff --git a/yarsa/yarsa.go b/yarsa/yarsa.go new file mode 100644 index 0000000..a2d1f1a --- /dev/null +++ b/yarsa/yarsa.go @@ -0,0 +1,66 @@ +package yarsa + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "errors" +) + +func Encrypt(plaintext []byte, public *rsa.PublicKey) ([]byte, error) { + hash := sha256.New() + + label := []byte(nil) + + maxChunk := public.Size() - 2*sha256.Size - 2 + if maxChunk <= 0 { + return nil, errors.New("invalid OAEP max chunk size") + } + + var out []byte + + for i := 0; i < len(plaintext); i += maxChunk { + end := i + maxChunk + + end = min(end, len(plaintext)) + + block, err := rsa.EncryptOAEP(hash, rand.Reader, public, plaintext[i:end], label) + if err != nil { + return nil, err + } + + out = append(out, block...) + } + + return out, nil +} + +func Decrypt(ciphertext []byte, private *rsa.PrivateKey) ([]byte, error) { + hash := sha256.New() + + label := []byte(nil) + + blockSize := private.Size() + if blockSize <= 0 { + return nil, errors.New("invalid RSA modulus size") + } + + if len(ciphertext)%blockSize != 0 { + return nil, errors.New("ciphertext length is not a multiple of RSA block size (expected exact 256-byte blocks)") + } + + var out []byte + + for i := 0; i < len(ciphertext); i += blockSize { + end := i + blockSize + + plain, err := rsa.DecryptOAEP(hash, rand.Reader, private, ciphertext[i:end], label) + if err != nil { + return nil, err + } + + out = append(out, plain...) + } + + return out, nil +} From d51f6978556404e4c5ae59d7a8079a34a9941911 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Thu, 25 Sep 2025 19:28:25 +0300 Subject: [PATCH 04/36] feat(yabase64): return buffer --- yabase64/yabase64.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/yabase64/yabase64.go b/yabase64/yabase64.go index 2e42ef8..501962c 100644 --- a/yabase64/yabase64.go +++ b/yabase64/yabase64.go @@ -6,10 +6,11 @@ import ( "encoding/json" ) -func Encode[T any](v T) ([]byte, error) { +func Encode[T any](v T) (*bytes.Buffer, error) { var buf bytes.Buffer encoder := base64.NewEncoder(base64.StdEncoding, &buf) + err := json.NewEncoder(encoder).Encode(v) if err != nil { return nil, err @@ -19,16 +20,17 @@ func Encode[T any](v T) ([]byte, error) { return nil, err } - return buf.Bytes(), nil + return &buf, nil } -func Decode[T any](value []byte) (*T, error) { - decoded, err := base64.StdEncoding.DecodeString(string(value)) +func Decode[T any](value string) (*T, error) { + decoded, err := base64.StdEncoding.DecodeString(value) if err != nil { return nil, err } var result T + err = json.NewDecoder(bytes.NewReader(decoded)).Decode(&result) if err != nil { return nil, err From 22a06bcc907682b5fd4036044bbb4a4054f280dd Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Thu, 25 Sep 2025 19:34:41 +0300 Subject: [PATCH 05/36] feat(yabase64): docs, yaerror --- yabase64/yabase64.go | 112 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 107 insertions(+), 5 deletions(-) diff --git a/yabase64/yabase64.go b/yabase64/yabase64.go index 501962c..0353239 100644 --- a/yabase64/yabase64.go +++ b/yabase64/yabase64.go @@ -1,11 +1,76 @@ +// Package yabase64 provides tiny helpers to serialize a Go value as JSON and +// encode it using base64 (and the reverse operation). The API is intentionally +// minimal: +// +// - Encode[T any](v T) -> *bytes.Buffer holding the base64 text of JSON(v) +// - Decode[T any](s string) -> *T reconstructed from base64(JSON(T)) +// +// Notes: +// +// - The JSON encoder used by Encode writes a trailing newline by default +// (standard library behavior). This is preserved inside the base64 output. +// - The helpers are stateless and threadsafe. +// - Errors are returned as yaerrors.Error on decode and wrapped with HTTP 500 +// semantics to match the rest of your codebase. +// +// Example (basic round-trip): +// +// var data = struct { +// ID int `json:"id"` +// Name string `json:"name"` +// }{ID: 7, Name: "RZK"} +// +// // Encode → base64(JSON(data)) +// buf, err := yabase64.Encode(data) +// if err != nil { +// log.Fatalf("encode failed: %v", err) +// } +// b64 := buf.String() +// +// // Decode ← base64(JSON(T)) +// got, yaerr := yabase64.Decode[struct { +// ID int `json:"id"` +// Name string `json:"name"` +// }](b64) +// if yaerr != nil { +// log.Fatalf("decode failed: %v", yaerr) +// } +// +// fmt.Println(got.ID, got.Name) // 7 RZK package yabase64 import ( "bytes" "encoding/base64" "encoding/json" + "fmt" + "net/http" + + "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" ) +// Encode marshals v to JSON and base64-encodes the JSON bytes. +// +// Returns: +// - *bytes.Buffer containing base64 text (StdEncoding) of JSON(v) +// - error wrapping the underlying cause (e.g., JSON or encoder close) +// +// Behavior: +// - A trailing newline is emitted by json.Encoder; this newline becomes part +// of the base64 output (this matches standard library defaults). +// - The returned buffer owns its contents and can be read via Bytes()/String(). +// +// Example: +// +// type Payload struct { +// Token string `json:"token"` +// } +// +// buf, err := yabase64.Encode(Payload{Token: "abc"}) +// if err != nil { +// log.Fatalf("encode failed: %v", err) +// } +// fmt.Println(buf.String()) // e.g. eyJ0b2tlbiI6ImFiYyJ9Cg== func Encode[T any](v T) (*bytes.Buffer, error) { var buf bytes.Buffer @@ -13,27 +78,64 @@ func Encode[T any](v T) (*bytes.Buffer, error) { err := json.NewEncoder(encoder).Encode(v) if err != nil { - return nil, err + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + fmt.Sprintf("[BASE64] failed to encode `%T` to bytes", v), + ) } if err := encoder.Close(); err != nil { - return nil, err + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[BASE64] failed to close encoder", + ) } return &buf, nil } -func Decode[T any](value string) (*T, error) { +// Decode base64-decodes value and then unmarshals JSON into T. +// +// Parameters: +// - value: base64 string created by Encode[T] (i.e., base64(JSON(T)) ) +// +// Returns: +// - *T on success +// - yaerrors.Error on failure with http.StatusInternalServerError semantics +// +// Example: +// +// type User struct { +// ID int `json:"id"` +// } +// +// buf, _ := yabase64.Encode(User{ID: 42}) +// u, err := yabase64.Decode[User](buf.String()) +// if err != nil { +// log.Fatalf("decode failed: %v", err) +// } +// fmt.Println(u.ID) // 42 +func Decode[T any](value string) (*T, yaerrors.Error) { decoded, err := base64.StdEncoding.DecodeString(value) if err != nil { - return nil, err + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[BASE64] failed to decode string to bytes", + ) } var result T err = json.NewDecoder(bytes.NewReader(decoded)).Decode(&result) if err != nil { - return nil, err + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + fmt.Sprintf("[BASE64] failed to decode string to `%T`", result), + ) } return &result, nil From 9b11da6db865420e1cd0177076309b47e0e21081 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Thu, 25 Sep 2025 19:40:09 +0300 Subject: [PATCH 06/36] feat(yagzip): docs, yaerror --- yagzip/yagzip.go | 78 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 6 deletions(-) diff --git a/yagzip/yagzip.go b/yagzip/yagzip.go index 8c1f307..e7d6b72 100644 --- a/yagzip/yagzip.go +++ b/yagzip/yagzip.go @@ -1,39 +1,105 @@ +// Package yagzip provides tiny helpers to gzip-compress and decompress []byte +// payloads using the standard library's gzip implementation. +// +// Notes: +// - Zip writes gzip data into an internal buffer and returns its bytes. +// - Unzip reads a full gzip stream from memory and returns the decompressed bytes. +// - Errors are wrapped with yaerrors (HTTP 500 semantics) for consistency with +// the rest of your codebase, while keeping the exported signatures as (.., error). +// +// Example (basic round-trip): +// +// data := []byte("Hello, RZK!") +// z, err := yagzip.Zip(data) +// if err != nil { +// log.Fatalf("zip failed: %v", err) +// } +// uz, err := yagzip.Unzip(z) +// if err != nil { +// log.Fatalf("unzip failed: %v", err) +// } +// fmt.Println(string(uz)) // "Hello, RZK!" package yagzip import ( "bytes" "compress/gzip" "io" + "net/http" + + "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" ) -func Zip(object []byte) ([]byte, error) { +// Zip compresses object using gzip and returns the compressed bytes. +// +// Returns: +// - []byte: gzip-compressed data +// - yaerror: wrapped with err on failure +// +// Behavior: +// - Uses gzip.NewWriter (default level). +// - Ensures the writer is closed/finished on both success and failure paths. +// +// Example: +// +// in := []byte("payload") +// out, err := yagzip.Zip(in) +// if err != nil { /* handle */ } +func Zip(object []byte) ([]byte, yaerrors.Error) { var buf bytes.Buffer w := gzip.NewWriter(&buf) _, err := w.Write(object) if err != nil { - return nil, err + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[GZIP] failed to write payload to gzip writer", + ) } if err := w.Close(); err != nil { - return nil, err + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[GZIP] failed to close gzip writer", + ) } return buf.Bytes(), nil } -func Unzip(compressed []byte) ([]byte, error) { +// Unzip decompresses gzip-compressed data back to its original bytes. +// +// Returns: +// - []byte: decompressed payload +// - yaerror: wrapped with err on failure +// +// Example: +// +// payload, err := yagzip.Unzip(zipped) +// if err != nil { /* handle */ } +func Unzip(compressed []byte) ([]byte, yaerrors.Error) { r, err := gzip.NewReader(bytes.NewReader(compressed)) if err != nil { - return nil, err + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[GZIP] failed to create gzip reader", + ) } defer r.Close() var out bytes.Buffer + _, err = io.Copy(&out, r) if err != nil { - return nil, err + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[GZIP] failed to read from gzip stream", + ) } return out.Bytes(), nil From bfe9dfafcfef37a7d5dfd9a126bd94ba5c2aba47 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Thu, 25 Sep 2025 19:44:08 +0300 Subject: [PATCH 07/36] feat(yarsa): docs, yaerror --- yarsa/yarsa.go | 118 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 109 insertions(+), 9 deletions(-) diff --git a/yarsa/yarsa.go b/yarsa/yarsa.go index a2d1f1a..e85dc03 100644 --- a/yarsa/yarsa.go +++ b/yarsa/yarsa.go @@ -1,20 +1,85 @@ +// Package yarsa provides practical helpers to encrypt and decrypt arbitrary-length +// data with RSA-OAEP (SHA-256), handling chunking under the hood. +// +// The API is intentionally minimal: +// +// - Encrypt(plaintext []byte, public *rsa.PublicKey) -> []byte (concatenated ciphertext blocks) +// - Decrypt(cipher []byte, private *rsa.PrivateKey) -> []byte (reconstructed plaintext) +// +// Notes: +// +// - RSA-OAEP(SHA-256) with a 2048-bit key allows at most 190 bytes of plaintext +// per block (k − 2*hLen − 2 = 256 − 2*32 − 2). Larger inputs are split into +// 190-byte chunks automatically. +// - Ciphertext block size is always exactly the modulus size (256 bytes for +// RSA-2048). Therefore, the total ciphertext length is a multiple of 256. +// - Transport encodings (e.g., base64) are intentionally not handled here. +// Keep base64 “at the edges” of your app. +// - Errors are returned as yaerrors.Error with HTTP 500 semantics to match +// the rest of your codebase. +// +// Example (basic round-trip with RSA-2048): +// +// key, _ := rsa.GenerateKey(rand.Reader, 2048) +// +// msg := []byte("Hello, RZK! This may be longer than 190 bytes; it will be chunked automatically.") +// +// // Encrypt → concatenated 256-byte blocks +// ct, err := yarsa.Encrypt(msg, &key.PublicKey) +// if err != nil { +// log.Fatalf("encrypt failed: %v", err) +// } +// +// // Decrypt ← validate multiple of 256, then OAEP-decrypt each block +// pt, err := yarsa.Decrypt(ct, key) +// if err != nil { +// log.Fatalf("decrypt failed: %v", err) +// } +// +// fmt.Println(string(pt)) // "Hello, RZK! …" package yarsa import ( "crypto/rand" "crypto/rsa" "crypto/sha256" - "errors" + "net/http" + + "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" ) -func Encrypt(plaintext []byte, public *rsa.PublicKey) ([]byte, error) { +// Encrypt applies RSA-OAEP(SHA-256) to plaintext, chunking as needed. +// Each plaintext chunk (≤190 bytes for RSA-2048) is encrypted into a fixed-size +// 256-byte ciphertext block. All blocks are concatenated and returned. +// +// Example: +// +// key, _ := rsa.GenerateKey(rand.Reader, 2048) +// plaintext := []byte("Hello, this message will be chunked at 190 bytes if longer.") +// +// ciphertext, err := yarsa.Encrypt(plaintext, &key.PublicKey) +// if err != nil { +// log.Fatalf("encrypt failed: %v", err) +// } +// +// fmt.Printf("ciphertext length: %d\n", len(ciphertext)) +// +// Returns: +// - []byte: concatenated ciphertext blocks +// - yaerrors.Error: wrapped error with HTTP 500 semantics +func Encrypt(plaintext []byte, public *rsa.PublicKey) ([]byte, yaerrors.Error) { hash := sha256.New() label := []byte(nil) - maxChunk := public.Size() - 2*sha256.Size - 2 + const padding = 2 + + maxChunk := public.Size() - padding*sha256.Size - padding if maxChunk <= 0 { - return nil, errors.New("invalid OAEP max chunk size") + return nil, yaerrors.FromString( + http.StatusInternalServerError, + "[RSA] invalid OAEP max chunk size", + ) } var out []byte @@ -26,7 +91,11 @@ func Encrypt(plaintext []byte, public *rsa.PublicKey) ([]byte, error) { block, err := rsa.EncryptOAEP(hash, rand.Reader, public, plaintext[i:end], label) if err != nil { - return nil, err + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[RSA] failed to encrypt chunk with OAEP", + ) } out = append(out, block...) @@ -35,18 +104,45 @@ func Encrypt(plaintext []byte, public *rsa.PublicKey) ([]byte, error) { return out, nil } -func Decrypt(ciphertext []byte, private *rsa.PrivateKey) ([]byte, error) { +// Decrypt reverses Encrypt by splitting ciphertext into fixed-size blocks +// (256 bytes for RSA-2048), decrypting each with RSA-OAEP(SHA-256), and +// concatenating results. +// +// Example: +// +// key, _ := rsa.GenerateKey(rand.Reader, 2048) +// plaintext := []byte("Hello, this message will be encrypted and decrypted.") +// +// ciphertext, _ := yarsa.Encrypt(plaintext, &key.PublicKey) +// +// decrypted, err := yarsa.Decrypt(ciphertext, key) +// if err != nil { +// log.Fatalf("decrypt failed: %v", err) +// } +// +// fmt.Println(string(decrypted)) // "Hello, this message will be encrypted and decrypted." +// +// Returns: +// - []byte: reconstructed plaintext +// - yaerrors.Error: wrapped error with HTTP 500 semantics +func Decrypt(ciphertext []byte, private *rsa.PrivateKey) ([]byte, yaerrors.Error) { hash := sha256.New() label := []byte(nil) blockSize := private.Size() if blockSize <= 0 { - return nil, errors.New("invalid RSA modulus size") + return nil, yaerrors.FromString( + http.StatusInternalServerError, + "[RSA] invalid RSA modulus size", + ) } if len(ciphertext)%blockSize != 0 { - return nil, errors.New("ciphertext length is not a multiple of RSA block size (expected exact 256-byte blocks)") + return nil, yaerrors.FromString( + http.StatusInternalServerError, + "[RSA] ciphertext length is not a multiple of RSA block size (expected exact 256-byte blocks)", + ) } var out []byte @@ -56,7 +152,11 @@ func Decrypt(ciphertext []byte, private *rsa.PrivateKey) ([]byte, error) { plain, err := rsa.DecryptOAEP(hash, rand.Reader, private, ciphertext[i:end], label) if err != nil { - return nil, err + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[RSA] failed to decrypt chunk with OAEP", + ) } out = append(out, plain...) From 60aa39c1331d42a91a830f0caa80ba1624a70deb Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Thu, 25 Sep 2025 19:45:53 +0300 Subject: [PATCH 08/36] fix(yabase64): yaerror --- yabase64/yabase64.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yabase64/yabase64.go b/yabase64/yabase64.go index 0353239..06986b7 100644 --- a/yabase64/yabase64.go +++ b/yabase64/yabase64.go @@ -71,7 +71,7 @@ import ( // log.Fatalf("encode failed: %v", err) // } // fmt.Println(buf.String()) // e.g. eyJ0b2tlbiI6ImFiYyJ9Cg== -func Encode[T any](v T) (*bytes.Buffer, error) { +func Encode[T any](v T) (*bytes.Buffer, yaerrors.Error) { var buf bytes.Buffer encoder := base64.NewEncoder(base64.StdEncoding, &buf) From d648edfa69a372ab713bc002e56fc8321a869f1a Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Thu, 25 Sep 2025 19:57:54 +0300 Subject: [PATCH 09/36] feat(yarsa): tests --- yarsa/yarsa_test.go | 217 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 yarsa/yarsa_test.go diff --git a/yarsa/yarsa_test.go b/yarsa/yarsa_test.go new file mode 100644 index 0000000..9df6dd6 --- /dev/null +++ b/yarsa/yarsa_test.go @@ -0,0 +1,217 @@ +package yarsa_test + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "testing" + + "github.com/YaCodeDev/GoYaCodeDevUtils/yarsa" + "github.com/stretchr/testify/assert" +) + +func mustKey2048(t *testing.T) *rsa.PrivateKey { + t.Helper() + + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("failed to generate RSA key: %v", err) + } + + return key +} + +func TestMaxChunkFormula2048(t *testing.T) { + const expected = 190 + + key := mustKey2048(t) + + result := key.PublicKey.Size() - 2*sha256.Size - 2 + + assert.Equal(t, expected, result) +} + +func TestRoundTrip_SmallMessages(t *testing.T) { + key := mustKey2048(t) + + vectors := [][]byte{ + []byte(""), + []byte("a"), + []byte("Hello, RZK!"), + bytes.Repeat([]byte("x"), 15), + bytes.Repeat([]byte("y"), 189), + bytes.Repeat([]byte("z"), 190), + } + + for i, msg := range vectors { + ct, err := yarsa.Encrypt(msg, &key.PublicKey) + if err != nil { + t.Fatalf("case %d: encrypt failed: %v", i, err) + } + + if len(ct)%key.PublicKey.Size() != 0 { + t.Fatalf("case %d: ciphertext length %d not multiple of %d", + i, len(ct), key.PublicKey.Size()) + } + + pt, err := yarsa.Decrypt(ct, key) + if err != nil { + t.Fatalf("case %d: decrypt failed: %v", i, err) + } + + if !bytes.Equal(pt, msg) { + t.Fatalf("case %d: plaintext mismatch\n got: %q\nwant: %q", i, pt, msg) + } + } +} + +func TestRoundTrip_LargeMessages(t *testing.T) { + key := mustKey2048(t) + const maxChunk = 190 + + sizes := []int{ + maxChunk + 1, + maxChunk*2 - 1, + maxChunk * 2, + maxChunk*2 + 17, + maxChunk*3 + 123, + maxChunk*10 + 3, + maxChunk*20 + 77, + } + + for _, n := range sizes { + msg := make([]byte, n) + if _, err := rand.Read(msg); err != nil { + t.Fatalf("rand.Read failed: %v", err) + } + + ct, err := yarsa.Encrypt(msg, &key.PublicKey) + if err != nil { + t.Fatalf("n=%d: encrypt failed: %v", n, err) + } + if len(ct)%key.PublicKey.Size() != 0 { + t.Fatalf("n=%d: ciphertext length %d not multiple of %d", + n, len(ct), key.PublicKey.Size()) + } + + pt, err := yarsa.Decrypt(ct, key) + if err != nil { + t.Fatalf("n=%d: decrypt failed: %v", n, err) + } + if !bytes.Equal(pt, msg) { + t.Fatalf("n=%d: plaintext mismatch", n) + } + } +} + +func TestDecrypt_WithWrongKey_ShouldFail(t *testing.T) { + key1 := mustKey2048(t) + + key2 := mustKey2048(t) + + msg := []byte("wrong key test") + ct, err := yarsa.Encrypt(msg, &key1.PublicKey) + if err != nil { + t.Fatalf("encrypt failed: %v", err) + } + + if _, err := yarsa.Decrypt(ct, key2); err == nil { + t.Fatalf("expected decrypt error with wrong key, got nil") + } +} + +func TestDecrypt_TamperedCiphertext_ShouldFail(t *testing.T) { + key := mustKey2048(t) + + msg := []byte("tamper test") + ct, err := yarsa.Encrypt(msg, &key.PublicKey) + if err != nil { + t.Fatalf("encrypt failed: %v", err) + } + + if len(ct) == 0 { + t.Fatalf("unexpected empty ciphertext") + } + ct[len(ct)/2] ^= 0xFF + + if _, err := yarsa.Decrypt(ct, key); err == nil { + t.Fatalf("expected decrypt error on tampered ciphertext, got nil") + } +} + +func TestDecrypt_InvalidLength_ShouldFail(t *testing.T) { + key := mustKey2048(t) + + msg := []byte("length test") + ct, err := yarsa.Encrypt(msg, &key.PublicKey) + if err != nil { + t.Fatalf("encrypt failed: %v", err) + } + + ct = ct[:len(ct)-1] + + if _, err := yarsa.Decrypt(ct, key); err == nil { + t.Fatalf("expected decrypt error for invalid block multiple, got nil") + } +} + +func FuzzEncryptDecrypt(f *testing.F) { + seed := [][]byte{ + {}, + []byte("a"), + []byte("hello"), + bytes.Repeat([]byte{0}, 190), + bytes.Repeat([]byte{1}, 191), + } + for _, s := range seed { + f.Add(s) + } + + f.Fuzz(func(t *testing.T, data []byte) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Skipf("keygen failed: %v", err) + } + + ct, err := yarsa.Encrypt(data, &key.PublicKey) + if err != nil { + t.Fatalf("encrypt failed: %v", err) + } + + pt, err := yarsa.Decrypt(ct, key) + if err != nil { + t.Fatalf("decrypt failed: %v", err) + } + + if !bytes.Equal(pt, data) { + t.Fatalf("round-trip mismatch") + } + }) +} + +func TestBudget_LargeMessage(t *testing.T) { + t.Skip("enable manually") + key := mustKey2048(t) + const N = 190*50 + 77 + msg := make([]byte, N) + _, _ = rand.Read(msg) + + ct, err := yarsa.Encrypt(msg, &key.PublicKey) + if err != nil { + t.Fatal(err) + } + + if len(ct)%key.PublicKey.Size() != 0 { + t.Fatalf("ciphertext size not multiple of block size") + } + + pt, err := yarsa.Decrypt(ct, key) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(pt, msg) { + t.Fatalf("plaintext mismatch") + } +} From adf500a86fa6ede238597fb6029463aeb84f902a Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Thu, 25 Sep 2025 20:03:32 +0300 Subject: [PATCH 10/36] feat(yabase64): test --- yabase64/yabase64_test.go | 65 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 yabase64/yabase64_test.go diff --git a/yabase64/yabase64_test.go b/yabase64/yabase64_test.go new file mode 100644 index 0000000..3ce2bcd --- /dev/null +++ b/yabase64/yabase64_test.go @@ -0,0 +1,65 @@ +package yabase64_test + +import ( + "bytes" + "testing" + + "github.com/YaCodeDev/GoYaCodeDevUtils/yabase64" +) + +func TestBase64_FlowWorks(t *testing.T) { + in := sample{ + ID: 7, + Name: "RZK", + Tags: []string{"a", "b", "c"}, + Meta: map[string]string{"k1": "v1", "k2": "v2"}, + Bytes: []byte{0, 1, 2, 250, 251, 252}, + } + + buf, err := yabase64.Encode(in) + if err != nil { + t.Fatalf("encode failed: %v", err) + } + b64 := buf.String() + + out, yaerr := yabase64.Decode[sample](b64) + if yaerr != nil { + t.Fatalf("decode failed: %v", yaerr) + } + + if !equal(in, *out) { + t.Fatalf("mismatch after round-trip\nin: %+v\nout: %+v", in, *out) + } +} + +type sample struct { + ID int `json:"id"` + Name string `json:"name"` + Tags []string `json:"tags"` + Meta map[string]string `json:"meta"` + Bytes []byte `json:"bytes"` +} + +func equal(a, b sample) bool { + if a.ID != b.ID || a.Name != b.Name { + return false + } + + if len(a.Tags) != len(b.Tags) || len(a.Meta) != len(b.Meta) || !bytes.Equal(a.Bytes, b.Bytes) { + return false + } + + for i := range a.Tags { + if a.Tags[i] != b.Tags[i] { + return false + } + } + + for k, v := range a.Meta { + if b.Meta[k] != v { + return false + } + } + + return true +} From 44c34380416a5f032a362160440a7305489e4861 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Thu, 25 Sep 2025 20:10:47 +0300 Subject: [PATCH 11/36] feat(yagzip): tests --- yabase64/yabase64.go | 4 +-- yabase64/yabase64_test.go | 1 + yagzip/yagzip_test.go | 70 +++++++++++++++++++++++++++++++++++++++ yarsa/yarsa.go | 4 +-- yarsa/yarsa_test.go | 21 ++++++++---- 5 files changed, 90 insertions(+), 10 deletions(-) create mode 100644 yagzip/yagzip_test.go diff --git a/yabase64/yabase64.go b/yabase64/yabase64.go index 06986b7..f4e5a2f 100644 --- a/yabase64/yabase64.go +++ b/yabase64/yabase64.go @@ -20,14 +20,14 @@ // Name string `json:"name"` // }{ID: 7, Name: "RZK"} // -// // Encode → base64(JSON(data)) +// // Encode - base64(JSON(data)) // buf, err := yabase64.Encode(data) // if err != nil { // log.Fatalf("encode failed: %v", err) // } // b64 := buf.String() // -// // Decode ← base64(JSON(T)) +// // Decode - base64(JSON(T)) // got, yaerr := yabase64.Decode[struct { // ID int `json:"id"` // Name string `json:"name"` diff --git a/yabase64/yabase64_test.go b/yabase64/yabase64_test.go index 3ce2bcd..d8e5ba0 100644 --- a/yabase64/yabase64_test.go +++ b/yabase64/yabase64_test.go @@ -20,6 +20,7 @@ func TestBase64_FlowWorks(t *testing.T) { if err != nil { t.Fatalf("encode failed: %v", err) } + b64 := buf.String() out, yaerr := yabase64.Decode[sample](b64) diff --git a/yagzip/yagzip_test.go b/yagzip/yagzip_test.go new file mode 100644 index 0000000..3223a5c --- /dev/null +++ b/yagzip/yagzip_test.go @@ -0,0 +1,70 @@ +package yagzip_test + +import ( + "bytes" + "math/rand" + "testing" + + "github.com/YaCodeDev/GoYaCodeDevUtils/yagzip" +) + +func TestFlow_BasicCases(t *testing.T) { + vectors := [][]byte{ + {}, + []byte("a"), + []byte("Hello, RZK!"), + bytes.Repeat([]byte("x"), 128), + bytes.Repeat([]byte{0x00}, 1024), + bytes.Repeat([]byte{0xEE, 0xFF, 0x00, 0x01}, 257), + } + + for i, in := range vectors { + z, err := yagzip.Zip(in) + if err != nil { + t.Fatalf("case %d: Zip failed: %v", i, err) + } + + out, err := yagzip.Unzip(z) + if err != nil { + t.Fatalf("case %d: Unzip failed: %v", i, err) + } + + if !bytes.Equal(in, out) { + t.Fatalf("case %d: mismatch\nin: %v\nout: %v", i, in, out) + } + } +} + +func TestFlow_LargeCase(t *testing.T) { + sizes := []int{1 << 10, 64 << 10, 256 << 10} + rng := rand.New(rand.NewSource(42)) + + for _, n := range sizes { + in := make([]byte, n) + if _, err := rng.Read(in); err != nil { + t.Fatalf("rng read failed: %v", err) + } + + z, err := yagzip.Zip(in) + if err != nil { + t.Fatalf("n=%d: Zip failed: %v", n, err) + } + + out, err := yagzip.Unzip(z) + if err != nil { + t.Fatalf("n=%d: Unzip failed: %v", n, err) + } + + if !bytes.Equal(in, out) { + t.Fatalf("n=%d: mismatch after round-trip", n) + } + } +} + +func TestUnzip_InvalidInput(t *testing.T) { + bad := []byte("not-a-gzip-stream") + + if _, err := yagzip.Unzip(bad); err == nil { + t.Fatalf("expected error for invalid gzip input, got nil") + } +} diff --git a/yarsa/yarsa.go b/yarsa/yarsa.go index e85dc03..dd30eac 100644 --- a/yarsa/yarsa.go +++ b/yarsa/yarsa.go @@ -24,13 +24,13 @@ // // msg := []byte("Hello, RZK! This may be longer than 190 bytes; it will be chunked automatically.") // -// // Encrypt → concatenated 256-byte blocks +// // Encrypt - concatenated 256-byte blocks // ct, err := yarsa.Encrypt(msg, &key.PublicKey) // if err != nil { // log.Fatalf("encrypt failed: %v", err) // } // -// // Decrypt ← validate multiple of 256, then OAEP-decrypt each block +// // Decrypt - validate multiple of 256, then OAEP-decrypt each block // pt, err := yarsa.Decrypt(ct, key) // if err != nil { // log.Fatalf("decrypt failed: %v", err) diff --git a/yarsa/yarsa_test.go b/yarsa/yarsa_test.go index 9df6dd6..3bdb698 100644 --- a/yarsa/yarsa_test.go +++ b/yarsa/yarsa_test.go @@ -27,7 +27,7 @@ func TestMaxChunkFormula2048(t *testing.T) { key := mustKey2048(t) - result := key.PublicKey.Size() - 2*sha256.Size - 2 + result := key.Size() - 2*sha256.Size - 2 assert.Equal(t, expected, result) } @@ -50,9 +50,9 @@ func TestRoundTrip_SmallMessages(t *testing.T) { t.Fatalf("case %d: encrypt failed: %v", i, err) } - if len(ct)%key.PublicKey.Size() != 0 { + if len(ct)%key.Size() != 0 { t.Fatalf("case %d: ciphertext length %d not multiple of %d", - i, len(ct), key.PublicKey.Size()) + i, len(ct), key.Size()) } pt, err := yarsa.Decrypt(ct, key) @@ -68,6 +68,7 @@ func TestRoundTrip_SmallMessages(t *testing.T) { func TestRoundTrip_LargeMessages(t *testing.T) { key := mustKey2048(t) + const maxChunk = 190 sizes := []int{ @@ -90,15 +91,17 @@ func TestRoundTrip_LargeMessages(t *testing.T) { if err != nil { t.Fatalf("n=%d: encrypt failed: %v", n, err) } - if len(ct)%key.PublicKey.Size() != 0 { + + if len(ct)%key.Size() != 0 { t.Fatalf("n=%d: ciphertext length %d not multiple of %d", - n, len(ct), key.PublicKey.Size()) + n, len(ct), key.Size()) } pt, err := yarsa.Decrypt(ct, key) if err != nil { t.Fatalf("n=%d: decrypt failed: %v", n, err) } + if !bytes.Equal(pt, msg) { t.Fatalf("n=%d: plaintext mismatch", n) } @@ -111,6 +114,7 @@ func TestDecrypt_WithWrongKey_ShouldFail(t *testing.T) { key2 := mustKey2048(t) msg := []byte("wrong key test") + ct, err := yarsa.Encrypt(msg, &key1.PublicKey) if err != nil { t.Fatalf("encrypt failed: %v", err) @@ -125,6 +129,7 @@ func TestDecrypt_TamperedCiphertext_ShouldFail(t *testing.T) { key := mustKey2048(t) msg := []byte("tamper test") + ct, err := yarsa.Encrypt(msg, &key.PublicKey) if err != nil { t.Fatalf("encrypt failed: %v", err) @@ -133,6 +138,7 @@ func TestDecrypt_TamperedCiphertext_ShouldFail(t *testing.T) { if len(ct) == 0 { t.Fatalf("unexpected empty ciphertext") } + ct[len(ct)/2] ^= 0xFF if _, err := yarsa.Decrypt(ct, key); err == nil { @@ -144,6 +150,7 @@ func TestDecrypt_InvalidLength_ShouldFail(t *testing.T) { key := mustKey2048(t) msg := []byte("length test") + ct, err := yarsa.Encrypt(msg, &key.PublicKey) if err != nil { t.Fatalf("encrypt failed: %v", err) @@ -193,7 +200,9 @@ func FuzzEncryptDecrypt(f *testing.F) { func TestBudget_LargeMessage(t *testing.T) { t.Skip("enable manually") key := mustKey2048(t) + const N = 190*50 + 77 + msg := make([]byte, N) _, _ = rand.Read(msg) @@ -202,7 +211,7 @@ func TestBudget_LargeMessage(t *testing.T) { t.Fatal(err) } - if len(ct)%key.PublicKey.Size() != 0 { + if len(ct)%key.Size() != 0 { t.Fatalf("ciphertext size not multiple of block size") } From 0a4cff3b17d5855b349ac83704ddfcd028e263b8 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Sat, 27 Sep 2025 01:27:00 +0300 Subject: [PATCH 12/36] feat(yabase64): to string/bytes --- yabase64/yabase64.go | 47 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/yabase64/yabase64.go b/yabase64/yabase64.go index f4e5a2f..cfbb4c5 100644 --- a/yabase64/yabase64.go +++ b/yabase64/yabase64.go @@ -140,3 +140,50 @@ func Decode[T any](value string) (*T, yaerrors.Error) { return &result, nil } + +// ToString encodes raw bytes to a base64 string (StdEncoding). +// +// Notes: +// - This is a low-level helper and does NOT perform JSON marshaling. +// - It is stateless and threadsafe. +// - Use when you already have []byte and just need a base64 string. +// +// Example: +// +// data := []byte("hello world") +// b64 := yabase64.ToString(data) +// fmt.Println(b64) // aGVsbG8gd29ybGQ= +func ToString(data []byte) string { + return base64.StdEncoding.EncodeToString(data) +} + +// ToBytes decodes a base64 string (StdEncoding) back to raw bytes. +// +// Returns: +// - []byte on success +// - yaerrors.Error on failure with HTTP 500 semantics +// +// Notes: +// - This is a low-level helper and does NOT perform JSON unmarshaling. +// - Useful for working with binary data stored as base64 text. +// +// Example: +// +// b64 := "aGVsbG8gd29ybGQ=" +// bytes, err := yabase64.ToBytes(b64) +// if err != nil { +// log.Fatalf("decode failed: %v", err) +// } +// fmt.Println(string(bytes)) // hello world +func ToBytes(data string) ([]byte, yaerrors.Error) { + bytes, err := base64.StdEncoding.DecodeString(data) + if err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "failed to decode string to bytes", + ) + } + + return bytes, nil +} From 442de6a44a89f645c4b3678c25118c451250b931 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Sat, 27 Sep 2025 19:34:27 +0300 Subject: [PATCH 13/36] feat(yarsa): parse private key from string --- go.mod | 31 ++++++- go.sum | 65 ++++++++++++++- yarsa/yarsa.go | 192 ++++++++++++++++++++++++++++++++++++++++++++ yarsa/yarsa_test.go | 11 +++ 4 files changed, 293 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index b49de10..35543e6 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/gotd/td v0.128.0 github.com/redis/go-redis/v9 v9.11.0 github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 golang.org/x/net v0.42.0 golang.org/x/text v0.27.0 gorm.io/driver/sqlite v1.6.0 @@ -18,6 +18,32 @@ require ( modernc.org/sqlite v1.38.2 ) +require ( + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect +) + require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -28,6 +54,7 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/fatih/color v1.18.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect + github.com/gin-gonic/gin v1.11.0 github.com/go-faster/errors v0.7.1 // indirect github.com/go-faster/jx v1.1.0 // indirect github.com/go-faster/xor v1.0.0 // indirect @@ -56,7 +83,7 @@ require ( golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/mod v0.26.0 // indirect golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.34.0 // indirect + golang.org/x/sys v0.35.0 // indirect golang.org/x/tools v0.35.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 853b352..cd7c45a 100644 --- a/go.sum +++ b/go.sum @@ -4,10 +4,16 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -21,8 +27,14 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= github.com/go-faster/jx v1.1.0 h1:ZsW3wD+snOdmTDy9eIVgQdjUpXRRV4rqW8NS3t+20bg= @@ -36,8 +48,21 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -54,24 +79,40 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ogen-go/ogen v1.12.0 h1:JMkn957i9/IPaSehqpblviy6Uao3eqQ+eVKUn4LM9pg= github.com/ogen-go/ogen v1.12.0/go.mod h1:RL25amedfhq5xKTUuPBPn6nhYU59CWaVWYJ8YIjNHs0= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs= github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -83,9 +124,19 @@ github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= @@ -100,10 +151,14 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= @@ -117,12 +172,14 @@ golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/yarsa/yarsa.go b/yarsa/yarsa.go index dd30eac..ab37d2e 100644 --- a/yarsa/yarsa.go +++ b/yarsa/yarsa.go @@ -43,7 +43,11 @@ import ( "crypto/rand" "crypto/rsa" "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/pem" "net/http" + "strings" "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" ) @@ -164,3 +168,191 @@ func Decrypt(ciphertext []byte, private *rsa.PrivateKey) ([]byte, yaerrors.Error return out, nil } + +// ParsePrivateKey tries to parse an RSA private key provided as: +// 1. PEM: "-----BEGIN RSA PRIVATE KEY-----" (PKCS#1) or "-----BEGIN PRIVATE KEY-----" (PKCS#8) +// 2. Base64 of PEM (standard or URL-safe, with/without padding) +// 3. Raw DER bytes encoded as base64 (PKCS#1 or PKCS#8) +// +// It returns a *rsa.PrivateKey or a yaerrors.Error describing what failed. +func ParsePrivateKey(s string) (*rsa.PrivateKey, yaerrors.Error) { + input := strings.TrimSpace(s) + + if looksLikePEMPrivateKey(input) { + return parsePrivateKey([]byte(input)) + } + + noCRLF := StripCRLF(input) + + decoded, err := base64.StdEncoding.DecodeString(noCRLF) + if err != nil { + if alt, altErr := tryBase64URLAll(noCRLF); altErr == nil { + decoded = alt + } else { + return nil, yaerrors.FromString( + http.StatusInternalServerError, + "[RSA] invalid key: expected PEM (PKCS#1/PKCS#8) or base64 of PEM/DER", + ) + } + } + + if looksLikePEMPrivateKey(string(decoded)) { + return parsePrivateKey(decoded) + } + + if key, yaErr := parsePKCS1DER(decoded); yaErr == nil { + return key, nil + } + + if key, yaErr := parsePKCS8DER(decoded); yaErr == nil { + return key, nil + } + + return nil, yaerrors.FromString( + http.StatusInternalServerError, + "[RSA] invalid key: expected PEM (PKCS#1/PKCS#8) or base64 of PEM/DER", + ) +} + +// looksLikePEMPrivateKey performs cheap string checks to detect any PEM +// private-key header/footer without fully decoding the PEM. It’s used as +// a fast-path before attempting base64. +func looksLikePEMPrivateKey(s string) bool { + upper := strings.ToUpper(s) + + return strings.Contains(upper, "-----BEGIN ") && + strings.Contains(upper, " PRIVATE KEY-----") +} + +// StripCRLF removes CR and LF characters and then trims surrounding spaces. +// This allows base64 payloads to be pasted with line wraps. +func StripCRLF(s string) string { + s = strings.ReplaceAll(s, "\r", "") + s = strings.ReplaceAll(s, "\n", "") + + return strings.TrimSpace(s) +} + +// tryBase64URLAll attempts to decode s as URL-safe base64 in both variants: +// - RawURLEncoding (no '=' padding expected) +// - URLEncoding (padding expected; we add best-effort padding if missing) +// +// It returns decoded bytes or an error if neither variant works. +func tryBase64URLAll(s string) ([]byte, yaerrors.Error) { + if b, err := base64.RawURLEncoding.DecodeString(s); err == nil { + return b, nil + } + + res, err := base64.URLEncoding.DecodeString(padBase64(s)) + if err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[RSA] failed to decode string as bytes", + ) + } + + return res, nil +} + +// parsePrivateKey parses a PEM-encoded RSA private key in PKCS#1 or PKCS#8 form. +// Returns *rsa.PrivateKey or yaerrors.Error on failure. +func parsePrivateKey(pemBytes []byte) (*rsa.PrivateKey, yaerrors.Error) { + block, _ := pem.Decode(pemBytes) + if block == nil { + return nil, yaerrors.FromString( + http.StatusInternalServerError, + "[RSA] failed to decode PEM block", + ) + } + + switch block.Type { + case "RSA PRIVATE KEY": + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[RSA] failed to parse PKCS#1", + ) + } + + return key, nil + + case "PRIVATE KEY": + parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[RSA] failed to parse PKCS#8", + ) + } + + key, ok := parsed.(*rsa.PrivateKey) + if !ok { + return nil, yaerrors.FromString( + http.StatusInternalServerError, + "[RSA] PKCS#8 is not an RSA key", + ) + } + + return key, nil + default: + return nil, yaerrors.FromString( + http.StatusInternalServerError, + "[RSA] unsupported PEM type: "+block.Type, + ) + } +} + +// parsePKCS1DER parses a PKCS#1 DER-encoded RSA private key. +func parsePKCS1DER(der []byte) (*rsa.PrivateKey, yaerrors.Error) { + key, err := x509.ParsePKCS1PrivateKey(der) + if err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[RSA] DER PKCS#1 parse failed", + ) + } + + return key, nil +} + +// parsePKCS8DER parses a PKCS#8 DER-encoded private key, ensuring the type is RSA. +func parsePKCS8DER(der []byte) (*rsa.PrivateKey, yaerrors.Error) { + parsed, err := x509.ParsePKCS8PrivateKey(der) + if err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[RSA] DER PKCS#8 parse failed", + ) + } + + key, ok := parsed.(*rsa.PrivateKey) + if !ok { + return nil, yaerrors.FromString( + http.StatusInternalServerError, + "[RSA] DER PKCS#8 is not an RSA key", + ) + } + + return key, nil +} + +// padBase64 appends '=' characters until len(s) is a multiple of 4. +// This is a best-effort fix for inputs that dropped base64 padding. +func padBase64(s string) string { + const ( + padding = 4 + change = "=" + ) + + if m := len(s) % padding; m != 0 { + s += strings.Repeat(change, padding-m) + } + + return s +} diff --git a/yarsa/yarsa_test.go b/yarsa/yarsa_test.go index 3bdb698..38d627a 100644 --- a/yarsa/yarsa_test.go +++ b/yarsa/yarsa_test.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "crypto/rsa" "crypto/sha256" + "crypto/x509" "testing" "github.com/YaCodeDev/GoYaCodeDevUtils/yarsa" @@ -224,3 +225,13 @@ func TestBudget_LargeMessage(t *testing.T) { t.Fatalf("plaintext mismatch") } } + +func TestParsePrivateKey(t *testing.T) { + key, _ := rsa.GenerateKey(rand.Reader, 2048) + + marshaled, _ := x509.MarshalPKCS8PrivateKey(key) + + key, _ = yarsa.ParsePrivateKey(string(marshaled)) + + assert.NotNil(t, key) +} From 553eb381eeb90a00d401be3d5e317ec114949919 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Sat, 27 Sep 2025 19:35:05 +0300 Subject: [PATCH 14/36] feat(yamiddleware): add encode rsa header mw --- yamiddleware/yamiddleware.go | 206 ++++++++++++++++++++++++++++++ yamiddleware/yamiddleware_test.go | 134 +++++++++++++++++++ 2 files changed, 340 insertions(+) create mode 100644 yamiddleware/yamiddleware.go create mode 100644 yamiddleware/yamiddleware_test.go diff --git a/yamiddleware/yamiddleware.go b/yamiddleware/yamiddleware.go new file mode 100644 index 0000000..f6116ea --- /dev/null +++ b/yamiddleware/yamiddleware.go @@ -0,0 +1,206 @@ +// Package yamiddleware exposes small Gin middlewares. +package yamiddleware + +import ( + "crypto/rsa" + "net/http" + + "github.com/YaCodeDev/GoYaCodeDevUtils/yabase64" + "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" + "github.com/YaCodeDev/GoYaCodeDevUtils/yagzip" + "github.com/YaCodeDev/GoYaCodeDevUtils/yarsa" + "github.com/gin-gonic/gin" +) + +// GinMiddleware is a minimal interface implemented by all Gin middlewares here. +type GinMiddleware interface { + Handle(ctx *gin.Context) +} + +// EncodeRSA[T] reads a request header that carries an RSA-encrypted, +// base64 string; it then: +// 1. base64-decodes the header value, +// 2. decrypts with the server RSA private key, +// 3. gunzips the result, +// 4. decodes base64(JSON(T)), +// 5. stores *T in Gin context under the provided CtxKey. +// +// Server-side flow (what the middleware does): +// - Read header with name HeaderKey. +// - Normalize it (remove CR/LF; trim spaces). +// - base64 -> []byte. +// - RSA decrypt with RSAKey (private) -> zipped []byte. +// - gunzip -> plaintext []byte. +// - base64(JSON(T)) -> *T. +// - ctx.Set(CtxKey, *T), then continue the handler chain. +// +// Client-side flow (how to produce the header): +// - Take value T. +// - Encode as base64(JSON(T)). +// - gzip the bytes. +// - RSA encrypt with the server's public key. +// - Convert to base64 string; send it in the HTTP header named HeaderKey. +// +// Security/format notes: +// - RSA padding/mode must match your yarsa implementation (e.g., OAEP or PKCS#1 v1.5) on both sides. +// - Gzip is required; if the decrypted bytes are not a gzip stream, decompression fails. +// - The header value is base64 text; newlines and carriage returns are removed automatically. +// +// Example (client-side: produce the header): +// +// key, _ := rsa.GenerateKey(rand.Reader, 2048) +// mw := yamiddleware.NewEncodeRSA[MyPayload]("X-Enc", "payload", key) +// headerValue, _ := mw.Encode(MyPayload{ID: 1}, &key.PublicKey) +// // Send request with header: X-Enc: +// +// Example (server-side: use with Gin): +// +// key, _ := rsa.GenerateKey(rand.Reader, 2048) +// mw := yamiddleware.NewEncodeRSA[MyPayload]("X-Enc", "payload", key) +// +// r := gin.New() +// r.Use(mw.Handle) +// +// r.GET("/ping", func(c *gin.Context) { +// v, ok := c.Get("payload") // "payload" == CtxKey +// if !ok { +// c.AbortWithStatus(http.StatusUnauthorized) +// return +// } +// payload := v.(*MyPayload) // type-safe by your generic T +// c.JSON(200, payload) +// }) +type EncodeRSA[T any] struct { + RSAKey *rsa.PrivateKey + HeaderKey string + CtxKey string +} + +// NewEncodeRSA constructs a new EncodeRSA[T] with the given header +// name, context key, and server RSA private key. +// +// Example: +// +// key, _ := rsa.GenerateKey(rand.Reader, 2048) +// middleware := yamiddleware.NewEncodeRSA[MyPayload]("X-Enc", "payload", key) +func NewEncodeRSA[T any]( + headerKey string, + ctxKey string, + rsaKey *rsa.PrivateKey, +) *EncodeRSA[T] { + return &EncodeRSA[T]{ + RSAKey: rsaKey, + CtxKey: ctxKey, + HeaderKey: headerKey, + } +} + +// Encode prepares a header value suitable for sending to a server protected by +// EncodeRSA. It serializes data as base64(JSON), gzips the result, RSA +// encrypts it with the provided public key, and base64-encodes the final bytes. +// +// On success it returns the header string. On failure it returns yaerrors.Error. +// +// Example: +// +// middleware := yamiddleware.NewEncodeRSA[Payload]("X-Enc", "payload", private) +// headerValue, err := middleware.Encode(Payload{ID: 7}, &private.PublicKey) +// if err != nil { log.Fatal(err) } +// req.Header.Set("X-Enc", headerValue) +func (e *EncodeRSA[T]) Encode(data any, public *rsa.PublicKey) (string, yaerrors.Error) { + bytes, err := yabase64.Encode(data) + if err != nil { + return "", err.Wrap("[RSA HEADER] failed to encode data to bytes") + } + + zip, err := yagzip.Zip(bytes.Bytes()) + if err != nil { + return "", err.Wrap("[RSA HEADER] failed to zip bytes") + } + + rsa, err := yarsa.Encrypt(zip, public) + if err != nil { + return "", err.Wrap("[RSA HEADER] failed to encrypt zipped") + } + + return yabase64.ToString(rsa), nil +} + +// Decode reverses Encode. It accepts a base64 string (as produced by Encode), +// validates RSA block alignment, decrypts with the private key, ungzips, and +// unmarshals into *T. +// +// On success it returns *T; otherwise yaerrors.Error. +// +// Example: +// +// got, err := middleware.Decode(headerValue, private) +// if err != nil { log.Fatal(err) } +// fmt.Println(got.ID) +func (e *EncodeRSA[T]) Decode(data string, private *rsa.PrivateKey) (*T, yaerrors.Error) { + bytes, err := yabase64.ToBytes(data) + if err != nil { + return nil, err.Wrap("failed to encode string") + } + + if len(bytes)%private.Size() != 0 { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[RSA HEADER] bad block string size", + ) + } + + zipped, err := yarsa.Decrypt(bytes, private) + if err != nil { + return nil, err.Wrap("[RSA HEADER] failed to got zipped") + } + + plaintext, err := yagzip.Unzip(zipped) + if err != nil { + return nil, err.Wrap("[RSA HEADER] failed to get plain text from zip") + } + + res, err := yabase64.Decode[T](string(plaintext)) + if err != nil { + return nil, err.Wrap("[RSA HEADER] failed to decode plaintext") + } + + return res, nil +} + +// Handle is the Gin middleware entrypoint. It reads the header named HeaderKey, +// cleans it up, decodes it with Decode, and stores the result under CtxKey in +// the Gin context. On error, it records the error, aborts the request, and does +// not call subsequent handlers. +// +// Example: +// +// middleware := yamiddleware.NewEncodeRSA[Payload]("X-Enc", "payload", key) +// r := gin.New() +// r.Use(middleware.Handle) +// +// r.GET("/ping", func(c *gin.Context) { +// v, ok := c.Get("payload") +// if !ok { c.AbortWithStatus(http.StatusUnauthorized); return } +// payload := v.(*Payload) +// c.JSON(200, payload) +// }) +func (e *EncodeRSA[T]) Handle(ctx *gin.Context) { + text := ctx.GetHeader(e.HeaderKey) + + text = yarsa.StripCRLF(text) + + data, err := e.Decode(text, e.RSAKey) + if err != nil { + _ = ctx.Error(err) + + ctx.Abort() + + return + } + + ctx.Set(e.CtxKey, data) + + ctx.Next() +} diff --git a/yamiddleware/yamiddleware_test.go b/yamiddleware/yamiddleware_test.go new file mode 100644 index 0000000..7fd81cc --- /dev/null +++ b/yamiddleware/yamiddleware_test.go @@ -0,0 +1,134 @@ +package yamiddleware_test + +import ( + "crypto/rand" + "crypto/rsa" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/YaCodeDev/GoYaCodeDevUtils/yamiddleware" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +type testData struct { + ID uint16 `json:"id"` + Text *string `json:"text"` + Data []byte `json:"data"` +} + +func TestEncodeRSAHeader_Flow(t *testing.T) { + t.Parallel() + + t.Run("[EncodeDecode] RoundTrip", func(t *testing.T) { + t.Parallel() + + key, err := rsa.GenerateKey(rand.Reader, 2048) + assert.NoError(t, err, "failed to generate rsa key") + + lol := "RZK&SKALSE<3" + + in := testData{ + ID: 100, + Text: &lol, + Data: []byte{1, 2, 3, 4, 5, 6, 7, 8}, + } + + header := yamiddleware.NewEncodeRSA[testData]("X-Data", "payload", key) + + enc, _ := header.Encode(in, &key.PublicKey) + + out, _ := header.Decode(enc, key) + + assert.Equal(t, string(in.Data), string(out.Data), "Data mismatch") + }) + + t.Run("[Middleware] Success", func(t *testing.T) { + t.Parallel() + + key, err := rsa.GenerateKey(rand.Reader, 2048) + assert.NoError(t, err) + + lol := "OK" + in := testData{ID: 7, Text: &lol, Data: []byte{9, 8, 7}} + + header := yamiddleware.NewEncodeRSA[testData]("X-Enc", "payload", key) + + enc, yaerr := header.Encode(in, &key.PublicKey) + assert.Nil(t, yaerr, "encode failed: %v", yaerr) + + gin.SetMode(gin.TestMode) + engine := gin.New() + engine.Use(header.Handle) + + engine.GET("/ping", func(c *gin.Context) { + v, exists := c.Get("payload") + assert.True(t, exists, "payload not set in context") + + td, ok := v.(*testData) + assert.True(t, ok, "payload has wrong type: %T", v) + c.JSON(http.StatusOK, td) + }) + + req := httptest.NewRequest(http.MethodGet, "/ping", nil) + req.Header.Set("X-Enc", enc) + + rec := httptest.NewRecorder() + + engine.ServeHTTP(rec, req) + + var got testData + + assert.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got), "failed to decode JSON response") + }) + + t.Run("[Middleware] AbortOnInvalidHeader", func(t *testing.T) { + t.Parallel() + + key, err := rsa.GenerateKey(rand.Reader, 2048) + assert.NoError(t, err) + + header := yamiddleware.NewEncodeRSA[testData]("X-Enc", "payload", key) + + gin.SetMode(gin.TestMode) + engine := gin.New() + engine.Use(header.Handle) + + handlerCalled := false + + engine.GET("/ping", func(c *gin.Context) { + handlerCalled = true + + c.JSON(http.StatusOK, gin.H{"message": "pong"}) + }) + + req := httptest.NewRequest(http.MethodGet, "/ping", nil) + req.Header.Set("X-Enc", "!!!not-base64!!!") + + rec := httptest.NewRecorder() + + engine.ServeHTTP(rec, req) + + assert.False(t, handlerCalled, "handler should NOT be called on abort") + }) + + t.Run("[Decode] WrongKey", func(t *testing.T) { + t.Parallel() + + privateA, _ := rsa.GenerateKey(rand.Reader, 2048) + privateB, _ := rsa.GenerateKey(rand.Reader, 2048) + + lol := "wrong-key" + in := testData{ID: 1, Text: &lol} + + header := yamiddleware.NewEncodeRSA[testData]("X-Enc", "payload", privateA) + + enc, _ := header.Encode(in, &privateB.PublicKey) + + _, err := header.Decode(enc, privateA) + + assert.Error(t, err, "expected error when decrypting with wrong private key") + }) +} From fb06d5fa0aa5aedf014a71c4285e99bb184dca06 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Mon, 29 Sep 2025 01:10:15 +0300 Subject: [PATCH 15/36] feat(yarsa): gen key determenictis, docs and tests --- yarsa/key.go | 429 +++++++++++++++++++++++++++++++++++++++++++ yarsa/key_test.go | 227 +++++++++++++++++++++++ yarsa/reader.go | 104 +++++++++++ yarsa/reader_test.go | 126 +++++++++++++ yarsa/yarsa.go | 196 +------------------- yarsa/yarsa_test.go | 272 ++++++++++----------------- 6 files changed, 982 insertions(+), 372 deletions(-) create mode 100644 yarsa/key.go create mode 100644 yarsa/key_test.go create mode 100644 yarsa/reader.go create mode 100644 yarsa/reader_test.go diff --git a/yarsa/key.go b/yarsa/key.go new file mode 100644 index 0000000..df6e7f2 --- /dev/null +++ b/yarsa/key.go @@ -0,0 +1,429 @@ +// Package yarsa — RSA key utilities (deterministic keygen + private-key parsing). +// +// This file provides two main capabilities: +// +// 1) Deterministic RSA key generation: +// GenerateDeterministicRSA(KeyOpts) -> *rsa.PrivateKey +// - Reproducible for the same (Seed, Bits, E). +// - Uses an internal DRBG (HMAC-SHA256(counter)) and a deterministic prime +// search with the top TWO bits forced for each prime; that strongly biases +// p and q to the top quarter of their ranges so the final modulus has the +// requested bit-length. +// - The stdlib rsa.GenerateKey is NOT guaranteed deterministic even with a +// deterministic io.Reader (due to internal jitter), so we implement our +// own prime generation. +// +// 2) Private key parsing convenience: +// ParsePrivateKey(string) -> *rsa.PrivateKey +// - Accepts: +// * PEM (PKCS#1 “RSA PRIVATE KEY” or PKCS#8 “PRIVATE KEY”) +// * Base64 of PEM (std or URL-safe, with/without padding) +// * Raw DER bytes (PKCS#1 or PKCS#8) encoded as base64 +// - Returns yaerrors.Error on failure with HTTP-500 semantics to fit the +// existing error handling style of this codebase. +// +// Notes: +// - For deterministic keygen, supply a high-entropy Seed. A weak or guessable +// seed trivially compromises the private key. +// - StripCRLF(s) helps when keys are transported with line-wraps (pasted base64). + +package yarsa + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "io" + "math/big" + "net/http" + "strings" + + "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" +) + +var ( + bigOne = big.NewInt(1) + bigTwo = big.NewInt(2) +) + +// KeyOpts holds parameters for deterministic RSA key generation. +// - Bits: modulus size (e.g., 2048, 3072, 4096). Must be even and >= 512. +// - E: public exponent (use 65537 if 0). +// - Seed: high-entropy secret seed; same inputs -> same keypair. +type KeyOpts struct { + Bits int + E int + Seed []byte +} + +// GenerateDeterministicRSA returns a reproducible *rsa.PrivateKey from KeyOpts. +// Implementation details: +// - Uses a deterministic byte stream (NewDeterministicReader) to draw prime candidates. +// - Forces each prime’s top two bits and oddness to ensure target bit length. +// - Ensures gcd(e, p−1) == gcd(e, q−1) == 1 and p != q. +// - Validates the key and precomputes CRT values. +// +// Errors if Bits invalid, Seed empty, or validation fails. +// +// Example: +// +// opts := yarsa.KeyOpts{ +// Bits: 2048, +// E: 65537, +// Seed: []byte("deterministic-seed"), +// } +// +// key, err := yarsa.GenerateDeterministicRSA(opts) +// if err != nil { +// log.Fatalf("failed to generate key: %v", err) +// } +// +// // Calling again with the same seed -> identical key +// key2, _ := yarsa.GenerateDeterministicRSA(opts) +// fmt.Println(key.N.Cmp(key2.N) == 0) // true +func GenerateDeterministicRSA(opts KeyOpts) (*rsa.PrivateKey, error) { + if opts.Bits < 512 || opts.Bits%2 != 0 { + return nil, errors.New("bits must be even and >= 512") + } + + if opts.E == 0 { + opts.E = 65537 + } + + if len(opts.Seed) == 0 { + return nil, errors.New("seed required") + } + + reader := NewDeterministicReader(opts.Seed) + + pBits := opts.Bits / 2 + qBits := opts.Bits - pBits + + e := big.NewInt(int64(opts.E)) + + var p, q *big.Int + var err error + + for { + p, err = nextPrime(reader, pBits) + if err != nil { + return nil, err + } + pm1 := new(big.Int).Sub(p, bigOne) + if new(big.Int).GCD(nil, nil, e, pm1).Cmp(bigOne) == 0 { + break + } + } + + for { + q, err = nextPrime(reader, qBits) + if err != nil { + return nil, err + } + + if p.Cmp(q) == 0 { + continue + } + + qm1 := new(big.Int).Sub(q, bigOne) + if new(big.Int).GCD(nil, nil, e, qm1).Cmp(bigOne) != 0 { + continue + } + + n := new(big.Int).Mul(p, q) + if n.BitLen() == opts.Bits { + break + } + } + + if p.Cmp(q) < 0 { + p, q = q, p + } + + n := new(big.Int).Mul(p, q) + phi := new(big.Int).Mul(new(big.Int).Sub(p, bigOne), new(big.Int).Sub(q, bigOne)) + + d := new(big.Int).ModInverse(e, phi) + if d == nil { + return nil, errors.New("no modular inverse for d") + } + + priv := &rsa.PrivateKey{ + PublicKey: rsa.PublicKey{ + N: n, + E: int(e.Int64()), + }, + D: d, + Primes: []*big.Int{new(big.Int).Set(p), new(big.Int).Set(q)}, + } + + if err := priv.Validate(); err != nil { + return nil, err + } + + priv.Precompute() + + return priv, nil +} + +// nextPrime returns a prime of exact bit length `bits` from reader r. +// It sets the top two bits and the low bit (odd), then checks ProbablyPrime(64). +// If the candidate isn’t prime, it does a bounded deterministic +2 search +// (staying within the bit length) before drawing fresh bytes again. +func nextPrime(r io.Reader, bits int) (*big.Int, error) { + if bits < 2 { + return nil, errors.New("bits too small") + } + + byteLen := (bits + 7) / 8 + buf := make([]byte, byteLen) + + for { + if _, err := io.ReadFull(r, buf); err != nil { + return nil, err + } + + topMask := byte(0xFF) + if m := bits % 8; m != 0 { + topMask = 0xFF >> (8 - m) + } + + buf[0] &= topMask + + if bits%8 == 0 { + buf[0] |= 0xC0 + } else { + msb := uint((bits - 1) % 8) + nmsb := uint((bits - 2) % 8) + buf[0] |= (1 << msb) + buf[0] |= (1 << nmsb) + } + + buf[len(buf)-1] |= 1 + + cand := new(big.Int).SetBytes(buf) + if cand.BitLen() != bits { + continue + } + + if cand.ProbablyPrime(64) { + return cand, nil + } + + limit := 1 << 12 + for i := 0; i < limit; i++ { + cand.Add(cand, bigTwo) + if cand.BitLen() != bits { + break + } + + if cand.ProbablyPrime(64) { + return cand, nil + } + } + } +} + +// ParsePrivateKey tries to parse an RSA private key provided as: +// 1. PEM: "-----BEGIN RSA PRIVATE KEY-----" (PKCS#1) or "-----BEGIN PRIVATE KEY-----" (PKCS#8) +// 2. Base64 of PEM (standard or URL-safe, with/without padding) +// 3. Raw DER bytes encoded as base64 (PKCS#1 or PKCS#8) +// +// It returns a *rsa.PrivateKey or a yaerrors.Error describing what failed. +// +// Example: +// +// // Parse PEM-formatted private key +// const pemKey = `-----BEGIN RSA PRIVATE KEY----- +// MIIEowIBAAKCAQEA3vRcvK... +// -----END RSA PRIVATE KEY-----` +// +// key, err := yarsa.ParsePrivateKey(pemKey) +// if err != nil { +// log.Fatalf("parse failed: %v", err) +// } +// +// fmt.Println("Modulus bits:", key.N.BitLen()) +func ParsePrivateKey(s string) (*rsa.PrivateKey, yaerrors.Error) { + input := strings.TrimSpace(s) + + if looksLikePEMPrivateKey(input) { + return parsePrivateKey([]byte(input)) + } + + noCRLF := StripCRLF(input) + + decoded, err := base64.StdEncoding.DecodeString(noCRLF) + if err != nil { + if alt, altErr := tryBase64URLAll(noCRLF); altErr == nil { + decoded = alt + } else { + return nil, yaerrors.FromString( + http.StatusInternalServerError, + "[RSA] invalid key: expected PEM (PKCS#1/PKCS#8) or base64 of PEM/DER", + ) + } + } + + if looksLikePEMPrivateKey(string(decoded)) { + return parsePrivateKey(decoded) + } + + if key, yaErr := parsePKCS1DER(decoded); yaErr == nil { + return key, nil + } + + if key, yaErr := parsePKCS8DER(decoded); yaErr == nil { + return key, nil + } + + return nil, yaerrors.FromString( + http.StatusInternalServerError, + "[RSA] invalid key: expected PEM (PKCS#1/PKCS#8) or base64 of PEM/DER", + ) +} + +// looksLikePEMPrivateKey performs cheap string checks to detect any PEM +// private-key header/footer without fully decoding the PEM. It’s used as +// a fast-path before attempting base64. +func looksLikePEMPrivateKey(s string) bool { + upper := strings.ToUpper(s) + + return strings.Contains(upper, "-----BEGIN ") && + strings.Contains(upper, " PRIVATE KEY-----") +} + +// StripCRLF removes CR and LF characters and then trims surrounding spaces. +// This allows base64 payloads to be pasted with line wraps. +func StripCRLF(s string) string { + s = strings.ReplaceAll(s, "\r", "") + s = strings.ReplaceAll(s, "\n", "") + + return strings.TrimSpace(s) +} + +// tryBase64URLAll attempts to decode s as URL-safe base64 in both variants: +// - RawURLEncoding (no '=' padding expected) +// - URLEncoding (padding expected; we add best-effort padding if missing) +// +// It returns decoded bytes or an error if neither variant works. +func tryBase64URLAll(s string) ([]byte, yaerrors.Error) { + if b, err := base64.RawURLEncoding.DecodeString(s); err == nil { + return b, nil + } + + res, err := base64.URLEncoding.DecodeString(padBase64(s)) + if err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[RSA] failed to decode string as bytes", + ) + } + + return res, nil +} + +// parsePrivateKey parses a PEM-encoded RSA private key in PKCS#1 or PKCS#8 form. +// Returns *rsa.PrivateKey or yaerrors.Error on failure. +func parsePrivateKey(pemBytes []byte) (*rsa.PrivateKey, yaerrors.Error) { + block, _ := pem.Decode(pemBytes) + if block == nil { + return nil, yaerrors.FromString( + http.StatusInternalServerError, + "[RSA] failed to decode PEM block", + ) + } + + switch block.Type { + case "RSA PRIVATE KEY": + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[RSA] failed to parse PKCS#1", + ) + } + + return key, nil + + case "PRIVATE KEY": + parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[RSA] failed to parse PKCS#8", + ) + } + + key, ok := parsed.(*rsa.PrivateKey) + if !ok { + return nil, yaerrors.FromString( + http.StatusInternalServerError, + "[RSA] PKCS#8 is not an RSA key", + ) + } + + return key, nil + default: + return nil, yaerrors.FromString( + http.StatusInternalServerError, + "[RSA] unsupported PEM type: "+block.Type, + ) + } +} + +// parsePKCS1DER parses a PKCS#1 DER-encoded RSA private key. +func parsePKCS1DER(der []byte) (*rsa.PrivateKey, yaerrors.Error) { + key, err := x509.ParsePKCS1PrivateKey(der) + if err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[RSA] DER PKCS#1 parse failed", + ) + } + + return key, nil +} + +// parsePKCS8DER parses a PKCS#8 DER-encoded private key, ensuring the type is RSA. +func parsePKCS8DER(der []byte) (*rsa.PrivateKey, yaerrors.Error) { + parsed, err := x509.ParsePKCS8PrivateKey(der) + if err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[RSA] DER PKCS#8 parse failed", + ) + } + + key, ok := parsed.(*rsa.PrivateKey) + if !ok { + return nil, yaerrors.FromString( + http.StatusInternalServerError, + "[RSA] DER PKCS#8 is not an RSA key", + ) + } + + return key, nil +} + +// padBase64 appends '=' characters until len(s) is a multiple of 4. +// This is a best-effort fix for inputs that dropped base64 padding. +func padBase64(s string) string { + const ( + padding = 4 + change = "=" + ) + + if m := len(s) % padding; m != 0 { + s += strings.Repeat(change, padding-m) + } + + return s +} diff --git a/yarsa/key_test.go b/yarsa/key_test.go new file mode 100644 index 0000000..4b3f22b --- /dev/null +++ b/yarsa/key_test.go @@ -0,0 +1,227 @@ +package yarsa_test + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "testing" + + "github.com/YaCodeDev/GoYaCodeDevUtils/yarsa" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func pubFingerprint(t *testing.T, pub *rsa.PublicKey) [32]byte { + t.Helper() + der, err := x509.MarshalPKIXPublicKey(pub) + require.NoError(t, err) + return sha256.Sum256(der) +} + +func Test_GenerateDeterministicRSA_Determinism(t *testing.T) { + t.Parallel() + + const bits = 2048 + + seed := []byte("correct-horse-battery-staple") + + key1, err := yarsa.GenerateDeterministicRSA(yarsa.KeyOpts{Bits: bits, E: 65537, Seed: seed}) + require.NoError(t, err) + + key2, err := yarsa.GenerateDeterministicRSA(yarsa.KeyOpts{Bits: bits, E: 65537, Seed: seed}) + require.NoError(t, err) + + assert.Equal(t, pubFingerprint(t, &key1.PublicKey), pubFingerprint(t, &key2.PublicKey), "public keys differ for the same seed") + assert.Equal(t, key1.D, key2.D, "private exponent differs for the same seed") + assert.Equal(t, 2, len(key1.Primes)) + assert.Equal(t, bits, key1.N.BitLen(), "modulus bit length mismatch") +} + +func Test_GenerateDeterministicRSA_DifferentSeedsDiffer(t *testing.T) { + t.Parallel() + + const bits = 2048 + + seedA := []byte("seed-A") + seedB := []byte("seed-B") + + keyA, err := yarsa.GenerateDeterministicRSA(yarsa.KeyOpts{Bits: bits, E: 65537, Seed: seedA}) + require.NoError(t, err) + + keyB, err := yarsa.GenerateDeterministicRSA(yarsa.KeyOpts{Bits: bits, E: 65537, Seed: seedB}) + require.NoError(t, err) + + assert.NotEqual(t, pubFingerprint(t, &keyA.PublicKey), pubFingerprint(t, &keyB.PublicKey), "different seeds yielded identical public keys") + + assert.NotEqual(t, keyA.D, keyB.D, "different seeds yielded identical private exponents") +} + +func Test_GenerateDeterministicRSA_DefaultExponent_And_PrimeOrder(t *testing.T) { + t.Parallel() + + key, err := yarsa.GenerateDeterministicRSA(yarsa.KeyOpts{Bits: 2048, E: 0, Seed: []byte("exp-default")}) + + require.NoError(t, err) + + assert.Equal(t, 65537, key.E, "default exponent should be 65537") + + require.Equal(t, 2, len(key.Primes)) + + assert.True(t, key.Primes[0].Cmp(key.Primes[1]) > 0, "expected p > q ordering") +} + +func Test_GenerateDeterministicRSA_MultiBitLengths(t *testing.T) { + t.Parallel() + + for _, bits := range []int{2048, 4096} { + t.Run(fmt.Sprintf("bits=%d", bits), func(t *testing.T) { + t.Parallel() + key, err := yarsa.GenerateDeterministicRSA(yarsa.KeyOpts{Bits: bits, E: 65537, Seed: []byte("multi")}) + + require.NoError(t, err) + + assert.Equal(t, bits, key.N.BitLen(), "modulus bit length mismatch") + + assert.NoError(t, key.Validate(), "stdlib rsa key validation failed") + }) + } +} + +func Test_GenerateDeterministicRSA_InvalidOpts(t *testing.T) { + t.Parallel() + + _, err := yarsa.GenerateDeterministicRSA(yarsa.KeyOpts{Bits: 511, E: 65537, Seed: []byte("x")}) + assert.Error(t, err, "odd bit length should fail") + + _, err = yarsa.GenerateDeterministicRSA(yarsa.KeyOpts{Bits: 2048, E: 65537, Seed: nil}) + assert.Error(t, err, "missing seed should fail") +} + +func Test_ParsePrivateKey_AllFormats(t *testing.T) { + t.Parallel() + + priv, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + pkcs1DER := x509.MarshalPKCS1PrivateKey(priv) + pkcs1PEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: pkcs1DER}) + + pkcs8DER, err := x509.MarshalPKCS8PrivateKey(priv) + require.NoError(t, err) + pkcs8PEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8DER}) + + t.Run("[PEM] PKCS1", func(t *testing.T) { + t.Parallel() + + got, err := yarsa.ParsePrivateKey(string(pkcs1PEM)) + + assert.Nil(t, err, "parse pkcs1 pem error: %v", err) + + require.NotNil(t, got) + + assert.Equal(t, priv.N, got.N) + }) + + t.Run("[PEM] PKCS8", func(t *testing.T) { + t.Parallel() + + got, err := yarsa.ParsePrivateKey(string(pkcs8PEM)) + + assert.Nil(t, err, "parse pkcs8 pem error: %v", err) + + require.NotNil(t, got) + + assert.Equal(t, priv.N, got.N) + }) + + t.Run("[Base64 std] PKCS1 DER", func(t *testing.T) { + t.Parallel() + + b64 := base64.StdEncoding.EncodeToString(pkcs1DER) + + got, err := yarsa.ParsePrivateKey(b64) + + assert.Nil(t, err, "parse base64(pkcs1 der) error: %v", err) + + require.NotNil(t, got) + + assert.Equal(t, priv.N, got.N) + }) + + t.Run("[Base64 std] PKCS8 DER", func(t *testing.T) { + t.Parallel() + + b64 := base64.StdEncoding.EncodeToString(pkcs8DER) + + got, err := yarsa.ParsePrivateKey(b64) + + assert.Nil(t, err, "parse base64(pkcs8 der) error: %v", err) + + require.NotNil(t, got) + + assert.Equal(t, priv.N, got.N) + }) + + t.Run("[Base64 URL raw] PKCS1 DER (no padding)", func(t *testing.T) { + t.Parallel() + + b64url := base64.RawURLEncoding.EncodeToString(pkcs1DER) + + got, err := yarsa.ParsePrivateKey(b64url) + + assert.Nil(t, err, "parse rawURL b64(pkcs1 der) error: %v", err) + + require.NotNil(t, got) + + assert.Equal(t, priv.N, got.N) + }) + + t.Run("[Base64 URL padded] PKCS8 DER", func(t *testing.T) { + t.Parallel() + + b64url := base64.URLEncoding.EncodeToString(pkcs8DER) + + got, err := yarsa.ParsePrivateKey(b64url) + + assert.Nil(t, err, "parse URL b64(pkcs8 der) error: %v", err) + + require.NotNil(t, got) + + assert.Equal(t, priv.N, got.N) + }) + + t.Run("[CRLF-wrapped base64] PKCS1 DER", func(t *testing.T) { + t.Parallel() + + b64 := base64.StdEncoding.EncodeToString(pkcs1DER) + + wrapped := bytes.Join([][]byte{ + []byte(b64[:48]), + []byte(b64[48:96]), + []byte(b64[96:]), + }, []byte("\r\n")) + + got, err := yarsa.ParsePrivateKey(string(wrapped)) + + assert.Nil(t, err, "parse wrapped base64(pkcs1 der) error: %v", err) + + require.NotNil(t, got) + + assert.Equal(t, priv.N, got.N) + }) + + t.Run("[Invalid] garbage string", func(t *testing.T) { + t.Parallel() + + got, err := yarsa.ParsePrivateKey("!!!not-a-key!!!") + + assert.NotNil(t, err, "expected error for invalid input") + + assert.Nil(t, got) + }) +} diff --git a/yarsa/reader.go b/yarsa/reader.go new file mode 100644 index 0000000..0fa5238 --- /dev/null +++ b/yarsa/reader.go @@ -0,0 +1,104 @@ +package yarsa + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/binary" +) + +// DeterministicReader is a deterministic byte stream (DRBG-like) backed by +// HMAC-SHA256(counter). For a fixed seed, it produces the same sequence of +// bytes on every run. The internal 64-bit counter is encoded in big-endian and +// incremented once per 32-byte block (the size of SHA-256 output). +// +// Security note: +// - This is a simple construction intended for reproducible randomness in tests +// or key derivation flows you *fully* control. Do not treat it as a drop-in +// replacement for a NIST-approved DRBG without careful review. +// - It is **not** concurrency-safe; use one instance per goroutine if needed. +// +// Usage: +// +// seed := []byte("my secret seed") +// r := yarsa.NewDeterministicReader(seed) +// +// // Read 64 bytes deterministically +// buf := make([]byte, 64) +// _, _ = r.Read(buf) +// +// // Re-create with the same seed -> identical 64 bytes in buf2 +// r2 := yarsa.NewDeterministicReader(seed) +// buf2 := make([]byte, 64) +// _, _ = r2.Read(buf2) +// fmt.Println(bytes.Equal(buf, buf2)) // true +type DeterministicReader struct { + seed []byte + counter uint64 + buf []byte + pos int +} + +// NewDeterministicReader constructs a new deterministic reader from seed. +// The seed slice is **copied** internally to avoid external mutation effects. +// For the same seed, the produced byte stream is identical across runs. +// +// Example: +// +// r := yarsa.NewDeterministicReader([]byte("seed")) +// b := make([]byte, 16) +// _, _ = r.Read(b) // b now contains first 16 bytes of HMAC-SHA256(seed, ctr=0) +func NewDeterministicReader(seed []byte) *DeterministicReader { + return &DeterministicReader{ + seed: append([]byte{}, seed...), + counter: 0, + } +} + +// Read fills p with deterministic bytes, refilling the internal 32-byte block +// as needed. It returns len(p), nil on success. +// +// Contract: +// - Always returns exactly len(p) unless an unexpected internal error occurs. +// - Not concurrency-safe. Use one instance per goroutine if needed. +func (r *DeterministicReader) Read(p []byte) (int, error) { + written := 0 + for written < len(p) { + if r.buf == nil || r.pos >= len(r.buf) { + r.refill() + } + + avail := len(r.buf) - r.pos + + toCopy := avail + + need := len(p) - written + if toCopy > need { + toCopy = need + } + + copy(p[written:written+toCopy], r.buf[r.pos:r.pos+toCopy]) + + r.pos += toCopy + + written += toCopy + } + + return written, nil +} + +// refill computes the next 32-byte block = HMAC-SHA256(seed, bigEndian(counter)) +// and resets the buffer position, then increments the counter. +// Not concurrency-safe. +func (r *DeterministicReader) refill() { + mac := hmac.New(sha256.New, r.seed) + + var ctrBytes [8]byte + binary.BigEndian.PutUint64(ctrBytes[:], r.counter) + mac.Write(ctrBytes[:]) + + r.buf = mac.Sum(nil) + + r.pos = 0 + + r.counter++ +} diff --git a/yarsa/reader_test.go b/yarsa/reader_test.go new file mode 100644 index 0000000..d9104be --- /dev/null +++ b/yarsa/reader_test.go @@ -0,0 +1,126 @@ +package yarsa_test + +import ( + "bytes" + "testing" + + "github.com/YaCodeDev/GoYaCodeDevUtils/yarsa" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDeterministicReader_SameSeedSameStream(t *testing.T) { + t.Parallel() + + seed := []byte("correct-horse-battery-staple") + + r1 := yarsa.NewDeterministicReader(seed) + r2 := yarsa.NewDeterministicReader(seed) + + out1 := make([]byte, 4096) + out2 := make([]byte, 4096) + + n1, err1 := r1.Read(out1) + n2, err2 := r2.Read(out2) + + require.NoError(t, err1) + require.NoError(t, err2) + + require.Equal(t, len(out1), n1) + require.Equal(t, len(out2), n2) + + assert.True(t, bytes.Equal(out1, out2), "streams differ for same seed") +} + +func TestDeterministicReader_DifferentSeedsDiffer(t *testing.T) { + t.Parallel() + + r1 := yarsa.NewDeterministicReader([]byte("seed-A")) + r2 := yarsa.NewDeterministicReader([]byte("seed-B")) + + out1 := make([]byte, 256) + out2 := make([]byte, 256) + + _, _ = r1.Read(out1) + _, _ = r2.Read(out2) + + assert.False(t, bytes.Equal(out1, out2), "different seeds produced identical output") +} + +func TestDeterministicReader_MultiReadEqualsSingleRead(t *testing.T) { + t.Parallel() + + seed := []byte("split-read") + rAll := yarsa.NewDeterministicReader(seed) + rParts := yarsa.NewDeterministicReader(seed) + + full := make([]byte, 10*1024+13) + _, err := rAll.Read(full) + require.NoError(t, err) + + part := make([]byte, 0, len(full)) + + chunks := []int{1, 3, 7, 31, 32, 33, 1000, 4096, len(full) - (1 + 3 + 7 + 31 + 32 + 33 + 1000 + 4096)} + for _, n := range chunks { + buf := make([]byte, n) + _, err := rParts.Read(buf) + require.NoError(t, err) + part = append(part, buf...) + } + + assert.Equal(t, full, part, "split reads do not match single read") +} + +func TestDeterministicReader_ZeroLengthRead(t *testing.T) { + t.Parallel() + + r := yarsa.NewDeterministicReader([]byte("zlr")) + buf := make([]byte, 0) + + n, err := r.Read(buf) + require.NoError(t, err) + assert.Equal(t, 0, n) +} +func TestDeterministicReader_LongRead_ManyRefills(t *testing.T) { + t.Parallel() + + r := yarsa.NewDeterministicReader([]byte("long-long-seed")) + + N := 1 << 20 + buf := make([]byte, N) + + n, err := r.Read(buf) + require.NoError(t, err) + assert.Equal(t, N, n) + + r2 := yarsa.NewDeterministicReader([]byte("long-long-seed")) + buf2 := make([]byte, N) + _, _ = r2.Read(buf2) + + assert.Equal(t, buf2, buf) +} + +func TestDeterministicReader_SeedCopyIsolation(t *testing.T) { + t.Parallel() + + seed := []byte("mutable") + r1 := yarsa.NewDeterministicReader(seed) + + seed[0] ^= 0xFF + + out1 := make([]byte, 256) + _, _ = r1.Read(out1) + + r2 := yarsa.NewDeterministicReader(seed) + out2 := make([]byte, 256) + _, _ = r2.Read(out2) + + original := []byte("mutable") + r1Expected := yarsa.NewDeterministicReader(original) + + exp := make([]byte, 256) + _, _ = r1Expected.Read(exp) + + assert.True(t, bytes.Equal(out1, exp), "reader created before seed mutation changed unexpectedly") + assert.False(t, bytes.Equal(out1, out2), "reader created after mutation should differ from original") +} diff --git a/yarsa/yarsa.go b/yarsa/yarsa.go index ab37d2e..bf32cac 100644 --- a/yarsa/yarsa.go +++ b/yarsa/yarsa.go @@ -43,11 +43,7 @@ import ( "crypto/rand" "crypto/rsa" "crypto/sha256" - "crypto/x509" - "encoding/base64" - "encoding/pem" "net/http" - "strings" "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" ) @@ -74,7 +70,7 @@ import ( func Encrypt(plaintext []byte, public *rsa.PublicKey) ([]byte, yaerrors.Error) { hash := sha256.New() - label := []byte(nil) + label := []byte{} const padding = 2 @@ -132,7 +128,7 @@ func Encrypt(plaintext []byte, public *rsa.PublicKey) ([]byte, yaerrors.Error) { func Decrypt(ciphertext []byte, private *rsa.PrivateKey) ([]byte, yaerrors.Error) { hash := sha256.New() - label := []byte(nil) + label := []byte{} blockSize := private.Size() if blockSize <= 0 { @@ -168,191 +164,3 @@ func Decrypt(ciphertext []byte, private *rsa.PrivateKey) ([]byte, yaerrors.Error return out, nil } - -// ParsePrivateKey tries to parse an RSA private key provided as: -// 1. PEM: "-----BEGIN RSA PRIVATE KEY-----" (PKCS#1) or "-----BEGIN PRIVATE KEY-----" (PKCS#8) -// 2. Base64 of PEM (standard or URL-safe, with/without padding) -// 3. Raw DER bytes encoded as base64 (PKCS#1 or PKCS#8) -// -// It returns a *rsa.PrivateKey or a yaerrors.Error describing what failed. -func ParsePrivateKey(s string) (*rsa.PrivateKey, yaerrors.Error) { - input := strings.TrimSpace(s) - - if looksLikePEMPrivateKey(input) { - return parsePrivateKey([]byte(input)) - } - - noCRLF := StripCRLF(input) - - decoded, err := base64.StdEncoding.DecodeString(noCRLF) - if err != nil { - if alt, altErr := tryBase64URLAll(noCRLF); altErr == nil { - decoded = alt - } else { - return nil, yaerrors.FromString( - http.StatusInternalServerError, - "[RSA] invalid key: expected PEM (PKCS#1/PKCS#8) or base64 of PEM/DER", - ) - } - } - - if looksLikePEMPrivateKey(string(decoded)) { - return parsePrivateKey(decoded) - } - - if key, yaErr := parsePKCS1DER(decoded); yaErr == nil { - return key, nil - } - - if key, yaErr := parsePKCS8DER(decoded); yaErr == nil { - return key, nil - } - - return nil, yaerrors.FromString( - http.StatusInternalServerError, - "[RSA] invalid key: expected PEM (PKCS#1/PKCS#8) or base64 of PEM/DER", - ) -} - -// looksLikePEMPrivateKey performs cheap string checks to detect any PEM -// private-key header/footer without fully decoding the PEM. It’s used as -// a fast-path before attempting base64. -func looksLikePEMPrivateKey(s string) bool { - upper := strings.ToUpper(s) - - return strings.Contains(upper, "-----BEGIN ") && - strings.Contains(upper, " PRIVATE KEY-----") -} - -// StripCRLF removes CR and LF characters and then trims surrounding spaces. -// This allows base64 payloads to be pasted with line wraps. -func StripCRLF(s string) string { - s = strings.ReplaceAll(s, "\r", "") - s = strings.ReplaceAll(s, "\n", "") - - return strings.TrimSpace(s) -} - -// tryBase64URLAll attempts to decode s as URL-safe base64 in both variants: -// - RawURLEncoding (no '=' padding expected) -// - URLEncoding (padding expected; we add best-effort padding if missing) -// -// It returns decoded bytes or an error if neither variant works. -func tryBase64URLAll(s string) ([]byte, yaerrors.Error) { - if b, err := base64.RawURLEncoding.DecodeString(s); err == nil { - return b, nil - } - - res, err := base64.URLEncoding.DecodeString(padBase64(s)) - if err != nil { - return nil, yaerrors.FromError( - http.StatusInternalServerError, - err, - "[RSA] failed to decode string as bytes", - ) - } - - return res, nil -} - -// parsePrivateKey parses a PEM-encoded RSA private key in PKCS#1 or PKCS#8 form. -// Returns *rsa.PrivateKey or yaerrors.Error on failure. -func parsePrivateKey(pemBytes []byte) (*rsa.PrivateKey, yaerrors.Error) { - block, _ := pem.Decode(pemBytes) - if block == nil { - return nil, yaerrors.FromString( - http.StatusInternalServerError, - "[RSA] failed to decode PEM block", - ) - } - - switch block.Type { - case "RSA PRIVATE KEY": - key, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return nil, yaerrors.FromError( - http.StatusInternalServerError, - err, - "[RSA] failed to parse PKCS#1", - ) - } - - return key, nil - - case "PRIVATE KEY": - parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes) - if err != nil { - return nil, yaerrors.FromError( - http.StatusInternalServerError, - err, - "[RSA] failed to parse PKCS#8", - ) - } - - key, ok := parsed.(*rsa.PrivateKey) - if !ok { - return nil, yaerrors.FromString( - http.StatusInternalServerError, - "[RSA] PKCS#8 is not an RSA key", - ) - } - - return key, nil - default: - return nil, yaerrors.FromString( - http.StatusInternalServerError, - "[RSA] unsupported PEM type: "+block.Type, - ) - } -} - -// parsePKCS1DER parses a PKCS#1 DER-encoded RSA private key. -func parsePKCS1DER(der []byte) (*rsa.PrivateKey, yaerrors.Error) { - key, err := x509.ParsePKCS1PrivateKey(der) - if err != nil { - return nil, yaerrors.FromError( - http.StatusInternalServerError, - err, - "[RSA] DER PKCS#1 parse failed", - ) - } - - return key, nil -} - -// parsePKCS8DER parses a PKCS#8 DER-encoded private key, ensuring the type is RSA. -func parsePKCS8DER(der []byte) (*rsa.PrivateKey, yaerrors.Error) { - parsed, err := x509.ParsePKCS8PrivateKey(der) - if err != nil { - return nil, yaerrors.FromError( - http.StatusInternalServerError, - err, - "[RSA] DER PKCS#8 parse failed", - ) - } - - key, ok := parsed.(*rsa.PrivateKey) - if !ok { - return nil, yaerrors.FromString( - http.StatusInternalServerError, - "[RSA] DER PKCS#8 is not an RSA key", - ) - } - - return key, nil -} - -// padBase64 appends '=' characters until len(s) is a multiple of 4. -// This is a best-effort fix for inputs that dropped base64 padding. -func padBase64(s string) string { - const ( - padding = 4 - change = "=" - ) - - if m := len(s) % padding; m != 0 { - s += strings.Repeat(change, padding-m) - } - - return s -} diff --git a/yarsa/yarsa_test.go b/yarsa/yarsa_test.go index 38d627a..808ad6c 100644 --- a/yarsa/yarsa_test.go +++ b/yarsa/yarsa_test.go @@ -5,233 +5,149 @@ import ( "crypto/rand" "crypto/rsa" "crypto/sha256" - "crypto/x509" + "fmt" "testing" "github.com/YaCodeDev/GoYaCodeDevUtils/yarsa" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func mustKey2048(t *testing.T) *rsa.PrivateKey { +func genKey2048(t *testing.T) *rsa.PrivateKey { t.Helper() key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - t.Fatalf("failed to generate RSA key: %v", err) - } + + require.NoError(t, err, "failed to generate RSA key") return key } -func TestMaxChunkFormula2048(t *testing.T) { - const expected = 190 +func TestEncryptAndDecrypt_Flow(t *testing.T) { + t.Parallel() - key := mustKey2048(t) + t.Run("[Math] MaxChunkFormula2048", func(t *testing.T) { + t.Parallel() - result := key.Size() - 2*sha256.Size - 2 + const expected = 190 - assert.Equal(t, expected, result) -} + key := genKey2048(t) -func TestRoundTrip_SmallMessages(t *testing.T) { - key := mustKey2048(t) + result := key.Size() - 2*sha256.Size - 2 + assert.Equal(t, expected, result) + }) - vectors := [][]byte{ - []byte(""), - []byte("a"), - []byte("Hello, RZK!"), - bytes.Repeat([]byte("x"), 15), - bytes.Repeat([]byte("y"), 189), - bytes.Repeat([]byte("z"), 190), - } + t.Run("[RoundTrip] SmallMessages", func(t *testing.T) { + t.Parallel() - for i, msg := range vectors { - ct, err := yarsa.Encrypt(msg, &key.PublicKey) - if err != nil { - t.Fatalf("case %d: encrypt failed: %v", i, err) - } + key := genKey2048(t) - if len(ct)%key.Size() != 0 { - t.Fatalf("case %d: ciphertext length %d not multiple of %d", - i, len(ct), key.Size()) + vectors := [][]byte{ + []byte("a"), + []byte("Hello, RZK!"), + bytes.Repeat([]byte("x"), 15), + bytes.Repeat([]byte("y"), 189), + bytes.Repeat([]byte("z"), 190), } - pt, err := yarsa.Decrypt(ct, key) - if err != nil { - t.Fatalf("case %d: decrypt failed: %v", i, err) - } + for i, msg := range vectors { + i, msg := i, msg + t.Run(fmt.Sprintf("case#%d_len=%d", i, len(msg)), func(t *testing.T) { + t.Parallel() - if !bytes.Equal(pt, msg) { - t.Fatalf("case %d: plaintext mismatch\n got: %q\nwant: %q", i, pt, msg) - } - } -} + ct, err := yarsa.Encrypt(msg, &key.PublicKey) + require.NoError(t, err, "encrypt failed") -func TestRoundTrip_LargeMessages(t *testing.T) { - key := mustKey2048(t) - - const maxChunk = 190 - - sizes := []int{ - maxChunk + 1, - maxChunk*2 - 1, - maxChunk * 2, - maxChunk*2 + 17, - maxChunk*3 + 123, - maxChunk*10 + 3, - maxChunk*20 + 77, - } - - for _, n := range sizes { - msg := make([]byte, n) - if _, err := rand.Read(msg); err != nil { - t.Fatalf("rand.Read failed: %v", err) - } + assert.Equal(t, 0, len(ct)%key.Size(), "ciphertext length must be multiple of block size") - ct, err := yarsa.Encrypt(msg, &key.PublicKey) - if err != nil { - t.Fatalf("n=%d: encrypt failed: %v", n, err) - } + pt, err := yarsa.Decrypt(ct, key) + require.NoError(t, err, "decrypt failed") - if len(ct)%key.Size() != 0 { - t.Fatalf("n=%d: ciphertext length %d not multiple of %d", - n, len(ct), key.Size()) + assert.Equal(t, msg, pt, "plaintext mismatch") + }) } + }) - pt, err := yarsa.Decrypt(ct, key) - if err != nil { - t.Fatalf("n=%d: decrypt failed: %v", n, err) - } - - if !bytes.Equal(pt, msg) { - t.Fatalf("n=%d: plaintext mismatch", n) - } - } -} - -func TestDecrypt_WithWrongKey_ShouldFail(t *testing.T) { - key1 := mustKey2048(t) - - key2 := mustKey2048(t) - - msg := []byte("wrong key test") - - ct, err := yarsa.Encrypt(msg, &key1.PublicKey) - if err != nil { - t.Fatalf("encrypt failed: %v", err) - } - - if _, err := yarsa.Decrypt(ct, key2); err == nil { - t.Fatalf("expected decrypt error with wrong key, got nil") - } -} - -func TestDecrypt_TamperedCiphertext_ShouldFail(t *testing.T) { - key := mustKey2048(t) - - msg := []byte("tamper test") - - ct, err := yarsa.Encrypt(msg, &key.PublicKey) - if err != nil { - t.Fatalf("encrypt failed: %v", err) - } - - if len(ct) == 0 { - t.Fatalf("unexpected empty ciphertext") - } + t.Run("[RoundTrip] LargeMessages", func(t *testing.T) { + t.Parallel() - ct[len(ct)/2] ^= 0xFF + key := genKey2048(t) + const maxChunk = 190 - if _, err := yarsa.Decrypt(ct, key); err == nil { - t.Fatalf("expected decrypt error on tampered ciphertext, got nil") - } -} + sizes := []int{ + maxChunk + 1, + maxChunk*2 - 1, + maxChunk * 2, + maxChunk*2 + 17, + maxChunk*3 + 123, + maxChunk*10 + 3, + maxChunk*20 + 77, + } -func TestDecrypt_InvalidLength_ShouldFail(t *testing.T) { - key := mustKey2048(t) + for _, n := range sizes { + t.Run(fmt.Sprintf("n=%d", n), func(t *testing.T) { + t.Parallel() - msg := []byte("length test") + msg := make([]byte, n) + _, err := rand.Read(msg) + require.NoError(t, err, "rand.Read failed") - ct, err := yarsa.Encrypt(msg, &key.PublicKey) - if err != nil { - t.Fatalf("encrypt failed: %v", err) - } + ct, err := yarsa.Encrypt(msg, &key.PublicKey) + require.NoError(t, err, "encrypt failed") - ct = ct[:len(ct)-1] + assert.Equal(t, 0, len(ct)%key.Size(), "ciphertext length must be multiple of block size") - if _, err := yarsa.Decrypt(ct, key); err == nil { - t.Fatalf("expected decrypt error for invalid block multiple, got nil") - } -} + pt, err := yarsa.Decrypt(ct, key) + require.NoError(t, err, "decrypt failed") -func FuzzEncryptDecrypt(f *testing.F) { - seed := [][]byte{ - {}, - []byte("a"), - []byte("hello"), - bytes.Repeat([]byte{0}, 190), - bytes.Repeat([]byte{1}, 191), - } - for _, s := range seed { - f.Add(s) - } - - f.Fuzz(func(t *testing.T, data []byte) { - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - t.Skipf("keygen failed: %v", err) + assert.Equal(t, msg, pt, "plaintext mismatch") + }) } + }) - ct, err := yarsa.Encrypt(data, &key.PublicKey) - if err != nil { - t.Fatalf("encrypt failed: %v", err) - } + t.Run("[Decrypt] WrongKey_ShouldFail", func(t *testing.T) { + t.Parallel() - pt, err := yarsa.Decrypt(ct, key) - if err != nil { - t.Fatalf("decrypt failed: %v", err) - } + key1 := genKey2048(t) + key2 := genKey2048(t) - if !bytes.Equal(pt, data) { - t.Fatalf("round-trip mismatch") - } - }) -} + msg := []byte("wrong key test") + ct, err := yarsa.Encrypt(msg, &key1.PublicKey) + require.NoError(t, err, "encrypt failed") -func TestBudget_LargeMessage(t *testing.T) { - t.Skip("enable manually") - key := mustKey2048(t) + _, err = yarsa.Decrypt(ct, key2) + assert.Error(t, err, "expected decrypt error with wrong key") + }) - const N = 190*50 + 77 + t.Run("[Decrypt] TamperedCiphertext_ShouldFail", func(t *testing.T) { + t.Parallel() - msg := make([]byte, N) - _, _ = rand.Read(msg) + key := genKey2048(t) - ct, err := yarsa.Encrypt(msg, &key.PublicKey) - if err != nil { - t.Fatal(err) - } + msg := []byte("tamper test") + ct, err := yarsa.Encrypt(msg, &key.PublicKey) + require.NoError(t, err, "encrypt failed") + require.NotEmpty(t, ct, "unexpected empty ciphertext") - if len(ct)%key.Size() != 0 { - t.Fatalf("ciphertext size not multiple of block size") - } + ct[len(ct)/2] ^= 0xFF - pt, err := yarsa.Decrypt(ct, key) - if err != nil { - t.Fatal(err) - } + _, err = yarsa.Decrypt(ct, key) + assert.Error(t, err, "expected decrypt error on tampered ciphertext") + }) - if !bytes.Equal(pt, msg) { - t.Fatalf("plaintext mismatch") - } -} + t.Run("[Decrypt] InvalidLength_ShouldFail", func(t *testing.T) { + t.Parallel() -func TestParsePrivateKey(t *testing.T) { - key, _ := rsa.GenerateKey(rand.Reader, 2048) + key := genKey2048(t) - marshaled, _ := x509.MarshalPKCS8PrivateKey(key) + msg := []byte("length test") + ct, err := yarsa.Encrypt(msg, &key.PublicKey) + require.NoError(t, err, "encrypt failed") + require.Greater(t, len(ct), 0, "ciphertext should not be empty") - key, _ = yarsa.ParsePrivateKey(string(marshaled)) + ct = ct[:len(ct)-1] - assert.NotNil(t, key) + _, err = yarsa.Decrypt(ct, key) + assert.Error(t, err, "expected decrypt error for invalid block multiple") + }) } From 97194d4d98382be03209d55a1a8e23df422ea0ca Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Mon, 29 Sep 2025 01:12:20 +0300 Subject: [PATCH 16/36] feat(yabase64): update tests using testify --- yabase64/yabase64_test.go | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/yabase64/yabase64_test.go b/yabase64/yabase64_test.go index d8e5ba0..d211a87 100644 --- a/yabase64/yabase64_test.go +++ b/yabase64/yabase64_test.go @@ -5,6 +5,8 @@ import ( "testing" "github.com/YaCodeDev/GoYaCodeDevUtils/yabase64" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestBase64_FlowWorks(t *testing.T) { @@ -17,20 +19,15 @@ func TestBase64_FlowWorks(t *testing.T) { } buf, err := yabase64.Encode(in) - if err != nil { - t.Fatalf("encode failed: %v", err) - } + require.NoError(t, err, "encode failed") b64 := buf.String() out, yaerr := yabase64.Decode[sample](b64) - if yaerr != nil { - t.Fatalf("decode failed: %v", yaerr) - } + require.Nil(t, yaerr, "decode failed: %v", yaerr) + require.NotNil(t, out, "decoded value is nil") - if !equal(in, *out) { - t.Fatalf("mismatch after round-trip\nin: %+v\nout: %+v", in, *out) - } + assert.Equal(t, in, *out, "mismatch after round-trip") } type sample struct { From 3de6b6a23b6125807d930517fc286182e587be8f Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Mon, 29 Sep 2025 01:12:26 +0300 Subject: [PATCH 17/36] feat(yagzip): update tests using testify --- yagzip/yagzip_test.go | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/yagzip/yagzip_test.go b/yagzip/yagzip_test.go index 3223a5c..c65c6d8 100644 --- a/yagzip/yagzip_test.go +++ b/yagzip/yagzip_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/YaCodeDev/GoYaCodeDevUtils/yagzip" + "github.com/stretchr/testify/require" ) func TestFlow_BasicCases(t *testing.T) { @@ -20,18 +21,12 @@ func TestFlow_BasicCases(t *testing.T) { for i, in := range vectors { z, err := yagzip.Zip(in) - if err != nil { - t.Fatalf("case %d: Zip failed: %v", i, err) - } + require.NoErrorf(t, err, "case %d: Zip failed", i) out, err := yagzip.Unzip(z) - if err != nil { - t.Fatalf("case %d: Unzip failed: %v", i, err) - } + require.NoErrorf(t, err, "case %d: Unzip failed", i) - if !bytes.Equal(in, out) { - t.Fatalf("case %d: mismatch\nin: %v\nout: %v", i, in, out) - } + require.Equalf(t, in, out, "case %d: mismatch", i) } } @@ -41,30 +36,22 @@ func TestFlow_LargeCase(t *testing.T) { for _, n := range sizes { in := make([]byte, n) - if _, err := rng.Read(in); err != nil { - t.Fatalf("rng read failed: %v", err) - } + _, err := rng.Read(in) + require.NoErrorf(t, err, "n=%d: rng read failed", n) z, err := yagzip.Zip(in) - if err != nil { - t.Fatalf("n=%d: Zip failed: %v", n, err) - } + require.NoErrorf(t, err, "n=%d: Zip failed", n) out, err := yagzip.Unzip(z) - if err != nil { - t.Fatalf("n=%d: Unzip failed: %v", n, err) - } + require.NoErrorf(t, err, "n=%d: Unzip failed", n) - if !bytes.Equal(in, out) { - t.Fatalf("n=%d: mismatch after round-trip", n) - } + require.Equalf(t, in, out, "n=%d: mismatch after round-trip", n) } } func TestUnzip_InvalidInput(t *testing.T) { bad := []byte("not-a-gzip-stream") - if _, err := yagzip.Unzip(bad); err == nil { - t.Fatalf("expected error for invalid gzip input, got nil") - } + _, err := yagzip.Unzip(bad) + require.Error(t, err, "expected error for invalid gzip input") } From fc83c76cbcab35edb9b03f04cef903434a5c8fc3 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Mon, 29 Sep 2025 01:14:24 +0300 Subject: [PATCH 18/36] chore(yamiddleware): naming --- yamiddleware/yamiddleware.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/yamiddleware/yamiddleware.go b/yamiddleware/yamiddleware.go index f6116ea..0810943 100644 --- a/yamiddleware/yamiddleware.go +++ b/yamiddleware/yamiddleware.go @@ -71,9 +71,9 @@ type GinMiddleware interface { // c.JSON(200, payload) // }) type EncodeRSA[T any] struct { - RSAKey *rsa.PrivateKey - HeaderKey string - CtxKey string + RSA *rsa.PrivateKey + HeaderName string + ContextKey string } // NewEncodeRSA constructs a new EncodeRSA[T] with the given header @@ -84,14 +84,14 @@ type EncodeRSA[T any] struct { // key, _ := rsa.GenerateKey(rand.Reader, 2048) // middleware := yamiddleware.NewEncodeRSA[MyPayload]("X-Enc", "payload", key) func NewEncodeRSA[T any]( - headerKey string, - ctxKey string, - rsaKey *rsa.PrivateKey, + headerName string, + contextKey string, + rsa *rsa.PrivateKey, ) *EncodeRSA[T] { return &EncodeRSA[T]{ - RSAKey: rsaKey, - CtxKey: ctxKey, - HeaderKey: headerKey, + RSA: rsa, + ContextKey: contextKey, + HeaderName: headerName, } } @@ -187,11 +187,11 @@ func (e *EncodeRSA[T]) Decode(data string, private *rsa.PrivateKey) (*T, yaerror // c.JSON(200, payload) // }) func (e *EncodeRSA[T]) Handle(ctx *gin.Context) { - text := ctx.GetHeader(e.HeaderKey) + text := ctx.GetHeader(e.HeaderName) text = yarsa.StripCRLF(text) - data, err := e.Decode(text, e.RSAKey) + data, err := e.Decode(text, e.RSA) if err != nil { _ = ctx.Error(err) @@ -200,7 +200,7 @@ func (e *EncodeRSA[T]) Handle(ctx *gin.Context) { return } - ctx.Set(e.CtxKey, data) + ctx.Set(e.ContextKey, data) ctx.Next() } From 339117e3ca67b52406c32c83628f309b6d55e0dc Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Mon, 29 Sep 2025 01:30:18 +0300 Subject: [PATCH 19/36] chore(yarsa): fix lint --- yarsa/key.go | 135 +++++++++++++++++++++++++++---------------- yarsa/key_test.go | 26 +++++++-- yarsa/reader_test.go | 26 ++++++++- yarsa/yarsa_test.go | 16 ++++- 4 files changed, 142 insertions(+), 61 deletions(-) diff --git a/yarsa/key.go b/yarsa/key.go index df6e7f2..724f9ff 100644 --- a/yarsa/key.go +++ b/yarsa/key.go @@ -2,31 +2,30 @@ // // This file provides two main capabilities: // -// 1) Deterministic RSA key generation: -// GenerateDeterministicRSA(KeyOpts) -> *rsa.PrivateKey -// - Reproducible for the same (Seed, Bits, E). -// - Uses an internal DRBG (HMAC-SHA256(counter)) and a deterministic prime -// search with the top TWO bits forced for each prime; that strongly biases -// p and q to the top quarter of their ranges so the final modulus has the -// requested bit-length. -// - The stdlib rsa.GenerateKey is NOT guaranteed deterministic even with a -// deterministic io.Reader (due to internal jitter), so we implement our -// own prime generation. +// 1. Deterministic RSA key generation: +// GenerateDeterministicRSA(KeyOpts) -> *rsa.PrivateKey +// - Reproducible for the same (Seed, Bits, E). +// - Uses an internal DRBG (HMAC-SHA256(counter)) and a deterministic prime +// search with the top TWO bits forced for each prime; that strongly biases +// p and q to the top quarter of their ranges so the final modulus has the +// requested bit-length. +// - The stdlib rsa.GenerateKey is NOT guaranteed deterministic even with a +// deterministic io.Reader (due to internal jitter), so we implement our +// own prime generation. // -// 2) Private key parsing convenience: -// ParsePrivateKey(string) -> *rsa.PrivateKey -// - Accepts: -// * PEM (PKCS#1 “RSA PRIVATE KEY” or PKCS#8 “PRIVATE KEY”) -// * Base64 of PEM (std or URL-safe, with/without padding) -// * Raw DER bytes (PKCS#1 or PKCS#8) encoded as base64 -// - Returns yaerrors.Error on failure with HTTP-500 semantics to fit the -// existing error handling style of this codebase. +// 2. Private key parsing convenience: +// ParsePrivateKey(string) -> *rsa.PrivateKey +// - Accepts: +// * PEM (PKCS#1 “RSA PRIVATE KEY” or PKCS#8 “PRIVATE KEY”) +// * Base64 of PEM (std or URL-safe, with/without padding) +// * Raw DER bytes (PKCS#1 or PKCS#8) encoded as base64 +// - Returns yaerrors.Error on failure with HTTP-500 semantics to fit the +// existing error handling style of this codebase. // // Notes: // - For deterministic keygen, supply a high-entropy Seed. A weak or guessable // seed trivially compromises the private key. // - StripCRLF(s) helps when keys are transported with line-wraps (pasted base64). - package yarsa import ( @@ -34,7 +33,6 @@ import ( "crypto/x509" "encoding/base64" "encoding/pem" - "errors" "io" "math/big" "net/http" @@ -43,9 +41,14 @@ import ( "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" ) +const ( + bigValueOne = 1 + bigValueTwo = 2 +) + var ( - bigOne = big.NewInt(1) - bigTwo = big.NewInt(2) + bigOne = big.NewInt(bigValueOne) + bigTwo = big.NewInt(bigValueTwo) ) // KeyOpts holds parameters for deterministic RSA key generation. @@ -83,9 +86,12 @@ type KeyOpts struct { // // Calling again with the same seed -> identical key // key2, _ := yarsa.GenerateDeterministicRSA(opts) // fmt.Println(key.N.Cmp(key2.N) == 0) // true -func GenerateDeterministicRSA(opts KeyOpts) (*rsa.PrivateKey, error) { +func GenerateDeterministicRSA(opts KeyOpts) (*rsa.PrivateKey, yaerrors.Error) { if opts.Bits < 512 || opts.Bits%2 != 0 { - return nil, errors.New("bits must be even and >= 512") + return nil, yaerrors.FromString( + http.StatusInternalServerError, + "bits must be even and >= 512", + ) } if opts.E == 0 { @@ -93,24 +99,29 @@ func GenerateDeterministicRSA(opts KeyOpts) (*rsa.PrivateKey, error) { } if len(opts.Seed) == 0 { - return nil, errors.New("seed required") + return nil, yaerrors.FromString(http.StatusInternalServerError, "seed required") } reader := NewDeterministicReader(opts.Seed) - pBits := opts.Bits / 2 + const bits = 2 + + pBits := opts.Bits / bits qBits := opts.Bits - pBits e := big.NewInt(int64(opts.E)) - var p, q *big.Int - var err error + var ( + p, q *big.Int + err yaerrors.Error + ) for { p, err = nextPrime(reader, pBits) if err != nil { - return nil, err + return nil, err.Wrap("failed to get next prime") } + pm1 := new(big.Int).Sub(p, bigOne) if new(big.Int).GCD(nil, nil, e, pm1).Cmp(bigOne) == 0 { break @@ -120,7 +131,7 @@ func GenerateDeterministicRSA(opts KeyOpts) (*rsa.PrivateKey, error) { for { q, err = nextPrime(reader, qBits) if err != nil { - return nil, err + return nil, err.Wrap("failed to get next prime") } if p.Cmp(q) == 0 { @@ -147,10 +158,10 @@ func GenerateDeterministicRSA(opts KeyOpts) (*rsa.PrivateKey, error) { d := new(big.Int).ModInverse(e, phi) if d == nil { - return nil, errors.New("no modular inverse for d") + return nil, yaerrors.FromString(http.StatusInternalServerError, "no modular inverse for d") } - priv := &rsa.PrivateKey{ + private := &rsa.PrivateKey{ PublicKey: rsa.PublicKey{ N: n, E: int(e.Int64()), @@ -159,44 +170,62 @@ func GenerateDeterministicRSA(opts KeyOpts) (*rsa.PrivateKey, error) { Primes: []*big.Int{new(big.Int).Set(p), new(big.Int).Set(q)}, } - if err := priv.Validate(); err != nil { - return nil, err + if err := private.Validate(); err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "failed to validate private key", + ) } - priv.Precompute() + private.Precompute() - return priv, nil + return private, nil } // nextPrime returns a prime of exact bit length `bits` from reader r. // It sets the top two bits and the low bit (odd), then checks ProbablyPrime(64). // If the candidate isn’t prime, it does a bounded deterministic +2 search // (staying within the bit length) before drawing fresh bytes again. -func nextPrime(r io.Reader, bits int) (*big.Int, error) { - if bits < 2 { - return nil, errors.New("bits too small") +func nextPrime(r io.Reader, bits int) (*big.Int, yaerrors.Error) { + const minBits = 2 + if bits < minBits { + return nil, yaerrors.FromString(http.StatusInternalServerError, "bits too small") } - byteLen := (bits + 7) / 8 + const ( + bit7 = 7 + bit8 = 8 + ) + + byteLen := (bits + bit7) / bit8 buf := make([]byte, byteLen) for { if _, err := io.ReadFull(r, buf); err != nil { - return nil, err + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "failed to read full buffer", + ) } - topMask := byte(0xFF) - if m := bits % 8; m != 0 { - topMask = 0xFF >> (8 - m) + const mask = 0xFF + + topMask := byte(mask) + if m := bits % bit8; m != 0 { + topMask = mask >> (bit8 - m) } buf[0] &= topMask - if bits%8 == 0 { + const bit2 = 2 + + if bits%bit8 == 0 { buf[0] |= 0xC0 } else { - msb := uint((bits - 1) % 8) - nmsb := uint((bits - 2) % 8) + msb := uint((bits - 1) % bit8) + nmsb := uint((bits - bit2) % bit8) buf[0] |= (1 << msb) buf[0] |= (1 << nmsb) } @@ -208,18 +237,22 @@ func nextPrime(r io.Reader, bits int) (*big.Int, error) { continue } - if cand.ProbablyPrime(64) { + const prime = 64 + if cand.ProbablyPrime(prime) { return cand, nil } - limit := 1 << 12 - for i := 0; i < limit; i++ { + const bit12 = 12 + + limit := 1 << bit12 + for range limit { cand.Add(cand, bigTwo) + if cand.BitLen() != bits { break } - if cand.ProbablyPrime(64) { + if cand.ProbablyPrime(prime) { return cand, nil } } diff --git a/yarsa/key_test.go b/yarsa/key_test.go index 4b3f22b..547581e 100644 --- a/yarsa/key_test.go +++ b/yarsa/key_test.go @@ -18,8 +18,10 @@ import ( func pubFingerprint(t *testing.T, pub *rsa.PublicKey) [32]byte { t.Helper() + der, err := x509.MarshalPKIXPublicKey(pub) require.NoError(t, err) + return sha256.Sum256(der) } @@ -36,7 +38,12 @@ func Test_GenerateDeterministicRSA_Determinism(t *testing.T) { key2, err := yarsa.GenerateDeterministicRSA(yarsa.KeyOpts{Bits: bits, E: 65537, Seed: seed}) require.NoError(t, err) - assert.Equal(t, pubFingerprint(t, &key1.PublicKey), pubFingerprint(t, &key2.PublicKey), "public keys differ for the same seed") + assert.Equal( + t, + pubFingerprint(t, &key1.PublicKey), + pubFingerprint(t, &key2.PublicKey), + "public keys differ for the same seed", + ) assert.Equal(t, key1.D, key2.D, "private exponent differs for the same seed") assert.Equal(t, 2, len(key1.Primes)) assert.Equal(t, bits, key1.N.BitLen(), "modulus bit length mismatch") @@ -56,7 +63,12 @@ func Test_GenerateDeterministicRSA_DifferentSeedsDiffer(t *testing.T) { keyB, err := yarsa.GenerateDeterministicRSA(yarsa.KeyOpts{Bits: bits, E: 65537, Seed: seedB}) require.NoError(t, err) - assert.NotEqual(t, pubFingerprint(t, &keyA.PublicKey), pubFingerprint(t, &keyB.PublicKey), "different seeds yielded identical public keys") + assert.NotEqual( + t, + pubFingerprint(t, &keyA.PublicKey), + pubFingerprint(t, &keyB.PublicKey), + "different seeds yielded identical public keys", + ) assert.NotEqual(t, keyA.D, keyB.D, "different seeds yielded identical private exponents") } @@ -64,7 +76,9 @@ func Test_GenerateDeterministicRSA_DifferentSeedsDiffer(t *testing.T) { func Test_GenerateDeterministicRSA_DefaultExponent_And_PrimeOrder(t *testing.T) { t.Parallel() - key, err := yarsa.GenerateDeterministicRSA(yarsa.KeyOpts{Bits: 2048, E: 0, Seed: []byte("exp-default")}) + key, err := yarsa.GenerateDeterministicRSA( + yarsa.KeyOpts{Bits: 2048, E: 0, Seed: []byte("exp-default")}, + ) require.NoError(t, err) @@ -81,7 +95,10 @@ func Test_GenerateDeterministicRSA_MultiBitLengths(t *testing.T) { for _, bits := range []int{2048, 4096} { t.Run(fmt.Sprintf("bits=%d", bits), func(t *testing.T) { t.Parallel() - key, err := yarsa.GenerateDeterministicRSA(yarsa.KeyOpts{Bits: bits, E: 65537, Seed: []byte("multi")}) + + key, err := yarsa.GenerateDeterministicRSA( + yarsa.KeyOpts{Bits: bits, E: 65537, Seed: []byte("multi")}, + ) require.NoError(t, err) @@ -113,6 +130,7 @@ func Test_ParsePrivateKey_AllFormats(t *testing.T) { pkcs8DER, err := x509.MarshalPKCS8PrivateKey(priv) require.NoError(t, err) + pkcs8PEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8DER}) t.Run("[PEM] PKCS1", func(t *testing.T) { diff --git a/yarsa/reader_test.go b/yarsa/reader_test.go index d9104be..718dac5 100644 --- a/yarsa/reader_test.go +++ b/yarsa/reader_test.go @@ -60,11 +60,22 @@ func TestDeterministicReader_MultiReadEqualsSingleRead(t *testing.T) { part := make([]byte, 0, len(full)) - chunks := []int{1, 3, 7, 31, 32, 33, 1000, 4096, len(full) - (1 + 3 + 7 + 31 + 32 + 33 + 1000 + 4096)} + chunks := []int{ + 1, + 3, + 7, + 31, + 32, + 33, + 1000, + 4096, + len(full) - (1 + 3 + 7 + 31 + 32 + 33 + 1000 + 4096), + } for _, n := range chunks { buf := make([]byte, n) _, err := rParts.Read(buf) require.NoError(t, err) + part = append(part, buf...) } @@ -81,6 +92,7 @@ func TestDeterministicReader_ZeroLengthRead(t *testing.T) { require.NoError(t, err) assert.Equal(t, 0, n) } + func TestDeterministicReader_LongRead_ManyRefills(t *testing.T) { t.Parallel() @@ -121,6 +133,14 @@ func TestDeterministicReader_SeedCopyIsolation(t *testing.T) { exp := make([]byte, 256) _, _ = r1Expected.Read(exp) - assert.True(t, bytes.Equal(out1, exp), "reader created before seed mutation changed unexpectedly") - assert.False(t, bytes.Equal(out1, out2), "reader created after mutation should differ from original") + assert.True( + t, + bytes.Equal(out1, exp), + "reader created before seed mutation changed unexpectedly", + ) + assert.False( + t, + bytes.Equal(out1, out2), + "reader created after mutation should differ from original", + ) } diff --git a/yarsa/yarsa_test.go b/yarsa/yarsa_test.go index 808ad6c..03825e0 100644 --- a/yarsa/yarsa_test.go +++ b/yarsa/yarsa_test.go @@ -51,14 +51,18 @@ func TestEncryptAndDecrypt_Flow(t *testing.T) { } for i, msg := range vectors { - i, msg := i, msg t.Run(fmt.Sprintf("case#%d_len=%d", i, len(msg)), func(t *testing.T) { t.Parallel() ct, err := yarsa.Encrypt(msg, &key.PublicKey) require.NoError(t, err, "encrypt failed") - assert.Equal(t, 0, len(ct)%key.Size(), "ciphertext length must be multiple of block size") + assert.Equal( + t, + 0, + len(ct)%key.Size(), + "ciphertext length must be multiple of block size", + ) pt, err := yarsa.Decrypt(ct, key) require.NoError(t, err, "decrypt failed") @@ -72,6 +76,7 @@ func TestEncryptAndDecrypt_Flow(t *testing.T) { t.Parallel() key := genKey2048(t) + const maxChunk = 190 sizes := []int{ @@ -95,7 +100,12 @@ func TestEncryptAndDecrypt_Flow(t *testing.T) { ct, err := yarsa.Encrypt(msg, &key.PublicKey) require.NoError(t, err, "encrypt failed") - assert.Equal(t, 0, len(ct)%key.Size(), "ciphertext length must be multiple of block size") + assert.Equal( + t, + 0, + len(ct)%key.Size(), + "ciphertext length must be multiple of block size", + ) pt, err := yarsa.Decrypt(ct, key) require.NoError(t, err, "decrypt failed") From 7d316b2b5b799b3bb35e620bb951306898f8f3b6 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Mon, 29 Sep 2025 01:30:33 +0300 Subject: [PATCH 20/36] chore(yabase64): remove useless func --- yabase64/yabase64_test.go | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/yabase64/yabase64_test.go b/yabase64/yabase64_test.go index d211a87..e3a1285 100644 --- a/yabase64/yabase64_test.go +++ b/yabase64/yabase64_test.go @@ -1,7 +1,6 @@ package yabase64_test import ( - "bytes" "testing" "github.com/YaCodeDev/GoYaCodeDevUtils/yabase64" @@ -37,27 +36,3 @@ type sample struct { Meta map[string]string `json:"meta"` Bytes []byte `json:"bytes"` } - -func equal(a, b sample) bool { - if a.ID != b.ID || a.Name != b.Name { - return false - } - - if len(a.Tags) != len(b.Tags) || len(a.Meta) != len(b.Meta) || !bytes.Equal(a.Bytes, b.Bytes) { - return false - } - - for i := range a.Tags { - if a.Tags[i] != b.Tags[i] { - return false - } - } - - for k, v := range a.Meta { - if b.Meta[k] != v { - return false - } - } - - return true -} From 8d2b4e0e7a78bc287ba4c36914e1451d6ac1071c Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Mon, 29 Sep 2025 01:31:49 +0300 Subject: [PATCH 21/36] chore(yamiddleware): naming --- yamiddleware/yamiddleware.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yamiddleware/yamiddleware.go b/yamiddleware/yamiddleware.go index 0810943..e865973 100644 --- a/yamiddleware/yamiddleware.go +++ b/yamiddleware/yamiddleware.go @@ -140,7 +140,7 @@ func (e *EncodeRSA[T]) Encode(data any, public *rsa.PublicKey) (string, yaerrors func (e *EncodeRSA[T]) Decode(data string, private *rsa.PrivateKey) (*T, yaerrors.Error) { bytes, err := yabase64.ToBytes(data) if err != nil { - return nil, err.Wrap("failed to encode string") + return nil, err.Wrap("failed to decode string to bytes") } if len(bytes)%private.Size() != 0 { @@ -153,7 +153,7 @@ func (e *EncodeRSA[T]) Decode(data string, private *rsa.PrivateKey) (*T, yaerror zipped, err := yarsa.Decrypt(bytes, private) if err != nil { - return nil, err.Wrap("[RSA HEADER] failed to got zipped") + return nil, err.Wrap("[RSA HEADER] failed to decrypt to zipped data") } plaintext, err := yagzip.Unzip(zipped) From f7596b3c1e269988c52ac7f1d0453999d21d37af Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Mon, 29 Sep 2025 01:33:29 +0300 Subject: [PATCH 22/36] fix(yamiddleware): return correct error --- yamiddleware/yamiddleware.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/yamiddleware/yamiddleware.go b/yamiddleware/yamiddleware.go index e865973..6bdd1fb 100644 --- a/yamiddleware/yamiddleware.go +++ b/yamiddleware/yamiddleware.go @@ -144,9 +144,8 @@ func (e *EncodeRSA[T]) Decode(data string, private *rsa.PrivateKey) (*T, yaerror } if len(bytes)%private.Size() != 0 { - return nil, yaerrors.FromError( + return nil, yaerrors.FromString( http.StatusInternalServerError, - err, "[RSA HEADER] bad block string size", ) } From 29afff4a99af8b33e7a171e64a502bec5800f08b Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Sat, 4 Oct 2025 18:15:55 +0300 Subject: [PATCH 23/36] fix(yaginmiddleware): fix API and naming --- .../yaginmiddleware.go | 53 ++++++++++--------- .../yaginmiddleware_test.go | 48 +++++------------ 2 files changed, 41 insertions(+), 60 deletions(-) rename yamiddleware/yamiddleware.go => yaginmiddleware/yaginmiddleware.go (82%) rename yamiddleware/yamiddleware_test.go => yaginmiddleware/yaginmiddleware_test.go (58%) diff --git a/yamiddleware/yamiddleware.go b/yaginmiddleware/yaginmiddleware.go similarity index 82% rename from yamiddleware/yamiddleware.go rename to yaginmiddleware/yaginmiddleware.go index 6bdd1fb..da362a1 100644 --- a/yamiddleware/yamiddleware.go +++ b/yaginmiddleware/yaginmiddleware.go @@ -12,12 +12,12 @@ import ( "github.com/gin-gonic/gin" ) -// GinMiddleware is a minimal interface implemented by all Gin middlewares here. -type GinMiddleware interface { +// Middleware is a minimal interface implemented by all Gin middlewares here. +type Middleware interface { Handle(ctx *gin.Context) } -// EncodeRSA[T] reads a request header that carries an RSA-encrypted, +// RSASecureHeader[T] reads a request header that carries an RSA-encrypted, // base64 string; it then: // 1. base64-decodes the header value, // 2. decrypts with the server RSA private key, @@ -62,7 +62,7 @@ type GinMiddleware interface { // r.Use(mw.Handle) // // r.GET("/ping", func(c *gin.Context) { -// v, ok := c.Get("payload") // "payload" == CtxKey +// v, ok := c.Get("payload") // "payload" == ContextKey // if !ok { // c.AbortWithStatus(http.StatusUnauthorized) // return @@ -70,10 +70,11 @@ type GinMiddleware interface { // payload := v.(*MyPayload) // type-safe by your generic T // c.JSON(200, payload) // }) -type EncodeRSA[T any] struct { - RSA *rsa.PrivateKey - HeaderName string - ContextKey string +type RSASecureHeader[T any] struct { + RSA *rsa.PrivateKey + HeaderName string + ContextKey string + ContextAbort bool } // NewEncodeRSA constructs a new EncodeRSA[T] with the given header @@ -87,11 +88,13 @@ func NewEncodeRSA[T any]( headerName string, contextKey string, rsa *rsa.PrivateKey, -) *EncodeRSA[T] { - return &EncodeRSA[T]{ - RSA: rsa, - ContextKey: contextKey, - HeaderName: headerName, + contextAbort bool, +) *RSASecureHeader[T] { + return &RSASecureHeader[T]{ + RSA: rsa, + ContextKey: contextKey, + HeaderName: headerName, + ContextAbort: contextAbort, } } @@ -107,18 +110,18 @@ func NewEncodeRSA[T any]( // headerValue, err := middleware.Encode(Payload{ID: 7}, &private.PublicKey) // if err != nil { log.Fatal(err) } // req.Header.Set("X-Enc", headerValue) -func (e *EncodeRSA[T]) Encode(data any, public *rsa.PublicKey) (string, yaerrors.Error) { +func (e *RSASecureHeader[T]) Encode(data any) (string, yaerrors.Error) { bytes, err := yabase64.Encode(data) if err != nil { return "", err.Wrap("[RSA HEADER] failed to encode data to bytes") } - zip, err := yagzip.Zip(bytes.Bytes()) + zip, err := yagzip.NewGzip().Zip([]byte(bytes)) if err != nil { return "", err.Wrap("[RSA HEADER] failed to zip bytes") } - rsa, err := yarsa.Encrypt(zip, public) + rsa, err := yarsa.Encrypt(zip, &e.RSA.PublicKey) if err != nil { return "", err.Wrap("[RSA HEADER] failed to encrypt zipped") } @@ -134,28 +137,28 @@ func (e *EncodeRSA[T]) Encode(data any, public *rsa.PublicKey) (string, yaerrors // // Example: // -// got, err := middleware.Decode(headerValue, private) +// got, err := middleware.Decode(headerValue) // if err != nil { log.Fatal(err) } // fmt.Println(got.ID) -func (e *EncodeRSA[T]) Decode(data string, private *rsa.PrivateKey) (*T, yaerrors.Error) { +func (e *RSASecureHeader[T]) Decode(data string) (*T, yaerrors.Error) { bytes, err := yabase64.ToBytes(data) if err != nil { return nil, err.Wrap("failed to decode string to bytes") } - if len(bytes)%private.Size() != 0 { + if len(bytes)%e.RSA.Size() != 0 { return nil, yaerrors.FromString( http.StatusInternalServerError, "[RSA HEADER] bad block string size", ) } - zipped, err := yarsa.Decrypt(bytes, private) + zipped, err := yarsa.Decrypt(bytes, e.RSA) if err != nil { return nil, err.Wrap("[RSA HEADER] failed to decrypt to zipped data") } - plaintext, err := yagzip.Unzip(zipped) + plaintext, err := yagzip.NewGzip().Unzip(zipped) if err != nil { return nil, err.Wrap("[RSA HEADER] failed to get plain text from zip") } @@ -185,16 +188,18 @@ func (e *EncodeRSA[T]) Decode(data string, private *rsa.PrivateKey) (*T, yaerror // payload := v.(*Payload) // c.JSON(200, payload) // }) -func (e *EncodeRSA[T]) Handle(ctx *gin.Context) { +func (e *RSASecureHeader[T]) Handle(ctx *gin.Context) { text := ctx.GetHeader(e.HeaderName) text = yarsa.StripCRLF(text) - data, err := e.Decode(text, e.RSA) + data, err := e.Decode(text) if err != nil { _ = ctx.Error(err) - ctx.Abort() + if e.ContextAbort { + ctx.Abort() + } return } diff --git a/yamiddleware/yamiddleware_test.go b/yaginmiddleware/yaginmiddleware_test.go similarity index 58% rename from yamiddleware/yamiddleware_test.go rename to yaginmiddleware/yaginmiddleware_test.go index 7fd81cc..2a7ef45 100644 --- a/yamiddleware/yamiddleware_test.go +++ b/yaginmiddleware/yaginmiddleware_test.go @@ -3,12 +3,11 @@ package yamiddleware_test import ( "crypto/rand" "crypto/rsa" - "encoding/json" "net/http" "net/http/httptest" "testing" - "github.com/YaCodeDev/GoYaCodeDevUtils/yamiddleware" + yaginmiddleware "github.com/YaCodeDev/GoYaCodeDevUtils/yaginmiddleware" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) @@ -36,13 +35,13 @@ func TestEncodeRSAHeader_Flow(t *testing.T) { Data: []byte{1, 2, 3, 4, 5, 6, 7, 8}, } - header := yamiddleware.NewEncodeRSA[testData]("X-Data", "payload", key) + header := yaginmiddleware.NewEncodeRSA[testData]("X-Data", "payload", key, true) - enc, _ := header.Encode(in, &key.PublicKey) + enc, _ := header.Encode(in) - out, _ := header.Decode(enc, key) + out, _ := header.Decode(enc) - assert.Equal(t, string(in.Data), string(out.Data), "Data mismatch") + assert.Equal(t, &in, out, "Data mismatch") }) t.Run("[Middleware] Success", func(t *testing.T) { @@ -54,9 +53,9 @@ func TestEncodeRSAHeader_Flow(t *testing.T) { lol := "OK" in := testData{ID: 7, Text: &lol, Data: []byte{9, 8, 7}} - header := yamiddleware.NewEncodeRSA[testData]("X-Enc", "payload", key) + header := yaginmiddleware.NewEncodeRSA[testData]("X-Enc", "payload", key, true) - enc, yaerr := header.Encode(in, &key.PublicKey) + enc, yaerr := header.Encode(in) assert.Nil(t, yaerr, "encode failed: %v", yaerr) gin.SetMode(gin.TestMode) @@ -64,12 +63,11 @@ func TestEncodeRSAHeader_Flow(t *testing.T) { engine.Use(header.Handle) engine.GET("/ping", func(c *gin.Context) { - v, exists := c.Get("payload") - assert.True(t, exists, "payload not set in context") + v, _ := c.Get("payload") - td, ok := v.(*testData) - assert.True(t, ok, "payload has wrong type: %T", v) - c.JSON(http.StatusOK, td) + assert.Equal(t, &in, v, "failed to decode response") + + c.JSON(http.StatusOK, v) }) req := httptest.NewRequest(http.MethodGet, "/ping", nil) @@ -78,10 +76,6 @@ func TestEncodeRSAHeader_Flow(t *testing.T) { rec := httptest.NewRecorder() engine.ServeHTTP(rec, req) - - var got testData - - assert.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got), "failed to decode JSON response") }) t.Run("[Middleware] AbortOnInvalidHeader", func(t *testing.T) { @@ -90,7 +84,7 @@ func TestEncodeRSAHeader_Flow(t *testing.T) { key, err := rsa.GenerateKey(rand.Reader, 2048) assert.NoError(t, err) - header := yamiddleware.NewEncodeRSA[testData]("X-Enc", "payload", key) + header := yaginmiddleware.NewEncodeRSA[testData]("X-Enc", "payload", key, true) gin.SetMode(gin.TestMode) engine := gin.New() @@ -113,22 +107,4 @@ func TestEncodeRSAHeader_Flow(t *testing.T) { assert.False(t, handlerCalled, "handler should NOT be called on abort") }) - - t.Run("[Decode] WrongKey", func(t *testing.T) { - t.Parallel() - - privateA, _ := rsa.GenerateKey(rand.Reader, 2048) - privateB, _ := rsa.GenerateKey(rand.Reader, 2048) - - lol := "wrong-key" - in := testData{ID: 1, Text: &lol} - - header := yamiddleware.NewEncodeRSA[testData]("X-Enc", "payload", privateA) - - enc, _ := header.Encode(in, &privateB.PublicKey) - - _, err := header.Decode(enc, privateA) - - assert.Error(t, err, "expected error when decrypting with wrong private key") - }) } From dfb9e12e1fcddf25ae1fb25ee101e04826c6d3af Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Sat, 4 Oct 2025 18:16:14 +0300 Subject: [PATCH 24/36] feat(yagzip): make cleaner API with struct --- yagzip/yagzip.go | 32 ++++++++++++++++++++++++-------- yagzip/yagzip_test.go | 17 ++++++----------- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/yagzip/yagzip.go b/yagzip/yagzip.go index e7d6b72..2296cfc 100644 --- a/yagzip/yagzip.go +++ b/yagzip/yagzip.go @@ -23,6 +23,7 @@ package yagzip import ( "bytes" + "compress/flate" "compress/gzip" "io" "net/http" @@ -30,27 +31,42 @@ import ( "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" ) +type Gzip struct { + Level int +} + +func NewGzipWithLevel(level int) *Gzip { + return &Gzip{ + Level: level, + } +} + +func NewGzip() *Gzip { + return &Gzip{ + Level: flate.DefaultCompression, + } +} + // Zip compresses object using gzip and returns the compressed bytes. // // Returns: // - []byte: gzip-compressed data // - yaerror: wrapped with err on failure // -// Behavior: -// - Uses gzip.NewWriter (default level). -// - Ensures the writer is closed/finished on both success and failure paths. -// // Example: // // in := []byte("payload") // out, err := yagzip.Zip(in) // if err != nil { /* handle */ } -func Zip(object []byte) ([]byte, yaerrors.Error) { +func (g *Gzip) Zip(object []byte) ([]byte, yaerrors.Error) { var buf bytes.Buffer - w := gzip.NewWriter(&buf) + w, err := gzip.NewWriterLevel(&buf, g.Level) + if err != nil { + return nil, yaerrors.FromError(http.StatusInternalServerError, err, "[GZIP] failed to create write") + } - _, err := w.Write(object) + _, err = w.Write(object) if err != nil { return nil, yaerrors.FromError( http.StatusInternalServerError, @@ -80,7 +96,7 @@ func Zip(object []byte) ([]byte, yaerrors.Error) { // // payload, err := yagzip.Unzip(zipped) // if err != nil { /* handle */ } -func Unzip(compressed []byte) ([]byte, yaerrors.Error) { +func (g *Gzip) Unzip(compressed []byte) ([]byte, yaerrors.Error) { r, err := gzip.NewReader(bytes.NewReader(compressed)) if err != nil { return nil, yaerrors.FromError( diff --git a/yagzip/yagzip_test.go b/yagzip/yagzip_test.go index c65c6d8..9562e09 100644 --- a/yagzip/yagzip_test.go +++ b/yagzip/yagzip_test.go @@ -19,11 +19,12 @@ func TestFlow_BasicCases(t *testing.T) { bytes.Repeat([]byte{0xEE, 0xFF, 0x00, 0x01}, 257), } + gzip := yagzip.NewGzip() for i, in := range vectors { - z, err := yagzip.Zip(in) + z, err := gzip.Zip(in) require.NoErrorf(t, err, "case %d: Zip failed", i) - out, err := yagzip.Unzip(z) + out, err := gzip.Unzip(z) require.NoErrorf(t, err, "case %d: Unzip failed", i) require.Equalf(t, in, out, "case %d: mismatch", i) @@ -39,19 +40,13 @@ func TestFlow_LargeCase(t *testing.T) { _, err := rng.Read(in) require.NoErrorf(t, err, "n=%d: rng read failed", n) - z, err := yagzip.Zip(in) + gzip := yagzip.NewGzip() + z, err := gzip.Zip(in) require.NoErrorf(t, err, "n=%d: Zip failed", n) - out, err := yagzip.Unzip(z) + out, err := gzip.Unzip(z) require.NoErrorf(t, err, "n=%d: Unzip failed", n) require.Equalf(t, in, out, "n=%d: mismatch after round-trip", n) } } - -func TestUnzip_InvalidInput(t *testing.T) { - bad := []byte("not-a-gzip-stream") - - _, err := yagzip.Unzip(bad) - require.Error(t, err, "expected error for invalid gzip input") -} From 2d173296f59c28756cc19e2a5f2d12588f0a2a8d Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Sat, 4 Oct 2025 18:16:33 +0300 Subject: [PATCH 25/36] perf(yabase64): use gob instead json decoder --- yabase64/yabase64.go | 77 ++++++++++++++++++--------------------- yabase64/yabase64_test.go | 4 +- 2 files changed, 36 insertions(+), 45 deletions(-) diff --git a/yabase64/yabase64.go b/yabase64/yabase64.go index cfbb4c5..dd0d27b 100644 --- a/yabase64/yabase64.go +++ b/yabase64/yabase64.go @@ -42,103 +42,96 @@ package yabase64 import ( "bytes" "encoding/base64" - "encoding/json" + "encoding/gob" "fmt" "net/http" "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" ) -// Encode marshals v to JSON and base64-encodes the JSON bytes. +// Encode serializes a Go value using gob and base64-encodes the resulting bytes. // // Returns: -// - *bytes.Buffer containing base64 text (StdEncoding) of JSON(v) -// - error wrapping the underlying cause (e.g., JSON or encoder close) +// - *bytes.Buffer containing the base64-encoded gob data. +// - yaerrors.Error wrapping the underlying cause (if any). // // Behavior: -// - A trailing newline is emitted by json.Encoder; this newline becomes part -// of the base64 output (this matches standard library defaults). -// - The returned buffer owns its contents and can be read via Bytes()/String(). +// - The returned buffer contains valid base64 text that represents gob-encoded data. +// - The encoding is Go-specific and can only be decoded by Go using gob. +// - The buffer is owned by the caller and can be accessed via Bytes() or String(). // // Example: // // type Payload struct { -// Token string `json:"token"` +// Token string +// ID int // } // -// buf, err := yabase64.Encode(Payload{Token: "abc"}) +// buf, err := yabase64.Encode(Payload{Token: "abc", ID: 42}) // if err != nil { // log.Fatalf("encode failed: %v", err) // } -// fmt.Println(buf.String()) // e.g. eyJ0b2tlbiI6ImFiYyJ9Cg== -func Encode[T any](v T) (*bytes.Buffer, yaerrors.Error) { +// fmt.Println(buf.String()) // e.g. "GgAAAAVQYXlsb2FkAgAAAAZhYmMIAAAAqg==" +func Encode[T any](v T) (string, yaerrors.Error) { var buf bytes.Buffer - encoder := base64.NewEncoder(base64.StdEncoding, &buf) - - err := json.NewEncoder(encoder).Encode(v) - if err != nil { - return nil, yaerrors.FromError( + enc := gob.NewEncoder(&buf) + if err := enc.Encode(v); err != nil { + return "", yaerrors.FromError( http.StatusInternalServerError, err, - fmt.Sprintf("[BASE64] failed to encode `%T` to bytes", v), + fmt.Sprintf("[BASE64] failed to encode `%T` using gob", v), ) } - if err := encoder.Close(); err != nil { - return nil, yaerrors.FromError( - http.StatusInternalServerError, - err, - "[BASE64] failed to close encoder", - ) - } + return base64.StdEncoding.EncodeToString(buf.Bytes()), nil - return &buf, nil } -// Decode base64-decodes value and then unmarshals JSON into T. +// Decode decodes a base64-encoded gob string into a Go struct of type T. // // Parameters: -// - value: base64 string created by Encode[T] (i.e., base64(JSON(T)) ) +// - base: Base64-encoded string created by Encode[T]. // // Returns: -// - *T on success -// - yaerrors.Error on failure with http.StatusInternalServerError semantics +// - *T on success. +// - yaerrors.Error on failure (e.g., invalid base64 or gob data). // // Example: // // type User struct { -// ID int `json:"id"` +// ID int +// Name string // } // -// buf, _ := yabase64.Encode(User{ID: 42}) -// u, err := yabase64.Decode[User](buf.String()) +// encoded, _ := yabase64.Encode(User{ID: 42, Name: "Alice"}) +// +// u, err := yabase64.Decode[User](encoded.String()) // if err != nil { // log.Fatalf("decode failed: %v", err) // } -// fmt.Println(u.ID) // 42 -func Decode[T any](value string) (*T, yaerrors.Error) { - decoded, err := base64.StdEncoding.DecodeString(value) +// fmt.Printf("%+v\n", u) // &{ID:42 Name:Alice} +func Decode[T any](base string) (*T, yaerrors.Error) { + data, err := base64.StdEncoding.DecodeString(base) if err != nil { return nil, yaerrors.FromError( http.StatusInternalServerError, err, - "[BASE64] failed to decode string to bytes", + "[BASE64] failed to decode base64 string to bytes", ) } - var result T - - err = json.NewDecoder(bytes.NewReader(decoded)).Decode(&result) - if err != nil { + var v T + dec := gob.NewDecoder(bytes.NewReader(data)) + if err := dec.Decode(&v); err != nil { return nil, yaerrors.FromError( http.StatusInternalServerError, err, - fmt.Sprintf("[BASE64] failed to decode string to `%T`", result), + fmt.Sprintf("[BASE64] failed to decode gob to `%T`", v), ) } - return &result, nil + return &v, nil } // ToString encodes raw bytes to a base64 string (StdEncoding). diff --git a/yabase64/yabase64_test.go b/yabase64/yabase64_test.go index e3a1285..165b21c 100644 --- a/yabase64/yabase64_test.go +++ b/yabase64/yabase64_test.go @@ -17,11 +17,9 @@ func TestBase64_FlowWorks(t *testing.T) { Bytes: []byte{0, 1, 2, 250, 251, 252}, } - buf, err := yabase64.Encode(in) + b64, err := yabase64.Encode(in) require.NoError(t, err, "encode failed") - b64 := buf.String() - out, yaerr := yabase64.Decode[sample](b64) require.Nil(t, yaerr, "decode failed: %v", yaerr) require.NotNil(t, out, "decoded value is nil") From ef0e2a7210e3a1967dd4b6710466de5fdccaca31 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Sat, 4 Oct 2025 18:17:00 +0300 Subject: [PATCH 26/36] chore(yarsa): naming --- yarsa/key.go | 40 +++++++++++++++++++++++++--------------- yarsa/key_test.go | 20 ++++++++++---------- yarsa/reader.go | 6 +++--- yarsa/yarsa.go | 16 +++++----------- 4 files changed, 43 insertions(+), 39 deletions(-) diff --git a/yarsa/key.go b/yarsa/key.go index 724f9ff..0710d04 100644 --- a/yarsa/key.go +++ b/yarsa/key.go @@ -53,15 +53,15 @@ var ( // KeyOpts holds parameters for deterministic RSA key generation. // - Bits: modulus size (e.g., 2048, 3072, 4096). Must be even and >= 512. -// - E: public exponent (use 65537 if 0). +// - Exponent: public exponent (use 65537 if 0). // - Seed: high-entropy secret seed; same inputs -> same keypair. type KeyOpts struct { - Bits int - E int - Seed []byte + Bits int + Exponent int + Seed []byte } -// GenerateDeterministicRSA returns a reproducible *rsa.PrivateKey from KeyOpts. +// GenerateDeterministicRSAPrivateKey returns a reproducible *rsa.PrivateKey from KeyOpts. // Implementation details: // - Uses a deterministic byte stream (NewDeterministicReader) to draw prime candidates. // - Forces each prime’s top two bits and oddness to ensure target bit length. @@ -78,15 +78,15 @@ type KeyOpts struct { // Seed: []byte("deterministic-seed"), // } // -// key, err := yarsa.GenerateDeterministicRSA(opts) +// key, err := yarsa.GenerateDeterministicRSAPrivateKey(opts) // if err != nil { // log.Fatalf("failed to generate key: %v", err) // } // // // Calling again with the same seed -> identical key -// key2, _ := yarsa.GenerateDeterministicRSA(opts) +// key2, _ := yarsa.GenerateDeterministicRSAPrivateKey(opts) // fmt.Println(key.N.Cmp(key2.N) == 0) // true -func GenerateDeterministicRSA(opts KeyOpts) (*rsa.PrivateKey, yaerrors.Error) { +func GenerateDeterministicRSAPrivateKey(opts KeyOpts) (*rsa.PrivateKey, yaerrors.Error) { if opts.Bits < 512 || opts.Bits%2 != 0 { return nil, yaerrors.FromString( http.StatusInternalServerError, @@ -94,8 +94,8 @@ func GenerateDeterministicRSA(opts KeyOpts) (*rsa.PrivateKey, yaerrors.Error) { ) } - if opts.E == 0 { - opts.E = 65537 + if opts.Exponent == 0 { + opts.Exponent = 65537 } if len(opts.Seed) == 0 { @@ -109,7 +109,7 @@ func GenerateDeterministicRSA(opts KeyOpts) (*rsa.PrivateKey, yaerrors.Error) { pBits := opts.Bits / bits qBits := opts.Bits - pBits - e := big.NewInt(int64(opts.E)) + e := big.NewInt(int64(opts.Exponent)) var ( p, q *big.Int @@ -283,7 +283,12 @@ func ParsePrivateKey(s string) (*rsa.PrivateKey, yaerrors.Error) { input := strings.TrimSpace(s) if looksLikePEMPrivateKey(input) { - return parsePrivateKey([]byte(input)) + key, err := parsePrivateKey([]byte(input)) + if err != nil { + return nil, err.Wrap("[RSA] failed to parse private PEM key") + } + + return key, nil } noCRLF := StripCRLF(input) @@ -301,14 +306,19 @@ func ParsePrivateKey(s string) (*rsa.PrivateKey, yaerrors.Error) { } if looksLikePEMPrivateKey(string(decoded)) { - return parsePrivateKey(decoded) + key, err := parsePrivateKey(decoded) + if err != nil { + return nil, err.Wrap("[RSA] failed to parse private PEM key") + } + + return key, nil } - if key, yaErr := parsePKCS1DER(decoded); yaErr == nil { + if key, yaerr := parsePKCS1DER(decoded); yaerr == nil { return key, nil } - if key, yaErr := parsePKCS8DER(decoded); yaErr == nil { + if key, yaerr := parsePKCS8DER(decoded); yaerr == nil { return key, nil } diff --git a/yarsa/key_test.go b/yarsa/key_test.go index 547581e..00cf168 100644 --- a/yarsa/key_test.go +++ b/yarsa/key_test.go @@ -32,10 +32,10 @@ func Test_GenerateDeterministicRSA_Determinism(t *testing.T) { seed := []byte("correct-horse-battery-staple") - key1, err := yarsa.GenerateDeterministicRSA(yarsa.KeyOpts{Bits: bits, E: 65537, Seed: seed}) + key1, err := yarsa.GenerateDeterministicRSAPrivateKey(yarsa.KeyOpts{Bits: bits, Exponent: 65537, Seed: seed}) require.NoError(t, err) - key2, err := yarsa.GenerateDeterministicRSA(yarsa.KeyOpts{Bits: bits, E: 65537, Seed: seed}) + key2, err := yarsa.GenerateDeterministicRSAPrivateKey(yarsa.KeyOpts{Bits: bits, Exponent: 65537, Seed: seed}) require.NoError(t, err) assert.Equal( @@ -57,10 +57,10 @@ func Test_GenerateDeterministicRSA_DifferentSeedsDiffer(t *testing.T) { seedA := []byte("seed-A") seedB := []byte("seed-B") - keyA, err := yarsa.GenerateDeterministicRSA(yarsa.KeyOpts{Bits: bits, E: 65537, Seed: seedA}) + keyA, err := yarsa.GenerateDeterministicRSAPrivateKey(yarsa.KeyOpts{Bits: bits, Exponent: 65537, Seed: seedA}) require.NoError(t, err) - keyB, err := yarsa.GenerateDeterministicRSA(yarsa.KeyOpts{Bits: bits, E: 65537, Seed: seedB}) + keyB, err := yarsa.GenerateDeterministicRSAPrivateKey(yarsa.KeyOpts{Bits: bits, Exponent: 65537, Seed: seedB}) require.NoError(t, err) assert.NotEqual( @@ -76,8 +76,8 @@ func Test_GenerateDeterministicRSA_DifferentSeedsDiffer(t *testing.T) { func Test_GenerateDeterministicRSA_DefaultExponent_And_PrimeOrder(t *testing.T) { t.Parallel() - key, err := yarsa.GenerateDeterministicRSA( - yarsa.KeyOpts{Bits: 2048, E: 0, Seed: []byte("exp-default")}, + key, err := yarsa.GenerateDeterministicRSAPrivateKey( + yarsa.KeyOpts{Bits: 2048, Exponent: 0, Seed: []byte("exp-default")}, ) require.NoError(t, err) @@ -96,8 +96,8 @@ func Test_GenerateDeterministicRSA_MultiBitLengths(t *testing.T) { t.Run(fmt.Sprintf("bits=%d", bits), func(t *testing.T) { t.Parallel() - key, err := yarsa.GenerateDeterministicRSA( - yarsa.KeyOpts{Bits: bits, E: 65537, Seed: []byte("multi")}, + key, err := yarsa.GenerateDeterministicRSAPrivateKey( + yarsa.KeyOpts{Bits: bits, Exponent: 65537, Seed: []byte("multi")}, ) require.NoError(t, err) @@ -112,10 +112,10 @@ func Test_GenerateDeterministicRSA_MultiBitLengths(t *testing.T) { func Test_GenerateDeterministicRSA_InvalidOpts(t *testing.T) { t.Parallel() - _, err := yarsa.GenerateDeterministicRSA(yarsa.KeyOpts{Bits: 511, E: 65537, Seed: []byte("x")}) + _, err := yarsa.GenerateDeterministicRSAPrivateKey(yarsa.KeyOpts{Bits: 511, Exponent: 65537, Seed: []byte("x")}) assert.Error(t, err, "odd bit length should fail") - _, err = yarsa.GenerateDeterministicRSA(yarsa.KeyOpts{Bits: 2048, E: 65537, Seed: nil}) + _, err = yarsa.GenerateDeterministicRSAPrivateKey(yarsa.KeyOpts{Bits: 2048, Exponent: 65537, Seed: nil}) assert.Error(t, err, "missing seed should fail") } diff --git a/yarsa/reader.go b/yarsa/reader.go index 0fa5238..7531f2b 100644 --- a/yarsa/reader.go +++ b/yarsa/reader.go @@ -34,7 +34,7 @@ import ( type DeterministicReader struct { seed []byte counter uint64 - buf []byte + buf [32]byte pos int } @@ -63,7 +63,7 @@ func NewDeterministicReader(seed []byte) *DeterministicReader { func (r *DeterministicReader) Read(p []byte) (int, error) { written := 0 for written < len(p) { - if r.buf == nil || r.pos >= len(r.buf) { + if r.pos >= len(r.buf) { r.refill() } @@ -96,7 +96,7 @@ func (r *DeterministicReader) refill() { binary.BigEndian.PutUint64(ctrBytes[:], r.counter) mac.Write(ctrBytes[:]) - r.buf = mac.Sum(nil) + copy(r.buf[:], mac.Sum(nil)) r.pos = 0 diff --git a/yarsa/yarsa.go b/yarsa/yarsa.go index bf32cac..134aa24 100644 --- a/yarsa/yarsa.go +++ b/yarsa/yarsa.go @@ -74,8 +74,8 @@ func Encrypt(plaintext []byte, public *rsa.PublicKey) ([]byte, yaerrors.Error) { const padding = 2 - maxChunk := public.Size() - padding*sha256.Size - padding - if maxChunk <= 0 { + chunksCount := public.Size() - padding*sha256.Size - padding + if chunksCount <= 0 { return nil, yaerrors.FromString( http.StatusInternalServerError, "[RSA] invalid OAEP max chunk size", @@ -84,8 +84,8 @@ func Encrypt(plaintext []byte, public *rsa.PublicKey) ([]byte, yaerrors.Error) { var out []byte - for i := 0; i < len(plaintext); i += maxChunk { - end := i + maxChunk + for i := 0; i < len(plaintext); i += chunksCount { + end := i + chunksCount end = min(end, len(plaintext)) @@ -131,17 +131,11 @@ func Decrypt(ciphertext []byte, private *rsa.PrivateKey) ([]byte, yaerrors.Error label := []byte{} blockSize := private.Size() - if blockSize <= 0 { - return nil, yaerrors.FromString( - http.StatusInternalServerError, - "[RSA] invalid RSA modulus size", - ) - } if len(ciphertext)%blockSize != 0 { return nil, yaerrors.FromString( http.StatusInternalServerError, - "[RSA] ciphertext length is not a multiple of RSA block size (expected exact 256-byte blocks)", + "[RSA] ciphertext length is not a multiple of RSA block size (expected exact {block size}-byte blocks)", ) } From ac3771b17dc80e05dbc9f6489308ef2f0d3da452 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Sat, 4 Oct 2025 18:17:34 +0300 Subject: [PATCH 27/36] style(lint): fix --- yabase64/yabase64.go | 2 +- yagzip/yagzip.go | 6 +++++- yalocales/utils.go | 2 +- yarsa/key_test.go | 24 ++++++++++++++++++------ 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/yabase64/yabase64.go b/yabase64/yabase64.go index dd0d27b..6f785fa 100644 --- a/yabase64/yabase64.go +++ b/yabase64/yabase64.go @@ -85,7 +85,6 @@ func Encode[T any](v T) (string, yaerrors.Error) { } return base64.StdEncoding.EncodeToString(buf.Bytes()), nil - } // Decode decodes a base64-encoded gob string into a Go struct of type T. @@ -122,6 +121,7 @@ func Decode[T any](base string) (*T, yaerrors.Error) { } var v T + dec := gob.NewDecoder(bytes.NewReader(data)) if err := dec.Decode(&v); err != nil { return nil, yaerrors.FromError( diff --git a/yagzip/yagzip.go b/yagzip/yagzip.go index 2296cfc..e62dc27 100644 --- a/yagzip/yagzip.go +++ b/yagzip/yagzip.go @@ -63,7 +63,11 @@ func (g *Gzip) Zip(object []byte) ([]byte, yaerrors.Error) { w, err := gzip.NewWriterLevel(&buf, g.Level) if err != nil { - return nil, yaerrors.FromError(http.StatusInternalServerError, err, "[GZIP] failed to create write") + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[GZIP] failed to create write", + ) } _, err = w.Write(object) diff --git a/yalocales/utils.go b/yalocales/utils.go index 428fbaf..a8850d0 100644 --- a/yalocales/utils.go +++ b/yalocales/utils.go @@ -57,7 +57,7 @@ func setDiff(a, b map[string]struct{}) (missingInB []string, extraInB []string) } } - return + return missingInB, extraInB } func subtractSets(a, b map[string]struct{}) []string { diff --git a/yarsa/key_test.go b/yarsa/key_test.go index 00cf168..0b76b19 100644 --- a/yarsa/key_test.go +++ b/yarsa/key_test.go @@ -32,10 +32,14 @@ func Test_GenerateDeterministicRSA_Determinism(t *testing.T) { seed := []byte("correct-horse-battery-staple") - key1, err := yarsa.GenerateDeterministicRSAPrivateKey(yarsa.KeyOpts{Bits: bits, Exponent: 65537, Seed: seed}) + key1, err := yarsa.GenerateDeterministicRSAPrivateKey( + yarsa.KeyOpts{Bits: bits, Exponent: 65537, Seed: seed}, + ) require.NoError(t, err) - key2, err := yarsa.GenerateDeterministicRSAPrivateKey(yarsa.KeyOpts{Bits: bits, Exponent: 65537, Seed: seed}) + key2, err := yarsa.GenerateDeterministicRSAPrivateKey( + yarsa.KeyOpts{Bits: bits, Exponent: 65537, Seed: seed}, + ) require.NoError(t, err) assert.Equal( @@ -57,10 +61,14 @@ func Test_GenerateDeterministicRSA_DifferentSeedsDiffer(t *testing.T) { seedA := []byte("seed-A") seedB := []byte("seed-B") - keyA, err := yarsa.GenerateDeterministicRSAPrivateKey(yarsa.KeyOpts{Bits: bits, Exponent: 65537, Seed: seedA}) + keyA, err := yarsa.GenerateDeterministicRSAPrivateKey( + yarsa.KeyOpts{Bits: bits, Exponent: 65537, Seed: seedA}, + ) require.NoError(t, err) - keyB, err := yarsa.GenerateDeterministicRSAPrivateKey(yarsa.KeyOpts{Bits: bits, Exponent: 65537, Seed: seedB}) + keyB, err := yarsa.GenerateDeterministicRSAPrivateKey( + yarsa.KeyOpts{Bits: bits, Exponent: 65537, Seed: seedB}, + ) require.NoError(t, err) assert.NotEqual( @@ -112,10 +120,14 @@ func Test_GenerateDeterministicRSA_MultiBitLengths(t *testing.T) { func Test_GenerateDeterministicRSA_InvalidOpts(t *testing.T) { t.Parallel() - _, err := yarsa.GenerateDeterministicRSAPrivateKey(yarsa.KeyOpts{Bits: 511, Exponent: 65537, Seed: []byte("x")}) + _, err := yarsa.GenerateDeterministicRSAPrivateKey( + yarsa.KeyOpts{Bits: 511, Exponent: 65537, Seed: []byte("x")}, + ) assert.Error(t, err, "odd bit length should fail") - _, err = yarsa.GenerateDeterministicRSAPrivateKey(yarsa.KeyOpts{Bits: 2048, Exponent: 65537, Seed: nil}) + _, err = yarsa.GenerateDeterministicRSAPrivateKey( + yarsa.KeyOpts{Bits: 2048, Exponent: 65537, Seed: nil}, + ) assert.Error(t, err, "missing seed should fail") } From e7108f4852d1490f0a3dd1fbcab2af112c2064da Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Sat, 4 Oct 2025 18:23:47 +0300 Subject: [PATCH 28/36] chore(yaginmiddleware): fix docs type encode --- yaginmiddleware/yaginmiddleware.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yaginmiddleware/yaginmiddleware.go b/yaginmiddleware/yaginmiddleware.go index da362a1..686a703 100644 --- a/yaginmiddleware/yaginmiddleware.go +++ b/yaginmiddleware/yaginmiddleware.go @@ -22,7 +22,7 @@ type Middleware interface { // 1. base64-decodes the header value, // 2. decrypts with the server RSA private key, // 3. gunzips the result, -// 4. decodes base64(JSON(T)), +// 4. decodes base64(T), // 5. stores *T in Gin context under the provided CtxKey. // // Server-side flow (what the middleware does): @@ -31,12 +31,12 @@ type Middleware interface { // - base64 -> []byte. // - RSA decrypt with RSAKey (private) -> zipped []byte. // - gunzip -> plaintext []byte. -// - base64(JSON(T)) -> *T. +// - base64(T) -> *T. // - ctx.Set(CtxKey, *T), then continue the handler chain. // // Client-side flow (how to produce the header): // - Take value T. -// - Encode as base64(JSON(T)). +// - Encode as base64(T). // - gzip the bytes. // - RSA encrypt with the server's public key. // - Convert to base64 string; send it in the HTTP header named HeaderKey. From 9a962dda3c020d5ac740be8348f19b47fcec384d Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Mon, 6 Oct 2025 13:59:35 +0300 Subject: [PATCH 29/36] feat(yaencoding): change name and support msg pack --- go.mod | 2 + go.sum | 4 + yabase64/yabase64.go | 182 ----------------------------- yabase64/yabase64_test.go | 36 ------ yaencoding/yaencoding.go | 175 +++++++++++++++++++++++++++ yaencoding/yaencoding_test.go | 103 ++++++++++++++++ yaginmiddleware/yaginmiddleware.go | 10 +- 7 files changed, 289 insertions(+), 223 deletions(-) delete mode 100644 yabase64/yabase64.go delete mode 100644 yabase64/yabase64_test.go create mode 100644 yaencoding/yaencoding.go create mode 100644 yaencoding/yaencoding_test.go diff --git a/go.mod b/go.mod index 35543e6..c7a684f 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/redis/go-redis/v9 v9.11.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.11.1 + github.com/vmihailenco/msgpack/v5 v5.4.1 golang.org/x/net v0.42.0 golang.org/x/text v0.27.0 gorm.io/driver/sqlite v1.6.0 @@ -39,6 +40,7 @@ require ( github.com/quic-go/quic-go v0.54.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect go.uber.org/mock v0.5.0 // indirect golang.org/x/arch v0.20.0 // indirect google.golang.org/protobuf v1.36.9 // indirect diff --git a/go.sum b/go.sum index cd7c45a..0c8a39e 100644 --- a/go.sum +++ b/go.sum @@ -137,6 +137,10 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= diff --git a/yabase64/yabase64.go b/yabase64/yabase64.go deleted file mode 100644 index 6f785fa..0000000 --- a/yabase64/yabase64.go +++ /dev/null @@ -1,182 +0,0 @@ -// Package yabase64 provides tiny helpers to serialize a Go value as JSON and -// encode it using base64 (and the reverse operation). The API is intentionally -// minimal: -// -// - Encode[T any](v T) -> *bytes.Buffer holding the base64 text of JSON(v) -// - Decode[T any](s string) -> *T reconstructed from base64(JSON(T)) -// -// Notes: -// -// - The JSON encoder used by Encode writes a trailing newline by default -// (standard library behavior). This is preserved inside the base64 output. -// - The helpers are stateless and threadsafe. -// - Errors are returned as yaerrors.Error on decode and wrapped with HTTP 500 -// semantics to match the rest of your codebase. -// -// Example (basic round-trip): -// -// var data = struct { -// ID int `json:"id"` -// Name string `json:"name"` -// }{ID: 7, Name: "RZK"} -// -// // Encode - base64(JSON(data)) -// buf, err := yabase64.Encode(data) -// if err != nil { -// log.Fatalf("encode failed: %v", err) -// } -// b64 := buf.String() -// -// // Decode - base64(JSON(T)) -// got, yaerr := yabase64.Decode[struct { -// ID int `json:"id"` -// Name string `json:"name"` -// }](b64) -// if yaerr != nil { -// log.Fatalf("decode failed: %v", yaerr) -// } -// -// fmt.Println(got.ID, got.Name) // 7 RZK -package yabase64 - -import ( - "bytes" - "encoding/base64" - "encoding/gob" - "fmt" - "net/http" - - "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" -) - -// Encode serializes a Go value using gob and base64-encodes the resulting bytes. -// -// Returns: -// - *bytes.Buffer containing the base64-encoded gob data. -// - yaerrors.Error wrapping the underlying cause (if any). -// -// Behavior: -// - The returned buffer contains valid base64 text that represents gob-encoded data. -// - The encoding is Go-specific and can only be decoded by Go using gob. -// - The buffer is owned by the caller and can be accessed via Bytes() or String(). -// -// Example: -// -// type Payload struct { -// Token string -// ID int -// } -// -// buf, err := yabase64.Encode(Payload{Token: "abc", ID: 42}) -// if err != nil { -// log.Fatalf("encode failed: %v", err) -// } -// fmt.Println(buf.String()) // e.g. "GgAAAAVQYXlsb2FkAgAAAAZhYmMIAAAAqg==" -func Encode[T any](v T) (string, yaerrors.Error) { - var buf bytes.Buffer - - enc := gob.NewEncoder(&buf) - if err := enc.Encode(v); err != nil { - return "", yaerrors.FromError( - http.StatusInternalServerError, - err, - fmt.Sprintf("[BASE64] failed to encode `%T` using gob", v), - ) - } - - return base64.StdEncoding.EncodeToString(buf.Bytes()), nil -} - -// Decode decodes a base64-encoded gob string into a Go struct of type T. -// -// Parameters: -// - base: Base64-encoded string created by Encode[T]. -// -// Returns: -// - *T on success. -// - yaerrors.Error on failure (e.g., invalid base64 or gob data). -// -// Example: -// -// type User struct { -// ID int -// Name string -// } -// -// encoded, _ := yabase64.Encode(User{ID: 42, Name: "Alice"}) -// -// u, err := yabase64.Decode[User](encoded.String()) -// if err != nil { -// log.Fatalf("decode failed: %v", err) -// } -// fmt.Printf("%+v\n", u) // &{ID:42 Name:Alice} -func Decode[T any](base string) (*T, yaerrors.Error) { - data, err := base64.StdEncoding.DecodeString(base) - if err != nil { - return nil, yaerrors.FromError( - http.StatusInternalServerError, - err, - "[BASE64] failed to decode base64 string to bytes", - ) - } - - var v T - - dec := gob.NewDecoder(bytes.NewReader(data)) - if err := dec.Decode(&v); err != nil { - return nil, yaerrors.FromError( - http.StatusInternalServerError, - err, - fmt.Sprintf("[BASE64] failed to decode gob to `%T`", v), - ) - } - - return &v, nil -} - -// ToString encodes raw bytes to a base64 string (StdEncoding). -// -// Notes: -// - This is a low-level helper and does NOT perform JSON marshaling. -// - It is stateless and threadsafe. -// - Use when you already have []byte and just need a base64 string. -// -// Example: -// -// data := []byte("hello world") -// b64 := yabase64.ToString(data) -// fmt.Println(b64) // aGVsbG8gd29ybGQ= -func ToString(data []byte) string { - return base64.StdEncoding.EncodeToString(data) -} - -// ToBytes decodes a base64 string (StdEncoding) back to raw bytes. -// -// Returns: -// - []byte on success -// - yaerrors.Error on failure with HTTP 500 semantics -// -// Notes: -// - This is a low-level helper and does NOT perform JSON unmarshaling. -// - Useful for working with binary data stored as base64 text. -// -// Example: -// -// b64 := "aGVsbG8gd29ybGQ=" -// bytes, err := yabase64.ToBytes(b64) -// if err != nil { -// log.Fatalf("decode failed: %v", err) -// } -// fmt.Println(string(bytes)) // hello world -func ToBytes(data string) ([]byte, yaerrors.Error) { - bytes, err := base64.StdEncoding.DecodeString(data) - if err != nil { - return nil, yaerrors.FromError( - http.StatusInternalServerError, - err, - "failed to decode string to bytes", - ) - } - - return bytes, nil -} diff --git a/yabase64/yabase64_test.go b/yabase64/yabase64_test.go deleted file mode 100644 index 165b21c..0000000 --- a/yabase64/yabase64_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package yabase64_test - -import ( - "testing" - - "github.com/YaCodeDev/GoYaCodeDevUtils/yabase64" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestBase64_FlowWorks(t *testing.T) { - in := sample{ - ID: 7, - Name: "RZK", - Tags: []string{"a", "b", "c"}, - Meta: map[string]string{"k1": "v1", "k2": "v2"}, - Bytes: []byte{0, 1, 2, 250, 251, 252}, - } - - b64, err := yabase64.Encode(in) - require.NoError(t, err, "encode failed") - - out, yaerr := yabase64.Decode[sample](b64) - require.Nil(t, yaerr, "decode failed: %v", yaerr) - require.NotNil(t, out, "decoded value is nil") - - assert.Equal(t, in, *out, "mismatch after round-trip") -} - -type sample struct { - ID int `json:"id"` - Name string `json:"name"` - Tags []string `json:"tags"` - Meta map[string]string `json:"meta"` - Bytes []byte `json:"bytes"` -} diff --git a/yaencoding/yaencoding.go b/yaencoding/yaencoding.go new file mode 100644 index 0000000..754f118 --- /dev/null +++ b/yaencoding/yaencoding.go @@ -0,0 +1,175 @@ +// Package yaencoding provides helpers for encoding and decoding data +// using Gob or MessagePack formats with Base64 string representation. +// It simplifies safe transmission of Go structures through text mediums (e.g., JSON, HTTP). +// +// Each encode/decode returns yaerrors.Error to unify structured error handling +// in backend systems that use GoYaCodeDevUtils. +// +// Supported formats: +// - Gob (native Go binary serialization) +// - MessagePack (efficient binary encoding similar to Protobuf) +// +// Example usage: +// +// type User struct { +// ID int +// Name string +// } +// +// // GOB Example +// user := User{ID: 1, Name: "Alice"} +// +// encoded, err := yaencoding.EncodeGob(user) +// if err != nil { +// log.Fatalf("encode failed: %v", err) +// } +// +// decoded, err := yaencoding.DecodeGob[User](encoded) +// if err != nil { +// log.Fatalf("decode failed: %v", err) +// } +// +// fmt.Println(decoded.Name) // Output: Alice +// +// // MessagePack Example +// msgpackStr, err := yaencoding.EncodeMessagePack(user) +// if err != nil { +// log.Fatalf("encode failed: %v", err) +// } +// +// mpDecoded, err := yaencoding.DecodeMessagePack[User](msgpackStr) +// if err != nil { +// log.Fatalf("decode failed: %v", err) +// } +// +// fmt.Println(mpDecoded.ID) // Output: 1 +package yaencoding + +import ( + "bytes" + "encoding/base64" + "encoding/gob" + "fmt" + "net/http" + + "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" + "github.com/vmihailenco/msgpack/v5" +) + +// EncodeGob serializes the given value `v` using the Gob encoder, +// then base64-encodes the binary data into a string. +// +// Returns the base64 string or a wrapped yaerrors.Error on failure. +// +// Example: +// +// s := MyStruct{ID: 5, Name: "Ya Code"} +// str, err := yaencoding.EncodeGob(s) +func EncodeGob(v any) (string, yaerrors.Error) { + var buf bytes.Buffer + + enc := gob.NewEncoder(&buf) + if err := enc.Encode(v); err != nil { + return "", yaerrors.FromError( + http.StatusInternalServerError, + err, + fmt.Sprintf("[ENCODING] failed to encode `%T` using gob", v), + ) + } + + return base64.StdEncoding.EncodeToString(buf.Bytes()), nil +} + +// DecodeGob decodes a base64 string that represents Gob-encoded data +// back into a Go structure of type T. +// +// Example: +// +// out, err := yaencoding.DecodeGob[MyStruct](encoded) +func DecodeGob[T any](base string) (*T, yaerrors.Error) { + data, err := base64.StdEncoding.DecodeString(base) + if err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[ENCODING] failed to decode base64 string to bytes", + ) + } + + var v T + + dec := gob.NewDecoder(bytes.NewReader(data)) + if err := dec.Decode(&v); err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + fmt.Sprintf("[ENCODING] failed to decode gob to `%T`", v), + ) + } + + return &v, nil +} + +// EncodeMessagePack serializes `value` using the MessagePack format, +// then base64-encodes it for text-safe transport. +// +// Example: +// +// str, err := yaencoding.EncodeMessagePack(myStruct) +func EncodeMessagePack(value any) (string, yaerrors.Error) { + bytes, err := msgpack.Marshal(value) + if err != nil { + return "", yaerrors.FromError( + http.StatusInternalServerError, + err, + fmt.Sprintf("[ENCODING] failed to marshal %T using message pack format", value), + ) + } + + return ToString(bytes), nil +} + +// DecodeMessagePack decodes a Base64 string containing MessagePack data +// into a Go structure of type T. +// +// Example: +// +// val, err := yaencoding.DecodeMessagePack[User](encoded) +func DecodeMessagePack[T any](value string) (*T, yaerrors.Error) { + var res T + + bytes, err := ToBytes(value) + if err != nil { + return nil, err.Wrap("[ENCODING] failed to decode string as bytes") + } + + if err := msgpack.Unmarshal(bytes, &res); err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + fmt.Sprintf("[ENCODING] failed to marshal %T as message pack format", value), + ) + } + + return &res, nil +} + +// ToString converts a byte slice into a base64 string. +// Useful for manual conversions. +func ToString(data []byte) string { + return base64.StdEncoding.EncodeToString(data) +} + +// ToBytes decodes a base64 string into bytes. +func ToBytes(data string) ([]byte, yaerrors.Error) { + bytes, err := base64.StdEncoding.DecodeString(data) + if err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "[ENCODING] failed to decode string to bytes", + ) + } + + return bytes, nil +} diff --git a/yaencoding/yaencoding_test.go b/yaencoding/yaencoding_test.go new file mode 100644 index 0000000..2442e38 --- /dev/null +++ b/yaencoding/yaencoding_test.go @@ -0,0 +1,103 @@ +package yaencoding_test + +import ( + "testing" + + "github.com/YaCodeDev/GoYaCodeDevUtils/yaencoding" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type sample struct { + ID int + Name string + Tags []string + Meta map[string]string + Bytes []byte +} + +func TestGobEncoding_Flow(t *testing.T) { + t.Run("Full Round Trip", func(t *testing.T) { + in := sample{ + ID: 7, + Name: "RZK", + Tags: []string{"a", "b", "c"}, + Meta: map[string]string{"k1": "v1", "k2": "v2"}, + Bytes: []byte{0, 1, 2, 250, 251, 252}, + } + + b64, err := yaencoding.EncodeGob(in) + require.NoError(t, err, "encode failed") + + out, yaerr := yaencoding.DecodeGob[sample](b64) + require.Nil(t, yaerr, "decode failed") + require.NotNil(t, out, "decoded value is nil") + + assert.Equal(t, in, *out, "mismatch after round-trip") + }) + + t.Run("Invalid Base64 Returns Error", func(t *testing.T) { + out, err := yaencoding.DecodeGob[sample]("!!!INVALID!!!") + require.Nil(t, out) + require.NotNil(t, err) + assert.Contains(t, err.Error(), "failed to decode base64") + }) + + t.Run("Invalid Gob Data Returns Error", func(t *testing.T) { + invalid := yaencoding.ToString([]byte("not-gob-data")) + out, err := yaencoding.DecodeGob[sample](invalid) + require.Nil(t, out) + require.NotNil(t, err) + assert.Contains(t, err.Error(), "failed to decode gob") + }) + + t.Run("Utility ToString-ToBytes Round Trip", func(t *testing.T) { + data := []byte{1, 2, 3, 4, 5} + str := yaencoding.ToString(data) + res, err := yaencoding.ToBytes(str) + require.Nil(t, err) + assert.Equal(t, data, res) + }) + + t.Run("Utility ToBytes Invalid Input", func(t *testing.T) { + res, err := yaencoding.ToBytes("!!bad!!") + require.Nil(t, res) + require.NotNil(t, err) + assert.Contains(t, err.Error(), "failed to decode string to bytes") + }) +} + +func TestMessagePackEncoding_Flow(t *testing.T) { + t.Run("Full Encode/Decode Round Trip", func(t *testing.T) { + in := sample{ + ID: 42, + Name: "YaCode", + Tags: []string{"x", "y"}, + Meta: map[string]string{"foo": "bar"}, + Bytes: []byte{1, 2, 3}, + } + + str, err := yaencoding.EncodeMessagePack(in) + require.NoError(t, err, "encode failed") + + out, yaerr := yaencoding.DecodeMessagePack[sample](str) + require.Nil(t, yaerr, "decode failed") + require.NotNil(t, out) + assert.Equal(t, in, *out) + }) + + t.Run("Invalid Base64 Returns Error", func(t *testing.T) { + out, err := yaencoding.DecodeMessagePack[sample]("!invalid-base64") + require.Nil(t, out) + require.NotNil(t, err) + assert.Contains(t, err.Error(), "failed to decode string as bytes") + }) + + t.Run("Invalid MessagePack Data Returns Error", func(t *testing.T) { + b64 := yaencoding.ToString([]byte("not-msgpack-data")) + out, err := yaencoding.DecodeMessagePack[sample](b64) + require.Nil(t, out) + require.NotNil(t, err) + assert.Contains(t, err.Error(), "failed to marshal") + }) +} diff --git a/yaginmiddleware/yaginmiddleware.go b/yaginmiddleware/yaginmiddleware.go index 686a703..1796ae4 100644 --- a/yaginmiddleware/yaginmiddleware.go +++ b/yaginmiddleware/yaginmiddleware.go @@ -5,7 +5,7 @@ import ( "crypto/rsa" "net/http" - "github.com/YaCodeDev/GoYaCodeDevUtils/yabase64" + "github.com/YaCodeDev/GoYaCodeDevUtils/yaencoding" "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" "github.com/YaCodeDev/GoYaCodeDevUtils/yagzip" "github.com/YaCodeDev/GoYaCodeDevUtils/yarsa" @@ -111,7 +111,7 @@ func NewEncodeRSA[T any]( // if err != nil { log.Fatal(err) } // req.Header.Set("X-Enc", headerValue) func (e *RSASecureHeader[T]) Encode(data any) (string, yaerrors.Error) { - bytes, err := yabase64.Encode(data) + bytes, err := yaencoding.EncodeMessagePack(data) if err != nil { return "", err.Wrap("[RSA HEADER] failed to encode data to bytes") } @@ -126,7 +126,7 @@ func (e *RSASecureHeader[T]) Encode(data any) (string, yaerrors.Error) { return "", err.Wrap("[RSA HEADER] failed to encrypt zipped") } - return yabase64.ToString(rsa), nil + return yaencoding.ToString(rsa), nil } // Decode reverses Encode. It accepts a base64 string (as produced by Encode), @@ -141,7 +141,7 @@ func (e *RSASecureHeader[T]) Encode(data any) (string, yaerrors.Error) { // if err != nil { log.Fatal(err) } // fmt.Println(got.ID) func (e *RSASecureHeader[T]) Decode(data string) (*T, yaerrors.Error) { - bytes, err := yabase64.ToBytes(data) + bytes, err := yaencoding.ToBytes(data) if err != nil { return nil, err.Wrap("failed to decode string to bytes") } @@ -163,7 +163,7 @@ func (e *RSASecureHeader[T]) Decode(data string) (*T, yaerrors.Error) { return nil, err.Wrap("[RSA HEADER] failed to get plain text from zip") } - res, err := yabase64.Decode[T](string(plaintext)) + res, err := yaencoding.DecodeMessagePack[T](string(plaintext)) if err != nil { return nil, err.Wrap("[RSA HEADER] failed to decode plaintext") } From bbcd721a5cc404921ad7d62dfbc5bffac6a453e9 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Tue, 7 Oct 2025 14:48:12 +0300 Subject: [PATCH 30/36] feat(yaencoding): return bytes instead base64 --- yaencoding/yaencoding.go | 32 ++-- yaencoding/yaencoding_test.go | 16 +- yaginmiddleware/yaginmiddleware.go | 249 +++++++++++++++++++---------- 3 files changed, 182 insertions(+), 115 deletions(-) diff --git a/yaencoding/yaencoding.go b/yaencoding/yaencoding.go index 754f118..25f9908 100644 --- a/yaencoding/yaencoding.go +++ b/yaencoding/yaencoding.go @@ -65,19 +65,19 @@ import ( // // s := MyStruct{ID: 5, Name: "Ya Code"} // str, err := yaencoding.EncodeGob(s) -func EncodeGob(v any) (string, yaerrors.Error) { +func EncodeGob(v any) ([]byte, yaerrors.Error) { var buf bytes.Buffer enc := gob.NewEncoder(&buf) if err := enc.Encode(v); err != nil { - return "", yaerrors.FromError( + return nil, yaerrors.FromError( http.StatusInternalServerError, err, fmt.Sprintf("[ENCODING] failed to encode `%T` using gob", v), ) } - return base64.StdEncoding.EncodeToString(buf.Bytes()), nil + return buf.Bytes(), nil } // DecodeGob decodes a base64 string that represents Gob-encoded data @@ -86,16 +86,7 @@ func EncodeGob(v any) (string, yaerrors.Error) { // Example: // // out, err := yaencoding.DecodeGob[MyStruct](encoded) -func DecodeGob[T any](base string) (*T, yaerrors.Error) { - data, err := base64.StdEncoding.DecodeString(base) - if err != nil { - return nil, yaerrors.FromError( - http.StatusInternalServerError, - err, - "[ENCODING] failed to decode base64 string to bytes", - ) - } - +func DecodeGob[T any](data []byte) (*T, yaerrors.Error) { var v T dec := gob.NewDecoder(bytes.NewReader(data)) @@ -116,17 +107,17 @@ func DecodeGob[T any](base string) (*T, yaerrors.Error) { // Example: // // str, err := yaencoding.EncodeMessagePack(myStruct) -func EncodeMessagePack(value any) (string, yaerrors.Error) { +func EncodeMessagePack(value any) ([]byte, yaerrors.Error) { bytes, err := msgpack.Marshal(value) if err != nil { - return "", yaerrors.FromError( + return nil, yaerrors.FromError( http.StatusInternalServerError, err, fmt.Sprintf("[ENCODING] failed to marshal %T using message pack format", value), ) } - return ToString(bytes), nil + return bytes, nil } // DecodeMessagePack decodes a Base64 string containing MessagePack data @@ -135,19 +126,14 @@ func EncodeMessagePack(value any) (string, yaerrors.Error) { // Example: // // val, err := yaencoding.DecodeMessagePack[User](encoded) -func DecodeMessagePack[T any](value string) (*T, yaerrors.Error) { +func DecodeMessagePack[T any](bytes []byte) (*T, yaerrors.Error) { var res T - bytes, err := ToBytes(value) - if err != nil { - return nil, err.Wrap("[ENCODING] failed to decode string as bytes") - } - if err := msgpack.Unmarshal(bytes, &res); err != nil { return nil, yaerrors.FromError( http.StatusInternalServerError, err, - fmt.Sprintf("[ENCODING] failed to marshal %T as message pack format", value), + fmt.Sprintf("[ENCODING] failed to marshal %T as message pack format", bytes), ) } diff --git a/yaencoding/yaencoding_test.go b/yaencoding/yaencoding_test.go index 2442e38..de75b98 100644 --- a/yaencoding/yaencoding_test.go +++ b/yaencoding/yaencoding_test.go @@ -37,15 +37,13 @@ func TestGobEncoding_Flow(t *testing.T) { }) t.Run("Invalid Base64 Returns Error", func(t *testing.T) { - out, err := yaencoding.DecodeGob[sample]("!!!INVALID!!!") + out, err := yaencoding.DecodeGob[sample]([]byte("!!!INVALID!!!")) require.Nil(t, out) require.NotNil(t, err) - assert.Contains(t, err.Error(), "failed to decode base64") }) t.Run("Invalid Gob Data Returns Error", func(t *testing.T) { - invalid := yaencoding.ToString([]byte("not-gob-data")) - out, err := yaencoding.DecodeGob[sample](invalid) + out, err := yaencoding.DecodeGob[sample]([]byte("not-gob-data")) require.Nil(t, out) require.NotNil(t, err) assert.Contains(t, err.Error(), "failed to decode gob") @@ -77,25 +75,23 @@ func TestMessagePackEncoding_Flow(t *testing.T) { Bytes: []byte{1, 2, 3}, } - str, err := yaencoding.EncodeMessagePack(in) + bytes, err := yaencoding.EncodeMessagePack(in) require.NoError(t, err, "encode failed") - out, yaerr := yaencoding.DecodeMessagePack[sample](str) + out, yaerr := yaencoding.DecodeMessagePack[sample](bytes) require.Nil(t, yaerr, "decode failed") require.NotNil(t, out) assert.Equal(t, in, *out) }) t.Run("Invalid Base64 Returns Error", func(t *testing.T) { - out, err := yaencoding.DecodeMessagePack[sample]("!invalid-base64") + out, err := yaencoding.DecodeMessagePack[sample]([]byte("!invalid-base64")) require.Nil(t, out) require.NotNil(t, err) - assert.Contains(t, err.Error(), "failed to decode string as bytes") }) t.Run("Invalid MessagePack Data Returns Error", func(t *testing.T) { - b64 := yaencoding.ToString([]byte("not-msgpack-data")) - out, err := yaencoding.DecodeMessagePack[sample](b64) + out, err := yaencoding.DecodeMessagePack[sample]([]byte("not-msgpack-data")) require.Nil(t, out) require.NotNil(t, err) assert.Contains(t, err.Error(), "failed to marshal") diff --git a/yaginmiddleware/yaginmiddleware.go b/yaginmiddleware/yaginmiddleware.go index 1796ae4..126c00a 100644 --- a/yaginmiddleware/yaginmiddleware.go +++ b/yaginmiddleware/yaginmiddleware.go @@ -1,4 +1,80 @@ -// Package yamiddleware exposes small Gin middlewares. +// Package yamiddleware provides ready-to-use Gin middlewares and helpers +// for secure, structured HTTP communication. +// +// The main feature in this package is RSASecureHeader — a generic middleware +// that allows you to transparently transmit compact, encrypted payloads inside +// HTTP headers using the following transformation pipeline: +// +// Go struct -> MessagePack -> gzip -> RSA encrypt -> base64 -> HTTP header +// +// On the receiving end, RSASecureHeader middleware automatically: +// - Reads the encrypted header from the request +// - Decrypts and decompresses the payload +// - Decodes the MessagePack bytes into the original Go type +// - Injects the resulting object into gin.Context under a configurable key +// +// This pattern is especially useful when you want to safely transmit +// small request-bound data without exposing secrets or relying on JWTs. +// +// Example end-to-end usage: +// +// package main +// +// import ( +// "crypto/rand" +// "crypto/rsa" +// "fmt" +// "net/http" +// +// "github.com/gin-gonic/gin" +// "github.com/YaCodeDev/GoYaCodeDevUtils/yaginmiddleware" +// ) +// +// type Session struct { +// UserID uint64 +// Token string +// } +// +// func main() { +// key, _ := rsa.GenerateKey(rand.Reader, 2048) +// +// // Create RSA-secured header middleware +// secureHeader := yaginmiddleware.NewEncodeRSA[Session]( +// "X-Secure", // header name +// "session", // context key +// key, // RSA private key +// true, // abort if decoding fails +// ) +// +// // Setup Gin engine +// r := gin.Default() +// r.Use(secureHeader.Handle) +// +// // Example route reading decoded payload +// r.GET("/me", func(c *gin.Context) { +// v, _ := c.Get("session") +// sess := v.(*Session) +// c.JSON(http.StatusOK, gin.H{ +// "user": sess.UserID, +// "token": sess.Token, +// }) +// }) +// +// // Example: encode outgoing header client-side +// s := Session{UserID: 10, Token: "abc123"} +// enc, _ := secureHeader.Encode(s) +// fmt.Println("Attach header X-Secure:", enc) +// +// _ = r.Run(":8080") +// } +// +// Internally, RSASecureHeader relies on these YaCodeDev utilities: +// - yaencoding — MessagePack serialization / base64 helpers +// - yagzip — gzip compression +// - yarsa — RSA chunk encryption +// - yaerrors — structured error wrapping +// +// Each step’s failure produces a yaerrors.Error for consistent handling. package yamiddleware import ( @@ -12,63 +88,55 @@ import ( "github.com/gin-gonic/gin" ) -// Middleware is a minimal interface implemented by all Gin middlewares here. +// Middleware defines the standard contract for middlewares in this package. +// Every middleware must implement the Handle(*gin.Context) method. type Middleware interface { Handle(ctx *gin.Context) } -// RSASecureHeader[T] reads a request header that carries an RSA-encrypted, -// base64 string; it then: -// 1. base64-decodes the header value, -// 2. decrypts with the server RSA private key, -// 3. gunzips the result, -// 4. decodes base64(T), -// 5. stores *T in Gin context under the provided CtxKey. -// -// Server-side flow (what the middleware does): -// - Read header with name HeaderKey. -// - Normalize it (remove CR/LF; trim spaces). -// - base64 -> []byte. -// - RSA decrypt with RSAKey (private) -> zipped []byte. -// - gunzip -> plaintext []byte. -// - base64(T) -> *T. -// - ctx.Set(CtxKey, *T), then continue the handler chain. -// -// Client-side flow (how to produce the header): -// - Take value T. -// - Encode as base64(T). -// - gzip the bytes. -// - RSA encrypt with the server's public key. -// - Convert to base64 string; send it in the HTTP header named HeaderKey. -// -// Security/format notes: -// - RSA padding/mode must match your yarsa implementation (e.g., OAEP or PKCS#1 v1.5) on both sides. -// - Gzip is required; if the decrypted bytes are not a gzip stream, decompression fails. -// - The header value is base64 text; newlines and carriage returns are removed automatically. -// -// Example (client-side: produce the header): +// RSASecureHeader provides RSA-encrypted header transmission for structured payloads. // -// key, _ := rsa.GenerateKey(rand.Reader, 2048) -// mw := yamiddleware.NewEncodeRSA[MyPayload]("X-Enc", "payload", key) -// headerValue, _ := mw.Encode(MyPayload{ID: 1}, &key.PublicKey) -// // Send request with header: X-Enc: +// It transparently handles the following pipeline: +// 1. Marshal (MessagePack via yaencoding.EncodeMessagePack) +// 2. Compress (gzip via yagzip) +// 3. Encrypt (RSA via yarsa) +// 4. Base64 encode (via yaencoding.ToString) +// +// During decoding, this process is reversed. // -// Example (server-side: use with Gin): +// Typical use case: securely transmit small JSON/struct data through +// an HTTP header (e.g., "X-Enc") while ensuring confidentiality and integrity. // +// # Example +// +// // Generate RSA key // key, _ := rsa.GenerateKey(rand.Reader, 2048) -// mw := yamiddleware.NewEncodeRSA[MyPayload]("X-Enc", "payload", key) -// -// r := gin.New() -// r.Use(mw.Handle) -// -// r.GET("/ping", func(c *gin.Context) { -// v, ok := c.Get("payload") // "payload" == ContextKey -// if !ok { -// c.AbortWithStatus(http.StatusUnauthorized) -// return -// } -// payload := v.(*MyPayload) // type-safe by your generic T -// c.JSON(200, payload) +// +// // Create the middleware handler +// secureHeader := yamiddleware.NewEncodeRSA[MyPayload]( +// "X-Enc", // header name to read/write +// "payload", // context key to store decoded data +// key, // RSA private key +// true, // abort context if decoding fails +// ) +// +// // Encode example payload to header-safe string +// token, _ := secureHeader.Encode(MyPayload{ +// ID: 1, +// Name: "RZK", +// }) +// +// // Example: attach token in header and send request +// req := httptest.NewRequest(http.MethodGet, "/ping", nil) +// req.Header.Set("X-Enc", token) +// +// // Gin middleware automatically decodes and injects payload +// engine := gin.New() +// engine.Use(secureHeader.Handle) +// engine.GET("/ping", func(c *gin.Context) { +// val, _ := c.Get("payload") +// fmt.Println(val.(*MyPayload)) +// c.JSON(200, val) // }) type RSASecureHeader[T any] struct { RSA *rsa.PrivateKey @@ -77,13 +145,18 @@ type RSASecureHeader[T any] struct { ContextAbort bool } -// NewEncodeRSA constructs a new EncodeRSA[T] with the given header -// name, context key, and server RSA private key. +// NewEncodeRSA creates a new RSA-secured header middleware instance. +// +// Parameters: +// - headerName: name of the header that carries the encoded data +// - contextKey: name used in gin.Context for decoded payload +// - rsa: RSA private key (encryption uses rsa.PublicKey) +// - contextAbort: whether to call ctx.Abort() on decode failure // // Example: // // key, _ := rsa.GenerateKey(rand.Reader, 2048) -// middleware := yamiddleware.NewEncodeRSA[MyPayload]("X-Enc", "payload", key) +// header := yamiddleware.NewEncodeRSA[MyType]("X-Enc", "payload", key, true) func NewEncodeRSA[T any]( headerName string, contextKey string, @@ -98,25 +171,30 @@ func NewEncodeRSA[T any]( } } -// Encode prepares a header value suitable for sending to a server protected by -// EncodeRSA. It serializes data as base64(JSON), gzips the result, RSA -// encrypts it with the provided public key, and base64-encodes the final bytes. +// Encode serializes, compresses, encrypts, and base64-encodes the given data. +// +// The process: +// 1. MessagePack encode (using yaencoding.EncodeMessagePack) +// 2. Gzip compress (using yagzip.NewGzip().Zip) +// 3. RSA encrypt (using yarsa.Encrypt) +// 4. Base64 encode (using yaencoding.ToString) // -// On success it returns the header string. On failure it returns yaerrors.Error. +// Returns an encrypted header-safe string and possible yaerrors.Error. // // Example: // -// middleware := yamiddleware.NewEncodeRSA[Payload]("X-Enc", "payload", private) -// headerValue, err := middleware.Encode(Payload{ID: 7}, &private.PublicKey) -// if err != nil { log.Fatal(err) } -// req.Header.Set("X-Enc", headerValue) +// enc, err := header.Encode(MyStruct{Field: "value"}) +// if err != nil { +// log.Fatalf("encode failed: %v", err) +// } +// req.Header.Set("X-Enc", enc) func (e *RSASecureHeader[T]) Encode(data any) (string, yaerrors.Error) { bytes, err := yaencoding.EncodeMessagePack(data) if err != nil { return "", err.Wrap("[RSA HEADER] failed to encode data to bytes") } - zip, err := yagzip.NewGzip().Zip([]byte(bytes)) + zip, err := yagzip.NewGzip().Zip(bytes) if err != nil { return "", err.Wrap("[RSA HEADER] failed to zip bytes") } @@ -129,17 +207,21 @@ func (e *RSASecureHeader[T]) Encode(data any) (string, yaerrors.Error) { return yaencoding.ToString(rsa), nil } -// Decode reverses Encode. It accepts a base64 string (as produced by Encode), -// validates RSA block alignment, decrypts with the private key, ungzips, and -// unmarshals into *T. +// Decode performs the inverse process of Encode: +// 1. Base64 decode → bytes +// 2. RSA decrypt → zipped data +// 3. Gzip decompress → plaintext MessagePack +// 4. Decode MessagePack → typed struct `T` // -// On success it returns *T; otherwise yaerrors.Error. +// It returns a typed pointer to the decoded struct or an error. // // Example: // -// got, err := middleware.Decode(headerValue) -// if err != nil { log.Fatal(err) } -// fmt.Println(got.ID) +// out, err := header.Decode(encString) +// if err != nil { +// log.Fatalf("decode failed: %v", err) +// } +// fmt.Printf("Decoded struct: %+v\n", out) func (e *RSASecureHeader[T]) Decode(data string) (*T, yaerrors.Error) { bytes, err := yaencoding.ToBytes(data) if err != nil { @@ -163,7 +245,7 @@ func (e *RSASecureHeader[T]) Decode(data string) (*T, yaerrors.Error) { return nil, err.Wrap("[RSA HEADER] failed to get plain text from zip") } - res, err := yaencoding.DecodeMessagePack[T](string(plaintext)) + res, err := yaencoding.DecodeMessagePack[T](plaintext) if err != nil { return nil, err.Wrap("[RSA HEADER] failed to decode plaintext") } @@ -171,22 +253,25 @@ func (e *RSASecureHeader[T]) Decode(data string) (*T, yaerrors.Error) { return res, nil } -// Handle is the Gin middleware entrypoint. It reads the header named HeaderKey, -// cleans it up, decodes it with Decode, and stores the result under CtxKey in -// the Gin context. On error, it records the error, aborts the request, and does -// not call subsequent handlers. +// Handle implements gin.HandlerFunc. +// +// It reads the encrypted header, decrypts it, and injects the resulting +// struct pointer into the gin context using `ContextKey`. If decoding fails, +// the middleware will record the error in ctx.Errors and optionally abort +// further handler execution if ContextAbort is true. // // Example: // -// middleware := yamiddleware.NewEncodeRSA[Payload]("X-Enc", "payload", key) -// r := gin.New() -// r.Use(middleware.Handle) +// key, _ := rsa.GenerateKey(rand.Reader, 2048) +// header := yamiddleware.NewEncodeRSA[UserData]("X-User", "payload", key, true) +// +// engine := gin.New() +// engine.Use(header.Handle) // -// r.GET("/ping", func(c *gin.Context) { -// v, ok := c.Get("payload") -// if !ok { c.AbortWithStatus(http.StatusUnauthorized); return } -// payload := v.(*Payload) -// c.JSON(200, payload) +// engine.GET("/me", func(c *gin.Context) { +// val, _ := c.Get("payload") +// user := val.(*UserData) +// c.JSON(200, user) // }) func (e *RSASecureHeader[T]) Handle(ctx *gin.Context) { text := ctx.GetHeader(e.HeaderName) From 5df57bd52ceb227b3b046b13507c3d48c6d9ea9f Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Fri, 10 Oct 2025 00:17:25 +0300 Subject: [PATCH 31/36] feat(yaginmiddleware): update API --- yaginmiddleware/yaginmiddleware.go | 341 ++++++++++++------------ yaginmiddleware/yaginmiddleware_test.go | 6 +- 2 files changed, 173 insertions(+), 174 deletions(-) diff --git a/yaginmiddleware/yaginmiddleware.go b/yaginmiddleware/yaginmiddleware.go index 126c00a..4082825 100644 --- a/yaginmiddleware/yaginmiddleware.go +++ b/yaginmiddleware/yaginmiddleware.go @@ -1,83 +1,7 @@ -// Package yamiddleware provides ready-to-use Gin middlewares and helpers -// for secure, structured HTTP communication. -// -// The main feature in this package is RSASecureHeader — a generic middleware -// that allows you to transparently transmit compact, encrypted payloads inside -// HTTP headers using the following transformation pipeline: -// -// Go struct -> MessagePack -> gzip -> RSA encrypt -> base64 -> HTTP header -// -// On the receiving end, RSASecureHeader middleware automatically: -// - Reads the encrypted header from the request -// - Decrypts and decompresses the payload -// - Decodes the MessagePack bytes into the original Go type -// - Injects the resulting object into gin.Context under a configurable key -// -// This pattern is especially useful when you want to safely transmit -// small request-bound data without exposing secrets or relying on JWTs. -// -// Example end-to-end usage: -// -// package main -// -// import ( -// "crypto/rand" -// "crypto/rsa" -// "fmt" -// "net/http" -// -// "github.com/gin-gonic/gin" -// "github.com/YaCodeDev/GoYaCodeDevUtils/yaginmiddleware" -// ) -// -// type Session struct { -// UserID uint64 -// Token string -// } -// -// func main() { -// key, _ := rsa.GenerateKey(rand.Reader, 2048) -// -// // Create RSA-secured header middleware -// secureHeader := yaginmiddleware.NewEncodeRSA[Session]( -// "X-Secure", // header name -// "session", // context key -// key, // RSA private key -// true, // abort if decoding fails -// ) -// -// // Setup Gin engine -// r := gin.Default() -// r.Use(secureHeader.Handle) -// -// // Example route reading decoded payload -// r.GET("/me", func(c *gin.Context) { -// v, _ := c.Get("session") -// sess := v.(*Session) -// c.JSON(http.StatusOK, gin.H{ -// "user": sess.UserID, -// "token": sess.Token, -// }) -// }) -// -// // Example: encode outgoing header client-side -// s := Session{UserID: 10, Token: "abc123"} -// enc, _ := secureHeader.Encode(s) -// fmt.Println("Attach header X-Secure:", enc) -// -// _ = r.Run(":8080") -// } -// -// Internally, RSASecureHeader relies on these YaCodeDev utilities: -// - yaencoding — MessagePack serialization / base64 helpers -// - yagzip — gzip compression -// - yarsa — RSA chunk encryption -// - yaerrors — structured error wrapping -// -// Each step’s failure produces a yaerrors.Error for consistent handling. -package yamiddleware +package yaginmiddleware import ( + "bytes" "crypto/rsa" "net/http" @@ -88,55 +12,49 @@ import ( "github.com/gin-gonic/gin" ) -// Middleware defines the standard contract for middlewares in this package. -// Every middleware must implement the Handle(*gin.Context) method. +// Middleware represents a generic Gin middleware component +// capable of processing requests via a `Handle` method. type Middleware interface { Handle(ctx *gin.Context) } -// RSASecureHeader provides RSA-encrypted header transmission for structured payloads. +// RSASecureHeader is a generic Gin middleware that enables transparent, +// type-safe encryption and decryption of structured data in HTTP headers +// using RSA-OAEP + GZIP + MessagePack. // -// It transparently handles the following pipeline: -// 1. Marshal (MessagePack via yaencoding.EncodeMessagePack) -// 2. Compress (gzip via yagzip) -// 3. Encrypt (RSA via yarsa) -// 4. Base64 encode (via yaencoding.ToString) +// It provides methods to encode/decode any struct `T` into a secure, +// base64-encoded header value, and a middleware handler (`Handle`) that +// automatically decrypts incoming headers and injects the resulting struct +// into Gin’s request context. // -// During decoding, this process is reversed. +// Pipeline: // -// Typical use case: securely transmit small JSON/struct data through -// an HTTP header (e.g., "X-Enc") while ensuring confidentiality and integrity. +// struct -> MessagePack -> gzip -> RSA encrypt -> base64 +// base64 -> RSA decrypt -> gunzip -> MessagePack -> struct // -// # Example +// Example: +// +// type Payload struct { +// ID uint16 +// Text string +// } // -// // Generate RSA key // key, _ := rsa.GenerateKey(rand.Reader, 2048) +// header := yaginmiddleware.NewEncodeRSA[Payload]("X-Data", "payload", key, true) // -// // Create the middleware handler -// secureHeader := yamiddleware.NewEncodeRSA[MyPayload]( -// "X-Enc", // header name to read/write -// "payload", // context key to store decoded data -// key, // RSA private key -// true, // abort context if decoding fails -// ) -// -// // Encode example payload to header-safe string -// token, _ := secureHeader.Encode(MyPayload{ -// ID: 1, -// Name: "RZK", -// }) +// in := Payload{ID: 7, Text: "Hello"} +// enc, _ := header.Encode(in) // -// // Example: attach token in header and send request -// req := httptest.NewRequest(http.MethodGet, "/ping", nil) -// req.Header.Set("X-Enc", token) +// _, out, _ := header.Decode(enc) +// fmt.Println(out.Text) // "Hello" // -// // Gin middleware automatically decodes and injects payload -// engine := gin.New() -// engine.Use(secureHeader.Handle) -// engine.GET("/ping", func(c *gin.Context) { -// val, _ := c.Get("payload") -// fmt.Println(val.(*MyPayload)) -// c.JSON(200, val) +// In a Gin app: +// +// r := gin.New() +// r.Use(header.Handle) +// r.GET("/ping", func(c *gin.Context) { +// v, _ := c.Get("payload") +// fmt.Println(v.(*Payload)) // }) type RSASecureHeader[T any] struct { RSA *rsa.PrivateKey @@ -145,18 +63,18 @@ type RSASecureHeader[T any] struct { ContextAbort bool } -// NewEncodeRSA creates a new RSA-secured header middleware instance. +// NewEncodeRSA constructs a new RSA-secure header middleware for a specific type `T`. // // Parameters: -// - headerName: name of the header that carries the encoded data -// - contextKey: name used in gin.Context for decoded payload -// - rsa: RSA private key (encryption uses rsa.PublicKey) -// - contextAbort: whether to call ctx.Abort() on decode failure +// - headerName: name of the HTTP header carrying the encrypted data +// - contextKey: key under which decoded data will be stored in Gin context +// - rsa: RSA private key (its public key used for encryption) +// - contextAbort: whether to abort the request on decode error // // Example: // // key, _ := rsa.GenerateKey(rand.Reader, 2048) -// header := yamiddleware.NewEncodeRSA[MyType]("X-Enc", "payload", key, true) +// header := yaginmiddleware.NewEncodeRSA[MyType]("X-Enc", "payload", key, true) func NewEncodeRSA[T any]( headerName string, contextKey string, @@ -171,24 +89,29 @@ func NewEncodeRSA[T any]( } } -// Encode serializes, compresses, encrypts, and base64-encodes the given data. +// Encode serializes and encrypts the provided data into a base64-encoded string. // -// The process: -// 1. MessagePack encode (using yaencoding.EncodeMessagePack) -// 2. Gzip compress (using yagzip.NewGzip().Zip) -// 3. RSA encrypt (using yarsa.Encrypt) -// 4. Base64 encode (using yaencoding.ToString) +// The process includes: +// 1. MessagePack encoding +// 2. GZIP compression +// 3. RSA encryption (public key) +// 4. Base64 encoding // -// Returns an encrypted header-safe string and possible yaerrors.Error. +// Returns the encoded header string or a `yaerrors.Error`. // // Example: // -// enc, err := header.Encode(MyStruct{Field: "value"}) -// if err != nil { -// log.Fatalf("encode failed: %v", err) +// type Payload struct { +// Name string // } -// req.Header.Set("X-Enc", enc) -func (e *RSASecureHeader[T]) Encode(data any) (string, yaerrors.Error) { +// +// key, _ := rsa.GenerateKey(rand.Reader, 2048) +// header := yaginmiddleware.NewEncodeRSA[Payload]("X-Enc", "payload", key, true) +// +// in := Payload{Name: "RZK"} +// enc, _ := header.Encode(in) +// fmt.Println(enc) // eyJ... (long base64) +func (h *RSASecureHeader[T]) Encode(data T) (string, yaerrors.Error) { bytes, err := yaencoding.EncodeMessagePack(data) if err != nil { return "", err.Wrap("[RSA HEADER] failed to encode data to bytes") @@ -199,7 +122,7 @@ func (e *RSASecureHeader[T]) Encode(data any) (string, yaerrors.Error) { return "", err.Wrap("[RSA HEADER] failed to zip bytes") } - rsa, err := yarsa.Encrypt(zip, &e.RSA.PublicKey) + rsa, err := yarsa.Encrypt(zip, &h.RSA.PublicKey) if err != nil { return "", err.Wrap("[RSA HEADER] failed to encrypt zipped") } @@ -207,89 +130,165 @@ func (e *RSASecureHeader[T]) Encode(data any) (string, yaerrors.Error) { return yaencoding.ToString(rsa), nil } -// Decode performs the inverse process of Encode: -// 1. Base64 decode → bytes -// 2. RSA decrypt → zipped data -// 3. Gzip decompress → plaintext MessagePack -// 4. Decode MessagePack → typed struct `T` +// emptySymbol is an invisible Unicode character used internally as a separator +// between the optional plaintext “source” prefix and the binary MessagePack data. // -// It returns a typed pointer to the decoded struct or an error. +// It helps `EncodeWithSrc` and `Decode` distinguish readable prefix text +// from encoded payload bytes. +const emptySymbol = "ᅠ" + +// EncodeWithSrc behaves like Encode but also prepends a plaintext “source” string +// before the encrypted MessagePack bytes, separated by an invisible rune (ᅠ). +// +// This allows embedding a readable prefix (e.g., client ID, version, signature) +// that survives decryption and can be retrieved alongside the struct. // // Example: // -// out, err := header.Decode(encString) -// if err != nil { -// log.Fatalf("decode failed: %v", err) +// type Payload struct { +// ID uint16 // } -// fmt.Printf("Decoded struct: %+v\n", out) -func (e *RSASecureHeader[T]) Decode(data string) (*T, yaerrors.Error) { - bytes, err := yaencoding.ToBytes(data) +// +// key, _ := rsa.GenerateKey(rand.Reader, 2048) +// header := yaginmiddleware.NewEncodeRSA[Payload]("X-Enc", "payload", key, true) +// +// in := Payload{ID: 10} +// enc, _ := header.EncodeWithSrc("ClientA", in) +// fmt.Println(enc) // base64 ciphertext +func (h *RSASecureHeader[T]) EncodeWithSrc(src string, data T) (string, yaerrors.Error) { + bytes, err := yaencoding.EncodeMessagePack(data) if err != nil { - return nil, err.Wrap("failed to decode string to bytes") + return "", err.Wrap("[RSA HEADER] failed to encode data to bytes") } - if len(bytes)%e.RSA.Size() != 0 { - return nil, yaerrors.FromString( + bytes = append([]byte(src), append([]byte(emptySymbol), bytes...)...) + + zip, err := yagzip.NewGzip().Zip(bytes) + if err != nil { + return "", err.Wrap("[RSA HEADER] failed to zip bytes") + } + + rsa, err := yarsa.Encrypt(zip, &h.RSA.PublicKey) + if err != nil { + return "", err.Wrap("[RSA HEADER] failed to encrypt zipped") + } + + return yaencoding.ToString(rsa), nil +} + +// Decode reverses the Encode / EncodeWithSrc process. +// +// It expects a base64-encoded ciphertext, decrypts it using the private key, +// decompresses, and decodes the underlying struct. +// +// Returns: +// - optional prefix string (if EncodeWithSrc was used, else empty) +// - pointer to decoded struct +// - yaerrors.Error if failure occurred +// +// Example: +// +// type Payload struct { Name string } +// key, _ := rsa.GenerateKey(rand.Reader, 2048) +// +// header := yaginmiddleware.NewEncodeRSA[Payload]("X-Enc", "payload", key, true) +// +// in := Payload{Name: "Test"} +// enc, _ := header.Encode(in) +// +// src, out, _ := header.Decode(enc) +// fmt.Println(src) // "" +// fmt.Println(out.Name) // "Test" +func (h *RSASecureHeader[T]) Decode(data string) (string, *T, yaerrors.Error) { + rawData, err := yaencoding.ToBytes(data) + if err != nil { + return "", nil, err.Wrap("[RSA HEADER] failed to decode string to bytes") + } + + if len(rawData)%h.RSA.Size() != 0 { + return "", nil, yaerrors.FromString( http.StatusInternalServerError, "[RSA HEADER] bad block string size", ) } - zipped, err := yarsa.Decrypt(bytes, e.RSA) + zipped, err := yarsa.Decrypt(rawData, h.RSA) if err != nil { - return nil, err.Wrap("[RSA HEADER] failed to decrypt to zipped data") + return "", nil, err.Wrap("[RSA HEADER] failed to decrypt to zipped data") } plaintext, err := yagzip.NewGzip().Unzip(zipped) if err != nil { - return nil, err.Wrap("[RSA HEADER] failed to get plain text from zip") + return "", nil, err.Wrap("[RSA HEADER] failed to get plain text from zip") + } + + index := bytes.IndexRune(plaintext, []rune(emptySymbol)[0]) + offset := len([]byte(emptySymbol)) + + switch index { + case 0: + offset = 0 + case -1: + index = 0 + offset = 0 } - res, err := yaencoding.DecodeMessagePack[T](plaintext) + res, err := yaencoding.DecodeMessagePack[T](plaintext[index+offset:]) if err != nil { - return nil, err.Wrap("[RSA HEADER] failed to decode plaintext") + return "", nil, err.Wrap("[RSA HEADER] failed to decode plaintext") } - return res, nil + return string(plaintext[:index+offset]), res, nil } -// Handle implements gin.HandlerFunc. -// -// It reads the encrypted header, decrypts it, and injects the resulting -// struct pointer into the gin context using `ContextKey`. If decoding fails, -// the middleware will record the error in ctx.Errors and optionally abort -// further handler execution if ContextAbort is true. +// Handle implements Gin middleware interface to automatically decrypt, +// decode, and inject data into Gin context. +// +// The middleware performs the following: +// 1. Reads the header specified in `HeaderName`. +// 2. Strips CR/LF characters (for safety). +// 3. Calls Decode(). +// 4. On success: +// - Rewrites request header to the plaintext prefix (if present). +// - Stores decoded struct in context under `ContextKey`. +// - Calls `ctx.Next()`. +// 5. On failure: +// - Logs error via ctx.Error(err). +// - Optionally aborts request if `ContextAbort == true`. // // Example: // +// type Payload struct { Msg string } +// // key, _ := rsa.GenerateKey(rand.Reader, 2048) -// header := yamiddleware.NewEncodeRSA[UserData]("X-User", "payload", key, true) +// header := yaginmiddleware.NewEncodeRSA[Payload]("X-Enc", "payload", key, true) // -// engine := gin.New() -// engine.Use(header.Handle) +// r := gin.New() +// r.Use(header.Handle) // -// engine.GET("/me", func(c *gin.Context) { +// r.GET("/ping", func(c *gin.Context) { // val, _ := c.Get("payload") -// user := val.(*UserData) -// c.JSON(200, user) +// fmt.Println(val.(*Payload).Msg) // }) -func (e *RSASecureHeader[T]) Handle(ctx *gin.Context) { - text := ctx.GetHeader(e.HeaderName) +func (h *RSASecureHeader[T]) Handle(ctx *gin.Context) { + text := ctx.GetHeader(h.HeaderName) text = yarsa.StripCRLF(text) - data, err := e.Decode(text) + src, data, err := h.Decode(text) if err != nil { _ = ctx.Error(err) - if e.ContextAbort { + if h.ContextAbort { ctx.Abort() } return } - ctx.Set(e.ContextKey, data) + ctx.Request.Header.Set(h.HeaderName, src) + + ctx.Set(h.ContextKey, data) ctx.Next() } diff --git a/yaginmiddleware/yaginmiddleware_test.go b/yaginmiddleware/yaginmiddleware_test.go index 2a7ef45..b904a72 100644 --- a/yaginmiddleware/yaginmiddleware_test.go +++ b/yaginmiddleware/yaginmiddleware_test.go @@ -1,4 +1,4 @@ -package yamiddleware_test +package yaginmiddleware_test import ( "crypto/rand" @@ -7,7 +7,7 @@ import ( "net/http/httptest" "testing" - yaginmiddleware "github.com/YaCodeDev/GoYaCodeDevUtils/yaginmiddleware" + "github.com/YaCodeDev/GoYaCodeDevUtils/yaginmiddleware" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) @@ -39,7 +39,7 @@ func TestEncodeRSAHeader_Flow(t *testing.T) { enc, _ := header.Encode(in) - out, _ := header.Decode(enc) + _, out, _ := header.Decode(enc) assert.Equal(t, &in, out, "Data mismatch") }) From dd9211062e10a799d057b476fdfcc10ee63492e1 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Fri, 10 Oct 2025 00:18:12 +0300 Subject: [PATCH 32/36] feat(yaginmiddleware): package comm --- yaginmiddleware/yaginmiddleware.go | 1 + 1 file changed, 1 insertion(+) diff --git a/yaginmiddleware/yaginmiddleware.go b/yaginmiddleware/yaginmiddleware.go index 4082825..10c7a98 100644 --- a/yaginmiddleware/yaginmiddleware.go +++ b/yaginmiddleware/yaginmiddleware.go @@ -1,3 +1,4 @@ +// Package yaginmiddleware provides secure middleware utilities for Gin. package yaginmiddleware import ( From 979aef7c396c012d9f5e1413e4e4b626db775407 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Sat, 11 Oct 2025 23:22:54 +0300 Subject: [PATCH 33/36] feat(yagzip): make const default compression --- yagzip/yagzip.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/yagzip/yagzip.go b/yagzip/yagzip.go index e62dc27..cdfb355 100644 --- a/yagzip/yagzip.go +++ b/yagzip/yagzip.go @@ -31,6 +31,8 @@ import ( "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" ) +const DefaultCompression = flate.DefaultCompression + type Gzip struct { Level int } From 676412f9e3c81267da458d7bc849605dcf083316 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Sat, 11 Oct 2025 23:23:05 +0300 Subject: [PATCH 34/36] feat(yarsa): format block size --- yarsa/yarsa.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/yarsa/yarsa.go b/yarsa/yarsa.go index 134aa24..d767b62 100644 --- a/yarsa/yarsa.go +++ b/yarsa/yarsa.go @@ -43,6 +43,7 @@ import ( "crypto/rand" "crypto/rsa" "crypto/sha256" + "fmt" "net/http" "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" @@ -135,7 +136,7 @@ func Decrypt(ciphertext []byte, private *rsa.PrivateKey) ([]byte, yaerrors.Error if len(ciphertext)%blockSize != 0 { return nil, yaerrors.FromString( http.StatusInternalServerError, - "[RSA] ciphertext length is not a multiple of RSA block size (expected exact {block size}-byte blocks)", + fmt.Sprintf("[RSA] ciphertext length is not a multiple of RSA block size (expected exact {%d}-byte blocks)", blockSize), ) } From f879674662d65ab56f67bfc05962ac051a8f8793 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Sat, 11 Oct 2025 23:24:09 +0300 Subject: [PATCH 35/36] feat(yaginmiddleware): extract logic, level conf --- yaginmiddleware/rsa_secure.go | 319 ++++++++++++++++++ ...nmiddleware_test.go => rsa_secure_test.go} | 0 yaginmiddleware/yaginmiddleware.go | 287 +--------------- 3 files changed, 320 insertions(+), 286 deletions(-) create mode 100644 yaginmiddleware/rsa_secure.go rename yaginmiddleware/{yaginmiddleware_test.go => rsa_secure_test.go} (100%) diff --git a/yaginmiddleware/rsa_secure.go b/yaginmiddleware/rsa_secure.go new file mode 100644 index 0000000..cb6afa2 --- /dev/null +++ b/yaginmiddleware/rsa_secure.go @@ -0,0 +1,319 @@ +package yaginmiddleware + +import ( + "bytes" + "crypto/rsa" + "net/http" + + "github.com/YaCodeDev/GoYaCodeDevUtils/yaencoding" + "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" + "github.com/YaCodeDev/GoYaCodeDevUtils/yagzip" + "github.com/YaCodeDev/GoYaCodeDevUtils/yarsa" + "github.com/gin-gonic/gin" +) + +// RSASecureHeader is a generic Gin middleware that enables transparent, +// type-safe encryption and decryption of structured data in HTTP headers +// using RSA-OAEP + GZIP + MessagePack. +// +// It provides methods to encode/decode any struct `T` into a secure, +// base64-encoded header value, and a middleware handler (`Handle`) that +// automatically decrypts incoming headers and injects the resulting struct +// into Gin’s request context. +// +// Pipeline: +// +// struct -> MessagePack -> gzip -> RSA encrypt -> base64 +// base64 -> RSA decrypt -> gunzip -> MessagePack -> struct +// +// Example: +// +// type Payload struct { +// ID uint16 +// Text string +// } +// +// key, _ := rsa.GenerateKey(rand.Reader, 2048) +// header := yaginmiddleware.NewEncodeRSA[Payload]("X-Data", "payload", key, true) +// +// in := Payload{ID: 7, Text: "Hello"} +// enc, _ := header.Encode(in) +// +// _, out, _ := header.Decode(enc) +// fmt.Println(out.Text) // "Hello" +// +// In a Gin app: +// +// r := gin.New() +// r.Use(header.Handle) +// r.GET("/ping", func(c *gin.Context) { +// v, _ := c.Get("payload") +// fmt.Println(v.(*Payload)) +// }) +type RSASecureHeader[T any] struct { + RSA *rsa.PrivateKey + HeaderName string + ContextKey string + ContextAbort bool + compressionLevel int +} + +// NewEncodeRSA constructs a new RSA-secure header middleware for a specific type `T`. +// +// Parameters: +// - headerName: name of the HTTP header carrying the encrypted data +// - contextKey: key under which decoded data will be stored in Gin context +// - rsa: RSA private key (its public key used for encryption) +// - contextAbort: whether to abort the request on decode error +// - compressionLevel: `yagzip` commpression level +// +// Example: +// +// key, _ := rsa.GenerateKey(rand.Reader, 2048) +// header := yaginmiddleware.NewEncodeRSA[MyType]("X-Enc", "payload", key, true) +func NewEncodeRSAWithCompressionLevel[T any]( + headerName string, + contextKey string, + rsa *rsa.PrivateKey, + contextAbort bool, + compressionLevel int, +) *RSASecureHeader[T] { + return &RSASecureHeader[T]{ + RSA: rsa, + ContextKey: contextKey, + HeaderName: headerName, + ContextAbort: contextAbort, + compressionLevel: compressionLevel, + } +} + +// NewEncodeRSA constructs a new RSA-secure header middleware for a specific type `T`. +// +// Parameters: +// - headerName: name of the HTTP header carrying the encrypted data +// - contextKey: key under which decoded data will be stored in Gin context +// - rsa: RSA private key (its public key used for encryption) +// - contextAbort: whether to abort the request on decode error +// +// Example: +// +// key, _ := rsa.GenerateKey(rand.Reader, 2048) +// header := yaginmiddleware.NewEncodeRSA[MyType]("X-Enc", "payload", key, true) +func NewEncodeRSA[T any]( + headerName string, + contextKey string, + rsa *rsa.PrivateKey, + contextAbort bool, +) *RSASecureHeader[T] { + return &RSASecureHeader[T]{ + RSA: rsa, + ContextKey: contextKey, + HeaderName: headerName, + ContextAbort: contextAbort, + compressionLevel: yagzip.DefaultCompression, + } +} + +// Encode serializes and encrypts the provided data into a base64-encoded string. +// +// The process includes: +// 1. MessagePack encoding +// 2. GZIP compression +// 3. RSA encryption (public key) +// 4. Base64 encoding +// +// Returns the encoded header string or a `yaerrors.Error`. +// +// Example: +// +// type Payload struct { +// Name string +// } +// +// key, _ := rsa.GenerateKey(rand.Reader, 2048) +// header := yaginmiddleware.NewEncodeRSA[Payload]("X-Enc", "payload", key, true) +// +// in := Payload{Name: "RZK"} +// enc, _ := header.Encode(in) +// fmt.Println(enc) // eyJ... (long base64) +func (h *RSASecureHeader[T]) Encode(data T) (string, yaerrors.Error) { + bytes, err := yaencoding.EncodeMessagePack(data) + if err != nil { + return "", err.Wrap("[RSA HEADER] failed to encode data to bytes") + } + + zip, err := yagzip.NewGzipWithLevel(h.compressionLevel).Zip(bytes) + if err != nil { + return "", err.Wrap("[RSA HEADER] failed to zip bytes") + } + + rsa, err := yarsa.Encrypt(zip, &h.RSA.PublicKey) + if err != nil { + return "", err.Wrap("[RSA HEADER] failed to encrypt zipped") + } + + return yaencoding.ToString(rsa), nil +} + +// emptySymbol is an invisible Unicode character used internally as a separator +// between the optional plaintext “source” prefix and the binary MessagePack data. +// +// It helps `EncodeWithSrc` and `Decode` distinguish readable prefix text +// from encoded payload bytes. +const emptySymbol = "ᅠ" + +// EncodeWithSrc behaves like Encode but also prepends a plaintext “source” string +// before the encrypted MessagePack bytes, separated by an invisible rune (ᅠ). +// +// This allows embedding a readable prefix (e.g., client ID, version, signature) +// that survives decryption and can be retrieved alongside the struct. +// +// Example: +// +// type Payload struct { +// ID uint16 +// } +// +// key, _ := rsa.GenerateKey(rand.Reader, 2048) +// header := yaginmiddleware.NewEncodeRSA[Payload]("X-Enc", "payload", key, true) +// +// in := Payload{ID: 10} +// enc, _ := header.EncodeWithSrc("ClientA", in) +// fmt.Println(enc) // base64 ciphertext +func (h *RSASecureHeader[T]) EncodeWithSrc(src string, data T) (string, yaerrors.Error) { + bytes, err := yaencoding.EncodeMessagePack(data) + if err != nil { + return "", err.Wrap("[RSA HEADER] failed to encode data to bytes") + } + + bytes = append([]byte(src), append([]byte(emptySymbol), bytes...)...) + + zip, err := yagzip.NewGzipWithLevel(h.compressionLevel).Zip(bytes) + if err != nil { + return "", err.Wrap("[RSA HEADER] failed to zip bytes") + } + + rsa, err := yarsa.Encrypt(zip, &h.RSA.PublicKey) + if err != nil { + return "", err.Wrap("[RSA HEADER] failed to encrypt zipped") + } + + return yaencoding.ToString(rsa), nil +} + +// Decode reverses the Encode / EncodeWithSrc process. +// +// It expects a base64-encoded ciphertext, decrypts it using the private key, +// decompresses, and decodes the underlying struct. +// +// Returns: +// - optional prefix string (if EncodeWithSrc was used, else empty) +// - pointer to decoded struct +// - yaerrors.Error if failure occurred +// +// Example: +// +// type Payload struct { Name string } +// key, _ := rsa.GenerateKey(rand.Reader, 2048) +// +// header := yaginmiddleware.NewEncodeRSA[Payload]("X-Enc", "payload", key, true) +// +// in := Payload{Name: "Test"} +// enc, _ := header.Encode(in) +// +// src, out, _ := header.Decode(enc) +// fmt.Println(src) // "" +// fmt.Println(out.Name) // "Test" +func (h *RSASecureHeader[T]) Decode(data string) (string, *T, yaerrors.Error) { + rawData, err := yaencoding.ToBytes(data) + if err != nil { + return "", nil, err.Wrap("[RSA HEADER] failed to decode string to bytes") + } + + if len(rawData)%h.RSA.Size() != 0 { + return "", nil, yaerrors.FromString( + http.StatusInternalServerError, + "[RSA HEADER] bad block string size", + ) + } + + zipped, err := yarsa.Decrypt(rawData, h.RSA) + if err != nil { + return "", nil, err.Wrap("[RSA HEADER] failed to decrypt to zipped data") + } + + plaintext, err := yagzip.NewGzipWithLevel(h.compressionLevel).Unzip(zipped) + if err != nil { + return "", nil, err.Wrap("[RSA HEADER] failed to get plain text from zip") + } + + index := bytes.IndexRune(plaintext, []rune(emptySymbol)[0]) + offset := len([]byte(emptySymbol)) + + switch index { + case 0: + offset = 0 + case -1: + index = 0 + offset = 0 + } + + res, err := yaencoding.DecodeMessagePack[T](plaintext[index+offset:]) + if err != nil { + return "", nil, err.Wrap("[RSA HEADER] failed to decode plaintext") + } + + return string(plaintext[:index+offset]), res, nil +} + +// Handle implements Gin middleware interface to automatically decrypt, +// decode, and inject data into Gin context. +// +// The middleware performs the following: +// 1. Reads the header specified in `HeaderName`. +// 2. Strips CR/LF characters (for safety). +// 3. Calls Decode(). +// 4. On success: +// - Rewrites request header to the plaintext prefix (if present). +// - Stores decoded struct in context under `ContextKey`. +// - Calls `ctx.Next()`. +// 5. On failure: +// - Logs error via ctx.Error(err). +// - Optionally aborts request if `ContextAbort == true`. +// +// Example: +// +// type Payload struct { Msg string } +// +// key, _ := rsa.GenerateKey(rand.Reader, 2048) +// header := yaginmiddleware.NewEncodeRSA[Payload]("X-Enc", "payload", key, true) +// +// r := gin.New() +// r.Use(header.Handle) +// +// r.GET("/ping", func(c *gin.Context) { +// val, _ := c.Get("payload") +// fmt.Println(val.(*Payload).Msg) +// }) +func (h *RSASecureHeader[T]) Handle(ctx *gin.Context) { + text := ctx.GetHeader(h.HeaderName) + + text = yarsa.StripCRLF(text) + + src, data, err := h.Decode(text) + if err != nil { + _ = ctx.Error(err) + + if h.ContextAbort { + ctx.Abort() + } + + return + } + + ctx.Request.Header.Set(h.HeaderName, src) + + ctx.Set(h.ContextKey, data) + + ctx.Next() +} diff --git a/yaginmiddleware/yaginmiddleware_test.go b/yaginmiddleware/rsa_secure_test.go similarity index 100% rename from yaginmiddleware/yaginmiddleware_test.go rename to yaginmiddleware/rsa_secure_test.go diff --git a/yaginmiddleware/yaginmiddleware.go b/yaginmiddleware/yaginmiddleware.go index 10c7a98..d6e4d17 100644 --- a/yaginmiddleware/yaginmiddleware.go +++ b/yaginmiddleware/yaginmiddleware.go @@ -1,295 +1,10 @@ // Package yaginmiddleware provides secure middleware utilities for Gin. package yaginmiddleware -import ( - "bytes" - "crypto/rsa" - "net/http" - - "github.com/YaCodeDev/GoYaCodeDevUtils/yaencoding" - "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" - "github.com/YaCodeDev/GoYaCodeDevUtils/yagzip" - "github.com/YaCodeDev/GoYaCodeDevUtils/yarsa" - "github.com/gin-gonic/gin" -) +import "github.com/gin-gonic/gin" // Middleware represents a generic Gin middleware component // capable of processing requests via a `Handle` method. type Middleware interface { Handle(ctx *gin.Context) } - -// RSASecureHeader is a generic Gin middleware that enables transparent, -// type-safe encryption and decryption of structured data in HTTP headers -// using RSA-OAEP + GZIP + MessagePack. -// -// It provides methods to encode/decode any struct `T` into a secure, -// base64-encoded header value, and a middleware handler (`Handle`) that -// automatically decrypts incoming headers and injects the resulting struct -// into Gin’s request context. -// -// Pipeline: -// -// struct -> MessagePack -> gzip -> RSA encrypt -> base64 -// base64 -> RSA decrypt -> gunzip -> MessagePack -> struct -// -// Example: -// -// type Payload struct { -// ID uint16 -// Text string -// } -// -// key, _ := rsa.GenerateKey(rand.Reader, 2048) -// header := yaginmiddleware.NewEncodeRSA[Payload]("X-Data", "payload", key, true) -// -// in := Payload{ID: 7, Text: "Hello"} -// enc, _ := header.Encode(in) -// -// _, out, _ := header.Decode(enc) -// fmt.Println(out.Text) // "Hello" -// -// In a Gin app: -// -// r := gin.New() -// r.Use(header.Handle) -// r.GET("/ping", func(c *gin.Context) { -// v, _ := c.Get("payload") -// fmt.Println(v.(*Payload)) -// }) -type RSASecureHeader[T any] struct { - RSA *rsa.PrivateKey - HeaderName string - ContextKey string - ContextAbort bool -} - -// NewEncodeRSA constructs a new RSA-secure header middleware for a specific type `T`. -// -// Parameters: -// - headerName: name of the HTTP header carrying the encrypted data -// - contextKey: key under which decoded data will be stored in Gin context -// - rsa: RSA private key (its public key used for encryption) -// - contextAbort: whether to abort the request on decode error -// -// Example: -// -// key, _ := rsa.GenerateKey(rand.Reader, 2048) -// header := yaginmiddleware.NewEncodeRSA[MyType]("X-Enc", "payload", key, true) -func NewEncodeRSA[T any]( - headerName string, - contextKey string, - rsa *rsa.PrivateKey, - contextAbort bool, -) *RSASecureHeader[T] { - return &RSASecureHeader[T]{ - RSA: rsa, - ContextKey: contextKey, - HeaderName: headerName, - ContextAbort: contextAbort, - } -} - -// Encode serializes and encrypts the provided data into a base64-encoded string. -// -// The process includes: -// 1. MessagePack encoding -// 2. GZIP compression -// 3. RSA encryption (public key) -// 4. Base64 encoding -// -// Returns the encoded header string or a `yaerrors.Error`. -// -// Example: -// -// type Payload struct { -// Name string -// } -// -// key, _ := rsa.GenerateKey(rand.Reader, 2048) -// header := yaginmiddleware.NewEncodeRSA[Payload]("X-Enc", "payload", key, true) -// -// in := Payload{Name: "RZK"} -// enc, _ := header.Encode(in) -// fmt.Println(enc) // eyJ... (long base64) -func (h *RSASecureHeader[T]) Encode(data T) (string, yaerrors.Error) { - bytes, err := yaencoding.EncodeMessagePack(data) - if err != nil { - return "", err.Wrap("[RSA HEADER] failed to encode data to bytes") - } - - zip, err := yagzip.NewGzip().Zip(bytes) - if err != nil { - return "", err.Wrap("[RSA HEADER] failed to zip bytes") - } - - rsa, err := yarsa.Encrypt(zip, &h.RSA.PublicKey) - if err != nil { - return "", err.Wrap("[RSA HEADER] failed to encrypt zipped") - } - - return yaencoding.ToString(rsa), nil -} - -// emptySymbol is an invisible Unicode character used internally as a separator -// between the optional plaintext “source” prefix and the binary MessagePack data. -// -// It helps `EncodeWithSrc` and `Decode` distinguish readable prefix text -// from encoded payload bytes. -const emptySymbol = "ᅠ" - -// EncodeWithSrc behaves like Encode but also prepends a plaintext “source” string -// before the encrypted MessagePack bytes, separated by an invisible rune (ᅠ). -// -// This allows embedding a readable prefix (e.g., client ID, version, signature) -// that survives decryption and can be retrieved alongside the struct. -// -// Example: -// -// type Payload struct { -// ID uint16 -// } -// -// key, _ := rsa.GenerateKey(rand.Reader, 2048) -// header := yaginmiddleware.NewEncodeRSA[Payload]("X-Enc", "payload", key, true) -// -// in := Payload{ID: 10} -// enc, _ := header.EncodeWithSrc("ClientA", in) -// fmt.Println(enc) // base64 ciphertext -func (h *RSASecureHeader[T]) EncodeWithSrc(src string, data T) (string, yaerrors.Error) { - bytes, err := yaencoding.EncodeMessagePack(data) - if err != nil { - return "", err.Wrap("[RSA HEADER] failed to encode data to bytes") - } - - bytes = append([]byte(src), append([]byte(emptySymbol), bytes...)...) - - zip, err := yagzip.NewGzip().Zip(bytes) - if err != nil { - return "", err.Wrap("[RSA HEADER] failed to zip bytes") - } - - rsa, err := yarsa.Encrypt(zip, &h.RSA.PublicKey) - if err != nil { - return "", err.Wrap("[RSA HEADER] failed to encrypt zipped") - } - - return yaencoding.ToString(rsa), nil -} - -// Decode reverses the Encode / EncodeWithSrc process. -// -// It expects a base64-encoded ciphertext, decrypts it using the private key, -// decompresses, and decodes the underlying struct. -// -// Returns: -// - optional prefix string (if EncodeWithSrc was used, else empty) -// - pointer to decoded struct -// - yaerrors.Error if failure occurred -// -// Example: -// -// type Payload struct { Name string } -// key, _ := rsa.GenerateKey(rand.Reader, 2048) -// -// header := yaginmiddleware.NewEncodeRSA[Payload]("X-Enc", "payload", key, true) -// -// in := Payload{Name: "Test"} -// enc, _ := header.Encode(in) -// -// src, out, _ := header.Decode(enc) -// fmt.Println(src) // "" -// fmt.Println(out.Name) // "Test" -func (h *RSASecureHeader[T]) Decode(data string) (string, *T, yaerrors.Error) { - rawData, err := yaencoding.ToBytes(data) - if err != nil { - return "", nil, err.Wrap("[RSA HEADER] failed to decode string to bytes") - } - - if len(rawData)%h.RSA.Size() != 0 { - return "", nil, yaerrors.FromString( - http.StatusInternalServerError, - "[RSA HEADER] bad block string size", - ) - } - - zipped, err := yarsa.Decrypt(rawData, h.RSA) - if err != nil { - return "", nil, err.Wrap("[RSA HEADER] failed to decrypt to zipped data") - } - - plaintext, err := yagzip.NewGzip().Unzip(zipped) - if err != nil { - return "", nil, err.Wrap("[RSA HEADER] failed to get plain text from zip") - } - - index := bytes.IndexRune(plaintext, []rune(emptySymbol)[0]) - offset := len([]byte(emptySymbol)) - - switch index { - case 0: - offset = 0 - case -1: - index = 0 - offset = 0 - } - - res, err := yaencoding.DecodeMessagePack[T](plaintext[index+offset:]) - if err != nil { - return "", nil, err.Wrap("[RSA HEADER] failed to decode plaintext") - } - - return string(plaintext[:index+offset]), res, nil -} - -// Handle implements Gin middleware interface to automatically decrypt, -// decode, and inject data into Gin context. -// -// The middleware performs the following: -// 1. Reads the header specified in `HeaderName`. -// 2. Strips CR/LF characters (for safety). -// 3. Calls Decode(). -// 4. On success: -// - Rewrites request header to the plaintext prefix (if present). -// - Stores decoded struct in context under `ContextKey`. -// - Calls `ctx.Next()`. -// 5. On failure: -// - Logs error via ctx.Error(err). -// - Optionally aborts request if `ContextAbort == true`. -// -// Example: -// -// type Payload struct { Msg string } -// -// key, _ := rsa.GenerateKey(rand.Reader, 2048) -// header := yaginmiddleware.NewEncodeRSA[Payload]("X-Enc", "payload", key, true) -// -// r := gin.New() -// r.Use(header.Handle) -// -// r.GET("/ping", func(c *gin.Context) { -// val, _ := c.Get("payload") -// fmt.Println(val.(*Payload).Msg) -// }) -func (h *RSASecureHeader[T]) Handle(ctx *gin.Context) { - text := ctx.GetHeader(h.HeaderName) - - text = yarsa.StripCRLF(text) - - src, data, err := h.Decode(text) - if err != nil { - _ = ctx.Error(err) - - if h.ContextAbort { - ctx.Abort() - } - - return - } - - ctx.Request.Header.Set(h.HeaderName, src) - - ctx.Set(h.ContextKey, data) - - ctx.Next() -} From d4875a3b4ff05b4ef66c78e1eabdcd44fdedfb26 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Sat, 11 Oct 2025 23:27:27 +0300 Subject: [PATCH 36/36] chore(yarsa): correct line len --- yarsa/yarsa.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/yarsa/yarsa.go b/yarsa/yarsa.go index d767b62..d4d44a8 100644 --- a/yarsa/yarsa.go +++ b/yarsa/yarsa.go @@ -136,7 +136,10 @@ func Decrypt(ciphertext []byte, private *rsa.PrivateKey) ([]byte, yaerrors.Error if len(ciphertext)%blockSize != 0 { return nil, yaerrors.FromString( http.StatusInternalServerError, - fmt.Sprintf("[RSA] ciphertext length is not a multiple of RSA block size (expected exact {%d}-byte blocks)", blockSize), + fmt.Sprintf( + "[RSA] ciphertext length is not a multiple of RSA block size (expected exact {%d}-byte blocks)", + blockSize, + ), ) }