Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ hashing algorithms to Go and to provide a simple and consistent interface to
each of them. As every hashing method is implemented in pure Go, this library
should be as portable as Go itself.

Supported Algorithms:
- Traditional DES (13-character standard UNIX crypt(3))
- MD5-crypt ($1$)
- Apache APR1 ($apr1$)
- SHA-256-crypt ($5$)
- SHA-512-crypt ($6$)

All hashing methods come with a test suite which verifies their operation
against itself as well as the output of other password hashing implementations
to ensure compatibility with them.
Expand All @@ -20,6 +27,37 @@ I hope you find this library to be useful and easy to use!
go get github.com/tredoe/crypt@latest


## Usage

```go
package main

import (
"fmt"

"github.com/tredoe/crypt"
_ "github.com/tredoe/crypt/sha256_crypt"
_ "github.com/tredoe/crypt/des_crypt"
)

func main() {
// SHA-256
c := crypt.SHA256.New()
hash, _ := c.Generate([]byte("secret"), []byte("$5$salt"))
fmt.Println(hash)

// Traditional DES (legacy verification)
des := crypt.DES.New()
desHash, _ := des.Generate([]byte("foob"), []byte("ar"))
fmt.Println(desHash) // arlEKn0OzVJn.
}
```

## Security Note

Traditional DES-based crypt(3) is cryptographically obsolete and provided exclusively
for backwards compatibility, legacy password verification, and data migration.

## Documentation

The documentation is available on
Expand Down
30 changes: 25 additions & 5 deletions crypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,25 +50,35 @@ const (
MD5 // import github.com/tredoe/crypt/md5_crypt
SHA256 // import github.com/tredoe/crypt/sha256_crypt
SHA512 // import github.com/tredoe/crypt/sha512_crypt
DES // import github.com/tredoe/crypt/des_crypt
maxCrypt
)

var (
crypts = make([]func() Crypter, maxCrypt)
cryptPrefixes = make([]string, maxCrypt)
crypts = make([]func() Crypter, maxCrypt)
cryptPrefixes = make([]string, maxCrypt)
fallbackValidators = make([]func(string) bool, maxCrypt)
)

// * * *

// New returns a new crypter.
func New(c Crypt) Crypter { return c.New() }

// NewFromHash returns a new Crypter using the prefix in the given hashed key.
// NewFromHash returns a new Crypter using the prefix in the given hashed key, or matching fallback validator.
func NewFromHash(hashedKey string) (Crypter, error) {
for i := range cryptPrefixes {
prefix := cryptPrefixes[i]

if crypts[i] != nil && strings.HasPrefix(hashedKey, prefix) {
if prefix != "" && crypts[i] != nil && strings.HasPrefix(hashedKey, prefix) {
c := Crypt(uint(i))
return c.New(), nil
}
}

for i := range fallbackValidators {
v := fallbackValidators[i]
if v != nil && crypts[i] != nil && v(hashedKey) {
c := Crypt(uint(i))
return c.New(), nil
}
Expand All @@ -81,7 +91,7 @@ func NewFromHash(hashedKey string) (Crypter, error) {

nDollar := strings.Count(hashedKey, "$")

if hashedKey[0] != '$' || nDollar < 3 || nDollar > 4 {
if len(hashedKey) == 0 || hashedKey[0] != '$' || nDollar < 3 || nDollar > 4 {
return nil, ErrUnknown
}
return nil, UnknownError(hashedKey)
Expand Down Expand Up @@ -114,3 +124,13 @@ func RegisterCrypt(c Crypt, f func() Crypter, prefix string) {
crypts[c] = f
cryptPrefixes[c] = prefix
}

// RegisterFallback registers a fallback validator function for prefixless
// crypt functions (such as traditional DES crypt).
func RegisterFallback(c Crypt, f func() Crypter, validator func(string) bool) {
if c >= maxCrypt {
panic(ErrUnknown)
}
crypts[c] = f
fallbackValidators[c] = validator
}
29 changes: 29 additions & 0 deletions crypt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/tredoe/crypt"
_ "github.com/tredoe/crypt/apr1_crypt"
_ "github.com/tredoe/crypt/des_crypt"
)

func TestSupport(t *testing.T) {
Expand All @@ -25,4 +26,32 @@ func TestSupport(t *testing.T) {
if !strings.HasSuffix(err.Error(), "$unknown$") {
t.Error("expect that error got the crypt magic identifier")
}

// Traditional DES hash
c, err := crypt.NewFromHash("arlEKn0OzVJn.")
if err != nil {
t.Errorf("expect support for DES hash: %v", err)
}
if err := c.Verify("arlEKn0OzVJn.", []byte("foob")); err != nil {
t.Errorf("verify failed for DES hash: %v", err)
}

// Invalid DES hash (wrong trailing bits)
if _, err := crypt.NewFromHash("arlEKn0OzVJn/"); err == nil {
t.Errorf("expected error for invalid DES hash")
}
}

func TestNewDES(t *testing.T) {
if !crypt.DES.Available() {
t.Fatalf("crypt.DES should be available")
}
c := crypt.DES.New()
hash, err := c.Generate([]byte("foob"), []byte("ar"))
if err != nil {
t.Fatalf("Generate returned error: %v", err)
}
if hash != "arlEKn0OzVJn." {
t.Errorf("expected arlEKn0OzVJn., got %q", hash)
}
}
123 changes: 123 additions & 0 deletions des_crypt/des_crypt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// SPDX-FileCopyrightText: 2019-2026 Francois Pesce
// SPDX-License-Identifier: BSD-2-Clause

// Package des_crypt implements the traditional Unix DES crypt(3) password hashing algorithm.
package des_crypt

import (
"crypto/rand"
"crypto/subtle"
"errors"

"github.com/tredoe/crypt"
"github.com/tredoe/crypt/common"
)

func init() {
crypt.RegisterCrypt(crypt.DES, New, "")
crypt.RegisterFallback(crypt.DES, New, Validate)
}

const (
SaltLenMin = 2
SaltLenMax = 2
RoundsDefault = 25
RoundsMin = 25
RoundsMax = 25
HashLen = 13
)

var (
ErrSaltLength = errors.New("des_crypt: salt must be at least 2 bytes")
)

type crypter struct {
Salt common.Salt
}

// New returns a new crypt.Crypter computing the traditional DES crypt(3) password hashing.
func New() crypt.Crypter {
return &crypter{
Salt: common.Salt{
MagicPrefix: []byte(""),
SaltLenMin: SaltLenMin,
SaltLenMax: SaltLenMax,
RoundsDefault: RoundsDefault,
RoundsMin: RoundsMin,
RoundsMax: RoundsMax,
},
}
}

// Validate checks if a string is a valid 13-character DES crypt(3) hash.
// It verifies the exact length, characters in the 64-symbol alphabet,
// and the 2-bit zero-mask on the 13th character.
func Validate(hash string) bool {
if len(hash) != HashLen {
return false
}
for i := 0; i < HashLen; i++ {
b := hash[i]
if !((b >= '.' && b <= '9') || (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z')) {
return false
}
}
if ascii_to_bin[hash[12]]&0x03 != 0 {
return false
}
return true
}

func (c *crypter) Generate(key, salt []byte) (string, error) {
var s [2]byte
if len(salt) == 0 {
var randBytes [2]byte
if _, err := rand.Read(randBytes[:]); err != nil {
return "", err
}
s[0] = ascii64Bytes[randBytes[0]&0x3f]
s[1] = ascii64Bytes[randBytes[1]&0x3f]
} else if len(salt) < 2 {
return "", ErrSaltLength
} else {
for i := 0; i < 2; i++ {
b := salt[i]
if !((b >= '.' && b <= '9') || (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z')) {
return "", common.ErrSaltFormat
}
}
s[0] = salt[0]
s[1] = salt[1]
}

var k [8]byte
keyLen := len(key)
if keyLen > 8 {
keyLen = 8
}
copy(k[:], key[:keyLen])

return DESCrypt(k, s), nil
}

func (c *crypter) Verify(hashedKey string, key []byte) error {
if !Validate(hashedKey) {
return crypt.ErrKeyMismatch
}
newHash, err := c.Generate(key, []byte(hashedKey))
if err != nil {
return err
}
if subtle.ConstantTimeCompare([]byte(newHash), []byte(hashedKey)) != 1 {
return crypt.ErrKeyMismatch
}
return nil
}

func (c *crypter) Cost(hashedKey string) (int, error) {
return RoundsDefault, nil
}

func (c *crypter) SetSalt(salt common.Salt) {
c.Salt = salt
}
Loading