passwordtype

package module
v0.0.0-...-c886a8e Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 4, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

README

passwordtype

A SQL-friendly, gqlgen-friendly argon2id password type. Hashes plaintext on the way in, refuses to leak the hash on the way out.

HashedPassword wraps github.com/ubgo/crypt's argon2id HashPassword / VerifyPassword. Plaintext is hashed once, on entry, and is never recoverable.

Integration Hooks Cost to your dependency tree
database/sql Value() / Scan() stdlib + ubgo/crypt
encoding/json MarshalJSON (always null) / UnmarshalJSON (hashes plaintext) stdlib
log/slog LogValue() returns [redacted] stdlib
gqlgen scalar MarshalGQL (always null) / UnmarshalGQL (hashes plaintext) none — duck-typed

Install

go get github.com/ubgo/passwordtype

Use

import "github.com/ubgo/passwordtype"

// Signup
p, err := passwordtype.New(input.Password)
if err != nil { return err }
db.User.Create().SetPassword(p).Save(ctx)

// Login
user, _ := db.User.Query().Where(user.EmailEQ(email)).Only(ctx)
if !user.Password.Verify(input.Password) {
    return errUnauthorized
}

In a gqlgen gqlgen.yml:

models:
  HashedPassword:
    model: github.com/ubgo/passwordtype.HashedPassword

In your GraphQL schema:

scalar HashedPassword

input SignupInput {
  email: String!
  password: HashedPassword!
}

type User {
  id: ID!
  password: HashedPassword   # always null in output
}

The UnmarshalGQL hook hashes the plaintext as gqlgen parses the input. Your resolver receives a HashedPassword whose stored value is already the argon2id hash — no manual hashing in resolvers, no chance of accidentally logging plaintext.

Defense in depth

Every channel that could leak the hash is closed:

Path Behavior
fmt.Sprint, fmt.Sprintf("%v", ...) "[redacted]"
fmt.Sprintf("%#v", ...) (GoString) "[redacted]"
encoding/json.Marshal null
log/slog "[redacted]"
gqlgen output null

Outbound Value() (the SQL driver) returns the hash because that's the column's storage representation. There is no path back to plaintext.

Behaviour

  • Empty plaintext rejected. New("") returns an error rather than producing a hash of nothing.
  • Zero value is unset. A HashedPassword{} returns false from Verify, nil from Value, and false from IsSet.
  • Scan(nil) clears the password — useful for nullable columns.
  • Hash() exists for trusted re-export only (admin tooling, snapshot import). Most callers should never use it.
  • FromHash(stored) wraps an already-hashed string for cases where you read from a trusted source that bypasses Scan.

License

Apache-2.0 — see LICENSE.

Documentation

Overview

Package passwordtype provides HashedPassword — a one-way argon2id-hashed password column type for SQL databases that doubles as a gqlgen scalar.

The type wraps github.com/ubgo/crypt's HashPassword/VerifyPassword (argon2id, OWASP-recommended modern password hash). Plaintext is never recoverable from a HashedPassword; the only operations on the stored value are Verify and round-trip through SQL.

Defense in depth: every redaction path is closed.

  • String, GoString return "[redacted]"
  • MarshalJSON returns null
  • LogValue returns "[redacted]" so slog never emits the hash
  • MarshalGQL writes null so the hash never reaches a GraphQL response

On the input side, UnmarshalGQL accepts a plaintext string from a GraphQL input field, hashes it via crypt.HashPassword, and stores the hash. Resolvers therefore receive a HashedPassword that is already hashed — no manual hashing in business code, no temptation to log the plaintext.

gqlgen integration is duck-typed: this package does not import gqlgen.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type HashedPassword

type HashedPassword struct {
	// contains filtered or unexported fields
}

HashedPassword is an argon2id password hash. The plaintext is never stored. The zero value is unset (Value returns NULL, Verify returns false).

func FromHash

func FromHash(stored string) HashedPassword

FromHash wraps an already-hashed PHC string. Use when reading from trusted storage that bypasses Scan (e.g. a snapshot import).

func New

func New(plaintext string) (HashedPassword, error)

New hashes plaintext with argon2id and returns a HashedPassword.

func (HashedPassword) GoString

func (p HashedPassword) GoString() string

GoString returns "[redacted]" — same protection for %#v formatting.

func (HashedPassword) Hash

func (p HashedPassword) Hash() string

Hash returns the raw stored hash. Intended only for callers that must re-export the value via a trusted boundary (e.g. user-export tooling behind admin auth). Most callers should prefer Verify.

func (HashedPassword) IsSet

func (p HashedPassword) IsSet() bool

IsSet reports whether the password has been set.

func (HashedPassword) LogValue

func (p HashedPassword) LogValue() slog.Value

LogValue makes HashedPassword safe for use with log/slog. The slog handler sees only "[redacted]", never the hash.

func (HashedPassword) MarshalGQL

func (p HashedPassword) MarshalGQL(w io.Writer)

MarshalGQL is the gqlgen-compatible marshal hook. It always writes null — the hash must never leave the server via GraphQL.

func (HashedPassword) MarshalJSON

func (p HashedPassword) MarshalJSON() ([]byte, error)

MarshalJSON always returns null. There is no legitimate JSON consumer that needs the raw hash; encode the entity without the password field, or expose Verify behind an authorization check.

func (*HashedPassword) Scan

func (p *HashedPassword) Scan(src any) error

Scan implements [sql.Scanner]. Accepts string, []byte, or nil.

func (HashedPassword) String

func (p HashedPassword) String() string

String returns "[redacted]" — the stored hash never appears in fmt output, debug prints, or panic traces.

func (*HashedPassword) UnmarshalGQL

func (p *HashedPassword) UnmarshalGQL(v any) error

UnmarshalGQL accepts a plaintext string from a GraphQL input field and hashes it. Resolvers receive a HashedPassword whose Value is the hash, not the plaintext.

func (*HashedPassword) UnmarshalJSON

func (p *HashedPassword) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts either null or a string. A string is treated as a plaintext password and hashed.

func (HashedPassword) Value

func (p HashedPassword) Value() (driver.Value, error)

Value implements driver.Valuer. An unset password persists as SQL NULL.

func (HashedPassword) Verify

func (p HashedPassword) Verify(plaintext string) bool

Verify reports whether plaintext matches the stored hash. Returns false when the password is unset or any comparison error occurs; callers who need to distinguish the two cases should call IsSet first.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL