WebPass / webpass /crypto_utils.py
ag235772's picture
Hardened entire suite with AES-GCM Authenticated Encryption
3e0b7ff
import os
import json
import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
# --- Helper: Generate AES Key ---
def generate_key(password: str, salt: bytes) -> bytes:
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100000,
)
return kdf.derive(password.encode())
# --- Encrypt password with AES-GCM ---
def encrypt_password(password: str, key: bytes):
# AES-GCM uses a 12-byte Nonce instead of a 16-byte IV
nonce = os.urandom(12)
aesgcm = AESGCM(key)
# The encrypt method returns: ciphertext + 16-byte authentication tag
ciphertext = aesgcm.encrypt(nonce, password.encode(), None)
# We store Nonce + Ciphertext (which includes the Tag)
return base64.b64encode(nonce + ciphertext).decode()
# --- Decrypt password with AES-GCM ---
def decrypt_password(encrypted_password: str, key: bytes):
raw = base64.b64decode(encrypted_password)
nonce, ciphertext = raw[:12], raw[12:]
aesgcm = AESGCM(key)
# Decrypt will automatically verify the Tag. If tampered with, it throws an InvalidTag exception.
return aesgcm.decrypt(nonce, ciphertext, None).decode()
# --- Store password ---
def store_password(account: str, username: str, password: str, master_password: str):
salt = os.urandom(16)
key = generate_key(master_password, salt)
encrypted_pw = encrypt_password(password, key)
data = {"account": account, "username": username, "password": encrypted_pw, "salt": base64.b64encode(salt).decode()}
try:
with open("passwords.json", "r") as f:
records = json.load(f)
except FileNotFoundError:
records = []
records = [r for r in records if r["account"] != account]
records.append(data)
with open("passwords.json", "w") as f:
json.dump(records, f, indent=4)
print(f"[+] Authenticated GCM password stored for: {account}")
# --- Retrieve password ---
def retrieve_password(account: str, master_password: str):
try:
with open("passwords.json", "r") as f:
records = json.load(f)
except FileNotFoundError:
return None
for r in records:
if r["account"] == account:
salt = base64.b64decode(r["salt"])
key = generate_key(master_password, salt)
try:
return decrypt_password(r["password"], key)
except Exception:
print("[-] Master password incorrect or data tampered with!")
return None
return None
# --- Encrypt file ---
def encrypt_file(file_path: str, account: str, master_password: str):
password = retrieve_password(account, master_password)
if not password: return
try:
with open(file_path, "rb") as f:
content = f.read()
except FileNotFoundError: return
salt = os.urandom(16)
nonce = os.urandom(12)
key = generate_key(password, salt)
aesgcm = AESGCM(key)
ciphertext = aesgcm.encrypt(nonce, content, None)
enc_file = file_path + ".enc"
with open(enc_file, "wb") as f:
f.write(ciphertext)
meta = {
"salt": base64.b64encode(salt).decode(),
"nonce": base64.b64encode(nonce).decode(), # Now storing Nonce instead of IV
"account": account
}
with open(enc_file + ".meta", "w") as f:
json.dump(meta, f)
print(f"[+] File encrypted with AES-GCM: {enc_file}")
# --- Decrypt file ---
def decrypt_file(enc_file: str, master_password: str):
meta_file = enc_file + ".meta"
if not os.path.exists(meta_file): return
with open(meta_file, "r") as f:
meta = json.load(f)
account = meta["account"]
salt = base64.b64decode(meta["salt"])
nonce = base64.b64decode(meta["nonce"])
password = retrieve_password(account, master_password)
if not password: return
key = generate_key(password, salt)
with open(enc_file, "rb") as f:
ciphertext = f.read()
aesgcm = AESGCM(key)
try:
data = aesgcm.decrypt(nonce, ciphertext, None)
dec_file = enc_file.replace(".enc", "_decrypted")
with open(dec_file, "wb") as f:
f.write(data)
print(f"[+] Authenticated decryption successful: {dec_file}")
except Exception:
print("[-] Decryption failed: Invalid password or corrupted data.")