CVE-2019-20138 (Other, published 2019-12-30T14:15Z)
Description: value: The HTTP Authentication library before 2019-12-27 for Nim has weak password hashing because the default algorithm for libsodium's crypto_pwhash_str is not used.
Severity: MEDIUM (CVSS v3 base score: 7.5)
Weakness: CWE-327 \u2014 Use of a Broken or Risky Cryptographic Algorithm
The product uses a broken or risky cryptographic algorithm or protocol.
Repository: https://github.com/FedericoCeratto/nim-httpauth
Files changed:
README.adoc
httpauth.nim
httpauth.nimble
makefile
nim.cfg
tests/unit.nim
Commit message: 0.2.0 implement libsodium crypto_pwhash_str

Fix #23
Add tests, improve docs


Vulnerable code:
.Not planned:
- bcrypt (obsolete)
- pbkdf2 (obsolete)

Passwords are stored using Scrypt or Argon2.
WARNING: Scrypt and Argon2 will not be robust enough forever. You'll have to update the library and rehash passwords.

    preferred_hashing_algorithm*: HashingAlgorithm
const scrypt_salt_size = 32
proc hash_scrypt(username, password: string, salt=""): string =
  ## Scrypt hashing. Return b64 encoded('s' + salt + hash)
  let salt =
    if salt == "":
      randombytes(scrypt_salt_size)
    else:
      salt
  assert salt.len == scrypt_salt_size

  let cleartext = "$#\0$#" % [username, password]
  # FIXME let h = scrypt.hash(cleartext, salt)
  let h = cleartext
  let hashed = "s" & salt & h  # 's' for scrypt
  return safe_encode(hashed)  # FIXME encoding/decoding required?

proc hash_argon2(username, password: string, salt=""): string =
  ##
  ""
proc hash(self: HTTPAuth, username, pwd: string, algo:HashingAlgorithm, salt=""): string =
  ## Hash username and password, generating salt value if required
  assert algo == Scrypt

  case algo
  of HashingAlgorithm.Scrypt:
    return hash_scrypt(username, pwd, salt=salt)

  of HashingAlgorithm.Argon2:
    return hash_argon2(username, pwd, salt=salt)
proc verify_password(username, password, hash: string): bool =
  ## Verify username/password pair against a salted hash
  assert hash != ""
  let decoded = safe_decode(hash)
  case decoded[0]
  of 's':
    let salt = decoded[1..(scrypt_salt_size)]
    assert salt.len == scrypt_salt_size, $salt.len
    let newhash = hash_scrypt(username, password, salt=salt)
    let newhash2 = hash_scrypt(username, password, salt=salt)
    assert newhash == newhash2
    #FIXME time independent comparison?
    return newhash == hash

  else:
    raise newException(AuthError, "Unknown hashing algorithm")
  let authenticated = verify_password(
    username,
    password,
    user.hash,
  )
  let user_pwd_hash = self.hash(username, password, self.preferred_hashing_algorithm)
    hash: user_pwd_hash,
  user.hash = self.hash(username, password, self.preferred_hashing_algorithm)
  let h = self.hash(username, password, self.preferred_hashing_algorithm)
    hash: h,
    preferred_hashing_algorithm = HashingAlgorithm.Scrypt,
    preferred_hashing_algorithm: preferred_hashing_algorithm,
  let h = self.hash(username, password, self.preferred_hashing_algorithm)
  # FIXME assert h.len == 52, $h.len
    hash: h,
  user.hash = self.hash(username, password, self.preferred_hashing_algorithm)
version       = "0.1.3"
functional: build_functional_tests functional_sqlite functional_mysql functional_etcd build_functional_tests_mongodb functional_mongodb
circleci: build_functional_tests functional_sqlite build_functional_tests_mongodb functional_mongodb
-d:ssl

Fixed code:
WARNING: Version 0.2.0 is not backward compatible due previously weak password hashing

Passwords are hashed using libsodium default algorithm.
The hash is regenerated automatically on login if needed.
proc password_pwhash_str*(password: string): string =
  ## Hash password using libsodium `crypto_pwhash_str`
  ## using the recommended algorithm. The output is ASCII-only
  ## and database-safe.
  crypto_pwhash_str(password)
proc password_needs_rehashing*(password: string): bool =
  crypto_pwhash_str_needs_rehash(password) != 0
proc verify_password(password, pwhash: string): bool =
  ## Verify password hashed by libsodium
  return crypto_pwhash_str_verify(pwhash, password)
  let authenticated = verify_password(password, user.hash)

  if password_needs_rehashing(user.hash):
    user.hash = password_pwhash_str(password)
    self.backend.set_user(user)

  let pwhash = password_pwhash_str(password)
    hash: pwhash,
  user.hash = password_pwhash_str(password)
  let pwhash = password_pwhash_str(password)
    hash: pwhash,
  let pwhash = password_pwhash_str(password)
    hash: pwhash,
  user.hash = password_pwhash_str(password)
version       = "0.2.0"
unit:
	nim c -p=. -r tests/unit.nim

functional: unit build_functional_tests functional_sqlite functional_mysql functional_etcd build_functional_tests_mongodb functional_mongodb
circleci: unit build_functional_tests functional_sqlite build_functional_tests_mongodb functional_mongodb
-d:ssl --hints:off

## Nim HTTP Authentication and Authorization
## Functional tests. HTTP headers are mocked
## Copyright 2019 Federico Ceratto <federico.ceratto@gmail.com>
## Released under LGPLv3 License, see LICENSE file

import unittest,
  strutils

from libsodium/sodium import crypto_pwhash_str_verify

import httpauthpkg/base

import httpauth

suite "hashing":
  test "password_pwhash_str":
    const pwd = "Correct Horse Battery Staple"
    let h = password_pwhash_str(pwd)
    check crypto_pwhash_str_verify(h, pwd) == true
    check crypto_pwhash_str_verify(h, pwd & "!") == false
    check password_needs_rehashing(h) == false