refactor: standardise coreerr import alias and fix shortenPackageName
Some checks failed
Security Scan / security (pull_request) Failing after 7s
Test / test (pull_request) Successful in 11m55s

- CLAUDE.md: update error convention from core.E() to coreerr.E() to
  match actual codebase usage
- Standardise go-log import alias from `core` to `coreerr` across 6
  files (crypt/symmetric.go, crypt/kdf.go, crypt/crypt.go, crypt/hash.go,
  crypt/checksum.go, crypt/openpgp/service.go) for consistency with the
  11 files already using `coreerr`
- Fix shortenPackageName to handle all forge.lthn.ai/core/* module
  prefixes instead of only cli/ and gui/, fixing TestShortenPackageName

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-17 07:22:34 +00:00
parent f4a219816a
commit 703dd4588c
8 changed files with 65 additions and 65 deletions

View file

@ -59,8 +59,8 @@ No C toolchain or CGo required — all crypto uses pure Go implementations.
- **Tests**: testify assert/require, `_Good`/`_Bad`/`_Ugly` naming convention
- **Concurrency tests**: 10 goroutines via WaitGroup; must pass `-race`
- **Imports**: stdlib → forge.lthn.ai → third-party, separated by blank lines
- **Errors**: use `core.E("package.Function", "lowercase message", err)` (imported
from `forge.lthn.ai/core/go-log`); never include secrets in error strings
- **Errors**: use `coreerr.E("package.Function", "lowercase message", err)` (imported
as `coreerr "forge.lthn.ai/core/go-log"`); never include secrets in error strings
- **Randomness**: `crypto/rand` only; never `math/rand`
- **Conventional commits**: `feat(auth):`, `fix(crypt):`, `refactor(trust):`
Scopes match package names: `auth`, `crypt`, `trust`, `pgp`, `lthn`, `rsa`,

View file

@ -171,15 +171,15 @@ func formatCoverage(cov float64) string {
}
func shortenPackageName(name string) string {
// Remove common prefixes
prefixes := []string{
"forge.lthn.ai/core/cli/",
"forge.lthn.ai/core/gui/",
}
for _, prefix := range prefixes {
if strings.HasPrefix(name, prefix) {
return strings.TrimPrefix(name, prefix)
const forgePrefix = "forge.lthn.ai/core/"
if strings.HasPrefix(name, forgePrefix) {
remainder := strings.TrimPrefix(name, forgePrefix)
// If there's a sub-path (e.g. "go/pkg/foo"), strip the module name
if idx := strings.Index(remainder, "/"); idx >= 0 {
return remainder[idx+1:]
}
// Module root (e.g. "cli-php") — return as-is
return remainder
}
return filepath.Base(name)
}

View file

@ -7,20 +7,20 @@ import (
"io"
"os"
core "forge.lthn.ai/core/go-log"
coreerr "forge.lthn.ai/core/go-log"
)
// SHA256File computes the SHA-256 checksum of a file and returns it as a hex string.
func SHA256File(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", core.E("crypt.SHA256File", "failed to open file", err)
return "", coreerr.E("crypt.SHA256File", "failed to open file", err)
}
defer func() { _ = f.Close() }()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", core.E("crypt.SHA256File", "failed to read file", err)
return "", coreerr.E("crypt.SHA256File", "failed to read file", err)
}
return hex.EncodeToString(h.Sum(nil)), nil
@ -30,13 +30,13 @@ func SHA256File(path string) (string, error) {
func SHA512File(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", core.E("crypt.SHA512File", "failed to open file", err)
return "", coreerr.E("crypt.SHA512File", "failed to open file", err)
}
defer func() { _ = f.Close() }()
h := sha512.New()
if _, err := io.Copy(h, f); err != nil {
return "", core.E("crypt.SHA512File", "failed to read file", err)
return "", coreerr.E("crypt.SHA512File", "failed to read file", err)
}
return hex.EncodeToString(h.Sum(nil)), nil

View file

@ -1,7 +1,7 @@
package crypt
import (
core "forge.lthn.ai/core/go-log"
coreerr "forge.lthn.ai/core/go-log"
)
// Encrypt encrypts data with a passphrase using ChaCha20-Poly1305.
@ -10,14 +10,14 @@ import (
func Encrypt(plaintext, passphrase []byte) ([]byte, error) {
salt, err := generateSalt(argon2SaltLen)
if err != nil {
return nil, core.E("crypt.Encrypt", "failed to generate salt", err)
return nil, coreerr.E("crypt.Encrypt", "failed to generate salt", err)
}
key := DeriveKey(passphrase, salt, argon2KeyLen)
encrypted, err := ChaCha20Encrypt(plaintext, key)
if err != nil {
return nil, core.E("crypt.Encrypt", "failed to encrypt", err)
return nil, coreerr.E("crypt.Encrypt", "failed to encrypt", err)
}
// Prepend salt to the encrypted data (which already has nonce prepended)
@ -31,7 +31,7 @@ func Encrypt(plaintext, passphrase []byte) ([]byte, error) {
// Expects format: salt (16 bytes) + nonce (24 bytes) + ciphertext.
func Decrypt(ciphertext, passphrase []byte) ([]byte, error) {
if len(ciphertext) < argon2SaltLen {
return nil, core.E("crypt.Decrypt", "ciphertext too short", nil)
return nil, coreerr.E("crypt.Decrypt", "ciphertext too short", nil)
}
salt := ciphertext[:argon2SaltLen]
@ -41,7 +41,7 @@ func Decrypt(ciphertext, passphrase []byte) ([]byte, error) {
plaintext, err := ChaCha20Decrypt(encrypted, key)
if err != nil {
return nil, core.E("crypt.Decrypt", "failed to decrypt", err)
return nil, coreerr.E("crypt.Decrypt", "failed to decrypt", err)
}
return plaintext, nil
@ -53,14 +53,14 @@ func Decrypt(ciphertext, passphrase []byte) ([]byte, error) {
func EncryptAES(plaintext, passphrase []byte) ([]byte, error) {
salt, err := generateSalt(argon2SaltLen)
if err != nil {
return nil, core.E("crypt.EncryptAES", "failed to generate salt", err)
return nil, coreerr.E("crypt.EncryptAES", "failed to generate salt", err)
}
key := DeriveKey(passphrase, salt, argon2KeyLen)
encrypted, err := AESGCMEncrypt(plaintext, key)
if err != nil {
return nil, core.E("crypt.EncryptAES", "failed to encrypt", err)
return nil, coreerr.E("crypt.EncryptAES", "failed to encrypt", err)
}
result := make([]byte, 0, len(salt)+len(encrypted))
@ -73,7 +73,7 @@ func EncryptAES(plaintext, passphrase []byte) ([]byte, error) {
// Expects format: salt (16 bytes) + nonce (12 bytes) + ciphertext.
func DecryptAES(ciphertext, passphrase []byte) ([]byte, error) {
if len(ciphertext) < argon2SaltLen {
return nil, core.E("crypt.DecryptAES", "ciphertext too short", nil)
return nil, coreerr.E("crypt.DecryptAES", "ciphertext too short", nil)
}
salt := ciphertext[:argon2SaltLen]
@ -83,7 +83,7 @@ func DecryptAES(ciphertext, passphrase []byte) ([]byte, error) {
plaintext, err := AESGCMDecrypt(encrypted, key)
if err != nil {
return nil, core.E("crypt.DecryptAES", "failed to decrypt", err)
return nil, coreerr.E("crypt.DecryptAES", "failed to decrypt", err)
}
return plaintext, nil

View file

@ -6,7 +6,7 @@ import (
"fmt"
"strings"
core "forge.lthn.ai/core/go-log"
coreerr "forge.lthn.ai/core/go-log"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/bcrypt"
)
@ -16,7 +16,7 @@ import (
func HashPassword(password string) (string, error) {
salt, err := generateSalt(argon2SaltLen)
if err != nil {
return "", core.E("crypt.HashPassword", "failed to generate salt", err)
return "", coreerr.E("crypt.HashPassword", "failed to generate salt", err)
}
hash := argon2.IDKey([]byte(password), salt, argon2Time, argon2Memory, argon2Parallelism, argon2KeyLen)
@ -36,29 +36,29 @@ func HashPassword(password string) (string, error) {
func VerifyPassword(password, hash string) (bool, error) {
parts := strings.Split(hash, "$")
if len(parts) != 6 {
return false, core.E("crypt.VerifyPassword", "invalid hash format", nil)
return false, coreerr.E("crypt.VerifyPassword", "invalid hash format", nil)
}
var version int
if _, err := fmt.Sscanf(parts[2], "v=%d", &version); err != nil {
return false, core.E("crypt.VerifyPassword", "failed to parse version", err)
return false, coreerr.E("crypt.VerifyPassword", "failed to parse version", err)
}
var memory uint32
var time uint32
var parallelism uint8
if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, &parallelism); err != nil {
return false, core.E("crypt.VerifyPassword", "failed to parse parameters", err)
return false, coreerr.E("crypt.VerifyPassword", "failed to parse parameters", err)
}
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil {
return false, core.E("crypt.VerifyPassword", "failed to decode salt", err)
return false, coreerr.E("crypt.VerifyPassword", "failed to decode salt", err)
}
expectedHash, err := base64.RawStdEncoding.DecodeString(parts[5])
if err != nil {
return false, core.E("crypt.VerifyPassword", "failed to decode hash", err)
return false, coreerr.E("crypt.VerifyPassword", "failed to decode hash", err)
}
computedHash := argon2.IDKey([]byte(password), salt, time, memory, parallelism, uint32(len(expectedHash)))
@ -71,7 +71,7 @@ func VerifyPassword(password, hash string) (bool, error) {
func HashBcrypt(password string, cost int) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), cost)
if err != nil {
return "", core.E("crypt.HashBcrypt", "failed to hash password", err)
return "", coreerr.E("crypt.HashBcrypt", "failed to hash password", err)
}
return string(hash), nil
}
@ -83,7 +83,7 @@ func VerifyBcrypt(password, hash string) (bool, error) {
return false, nil
}
if err != nil {
return false, core.E("crypt.VerifyBcrypt", "failed to verify password", err)
return false, coreerr.E("crypt.VerifyBcrypt", "failed to verify password", err)
}
return true, nil
}

View file

@ -7,7 +7,7 @@ import (
"crypto/sha256"
"io"
core "forge.lthn.ai/core/go-log"
coreerr "forge.lthn.ai/core/go-log"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/hkdf"
"golang.org/x/crypto/scrypt"
@ -33,7 +33,7 @@ func DeriveKey(passphrase, salt []byte, keyLen uint32) []byte {
func DeriveKeyScrypt(passphrase, salt []byte, keyLen int) ([]byte, error) {
key, err := scrypt.Key(passphrase, salt, 32768, 8, 1, keyLen)
if err != nil {
return nil, core.E("crypt.DeriveKeyScrypt", "failed to derive key", err)
return nil, coreerr.E("crypt.DeriveKeyScrypt", "failed to derive key", err)
}
return key, nil
}
@ -45,7 +45,7 @@ func HKDF(secret, salt, info []byte, keyLen int) ([]byte, error) {
reader := hkdf.New(sha256.New, secret, salt, info)
key := make([]byte, keyLen)
if _, err := io.ReadFull(reader, key); err != nil {
return nil, core.E("crypt.HKDF", "failed to derive key", err)
return nil, coreerr.E("crypt.HKDF", "failed to derive key", err)
}
return key, nil
}
@ -54,7 +54,7 @@ func HKDF(secret, salt, info []byte, keyLen int) ([]byte, error) {
func generateSalt(length int) ([]byte, error) {
salt := make([]byte, length)
if _, err := rand.Read(salt); err != nil {
return nil, core.E("crypt.generateSalt", "failed to generate random salt", err)
return nil, coreerr.E("crypt.generateSalt", "failed to generate random salt", err)
}
return salt, nil
}

View file

@ -10,7 +10,7 @@ import (
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/packet"
core "forge.lthn.ai/core/go-log"
coreerr "forge.lthn.ai/core/go-log"
framework "forge.lthn.ai/core/go/pkg/core"
)
@ -36,19 +36,19 @@ func (s *Service) CreateKeyPair(name, passphrase string) (string, error) {
entity, err := openpgp.NewEntity(name, "Workspace Key", "", config)
if err != nil {
return "", core.E("openpgp.CreateKeyPair", "failed to create entity", err)
return "", coreerr.E("openpgp.CreateKeyPair", "failed to create entity", err)
}
// Encrypt private key if passphrase is provided
if passphrase != "" {
err = entity.PrivateKey.Encrypt([]byte(passphrase))
if err != nil {
return "", core.E("openpgp.CreateKeyPair", "failed to encrypt private key", err)
return "", coreerr.E("openpgp.CreateKeyPair", "failed to encrypt private key", err)
}
for _, subkey := range entity.Subkeys {
err = subkey.PrivateKey.Encrypt([]byte(passphrase))
if err != nil {
return "", core.E("openpgp.CreateKeyPair", "failed to encrypt subkey", err)
return "", coreerr.E("openpgp.CreateKeyPair", "failed to encrypt subkey", err)
}
}
}
@ -56,14 +56,14 @@ func (s *Service) CreateKeyPair(name, passphrase string) (string, error) {
var buf bytes.Buffer
w, err := armor.Encode(&buf, openpgp.PrivateKeyType, nil)
if err != nil {
return "", core.E("openpgp.CreateKeyPair", "failed to create armor encoder", err)
return "", coreerr.E("openpgp.CreateKeyPair", "failed to create armor encoder", err)
}
// Manual serialization to avoid panic from re-signing encrypted keys
err = s.serializeEntity(w, entity)
if err != nil {
w.Close()
return "", core.E("openpgp.CreateKeyPair", "failed to serialize private key", err)
return "", coreerr.E("openpgp.CreateKeyPair", "failed to serialize private key", err)
}
w.Close()
@ -104,13 +104,13 @@ func (s *Service) serializeEntity(w goio.Writer, e *openpgp.Entity) error {
func (s *Service) EncryptPGP(writer goio.Writer, recipientPath, data string, opts ...any) (string, error) {
entityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(recipientPath))
if err != nil {
return "", core.E("openpgp.EncryptPGP", "failed to read recipient key", err)
return "", coreerr.E("openpgp.EncryptPGP", "failed to read recipient key", err)
}
var armoredBuf bytes.Buffer
armoredWriter, err := armor.Encode(&armoredBuf, "PGP MESSAGE", nil)
if err != nil {
return "", core.E("openpgp.EncryptPGP", "failed to create armor encoder", err)
return "", coreerr.E("openpgp.EncryptPGP", "failed to create armor encoder", err)
}
// MultiWriter to write to both the provided writer and our armored buffer
@ -119,14 +119,14 @@ func (s *Service) EncryptPGP(writer goio.Writer, recipientPath, data string, opt
w, err := openpgp.Encrypt(mw, entityList, nil, nil, nil)
if err != nil {
armoredWriter.Close()
return "", core.E("openpgp.EncryptPGP", "failed to start encryption", err)
return "", coreerr.E("openpgp.EncryptPGP", "failed to start encryption", err)
}
_, err = goio.WriteString(w, data)
if err != nil {
w.Close()
armoredWriter.Close()
return "", core.E("openpgp.EncryptPGP", "failed to write data", err)
return "", coreerr.E("openpgp.EncryptPGP", "failed to write data", err)
}
w.Close()
@ -139,14 +139,14 @@ func (s *Service) EncryptPGP(writer goio.Writer, recipientPath, data string, opt
func (s *Service) DecryptPGP(privateKey, message, passphrase string, opts ...any) (string, error) {
entityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(privateKey))
if err != nil {
return "", core.E("openpgp.DecryptPGP", "failed to read private key", err)
return "", coreerr.E("openpgp.DecryptPGP", "failed to read private key", err)
}
entity := entityList[0]
if entity.PrivateKey.Encrypted {
err = entity.PrivateKey.Decrypt([]byte(passphrase))
if err != nil {
return "", core.E("openpgp.DecryptPGP", "failed to decrypt private key", err)
return "", coreerr.E("openpgp.DecryptPGP", "failed to decrypt private key", err)
}
for _, subkey := range entity.Subkeys {
_ = subkey.PrivateKey.Decrypt([]byte(passphrase))
@ -156,18 +156,18 @@ func (s *Service) DecryptPGP(privateKey, message, passphrase string, opts ...any
// Decrypt armored message
block, err := armor.Decode(strings.NewReader(message))
if err != nil {
return "", core.E("openpgp.DecryptPGP", "failed to decode armored message", err)
return "", coreerr.E("openpgp.DecryptPGP", "failed to decode armored message", err)
}
md, err := openpgp.ReadMessage(block.Body, entityList, nil, nil)
if err != nil {
return "", core.E("openpgp.DecryptPGP", "failed to read message", err)
return "", coreerr.E("openpgp.DecryptPGP", "failed to read message", err)
}
var buf bytes.Buffer
_, err = goio.Copy(&buf, md.UnverifiedBody)
if err != nil {
return "", core.E("openpgp.DecryptPGP", "failed to read decrypted body", err)
return "", coreerr.E("openpgp.DecryptPGP", "failed to read decrypted body", err)
}
return buf.String(), nil

View file

@ -5,7 +5,7 @@ import (
"crypto/cipher"
"crypto/rand"
core "forge.lthn.ai/core/go-log"
coreerr "forge.lthn.ai/core/go-log"
"golang.org/x/crypto/chacha20poly1305"
)
@ -15,12 +15,12 @@ import (
func ChaCha20Encrypt(plaintext, key []byte) ([]byte, error) {
aead, err := chacha20poly1305.NewX(key)
if err != nil {
return nil, core.E("crypt.ChaCha20Encrypt", "failed to create cipher", err)
return nil, coreerr.E("crypt.ChaCha20Encrypt", "failed to create cipher", err)
}
nonce := make([]byte, aead.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, core.E("crypt.ChaCha20Encrypt", "failed to generate nonce", err)
return nil, coreerr.E("crypt.ChaCha20Encrypt", "failed to generate nonce", err)
}
ciphertext := aead.Seal(nonce, nonce, plaintext, nil)
@ -32,18 +32,18 @@ func ChaCha20Encrypt(plaintext, key []byte) ([]byte, error) {
func ChaCha20Decrypt(ciphertext, key []byte) ([]byte, error) {
aead, err := chacha20poly1305.NewX(key)
if err != nil {
return nil, core.E("crypt.ChaCha20Decrypt", "failed to create cipher", err)
return nil, coreerr.E("crypt.ChaCha20Decrypt", "failed to create cipher", err)
}
nonceSize := aead.NonceSize()
if len(ciphertext) < nonceSize {
return nil, core.E("crypt.ChaCha20Decrypt", "ciphertext too short", nil)
return nil, coreerr.E("crypt.ChaCha20Decrypt", "ciphertext too short", nil)
}
nonce, encrypted := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, err := aead.Open(nil, nonce, encrypted, nil)
if err != nil {
return nil, core.E("crypt.ChaCha20Decrypt", "failed to decrypt", err)
return nil, coreerr.E("crypt.ChaCha20Decrypt", "failed to decrypt", err)
}
return plaintext, nil
@ -55,17 +55,17 @@ func ChaCha20Decrypt(ciphertext, key []byte) ([]byte, error) {
func AESGCMEncrypt(plaintext, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, core.E("crypt.AESGCMEncrypt", "failed to create cipher", err)
return nil, coreerr.E("crypt.AESGCMEncrypt", "failed to create cipher", err)
}
aead, err := cipher.NewGCM(block)
if err != nil {
return nil, core.E("crypt.AESGCMEncrypt", "failed to create GCM", err)
return nil, coreerr.E("crypt.AESGCMEncrypt", "failed to create GCM", err)
}
nonce := make([]byte, aead.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, core.E("crypt.AESGCMEncrypt", "failed to generate nonce", err)
return nil, coreerr.E("crypt.AESGCMEncrypt", "failed to generate nonce", err)
}
ciphertext := aead.Seal(nonce, nonce, plaintext, nil)
@ -77,23 +77,23 @@ func AESGCMEncrypt(plaintext, key []byte) ([]byte, error) {
func AESGCMDecrypt(ciphertext, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, core.E("crypt.AESGCMDecrypt", "failed to create cipher", err)
return nil, coreerr.E("crypt.AESGCMDecrypt", "failed to create cipher", err)
}
aead, err := cipher.NewGCM(block)
if err != nil {
return nil, core.E("crypt.AESGCMDecrypt", "failed to create GCM", err)
return nil, coreerr.E("crypt.AESGCMDecrypt", "failed to create GCM", err)
}
nonceSize := aead.NonceSize()
if len(ciphertext) < nonceSize {
return nil, core.E("crypt.AESGCMDecrypt", "ciphertext too short", nil)
return nil, coreerr.E("crypt.AESGCMDecrypt", "ciphertext too short", nil)
}
nonce, encrypted := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, err := aead.Open(nil, nonce, encrypted, nil)
if err != nil {
return nil, core.E("crypt.AESGCMDecrypt", "failed to decrypt", err)
return nil, coreerr.E("crypt.AESGCMDecrypt", "failed to decrypt", err)
}
return plaintext, nil