Skip to content

Commit

Permalink
feat: md5 plain digest support (#39)
Browse files Browse the repository at this point in the history
This change adds support for verification of plain md5 digests without salt.
  • Loading branch information
muhlemmer authored Jun 21, 2024
1 parent 771a282 commit 3e22d3e
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 18 deletions.
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)
}
})
}
}

0 comments on commit 3e22d3e

Please sign in to comment.