Commit 26b1d4dd authored by Deimos's avatar Deimos

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 (

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.
parent a70cc614
- name: /tmp/pts_lbsearch.c
- source:
- source_hash: sha256=ef79efc2f1ecde504b6074f9c89bdc71259a833fa2a2dda4538ed5ea3e04aea1
- creates: /usr/local/bin/pts_lbsearch
- 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
......@@ -22,6 +22,7 @@ base:
- tildes-wiki
- boussole
- webassets
- pts-lbsearch
- cronjobs
- final-setup # keep this state file last
......@@ -27,6 +27,15 @@ stripe.recurring_donation_product_id = prod_ProductID
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 (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.base_dir = %(here)s/static
webassets.base_url = /
......@@ -28,6 +28,7 @@ def main(global_config: Dict[str, str], **settings: str) -> PrefixMiddleware:
config.add_webasset("javascript", Bundle(output="js/tildes.js"))
......@@ -3,10 +3,10 @@
"""Functions/constants related to user passwords."""
import subprocess
from hashlib import sha1
from redis import ConnectionError, Redis, ResponseError # noqa
from tildes import settings
from tildes.metrics import summary_timer
......@@ -19,16 +19,28 @@ BREACHED_PASSWORDS_BF_KEY = "breached_passwords_bloom"
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 (SHA-1 format, "ordered by hash"), but any
other file with a compatible format will also work.
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
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["pts_lbsearch", "-p", hash_list_path, hashed], check=True)
except subprocess.CalledProcessError:
return False
return True
# Copyright (c) 2020 Tildes contributors <[email protected]>
# 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
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."
setting[len(setting_prefix) :]: value
for setting, value in config.get_settings().items()
if setting.startswith(setting_prefix)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment