Skip to content

[sync] go-crypt: 5 commits from Forge#1

Merged
Snider merged 5 commits intomainfrom
dev
Mar 17, 2026
Merged

[sync] go-crypt: 5 commits from Forge#1
Snider merged 5 commits intomainfrom
dev

Conversation

@Snider
Copy link
Contributor

@Snider Snider commented Mar 17, 2026

Forge → GitHub Sync

Commits: 5
Files changed: 15

Automated sync from Forge (forge.lthn.ai) to GitHub mirror.


Co-Authored-By: Virgil virgil@lethean.io

Summary by CodeRabbit

  • Chores

    • Upgraded multiple core and indirect dependencies for improved stability and compatibility.
    • Added CI automation to streamline testing, auto-fixing and merge workflows.
  • Refactor

    • Standardised error handling across the codebase for more consistent reporting and diagnostics.

Snider and others added 5 commits March 15, 2026 15:36
Co-Authored-By: Virgil <virgil@lethean.io>
Replace all fmt.Errorf/errors.New calls in auth/auth.go,
crypt/pgp/pgp.go, crypt/rsa/rsa.go, crypt/chachapoly/chachapoly.go,
and trust/trust.go with coreerr.E(op, msg, err) from go-log.
No stale pkg/framework imports found.

Co-Authored-By: Virgil <virgil@lethean.io>
Go 1.26 rsa.GenerateKey recovers from reader errors internally.
Test now verifies no panic instead of expecting an error.

Co-Authored-By: Virgil <virgil@lethean.io>
…ions

Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Virgil <virgil@lethean.io>
@Snider
Copy link
Contributor Author

Snider commented Mar 17, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 17, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Mar 17, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5e6dc0bc-5c55-4219-9f92-3523737a8250

📥 Commits

Reviewing files that changed from the base of the PR and between f4a2198 and 79649d6.

📒 Files selected for processing (1)
  • .github/workflows/ci.yml

📝 Walkthrough

Walkthrough

Refactors error construction across many packages to use the centralized core error wrapper (coreerr.E), replaces some direct file I/O with a coreio.Local abstraction, updates several module dependencies in go.mod, adjusts one RSA test, and adds a CI workflow file.

Changes

Cohort / File(s) Summary
Authentication
auth/auth.go, auth/session_store.go
Replaced plain errors/fmt.Errorf returns with coreerr.E, added operation identifiers for contextual error wrapping; ErrSessionNotFound initialisation now uses coreerr.E.
Cryptography
crypt/chachapoly/chachapoly.go, crypt/pgp/pgp.go, crypt/rsa/rsa.go
Swapped fmt.Errorf/errors.New for coreerr.E(op, ...) across PGP, RSA and ChaChaPoly paths; per-function op constants added for consistent error context.
Crypto Tests
crypt/rsa/rsa_test.go
Removed an error assertion in TestRSA_Ugly for GenerateKeyPair(2048) and added a comment about Go 1.26+ behaviour.
Commands / I/O
cmd/crypt/cmd_encrypt.go, cmd/testcmd/cmd_runner.go
Replaced os.ReadFile/os.WriteFile with coreio.Local.Read/Write in encrypt command; replaced ad-hoc errors.New with coreerr.E in test runner error construction.
Trust / Policy
trust/approval.go, trust/audit.go, trust/config.go, trust/policy.go, trust/trust.go
Unified error creation to coreerr.E for validation, JSON/file I/O and audit failures; imports updated accordingly, no signature changes.
Module Dependencies
go.mod
Bumped multiple forge.lthn.ai/core/* modules and external libs (e.g., ProtonMail/go-crypto, golang.org/x/crypto); adjusted indirect deps (added golang.org/x/mod, other version updates).
CI Workflow
.github/workflows/ci.yml
Added new GitHub Actions workflow with test, auto-fix, and auto-merge jobs, conditional triggers and required permissions.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[sync] go-crypt: 5 commits from Forge' directly describes the pull request's primary purpose: syncing commits from the Forge repository to the GitHub mirror.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
auth/auth.go (1)

647-664: ⚠️ Potential issue | 🟠 Major

Fail closed when the Argon2id hash exists but cannot be read or parsed.

This branch currently falls through to the legacy .lthn verifier on any .hash read or format failure. If a stale .lthn file is still present, an old password can authorise verifyPassword instead of being rejected.

Proposed fix
 	// Try Argon2id hash first (.hash file)
 	if a.medium.IsFile(userPath(userID, ".hash")) {
 		storedHash, err := a.medium.Read(userPath(userID, ".hash"))
-		if err == nil && strings.HasPrefix(storedHash, "$argon2id$") {
-			valid, verr := crypt.VerifyPassword(password, storedHash)
-			if verr != nil {
-				return coreerr.E(op, "failed to verify password", nil)
-			}
-			if !valid {
-				return coreerr.E(op, "invalid password", nil)
-			}
-			return nil
+		if err != nil {
+			return coreerr.E(op, "failed to read password hash", err)
 		}
+		if !strings.HasPrefix(storedHash, "$argon2id$") {
+			return coreerr.E(op, "invalid password hash format", nil)
+		}
+		valid, verr := crypt.VerifyPassword(password, storedHash)
+		if verr != nil {
+			return coreerr.E(op, "failed to verify password", verr)
+		}
+		if !valid {
+			return coreerr.E(op, "invalid password", nil)
+		}
+		return nil
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@auth/auth.go` around lines 647 - 664, The current logic allows falling back
to legacy `.lthn` verification when a `.hash` file exists but cannot be read or
isn't in the expected argon2id format; change verifyPassword handling so that if
a `.hash` file exists you fail closed on read or format errors instead of
falling back. In the block that checks a.medium.IsFile(userPath(userID,
".hash")), ensure that a read error or missing "$argon2id$" prefix returns a
clear error via coreerr.E (using op) rather than continuing to the `.lthn`
check; keep using crypt.VerifyPassword for successful reads and only proceed to
read `.lthn` when the `.hash` file does not exist.
🧹 Nitpick comments (2)
cmd/crypt/cmd_encrypt.go (1)

56-60: Use coreio streaming APIs instead of full-buffer string conversions.

Lines 56/60 and 90/94 load entire files into memory via Read() (returns string), then convert to []byte. Lines 73 and 111 convert the result back to string for Write(). This causes unnecessary allocations for each read and write, particularly problematic with large files.

The coreio.Local library exposes streaming methods—ReadStream() and WriteStream() (or Open() and Create())—which return io.ReadCloser and io.WriteCloser respectively. These are designed for binary payloads and can be used with io.Copy or streamed cipher operations, avoiding full-buffer conversions entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/crypt/cmd_encrypt.go` around lines 56 - 60, The code currently uses
coreio.Local.Read (returning string) and string-to-[]byte conversions (variables
raw, data) and later writes with string conversions, causing full-buffer
allocations; replace those calls with the streaming APIs
(coreio.Local.ReadStream / Open / Create / WriteStream) to obtain
io.ReadCloser/io.WriteCloser for the same `path` and stream data with io.Copy or
streaming cipher operations instead of converting to string/[]byte. Locate the
read sites (uses of coreio.Local.Read, variables named raw/data) and the write
sites that convert back to string, open a reader and writer via the coreio
streaming methods, defer Close(), and pipe data through the encrypt/decrypt
stream with io.Copy to avoid loading the entire file into memory. Ensure
binary-safe streaming (no string conversions) and proper error handling/closing
for the reader and writer.
crypt/chachapoly/chachapoly.go (1)

37-37: Consider wrapping other error returns for consistency.

The validation error is now wrapped with coreerr.E, but other error returns in this function (lines 17, 22, 32, 44) still return raw errors. For consistent error context across the codebase, consider wrapping these as well.

That said, the current change is correct and aligns with the PR's standardisation effort.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crypt/chachapoly/chachapoly.go` at line 37, Wrap all raw error returns in the
chachapoly.Decrypt function using coreerr.E for consistency: locate the Decrypt
function and replace each direct error return (the checks for nil key, invalid
nonce size, aead.New or aead.Open failures, and any other early returns
referenced) with coreerr.E("chachapoly.Decrypt", "<descriptive message>", err)
preserving the original error as the third argument; keep the same descriptive
messages as the existing ciphertext-too-short message style so all errors from
Decrypt use a consistent context and include the original error cause.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@trust/config.go`:
- Around line 38-43: The JSON decoder currently decodes one value but ignores
any trailing data; after calling dec.Decode(&cfg) in LoadPolicies (using
json.NewDecoder and dec.DisallowUnknownFields), attempt a second Decode into an
empty value and ensure it returns io.EOF — if it returns anything else
(including nil), return an error from LoadPolicies instead of proceeding to
convertPolicies(cfg); this guarantees rejection of any trailing JSON/text after
the first object.

---

Outside diff comments:
In `@auth/auth.go`:
- Around line 647-664: The current logic allows falling back to legacy `.lthn`
verification when a `.hash` file exists but cannot be read or isn't in the
expected argon2id format; change verifyPassword handling so that if a `.hash`
file exists you fail closed on read or format errors instead of falling back. In
the block that checks a.medium.IsFile(userPath(userID, ".hash")), ensure that a
read error or missing "$argon2id$" prefix returns a clear error via coreerr.E
(using op) rather than continuing to the `.lthn` check; keep using
crypt.VerifyPassword for successful reads and only proceed to read `.lthn` when
the `.hash` file does not exist.

---

Nitpick comments:
In `@cmd/crypt/cmd_encrypt.go`:
- Around line 56-60: The code currently uses coreio.Local.Read (returning
string) and string-to-[]byte conversions (variables raw, data) and later writes
with string conversions, causing full-buffer allocations; replace those calls
with the streaming APIs (coreio.Local.ReadStream / Open / Create / WriteStream)
to obtain io.ReadCloser/io.WriteCloser for the same `path` and stream data with
io.Copy or streaming cipher operations instead of converting to string/[]byte.
Locate the read sites (uses of coreio.Local.Read, variables named raw/data) and
the write sites that convert back to string, open a reader and writer via the
coreio streaming methods, defer Close(), and pipe data through the
encrypt/decrypt stream with io.Copy to avoid loading the entire file into
memory. Ensure binary-safe streaming (no string conversions) and proper error
handling/closing for the reader and writer.

In `@crypt/chachapoly/chachapoly.go`:
- Line 37: Wrap all raw error returns in the chachapoly.Decrypt function using
coreerr.E for consistency: locate the Decrypt function and replace each direct
error return (the checks for nil key, invalid nonce size, aead.New or aead.Open
failures, and any other early returns referenced) with
coreerr.E("chachapoly.Decrypt", "<descriptive message>", err) preserving the
original error as the third argument; keep the same descriptive messages as the
existing ciphertext-too-short message style so all errors from Decrypt use a
consistent context and include the original error cause.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 07101d56-53d7-455f-b527-05a84d5e2093

📥 Commits

Reviewing files that changed from the base of the PR and between 70ebe68 and f4a2198.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (14)
  • auth/auth.go
  • auth/session_store.go
  • cmd/crypt/cmd_encrypt.go
  • cmd/testcmd/cmd_runner.go
  • crypt/chachapoly/chachapoly.go
  • crypt/pgp/pgp.go
  • crypt/rsa/rsa.go
  • crypt/rsa/rsa_test.go
  • go.mod
  • trust/approval.go
  • trust/audit.go
  • trust/config.go
  • trust/policy.go
  • trust/trust.go

Comment on lines 38 to 43
dec := json.NewDecoder(r)
dec.DisallowUnknownFields()
if err := dec.Decode(&cfg); err != nil {
return nil, fmt.Errorf("trust.LoadPolicies: %w", err)
return nil, coreerr.E("trust.LoadPolicies", "failed to decode JSON", err)
}
return convertPolicies(cfg)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Go's encoding/json package, does Decoder.Decode accept trailing JSON values or bytes unless a second decode confirms io.EOF?

💡 Result:

Yes. (*json.Decoder).Decode reads one “next JSON-encoded value” from the stream and returns after decoding that value; it does not require the underlying io.Reader to be at io.EOF, so additional JSON values (or other non-whitespace bytes) can remain unread unless you explicitly check for them (commonly by calling Decode a second time and requiring io.EOF). The Decoder also buffers and may read past the end of the first value internally, but it still conceptually decodes one value per Decode call. [1], [2]

Sources:
[1] Go encoding/json docs (Decoder.Decode, NewDecoder) (pkg.go.dev)
[2] “Let’s Go Further” (example showing extra values/garbage are ignored unless a second Decode confirms io.EOF) (files.rileysnyder.dev)

Citations:


Reject trailing JSON after the first config object.

Decoder.Decode only consumes one JSON value and does not require the underlying reader to be at io.EOF. A file starting with a valid policy document followed by extra JSON or text will be silently accepted, which is a security risk for a trust-policy loader.

Proposed fix
 	dec := json.NewDecoder(r)
 	dec.DisallowUnknownFields()
 	if err := dec.Decode(&cfg); err != nil {
 		return nil, coreerr.E("trust.LoadPolicies", "failed to decode JSON", err)
 	}
+	var extra json.RawMessage
+	if err := dec.Decode(&extra); err != io.EOF {
+		if err != nil {
+			return nil, coreerr.E("trust.LoadPolicies", "failed to decode trailing JSON", err)
+		}
+		return nil, coreerr.E("trust.LoadPolicies", "unexpected trailing JSON", nil)
+	}
 	return convertPolicies(cfg)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@trust/config.go` around lines 38 - 43, The JSON decoder currently decodes one
value but ignores any trailing data; after calling dec.Decode(&cfg) in
LoadPolicies (using json.NewDecoder and dec.DisallowUnknownFields), attempt a
second Decode into an empty value and ensure it returns io.EOF — if it returns
anything else (including nil), return an error from LoadPolicies instead of
proceeding to convertPolicies(cfg); this guarantees rejection of any trailing
JSON/text after the first object.

@Snider Snider merged commit b8e8247 into main Mar 17, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant