Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: md5 plain digest support #39

Merged
merged 1 commit into from
Jun 21, 2024
Merged
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: 25 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ needs to be updated.

## Features

* Secure salt generation (from `crypto/rand`) for all algorithms included.
* Automatic update of passwords.
* Only [depends](go.mod) on the Go standard library and `golang.org/x/{sys,crypto}`.
* The `Hasher` and `Verifier` interfaces allow the use of custom algorithms and
- Secure salt generation (from `crypto/rand`) for all algorithms included.
- Automatic update of passwords.
- Only [depends](go.mod) 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
Expand All @@ -33,14 +33,16 @@ needs to be updated.
| [argon2][1] | argon2i, argon2id | :heavy_check_mark: |
| [bcrypt][2] | 2, 2a, 2b, 2y | :heavy_check_mark: |
| [md5-crypt][3] | 1 | :x: |
| [scrypt][4] | scrypt, 7 | :heavy_check_mark: |
| [pbkpdf2][5] | pbkdf2, pbkdf2-sha224, pbkdf2-sha256, pbkdf2-sha384, pbkdf2-sha512 | :heavy_check_mark: |
| [md5 plain][4] | Hex encoded string | :x: |
| [scrypt][5] | scrypt, 7 | :heavy_check_mark: |
| [pbkpdf2][6] | pbkdf2, pbkdf2-sha224, pbkdf2-sha256, pbkdf2-sha384, pbkdf2-sha512 | :heavy_check_mark: |

[1]: https://pkg.go.dev/github.com/zitadel/passwap/argon2
[2]: https://pkg.go.dev/github.com/zitadel/passwap/bcrypt
[3]: https://pkg.go.dev/github.com/zitadel/passwap/md5
[4]: https://pkg.go.dev/github.com/zitadel/passwap/scrypt
[5]: https://pkg.go.dev/github.com/zitadel/passwap/pbkdf2
[4]: https://pkg.go.dev/github.com/zitadel/passwap/md5plain
[5]: https://pkg.go.dev/github.com/zitadel/passwap/scrypt
[6]: https://pkg.go.dev/github.com/zitadel/passwap/pbkdf2

### Encoding

Expand All @@ -64,7 +66,6 @@ $argon2i$v=19$m=4096,t=3,p=1$cmFuZG9tc2FsdGlzaGFyZA$YMvo8AUoNtnKYGqeODruCjHdiEbl
(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.
Expand All @@ -90,12 +91,11 @@ $2a$12$aLYFkieuqJyeynvptPTxpehSViui5WeAPuR2Xw1wui9CPHEaacmFq
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.

4. The Base64-encoded Bcrypt hash output of the password and salt combined.

### MD5
### MD5 Crypt

MD5 uses its own encoding scheme, which is part of the [hashing algorithm](https://passlib.readthedocs.io/en/stable/lib/passlib.hash.md5_crypt.html#algorithm). It uses a similar alphabet as Base64 but performs an additional shuffling of bytes.
MD5 Crypt uses its own encoding scheme, which is part of the [hashing algorithm](https://passlib.readthedocs.io/en/stable/lib/passlib.hash.md5_crypt.html#algorithm). It uses a similar alphabet as Base64 but performs an additional shuffling of bytes.
The resulting Modular Crypt Format string looks as follows:

```
Expand All @@ -109,6 +109,18 @@ $1$kJ4QkJaQ$3EbD/pJddrq5HW3mpZ4KZ1

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.

### MD5 Plain

MD5 Plain are hex encoded digests of a single iteration of a password without salt.
For example passwap can verify passwords hashed by the following methods:

- `printf "password" | md5sum` on most linux systems.
- PHP's `md5("password")`
- Python3's `hashlib.md5(b"password").hexdigest()`

MD5 is considered cryptographically broken and insecure. Also hashing without salt is a bad idea.
Therefore passwap only supports verification to allow applications to migrate to better methods.

### Scrypt

Scrypt uses standard raw Base64 encoding (no padding) for the salt and hash.
Expand Down
7 changes: 7 additions & 0 deletions internal/testvalues/hashlib_md5.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env python3

import hashlib

password = b"password"

print("MD5PlainHex = `", hashlib.md5(password).hexdigest(), "`", sep="")
7 changes: 4 additions & 3 deletions internal/testvalues/md5.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import (
)

const (
MD5Encoded = `$1$kJ4QkJaQ$3EbD/pJddrq5HW3mpZ4KZ1`
MD5SaltRaw = "pepper"
MD5Salt = "kJ4QkJaQ"
MD5Encoded = `$1$kJ4QkJaQ$3EbD/pJddrq5HW3mpZ4KZ1`
MD5SaltRaw = "pepper"
MD5Salt = "kJ4QkJaQ"
MD5PlainHex = `5f4dcc3b5aa765d61d8327deb882cf99`
)

var MD5Checksum []byte
Expand Down
8 changes: 6 additions & 2 deletions md5/md5.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
// Package md5 provides hashing and verification or
// md5Crypt encoded passwords.
// Package md5 provides hashing and verification of
// md5Crypt encoded passwords with salt.
// [The algorithm](https://passlib.readthedocs.io/en/stable/lib/passlib.hash.md5_crypt.html#algorithm)
// builds hashes through multiple digest iterations
// with shuffles of password and salt.
//
// Note that md5 is considered cryptographically broken
// and should not be used for new applications.
// This package is only provided for legacy applications
Expand Down
36 changes: 36 additions & 0 deletions md5plain/md5plain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Package md5plain provides verification of
// plain md5 digests of passwords without salt.
//
// Note that md5 is considered cryptographically broken
// and should not be used for new applications.
// This package is only provided for legacy applications
// that wish to migrate away from md5 to newer hashing methods.
package md5plain

import (
"crypto/md5"
"crypto/subtle"
"encoding/hex"
"fmt"

"github.com/zitadel/passwap/verifier"
)

// Verify an plain md5 digest without salt.
// Digest must be hex encoded.
//
// Note that md5 digests do not have an identifier.
// Therefore it might be that Verify accepts any hex encoded string
// but fails password verification.
func Verify(digest, password string) (verifier.Result, error) {
decoded, err := hex.DecodeString(digest)
if err != nil {
return verifier.Skip, fmt.Errorf("md5plain parse: %w", err)
}
sum := md5.Sum([]byte(password))
res := subtle.ConstantTimeCompare(sum[:], decoded)

return verifier.Result(res), nil
}

var Verifier = verifier.VerifyFunc(Verify)
51 changes: 51 additions & 0 deletions md5plain/md5plain_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package md5plain

import (
"reflect"
"testing"

"github.com/zitadel/passwap/internal/testvalues"
"github.com/zitadel/passwap/verifier"
)

func TestVerify(t *testing.T) {
type args struct {
hash string
password string
}
tests := []struct {
name string
args args
want verifier.Result
wantErr bool
}{
{
name: "decode error",
args: args{"!!!", testvalues.Password},
want: verifier.Skip,
wantErr: true,
},
{
name: "wrong password",
args: args{testvalues.MD5PlainHex, "foobar"},
want: verifier.Fail,
},
{
name: "success",
args: args{testvalues.MD5PlainHex, testvalues.Password},
want: verifier.OK,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Verify(tt.args.hash, tt.args.password)
if (err != nil) != tt.wantErr {
t.Errorf("Verify() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Verify() = %v, want %v", got, tt.want)
}
})
}
}
Loading