From 8adf19327f5cb9671964ce3b807ca4b03bc0b3cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 1 Aug 2023 21:18:53 +0300 Subject: [PATCH 1/3] chore: update readme with examples and features --- .github/workflows/go.yml | 2 +- README.md | 174 +++++++++++++++++++++++++++++++++++++-- passwap.go | 14 +--- passwap_example_test.go | 81 ++++++++++++++++++ passwap_test.go | 90 -------------------- 5 files changed, 248 insertions(+), 113 deletions(-) create mode 100644 passwap_example_test.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 01cc9a4..4a035b1 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go: ['1.18', '1.19'] + go: ['1.18', '1.19', '1.20'] steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index 8d0d39e..c018534 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![codecov](https://codecov.io/gh/zitadel/passwap/branch/main/graph/badge.svg?token=GrPT2nbCjj)](https://codecov.io/gh/zitadel/passwap) Package passwap provides a unified implementation between -different password hashing algorithms. +different password hashing algorithms in the Go ecosystem. It allows for easy swapping between algorithms, using the same API for all of them. @@ -17,14 +17,170 @@ automatically return an updated hash when applicable. Only when an updated hash is returned, the record in the database needs to be updated. -Resulting password hashes are encoded using dollar sign ($) -notation. It's origin lies in Glibc, but there is no clear -standard on the matter For passwap it is choosen to follow -suit with python's passlib identifiers to be (hopefully) -as portable as possible. Suplemental information can be found: +## Features -Glibc: ; +* 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. -Passlib "Modular Crypt Format": ; +### Algorithms -Password Hashing Competition string format: ; +| Algorithm | Identifiers | Secure | +|-----------|--------------------------------------------------------------------|--------------------| +| argon2 | argon2i, argon2id | :heavy_check_mark: | +| bcrypt | 2, 2a, 2b, 2y | :heavy_check_mark: | +| md5-crypt | 1 | :x: | +| scrypt | scrypt, 7 | :heavy_check_mark: | +| pbkpdf2 | pbkdf2, pbkdf2-sha224, pbkdf2-sha256, pbkdf2-sha384, pbkdf2-sha512 | :heavy_check_mark: | + +### Encoding + +There is no unified standard for encoding password hashes. Essentialy 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 dollar sign (`$`) delimited +encoding. This results in a single string containing all of the above for +later password verification. + +It's origin can be found in +[Glibc](https://man.archlinux.org/man/crypt.5). Passlib for python is the +most complete implementation and there the +[Modular Crypt Format](https://passlib.readthedocs.io/en/stable/modular_crypt_format.htm) +expands the subject further. Althought MCF is superseeded by +the [Password Hashing Competition string format](https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md), +passlib still provides the most complete documentation on the format and +encodings used for each algorithm. + +Each althorithm 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 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. + +```go +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 untill 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. + +```go +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. + +```go +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 lets say that we upgraded our hardware with more powerfull CPUs. +We should now also increase the `time` parameter accordingly, so that +the security of our hashes grow with the increased performance available +on the market. + +In this case we do not need to supply a seperate `argon2.Verifier`, +as the returned `Hasher` from `NewArgon2id()` should already implement +the `Verifier` interface for its own algorithm. We do keep the `bcrypt.Verifier` +around, because we might still have users that didn't use their password since the +last update. + +```go +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](https://pkg.go.dev/github.com/zitadel/passwap#example-package). + +## Supported Go Versions + +For security reasons, we only support and recommend the use of one of the latest two Go versions (:white_check_mark:). +Versions that also build are marked with :warning:. + +| Version | Supported | +| ------- | ------------------ | +| <1.18 | :x: | +| 1.18 | :warning: | +| 1.19 | :white_check_mark: | +| 1.20 | :white_check_mark: | + +## License + +The full functionality of this library is and stays open source and free to use for everyone. Visit +our [website](https://zitadel.com) and get in touch. + +See the exact licensing terms [here](LICENSE) + +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. diff --git a/passwap.go b/passwap.go index 1b51a1f..015ae56 100644 --- a/passwap.go +++ b/passwap.go @@ -1,6 +1,6 @@ /* Package passwap provides a unified implementation between -different password hashing algorithms. +different password hashing algorithms in the Go ecosystem. It allows for easy swapping between algorithms, using the same API for all of them. @@ -11,18 +11,6 @@ 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. - -Resulting password hashes are encoded using dollar sign ($) -notation. It's origin lies in Glibc, but there is no clear -standard on the matter For passwap it is choosen to follow -suit with python's passlib identifiers to be (hopefully) -as portable as possible. Suplemental information can be found: - -Glibc: https://man.archlinux.org/man/crypt.5; - -Passlib "Modular Crypt Format": https://passlib.readthedocs.io/en/stable/modular_crypt_format.html; - -Password Hashing Competition string format: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md; */ package passwap diff --git a/passwap_example_test.go b/passwap_example_test.go new file mode 100644 index 0000000..7b5083a --- /dev/null +++ b/passwap_example_test.go @@ -0,0 +1,81 @@ +package passwap_test + +import ( + "fmt" + + "github.com/zitadel/passwap" + "github.com/zitadel/passwap/argon2" + "github.com/zitadel/passwap/bcrypt" +) + +func Example() { + // 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. +} diff --git a/passwap_test.go b/passwap_test.go index 4b10d34..ec14a73 100644 --- a/passwap_test.go +++ b/passwap_test.go @@ -2,12 +2,10 @@ package passwap import ( "errors" - "fmt" "reflect" "testing" "github.com/zitadel/passwap/argon2" - "github.com/zitadel/passwap/bcrypt" tv "github.com/zitadel/passwap/internal/testvalues" "github.com/zitadel/passwap/scrypt" "github.com/zitadel/passwap/verifier" @@ -211,91 +209,3 @@ func TestSwapper(t *testing.T) { } }) } - -func Example() { - // Create a new swapper which hashes using bcrypt, - // verifies and upgrades scrypt. - passwords := NewSwapper( - bcrypt.New(bcrypt.DefaultCost), - scrypt.Verifier, - ) - - // 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 scrypt and bcrypt. - passwords = NewSwapper( - argon2.NewArgon2id(argon2.RecommendedIDParams), - bcrypt.Verifier, - scrypt.Verifier, - ) - - // Attempt to verify encoded bcrypt string with a wrong password. - // Returns an error and empty "updated" - if updated, err := passwords.Verify(encoded, "wrong_password"); err != nil { - fmt.Println(err) - // passwap: password does not match hash - } else if updated != "" { - encoded = updated - } - fmt.Println(encoded) - // $2a$10$eS.mS5Zc5YAJFlImXCpLMu9TxXwKUhgQxsbghlvyVwvwYO/17E2qy - // encoded is unchanged. - - // 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 - } - 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 = NewSwapper( - argon2.NewArgon2id(argon2.Params{ - Time: 2, - Memory: 64 * 1024, - Threads: 4, - KeyLen: 32, - SaltLen: 16, - }), - bcrypt.Verifier, - scrypt.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. -} From 9bdc35c1611251b8fd45bb93c2a3dbdd56072477 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Tue, 15 Aug 2023 11:09:48 +0200 Subject: [PATCH 2/3] some small typos --- README.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index c018534..b183c41 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,15 @@ [![Go](https://github.com/zitadel/passwap/actions/workflows/go.yml/badge.svg)](https://github.com/zitadel/passwap/actions/workflows/go.yml) [![codecov](https://codecov.io/gh/zitadel/passwap/branch/main/graph/badge.svg?token=GrPT2nbCjj)](https://codecov.io/gh/zitadel/passwap) -Package passwap provides a unified implementation between +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 +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 +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. @@ -37,7 +37,7 @@ needs to be updated. ### Encoding -There is no unified standard for encoding password hashes. Essentialy one +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. @@ -46,25 +46,25 @@ All of the Passwap supplied algorithms use dollar sign (`$`) delimited encoding. This results in a single string containing all of the above for later password verification. -It's origin can be found in -[Glibc](https://man.archlinux.org/man/crypt.5). Passlib for python is the +Its origin can be found in +[Glibc](https://man.archlinux.org/man/crypt.5). Passlib for Python is the most complete implementation and there the [Modular Crypt Format](https://passlib.readthedocs.io/en/stable/modular_crypt_format.htm) -expands the subject further. Althought MCF is superseeded by +expands the subject further. Although MCF is superseded by the [Password Hashing Competition string format](https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md), passlib still provides the most complete documentation on the format and encodings used for each algorithm. -Each althorithm supplied by Passwap is compatible with Passlib's encoding +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 user would want to store `good_password` as a password, +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. +stored in a database. In this case, we keep it just in the `encoded` variable. ```go passwords := passwap.NewSwapper( @@ -80,16 +80,16 @@ fmt.Println(encoded) ``` 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 +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. +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 +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 untill they supply +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, @@ -126,14 +126,14 @@ fmt.Println(encoded) // $argon2id$v=19$m=65536,t=1,p=4$d6SOdxdIip9BC7sM5H7PUQ$2E7OIz7C1NkMLOsXi5nSe5vfbthdc9N9SWVlArd200E ``` -Now lets say that we upgraded our hardware with more powerfull CPUs. +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 grow with the increased performance available +the security of our hashes grows with the increased performance available on the market. -In this case we do not need to supply a seperate `argon2.Verifier`, +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 own algorithm. We do keep the `bcrypt.Verifier` +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. From 9ef4a42dc11f223e86916ede319b79e85692c31b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 15 Aug 2023 12:13:34 +0300 Subject: [PATCH 3/3] add go 1.21 to pipeline Co-authored-by: Florian Forster --- .github/workflows/go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index d183669..e99b306 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go: ['1.18', '1.19', '1.20'] + go: ['1.18', '1.19', '1.20', '1.21'] steps: - uses: actions/checkout@v3