{"id":13413789,"url":"https://github.com/zitadel/passwap","last_synced_at":"2025-04-07T10:27:13.672Z","repository":{"id":144436423,"uuid":"614035342","full_name":"zitadel/passwap","owner":"zitadel","description":"Package passwap provides a unified implementation between different password hashing algorithms. It allows for easy swapping between algorithms, using the same API for all of them.","archived":false,"fork":false,"pushed_at":"2024-11-19T10:58:50.000Z","size":112,"stargazers_count":53,"open_issues_count":1,"forks_count":3,"subscribers_count":9,"default_branch":"main","last_synced_at":"2024-12-04T13:48:14.367Z","etag":null,"topics":["argon2","bcrypt","go","hashing","md5-crypt","passlib","password","pbkdf2","scrypt"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zitadel.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-03-14T19:00:07.000Z","updated_at":"2024-12-03T04:44:30.000Z","dependencies_parsed_at":null,"dependency_job_id":"0b936696-ec70-4454-aec6-149f4599be65","html_url":"https://github.com/zitadel/passwap","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zitadel%2Fpasswap","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zitadel%2Fpasswap/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zitadel%2Fpasswap/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zitadel%2Fpasswap/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zitadel","download_url":"https://codeload.github.com/zitadel/passwap/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247634489,"owners_count":20970533,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["argon2","bcrypt","go","hashing","md5-crypt","passlib","password","pbkdf2","scrypt"],"created_at":"2024-07-30T20:01:49.377Z","updated_at":"2025-04-07T10:27:13.632Z","avatar_url":"https://github.com/zitadel.png","language":"Go","funding_links":[],"categories":["安全","Security"],"sub_categories":["HTTP客户端","HTTP Clients"],"readme":"# Passwap\n\n[![Go Reference](https://pkg.go.dev/badge/github.com/zitadel/passwap.svg)](https://pkg.go.dev/github.com/zitadel/passwap)\n[![Go](https://github.com/zitadel/passwap/actions/workflows/go.yml/badge.svg)](https://github.com/zitadel/passwap/actions/workflows/go.yml)\n[![codecov](https://codecov.io/gh/zitadel/passwap/branch/main/graph/badge.svg?token=GrPT2nbCjj)](https://codecov.io/gh/zitadel/passwap)\n[![Go Report Card](https://goreportcard.com/badge/github.com/zitadel/passwap)](https://goreportcard.com/report/github.com/zitadel/passwap)\n\nPackage Passwap provides a unified implementation between\ndifferent password hashing algorithms in the Go ecosystem.\nIt allows for easy swapping between algorithms,\nusing the same API for all of them.\n\nPasswords hashed with Passwap, using a certain algorithm\nand parameters can be stored in a database.\nIf at a later moment parameters or even the algorithm is changed,\nPasswap is still able to verify the \"outdated\" hashes and\nautomatically return an updated hash when applicable.\nOnly when an updated hash is returned, the record in the database\nneeds to be updated.\n\n## Features\n\n- Secure salt generation (from `crypto/rand`) for all algorithms included.\n- Automatic update of passwords.\n- Only [depends](go.mod) on the Go standard library and `golang.org/x/{sys,crypto}`.\n- The `Hasher` and `Verifier` interfaces allow the use of custom algorithms and\n  encoding schemes.\n\n### Algorithms\n\n| Algorithm       | Identifiers                                                        | Secure             |\n| --------------- | ------------------------------------------------------------------ | ------------------ |\n| [argon2][1]     | argon2i, argon2id                                                  | :heavy_check_mark: |\n| [bcrypt][2]     | 2, 2a, 2b, 2y                                                      | :heavy_check_mark: |\n| [md5-crypt][3]  | 1                                                                  | :x:                |\n| [md5 plain][4]  | Hex encoded string                                                 | :x:                |\n| [md5 salted][5] | md5salted-suffix,md5salted-prefix                                  | :x:                |\n| [sha2-crypt][6] | 5, 6                                                               | :heavy_check_mark: |\n| [scrypt][7]     | scrypt, 7                                                          | :heavy_check_mark: |\n| [pbkpdf2][8]    | pbkdf2, pbkdf2-sha224, pbkdf2-sha256, pbkdf2-sha384, pbkdf2-sha512 | :heavy_check_mark: |\n\n[1]: https://pkg.go.dev/github.com/zitadel/passwap/argon2\n[2]: https://pkg.go.dev/github.com/zitadel/passwap/bcrypt\n[3]: https://pkg.go.dev/github.com/zitadel/passwap/md5\n[4]: https://pkg.go.dev/github.com/zitadel/passwap/md5plain\n[5]: https://pkg.go.dev/github.com/zitadel/passwap/md5salted\n[6]: https://pkg.go.dev/github.com/zitadel/passwap/sha2\n[7]: https://pkg.go.dev/github.com/zitadel/passwap/scrypt\n[8]: https://pkg.go.dev/github.com/zitadel/passwap/pbkdf2\n\n### Encoding\n\nThere is no unified standard for encoding password hashes. Essentially one\nwould need to store the parameters used, salt and the resulting hash.\nAs the salt and hash are typically raw bytes, they also need to be converted\nto characters, for example using base64.\n\nAll of the Passwap supplied algorithms use the dollar sign (`$`) delimited\nencoding, aka [Modular Crypt Format](https://passlib.readthedocs.io/en/stable/modular_crypt_format.htm).\nThis results in a single string containing all of the above for\nlater password verification.\n\n#### Argon2\n\nArgon2 uses standard raw Base64 encoding (without padding) for salt and hash.\nThe resulting Modular Crypt Format string looks as follows:\n\n```\n$argon2i$v=19$m=4096,t=3,p=1$cmFuZG9tc2FsdGlzaGFyZA$YMvo8AUoNtnKYGqeODruCjHdiEbl1pKL2MsYy9VgU/E\n   (1)              (2)               (3)                            (4)\n```\n\n1. The identifier, which can be `argon2i` or `argon2id`. `argon2d`, is not supported by Go, and therefore, is not supported by this library either.\n2. Cost parameters.\n   1. `m` for memory -`4096` KiB in this example.\n   2. `t` for time - `3` in this example.\n   3. `p` for parallelism (threads) - `1` in this example.\n3. Base64 encoded salt.\n4. Base64 encoded Argon2 hash output of the password and salt combined.\n\nChanging any of the parameters or salt produces a different hash output.\nMore information about the parameters can be found in the upstream [Argon2 package documentation](https://pkg.go.dev/golang.org/x/crypto/argon2).\n\n### Bcrypt\n\nBcrypt uses a custom Base64 encoding with the character set of `[./A-Za-z0-9]` and padding.\nThe actual formatting is fully implemented by the [Go package](https://pkg.go.dev/golang.org/x/crypto/bcrypt).\nThe resulting Modular Crypt Format string looks as follows:\n\n```\n$2a$12$aLYFkieuqJyeynvptPTxpehSViui5WeAPuR2Xw1wui9CPHEaacmFq\n (1)(2)          (3)                      (4)\n```\n\n1. The identifier can be `2a`, `2b` or, `2y`. It indicates the Bcrypt version but is ignored and the same is always produced.\n2. The cost parameter that is exponential - `12` in this example.\n3. The Base64-encoded salt, always 22 character long.\n4. The Base64-encoded Bcrypt hash output of the password and salt combined.\n\n### MD5 Crypt\n\nMD5 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.\nThe resulting Modular Crypt Format string looks as follows:\n\n```\n$1$kJ4QkJaQ$3EbD/pJddrq5HW3mpZ4KZ1\n(1)   (2)           (3)\n```\n\n1. The identifier is always `1`\n2. Base64-like-encoded salt.\n3. Base64-like-encoded MD5 hash output of the password and salt combined.\n\nThere 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.\n\n### MD5 Plain\n\nMD5 Plain are hex encoded digests of a single iteration of a password without salt.\nFor example passwap can verify passwords hashed by the following methods:\n\n- `printf \"password\" | md5sum` on most linux systems.\n- PHP's `md5(\"password\")`\n- Python3's `hashlib.md5(b\"password\").hexdigest()`\n\nMD5 is considered cryptographically broken and insecure. Also hashing without salt is a bad idea.\nTherefore passwap only supports verification to allow applications to migrate to better methods.\n\n### MD5 Salted\n\nMD5 Salted are base64 encode digest of password+salt (resp. salt+password)\nThe resulting MD5salted Format string looks as follows:\n\n```\n$md5salted-suffix$kJ4QkJaQ$3EbD/pJddrq5HW3mpZ4KZ1\n(1)                  (2)          (3)\n```\n\n1. The identifier is md5salted-suffix or md5salted-prefix\n2. Salt string (will be added to password in exactly this form).\n3. Base64-like-encoded MD5 hash output of the password and salt combined (password+salt or salt+password).\n\nThere 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.\n\n### SHA2 crypt\n\nSHA2 Crypt shares its encoding scheme with MD5 Crypt, but uses SHA-256 or SHA-512 instead of MD5 and uses a different [hashing algorithm](https://www.akkadia.org/drepper/SHA-crypt.txt)\nThe resulting Modular Crypt Format string looks as follows:\n\n```\n$5$rounds=5000$RPvilwjD1ebXJfzg$5B8vYYiY.CVt1RlTTf8KbXBH3hsxY/GNooZaBBGWEc5\n(1)   (2)           (3)                 (4)\n```\n\n1. The identifier is always `5` (SHA-256) or `6` (SHA-512)\n2. The cost parameter in rounds, which is a linear value - `5000` in this example. Note that according to the specification this part is optional (in which case the default of 5000 rounds will be used). In this implementation the rounds are always returned, even when they match the default\n3. Base64-like-encoded salt.\n4. Base64-like-encoded SHA-256/512 hash output of the password and salt combined.\n\n### Scrypt\n\nScrypt uses standard raw Base64 encoding (no padding) for the salt and hash.\nThe resulting Modular Crypt Format string looks as follows:\n\n```\n$scrypt$ln=16,r=8,p=1$cmFuZG9tc2FsdGlzaGFyZA$Rh+NnJNo1I6nRwaNqbDm6kmADswD1+7FTKZ7Ln9D8nQ\n  (1)        (2)              (3)                              (4)\n```\n\n1. The identifier is always `scrypt`.\n2. Cost parameters:\n   1. `ln` is the exponential cost parameter for memory and CPU - `16` in this example.\n   2. `r` is the block size for optimal performance of the CPU architecture - `8` in this example.\n   3. `p` is to indicate parallelism - `1` in this example.\n3. Base64-encoded salt\n4. Base64-encoded Scrypt hash output of the password and salt combined.\n\n### PBKDF2\n\nPBKDF2 uses an alternative Base64 encoding, which is based on the standard with `+` replaced by `.`, and it comes without padding. As we've also seen standard encoding with padding in the wild, the verifier will accept alternative standards with or without padding. The Hasher always produces alternative encoding.\n\nThe resulting Modular Crypt Format string looks as follows:\n\n```\n$pbkdf2-sha256$12$cmFuZG9tc2FsdGlzaGFyZA$OFvEcLOIPFd/oq8egf10i.qJLI7A8nDjPLnolCWarQY\n      (1)     (2)         (3)                            (4)\n```\n\n1. The identifier is made of 2 parts:\n   1. `pbkdf2` is the identifier prefix for the algorithm.\n   2. `-sha256` is an optional suffix with dash separator and is the identifier for the hash backend. When omitted, `sha1` is used as a default.\n2. The cost parameter in rounds, which is a linear value - `12` in this example.\n3. Alternative Base64-encoded salt\n4. Alternative Base64 encoded Scrypt hash output of the password and salt combined.\n\n#### Reference\n\nIts origin can be found in\n[Glibc](https://man.archlinux.org/man/crypt.5). Passlib for Python is the\nmost complete implementation and there the\n[Modular Crypt Format](https://passlib.readthedocs.io/en/stable/modular_crypt_format.htm)\nexpands the subject further. Although MCF is superseded by\nthe [Password Hashing Competition string format](https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md),\npasslib still provides the most complete documentation on the format and\nencodings used for each algorithm.\n\nEach algorithm supplied by Passwap is compatible with Passlib's encoding\nand tested against reference hashes created with Passlib.\n\n## Example\n\nFirst, we want our application to hash passwords using **bcrypt**,\nusing the default cost. We will create a `Swapper` for it.\nWhen a user would want to store `good_password` as a password,\nit is passed into `passwords.Hash()` and the result is typically\nstored in a database. In this case, we keep it just in the `encoded` variable.\n\n```go\npasswords := passwap.NewSwapper(\n    bcrypt.New(bcrypt.DefaultCost),\n)\n\nencoded, err := passwords.Hash(\"good_password\")\nif err != nil {\n    panic(err)\n}\nfmt.Println(encoded)\n// $2a$10$eS.mS5Zc5YAJFlImXCpLMu9TxXwKUhgQxsbghlvyVwvwYO/17E2qy\n```\n\nAt this point `encoded` has the value of `$2a$10$eS.mS5Zc5YAJFlImXCpLMu9TxXwKUhgQxsbghlvyVwvwYO/17E2qy`.\nIt is an encoded string containing the **bcrypt** identifier, cost, salt and hashed password which later\ncan be used for verification.\n\nAt a later moment, you can reconfigure your application to use another hashing algorithm.\nThis might be because the former is cryptographically broken, customer demand\nor just because you can. Next, we will create a new `Swapper` configured to hash using\nthe **argon2id** algorithm.\n\nWe already have users that have created passwords using **bcrypt**.\nAs hashing is a one-way operation we can't migrate them until they supply\nthe password again. Therefore we must pass the `bcrypt.Verifier` as well.\n\nOnce the user supplies his password again and we need to verify it,\n`passwords.Verify()` will return an `updated` encoded string automatically,\nbecause the Swapper figured out that the original `encoded` was created using\na different algorithm.\n\n```go\npasswords = passwap.NewSwapper(\n    argon2.NewArgon2id(argon2.RecommendedIDParams),\n    bcrypt.Verifier,\n)\nif updated, err := passwords.Verify(encoded, \"good_password\"); err != nil {\n    panic(err)\n} else if updated != \"\" {\n    encoded = updated // store in \"DB\"\n}\nfmt.Println(encoded)\n```\n\nAt this point `encoded` will look something like\n`$argon2id$v=19$m=65536,t=1,p=4$d6SOdxdIip9BC7sM5H7PUQ$2E7OIz7C1NkMLOsXi5nSe5vfbthdc9N9SWVlArd200E`.\n\nIf we would call `passwords.Verify()` again, `updated` returns empty.\nThat's because `encoded` was created using the same algorithm and parameters.\n\n```go\nif updated, err := passwords.Verify(encoded, \"good_password\"); err != nil {\n    panic(err)\n} else if updated != \"\" { // updated is empty, nothing is stored\n    encoded = updated\n}\nfmt.Println(encoded)\n// $argon2id$v=19$m=65536,t=1,p=4$d6SOdxdIip9BC7sM5H7PUQ$2E7OIz7C1NkMLOsXi5nSe5vfbthdc9N9SWVlArd200E\n```\n\nNow let's say that we upgraded our hardware with more powerful CPUs.\nWe should now also increase the `time` parameter accordingly, so that\nthe security of our hashes grows with the increased performance available\non the market.\n\nIn this case, we do not need to supply a separate `argon2.Verifier`,\nas the returned `Hasher` from `NewArgon2id()` should already implement\nthe `Verifier` interface for its algorithm. We do keep the `bcrypt.Verifier`\naround, because we might still have users that didn't use their password since the\nlast update.\n\n```go\npasswords = passwap.NewSwapper(\n    argon2.NewArgon2id(argon2.Params{\n        Time:    2,\n        Memory:  64 * 1024,\n        Threads: 4,\n        KeyLen:  32,\n        SaltLen: 16,\n    }),\n    bcrypt.Verifier,\n)\nif updated, err := passwords.Verify(encoded, \"good_password\"); err != nil {\n    panic(err)\n} else if updated != \"\" {\n    encoded = updated\n}\n```\n\nAt this point `encoded` would be updated again and look like\n`$argon2id$v=19$m=65536,t=2,p=4$44X+dwU+aSS85Kl1qH3/Jg$n/tQoAtx/I/Rt9BXHH9tScshWucltPPmB0HBLVtXCq0`\nYou'll see that the `t=2` parameter is updated as well as the resulting\nsalt and hash. A new salt is always obtained during hashing.\n\nThe full example is also part of the [Go documentation](https://pkg.go.dev/github.com/zitadel/passwap#example-package).\n\n## Supported Go Versions\n\nFor security reasons, we only support and recommend the use of one of the latest two Go versions (:white_check_mark:).  \nVersions that also build are marked with :warning:.\n\n| Version | Supported          |\n| ------- | ------------------ |\n| \u003c1.23   | :x:                |\n| 1.23    | :white_check_mark: |\n| 1.24    | :white_check_mark: |\n\n## License\n\nThe full functionality of this library is and stays open source and free to use for everyone. Visit\nour [website](https://zitadel.com) and get in touch.\n\nSee the exact licensing terms [here](LICENSE)\n\nUnless required by applicable law or agreed to in writing, software distributed under the License is distributed on an\n\"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific\nlanguage governing permissions and limitations under the License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzitadel%2Fpasswap","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzitadel%2Fpasswap","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzitadel%2Fpasswap/lists"}