Skip to content
Merged
42 changes: 42 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Lint

on:
push:
branches:
- main
- master
pull_request:
branches:
- main
- master

permissions:
contents: read
# Optional: allow read access to pull request. Use with `only-new-issues` option.
pull-requests: read

jobs:
golangci:
name: golangci-lint
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.25.3"
cache: true

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v7
with:
version: latest
# Optional: show only new issues if it's a pull request.
only-new-issues: true
# Optional: golangci-lint command line arguments.
args: --timeout=5m
# Skip the default install-mode since Go is already set up
skip-cache: false
skip-save-cache: false
103 changes: 103 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
version: "2"
run:
go: "1.25"
issues-exit-code: 1
tests: true
linters:
enable:
# Critical Security & Correctness
- gosec # Security-focused linter (critical for crypto code)
- errcheck # Ensures errors aren't ignored
- govet # Go's official checker for suspicious constructs
- staticcheck # Comprehensive bug/performance/style checker

# Bug Prevention
- ineffassign # Detects ineffectual assignments
- unused # Finds unused code
- nilnil # Detects returning nil with nil errors
- nilerr # Finds code returning nil even on error
- errchkjson # Checks for unchecked errors in JSON encoding/decoding
- bodyclose # Ensures HTTP response bodies are closed
- wastedassign # Detects wasted assignments

# Code Quality & Maintainability
- gocyclo # Cyclomatic complexity
- gocognit # Cognitive complexity
- dupl # Code duplication detection
- unconvert # Unnecessary type conversions
- unparam # Unused function parameters
- misspell # Spelling mistakes
- goconst # Repeated strings that could be constants

# Error Handling
- errorlint # Error wrapping and assertions
- errname # Error naming conventions
- nilnesserr # Nilness checks

# Context & Concurrency
- fatcontext # Context.Context should be first parameter
- contextcheck # Context usage checks

# Embedded & Struct Checks
- embeddedstructfieldcheck # Embedded struct field checks
- exhaustive # Exhaustive enum/switch checks

# Style & Modernization
- asciicheck # ASCII character checks
- canonicalheader # Canonical header checks
- intrange # Integer range checks
- mirror # Mirror checks
- modernize # Modernization suggestions
- nolintlint # Nolint directive checks
- predeclared # Predeclared identifier checks
- usestdlibvars # Use standard library variables

# Performance
- prealloc # Preallocation checks
- perfsprint # Sprint performance checks

# Testing
- paralleltest # Parallel test checks
- testifylint # Testify assertions checks
- thelper # Test helper checks
- tparallel # t.Parallel() checks

# Other
- exptostd # Experimental to standard library
- protogetter # Protobuf getter checks
- reassign # Reassignment checks
- sloglint # Structured logging checks
- godox # Highlight FIXMEs

settings:
exhaustive:
default-signifies-exhaustive: true
godox:
keywords:
- FIXME
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
settings:
gofmt:
simplify: true
goimports:
local-prefixes:
- github.com/mycoria/crop
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
53 changes: 53 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Justfile for CROP - Cryptographic Routines, Operations & Primitives

# Default recipe: list all available commands
default:
@just --list

# Install development tools
install-tools:
@echo "Installing golangci-lint..."
@curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin latest
@echo "golangci-lint installed successfully"
@golangci-lint --version

# Run golangci-lint
lint:
@echo "Running golangci-lint..."
@golangci-lint run --timeout=5m

# Run tests
test:
@echo "Running tests..."
@go test -v ./...

# Run tests with coverage
test-coverage:
@echo "Running tests with coverage..."
@go test -v -coverprofile=coverage.out ./...
@go tool cover -html=coverage.out -o coverage.html
@echo "Coverage report generated: coverage.html"

# Build the project
build:
@echo "Building..."
@go build ./...

# Format code
fmt:
@echo "Formatting code..."
@go fmt ./...

# Tidy dependencies
tidy:
@echo "Tidying dependencies..."
@go mod tidy

# Clean build artifacts
clean:
@echo "Cleaning..."
@rm -f coverage.out coverage.html

# Run all checks (format, lint, test)
check: fmt lint test
@echo "All checks passed!"
4 changes: 2 additions & 2 deletions challenge.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func (hcc *HashedContextChallenge) MakeResponse(challenge []byte) (response []by
}

func (hcc *HashedContextChallenge) makeHash(input []byte, reverse bool) []byte {
vh := NewValueHasher(hcc.hash)
vh := NewValueHasher(hcc.hash.New())

vh.AddString("hashed context challenge") // Fixed internal value.
vh.AddString(hcc.purpose) // Add purpose.
Expand All @@ -111,5 +111,5 @@ func (hcc *HashedContextChallenge) makeHash(input []byte, reverse bool) []byte {
}
vh.Add(input)

return vh.Sum()
return vh.Sum(nil)
}
6 changes: 3 additions & 3 deletions challenge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,13 +244,13 @@ func TestHashedContextChallenge_ResponseMatchesIndependentComputation(t *testing

// Independent recomputation matching the requester's expected order:
// fixed string, purpose, requester, responder, challenge.
vh := NewValueHasher(BLAKE3)
vh := NewValueHasher(BLAKE3.New())
vh.AddString("hashed context challenge")
vh.AddString(purpose)
vh.AddString(reqCtx)
vh.AddString(resCtx)
vh.Add(chal)
resp2 := vh.Sum()
resp2 := vh.Sum(nil)

if !bytes.Equal(resp1, resp2) {
t.Fatalf("independent computation mismatch\n got: %x\nwant: %x", resp1, resp2)
Expand All @@ -260,4 +260,4 @@ func TestHashedContextChallenge_ResponseMatchesIndependentComputation(t *testing
if err := hReq.CheckResponse(resp1); err != nil {
t.Fatalf("CheckResponse failed for valid response: %v", err)
}
}
}
26 changes: 20 additions & 6 deletions hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ func (h Hash) Verify(data, checksum []byte) error {
}

// NewValueHasher creates a structured hasher for multiple values.
func NewValueHasher(hash Hash) *ValueHasher {
func NewValueHasher(h hash.Hash) *ValueHasher {
return &ValueHasher{
hasher: hash.New(),
hasher: h,
}
}

Expand Down Expand Up @@ -176,18 +176,32 @@ func (vh *ValueHasher) AddString(data string) {
vh.Add([]byte(data))
}

// AddUint hashes an uint field.
func (vh *ValueHasher) AddUint(n uint64) {
var buf [8]byte
b := buf[:]
binary.BigEndian.PutUint64(b, n)
vh.Add(b)
}

// Sum finalizes and returns the hash result.
func (vh *ValueHasher) Sum() []byte {
// Write finisher.
func (vh *ValueHasher) Sum(dst []byte) []byte {
// Create finisher.
finisher := [16]byte{
// Total field count.
0, 0, 0, 0,
0, 0, 0, 0,
// Max uint64 as the "field length".
// Use a max uint64 in the place of the "field length" of a next field as an otherwise impossible finalizer.
0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF,
}
binary.BigEndian.PutUint64(finisher[:8], vh.fieldCnt)

return vh.hasher.Sum(finisher[:])
// Write finisher.
_, err := vh.hasher.Write(finisher[:])
if err != nil {
panic(err)
}

return vh.hasher.Sum(dst)
}
28 changes: 14 additions & 14 deletions hash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,11 @@ func TestValueHasher_Sum_FormatAndDeterminism(t *testing.T) {
for _, algo := range algos {
algo := algo
t.Run(string(algo), func(t *testing.T) {
vh := NewValueHasher(algo)
vh := NewValueHasher(algo.New())
for _, f := range fields {
vh.Add(f)
}
sum := vh.Sum()
sum := vh.Sum(nil)

hasher := algo.New()
if hasher == nil {
Expand Down Expand Up @@ -198,11 +198,11 @@ func TestValueHasher_Sum_FormatAndDeterminism(t *testing.T) {
}

// Determinism: re-run and expect the same output.
vh2 := NewValueHasher(algo)
vh2 := NewValueHasher(algo.New())
for _, f := range fields {
vh2.Add(f)
}
sum2 := vh2.Sum()
sum2 := vh2.Sum(nil)
if !bytes.Equal(sum, sum2) {
t.Fatalf("non-deterministic result for ValueHasher\n1: %x\n2: %x", sum, sum2)
}
Expand All @@ -213,31 +213,31 @@ func TestValueHasher_Sum_FormatAndDeterminism(t *testing.T) {
func TestValueHasher_AddString(t *testing.T) {
algo := SHA2_256

vh1 := NewValueHasher(algo)
vh1 := NewValueHasher(algo.New())
vh1.Add([]byte("hello"))
vh1.Add([]byte("world"))

vh2 := NewValueHasher(algo)
vh2 := NewValueHasher(algo.New())
vh2.AddString("hello")
vh2.AddString("world")

if got1, got2 := vh1.Sum(), vh2.Sum(); !bytes.Equal(got1, got2) {
if got1, got2 := vh1.Sum(nil), vh2.Sum(nil); !bytes.Equal(got1, got2) {
t.Fatalf("AddString mismatch with Add\nAdd: %x\nAddString: %x", got1, got2)
}
}

func TestValueHasher_OrderMatters(t *testing.T) {
algo := BLAKE2b_256

vh1 := NewValueHasher(algo)
vh1 := NewValueHasher(algo.New())
vh1.Add([]byte("first"))
vh1.Add([]byte("second"))

vh2 := NewValueHasher(algo)
vh2 := NewValueHasher(algo.New())
vh2.Add([]byte("second"))
vh2.Add([]byte("first"))

if bytes.Equal(vh1.Sum(), vh2.Sum()) {
if bytes.Equal(vh1.Sum(nil), vh2.Sum(nil)) {
t.Fatalf("expected different sums when field order differs")
}
}
Expand All @@ -249,7 +249,7 @@ func TestNewValueHasher_WithInvalidAlgo_PanicsOnUse(t *testing.T) {
}
}()
var invalid Hash = "NOPE"
vh := NewValueHasher(invalid)
vh := NewValueHasher(invalid.New())
// Should panic on first write due to nil hasher
vh.Add([]byte("data"))
}
Expand Down Expand Up @@ -279,9 +279,9 @@ func preview(b []byte) string {
if len(b) == 0 {
return ""
}
const max = 32
if len(b) <= max {
const maxLen = 32
if len(b) <= maxLen {
return string(b)
}
return string(b[:max]) + "..."
return string(b[:maxLen]) + "..."
}
4 changes: 2 additions & 2 deletions key_maker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ func TestBlake3Keymaker_DeriveKeyInto_MinLength(t *testing.T) {
}

// Exactly min should succeed
min := make([]byte, keyMakerMinKeySize)
if err := km.DeriveKeyInto("", "", min); err != nil {
minBuf := make([]byte, keyMakerMinKeySize)
if err := km.DeriveKeyInto("", "", minBuf); err != nil {
t.Fatalf("DeriveKeyInto(min) error: %v", err)
}
}
Expand Down
2 changes: 1 addition & 1 deletion keymaker.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const (
// KeyMakerTypeBlake3 derives keys using BLAKE3.
KeyMakerTypeBlake3 KeyMakerType = "BLAKE3"

keyMakerBaseContext = "nexufend key mkr"
keyMakerBaseContext = "_crop key maker_"

keyMakerMinKeySize = 16
)
Expand Down
Loading
Loading