Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# These are supported funding model platforms

github: [hyp3rd]
1 change: 1 addition & 0 deletions .yamllint
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ rules:
ignore: |
.golangci.yaml
.github/workflows/*
.github/FUNDING.yml
buf.gen.yaml
buf.yaml
**/vendor/**
Expand Down
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Security-focused Go helpers for file I/O, in-memory handling of sensitive data,
- JWT/PASETO helpers with strict validation and safe defaults
- Password hashing presets for argon2id/bcrypt with rehash detection
- Email and URL validation with optional DNS/redirect/reputation checks
- Random token generation and validation with entropy/length caps
- Bounded base64/hex encoding and strict JSON decoding
- HTML/Markdown sanitization, SQL/NoSQL input guards, and filename sanitizers
- Safe integer conversion helpers with overflow/negative guards

Expand Down Expand Up @@ -234,6 +236,52 @@ func main() {
}
```

### Tokens

```go
package main

import (
"github.com/hyp3rd/sectools/pkg/tokens"
)

func main() {
generator, err := tokens.NewGenerator()
if err != nil {
panic(err)
}

validator, err := tokens.NewValidator()
if err != nil {
panic(err)
}

token, _ := generator.Generate()
_, _ = validator.Validate(token)
Comment on lines +259 to +260
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example code silently ignores errors from Generate() and Validate() using blank identifier (_). This sets a poor example for users, especially in a security-focused library. Even in example code, errors from token generation and validation should be properly handled or at least demonstrated with proper error handling patterns.

Suggested change
token, _ := generator.Generate()
_, _ = validator.Validate(token)
token, err := generator.Generate()
if err != nil {
panic(err)
}
if _, err := validator.Validate(token); err != nil {
panic(err)
}

Copilot uses AI. Check for mistakes.
}
```

### Encoding

```go
package main

import (
"github.com/hyp3rd/sectools/pkg/encoding"
)

func main() {
encoded, _ := encoding.EncodeBase64([]byte("secret"))
_, _ = encoding.DecodeBase64(encoded)

type payload struct {
Name string `json:"name"`
}

_ = encoding.DecodeJSON([]byte(`{"name":"alpha"}`), &payload{})
}
```

### Sanitization

```go
Expand Down
5 changes: 5 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
"Atoi",
"aud",
"autobuild",
"base64",
"base64url",
"bcrypt",
"behaviour",
"benchmem",
Expand All @@ -59,6 +61,7 @@
"embeddedstructfieldcheck",
"EnforceFileMode",
"EnforceMode",
"entropy",
"envoyproxy",
"errcheck",
"ewrap",
Expand All @@ -73,6 +76,7 @@
"gid",
"gitleaks",
"gocache",
"goccy",
"gofiber",
"GOFILES",
"gofmt",
Expand All @@ -87,6 +91,7 @@
"gosec",
"GOTOOLCHAIN",
"govulncheck",
"hex",
"honnef",
"hostnames",
"HS256",
Expand Down
10 changes: 10 additions & 0 deletions docs/security-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ This checklist is a quick reference for teams using sectools in production.
- Use `WithJWTVerificationKeys` with `kid` for key rotation and enforce short expirations.
- Prefer PASETO v4 local/public helpers for new tokens and keep issuer/audience rules consistent.

## Random Tokens

- Use `pkg/tokens` to generate API keys or reset tokens with sufficient entropy.
- Set a max token length and reject tokens containing whitespace.

## Encoding

- Use `pkg/encoding` for base64/hex decoding with length caps.
- Keep JSON decoding strict and set `WithJSONMaxBytes` for untrusted payloads.

## Passwords

- Use Argon2id presets unless you need bcrypt compatibility.
Expand Down
50 changes: 50 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ supporting implementations in `internal/`.
- `pkg/auth`: JWT and PASETO helpers with strict validation.
- `pkg/password`: password hashing helpers.
- `pkg/validate`: email and URL validation helpers.
- `pkg/tokens`: random token generation and validation helpers.
- `pkg/encoding`: bounded encoding and decoding helpers.
- `pkg/sanitize`: HTML/Markdown sanitizers, SQL input guards, and filename sanitizers.
- `pkg/memory`: secure in-memory buffers.
- `pkg/converters`: safe numeric conversions.
Expand Down Expand Up @@ -307,6 +309,54 @@ Behavior:
- Optional redirect checks with `WithURLCheckRedirects` and an HTTP client.
- Optional reputation checks with `WithURLReputationChecker`.

## pkg/tokens

### Token generation and validation

```go
func NewGenerator(opts ...TokenOption) (*TokenGenerator, error)
func (g *TokenGenerator) Generate() (string, error)
func NewValidator(opts ...TokenOption) (*TokenValidator, error)
func (v *TokenValidator) Validate(token string) ([]byte, error)
```

Behavior:

- Generates cryptographically secure random tokens (base64url by default).
- Enforces minimum entropy (bits) and optional minimum byte length.
- Rejects tokens over the configured max length or with whitespace.

## pkg/encoding

### Base64/Hex encoding

```go
func EncodeBase64(data []byte, opts ...Base64Option) (string, error)
func DecodeBase64(input string, opts ...Base64Option) ([]byte, error)
func EncodeHex(data []byte, opts ...HexOption) (string, error)
func DecodeHex(input string, opts ...HexOption) ([]byte, error)
```

Behavior:

- Enforces max input length and rejects whitespace.
- Uses base64url without padding by default.

### JSON decoding

```go
func EncodeJSON(value any, opts ...JSONOption) ([]byte, error)
func DecodeJSON(data []byte, value any, opts ...JSONOption) error
func DecodeJSONReader(reader io.Reader, value any, opts ...JSONOption) error
```

Behavior:

- Uses go-json under the hood for encoding/decoding.
- Disallows unknown fields by default; can be toggled.
- Rejects payloads larger than the configured max size.
- Rejects trailing data after the first JSON value.

## pkg/sanitize

### HTML sanitization
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.25.5

require (
aidanwoods.dev/go-paseto v1.6.0
github.com/goccy/go-json v0.10.5
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/hyp3rd/ewrap v1.3.5
github.com/hyp3rd/hyperlogger v0.0.8
Expand All @@ -15,7 +16,6 @@ require (
require (
aidanwoods.dev/go-result v0.3.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
Expand Down
148 changes: 148 additions & 0 deletions pkg/encoding/base64.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package encoding

import (
"encoding/base64"
"strings"
)

const (
base64DefaultMaxLength = 4096
)

// Base64Encoding identifies a base64 encoding variant.
type Base64Encoding int

const (
// Base64EncodingRawURL uses URL-safe base64 without padding.
Base64EncodingRawURL Base64Encoding = iota
// Base64EncodingRawStd uses standard base64 without padding.
Base64EncodingRawStd
// Base64EncodingURL uses URL-safe base64 with padding.
Base64EncodingURL
// Base64EncodingStd uses standard base64 with padding.
Base64EncodingStd
)

// Base64Option configures base64 encoding and decoding.
type Base64Option func(*base64Options) error

type base64Options struct {
encoding *base64.Encoding
maxLength int
}

// EncodeBase64 encodes data using the configured base64 encoding.
func EncodeBase64(data []byte, opts ...Base64Option) (string, error) {
cfg, err := resolveBase64Options(opts)
if err != nil {
return "", err
}

if cfg.encoding.EncodedLen(len(data)) > cfg.maxLength {
return "", ErrBase64TooLong
}

return cfg.encoding.EncodeToString(data), nil
}

// DecodeBase64 decodes a base64-encoded string.
func DecodeBase64(input string, opts ...Base64Option) ([]byte, error) {
cfg, err := resolveBase64Options(opts)
if err != nil {
return nil, err
}

if strings.TrimSpace(input) == "" {
return nil, ErrBase64Empty
}

if containsWhitespace(input) {
return nil, ErrBase64Invalid
}

if len(input) > cfg.maxLength {
return nil, ErrBase64TooLong
}

decoded, err := cfg.encoding.DecodeString(input)
if err != nil {
return nil, ErrBase64Invalid
}

return decoded, nil
}

// WithBase64Encoding sets the base64 encoding variant.
func WithBase64Encoding(encoding Base64Encoding) Base64Option {
return func(cfg *base64Options) error {
enc, err := base64Encoding(encoding)
if err != nil {
return err
}

cfg.encoding = enc

return nil
}
}

// WithBase64MaxLength sets the maximum accepted base64 string length.
func WithBase64MaxLength(maxLength int) Base64Option {
return func(cfg *base64Options) error {
if maxLength <= 0 {
return ErrInvalidBase64Config
}

cfg.maxLength = maxLength

return nil
}
}

func resolveBase64Options(opts []Base64Option) (base64Options, error) {
cfg := base64Options{
encoding: base64.RawURLEncoding,
maxLength: base64DefaultMaxLength,
}

for _, opt := range opts {
if opt == nil {
continue
}

err := opt(&cfg)
if err != nil {
return base64Options{}, err
}
}

err := validateBase64Options(cfg)
if err != nil {
return base64Options{}, err
}

return cfg, nil
}

func validateBase64Options(cfg base64Options) error {
if cfg.encoding == nil || cfg.maxLength <= 0 {
return ErrInvalidBase64Config
}

return nil
}

func base64Encoding(encoding Base64Encoding) (*base64.Encoding, error) {
switch encoding {
case Base64EncodingRawURL:
return base64.RawURLEncoding, nil
case Base64EncodingRawStd:
return base64.RawStdEncoding, nil
case Base64EncodingURL:
return base64.URLEncoding, nil
case Base64EncodingStd:
return base64.StdEncoding, nil
default:
return nil, ErrInvalidBase64Config
}
}
2 changes: 2 additions & 0 deletions pkg/encoding/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package encoding provides bounded encoding and decoding helpers.
package encoding
Loading
Loading