Skip to content
Permalink
Browse files Browse the repository at this point in the history
0.2.0 implement libsodium crypto_pwhash_str
Fix #23
Add tests, improve docs
  • Loading branch information
FedericoCeratto committed Dec 28, 2019
1 parent 2e2b114 commit 15fd068
Show file tree
Hide file tree
Showing 6 changed files with 57 additions and 73 deletions.
11 changes: 4 additions & 7 deletions README.adoc
Expand Up @@ -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
Expand All @@ -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]
----
Expand Down Expand Up @@ -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]
----
Expand Down
86 changes: 24 additions & 62 deletions httpauth.nim
Expand Up @@ -55,7 +55,6 @@ type
Argon2, Scrypt

HTTPAuth* = object of RootObj
preferred_hashing_algorithm*: HashingAlgorithm
password_reset_timeout*: int
domain*: string
cookie_name*: string
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()

Expand Down
2 changes: 1 addition & 1 deletion httpauth.nimble
@@ -1,6 +1,6 @@
# Package

version = "0.1.3"
version = "0.2.0"
author = "Federico Ceratto"
description = "HTTP Authentication and Authorization"
license = "LGPLv3"
Expand Down
7 changes: 5 additions & 2 deletions makefile
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion nim.cfg
@@ -1 +1 @@
-d:ssl
-d:ssl --hints:off
22 changes: 22 additions & 0 deletions tests/unit.nim
@@ -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

0 comments on commit 15fd068

Please sign in to comment.