File size: 3,600 Bytes
f96d5c8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | #!/usr/bin/env python3
"""
src/encryption.py β BIP-39 + Fernet + OS keyring encryption for private_facts
"""
import os
import json
from pathlib import Path
import keyring
from cryptography.fernet import Fernet, InvalidToken
from mnemonic import Mnemonic # pip install mnemonic
mnemo = Mnemonic("english")
PRIVATE_FACTS_ENC = Path("facts/private_facts.json.enc")
KEYRING_SERVICE = "qvpic"
KEYRING_USERNAME = "private_facts_key"
def generate_bip39_mnemonic() -> str:
"""Generate a new 12-word BIP-39 mnemonic."""
return mnemo.generate(strength=128)
def mnemonic_to_fernet_key(mnemonic: str) -> bytes:
"""Derive a Fernet key from BIP-39 mnemonic (deterministic)."""
# Simple PBKDF2-style derivation (good enough for this use case)
import hashlib
salt = b"qvpic_salt_v1"
key_material = hashlib.pbkdf2_hmac('sha256', mnemonic.encode(), salt, 100_000, dklen=32)
return Fernet.generate_key() if not key_material else base64.urlsafe_b64encode(key_material)
import base64 # needed for the line above
def setup_encryption():
"""One-time setup: ask for mnemonic or generate new one."""
print("π QVPIC Private Facts Encryption Setup")
print("We will use a BIP-39 recovery phrase (12 words) to protect your private facts.\n")
choice = input("Do you want to (1) generate a new recovery phrase or (2) enter an existing one? [1/2]: ").strip()
if choice == "1":
mnemonic = generate_bip39_mnemonic()
print("\nβ
Generated new BIP-39 recovery phrase:")
print(f" {mnemonic}")
print("\nβ οΈ WRITE THIS DOWN AND STORE IT SAFELY. This is your only way to recover your private facts.")
print(" If you lose it, your private data cannot be recovered.\n")
else:
mnemonic = input("Enter your 12-word BIP-39 recovery phrase: ").strip()
if not mnemo.check(mnemonic):
print("β Invalid mnemonic. Please try again.")
return False
# Derive and store key in OS keyring
fernet_key = mnemonic_to_fernet_key(mnemonic)
keyring.set_password(KEYRING_SERVICE, KEYRING_USERNAME, fernet_key.decode())
print("β
Encryption key stored securely in OS keyring.")
print("Private facts will now be encrypted at rest.")
return True
def get_fernet() -> Fernet:
"""Retrieve Fernet key from OS keyring."""
key_b64 = keyring.get_password(KEYRING_SERVICE, KEYRING_USERNAME)
if not key_b64:
raise RuntimeError("No encryption key found. Run setup_encryption() first.")
return Fernet(key_b64.encode())
def encrypt_private_facts(data: list) -> bool:
"""Encrypt private facts and save as .enc file."""
try:
fernet = get_fernet()
plaintext = json.dumps(data, indent=2, ensure_ascii=False).encode()
ciphertext = fernet.encrypt(plaintext)
PRIVATE_FACTS_ENC.write_bytes(ciphertext)
print(f"β
Private facts encrypted and saved ({PRIVATE_FACTS_ENC})")
return True
except Exception as e:
print(f"β Encryption failed: {e}")
return False
def decrypt_private_facts() -> list:
"""Decrypt and return private facts."""
if not PRIVATE_FACTS_ENC.exists():
return []
try:
fernet = get_fernet()
ciphertext = PRIVATE_FACTS_ENC.read_bytes()
plaintext = fernet.decrypt(ciphertext)
return json.loads(plaintext.decode())
except InvalidToken:
print("β Decryption failed β wrong recovery phrase or corrupted file.")
return []
except Exception as e:
print(f"β Decryption error: {e}")
return [] |