CVE-2019-20138 (published 2019-12-30T14:15Z)

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 v2 base score: 5.0 (AV:N/AC:L/Au:N/C:P/I:N/A:N)
CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-327
cwe_name: Use of a Broken or Risky Cryptographic Algorithm
description: The product uses a broken or risky cryptographic algorithm or protocol.
extended_description: 
	   Cryptographic algorithms are the methods by which data is scrambled to prevent observation or influence by unauthorized actors. Insecure cryptography can be exploited to expose sensitive information, modify data in unexpected ways, spoof identities of other users or devices, or other impacts.
           It is very difficult to produce a secure algorithm, and even high-profile algorithms by accomplished cryptographic experts have been broken. Well-known techniques exist to break or weaken various kinds of cryptography. Accordingly, there are a small number of well-understood and heavily studied algorithms that should be used by most products. Using a non-standard or known-insecure algorithm is dangerous because a determined adversary may be able to break the algorithm and compromise whatever data has been protected.
           Since the state of cryptography advances so rapidly, it is common for an algorithm to be considered "unsafe" even if it was once thought to be strong. This can happen when new attacks are discovered, or if computing power increases so much that the cryptographic algorithm no longer provides the amount of protection that was originally thought.
	   For a number of reasons, this weakness is even more challenging to manage with hardware deployment of cryptographic algorithms as opposed to software implementation. First, if a flaw is discovered with hardware-implemented cryptography, the flaw cannot be fixed in most cases without a recall of the product, because hardware is not easily replaceable like software. Second, because the hardware product is expected to work for years, the adversary's computing power will only increase over time.
	 
         
cwe_id: CWE-916
cwe_name: Use of Password Hash With Insufficient Computational Effort
description: The product generates a hash for a password, but it uses a scheme that does not provide a sufficient level of computational effort that would make password cracking attacks infeasible or expensive.
extended_description: 
            Many password storage mechanisms compute a hash and store the hash, instead of storing the original password in plaintext. In this design, authentication involves accepting an incoming password, computing its hash, and comparing it to the stored hash.
            Many hash algorithms are designed to execute quickly with minimal overhead, even cryptographic hashes. However, this efficiency is a problem for password storage, because it can reduce an attacker's workload for brute-force password cracking. If an attacker can obtain the hashes through some other method (such as SQL injection on a database that stores hashes), then the attacker can store the hashes offline and use various techniques to crack the passwords by computing hashes efficiently. Without a built-in workload, modern attacks can compute large numbers of hashes, or even exhaust the entire space of all possible passwords, within a very short amount of time, using massively-parallel computing (such as cloud computing) and GPU, ASIC, or FPGA hardware. In such a scenario, an efficient hash algorithm helps the attacker.
            There are several properties of a hash scheme that are relevant to its strength against an offline, massively-parallel attack:
               
                  The amount of CPU time required to compute the hash ("stretching")
                  The amount of memory required to compute the hash ("memory-hard" operations)
                  Including a random value, along with the password, as input to the hash computation ("salting")
                  Given a hash, there is no known way of determining an input (e.g., a password) that produces this hash value, other than by guessing possible inputs ("one-way" hashing)
                  Relative to the number of all possible hashes that can be generated by the scheme, there is a low likelihood of producing the same hash for multiple different inputs ("collision resistance")
               
            Note that the security requirements for the product may vary depending on the environment and the value of the passwords. Different schemes might not provide all of these properties, yet may still provide sufficient security for the environment. Conversely, a solution might be very strong in preserving one property, which still being very weak for an attack against another property, or it might not be able to significantly reduce the efficiency of a massively-parallel attack.
         
         

Fix information:
repo_url: https://github.com/FedericoCeratto/nim-httpauth
commit_details: repo_url: https://github.com/FedericoCeratto/nim-httpauth
author: Federico Ceratto
author_date: 2019-12-27 23:56:28+00:00
msg: 0.2.0 implement libsodium crypto_pwhash_str

Fix #23
Add tests, improve docs
repository: repo_url: https://github.com/FedericoCeratto/nim-httpauth
repo_name: FedericoCeratto/nim-httpauth
description: HTTP Authentication library for Nim
repo_language: Nim
owner: FedericoCeratto
file_changes: filename: README.adoc
change_type: ModificationType.MODIFY
diff: @@ -6,6 +6,8 @@ image:https://img.shields.io/badge/status-stable-green.svg[badge]
 image:https://img.shields.io/github/tag/FedericoCeratto/nim-httpauth.svg[tags]
 image:https://img.shields.io/badge/License-LGPL%20v3-blue.svg[License]
 
+WARNING: Version 0.2.0 is not backward compatible due previously weak password hashing
+
 .Features:
 [none]
 - [x] Encrypted+signed session cookies
@@ -21,10 +23,6 @@ image:https://img.shields.io/badge/License-LGPL%20v3-blue.svg[License]
 - [ ] Async JSON backend
 - [ ] Async backends (when async libraries will be available)
 
-.Not planned:
-- bcrypt (obsolete)
-- pbkdf2 (obsolete)
-
 Install the dependencies:
 [source,bash]
 ----
@@ -122,12 +120,11 @@ var auth = newHTTPAuth("localhost", backend)
 
 Cookie-based sessions are encrypted and signed to protect from cookie exfiltration from the browser.
 Registration and password reset tokens are signed and have a timeout.
-Passwords are stored using Scrypt or Argon2.
+Passwords are hashed using libsodium default algorithm.
+The hash is regenerated automatically on login if needed.
 
 WARNING: Remember to filter user input to prevent XSS, SQL injections and similar attacks.
 
-WARNING: Scrypt and Argon2 will not be robust enough forever. You'll have to update the library and rehash passwords.
-
 ==== Usage example:
 [source,nim]
 ----

programming_language: Markdown
filename: httpauth.nim
change_type: ModificationType.MODIFY
diff: @@ -55,7 +55,6 @@ type
     Argon2, Scrypt
 
   HTTPAuth* = object of RootObj
-    preferred_hashing_algorithm*: HashingAlgorithm
     password_reset_timeout*: int
     domain*: string
     cookie_name*: string
@@ -101,37 +100,15 @@ proc safe_decode(i: string): string =
 
 # Crypto
 
-const scrypt_salt_size = 32
+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 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 password_needs_rehashing*(password: string): bool =
+  crypto_pwhash_str_needs_rehash(password) != 0
 
-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)
 
 # # Cookies and session
 
@@ -211,22 +188,9 @@ proc is_user_anonymous*(self: HTTPAuth): bool =
 
 # # Procs that can be run by unauthenticated users
 
-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")
+proc verify_password(password, pwhash: string): bool =
+  ## Verify password hashed by libsodium
+  return crypto_pwhash_str_verify(pwhash, password)
 
 proc login*(self: HTTPAuth, username, password: string) =
   ## Check login credentials and set a cookie on success
@@ -240,13 +204,14 @@ proc login*(self: HTTPAuth, username, password: string) =
   # FIXME prevent timing attacks
 
   assert user.hash != ""
-  let authenticated = verify_password(
-    username,
-    password,
-    user.hash,
-  )
+  let authenticated = verify_password(password, user.hash)
   if not authenticated:
     raise newException(LoginError, "Failed login")
+
+  if password_needs_rehashing(user.hash):
+    user.hash = password_pwhash_str(password)
+    self.backend.set_user(user)
+
   self.store_session(username)
 
   # if login_time
@@ -310,12 +275,12 @@ proc register*(self: HTTPAuth, username, password, email_addr: string, role="use
   asyncCheck self.mailer.send_email(email_addr, subject, email_text)
 
   # store pending registration
-  let user_pwd_hash = self.hash(username, password, self.preferred_hashing_algorithm)
+  let pwhash = password_pwhash_str(password)
 
   self.backend.set_pending_registration(registration_code, PendingRegistration(
     username: username,
     role: role,
-    hash: user_pwd_hash,
+    hash: pwhash,
     email_addr: email_addr,
     description: description,
     creation_date: creation_date,
@@ -426,7 +391,7 @@ proc reset_password*(self: HTTPAuth, reset_code, password: string) =
   var user = self.backend.get_user(username)
   if user.email_addr != email_addr:
     raise newException(AuthError, "Incorrect email address in reset code")
-  user.hash = self.hash(username, password, self.preferred_hashing_algorithm)
+  user.hash = password_pwhash_str(password)
   self.backend.set_user(user)
   self.backend.save_users()
 
@@ -469,12 +434,12 @@ proc create_user*(self: HTTPAuth, username, password: string, role = "user",
   except:
     raise newException(AuthError, "Nonexistent user role.")
 
-  let h = self.hash(username, password, self.preferred_hashing_algorithm)
+  let pwhash = password_pwhash_str(password)
   let tstamp = getTime().getGMTime
   self.backend.set_user(User(
     username: username,
     role: role,
-    hash: h,
+    hash: pwhash,
     email_addr: email_addr,
     description: description,
     creation_date: tstamp,
@@ -539,13 +504,11 @@ proc delete_role*(self: HTTPAuth, role: string) =
 const one_day = 24 * 3600
 
 proc newHTTPAuth*(domain: string, backend: HTTPAuthBackend, cookie_name="", cookie_domain="",
-    preferred_hashing_algorithm = HashingAlgorithm.Scrypt,
     password_reset_timeout=one_day, session_key="", https_only_cookies=true): HTTPAuth =
   ## Initialize HTTPAuth
   result = HTTPAuth(domain: domain, backend: backend,
     password_reset_timeout: password_reset_timeout,
     https_only_cookies: https_only_cookies,
-    preferred_hashing_algorithm: preferred_hashing_algorithm,
   )
   result.mailer = newMailer() #FIXME: pass arguments
   assert result.backend != nil
@@ -590,13 +553,12 @@ proc initialize_admin_user*(self: HTTPAuth, username="admin", password="", role=
     level: admin_level
   ))
   self.backend.save_roles()
-  let h = self.hash(username, password, self.preferred_hashing_algorithm)
-  # FIXME assert h.len == 52, $h.len
+  let pwhash = password_pwhash_str(password)
   let tstamp = getTime().getGMTime
   self.backend.set_user(User(
     username: username,
     role: role,
-    hash: h,
+    hash: pwhash,
     email_addr: email_addr,
     description: description,
     creation_date: tstamp,
@@ -641,7 +603,7 @@ proc require*(self: HTTPAuth, username="", role="", fixed_role=false) =
 proc update_user_password*(self: HTTPAuth, username, password: string) =
   ## Update user password
   var user = self.backend.get_user(username)
-  user.hash = self.hash(username, password, self.preferred_hashing_algorithm)
+  user.hash = password_pwhash_str(password)
   self.backend.set_user(user)
   self.backend.save_users()
 

programming_language: Python
filename: httpauth.nimble
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,6 @@
 # Package
 
-version       = "0.1.3"
+version       = "0.2.0"
 author        = "Federico Ceratto"
 description   = "HTTP Authentication and Authorization"
 license       = "LGPLv3"

programming_language: Ruby
filename: makefile
change_type: ModificationType.MODIFY
diff: @@ -67,11 +67,14 @@ functional_mongodb:
 	mongo httpauth_test --eval 'db.users.drop()'
 	./tests/functional mongodb://127.0.0.1/httpauth_test
 
-functional: build_functional_tests functional_sqlite functional_mysql functional_etcd build_functional_tests_mongodb functional_mongodb
+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
 # FIXME MySQL Etcd Redis
-circleci: build_functional_tests functional_sqlite build_functional_tests_mongodb functional_mongodb
+circleci: unit build_functional_tests functional_sqlite build_functional_tests_mongodb functional_mongodb
 
 start_databases:
 	sudo systemctl start etcd.service

programming_language: Shell
filename: nim.cfg
change_type: ModificationType.MODIFY
diff: @@ -1 +1 @@
--d:ssl
+-d:ssl --hints:off

programming_language: Batchfile
filename: unit.nim
change_type: ModificationType.ADD
diff: @@ -0,0 +1,22 @@
+
+## 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

programming_language: Python