-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
x/crypto/crypt/md5crypt: MD5 APR1 implementation of crypt(3).
This package implements Apache-specific MD5 APR1 password hashing algorithm, as documented at https://httpd.apache.org/docs/2.4/en/misc/password_encryptions.html. Same hashing is also used by various Linux/BSD crypt(3) implementations with slightly different salt prefix. This is also supported by this package.
- Loading branch information
Showing
2 changed files
with
265 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
// Copyright 2021 The Go Authors. All rights reserved. | ||
// Use of this source code is governed by a BSD-style | ||
// license that can be found in the LICENSE file. | ||
|
||
// Package md5crypt implements Apache-specific MD5 apr1 password hashing | ||
// algorithm. Details and reference implementation are available at | ||
// https://httpd.apache.org/docs/2.4/en/misc/password_encryptions.html | ||
package md5crypt // import "golang.org/x/crypto/crypt/md5crypt" | ||
|
||
import ( | ||
"crypto/md5" | ||
"crypto/subtle" | ||
"errors" | ||
) | ||
|
||
var ( | ||
// APR1Magic salt prefix is used by the Apache for MD5 encryption. | ||
APR1Magic = []byte("$apr1$") | ||
// MD5Magic salt prefix is used by various Linux/BSD crypt implementations. | ||
MD5Magic = []byte("$1$") | ||
|
||
// ErrUnsupportedSalt is returned when provided salt or hashed password | ||
// doesn't have APR1Magic or MD5Magic prefix. | ||
ErrUnsupportedSalt = errors.New("crypto/crypt/md5crypt: unsupported salt, must have $apr1$ or $1$ prefix") | ||
|
||
// ErrMismatchedHashAndPassword is returned from CompareHashAndPassword when | ||
// a password and hash do not match. | ||
ErrMismatchedHashAndPassword = errors.New("crypto/crypt/md5crypt: hashedPassword is not the hash of the given password") | ||
) | ||
|
||
const itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" | ||
|
||
var md5CryptSwaps = [16]int{12, 6, 0, 13, 7, 1, 14, 8, 2, 15, 9, 3, 5, 10, 4, 11} | ||
|
||
// GenerateFromPassword returns the MD5 APR1 hash of the password. Salt must | ||
// start with either APR1Magic or MD5Magic prefix, followed by up to 8 bytes of | ||
// salt prefix and optionally terminate with a '$'. Any remaining salt bytes | ||
// will be ignored. | ||
func GenerateFromPassword(password, salt []byte) ([]byte, error) { | ||
magic, trueSalt, err := decodeSalt(salt) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
d := md5.New() | ||
d.Write(password) | ||
d.Write(magic) | ||
d.Write(trueSalt) | ||
|
||
d2 := md5.New() | ||
d2.Write(password) | ||
d2.Write(trueSalt) | ||
d2.Write(password) | ||
|
||
for i, mixin := 0, d2.Sum(nil); i < len(password); i++ { | ||
d.Write([]byte{mixin[i%16]}) | ||
} | ||
|
||
for i := len(password); i != 0; i >>= 1 { | ||
if i&1 == 0 { | ||
d.Write([]byte{password[0]}) | ||
} else { | ||
d.Write([]byte{0}) | ||
} | ||
} | ||
|
||
final := d.Sum(nil) | ||
|
||
for i := 0; i < 1000; i++ { | ||
d2 := md5.New() | ||
if i&1 == 0 { | ||
d2.Write(final) | ||
} else { | ||
d2.Write(password) | ||
} | ||
|
||
if i%3 != 0 { | ||
d2.Write(trueSalt) | ||
} | ||
|
||
if i%7 != 0 { | ||
d2.Write(password) | ||
} | ||
|
||
if i&1 == 0 { | ||
d2.Write(password) | ||
} else { | ||
d2.Write(final) | ||
} | ||
final = d2.Sum(nil) | ||
} | ||
|
||
saltPrefixLength := len(magic) + len(trueSalt) | ||
result := make([]byte, saltPrefixLength, saltPrefixLength+23) | ||
copy(result, salt) | ||
result = append(result, '$') | ||
var v, bits uint | ||
for _, i := range md5CryptSwaps { | ||
v |= (uint(final[i]) << bits) | ||
for bits = bits + 8; bits > 6; bits -= 6 { | ||
result = append(result, itoa64[v&0x3f]) | ||
v >>= 6 | ||
} | ||
} | ||
result = append(result, itoa64[v&0x3f]) | ||
return result, nil | ||
} | ||
|
||
// CompareHashAndPassword compares a hashed password with its possible plaintext | ||
// equivalent. Returns nil on success, or an error on failure. | ||
func CompareHashAndPassword(hashedPassword, password []byte) error { | ||
hashed, err := GenerateFromPassword(password, hashedPassword) | ||
if err != nil { | ||
return err | ||
} | ||
if subtle.ConstantTimeCompare(hashed, hashedPassword) == 1 { | ||
return nil | ||
} | ||
return ErrMismatchedHashAndPassword | ||
} | ||
|
||
// decodeSalt splits the salt into magic and "true" salt. Returned values are | ||
// subslices of the input slice. | ||
func decodeSalt(salt []byte) (magic []byte, trueSalt []byte, err error) { | ||
maybeAPR1 := salt[:len(APR1Magic)] | ||
maybeMD5 := salt[:len(MD5Magic)] | ||
a := subtle.ConstantTimeCompare(maybeAPR1, APR1Magic) | ||
b := subtle.ConstantTimeCompare(maybeMD5, MD5Magic) | ||
if a+b == 0 { | ||
return nil, nil, ErrUnsupportedSalt | ||
} | ||
if a == 1 { | ||
magic = maybeAPR1 | ||
salt = salt[len(APR1Magic):] | ||
} else { | ||
magic = maybeMD5 | ||
salt = salt[len(MD5Magic):] | ||
} | ||
|
||
if len(salt) == 0 { | ||
return magic, salt, nil | ||
} | ||
slen := len(salt) | ||
if slen > 8 { | ||
slen = 8 | ||
} | ||
for i := 0; i < slen; i++ { | ||
if salt[i] == '$' { | ||
return magic, salt[:i], nil | ||
} | ||
} | ||
return magic, salt[:slen], nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
package md5crypt | ||
|
||
import ( | ||
"bytes" | ||
"testing" | ||
) | ||
|
||
func TestDecodeSupportedSalt(t *testing.T) { | ||
t.Parallel() | ||
for _, tt := range []struct { | ||
salt, want string | ||
wantMagic []byte | ||
}{ | ||
{"$1$", "", MD5Magic}, | ||
{"$1$a", "a", MD5Magic}, | ||
{"$1$a$", "a", MD5Magic}, | ||
{"$1$ab", "ab", MD5Magic}, | ||
{"$1$ab$", "ab", MD5Magic}, | ||
{"$1$abcdefgh", "abcdefgh", MD5Magic}, | ||
{"$1$abcdefgh$", "abcdefgh", MD5Magic}, | ||
{"$1$abcdefghi", "abcdefgh", MD5Magic}, | ||
{"$1$abcdefghi$", "abcdefgh", MD5Magic}, | ||
{"$apr1$", "", APR1Magic}, | ||
{"$apr1$a", "a", APR1Magic}, | ||
{"$apr1$a$", "a", APR1Magic}, | ||
{"$apr1$ab", "ab", APR1Magic}, | ||
{"$apr1$ab$", "ab", APR1Magic}, | ||
{"$apr1$abcdefgh", "abcdefgh", APR1Magic}, | ||
{"$apr1$abcdefgh$", "abcdefgh", APR1Magic}, | ||
{"$apr1$abcdefghi", "abcdefgh", APR1Magic}, | ||
{"$apr1$abcdefghi$", "abcdefgh", APR1Magic}, | ||
} { | ||
magic, got, err := decodeSalt([]byte(tt.salt)) | ||
if err != nil { | ||
t.Errorf("Error decoding salt %q: %v.", tt.salt, err) | ||
} | ||
if !bytes.Equal(magic, tt.wantMagic) { | ||
t.Errorf("Decoded magic is %q, want %q.", magic, tt.wantMagic) | ||
} | ||
if string(got) != tt.want { | ||
t.Errorf("Decoded salt %q: got %q, want %q.", tt.salt, got, tt.want) | ||
} | ||
|
||
} | ||
} | ||
|
||
func TestDecodeUnsupportedSalt(t *testing.T) { | ||
_, _, err := decodeSalt([]byte("$2$whatever$")) | ||
if err != ErrUnsupportedSalt { | ||
t.Errorf("Decoding unsupported salt returned error %v, want %v.", err, ErrUnsupportedSalt) | ||
} | ||
} | ||
|
||
func TestGenerateFromPassword(t *testing.T) { | ||
t.Parallel() | ||
for _, tt := range []struct { | ||
password, salt, want string | ||
}{ | ||
// test vectors generated using htpasswd(1) and crypt(3) on a Linux system. | ||
{"apache", "$apr1$uvV3T7fu", "$apr1$uvV3T7fu$gvDOBExDieXrhdDxL8.hb."}, | ||
{"apache", "$apr1$uvV3T7fu$", "$apr1$uvV3T7fu$gvDOBExDieXrhdDxL8.hb."}, | ||
{"apache", "$apr1$uvV3T7fu$gvDOBExDieXrhdDxL8.hb.", "$apr1$uvV3T7fu$gvDOBExDieXrhdDxL8.hb."}, | ||
{"topsecret", "$apr1$iKNcB2Be$", "$apr1$iKNcB2Be$.IZPKdGtT8wV99cJ2cmm21"}, | ||
{"topsecret", "$1$", "$1$$s/sSkcXFvhLMpizXR5c7/0"}, | ||
{"topsecret", "$1$$", "$1$$s/sSkcXFvhLMpizXR5c7/0"}, | ||
} { | ||
got, err := GenerateFromPassword([]byte(tt.password), []byte(tt.salt)) | ||
if err != nil { | ||
t.Errorf("GenerateFromPassword(%q, %q) returned error %v.", tt.password, tt.salt, err) | ||
} | ||
if string(got) != tt.want { | ||
t.Errorf("GenerateFromPassword(%q, %q): got %q, want %q.", tt.password, tt.salt, got, tt.want) | ||
} | ||
if cap(got) != len(tt.want) { | ||
t.Errorf("Returned slice preallocated more memory than required: got %d, want %d.", cap(got), len(tt.want)) | ||
} | ||
} | ||
} | ||
|
||
func TestGenerateFromPasswordUnsupported(t *testing.T) { | ||
t.Parallel() | ||
_, err := GenerateFromPassword([]byte("topsecret"), []byte("$2$whatever$")) | ||
if err != ErrUnsupportedSalt { | ||
t.Errorf("GenerateFromPassword with unsupported salt returned error %v, want %v.", err, ErrUnsupportedSalt) | ||
} | ||
} | ||
|
||
func TestCompareHashAndPassword(t *testing.T) { | ||
t.Parallel() | ||
for _, tt := range []struct { | ||
password, hashedPassword string | ||
}{ | ||
{"apache", "$apr1$uvV3T7fu$gvDOBExDieXrhdDxL8.hb."}, | ||
{"topsecret", "$apr1$iKNcB2Be$.IZPKdGtT8wV99cJ2cmm21"}, | ||
{"topsecret", "$1$$s/sSkcXFvhLMpizXR5c7/0"}, | ||
{"topsecret", "$1$ALwsXB9w$B/FdgWMtcav/q8kuxQ/BK1"}, | ||
} { | ||
if err := CompareHashAndPassword([]byte(tt.hashedPassword), []byte(tt.password)); err != nil { | ||
t.Errorf("CompareHashAndPassword(%q, %q) returned error %q, want nil.", tt.hashedPassword, tt.password, err) | ||
} | ||
if err := CompareHashAndPassword([]byte(tt.hashedPassword), []byte(tt.password+"x")); err == nil { | ||
t.Errorf("CompareHashAndPassword(%q, %q) returned no error.", tt.hashedPassword, tt.password+"x") | ||
} | ||
} | ||
} | ||
|
||
func TestCompareHashAndPasswordUnsupported(t *testing.T) { | ||
t.Parallel() | ||
if err := CompareHashAndPassword([]byte("topsecret"), []byte("9yH.Z916aam4E")); err != ErrUnsupportedSalt { | ||
t.Errorf("CompareHashAndPassword returned error %v for unsupported salt, want %v.", err, ErrUnsupportedSalt) | ||
} | ||
} |