├── .github ├── dependabot.yml └── workflows │ ├── docs.yml │ └── tests.yml ├── README.md ├── changelog.md ├── docker-compose.yml ├── dockerfile ├── nimword.nimble ├── src ├── nimword.nim └── nimword │ ├── argon2.nim │ ├── pbkdf2_sha256.nim │ ├── pbkdf2_sha512.nim │ └── private │ ├── base64_utils.nim │ ├── pbkdf2_utils.nim │ └── types.nim └── tests ├── config.nims ├── t_argon2.nim ├── t_nimword.nim ├── t_pbkdf2_sha256.nim └── t_pbkdf2_sha512.nim /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | api-docs: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Cache choosenim 16 | id: cache-choosenim 17 | uses: actions/cache@v4 18 | with: 19 | path: ~/.choosenim 20 | key: ${{ runner.os }}-choosenim-stable 21 | 22 | - name: Cache nimble 23 | id: cache-nimble 24 | uses: actions/cache@v4 25 | with: 26 | path: ~/.nimble 27 | key: ${{ runner.os }}-nimble-${{ hashFiles('nimword.nimble') }} 28 | restore-keys: | 29 | ${{ runner.os }}-nimble- 30 | 31 | - name: Setup nim 32 | uses: jiro4989/setup-nim-action@v2 33 | with: 34 | nim-version: devel 35 | 36 | - name: Install Packages 37 | run: nimble install -y 38 | 39 | - name: Build API docs 40 | run: nimble --verbose apis 41 | 42 | - name: Archive API docs 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: api-docs 46 | path: | 47 | docs/apidocs 48 | 49 | deploy-docs: 50 | needs: 51 | - api-docs 52 | runs-on: ubuntu-latest 53 | steps: 54 | - name: Download all docs 55 | uses: actions/download-artifact@v4 56 | 57 | - name: Check files 58 | run: | 59 | find . 60 | 61 | - name: Setup docs 62 | run: | 63 | mv api-docs docs/ 64 | 65 | - name: Deploy 66 | if: success() 67 | uses: crazy-max/ghaction-github-pages@v4.1.0 68 | with: 69 | target_branch: gh-pages 70 | build_dir: ./docs 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 73 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - devel 7 | - main 8 | pull_request: 9 | branches: 10 | - devel 11 | - main 12 | 13 | jobs: 14 | Tests: 15 | strategy: 16 | matrix: 17 | nimversion: 18 | - binary:2.0.0 19 | - binary:1.6.10 20 | os: 21 | - ubuntu-latest 22 | #- windows-latest 23 | #- macOS-latest 24 | runs-on: ${{ matrix.os }} 25 | timeout-minutes: 30 26 | 27 | name: Nim ${{ matrix.nimversion }} - ${{ matrix.os }} 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: iffy/install-nim@v5 32 | with: 33 | version: ${{ matrix.nimversion }} 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | - name: Test 37 | run: | 38 | sudo apt-get update 39 | sudo apt-get install -y openssl libsodium-dev xz-utils argon2 40 | nimble install -y 41 | nimble test 42 | 43 | Docs: 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: KengoTODA/actions-setup-docker-compose@v1 47 | with: 48 | version: "2.14.2" # the full version of `docker-compose` command 49 | - uses: actions/checkout@v4 50 | - name: Docs 51 | run: docker-compose run docs 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nimword 2 | #### A mini password hashing collection 3 | 4 | [![Run Tests](https://github.com/PhilippMDoerner/nimword/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/PhilippMDoerner/nimword/actions/workflows/tests.yml) 5 | 6 | [![github pages](https://github.com/PhilippMDoerner/nimword/actions/workflows/docs.yml/badge.svg?branch=main)](https://github.com/PhilippMDoerner/nimword/actions/workflows/docs.yml) 7 | 8 | - [API index](https://philippmdoerner.github.io/nimword/nimword.html) 9 | 10 | This package is a collection of functions for password hashing implemented by other packages, presented with a unified interface. 11 | It is currently only tested for Linux, but *should* work for Windows as well assuming the same libraries are installed. 12 | 13 | Currently available hashing algorithms: 14 | - PBKDF2 - HMAC with SHA256 from [openssl](https://nim-lang.org/docs/openssl.html) 15 | - PBKDF2 - HMAC with SHA512 from [openssl](https://nim-lang.org/docs/openssl.html) 16 | - Argon2 from [libsodium](https://github.com/FedericoCeratto/nim-libsodium) 17 | 18 | ## Installation 19 | Install Nimword with [Nimble](https://github.com/nim-lang/nimble): 20 | 21 | $ nimble install -y nimword 22 | 23 | Add Nimword to your .nimble file: 24 | 25 | requires "nimword" 26 | 27 | 28 | If you want to use argon2, ensure you have [libsodium](https://doc.libsodium.org/installation) installed. 29 | 30 | If you want to use pbkdf2, ensure you have OpenSSL version 1 or 3 installed 31 | 32 | ## Basic usage 33 | The following will work for every module: 34 | ```nim 35 | let password: string = "my-super-secret-password" 36 | let iterations: int = 3 # For Argon2 this is sensible, for pbkdf2 consider a number above 100.000 37 | let encodedHash: string = hashEncodePassword(password, iterations) 38 | 39 | assert password.isValidPassword(encodedHash) == true 40 | ``` 41 | 42 | ## Core-API 43 | The core module of nimword provides the simple api of `hashEncodePassword` and `isValidPassword`: 44 | - `hashEncodePassword`: 45 | Proc to create base64 encoded hashes and further encodes them in a specific format that can be stored in e.g. a database and used with `isValidPassword`. 46 | Always takes the plain-text password, the algorithm to use for hashing and a number of iterations for the algorithm. Any further values needed by the algorithm will use sensible defaults. The salts for hashing will be generated and returned as part of the encoded string. 47 | - `isValidPassword`: 48 | Proc to validate if a given password is identical to the one that was used to create an encoded hash. 49 | 50 | These core procs are also available in the individual modules for each algorithm, there `hashEncodePassword` may expose further options depending on the algorithm. 51 | 52 | The individual algorithm-modules further provide 2 procs in case some customization is needed: 53 | - `hashPassword`: 54 | Proc to create unencoded raw hashes like `hashEncodePassword`, but returns the hash-bytes directly from there without turning it into a specific format like `hashEncodePassword` does. 55 | - `encodeHash`: 56 | Proc to generate strings of the format that `hashEncodePassword` outputs, but without doing any of the hashing itself. The output can be used with `isValidPassword`. 57 | 58 | ## Running tests 59 | You can run the tests either locally or in a container: 60 | - `nimble test` 61 | - `nimble containerTest` - This assumes you have docker and docker-compose installed -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | - [!]—backward incompatible change 4 | - [+]—new feature 5 | - [f]—bugfix 6 | - [r]—refactoring 7 | - [t]—test suite improvement 8 | - [d]—docs improvement 9 | 10 | 11 | ## 0.2.0 (Febuary 02, 2022) 12 | - [r] Add `raise` pragma to public-api to enforce users to deal with potential exceptions 13 | - [d] Add info to Readme.md to install libsodium/openssl for usage 14 | - [d] Add changelog.md 15 | - [r] Moved fetching `EVP_MD_get_size`/ `EVP_MD_size` depending on Openssl version out of a proc and into global space so that it fails on startup rather than at actual runtime. 16 | 17 | ## 0.1.0 (Febuary 02, 2023) 18 | - 🎉 initial release. -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | tests: 3 | build: . 4 | image: nimword 5 | volumes: 6 | - .:/usr/src/app 7 | command: nimble test 8 | 9 | docs: 10 | build: . 11 | image: nimword 12 | volumes: 13 | - .:/usr/src/app 14 | command: nimble apis 15 | -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | FROM bitnami/minideb 2 | 3 | RUN apt-get update && apt-get install -y curl xz-utils gcc git openssl ca-certificates libsodium-dev argon2 4 | RUN git config --global safe.directory '*' 5 | 6 | WORKDIR /root/ 7 | RUN curl https://nim-lang.org/choosenim/init.sh -sSf | bash -s -- -y 8 | ENV PATH=/root/.nimble/bin:$PATH 9 | 10 | RUN apt -y autoremove 11 | RUN apt -y autoclean 12 | RUN apt -y clean 13 | RUN rm -r /tmp/* 14 | WORKDIR /usr/src/app 15 | 16 | COPY . /usr/src/app 17 | 18 | RUN nimble install -y -------------------------------------------------------------------------------- /nimword.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "1.0.1" 4 | author = "Philipp Doerner" 5 | description = "A simple library with a simple interface to do password hashing and validation with different algorithms" 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | 10 | # Dependencies 11 | 12 | requires "nim >= 1.6.10" 13 | requires "libsodium#144d6d8" 14 | 15 | task apis, "docs only for api": 16 | exec "nim doc --verbosity:0 --warnings:off --project --index:on " & 17 | "--git.url:https://github.com/PhilippMDoerner/nimword " & 18 | "--git.commit:main " & 19 | "-o:docs/apidocs " & 20 | "src/nimword.nim" 21 | 22 | exec "nim buildIndex -o:docs/apidocs/index.html docs/apidocs" 23 | 24 | task containerTests, "Runs the tests within a docker container": 25 | echo staticExec "sudo docker image rm nimword" 26 | exec "sudo docker-compose run --rm tests" -------------------------------------------------------------------------------- /src/nimword.nim: -------------------------------------------------------------------------------- 1 | import nimword/[argon2, pbkdf2_sha256, pbkdf2_sha512] 2 | import std/[strutils, strformat] 3 | 4 | export argon2.SodiumError 5 | export pbkdf2_sha256.Pbkdf2Error 6 | export Password 7 | export toPassword 8 | export Hash 9 | 10 | type NimwordHashingAlgorithm* = enum 11 | ## The number of different hashing algorithms nimword supports 12 | nhaPbkdf2Sha256 = "pbkdf2_sha256" 13 | nhaPbkdf2Sha512 = "pbkdf2_sha512" 14 | nhaArgon2i = "argon2i" 15 | nhaArgon2id = "argon2id" 16 | nhaDefault 17 | 18 | type UnknownAlgorithmError = object of ValueError 19 | 20 | proc hashEncodePassword*( 21 | password: Password, 22 | iterations: int, 23 | algorithm: NimwordHashingAlgorithm = nhaDefault 24 | ): string = 25 | ## Hashes and encodes the given password using the argon2 algorithm from libsodium. 26 | ## 27 | ## Returns the hash as part of a larger string containing hash, iterations, algorithm, 28 | ## salt and any further values used to calculate the hash. The pattern depends on the 29 | ## algorithm chosen. 30 | ## 31 | ## The return value can be used with `isValidPassword<#isValidPassword%2Cstring%2Cstring>`_ . 32 | ## 33 | ## The salt is randomly generated during the process. 34 | ## 35 | ## For guidance on choosing values for `iterations` consult the 36 | ## `libsodium-documentation`_ 37 | result = case algorithm: 38 | of nhaPbkdf2Sha256: 39 | pbkdf2_sha256.hashEncodePassword(password, iterations) 40 | of nhaPbkdf2Sha512: 41 | pbkdf2_sha512.hashEncodePassword(password, iterations) 42 | of nhaArgon2i: 43 | argon2.hashEncodePassword(password, iterations, PasswordHashingAlgorithm.phaArgon2i13) 44 | of nhaArgon2id: 45 | argon2.hashEncodePassword(password, iterations, PasswordHashingAlgorithm.phaArgon2id13) 46 | of nhaDefault: 47 | argon2.hashEncodePassword(password, iterations, PasswordHashingAlgorithm.phaDefault) 48 | 49 | 50 | proc isValidPassword*( 51 | password: Password, 52 | encodedHash: string 53 | ): bool {.raises: {UnknownAlgorithmError, ValueError, Pbkdf2Error, SodiumError, Exception}.} = 54 | ## Verifies that a given plain-text password can be used to generate 55 | ## the hash contained in `encodedHash` with the parameters provided in `encodedHash`. 56 | ## 57 | ## `encodedHash` must be a string with the kind of pattern that `encodeHash` procs or 58 | ## and `hashEncodePassword<#hashEncodePassword%2Cstring%2Cint>`_ generate. 59 | ## 60 | ## Raises UnknownAlgorithmError if the encoded hash string is for an algorithm not 61 | ## supported by nimword. 62 | var algorithm: NimwordHashingAlgorithm 63 | let algorithmStr: string = encodedHash.split("$")[1] 64 | try: 65 | algorithm = parseEnum[NimwordHashingAlgorithm](algorithmStr) 66 | except ValueError as e: 67 | raise newException(UnknownAlgorithmError, fmt"'{algorithmStr}' is not an algorithm supported by nimword. Consult the NimwordHashingAlgorithm to see which algorithm options are supported.") 68 | 69 | case algorithm: 70 | of nhaPbkdf2Sha256: 71 | result = pbkdf2_sha256.isValidPassword(password, encodedHash) 72 | of nhaPbkdf2Sha512: 73 | result = pbkdf2_sha512.isValidPassword(password, encodedHash) 74 | of nhaArgon2i, nhaArgon2id, nhaDefault: 75 | result = argon2.isValidPassword(password, encodedHash) -------------------------------------------------------------------------------- /src/nimword/argon2.nim: -------------------------------------------------------------------------------- 1 | import std/[strformat, base64, strutils] 2 | import libsodium/[sodium, sodium_sizes] 3 | import ./private/types 4 | 5 | export sodium.PasswordHashingAlgorithm 6 | export sodium.SodiumError 7 | export types.Password 8 | export types.toPassword 9 | export types.Hash 10 | 11 | proc encodeHash*( 12 | hash: Hash, 13 | salt: seq[byte], 14 | iterations: int, 15 | algorithm: PasswordHashingAlgorithm; 16 | memoryLimitKibiBytes: int; 17 | ): string = 18 | ## Encodes all relevant data for a password hash in a string. 19 | ## 20 | ## The returned string can be used with `isValidPassword<#isValidPassword%2Cstring%2Cstring>`_ . 21 | ## Hash is a seq[byte] like salt and gets turned into a base64 encoded string with all padding suffix characters of "=" removed". 22 | ## 23 | ## Salt gets turned into a base64 encoded string with all padding suffix character of "=" removed. 24 | ## memoryLimitKibiBytes is the number of KiB used for the hashing process. 25 | ## algorithm is either "argon2id" or "argon2i". 26 | ## 27 | ## The pattern is: 28 | ## $$v=19$m=,t=,p=1$$ 29 | var encodedSalt = salt.encode() 30 | encodedSalt.removeSuffix('=') 31 | var encodedHash = hash.encode() 32 | encodedHash.removeSuffix('=') 33 | 34 | let algorithmStr = case algorithm: 35 | of phaDefault, phaArgon2id13: 36 | "argon2id" 37 | of phaArgon2i13: 38 | "argon2i" 39 | 40 | result = fmt"${algorithmStr}$v=19$m={memoryLimitKibiBytes},t={iterations},p=1${encodedSalt}${encodedHash}" 41 | 42 | 43 | 44 | proc hashPassword*( 45 | password: Password, 46 | salt: seq[byte], 47 | iterations: int = crypto_pwhash_opslimit_moderate().int, 48 | hashLength: int = 32, 49 | algorithm: PasswordHashingAlgorithm = phaDefault, 50 | memoryLimitKibiBytes: int = (crypto_pwhash_memlimit_moderate().int / 1024).int 51 | ): Hash {.raises: {SodiumError, ValueError}.} = 52 | ## Hashes the given password using the argon2 algorithm from libsodium. 53 | ## Returns the hash as seq[byte] 54 | ## 55 | ## Salt must be exactly 16 bytes long. 56 | ## 57 | ## Iterations is the number of times the argon-algorithm is applied during hashing. 58 | ## For guidance on how to choose a number for this value, consult the 59 | ## `libsodium-documentation`_ 60 | ## for the `opslimit` value. 61 | ## 62 | ## hashLength is the number of bytes that the hash should be long. 63 | ## For guidance on how to choose a number for this value, consult the 64 | ## `libsodium-documentation`_ 65 | ## for the `outlen` value. 66 | ## 67 | ## The algorithm defaults to the default of libsodium. For guidance on which Argon variant to choose, 68 | ## consult the `argon readme`_ . Do note that libsodium 69 | ## and thus this package does not provide a way to call Argon2D. 70 | ## 71 | ## The memoryLimit must be provided in KibiBytes aka KiB, it designates the 72 | ## amount of memory used during hashing. 73 | ## For guidance on how to choose a number for this value, consult the 74 | ## `libsodium-documentation`_ 75 | ## for the `memlimit` value. 76 | ## 77 | ## Raises SodiumError for invalid values for memoryLimit or iterations. 78 | 79 | let memoryLimitBytes = memoryLimitKibiBytes * 1024 80 | let hash: Hash = crypto_pwhash( 81 | password.string, 82 | salt, 83 | hashLength, 84 | algorithm, 85 | iterations.csize_t, 86 | memoryLimitBytes.csize_t 87 | ) 88 | return hash 89 | 90 | proc hashEncodePassword*( 91 | password: Password, 92 | iterations: int = crypto_pwhash_opslimit_moderate().int, 93 | algorithm: PasswordHashingAlgorithm = phaDefault, 94 | memoryLimitKibiBytes: int = (crypto_pwhash_memlimit_moderate().int / 1024).int 95 | ): string {.raises: {SodiumError, ValueError}.} = 96 | ## Hashes and encodes the given password using the argon2 algorithm from libsodium. 97 | ## 98 | ## Returns the hash as part of a larger string containing hash, iterations, algorithm, 99 | ## memoryLimitKibiBytes and salt. For information about the pattern see `encodeHash<#encodeHash%2Cstring%2Cseq[byte]%2CSomeInteger>`_ 100 | ## 101 | ## The return value can be used with `isValidPassword<#isValidPassword%2Cstring%2Cstring>`_ . 102 | ## 103 | ## The salt is randomly generated during the process. 104 | ## 105 | ## For guidance on choosing values for `iterations`, `algorithm`and `memorylimitKibiBytes` 106 | ## see `hashPassword<#hashPassword%2Cstring%2Cseq[byte]%2Cint>`_ . 107 | ## 108 | ## Raises SodiumError for invalid values for memoryLimit or iterations. 109 | let memoryLimitBytes: int = memoryLimitKibiBytes * 1024 110 | result = crypto_pwhash_str( 111 | password.string, 112 | algorithm, 113 | iterations.csize_t, 114 | memoryLimitBytes.csize_t 115 | ) 116 | 117 | proc isValidPassword*(password: Password, encodedHash: string): bool {.raises: SodiumError.} = 118 | ## Verifies that a given plain-text password can be used to generate 119 | ## the hash contained in `encodedHash` with the parameters provided in `encodedHash`. 120 | ## 121 | ## `encodedHash` must be a string with the kind of pattern that `encodeHash<#encodeHash%2Cstring%2Cseq[byte]%2CSomeInteger>`_ 122 | ## and `hashEncodePassword<#hashEncodePassword%2Cstring%2Cint>`_ generate. 123 | ## 124 | ## Raises SodiumError if an error happens during the process. 125 | result = crypto_pwhash_str_verify(encodedHash, password.string) -------------------------------------------------------------------------------- /src/nimword/pbkdf2_sha256.nim: -------------------------------------------------------------------------------- 1 | import std/[strformat, strutils, sysrand] 2 | from std/openssl import DLLSSLName, EVP_MD, DLLUtilName 3 | import ./private/[base64_utils, pbkdf2_utils] 4 | 5 | export pbkdf2_utils.Pbkdf2Error 6 | export Password 7 | export toPassword 8 | export Hash 9 | 10 | # Imports that sometimes break when importing from std/openssl - START 11 | proc EVP_sha256_fixed(): EVP_MD {.cdecl, dynlib: DLLUtilName, importc: "EVP_sha256".} 12 | # Imports that sometimes break when importing from std/openssl - END 13 | 14 | proc encodeHash*( 15 | hash: Hash, 16 | salt: seq[byte], 17 | iterations: SomeInteger, 18 | ): string = 19 | ## Convenience proc to encode all relevant data for a password hash 20 | ## using pbkdf2_sha256 into a string. 21 | ## 22 | ## The returned string can be used with `isValidPassword<#isValidPassword%2Cstring%2Cstring>`_ . 23 | ## 24 | ## For further information, see `encodeHash`_ 25 | 26 | result = encodeHash(hash, salt, iterations, Pbkdf2Algorithm.pbkdf2_sha256) 27 | 28 | proc hashPassword*(password: Password, salt: seq[byte], iterations: int): Hash {.gcsafe.} = 29 | ## Hashes the given plain-text password with the PBKDF2 using an HMAC 30 | ## with the SHA256 hashing algorithm from openssl. 31 | ## 32 | ## Returns the hash as Hash type. 33 | ## 34 | ## Salt can be of any size, but is recommended to be at least 16 bytes long. 35 | ## 36 | ## Iterations is the number of times the argon-algorithm is applied during hashing. 37 | ## Set the number of iterations to be as high as you can as long as hashing 38 | ## times remain acceptable for your application. 39 | ## For online use (e.g. logging in on a website), a 1 second computation is likely to be the acceptable maximum. 40 | ## For interactive use (e.g. a desktop application), a 5 second pause after having entered a password is acceptable if the password doesn't need to be entered more than once per session. 41 | ## For non-interactive and infrequent use (e.g. restoring an encrypted backup), an even slower computation can be an option. 42 | let digestFunction: EVP_MD = EVP_sha256_fixed() 43 | result = hashPbkdf2(password, salt, iterations, digestFunction) 44 | 45 | proc hashEncodePassword*(password: Password, iterations: int): string {.gcsafe.} = 46 | ## Hashes and encodes the given password with the PBKDF2 using an HMAC 47 | ## with the SHA256 hashing algorithm from openssl. 48 | ## 49 | ## Returns the hash in an encoded form as part of a larger string containing it, iterations and salt. 50 | ## For information about the pattern see `encodeHash<#encodeHash%2Cstring%2Cseq[byte]%2CSomeInteger>`_ 51 | ## 52 | ## The return value can be used with `isValidPassword<#isValidPassword%2Cstring%2Cstring>`_ . 53 | ## 54 | ## For guidance on choosing values for `iterations`, `algorithm`and `memorylimitKibiBytes` 55 | ## see `hashPassword<#hashPassword%2Cstring%2Cseq[byte]%2Cint>`_ . 56 | ## 57 | ## The salt used for the hash is randomly generated during the process. 58 | let salt = urandom(16) 59 | let hash: Hash = hashPassword(password, salt, iterations) 60 | result = hash.encodeHash(salt, iterations) 61 | 62 | proc isValidPassword*(password: Password, encodedHash: string): bool {.raises: {Pbkdf2Error, Exception} .} = 63 | ## Verifies that a given plain-text password can be used to generate 64 | ## the hash contained in `encodedHash` with the parameters provided in `encodedHash`. 65 | ## 66 | ## `encodedHash` must be a string with the kind of pattern that `encodeHash<#encodeHash%2Cstring%2Cseq[byte]%2CSomeInteger>`_ 67 | ## and `hashEncodePassword<#hashEncodePassword%2Cstring%2Cint>`_ generate. 68 | ## 69 | ## Raises Pbkdf2Error if an error happens during the process. 70 | 71 | try: 72 | let hashPieces: seq[string] = encodedHash.split('$')[1..^1] 73 | let iterations: int = parseInt(hashPieces[1]) 74 | let salt: seq[byte] = hashPieces[2].decode() 75 | 76 | let passwordHash: Hash = password.hashPassword(salt, iterations) 77 | 78 | let hash: Hash = hashPieces[3].decode() 79 | result = passwordHash == hash 80 | 81 | except CatchableError as e: 82 | raise newException( 83 | Pbkdf2Error, 84 | fmt"Could not calculate password hash from the encoded Hash string", 85 | e 86 | ) -------------------------------------------------------------------------------- /src/nimword/pbkdf2_sha512.nim: -------------------------------------------------------------------------------- 1 | import std/[strformat, strutils, sysrand] 2 | from std/openssl import DLLSSLName, EVP_MD, DLLUtilName 3 | import ./private/[base64_utils, pbkdf2_utils] 4 | 5 | export pbkdf2_utils.Pbkdf2Error 6 | export Password 7 | export toPassword 8 | export Hash 9 | 10 | # Imports that sometimes break when importing from std/openssl - START 11 | proc EVP_sha512_fixed(): EVP_MD {.cdecl, dynlib: DLLUtilName, importc: "EVP_sha512".} 12 | # Imports that sometimes break when importing from std/openssl - END 13 | 14 | proc encodeHash*( 15 | hash: Hash, 16 | salt: seq[byte], 17 | iterations: SomeInteger, 18 | ): string = 19 | ## Convenience proc to encode all relevant data for a password hash 20 | ## using pbkdf2_sha512 into a string. 21 | ## 22 | ## The returned string can be used with `isValidPassword<#isValidPassword%2Cstring%2Cstring>`_ . 23 | ## 24 | ## For further information, see `encodeHash`_ 25 | 26 | result = encodeHash(hash, salt, iterations, Pbkdf2Algorithm.pbkdf2_sha512) 27 | 28 | proc hashPassword*(password: Password, salt: seq[byte], iterations: int): Hash {.gcsafe.} = 29 | ## Hashes the given plain-text password with the PBKDF2 using an HMAC 30 | ## with the SHA512 hashing algorithm from openssl. 31 | ## 32 | ## Returns the hash as Hash type. 33 | ## 34 | ## Salt can be of any size, but is recommended to be at least 16 bytes long. 35 | ## 36 | ## Iterations is the number of times the argon-algorithm is applied during hashing. 37 | ## Set the number of iterations to be as high as you can as long as hashing 38 | ## times remain acceptable for your application. 39 | ## For online use (e.g. logging in on a website), a 1 second computation is likely to be the acceptable maximum. 40 | ## For interactive use (e.g. a desktop application), a 5 second pause after having entered a password is acceptable if the password doesn't need to be entered more than once per session. 41 | ## For non-interactive and infrequent use (e.g. restoring an encrypted backup), an even slower computation can be an option. 42 | let digestFunction: EVP_MD = EVP_sha512_fixed() 43 | 44 | result = hashPbkdf2(password, salt, iterations, digestFunction) 45 | 46 | proc hashEncodePassword*(password: Password, iterations: int): string {.gcsafe.} = 47 | ## Hashes and encodes the given password with the PBKDF2 using an HMAC 48 | ## with the SHA256 hashing algorithm from openssl. 49 | ## 50 | ## Returns the hash in an encoded form as part of a larger string containing hash, iterations and salt. 51 | ## For information about the pattern see `encodeHash<#encodeHash%2Cstring%2Cseq[byte]%2CSomeInteger>`_ 52 | ## 53 | ## The return value can be used with `isValidPassword<#isValidPassword%2Cstring%2Cstring>`_ . 54 | ## 55 | ## For guidance on choosing values for `iterations`, `algorithm`and `memorylimitKibiBytes` 56 | ## see `hashPassword<#hashPassword%2Cstring%2Cseq[byte]%2Cint>`_ . 57 | ## 58 | ## The salt used for the hash is randomly generated during the process. 59 | 60 | let salt = urandom(16) 61 | let hash = hashPassword(password, salt, iterations) 62 | result = hash.encodeHash(salt, iterations, Pbkdf2Algorithm.pbkdf2_sha512) 63 | 64 | proc isValidPassword*(password: Password, encodedHash: string): bool {.raises: {Pbkdf2Error, Exception} .}= 65 | ## Verifies that a given plain-text password can be used to generate 66 | ## the hash contained in `encodedHash` with the parameters provided in `encodedHash`. 67 | ## 68 | ## `encodedHash` must be a string with the kind of pattern that `encodeHash<#encodeHash%2Cstring%2Cseq[byte]%2CSomeInteger>`_ 69 | ## and `hashEncodePassword<#hashEncodePassword%2Cstring%2Cint>`_ generate. 70 | ## 71 | ## Raises Pbkdf2Error if an error happens during the process. 72 | 73 | try: 74 | let hashPieces: seq[string] = encodedHash.split('$')[1..^1] 75 | let iterations: int = parseInt(hashPieces[1]) 76 | let salt: seq[byte] = hashPieces[2].decode() 77 | 78 | let passwordHash: Hash = password.hashPassword(salt, iterations) 79 | 80 | let hash: Hash = hashPieces[3].decode() 81 | result = passwordHash == hash 82 | 83 | except CatchableError as e: 84 | raise newException( 85 | Pbkdf2Error, 86 | fmt"Could not calculate password hash from the encodedHash", 87 | e 88 | ) -------------------------------------------------------------------------------- /src/nimword/private/base64_utils.nim: -------------------------------------------------------------------------------- 1 | import std/base64 2 | 3 | export base64.encode 4 | 5 | proc toBytes*(s: string): seq[byte] = 6 | ## Simply casts a string into a byte-sequence without modifying it 7 | result = cast[ptr seq[byte]](unsafeAddr s)[] 8 | 9 | proc decode*(s: string): seq[byte] = 10 | ## Decodes a base64 encoded string and returns it as a byte-sequence 11 | let decodedStr: string = base64.decode(s) 12 | result = decodedStr.toBytes() 13 | -------------------------------------------------------------------------------- /src/nimword/private/pbkdf2_utils.nim: -------------------------------------------------------------------------------- 1 | from std/openssl import DLLSSLName, EVP_MD, DLLUtilName, getOpenSSLVersion 2 | import std/[strformat, strutils, dynlib] 3 | import ./base64_utils 4 | import ./types 5 | 6 | export types 7 | 8 | type Pbkdf2Error* = object of ValueError 9 | 10 | type Pbkdf2Algorithm* = enum 11 | ## The hash algorithms that can be used with pbkdf2 12 | pbkdf2_sha256 13 | pbkdf2_sha512 14 | 15 | 16 | # Imports that sometimes break when importing from std/openssl - START 17 | type DigestSizeProc = proc(md: EVP_MD): cint {.cdecl, gcsafe.} 18 | 19 | let lib = loadLibPattern(DLLUtilName) 20 | assert lib != nil, fmt"Could not find lib {DLLUtilName}" 21 | 22 | proc getOpenSSLMajorVersion(): uint = 23 | ## Returns the major version of openssl 24 | result = (getOpenSSLVersion() shr 28) and 0xF 25 | 26 | let sizeProc: DigestSizeProc = 27 | if getOpenSSLMajorVersion() == 3: 28 | cast[DigestSizeProc](lib.symAddr("EVP_MD_get_size")) 29 | 30 | elif getOpenSSLMajorVersion() == 1: 31 | cast[DigestSizeProc](lib.symAddr("EVP_MD_size")) 32 | 33 | else: 34 | raise newException(ValueError, fmt"This library supports only openssl 1 and 3. The openssl version we found was {getOpenSSLMajorVersion()}") 35 | assert sizeProc != nil, "Failed to load hash size for digest function" 36 | 37 | unloadLib(lib) 38 | 39 | proc EVP_MD_size_fixed(md: EVP_MD): cint = 40 | assert md != nil, "Tried to get the hash size for a digest function but the digest function was nil!" 41 | result = sizeProc(md) 42 | 43 | # Imports that sometimes break when importing from std/openssl - END 44 | 45 | 46 | 47 | proc `$`(s: seq[byte]): string = 48 | ## Casts a 49 | result = cast[ptr string](unsafeAddr s)[] 50 | 51 | proc encodeHash*( 52 | hash: Hash, 53 | salt: seq[byte], 54 | iterations: SomeInteger, 55 | algorithm: Pbkdf2Algorithm, 56 | ): string = 57 | ## Encodes all relevant data for a password hash in a string. 58 | ## 59 | ## Hash is a seq[byte] like salt and gets turned into a base64 encoded string with all padding suffix characters of "=" removed". 60 | ## Salt gets turned into a base64 encoded string with all padding suffix character of "=" removed. 61 | ## Algorithm is either "pbkdf2_sha256" or "pbkdf2_sha512" 62 | ## 63 | ## The pattern is: 64 | ## $$$$ 65 | var encodedHash = hash.encode() 66 | encodedHash.removeSuffix('=') 67 | var encodedSalt = salt.encode() 68 | encodedSalt.removeSuffix('=') 69 | result = fmt"${algorithm}${iterations}${encodedSalt}${encodedHash}" 70 | 71 | proc PKCS5_PBKDF2_HMAC( 72 | pass: cstring, 73 | passLen: cint, 74 | salt: cstring, 75 | saltLen: cint, 76 | iter: cint, 77 | digest: EVP_MD, 78 | keylen: cint, 79 | output: cstring 80 | ): cint {.cdecl, dynlib: DLLSSLName, importc: "PKCS5_PBKDF2_HMAC".} ## 81 | ## Documentation as per : https://www.openssl.org/docs/manmaster/man3/PKCS5_PBKDF2_HMAC.html 82 | ## PKCS5_PBKDF2_HMAC() derives a key from a password using a salt and iteration count. 83 | ## 84 | ## pass is the password used in the derivation of length passlen. pass is an optional 85 | ## parameter and can be NULL. 86 | ## If passlen is -1, then the function will calculate the length of pass using strlen(). 87 | ## 88 | ## salt is the salt used in the derivation of length saltlen. 89 | ## If the salt is NULL, then saltlen must be 0. 90 | ## The function will not attempt to calculate the length of the salt because it is not assumed to be NULL terminated. 91 | ## 92 | ## iter is the iteration count and its value should be greater than or equal to 1. 93 | ## Any iter less than 1 is treated as a single iteration. 94 | ## 95 | ## digest is the message digest function used in the derivation. 96 | ## 97 | ## The derived key will be written to out. 98 | ## The size of the out buffer is specified via keylen. 99 | 100 | 101 | proc hashPbkdf2*(password: Password, salt: seq[byte], iterations: int, digestFunction: EVP_MD): Hash {.gcsafe.} = 102 | ## Hashes the given password with a SHA256 digest and the PBKDF2 hashing function 103 | ## from openSSL. This will execute the PBKDF2. 104 | ## HMAC = Hash based message authentication code 105 | let hasTooManyIterations = iterations > cint.high 106 | if hasTooManyIterations: 107 | raise newException(ValueError, fmt"You can not have more iterations than a c integer can carry. Choose a number below {cint.high}") 108 | 109 | let hashLength: cint = EVP_MD_size_fixed(digestFunction) 110 | let output: string = newString(hashLength) 111 | let outputStartingpoint: cstring = cast[cstring](output[0].unsafeAddr) 112 | 113 | let hashOperationReturnCode = PKCS5_PBKDF2_HMAC( 114 | password.cstring, 115 | -1, 116 | ($salt).cstring, 117 | len(salt).cint, 118 | iterations.cint, 119 | digestFunction, 120 | hashLength, 121 | outputStartingpoint 122 | ) 123 | 124 | let wasHashSuccessful = hashOperationReturnCode == 1 125 | doAssert wasHashSuccessful 126 | 127 | result = cast[Hash](output) -------------------------------------------------------------------------------- /src/nimword/private/types.nim: -------------------------------------------------------------------------------- 1 | type Password* = distinct string ## A special type for plain-text password string to prevent performing normal string operations with them. They are security critical and should not be accidentally logged or the like. 2 | 3 | converter toPassword*(str: string): Password = str.Password ## Converter that implicitly converts any string to a `Password` type. Makes it easier to use procs with strings. 4 | 5 | type Hash* = seq[byte] ## A convenience type to better express hashes. -------------------------------------------------------------------------------- /tests/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") -------------------------------------------------------------------------------- /tests/t_argon2.nim: -------------------------------------------------------------------------------- 1 | import std/[strformat, osproc, strutils] 2 | import unittest 3 | import libsodium/[sodium_sizes] 4 | import nimword/argon2 5 | import nimword/private/base64_utils 6 | 7 | const password = "lala" 8 | let saltStr = "1234567812345678" 9 | let salt: seq[byte] = saltStr.toBytes() 10 | const hashLength = 32 11 | let iterations = 1 12 | 13 | let memoryLimitBytes = crypto_pwhash_memlimit_moderate().int 14 | let memoryLimitInKiB = (memoryLimitBytes / 1024).int 15 | 16 | suite "nimword-basics": 17 | test """ 18 | Given a password and its hash 19 | When calculating the hash with hashPassword using a different salt 20 | It should produce an different hash from the initial one 21 | """: 22 | # Given 23 | let differentSalt: seq[byte] = "1234123412341234".toBytes() 24 | let initialHash = hashPassword( 25 | password, 26 | salt, 27 | iterations, 28 | hashLength, 29 | algorithm = phaArgon2id13, 30 | memoryLimitKibiBytes = memoryLimitInKiB 31 | ) 32 | 33 | # When 34 | let differentHash = hashPassword( 35 | password, 36 | differentSalt, 37 | iterations, 38 | hashLength, 39 | algorithm = phaArgon2id13, 40 | memoryLimitKibiBytes = memoryLimitInKiB 41 | ) 42 | 43 | # Then 44 | check initialHash != differentHash 45 | 46 | 47 | test """ 48 | Given a password and its hash 49 | When calculating the hash with hashPassword using a different number of iterations 50 | It should produce an different hash from the initial one 51 | """: 52 | # Given 53 | let differentIterations = iterations + 1 54 | let initialHash = hashPassword( 55 | password, 56 | salt, 57 | iterations, 58 | hashLength, 59 | algorithm = phaArgon2id13, 60 | memoryLimitKibiBytes = memoryLimitInKiB 61 | ) 62 | 63 | # When 64 | let differentHash = hashPassword( 65 | password, 66 | salt, 67 | differentIterations, 68 | hashLength, 69 | algorithm = phaArgon2id13, 70 | memoryLimitKibiBytes = memoryLimitInKiB 71 | ) 72 | 73 | # Then 74 | check initialHash != differentHash 75 | 76 | 77 | test """ 78 | Given a password, its hash and all parameters used to calculate the hash, 79 | When encoding the hash with `encodeHash` 80 | Then it should produce a string that is identical to one produced by `hashEncodePassword` 81 | """: 82 | # Given 83 | let expectedEncodedHash: string = hashEncodePassword(password, iterations, phaArgon2id13, memoryLimitInKiB) 84 | let encodedSalt: string = expectedEncodedHash.split("$")[^2] 85 | let salt: seq[byte] = encodedSalt.decode() 86 | let hash: Hash = hashPassword( 87 | password, 88 | salt, 89 | iterations, 90 | hashLength, 91 | algorithm = phaArgon2id13, 92 | memoryLimitKibiBytes = memoryLimitInKiB 93 | ) 94 | 95 | # When 96 | let encodedHash: string = encodeHash( 97 | hash, 98 | salt, 99 | iterations, 100 | phaArgon2id13, 101 | memoryLimitKibiBytes = memoryLimitInKiB 102 | ) 103 | 104 | # Then 105 | check expectedEncodedHash == encodedHash 106 | 107 | 108 | test """ 109 | Given a password and its encoded hash 110 | When verifying that the password can be turned into the encoded hash with "isValidPassword" 111 | Then return true 112 | """: 113 | # Given 114 | let encodedHash = hashEncodePassword(password, iterations, phaArgon2id13, memoryLimitInKiB) 115 | 116 | # When 117 | let isValid = password.isValidPassword(encodedHash) 118 | 119 | # Then 120 | check isValid == true 121 | 122 | 123 | test """ 124 | Given a password and an encoded hash from a different password 125 | When verifying that the password can be turned into the encoded hash with "isValidPassword" 126 | Then return false 127 | """: 128 | # Given 129 | let encodedHash = hashEncodePassword(password, iterations, phaArgon2id13, memoryLimitInKiB) 130 | let differentPassword = fmt"{password}andmore" 131 | 132 | # When 133 | let isValid = differentPassword.isValidPassword(encodedHash) 134 | 135 | # Then 136 | check isValid == false 137 | 138 | 139 | suite "Argon2 specific": 140 | test """ 141 | Given a password, a salt and a number of iterations, 142 | When calculating a hash with hashPassword 143 | It should produce an identical hash to calling the argon cli command 144 | `echo -n lala | argon2 1234567812345678 -v 13 -id -p 1 -k 262144.0 -t 3 -l 32 -e` 145 | """: 146 | # Given 147 | let argonCommand = fmt"echo -n {password} | argon2 {saltStr} -v 13 -id -p 1 -k {memoryLimitInKiB} -t {iterations} -l {hashLength} -e" 148 | let cliResult = execCmdEx(argonCommand) 149 | doAssert cliResult.exitCode == 0, "Failed to compute cli-hash. Problem was: " & cliResult.output 150 | let cliEncodedHash = cliResult.output 151 | 152 | echo cliEncodedHash.split('$') # TODO: Remove 153 | var cliHash: Hash = cliEncodedHash.split('$')[^1].decode() 154 | 155 | # When 156 | let libHash: Hash = hashPassword( 157 | password, 158 | salt, 159 | iterations, 160 | hashLength, 161 | algorithm = phaArgon2id13, 162 | memoryLimitKibiBytes = memoryLimitInKiB 163 | ) 164 | 165 | # Then 166 | check cliHash == libHash 167 | -------------------------------------------------------------------------------- /tests/t_nimword.nim: -------------------------------------------------------------------------------- 1 | import unittest 2 | import std/[strformat] 3 | import nimword 4 | import nimword/private/base64_utils 5 | 6 | const password = "lala" 7 | const hashLength = 32 8 | let salt = "1234567812345678".toBytes() 9 | let iterations = 3 10 | 11 | template testSuite(algorithm: NimwordHashingAlgorithm) = 12 | suite "Nimword isValidPassword and hashEncodePassword - " & $algorithm : 13 | test """ 14 | Given a password and its encoded hash 15 | When verifying that the password can be turned into the encoded hash with "isValidPassword" 16 | Then return true 17 | """: 18 | for algorithm in NimwordHashingAlgorithm: 19 | # Given 20 | let encodedHash = hashEncodePassword(password, iterations, algorithm) 21 | 22 | # When 23 | let isValid = password.isValidPassword(encodedHash) 24 | 25 | # Then 26 | check isValid == true 27 | 28 | 29 | test """ 30 | Given a password and an encoded hash from a different password 31 | When verifying that the password can be turned into the encoded hash with "isValidPassword" 32 | Then return false 33 | """: 34 | for algorithm in NimwordHashingAlgorithm: 35 | # Given 36 | let encodedHash = hashEncodePassword(password, iterations, algorithm) 37 | let differentPassword = fmt"{password}andmore" 38 | # When 39 | let isValid = differentPassword.isValidPassword(encodedHash) 40 | 41 | # Then 42 | check isValid == false 43 | 44 | for algorithm in NimwordHashingAlgorithm: 45 | testSuite(algorithm) -------------------------------------------------------------------------------- /tests/t_pbkdf2_sha256.nim: -------------------------------------------------------------------------------- 1 | import unittest 2 | import std/[strformat, strutils] 3 | import nimword/pbkdf2_sha256 4 | import nimword/private/base64_utils 5 | 6 | const password = "lala" 7 | const hashLength = 32 8 | let salt = "1234567812345678".toBytes() 9 | let iterations = 1000 10 | 11 | 12 | suite "PBKDF2-HMAC-SHA256": 13 | test """ 14 | Given a password and its hash 15 | When calculating the hash with hashPassword using a different salt 16 | It should produce an different hash from the initial one 17 | """: 18 | # Given 19 | let differentSalt = "1234123412341234".toBytes() 20 | let initialHash = hashPassword( 21 | password, 22 | salt, 23 | iterations 24 | ) 25 | 26 | # When 27 | let differentHash = hashPassword( 28 | password, 29 | differentSalt, 30 | iterations 31 | ) 32 | 33 | # Then 34 | check initialHash != differentHash 35 | 36 | 37 | test """ 38 | Given a password and its hash 39 | When calculating the hash with hashPassword using a different number of iterations 40 | It should produce an different hash from the initial one 41 | """: 42 | # Given 43 | let differentIterations = iterations - 1 44 | let initialHash = hashPassword( 45 | password, 46 | salt, 47 | iterations 48 | ) 49 | 50 | # When 51 | let differentHash = hashPassword( 52 | password, 53 | salt, 54 | differentIterations 55 | ) 56 | 57 | # Then 58 | check initialHash != differentHash 59 | 60 | 61 | test """ 62 | Given a password, its hash and all parameters used to calculate the hash, 63 | When encoding the hash with `encodeHash` 64 | Then it should produce a string that is identical to one produced by `hashEncodePassword` 65 | """: 66 | # Given 67 | let expectedEncodedHash: string = hashEncodePassword(password, iterations) 68 | let encodedSalt: string = expectedEncodedHash.split("$")[^2] 69 | let salt: seq[byte] = encodedSalt.decode() 70 | let hash: Hash = hashPassword( 71 | password, 72 | salt, 73 | iterations 74 | ) 75 | 76 | # When 77 | let encodedHash: string = encodeHash( 78 | hash, 79 | salt, 80 | iterations, 81 | ) 82 | 83 | # Then 84 | check expectedEncodedHash == encodedHash 85 | 86 | 87 | test """ 88 | Given a password and its encoded hash 89 | When verifying that the password can be turned into the encoded hash with "isValidPassword" 90 | Then return true 91 | """: 92 | # Given 93 | let encodedHash = hashEncodePassword(password, iterations) 94 | 95 | # When 96 | let isValid = password.isValidPassword(encodedHash) 97 | 98 | # Then 99 | check isValid == true 100 | 101 | 102 | test """ 103 | Given a password and an encoded hash from a different password 104 | When verifying that the password can be turned into the encoded hash with "isValidPassword" 105 | Then return false 106 | """: 107 | # Given 108 | let encodedHash = hashEncodePassword(password, iterations) 109 | let differentPassword = fmt"{password}andmore" 110 | # When 111 | let isValid = differentPassword.isValidPassword(encodedHash) 112 | 113 | # Then 114 | check isValid == false 115 | -------------------------------------------------------------------------------- /tests/t_pbkdf2_sha512.nim: -------------------------------------------------------------------------------- 1 | import unittest 2 | import std/[strformat, strutils] 3 | import nimword/pbkdf2_sha512 4 | import nimword/private/base64_utils 5 | 6 | const password = "lala" 7 | const hashLength = 32 8 | let salt = "1234567812345678".toBytes() 9 | let iterations = 1000 10 | 11 | 12 | suite "PBKDF2-HMAC-SHA512": 13 | test """ 14 | Given a password and its hash 15 | When calculating the hash with hashPassword using a different salt 16 | It should produce an different hash from the initial one 17 | """: 18 | # Given 19 | let differentSalt: seq[byte] = "1234123412341234".toBytes() 20 | let initialHash = hashPassword( 21 | password, 22 | salt, 23 | iterations 24 | ) 25 | 26 | # When 27 | let differentHash = hashPassword( 28 | password, 29 | differentSalt, 30 | iterations 31 | ) 32 | 33 | # Then 34 | check initialHash != differentHash 35 | 36 | 37 | test """ 38 | Given a password and its hash 39 | When calculating the hash with hashPassword using a different number of iterations 40 | It should produce an different hash from the initial one 41 | """: 42 | # Given 43 | let differentIterations = iterations - 1 44 | let initialHash = hashPassword( 45 | password, 46 | salt, 47 | iterations 48 | ) 49 | 50 | # When 51 | let differentHash = hashPassword( 52 | password, 53 | salt, 54 | differentIterations 55 | ) 56 | 57 | # Then 58 | check initialHash != differentHash 59 | 60 | 61 | test """ 62 | Given a password, its hash and all parameters used to calculate the hash, 63 | When encoding the hash with `encodeHash` 64 | Then it should produce a string that is identical to one produced by `hashEncodePassword` 65 | """: 66 | # Given 67 | let expectedEncodedHash: string = hashEncodePassword(password, iterations) 68 | let encodedSalt: string = expectedEncodedHash.split("$")[^2] 69 | let salt: seq[byte] = encodedSalt.decode() 70 | let hash: Hash = hashPassword( 71 | password, 72 | salt, 73 | iterations 74 | ) 75 | 76 | # When 77 | let encodedHash: string = encodeHash( 78 | hash, 79 | salt, 80 | iterations, 81 | ) 82 | 83 | # Then 84 | check expectedEncodedHash == encodedHash 85 | 86 | 87 | test """ 88 | Given a password and its encoded hash 89 | When verifying that the password can be turned into the encoded hash with "isValidPassword" 90 | Then return true 91 | """: 92 | # Given 93 | let encodedHash = hashEncodePassword(password, iterations) 94 | 95 | # When 96 | let isValid = password.isValidPassword(encodedHash) 97 | 98 | # Then 99 | check isValid == true 100 | 101 | 102 | test """ 103 | Given a password and an encoded hash from a different password 104 | When verifying that the password can be turned into the encoded hash with "isValidPassword" 105 | Then return false 106 | """: 107 | # Given 108 | let encodedHash = hashEncodePassword(password, iterations) 109 | let differentPassword = fmt"{password}andmore" 110 | # When 111 | let isValid = differentPassword.isValidPassword(encodedHash) 112 | 113 | # Then 114 | check isValid == false 115 | --------------------------------------------------------------------------------