christiangeorgelucas/password-hash-tools
v0.1.0GoThe password-hash-tools package provides a set of composable nodes for securely hashing and verifying passwords using various algorithms, including Argon2id, bcrypt, scrypt, and PBKDF2-HMAC. It is designed for developers looking to implement robust password storage solutions with features like constant-time verification and cost-upgrade checks.
Published by Christian George Lucas· MIT
Use cases
- •Implement secure user authentication in web applications
- •Migrate existing password hashes to stronger algorithms
- •Perform password strength checks during user registration
- •Upgrade password storage parameters without user intervention
Nodes (11)
The HashArgon2id node derives a password hash using the Argon2id algorithm, which is recommended by OWASP for secure password storage. It takes a password, a user-supplied salt, and configurable cost parameters to produce a deterministic, PHC-format hash string along with the raw derived key and the salt used.
View source
package nodes
import (
"context"
"encoding/hex"
"christiangeorgelucas/password-hash-tools/axiom"
gen "christiangeorgelucas/password-hash-tools/gen"
"golang.org/x/crypto/argon2"
)
// HashArgon2id derives a password hash using Argon2id (RFC 9106) with a
// caller-supplied salt and cost parameters, returning a standard PHC-format
// string plus the raw derived key. Deterministic: the same password, salt,
// and parameters always produce the same output.
func HashArgon2id(ctx context.Context, ax axiom.Context, input *gen.HashArgon2IdInput) (*gen.PasswordHash, error) {
salt, errTok := decodeSalt(input.GetSalt(), input.GetSaltEncoding())
if errTok != "" {
return &gen.PasswordHash{Error: errTok}, nil
}
if len(salt) == 0 {
return &gen.PasswordHash{Error: "EMPTY_SALT"}, nil
}
if len(salt) < 8 {
return &gen.PasswordHash{Error: "SALT_TOO_SHORT"}, nil
}
memKiB := input.GetMemoryKib()
if memKiB == 0 {
memKiB = 65536
}
iterations := input.GetIterations()
if iterations == 0 {
iterations = 3
}
parallelism := input.GetParallelism()
if parallelism == 0 {
parallelism = 4
}
if parallelism > 255 {
return &gen.PasswordHash{Error: "INVALID_PARAMS"}, nil
}
keyLen := input.GetKeyLen()
if keyLen == 0 {
keyLen = 32
}
hash := argon2.IDKey([]byte(input.GetPassword()), salt, iterations, memKiB, uint8(parallelism), keyLen)
return &gen.PasswordHash{
Algorithm: "argon2id",
PhcHash: encodeArgon2idPHC(memKiB, iterations, parallelism, salt, hash),
HashHex: hex.EncodeToString(hash),
HashBase64: b64std(hash),
SaltBase64: b64std(salt),
}, nil
}
The HashBcrypt node derives a password hash using the bcrypt algorithm, which includes an internal random salt generation to ensure unique hashes for identical passwords. It accepts a password and a cost factor, returning a bcrypt-compatible hash string.
View source
package nodes
import (
"context"
"errors"
"christiangeorgelucas/password-hash-tools/axiom"
gen "christiangeorgelucas/password-hash-tools/gen"
"golang.org/x/crypto/bcrypt"
)
// HashBcrypt derives a password hash using bcrypt. Unlike this package's
// other three hashers, bcrypt generates its own random salt internally (the
// reference algorithm gives callers no salt-injection point), so repeated
// calls with the same password produce a different `phc_hash` every time —
// VerifyBcrypt against any of them still succeeds.
func HashBcrypt(ctx context.Context, ax axiom.Context, input *gen.HashBcryptInput) (*gen.PasswordHash, error) {
cost := int(input.GetCost())
if cost == 0 {
cost = bcrypt.DefaultCost
}
if cost < bcrypt.MinCost || cost > bcrypt.MaxCost {
return &gen.PasswordHash{Error: "INVALID_PARAMS"}, nil
}
hashBytes, err := bcrypt.GenerateFromPassword([]byte(input.GetPassword()), cost)
if err != nil {
if errors.Is(err, bcrypt.ErrPasswordTooLong) {
return &gen.PasswordHash{Error: "PASSWORD_TOO_LONG"}, nil
}
return &gen.PasswordHash{Error: "INVALID_PARAMS"}, nil
}
return &gen.PasswordHash{
Algorithm: "bcrypt",
PhcHash: string(hashBytes),
}, nil
}
The HashPbkdf2 node derives a password hash using the PBKDF2-HMAC algorithm as specified in RFC 8018. It takes a password, a non-empty salt, an iteration count, a hash algorithm, and an output key length to produce a deterministic hash output.
View source
package nodes
import (
"context"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"hash"
"strings"
"christiangeorgelucas/password-hash-tools/axiom"
gen "christiangeorgelucas/password-hash-tools/gen"
"golang.org/x/crypto/pbkdf2"
)
// pbkdf2HashFunc resolves a caller-supplied hash algorithm name (case-
// insensitive; '-'/'_' equivalent) to its constructor and normalized name.
// Defaults to sha256 when empty.
func pbkdf2HashFunc(name string) (func() hash.Hash, string, bool) {
norm := strings.ToLower(strings.NewReplacer("-", "", "_", "").Replace(name))
switch norm {
case "", "sha256":
return sha256.New, "sha256", true
case "sha1":
return sha1.New, "sha1", true
case "sha384":
return sha512.New384, "sha384", true
case "sha512":
return sha512.New, "sha512", true
default:
return nil, "", false
}
}
// HashPbkdf2 derives a password hash using PBKDF2-HMAC (RFC 8018) with a
// caller-supplied salt, iteration count, and underlying hash, returning this
// package's own documented PHC-style string plus the raw derived key.
// Deterministic: the same password, salt, and parameters always produce the
// same output.
func HashPbkdf2(ctx context.Context, ax axiom.Context, input *gen.HashPbkdf2Input) (*gen.PasswordHash, error) {
salt, errTok := decodeSalt(input.GetSalt(), input.GetSaltEncoding())
if errTok != "" {
return &gen.PasswordHash{Error: errTok}, nil
}
if len(salt) == 0 {
return &gen.PasswordHash{Error: "EMPTY_SALT"}, nil
}
hf, hfName, ok := pbkdf2HashFunc(input.GetHashAlgorithm())
if !ok {
return &gen.PasswordHash{Error: "UNKNOWN_HASH_ALGORITHM"}, nil
}
iterations := input.GetIterations()
if iterations == 0 {
iterations = 600000
}
keyLen := input.GetKeyLen()
if keyLen == 0 {
keyLen = 32
}
hashBytes := pbkdf2.Key([]byte(input.GetPassword()), salt, int(iterations), int(keyLen), hf)
return &gen.PasswordHash{
Algorithm: "pbkdf2-" + hfName,
PhcHash: encodePbkdf2PHC(hfName, iterations, salt, hashBytes),
HashHex: hex.EncodeToString(hashBytes),
HashBase64: b64std(hashBytes),
SaltBase64: b64std(salt),
}, nil
}
The HashScrypt node derives a password hash using the scrypt algorithm as specified in RFC 7914, utilizing a user-defined salt and configurable cost parameters. It produces a deterministic output, ensuring that the same inputs will always yield the same hash.
View source
package nodes
import (
"context"
"encoding/hex"
"christiangeorgelucas/password-hash-tools/axiom"
gen "christiangeorgelucas/password-hash-tools/gen"
"golang.org/x/crypto/scrypt"
)
// HashScrypt derives a password hash using scrypt (RFC 7914) with a
// caller-supplied salt and cost parameters, returning this package's own
// documented PHC-style string plus the raw derived key. Deterministic: the
// same password, salt, and parameters always produce the same output.
func HashScrypt(ctx context.Context, ax axiom.Context, input *gen.HashScryptInput) (*gen.PasswordHash, error) {
salt, errTok := decodeSalt(input.GetSalt(), input.GetSaltEncoding())
if errTok != "" {
return &gen.PasswordHash{Error: errTok}, nil
}
if len(salt) == 0 {
return &gen.PasswordHash{Error: "EMPTY_SALT"}, nil
}
n := input.GetN()
if n == 0 {
n = 16384
}
r := input.GetR()
if r == 0 {
r = 8
}
p := input.GetP()
if p == 0 {
p = 1
}
keyLen := input.GetKeyLen()
if keyLen == 0 {
keyLen = 32
}
hash, err := scrypt.Key([]byte(input.GetPassword()), salt, int(n), int(r), int(p), int(keyLen))
if err != nil {
return &gen.PasswordHash{Error: "INVALID_PARAMS"}, nil
}
return &gen.PasswordHash{
Algorithm: "scrypt",
PhcHash: encodeScryptPHC(n, r, p, salt, hash),
HashHex: hex.EncodeToString(hash),
HashBase64: b64std(hash),
SaltBase64: b64std(salt),
}, nil
}
The NeedsRehash node determines if a stored password hash's cost parameters are below a specified target, indicating that the hash should be rehashed on the next successful login. It evaluates the parameters of the stored hash against the provided target parameters and returns a decision along with the current parameters of the hash.
View source
package nodes
import (
"context"
"strconv"
"christiangeorgelucas/password-hash-tools/axiom"
gen "christiangeorgelucas/password-hash-tools/gen"
)
// NeedsRehash decides whether a stored hash's embedded cost parameters have
// fallen below a caller-chosen current target (e.g. after raising the
// site-wide Argon2id memory cost), so it should be re-hashed the next time
// the plaintext password is available (typically right after a successful
// login). It reuses ParsePhcString to identify the algorithm and parse the
// hash's own parameters, then compares each parameter present in both sets
// numerically; a target key with no numeric counterpart (or absent from the
// hash's own params) is skipped, not an error.
func NeedsRehash(ctx context.Context, ax axiom.Context, input *gen.NeedsRehashInput) (*gen.NeedsRehashResult, error) {
info, err := ParsePhcString(ctx, ax, &gen.ParsePhcStringInput{PhcHash: input.GetPhcHash()})
if err != nil {
return nil, err
}
if info.GetError() != "" {
return &gen.NeedsRehashResult{Algorithm: "unknown", Error: info.GetError()}, nil
}
needs := false
for key, targetStr := range input.GetTargetParams() {
curStr, ok := info.GetParams()[key]
if !ok {
continue
}
targetN, err1 := strconv.ParseFloat(targetStr, 64)
curN, err2 := strconv.ParseFloat(curStr, 64)
if err1 != nil || err2 != nil {
continue // non-numeric params (e.g. pbkdf2's "hash" name) are skipped
}
if curN < targetN {
needs = true
}
}
return &gen.NeedsRehashResult{
NeedsRehash: needs,
Algorithm: info.GetAlgorithm(),
CurrentParams: info.GetParams(),
}, nil
}
The ParsePhcString node inspects a stored hash string generated by various hashing algorithms and extracts its algorithm type, cost/tuning parameters, and salt without performing any verification. It returns 'unknown' for unrecognized or unparseable strings.
View source
package nodes
import (
"context"
"strconv"
"strings"
"christiangeorgelucas/password-hash-tools/axiom"
gen "christiangeorgelucas/password-hash-tools/gen"
)
// ParsePhcString inspects a stored hash string — as produced by any of this
// package's four Hash* nodes — and reports its algorithm, cost/tuning
// parameters, and salt, without verifying anything against it.
func ParsePhcString(ctx context.Context, ax axiom.Context, input *gen.ParsePhcStringInput) (*gen.PhcInfo, error) {
phc := input.GetPhcHash()
if phc == "" {
return &gen.PhcInfo{Algorithm: "unknown", Error: "EMPTY_INPUT"}, nil
}
switch detectAlgorithmFamily(phc) {
case "argon2id":
p, ok := parseArgon2idPHC(phc)
if !ok {
return &gen.PhcInfo{Algorithm: "unknown", Error: "UNKNOWN_HASH_FORMAT"}, nil
}
return &gen.PhcInfo{
Algorithm: "argon2id",
Params: map[string]string{
"v": strconv.Itoa(int(p.version)),
"m": strconv.Itoa(int(p.memKiB)),
"t": strconv.Itoa(int(p.iterations)),
"p": strconv.Itoa(int(p.parallelism)),
},
SaltBase64: b64std(p.salt),
}, nil
case "bcrypt":
parts := strings.Split(phc, "$")
// ["", "2a"/"2b"/"2y", cost, saltandhash]
if len(parts) != 4 {
return &gen.PhcInfo{Algorithm: "unknown", Error: "UNKNOWN_HASH_FORMAT"}, nil
}
if _, err := strconv.Atoi(parts[2]); err != nil {
return &gen.PhcInfo{Algorithm: "unknown", Error: "UNKNOWN_HASH_FORMAT"}, nil
}
return &gen.PhcInfo{
Algorithm: "bcrypt",
Params: map[string]string{"cost": parts[2]},
}, nil
case "scrypt":
p, ok := parseScryptPHC(phc)
if !ok {
return &gen.PhcInfo{Algorithm: "unknown", Error: "UNKNOWN_HASH_FORMAT"}, nil
}
return &gen.PhcInfo{
Algorithm: "scrypt",
Params: map[string]string{
"n": strconv.Itoa(int(p.n)),
"r": strconv.Itoa(int(p.r)),
"p": strconv.Itoa(int(p.p)),
},
SaltBase64: b64std(p.salt),
}, nil
case "pbkdf2":
p, ok := parsePbkdf2PHC(phc)
if !ok {
return &gen.PhcInfo{Algorithm: "unknown", Error: "UNKNOWN_HASH_FORMAT"}, nil
}
return &gen.PhcInfo{
Algorithm: "pbkdf2-" + p.hashName,
Params: map[string]string{
"i": strconv.Itoa(int(p.iterations)),
"hash": p.hashName,
},
SaltBase64: b64std(p.salt),
}, nil
default:
return &gen.PhcInfo{Algorithm: "unknown", Error: "UNKNOWN_HASH_FORMAT"}, nil
}
}
The Verify node checks a candidate password against a stored hash string by auto-detecting the hashing algorithm used (argon2id, bcrypt, scrypt, or pbkdf2) based on the hash's prefix. It allows for seamless verification of passwords from multiple hashing algorithms without requiring the caller to track which algorithm was used for each hash.
View source
package nodes
import (
"context"
"christiangeorgelucas/password-hash-tools/axiom"
gen "christiangeorgelucas/password-hash-tools/gen"
)
// Verify checks a candidate password against a stored hash string,
// auto-detecting the algorithm (argon2id, bcrypt, scrypt, or pbkdf2) from
// the string's own prefix and dispatching to the matching per-algorithm
// verifier — for a caller that stores hashes from more than one algorithm
// (e.g. after migrating cost parameters) and doesn't want to track which
// produced which. A wrong password is a clean non-match (matches=false,
// error empty), never an error.
func Verify(ctx context.Context, ax axiom.Context, input *gen.VerifyInput) (*gen.VerifyResult, error) {
phc := input.GetPhcHash()
if phc == "" {
return &gen.VerifyResult{Error: "EMPTY_INPUT"}, nil
}
switch detectAlgorithmFamily(phc) {
case "argon2id":
return VerifyArgon2id(ctx, ax, input)
case "bcrypt":
return VerifyBcrypt(ctx, ax, input)
case "scrypt":
return VerifyScrypt(ctx, ax, input)
case "pbkdf2":
return VerifyPbkdf2(ctx, ax, input)
default:
return &gen.VerifyResult{Error: "UNKNOWN_HASH_FORMAT"}, nil
}
}
The VerifyArgon2id node checks a candidate password against a stored Argon2id PHC hash string, recomputing the hash using the embedded parameters and performing a constant-time comparison. It returns a result indicating whether the password matches the hash, along with any relevant error messages for incorrect formats or algorithms.
View source
package nodes
import (
"context"
"crypto/subtle"
"christiangeorgelucas/password-hash-tools/axiom"
gen "christiangeorgelucas/password-hash-tools/gen"
"golang.org/x/crypto/argon2"
)
// VerifyArgon2id checks a candidate password against a stored Argon2id PHC
// string, recomputing the hash under the string's own embedded parameters
// and comparing in constant time. A wrong password is a clean non-match
// (matches=false, error empty), never an error.
func VerifyArgon2id(ctx context.Context, ax axiom.Context, input *gen.VerifyInput) (*gen.VerifyResult, error) {
phc := input.GetPhcHash()
if phc == "" {
return &gen.VerifyResult{Error: "EMPTY_INPUT"}, nil
}
fam := detectAlgorithmFamily(phc)
if fam != "argon2id" {
if fam == "" {
return &gen.VerifyResult{Error: "UNKNOWN_HASH_FORMAT"}, nil
}
return &gen.VerifyResult{Algorithm: fam, Error: "WRONG_ALGORITHM"}, nil
}
p, ok := parseArgon2idPHC(phc)
if !ok {
return &gen.VerifyResult{Error: "UNKNOWN_HASH_FORMAT"}, nil
}
computed := argon2.IDKey([]byte(input.GetPassword()), p.salt, p.iterations, p.memKiB, uint8(p.parallelism), uint32(len(p.hash)))
matches := subtle.ConstantTimeCompare(computed, p.hash) == 1
return &gen.VerifyResult{Matches: matches, Algorithm: "argon2id"}, nil
}
The VerifyBcrypt node checks a candidate password against a stored bcrypt hash string using bcrypt's constant-time comparison method. It returns whether the password matches the hash, along with any relevant error messages for incorrect formats or algorithms.
View source
package nodes
import (
"context"
"errors"
"christiangeorgelucas/password-hash-tools/axiom"
gen "christiangeorgelucas/password-hash-tools/gen"
"golang.org/x/crypto/bcrypt"
)
// VerifyBcrypt checks a candidate password against a stored bcrypt hash
// string ($2a$/$2b$/$2y$). bcrypt.CompareHashAndPassword performs the
// comparison in constant time internally. A wrong password is a clean
// non-match (matches=false, error empty), never an error.
func VerifyBcrypt(ctx context.Context, ax axiom.Context, input *gen.VerifyInput) (*gen.VerifyResult, error) {
phc := input.GetPhcHash()
if phc == "" {
return &gen.VerifyResult{Error: "EMPTY_INPUT"}, nil
}
fam := detectAlgorithmFamily(phc)
if fam != "bcrypt" {
if fam == "" {
return &gen.VerifyResult{Error: "UNKNOWN_HASH_FORMAT"}, nil
}
return &gen.VerifyResult{Algorithm: fam, Error: "WRONG_ALGORITHM"}, nil
}
err := bcrypt.CompareHashAndPassword([]byte(phc), []byte(input.GetPassword()))
switch {
case err == nil:
return &gen.VerifyResult{Matches: true, Algorithm: "bcrypt"}, nil
case errors.Is(err, bcrypt.ErrMismatchedHashAndPassword):
return &gen.VerifyResult{Matches: false, Algorithm: "bcrypt"}, nil
default:
return &gen.VerifyResult{Algorithm: "bcrypt", Error: "UNKNOWN_HASH_FORMAT"}, nil
}
}
The VerifyPbkdf2 node checks a candidate password against a stored PBKDF2-HMAC hash string, recomputing the hash with the parameters embedded in the hash string and performing a constant-time comparison. It returns a result indicating whether the password matches, along with any errors related to the hash format or algorithm used.
View source
package nodes
import (
"context"
"crypto/subtle"
"christiangeorgelucas/password-hash-tools/axiom"
gen "christiangeorgelucas/password-hash-tools/gen"
"golang.org/x/crypto/pbkdf2"
)
// VerifyPbkdf2 checks a candidate password against a stored PBKDF2-HMAC hash
// string, recomputing the hash under the string's own embedded parameters
// and comparing in constant time. A wrong password is a clean non-match
// (matches=false, error empty), never an error.
func VerifyPbkdf2(ctx context.Context, ax axiom.Context, input *gen.VerifyInput) (*gen.VerifyResult, error) {
phc := input.GetPhcHash()
if phc == "" {
return &gen.VerifyResult{Error: "EMPTY_INPUT"}, nil
}
fam := detectAlgorithmFamily(phc)
if fam != "pbkdf2" {
if fam == "" {
return &gen.VerifyResult{Error: "UNKNOWN_HASH_FORMAT"}, nil
}
return &gen.VerifyResult{Algorithm: fam, Error: "WRONG_ALGORITHM"}, nil
}
p, ok := parsePbkdf2PHC(phc)
if !ok {
return &gen.VerifyResult{Error: "UNKNOWN_HASH_FORMAT"}, nil
}
hf, _, ok := pbkdf2HashFunc(p.hashName)
if !ok {
return &gen.VerifyResult{Algorithm: "pbkdf2", Error: "UNKNOWN_HASH_FORMAT"}, nil
}
computed := pbkdf2.Key([]byte(input.GetPassword()), p.salt, int(p.iterations), len(p.hash), hf)
matches := subtle.ConstantTimeCompare(computed, p.hash) == 1
return &gen.VerifyResult{Matches: matches, Algorithm: "pbkdf2-" + p.hashName}, nil
}
The VerifyScrypt node checks a candidate password against a stored scrypt hash string, recomputing the hash with the embedded parameters and ensuring a constant time comparison to prevent timing attacks. It returns a result indicating whether the password matches the hash, along with any relevant error messages for incorrect formats or algorithms.
View source
package nodes
import (
"context"
"crypto/subtle"
"christiangeorgelucas/password-hash-tools/axiom"
gen "christiangeorgelucas/password-hash-tools/gen"
"golang.org/x/crypto/scrypt"
)
// VerifyScrypt checks a candidate password against a stored scrypt hash
// string, recomputing the hash under the string's own embedded parameters
// and comparing in constant time. A wrong password is a clean non-match
// (matches=false, error empty), never an error.
func VerifyScrypt(ctx context.Context, ax axiom.Context, input *gen.VerifyInput) (*gen.VerifyResult, error) {
phc := input.GetPhcHash()
if phc == "" {
return &gen.VerifyResult{Error: "EMPTY_INPUT"}, nil
}
fam := detectAlgorithmFamily(phc)
if fam != "scrypt" {
if fam == "" {
return &gen.VerifyResult{Error: "UNKNOWN_HASH_FORMAT"}, nil
}
return &gen.VerifyResult{Algorithm: fam, Error: "WRONG_ALGORITHM"}, nil
}
p, ok := parseScryptPHC(phc)
if !ok {
return &gen.VerifyResult{Error: "UNKNOWN_HASH_FORMAT"}, nil
}
computed, err := scrypt.Key([]byte(input.GetPassword()), p.salt, int(p.n), int(p.r), int(p.p), len(p.hash))
if err != nil {
return &gen.VerifyResult{Algorithm: "scrypt", Error: "UNKNOWN_HASH_FORMAT"}, nil
}
matches := subtle.ConstantTimeCompare(computed, p.hash) == 1
return &gen.VerifyResult{Matches: matches, Algorithm: "scrypt"}, nil
}
Messages (11)
Download .protoThe plaintext password that needs to be hashed.
A caller-supplied salt used in the hashing process, which must be at least 8 bytes long.
The encoding format of the salt, which can be base64, base64url, hex, or utf8.
The memory cost in KiB for the hashing process, defaulting to 64 MiB if set to 0.
The number of iterations for the hashing process, defaulting to 3 if set to 0.
The number of parallel threads to use during hashing, defaulting to 4 if set to 0, with a maximum of 255.
The desired length of the derived key in bytes, defaulting to 32 bytes if set to 0.
The plaintext password to be hashed, which is limited to the first 72 bytes.
The cost factor for the bcrypt algorithm, which ranges from 4 to 31, defaulting to 10 when set to 0.
The plaintext password to be hashed.
A non-empty salt value used in the hashing process.
The encoding format of the salt (e.g., base64).
The number of iterations for the PBKDF2 algorithm, defaulting to 600000 if set to 0.
The underlying HMAC hash algorithm to use (sha1, sha256, sha384, sha512), defaulting to sha256 if not recognized.
The desired length of the output key in bytes, defaulting to 32 if set to 0.
The plaintext password that needs to be hashed.
A non-empty salt value used in the hashing process, which must be decoded according to the specified salt encoding.
The encoding format for the salt, which determines how the salt is interpreted.
The block size parameter that affects memory usage; defaults to 8 if set to 0.
The parallelization factor that determines the number of parallel computations; defaults to 1 if set to 0.
The stored hash string that needs to be evaluated.
A boolean indicating whether the stored hash needs to be rehashed based on the comparison of parameters.
The algorithm used for the stored hash, which is identified during the evaluation.
An error message indicating if the hash format is unrecognized or unparseable.
The hashing algorithm used to generate the hash (e.g., argon2id, bcrypt, scrypt, pbkdf2).
The candidate password that needs to be verified against the stored hash.
Indicates whether the candidate password matches the stored hash.
The algorithm used for hashing, which should be 'bcrypt' for this node.
Any error message that indicates issues with the input or hashing process.
Use as a tool in any AI agent
christiangeorgelucas/password-hash-tools is callable as a tool from any MCP client (Claude, Cursor, or your own agent) via Axiom's hosted MCP server — no install, no download. Add the server, then your agent can search and invoke it, or pin it as a typed tool.
claude mcp add --transport http axiom https://api.axiomide.com/mcp --header "Authorization: Bearer YOUR_AXIOM_API_KEY"Replace YOUR_AXIOM_API_KEY with a key you create in Console → API Keys.
MCP server setup & other clients →Use this package
Sign in to import christiangeorgelucas/password-hash-tools into the graph editor — drop it into a flow and hit run. Or call it directly via the API Reference with your API key.
Sign in to useOpen editor