Browse Source

Use pts_lbsearch to check for breached passwords

This replaces the current method of using a Bloom filter in Redis to
check for breached passwords with searching the text file directly using
pts_lbsearch (https://github.com/pts/pts-line-bisect/).

I'm not removing the Redis-based method yet because I want to test the
performance of this first, but this is *far* simpler and doesn't have
the possibility for false positives like the Bloom filter does.
merge-requests/126/merge
Deimos 4 years ago
parent
commit
26b1d4dd9b
  1. 12
      salt/salt/pts-lbsearch.sls
  2. 1
      salt/salt/top.sls
  3. 9
      tildes/production.ini.example
  4. 1
      tildes/tildes/__init__.py
  5. 34
      tildes/tildes/lib/password.py
  6. 30
      tildes/tildes/settings.py

12
salt/salt/pts-lbsearch.sls

@ -0,0 +1,12 @@
compile-pts-lbsearch:
file.managed:
- name: /tmp/pts_lbsearch.c
- source:
- https://raw.githubusercontent.com/pts/pts-line-bisect/2ecd9f59246cfa28cb1aeac7cd8d98a8eea2914f/pts_lbsearch.c
- source_hash: sha256=ef79efc2f1ecde504b6074f9c89bdc71259a833fa2a2dda4538ed5ea3e04aea1
- creates: /usr/local/bin/pts_lbsearch
cmd.run:
- cwd: /tmp/
# compilation command taken from the top of the source file
- name: gcc -ansi -W -Wall -Wextra -Werror=missing-declarations -s -O2 -DNDEBUG -o /usr/local/bin/pts_lbsearch pts_lbsearch.c
- creates: /usr/local/bin/pts_lbsearch

1
salt/salt/top.sls

@ -22,6 +22,7 @@ base:
- tildes-wiki - tildes-wiki
- boussole - boussole
- webassets - webassets
- pts-lbsearch
- cronjobs - cronjobs
- final-setup # keep this state file last - final-setup # keep this state file last
'dev': 'dev':

9
tildes/production.ini.example

@ -27,6 +27,15 @@ stripe.recurring_donation_product_id = prod_ProductID
tildes.default_user_comment_label_weight = 1.0 tildes.default_user_comment_label_weight = 1.0
# Path to the file to use to check for passwords that have been in data breaches, which
# users will be prevented from using as their password. It's recommended to use the
# "Pwned Passwords" list downloaded from https://haveibeenpwned.com/passwords (must be
# the SHA-1 format, "ordered by hash" one), but you can use any file with a compatible
# format: each line starting with a single uppercase SHA-1 hash of a password to block,
# with the entire file sorted in lexographical order.
# Leave this line commented out to allow all passwords.
# tildes.breached_passwords_hash_file_path = /opt/tildes/pwned-passwords-sha1-ordered-by-hash-v6.txt
webassets.auto_build = false webassets.auto_build = false
webassets.base_dir = %(here)s/static webassets.base_dir = %(here)s/static
webassets.base_url = / webassets.base_url = /

1
tildes/tildes/__init__.py

@ -28,6 +28,7 @@ def main(global_config: Dict[str, str], **settings: str) -> PrefixMiddleware:
config.include("tildes.json") config.include("tildes.json")
config.include("tildes.request_methods") config.include("tildes.request_methods")
config.include("tildes.routes") config.include("tildes.routes")
config.include("tildes.settings")
config.include("tildes.tweens") config.include("tildes.tweens")
config.add_webasset("javascript", Bundle(output="js/tildes.js")) config.add_webasset("javascript", Bundle(output="js/tildes.js"))

34
tildes/tildes/lib/password.py

@ -3,10 +3,10 @@
"""Functions/constants related to user passwords.""" """Functions/constants related to user passwords."""
import subprocess
from hashlib import sha1 from hashlib import sha1
from redis import ConnectionError, Redis, ResponseError # noqa
from tildes import settings
from tildes.metrics import summary_timer from tildes.metrics import summary_timer
@ -19,16 +19,28 @@ BREACHED_PASSWORDS_BF_KEY = "breached_passwords_bloom"
@summary_timer("breached_password_check") @summary_timer("breached_password_check")
def is_breached_password(password: str) -> bool: def is_breached_password(password: str) -> bool:
"""Return whether the password is in the breached-passwords list."""
redis = Redis(unix_socket_path=BREACHED_PASSWORDS_REDIS_SOCKET)
"""Return whether the password is in the breached-passwords list.
Note: this function uses a binary-search utility on the breached-passwords file, so
the file's format is not flexible. Each line of the file must begin with a single
uppercase SHA-1 hash corresponding to a password that should be blocked, and the
lines must be sorted in lexographical order.
This is specifically intended for use with a "Pwned Passwords" list downloaded from
https://haveibeenpwned.com/passwords (SHA-1 format, "ordered by hash"), but any
other file with a compatible format will also work.
"""
try:
hash_list_path = settings.INI_FILE_SETTINGS["breached_passwords_hash_file_path"]
except KeyError:
return False
hashed = sha1(password.encode("utf-8")).hexdigest()
hashed = sha1(password.encode("utf-8")).hexdigest().upper()
# call pts_lbsearch in "prefix search" mode - exit code 0 means it found a match
try: try:
return bool(
redis.execute_command("BF.EXISTS", BREACHED_PASSWORDS_BF_KEY, hashed)
)
except (ConnectionError, ResponseError):
# server isn't running, bloom filter doesn't exist or the key is a different
# data type
subprocess.run(["pts_lbsearch", "-p", hash_list_path, hashed], check=True)
except subprocess.CalledProcessError:
return False return False
return True

30
tildes/tildes/settings.py

@ -0,0 +1,30 @@
# Copyright (c) 2020 Tildes contributors <code@tildes.net>
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Global-like settings for the application.
This module should always be imported as a whole ("from tildes import settings"), not
importing individual names, since that will cause re-initialization.
Currently, this module only contains a dict with some of the settings defined in the
INI file, specifically ones with the "tildes." prefix. The values in this dict are
initialized during app startup.
Important note: this module may be a terrible idea and I may regret this.
"""
from pyramid.config import Configurator
INI_FILE_SETTINGS = {}
def includeme(config: Configurator) -> None:
"""Initialize ini_file_settings with all prefixed settings from the INI file."""
global INI_FILE_SETTINGS # pylint: disable=global-statement
setting_prefix = "tildes."
INI_FILE_SETTINGS = {
setting[len(setting_prefix) :]: value
for setting, value in config.get_settings().items()
if setting.startswith(setting_prefix)
}
Loading…
Cancel
Save