From e33a8a32aa011056c708aafa3647f9aa834d8415 Mon Sep 17 00:00:00 2001 From: Pieter Hagen Date: Mon, 14 Oct 2024 16:51:31 +0200 Subject: [PATCH 1/4] Span name and PBKDF2 hash --- .../accounts-db/initdb/1-load-testdata.sh | 5 +++- src/accounts/userservice/userservice.py | 23 ++++++++----------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/accounts/accounts-db/initdb/1-load-testdata.sh b/src/accounts/accounts-db/initdb/1-load-testdata.sh index a347afbd7..55eddb7d4 100644 --- a/src/accounts/accounts-db/initdb/1-load-testdata.sh +++ b/src/accounts/accounts-db/initdb/1-load-testdata.sh @@ -99,8 +99,11 @@ main() { # A password hash + salt for the demo password 'bankofanthos' # Via Python3: bycrypt.hashpw('password'.encode('utf-8'), bcrypt.gensalt()) - DEFAULT_PASSHASH='\x243262243132244c48334f54422e70653274596d6834534b756673727563564b3848774630494d2f34717044746868366e42352e744b575978314e61' + #DEFAULT_PASSHASH='\x243262243132244c48334f54422e70653274596d6834534b756673727563564b3848774630494d2f34717044746868366e42352e744b575978314e61' + #using PBKDF2 + DEFAULT_PASSHASH='47e6106bff23748cad32f10911a094e8:b4d9e941eaf9e1a9b5b5c4f57d22ddb8123b8a47467e52685dd4f06b4152398d' + #DEFAULT_PASSHASH='8dc1c220e41b3ce76e73f73711795929:0bf82d9c1ce2eede79043449f3a4e2d5fcf0039f148bd249bda8b9736802da09' create_accounts } diff --git a/src/accounts/userservice/userservice.py b/src/accounts/userservice/userservice.py index 98c213ade..56be430fb 100644 --- a/src/accounts/userservice/userservice.py +++ b/src/accounts/userservice/userservice.py @@ -209,7 +209,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: @@ -219,25 +219,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'), bytes.fromhex(user['passhash'].split(':')[0]), 10000).hex() == user['passhash'].split(':')[1]: 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_json_web_token") as jwt_span: try: app.logger.debug('Creating jwt 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_json_web_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']) @@ -254,7 +249,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_json_web_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'] @@ -262,8 +257,8 @@ def login(): encode_span.set_attribute("public_key_bit_size", public_key_bit_size) # 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("json web token.success", True) + app.logger.info('JWT session token successfully created.') except Exception as e: jwt_span.record_exception(e) From 37baa6e2fac3f650cad87c421066d280d6a0bbfb Mon Sep 17 00:00:00 2001 From: Pieter Hagen Date: Mon, 14 Oct 2024 17:11:41 +0200 Subject: [PATCH 2/4] added the hasher code --- src/hash/hash.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/hash/hash.py diff --git a/src/hash/hash.py b/src/hash/hash.py new file mode 100644 index 000000000..dea24b660 --- /dev/null +++ b/src/hash/hash.py @@ -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 ") + 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() \ No newline at end of file From 121a1acd2ff6b8f697392995b069aa470b01ab31 Mon Sep 17 00:00:00 2001 From: Pieter Hagen Date: Tue, 15 Oct 2024 17:42:45 +0200 Subject: [PATCH 3/4] changed bycrypt to PBKDF2 --- kubernetes-manifests/config.yaml | 4 +- .../accounts-db/initdb/0-accounts-schema.sql | 1 + .../accounts-db/initdb/1-load-testdata.sh | 39 ++++++++-------- .../accounts-db/initdb/1-load-testdata.sql | 13 +++--- src/accounts/userservice/db.py | 3 +- src/accounts/userservice/userservice.py | 44 ++++++++++++------- src/hash/hash.py | 2 +- src/hash/test.py | 23 ++++++++++ 8 files changed, 87 insertions(+), 42 deletions(-) create mode 100644 src/hash/test.py diff --git a/kubernetes-manifests/config.yaml b/kubernetes-manifests/config.yaml index 0d0a74254..a49f6a562 100644 --- a/kubernetes-manifests/config.yaml +++ b/kubernetes-manifests/config.yaml @@ -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" diff --git a/src/accounts/accounts-db/initdb/0-accounts-schema.sql b/src/accounts/accounts-db/initdb/0-accounts-schema.sql index 212962a63..e37d79d3a 100644 --- a/src/accounts/accounts-db/initdb/0-accounts-schema.sql +++ b/src/accounts/accounts-db/initdb/0-accounts-schema.sql @@ -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, diff --git a/src/accounts/accounts-db/initdb/1-load-testdata.sh b/src/accounts/accounts-db/initdb/1-load-testdata.sh index 55eddb7d4..f9e3b568f 100644 --- a/src/accounts/accounts-db/initdb/1-load-testdata.sh +++ b/src/accounts/accounts-db/initdb/1-load-testdata.sh @@ -22,7 +22,6 @@ if [ "$USE_DEMO_DATA" != "True" ]; then exit 0 fi - # Expected environment variables readonly ENV_VARS=( "POSTGRES_DB" @@ -30,16 +29,28 @@ readonly ENV_VARS=( "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" @@ -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" @@ -57,7 +67,6 @@ add_contact() { EOSQL } - # Load test data into the database create_accounts() { # Add demo users. @@ -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 @@ -97,15 +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' - #using PBKDF2 - - DEFAULT_PASSHASH='47e6106bff23748cad32f10911a094e8:b4d9e941eaf9e1a9b5b5c4f57d22ddb8123b8a47467e52685dd4f06b4152398d' - #DEFAULT_PASSHASH='8dc1c220e41b3ce76e73f73711795929:0bf82d9c1ce2eede79043449f3a4e2d5fcf0039f148bd249bda8b9736802da09' + # A password hash + salt for the demo password 'bankofsplunk' + #DEFAULT_PASSHASH='47e6106bff23748cad32f10911a094e8|b4d9e941eaf9e1a9b5b5c4f57d22ddb8123b8a47467e52685dd4f06b4152398d' + DEFAULT_PASSHASH='0e0ab7fc8ad0f68d8c0a88da06a7e79e|5da0a1304c22bec69bdb74f66d590cd4a0d585063ecbd7d1120662ddf55c49c8' create_accounts } - -main +main \ No newline at end of file diff --git a/src/accounts/accounts-db/initdb/1-load-testdata.sql b/src/accounts/accounts-db/initdb/1-load-testdata.sql index db261ad2a..65c192fce 100644 --- a/src/accounts/accounts-db/initdb/1-load-testdata.sql +++ b/src/accounts/accounts-db/initdb/1-load-testdata.sql @@ -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 @@ -43,3 +44,5 @@ INSERT INTO contacts VALUES ('bob', 'External Bank', '9099791699', '808889588', 'true'), ('eve', 'External Bank', '9099791699', '808889588', 'true') ON CONFLICT DO NOTHING; + + diff --git a/src/accounts/userservice/db.py b/src/accounts/userservice/db.py index fec71abb5..b1bdee53e 100644 --- a/src/accounts/userservice/db.py +++ b/src/accounts/userservice/db.py @@ -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), diff --git a/src/accounts/userservice/userservice.py b/src/accounts/userservice/userservice.py index 56be430fb..046b1317f 100644 --- a/src/accounts/userservice/userservice.py +++ b/src/accounts/userservice/userservice.py @@ -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 @@ -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']) @@ -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: @@ -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'], @@ -221,18 +235,18 @@ def login(): app.logger.debug('Validating the password.') #if not bcrypt.checkpw(password.encode('utf-8'), user['passhash']): - if not hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), bytes.fromhex(user['passhash'].split(':')[0]), 10000).hex() == user['passhash'].split(':')[1]: + 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_json_web_token") 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_json_web_token_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']) @@ -249,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_json_web_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'] @@ -257,13 +271,13 @@ def login(): encode_span.set_attribute("public_key_bit_size", public_key_bit_size) # Log the success of the JWT generation - jwt_span.set_attribute("json web token.success", True) - app.logger.info('JWT session 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.') diff --git a/src/hash/hash.py b/src/hash/hash.py index dea24b660..71e54b681 100644 --- a/src/hash/hash.py +++ b/src/hash/hash.py @@ -10,7 +10,7 @@ def hash_password_pbkdf2(password: str) -> str: # 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() + return salt.hex() + "|" + hashed.hex() # Main function for the command-line interface def main(): diff --git a/src/hash/test.py b/src/hash/test.py new file mode 100644 index 000000000..0af00919f --- /dev/null +++ b/src/hash/test.py @@ -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() From 9f6eccb0969b48f0af208a12472719a98dc58337 Mon Sep 17 00:00:00 2001 From: Pieter Hagen Date: Tue, 15 Oct 2024 17:49:45 +0200 Subject: [PATCH 4/4] verify push --- kubernetes-manifests/userservice.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/kubernetes-manifests/userservice.yaml b/kubernetes-manifests/userservice.yaml index 433f57125..0b7538201 100644 --- a/kubernetes-manifests/userservice.yaml +++ b/kubernetes-manifests/userservice.yaml @@ -235,3 +235,4 @@ spec: - key: jwtRS256.key.pub path: publickey secretName: jwt-key +