Skip to content

Commit

Permalink
Merge pull request #32 from splunk/Spans
Browse files Browse the repository at this point in the history
NEw hash functionality  replacing the slow bcrypt
  • Loading branch information
hagen-p authored Oct 15, 2024
2 parents 5aebfb5 + 9f6eccb commit 9173dbe
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 46 deletions.
4 changes: 2 additions & 2 deletions kubernetes-manifests/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,5 @@ metadata:
data:
USE_DEMO_DATA: "True"
DEMO_LOGIN_USERNAME: "testuser"
# All demo user accounts are hardcoded to use the login password 'bankofanthos'
DEMO_LOGIN_PASSWORD: "bankofanthos"
# All demo user accounts are hardcoded to use the login password 'bankofsplunk'
DEMO_LOGIN_PASSWORD: "bankofsplunk"
1 change: 1 addition & 0 deletions kubernetes-manifests/userservice.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,4 @@ spec:
- key: jwtRS256.key.pub
path: publickey
secretName: jwt-key

1 change: 1 addition & 0 deletions src/accounts/accounts-db/initdb/0-accounts-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ CREATE TABLE IF NOT EXISTS users (
accountid CHAR(10) PRIMARY KEY,
username VARCHAR(64) UNIQUE NOT NULL,
passhash BYTEA NOT NULL,
salt BYTEA NOT NULL,
firstname VARCHAR(64) NOT NULL,
lastname VARCHAR(64) NOT NULL,
birthday DATE NOT NULL,
Expand Down
36 changes: 21 additions & 15 deletions src/accounts/accounts-db/initdb/1-load-testdata.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,35 @@ if [ "$USE_DEMO_DATA" != "True" ]; then
exit 0
fi


# Expected environment variables
readonly ENV_VARS=(
"POSTGRES_DB"
"POSTGRES_USER"
"LOCAL_ROUTING_NUM"
)


# Function to add users with separate salt and passhash fields
add_user() {
# Usage: add_user "ACCOUNTID" "USERNAME" "FIRST_NAME"
# Usage: add_user "ACCOUNTID" "USERNAME" "FIRST_NAME"
echo "adding user: $2"
psql -X -v ON_ERROR_STOP=1 -v account="$1" -v username="$2" -v firstname="$3" -v passhash="$DEFAULT_PASSHASH" --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
INSERT INTO users VALUES (:'account', :'username', :'passhash', :'firstname', 'User', '2000-01-01', '-5', 'Bowling Green, New York City', 'NY', '10004', '111-22-3333') ON CONFLICT DO NOTHING;

# Split the DEFAULT_PASSHASH into salt and passhash
IFS='|' read -r salt passhash <<< "$DEFAULT_PASSHASH"

# Insert user data into the database
psql -X -v ON_ERROR_STOP=1 \
-v account="$1" \
-v username="$2" \
-v firstname="$3" \
-v passhash="\\x$passhash" \
-v salt="\\x$salt" \
--username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
INSERT INTO users (accountid, username, passhash, salt, firstname, lastname, birthday, timezone, address, state, zip, ssn)
VALUES (:'account', :'username', :'passhash', :'salt', :'firstname', 'User', '2000-01-01', '-5', 'Bowling Green, New York City', 'NY', '10004', '111-22-3333')
ON CONFLICT DO NOTHING;
EOSQL
}


add_external_account() {
# Usage: add_external_account "OWNER_USERNAME" "LABEL" "ACCOUNT" "ROUTING"
echo "user $1 adding contact: $2"
Expand All @@ -48,7 +59,6 @@ add_external_account() {
EOSQL
}


add_contact() {
# Usage: add_contact "OWNER_USERNAME" "CONTACT_LABEL" "CONTACT_ACCOUNT"
echo "user $1 adding external account: $2"
Expand All @@ -57,7 +67,6 @@ add_contact() {
EOSQL
}


# Load test data into the database
create_accounts() {
# Add demo users.
Expand Down Expand Up @@ -87,7 +96,6 @@ create_accounts() {
add_external_account "eve" "External Bank" "9099791699" "808889588"
}


main() {
# Check environment variables are set
for env_var in ${ENV_VARS[@]}; do
Expand All @@ -97,12 +105,10 @@ main() {
fi
done

# A password hash + salt for the demo password 'bankofanthos'
# Via Python3: bycrypt.hashpw('password'.encode('utf-8'), bcrypt.gensalt())
DEFAULT_PASSHASH='\x243262243132244c48334f54422e70653274596d6834534b756673727563564b3848774630494d2f34717044746868366e42352e744b575978314e61'

# A password hash + salt for the demo password 'bankofsplunk'
#DEFAULT_PASSHASH='47e6106bff23748cad32f10911a094e8|b4d9e941eaf9e1a9b5b5c4f57d22ddb8123b8a47467e52685dd4f06b4152398d'
DEFAULT_PASSHASH='0e0ab7fc8ad0f68d8c0a88da06a7e79e|5da0a1304c22bec69bdb74f66d590cd4a0d585063ecbd7d1120662ddf55c49c8'
create_accounts
}


main
main
13 changes: 8 additions & 5 deletions src/accounts/accounts-db/initdb/1-load-testdata.sql
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
*/


INSERT INTO users VALUES
('1011226111', 'testuser', '\x243262243132244c48334f54422e70653274596d6834534b756673727563564b3848774630494d2f34717044746868366e42352e744b575978314e61', 'Test', 'User', '2000-01-01', '-5', 'Bowling Green, New York City', 'NY', '10004', '111-22-3333'),
('1033623433', 'alice', '\x243262243132244c48334f54422e70653274596d6834534b756673727563564b3848774630494d2f34717044746868366e42352e744b575978314e61', 'Alice', 'User', '2000-01-01', '-5', 'Bowling Green, New York City', 'NY', '10004', '111-22-3333'),
('1055757655', 'bob', '\x243262243132244c48334f54422e70653274596d6834534b756673727563564b3848774630494d2f34717044746868366e42352e744b575978314e61', 'Bob', 'User', '2000-01-01', '-5', 'Bowling Green, New York City', 'NY', '10004', '111-22-3333'),
('1077441377', 'eve', '\x243262243132244c48334f54422e70653274596d6834534b756673727563564b3848774630494d2f34717044746868366e42352e744b575978314e61', 'Eve', 'User', '2000-01-01', '-5', 'Bowling Green, New York City', 'NY', '10004', '111-22-3333')
INSERT INTO users (accountid, username, passhash, salt, firstname, lastname, birthday, timezone, address, state, zip, ssn)
VALUES
('1011226111', 'testuser', '\x5da0a1304c22bec69bdb74f66d590cd4a0d585063ecbd7d1120662ddf55c49c8', '\x0e0ab7fc8ad0f68d8c0a88da06a7e79e', 'Test', 'User', '2000-01-01', '-5', 'Bowling Green, New York City', 'NY', '10004', '111-22-3333'),
('1033623433', 'alice', '\x5da0a1304c22bec69bdb74f66d590cd4a0d585063ecbd7d1120662ddf55c49c8', '\x0e0ab7fc8ad0f68d8c0a88da06a7e79e', 'Alice', 'User', '2000-01-01', '-5', 'Bowling Green, New York City', 'NY', '10004', '111-22-3333'),
('1055757655', 'bob', '\x5da0a1304c22bec69bdb74f66d590cd4a0d585063ecbd7d1120662ddf55c49c8', '\x0e0ab7fc8ad0f68d8c0a88da06a7e79e', 'Bob', 'User', '2000-01-01', '-5', 'Bowling Green, New York City', 'NY', '10004', '111-22-3333'),
('1077441377', 'eve', '\x5da0a1304c22bec69bdb74f66d590cd4a0d585063ecbd7d1120662ddf55c49c8', '\x0e0ab7fc8ad0f68d8c0a88da06a7e79e', 'Eve', 'User', '2000-01-01', '-5', 'Bowling Green, New York City', 'NY', '10004', '111-22-3333')
ON CONFLICT DO NOTHING;

INSERT INTO contacts VALUES
Expand All @@ -43,3 +44,5 @@ INSERT INTO contacts VALUES
('bob', 'External Bank', '9099791699', '808889588', 'true'),
('eve', 'External Bank', '9099791699', '808889588', 'true')
ON CONFLICT DO NOTHING;


3 changes: 2 additions & 1 deletion src/accounts/userservice/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ def __init__(self, uri, logger=logging):
MetaData(self.engine),
Column('accountid', String, primary_key=True),
Column('username', String, unique=True, nullable=False),
Column('passhash', LargeBinary, nullable=False),
Column('passhash', LargeBinary, nullable=False), # Passhash (binary)
Column('salt', LargeBinary, nullable=False), # Salt (binary)
Column('firstname', String, nullable=False),
Column('lastname', String, nullable=False),
Column('birthday', Date, nullable=False),
Expand Down
55 changes: 32 additions & 23 deletions src/accounts/userservice/userservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@
import sys
import re

import bcrypt
#import bcrypt
import jwt
from flask import Flask, jsonify, request
import bleach
import hashlib

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
Expand Down Expand Up @@ -91,13 +92,17 @@ def create_app():
@app.route('/version', methods=['GET'])
def version():
"""Service version endpoint"""
with tracer.start_as_current_span("version", kind=SpanKind.SERVER):
with tracer.start_as_current_span("version", kind=SpanKind.SERVER) as version_span:
public_key_bit_size = app.config.get('PUBLIC_KEY_BIT_SIZE', None)
version_span.set_attribute("public_key_bit_size", public_key_bit_size)
return app.config['VERSION'], 200

@app.route('/ready', methods=['GET'])
def readiness():
"""Readiness probe"""
with tracer.start_as_current_span("readiness", kind=SpanKind.SERVER):
with tracer.start_as_current_span("readiness", kind=SpanKind.SERVER) as ready_span:
public_key_bit_size = app.config.get('PUBLIC_KEY_BIT_SIZE', None)
ready_span.set_attribute("public_key_bit_size", public_key_bit_size)
return 'ok', 200

@app.route('/users', methods=['POST'])
Expand All @@ -122,13 +127,21 @@ def create_user():
raise NameError(f'user {req["username"]} already exists')
check_span.set_attribute("user_exists", False)

# Step 4: Create password hash with salt
# Step 4: Create password hash with PBKDF2
with tracer.start_as_current_span("hash_password") as hash_span:
app.logger.debug("Creating password hash.")
password = req['password']
salt = bcrypt.gensalt()
passhash = bcrypt.hashpw(password.encode('utf-8'), salt)

# We can generate a smaller salt (8 bytes vs 16 that is the default)
salt = os.urandom(16)

# Hash the password using PBKDF2 with SHA-256
hashed = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 10000)

# Mark password hashing as completed
hash_span.set_attribute("password_hashed", True)
public_key_bit_size = app.config.get('PUBLIC_KEY_BIT_SIZE', None)
hash_span.set_attribute("public_key_bit_size", public_key_bit_size)

# Step 5: Generate unique account ID
with tracer.start_as_current_span("generate_accountid") as account_span:
Expand All @@ -139,7 +152,8 @@ def create_user():
user_data = {
'accountid': accountid,
'username': req['username'],
'passhash': passhash,
'passhash': hashed, # The hashed password (binary)
'salt': salt, # The salt (binary)
'firstname': req['firstname'],
'lastname': req['lastname'],
'birthday': req['birthday'],
Expand Down Expand Up @@ -209,7 +223,7 @@ def login():
sanitize_span.set_attribute("username", username)

# Step 2: Get user data from the database
with tracer.start_as_current_span("get_user_data") as get_user_span:
with tracer.start_as_current_span("lookup_user_data") as get_user_span:
app.logger.debug('Getting the user data.')
user = users_db.get_user(username)
if user is None:
Expand All @@ -219,25 +233,20 @@ def login():
# Step 3: Validate the password
with tracer.start_as_current_span("validate_password") as password_span:
app.logger.debug('Validating the password.')
#@@@password_span.set_attribute("public_key_bit_size", public_key_bit_size)
# Assuming the public key bit size has been set in app.config['PUBLIC_KEY_BIT_SIZE']
#public_key_bit_size = app.config.get('PUBLIC_KEY_BIT_SIZE', None)

# Add public key bit size as a span attribute
# if public_key_bit_size:
# Validate the password
if not bcrypt.checkpw(password.encode('utf-8'), user['passhash']):

#if not bcrypt.checkpw(password.encode('utf-8'), user['passhash']):
if not hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), user['salt'], 10000) == user['passhash']:
password_span.set_attribute("password_valid", False)
raise PermissionError('invalid login')
password_span.set_attribute("password_valid", True)

# Step 4: Generate JWT token
with tracer.start_as_current_span("generate_jwt") as jwt_span:
with tracer.start_as_current_span("generate_session_token") as jwt_span:
try:
app.logger.debug('Creating jwt token.')
app.logger.debug('Creating session token.')

# Sub-step: Create JWT payload
with tracer.start_as_current_span("create_jwt_payload") as payload_span:
with tracer.start_as_current_span("create_session_token_payload") as payload_span:
full_name = '{} {}'.format(user['firstname'], user['lastname'])
#exp_time = datetime.utcnow() + timedelta(seconds=app.config['EXPIRY_SECONDS'])
exp_time = datetime.now(timezone.utc).replace(tzinfo=None) + timedelta(seconds=app.config['EXPIRY_SECONDS'])
Expand All @@ -254,7 +263,7 @@ def login():
#payload_span.set_attribute("public_key_bit_size", public_key_bit_size)

# Sub-step: Encode JWT token using the cached private key
with tracer.start_as_current_span("encode_jwt_token") as encode_span:
with tracer.start_as_current_span("encode_session_token") as encode_span:
token = jwt.encode(payload, app.config['PRIVATE_KEY'], algorithm='RS256')
encode_span.set_attribute("token_generated", True)
# Assuming the public key bit size has been set in app.config['PUBLIC_KEY_BIT_SIZE']
Expand All @@ -266,13 +275,13 @@ def login():
app.logger.info(f"Public key bit size equal to {public_key_bit_size} bits.")

# Log the success of the JWT generation
jwt_span.set_attribute("jwt.success", True)
app.logger.info('JWT token successfully created.')
jwt_span.set_attribute("Session_Token.success", True)
app.logger.info('Session token successfully created.')

except Exception as e:
jwt_span.record_exception(e)
jwt_span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
app.logger.error("Failed to create JWT token: %s", str(e))
app.logger.error("Failed to create Session token: %s", str(e))
raise e

app.logger.info('Login Successful.')
Expand Down
28 changes: 28 additions & 0 deletions src/hash/hash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

import hashlib
import os
import sys

# Function to hash a password using PBKDF2
def hash_password_pbkdf2(password: str) -> str:
# Generate a new salt
salt = os.urandom(16)
# Hash the password using PBKDF2 with SHA-256
hashed = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 10000)
# Return the salt and hashed password as a combined string
return salt.hex() + "|" + hashed.hex()

# Main function for the command-line interface
def main():
if len(sys.argv) != 2:
print("Usage: python hash.py <password>")
sys.exit(1)

password = sys.argv[1]

# Hash the password using PBKDF2
pbkdf2_hashed_password = hash_password_pbkdf2(password)
print(f"PBKDF2 Hashed Password: {pbkdf2_hashed_password}")

if __name__ == "__main__":
main()
23 changes: 23 additions & 0 deletions src/hash/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import hashlib

# Function to verify the password using PBKDF2
def verify_password(password: str, stored_hash: str) -> bool:
# One-liner to verify the password with PBKDF2
return hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), bytes.fromhex(stored_hash.split('|')[0]), 10000).hex() == stored_hash.split(':')[1]

# Main function for testing the condition
def main():
# Known hash generated by the hashing function (format: salt:hash)
stored_hash = input("Enter the known hash (format: salt:hash): ")

# Input password to be tested
password = input("Enter the password to verify: ")

# Verifying the password
if not verify_password(password, stored_hash):
print("Password is invalid!")
else:
print("Password is valid!")

if __name__ == "__main__":
main()

0 comments on commit 9173dbe

Please sign in to comment.