From 0e8ae28b8ce88d3edc8651e35feff55e402dca91 Mon Sep 17 00:00:00 2001 From: Mobarak Hosen Date: Mon, 4 May 2026 18:05:05 +0600 Subject: [PATCH 1/3] Potential fix for code scanning alert no. 34: Use of a broken or weak cryptographic hashing algorithm on sensitive data Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- pacli/store.py | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/pacli/store.py b/pacli/store.py index bf3ab8b..a91ca70 100644 --- a/pacli/store.py +++ b/pacli/store.py @@ -5,6 +5,7 @@ import sqlite3 import threading import hashlib +import hmac from cryptography.fernet import Fernet from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives import hashes @@ -237,12 +238,50 @@ def get_secrets_by_label(self, label): ) return results + PBKDF2_ITERATIONS = 310000 + PBKDF2_SALT_BYTES = 16 + + def _hash_password_pbkdf2(self, password: str, salt: bytes = None) -> str: + if salt is None: + salt = os.urandom(self.PBKDF2_SALT_BYTES) + dk = hashlib.pbkdf2_hmac( + "sha256", + password.encode(), + salt, + self.PBKDF2_ITERATIONS, + ) + salt_b64 = base64.b64encode(salt).decode("utf-8") + dk_b64 = base64.b64encode(dk).decode("utf-8") + return f"pbkdf2_sha256${self.PBKDF2_ITERATIONS}${salt_b64}${dk_b64}" + + def _verify_password_pbkdf2(self, password: str, stored: str) -> bool: + try: + scheme, iter_str, salt_b64, expected_b64 = stored.split("$", 3) + if scheme != "pbkdf2_sha256": + return False + iterations = int(iter_str) + salt = base64.b64decode(salt_b64.encode("utf-8")) + expected = base64.b64decode(expected_b64.encode("utf-8")) + actual = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, iterations) + return hmac.compare_digest(actual, expected) + except Exception: + return False + def verify_master_password(self, password): try: if os.path.exists(PASSWORD_HASH_PATH): with open(PASSWORD_HASH_PATH, "r") as f: stored_hash = f.read().strip() - return hashlib.sha256(password.encode()).hexdigest() == stored_hash + + if stored_hash.startswith("pbkdf2_sha256$"): + return self._verify_password_pbkdf2(password, stored_hash) + + # Legacy fallback: support old SHA-256 hashes and transparently upgrade. + legacy_ok = hashlib.sha256(password.encode()).hexdigest() == stored_hash + if legacy_ok: + with open(PASSWORD_HASH_PATH, "w") as f: + f.write(self._hash_password_pbkdf2(password)) + return legacy_ok # Fallback: attempt decryption salt = get_salt() test_fernet = self._derive_fernet(password, salt) From 86c88b00862de7978c9c96eee7e7e8c4e8ffd5cd Mon Sep 17 00:00:00 2001 From: Mobarak Hosen Date: Mon, 4 May 2026 22:32:34 +0600 Subject: [PATCH 2/3] Potential fix for pull request finding 'CodeQL / Use of a broken or weak cryptographic hashing algorithm on sensitive data' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- pacli/store.py | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/pacli/store.py b/pacli/store.py index a91ca70..99a949a 100644 --- a/pacli/store.py +++ b/pacli/store.py @@ -267,6 +267,33 @@ def _verify_password_pbkdf2(self, password: str, stored: str) -> bool: except Exception: return False + def _hash_legacy_sha256_with_pbkdf2(self, legacy_sha256_hex: str, salt: bytes = None) -> str: + if salt is None: + salt = os.urandom(self.PBKDF2_SALT_BYTES) + dk = hashlib.pbkdf2_hmac( + "sha256", + legacy_sha256_hex.encode("utf-8"), + salt, + self.PBKDF2_ITERATIONS, + ) + salt_b64 = base64.b64encode(salt).decode("utf-8") + dk_b64 = base64.b64encode(dk).decode("utf-8") + return f"legacy_pbkdf2_sha256${self.PBKDF2_ITERATIONS}${salt_b64}${dk_b64}" + + def _verify_legacy_sha256_with_pbkdf2(self, password: str, stored: str) -> bool: + try: + scheme, iter_str, salt_b64, expected_b64 = stored.split("$", 3) + if scheme != "legacy_pbkdf2_sha256": + return False + iterations = int(iter_str) + salt = base64.b64decode(salt_b64.encode("utf-8")) + expected = base64.b64decode(expected_b64.encode("utf-8")) + legacy_actual = hashlib.sha256(password.encode()).hexdigest().encode("utf-8") + actual = hashlib.pbkdf2_hmac("sha256", legacy_actual, salt, iterations) + return hmac.compare_digest(actual, expected) + except Exception: + return False + def verify_master_password(self, password): try: if os.path.exists(PASSWORD_HASH_PATH): @@ -277,7 +304,18 @@ def verify_master_password(self, password): return self._verify_password_pbkdf2(password, stored_hash) # Legacy fallback: support old SHA-256 hashes and transparently upgrade. - legacy_ok = hashlib.sha256(password.encode()).hexdigest() == stored_hash + legacy_ok = False + + if stored_hash.startswith("legacy_pbkdf2_sha256$"): + legacy_ok = self._verify_legacy_sha256_with_pbkdf2(password, stored_hash) + else: + # One-time migration for existing raw SHA-256 legacy values. + legacy_sha256 = hashlib.sha256(password.encode()).hexdigest() + wrapped = self._hash_legacy_sha256_with_pbkdf2(legacy_sha256) + with open(PASSWORD_HASH_PATH, "w") as f: + f.write(wrapped) + legacy_ok = self._verify_legacy_sha256_with_pbkdf2(password, wrapped) + if legacy_ok: with open(PASSWORD_HASH_PATH, "w") as f: f.write(self._hash_password_pbkdf2(password)) From 16475f226edf3d12dc864bd2436752d8402cde55 Mon Sep 17 00:00:00 2001 From: Mobarak Hosen Date: Mon, 4 May 2026 22:45:17 +0600 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- pacli/store.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/pacli/store.py b/pacli/store.py index 99a949a..ef3344f 100644 --- a/pacli/store.py +++ b/pacli/store.py @@ -280,7 +280,7 @@ def _hash_legacy_sha256_with_pbkdf2(self, legacy_sha256_hex: str, salt: bytes = dk_b64 = base64.b64encode(dk).decode("utf-8") return f"legacy_pbkdf2_sha256${self.PBKDF2_ITERATIONS}${salt_b64}${dk_b64}" - def _verify_legacy_sha256_with_pbkdf2(self, password: str, stored: str) -> bool: + def _verify_legacy_sha256_with_pbkdf2(self, legacy_sha256_hex: str, stored: str) -> bool: try: scheme, iter_str, salt_b64, expected_b64 = stored.split("$", 3) if scheme != "legacy_pbkdf2_sha256": @@ -288,8 +288,7 @@ def _verify_legacy_sha256_with_pbkdf2(self, password: str, stored: str) -> bool: iterations = int(iter_str) salt = base64.b64decode(salt_b64.encode("utf-8")) expected = base64.b64decode(expected_b64.encode("utf-8")) - legacy_actual = hashlib.sha256(password.encode()).hexdigest().encode("utf-8") - actual = hashlib.pbkdf2_hmac("sha256", legacy_actual, salt, iterations) + actual = hashlib.pbkdf2_hmac("sha256", legacy_sha256_hex.encode("utf-8"), salt, iterations) return hmac.compare_digest(actual, expected) except Exception: return False @@ -306,15 +305,13 @@ def verify_master_password(self, password): # Legacy fallback: support old SHA-256 hashes and transparently upgrade. legacy_ok = False + legacy_sha256 = hashlib.sha256(password.encode()).hexdigest() + if stored_hash.startswith("legacy_pbkdf2_sha256$"): - legacy_ok = self._verify_legacy_sha256_with_pbkdf2(password, stored_hash) + legacy_ok = self._verify_legacy_sha256_with_pbkdf2(legacy_sha256, stored_hash) else: # One-time migration for existing raw SHA-256 legacy values. - legacy_sha256 = hashlib.sha256(password.encode()).hexdigest() - wrapped = self._hash_legacy_sha256_with_pbkdf2(legacy_sha256) - with open(PASSWORD_HASH_PATH, "w") as f: - f.write(wrapped) - legacy_ok = self._verify_legacy_sha256_with_pbkdf2(password, wrapped) + legacy_ok = hmac.compare_digest(stored_hash, legacy_sha256) if legacy_ok: with open(PASSWORD_HASH_PATH, "w") as f: