passwap

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Jan 9, 2024 License: BSD-3-Clause Imports: 4 Imported by: 2

README

Passwap

Go Reference Go codecov Go Report Card

Package Passwap provides a unified implementation between different password hashing algorithms in the Go ecosystem. It allows for easy swapping between algorithms, using the same API for all of them.

Passwords hashed with Passwap, using a certain algorithm and parameters can be stored in a database. If at a later moment parameters or even the algorithm is changed, Passwap is still able to verify the "outdated" hashes and automatically return an updated hash when applicable. Only when an updated hash is returned, the record in the database needs to be updated.

Features

  • Secure salt generation (from crypto/rand) for all algorithms included.
  • Automatic update of passwords.
  • Only depends on the Go standard library and golang.org/x/{sys,crypto}.
  • The Hasher and Verifier interfaces allow the use of custom algorithms and encoding schemes.
Algorithms
Algorithm Identifiers Secure
argon2 argon2i, argon2id
bcrypt 2, 2a, 2b, 2y
md5-crypt 1
scrypt scrypt, 7
pbkpdf2 pbkdf2, pbkdf2-sha224, pbkdf2-sha256, pbkdf2-sha384, pbkdf2-sha512
Encoding

There is no unified standard for encoding password hashes. Essentially one would need to store the parameters used, salt and the resulting hash. As the salt and hash are typically raw bytes, they also need to be converted to characters, for example using base64.

All of the Passwap supplied algorithms use the dollar sign ($) delimited encoding, aka Modular Crypt Format. This results in a single string containing all of the above for later password verification.

Argon2

Argon2 uses standard raw Base64 encoding (without padding) for salt and hash. The resulting Modular Crypt Format string looks as follows:

$argon2i$v=19$m=4096,t=3,p=1$cmFuZG9tc2FsdGlzaGFyZA$YMvo8AUoNtnKYGqeODruCjHdiEbl1pKL2MsYy9VgU/E
   (1)              (2)               (3)                            (4)
  1. The identifier, which can be argon2i or argon2id. argon2d, is not supported by Go, and therefore, is not supported by this library either.
  2. Cost parameters.
    1. m for memory -4096 KiB in this example.
    2. t for time - 3 in this example.
    3. p for parallelism (threads) - 1 in this example.
  3. Base64 encoded salt.
  4. Base64 encoded Argon2 hash output of the password and salt combined.

Changing any of the parameters or salt produces a different hash output. More information about the parameters can be found in the upstream Argon2 package documentation.

Bcrypt

Bcrypt uses a custom Base64 encoding with the character set of [./A-Za-z0-9] and padding. The actual formatting is fully implemented by the Go package. The resulting Modular Crypt Format string looks as follows:

$2a$12$aLYFkieuqJyeynvptPTxpehSViui5WeAPuR2Xw1wui9CPHEaacmFq
 (1)(2)          (3)                      (4)
  1. The identifier can be 2a, 2b or, 2y. It indicates the Bcrypt version but is ignored and the same is always produced.
  2. The cost parameter that is exponential - 12 in this example.
  3. The Base64-encoded salt, always 22 character long.
  4. The Base64-encoded Bcrypt hash output of the password and salt combined.
MD5

MD5 uses its own encoding scheme, which is part of the hashing algorithm. It uses a similar alphabet as Base64 but performs an additional shuffling of bytes. The resulting Modular Crypt Format string looks as follows:

$1$kJ4QkJaQ$3EbD/pJddrq5HW3mpZ4KZ1
(1)   (2)           (3)
  1. The identifier is always 1
  2. Base64-like-encoded salt.
  3. Base64-like-encoded MD5 hash output of the password and salt combined.

There is no cost parameter for MD5 because MD5 is old and is considered too light and insecure. It is provided to verify and migrate to a better algorithm. Do not use for new hashes.

Scrypt

Scrypt uses standard raw Base64 encoding (no padding) for the salt and hash. The resulting Modular Crypt Format string looks as follows:

$scrypt$ln=16,r=8,p=1$cmFuZG9tc2FsdGlzaGFyZA$Rh+NnJNo1I6nRwaNqbDm6kmADswD1+7FTKZ7Ln9D8nQ
  (1)        (2)              (3)                              (4)
  1. The identifier is always scrypt.
  2. Cost parameters:
    1. ln is the exponential cost parameter for memory and CPU - 16 in this example.
    2. r is the block size for optimal performance of the CPU architecture - 8 in this example.
    3. p is to indicate parallelism - 1 in this example.
  3. Base64-encoded salt
  4. Base64-encoded Scrypt hash output of the password and salt combined.
PBKDF2

PBKDF2 uses an alternative Base64 encoding, which is based on the standard with + replaced by ., and it comes without padding. As we've also seen standard encoding with padding in the wild, the verifier will accept alternative standards with or without padding. The Hasher always produces alternative encoding.

The resulting Modular Crypt Format string looks as follows:

$pbkdf2-sha256$12$cmFuZG9tc2FsdGlzaGFyZA$OFvEcLOIPFd/oq8egf10i.qJLI7A8nDjPLnolCWarQY
      (1)     (2)         (3)                            (4)
  1. The identifier is made of 2 parts:
    1. pbkdf2 is the identifier prefix for the algorithm.
    2. -sha256 is an optional suffix with dash separator and is the identifier for the hash backend. When omitted, sha1 is used as a default.
  2. The cost parameter in rounds, which is a linear value - 12 in this example.
  3. Alternative Base64-encoded salt
  4. Alternative Base64 encoded Scrypt hash output of the password and salt combined.
Reference

Its origin can be found in Glibc. Passlib for Python is the most complete implementation and there the Modular Crypt Format expands the subject further. Although MCF is superseded by the Password Hashing Competition string format, passlib still provides the most complete documentation on the format and encodings used for each algorithm.

Each algorithm supplied by Passwap is compatible with Passlib's encoding and tested against reference hashes created with Passlib.

Example

First, we want our application to hash passwords using bcrypt, using the default cost. We will create a Swapper for it. When a user would want to store good_password as a password, it is passed into passwords.Hash() and the result is typically stored in a database. In this case, we keep it just in the encoded variable.

passwords := passwap.NewSwapper(
    bcrypt.New(bcrypt.DefaultCost),
)

encoded, err := passwords.Hash("good_password")
if err != nil {
    panic(err)
}
fmt.Println(encoded)
// $2a$10$eS.mS5Zc5YAJFlImXCpLMu9TxXwKUhgQxsbghlvyVwvwYO/17E2qy

At this point encoded has the value of $2a$10$eS.mS5Zc5YAJFlImXCpLMu9TxXwKUhgQxsbghlvyVwvwYO/17E2qy. It is an encoded string containing the bcrypt identifier, cost, salt and hashed password which later can be used for verification.

At a later moment, you can reconfigure your application to use another hashing algorithm. This might be because the former is cryptographically broken, customer demand or just because you can. Next, we will create a new Swapper configured to hash using the argon2id algorithm.

We already have users that have created passwords using bcrypt. As hashing is a one-way operation we can't migrate them until they supply the password again. Therefore we must pass the bcrypt.Verifier as well.

Once the user supplies his password again and we need to verify it, passwords.Verify() will return an updated encoded string automatically, because the Swapper figured out that the original encoded was created using a different algorithm.

passwords = passwap.NewSwapper(
    argon2.NewArgon2id(argon2.RecommendedIDParams),
    bcrypt.Verifier,
)
if updated, err := passwords.Verify(encoded, "good_password"); err != nil {
    panic(err)
} else if updated != "" {
    encoded = updated // store in "DB"
}
fmt.Println(encoded)

At this point encoded will look something like $argon2id$v=19$m=65536,t=1,p=4$d6SOdxdIip9BC7sM5H7PUQ$2E7OIz7C1NkMLOsXi5nSe5vfbthdc9N9SWVlArd200E.

If we would call passwords.Verify() again, updated returns empty. That's because encoded was created using the same algorithm and parameters.

if updated, err := passwords.Verify(encoded, "good_password"); err != nil {
    panic(err)
} else if updated != "" { // updated is empty, nothing is stored
    encoded = updated
}
fmt.Println(encoded)
// $argon2id$v=19$m=65536,t=1,p=4$d6SOdxdIip9BC7sM5H7PUQ$2E7OIz7C1NkMLOsXi5nSe5vfbthdc9N9SWVlArd200E

Now let's say that we upgraded our hardware with more powerful CPUs. We should now also increase the time parameter accordingly, so that the security of our hashes grows with the increased performance available on the market.

In this case, we do not need to supply a separate argon2.Verifier, as the returned Hasher from NewArgon2id() should already implement the Verifier interface for its algorithm. We do keep the bcrypt.Verifier around, because we might still have users that didn't use their password since the last update.

passwords = passwap.NewSwapper(
    argon2.NewArgon2id(argon2.Params{
        Time:    2,
        Memory:  64 * 1024,
        Threads: 4,
        KeyLen:  32,
        SaltLen: 16,
    }),
    bcrypt.Verifier,
)
if updated, err := passwords.Verify(encoded, "good_password"); err != nil {
    panic(err)
} else if updated != "" {
    encoded = updated
}

At this point encoded would be updated again and look like $argon2id$v=19$m=65536,t=2,p=4$44X+dwU+aSS85Kl1qH3/Jg$n/tQoAtx/I/Rt9BXHH9tScshWucltPPmB0HBLVtXCq0 You'll see that the t=2 parameter is updated as well as the resulting salt and hash. A new salt is always obtained during hashing.

The full example is also part of the Go documentation.

Supported Go Versions

For security reasons, we only support and recommend the use of one of the latest two Go versions (✅).
Versions that also build are marked with ⚠.

Version Supported
<1.18
1.18
1.19
1.20
1.21

License

The full functionality of this library is and stays open source and free to use for everyone. Visit our website and get in touch.

See the exact licensing terms here

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Documentation

Overview

Package passwap provides a unified implementation between different password hashing algorithms in the Go ecosystem. It allows for easy swapping between algorithms, using the same API for all of them.

Passwords hashed with passwap, using a certain algorithm and parameters can be stored in a database. If at a later moment paramers or even the algorithm is changed, passwap is still able to verify the "outdated" hashes and automatically return an updated hash when applicable. Only when an updated hash is returned, the record in the database needs to be updated.

Example
package main

import (
	"fmt"

	"github.com/zitadel/passwap"
	"github.com/zitadel/passwap/argon2"
	"github.com/zitadel/passwap/bcrypt"
)

func main() {
	// Create a new swapper which hashes using bcrypt.
	passwords := passwap.NewSwapper(
		bcrypt.New(bcrypt.DefaultCost),
	)

	// Create an encoded bcrypt hash string of password with salt.
	encoded, err := passwords.Hash("good_password")
	if err != nil {
		panic(err)
	}
	fmt.Println(encoded)
	// $2a$10$eS.mS5Zc5YAJFlImXCpLMu9TxXwKUhgQxsbghlvyVwvwYO/17E2qy

	// Replace the swapper to hash using argon2id,
	// verifies and upgrades bcrypt.
	passwords = passwap.NewSwapper(
		argon2.NewArgon2id(argon2.RecommendedIDParams),
		bcrypt.Verifier,
	)

	// Verify encoded bcrypt string with a good password.
	// Returns a new encoded string with argon2id hash
	// of password and new random salt.
	if updated, err := passwords.Verify(encoded, "good_password"); err != nil {
		panic(err)
	} else if updated != "" {
		encoded = updated // store in "DB"
	}
	fmt.Println(encoded)
	// $argon2id$v=19$m=65536,t=1,p=4$d6SOdxdIip9BC7sM5H7PUQ$2E7OIz7C1NkMLOsXi5nSe5vfbthdc9N9SWVlArd200E
	// encoded is updated.

	// Verify encoded argon2 string with a good password.
	// "updated" now is empty because the parameters of the Hasher
	// match the one in the encoded string.
	if updated, err := passwords.Verify(encoded, "good_password"); err != nil {
		panic(err)
	} else if updated != "" { // updated is empty, nothing is stored
		encoded = updated
	}
	fmt.Println(encoded)
	// $argon2id$v=19$m=65536,t=1,p=4$d6SOdxdIip9BC7sM5H7PUQ$2E7OIz7C1NkMLOsXi5nSe5vfbthdc9N9SWVlArd200E
	// encoded in unchanged.

	// Replace the swapper again. This time we still
	// use argon2id, but increased the Time parameter.
	passwords = passwap.NewSwapper(
		argon2.NewArgon2id(argon2.Params{
			Time:    2,
			Memory:  64 * 1024,
			Threads: 4,
			KeyLen:  32,
			SaltLen: 16,
		}),
		bcrypt.Verifier,
	)

	// Verify encoded argon2id string with a good password.
	// Returns a new encoded string with argon2id hash
	// of password and new random salt,
	// because of paremeter mis-match.
	if updated, err := passwords.Verify(encoded, "good_password"); err != nil {
		panic(err)
	} else if updated != "" {
		encoded = updated
	}
	fmt.Println(encoded)
	// $argon2id$v=19$m=65536,t=2,p=4$44X+dwU+aSS85Kl1qH3/Jg$n/tQoAtx/I/Rt9BXHH9tScshWucltPPmB0HBLVtXCq0
	// encoded is updated.
}
Output:

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrPasswordMismatch = errors.New("passwap: password does not match hash")
	ErrPasswordNoChange = errors.New("passwap: new password same as old password")
	ErrNoVerifier       = errors.New("passwap: no verifier found for encoded string")
)

Functions

This section is empty.

Types

type Hasher

type Hasher interface {
	verifier.Verifier
	Hash(password string) (encoded string, err error)
}

Hasher is capable of creating new hashes of passwords, and verify passwords against existing hashes created by itself.

type SkipErrors

type SkipErrors []error

SkipErrors is only returned when multiple Verifiers matched an encoding string, but encountered an error decoding it.

func (SkipErrors) Error

func (e SkipErrors) Error() string

type Swapper

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

Swapper is capable of creating new hashes of passwords and verify passwords against existing hashes for which it has verifiers configured. Swapper also updates hashes that are not created by the main hasher or use outdated cost parameters.

func NewSwapper

func NewSwapper(h Hasher, verifiers ...verifier.Verifier) *Swapper

NewSwapper with Hasher used for creating new hashes and primary verifier. Suplemental verifiers can be provided and will be used as fallback.

func (*Swapper) Hash

func (s *Swapper) Hash(password string) (encoded string, err error)

Hash returns a new encoded password hash using the configured Hasher.

func (*Swapper) Verify

func (s *Swapper) Verify(encoded, password string) (updated string, err error)

Verify a password against an existing encoded hash, using the configured Hasher or one of the Verifiers.

ErrNoVerifier is returned if no matching Verifier is found for the encoded string. ErrPasswordMismatch when the password hash doesn't match the encoded hash. When multiple Verifiers match and encounter an error during decoding, a SkipErrors is returned containing all those errors is returned.

If the used Verifier is different from the the current Hasher or the cost parameters differ, an updated encoded hash string is returned for the same (valid) password. In all other cases updated remains empty. When updated is not empty, it must be stored until next use.

func (*Swapper) VerifyAndUpdate added in v0.2.0

func (s *Swapper) VerifyAndUpdate(encoded, oldPassword, newPassword string) (updated string, err error)

VerifyAndUpdate operates like [Verify], only it always returns a new encoded hash of newPassword, if oldPassword passes verification. An error is returned of newPassword equals oldPassword.

Directories

Path Synopsis
Package argon2 provides salt generation, hashing and verification for x/crypto/argon2.
Package argon2 provides salt generation, hashing and verification for x/crypto/argon2.
internal
encoding
Package encoding provides custom encoding schemes which differ from the standard base64 provided in the stdlib.
Package encoding provides custom encoding schemes which differ from the standard base64 provided in the stdlib.
salt
Package salt provides utilities for generating salts.
Package salt provides utilities for generating salts.
Package md5 provides hashing and verification or md5Crypt encoded passwords.
Package md5 provides hashing and verification or md5Crypt encoded passwords.
Package pbkdf2 provides salt generation, hashing and verification for x/crypto/pbkdf2.
Package pbkdf2 provides salt generation, hashing and verification for x/crypto/pbkdf2.
Package scrypt provides salt generation, hashing and verification for x/crypto/scrypt.
Package scrypt provides salt generation, hashing and verification for x/crypto/scrypt.
Package verifier provides types and interfaces for building verifiers, used by passwap.
Package verifier provides types and interfaces for building verifiers, used by passwap.

Jump to

Keyboard shortcuts

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