-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdatabase.py
More file actions
342 lines (287 loc) · 12.2 KB
/
Copy pathdatabase.py
File metadata and controls
342 lines (287 loc) · 12.2 KB
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
import os
import sys
import json
import csv
import sqlite3
import base64
from datetime import datetime
from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
# Locate the database file next to the main executable or script
if getattr(sys, 'frozen', False):
executable_dir = os.path.dirname(sys.executable)
else:
executable_dir = os.path.dirname(os.path.abspath(__file__))
DB_FILE = os.path.join(executable_dir, "vault.db")
VERIFICATION_STRING = b"verification_token"
def get_db_connection():
"""Establishes and returns a connection to the SQLite database."""
conn = sqlite3.connect(DB_FILE)
conn.row_factory = sqlite3.Row
return conn
def db_init():
"""Initializes the SQLite database, creates tables, and handles dynamic migrations."""
conn = get_db_connection()
cursor = conn.cursor()
# Settings table: for vault initialization details (salt, verifier token)
cursor.execute('''
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
val BLOB NOT NULL
)
''')
# Credentials table: for storing encrypted password details
cursor.execute('''
CREATE TABLE IF NOT EXISTS credentials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
service TEXT NOT NULL,
username TEXT NOT NULL,
password_encrypted BLOB NOT NULL
)
''')
# Dynamic Migration: Check existing columns in credentials table
cursor.execute("PRAGMA table_info(credentials)")
existing_columns = {column['name'] for column in cursor.fetchall()}
# Add new columns if missing
new_columns = [
("url", "TEXT DEFAULT ''"),
("notes", "TEXT DEFAULT ''"),
("category", "TEXT DEFAULT 'General'"),
("totp_secret_encrypted", "BLOB DEFAULT NULL"),
("created_at", "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"),
("updated_at", "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
]
for col_name, col_type in new_columns:
if col_name not in existing_columns:
cursor.execute(f"ALTER TABLE credentials ADD COLUMN {col_name} {col_type}")
conn.commit()
conn.close()
def is_vault_initialized() -> bool:
"""Checks whether the vault has been set up with a master password."""
db_init()
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT val FROM settings WHERE key = 'master_verifier'")
row = cursor.fetchone()
conn.close()
return row is not None
def derive_session_key(master_password: str, salt: bytes) -> bytes:
"""
Derives a 32-byte Fernet key from the master password using PBKDF2.
Using a high number of iterations (100,000) prevents brute-forcing offline database dumps.
"""
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100000
)
return base64.urlsafe_b64encode(kdf.derive(master_password.encode('utf-8')))
def initialize_vault(master_password: str):
"""
Sets up the vault for the first time by generating a master salt and
storing an encrypted verification token.
"""
db_init()
conn = get_db_connection()
cursor = conn.cursor()
master_salt = os.urandom(16)
key = derive_session_key(master_password, master_salt)
fernet = Fernet(key)
verifier_encrypted = fernet.encrypt(VERIFICATION_STRING)
cursor.execute("INSERT OR REPLACE INTO settings (key, val) VALUES (?, ?)", ("master_salt", master_salt))
cursor.execute("INSERT OR REPLACE INTO settings (key, val) VALUES (?, ?)", ("master_verifier", verifier_encrypted))
conn.commit()
conn.close()
def verify_master_password(master_password: str) -> bytes | None:
"""
Verifies if the master password is correct.
Returns the derived session key (bytes) on success, or None on failure.
"""
if not is_vault_initialized():
return None
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT val FROM settings WHERE key = 'master_salt'")
salt_row = cursor.fetchone()
cursor.execute("SELECT val FROM settings WHERE key = 'master_verifier'")
verifier_row = cursor.fetchone()
conn.close()
if not salt_row or not verifier_row:
return None
salt = salt_row['val']
verifier_encrypted = verifier_row['val']
key = derive_session_key(master_password, salt)
try:
fernet = Fernet(key)
decrypted = fernet.decrypt(verifier_encrypted)
if decrypted == VERIFICATION_STRING:
return key
except (InvalidToken, ValueError):
pass
return None
def add_credential(session_key: bytes, service: str, username: str, password_plaintext: str,
url: str = "", notes: str = "", category: str = "General", totp_secret: str = ""):
"""Encrypts the credential password and TOTP secret, then saves to the database."""
fernet = Fernet(session_key)
encrypted_password = fernet.encrypt(password_plaintext.encode('utf-8'))
encrypted_totp = None
if totp_secret:
encrypted_totp = fernet.encrypt(totp_secret.strip().encode('utf-8'))
now_str = datetime.now().isoformat()
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute(
'''INSERT INTO credentials
(service, username, password_encrypted, url, notes, category, totp_secret_encrypted, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)''',
(service, username, encrypted_password, url, notes, category, encrypted_totp, now_str, now_str)
)
conn.commit()
conn.close()
def update_credential(session_key: bytes, cred_id: int, service: str, username: str, password_plaintext: str,
url: str = "", notes: str = "", category: str = "General", totp_secret: str = ""):
"""Updates an existing credential in the database."""
fernet = Fernet(session_key)
encrypted_password = fernet.encrypt(password_plaintext.encode('utf-8'))
encrypted_totp = None
if totp_secret:
encrypted_totp = fernet.encrypt(totp_secret.strip().encode('utf-8'))
now_str = datetime.now().isoformat()
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute(
'''UPDATE credentials
SET service = ?, username = ?, password_encrypted = ?, url = ?, notes = ?, category = ?,
totp_secret_encrypted = ?, updated_at = ?
WHERE id = ?''',
(service, username, encrypted_password, url, notes, category, encrypted_totp, now_str, cred_id)
)
conn.commit()
conn.close()
def get_all_credentials(session_key: bytes, search_query: str = "", category_filter: str = "All") -> list:
"""
Fetches all credentials from the DB, decrypts passwords/TOTP keys,
and returns a list of dictionaries. Filters by query or category if provided.
"""
conn = get_db_connection()
cursor = conn.cursor()
query = "SELECT id, service, username, password_encrypted, url, notes, category, totp_secret_encrypted, created_at, updated_at FROM credentials WHERE 1=1"
params = []
if search_query:
query += " AND (service LIKE ? OR username LIKE ? OR notes LIKE ? OR url LIKE ?)"
term = f"%{search_query}%"
params.extend([term, term, term, term])
if category_filter and category_filter != "All":
query += " AND category = ?"
params.append(category_filter)
query += " ORDER BY service ASC"
cursor.execute(query, params)
rows = cursor.fetchall()
conn.close()
fernet = Fernet(session_key)
results = []
for row in rows:
try:
decrypted_password = fernet.decrypt(row['password_encrypted']).decode('utf-8')
totp_secret = ""
if row['totp_secret_encrypted']:
try:
totp_secret = fernet.decrypt(row['totp_secret_encrypted']).decode('utf-8')
except (InvalidToken, ValueError):
totp_secret = ""
results.append({
"id": row['id'],
"service": row['service'],
"username": row['username'],
"password": decrypted_password,
"url": row['url'] or "",
"notes": row['notes'] or "",
"category": row['category'] or "General",
"totp_secret": totp_secret,
"created_at": row['created_at'] or "",
"updated_at": row['updated_at'] or ""
})
except (InvalidToken, ValueError):
continue
return results
def delete_credential(cred_id: int):
"""Deletes a credential from the vault by database ID."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("DELETE FROM credentials WHERE id = ?", (cred_id,))
conn.commit()
conn.close()
def export_vault_encrypted(session_key: bytes, file_path: str):
"""Exports all credentials to a Fernet-encrypted JSON file."""
creds = get_all_credentials(session_key)
raw_data = json.dumps(creds).encode('utf-8')
fernet = Fernet(session_key)
encrypted_data = fernet.encrypt(raw_data)
with open(file_path, 'wb') as f:
f.write(encrypted_data)
def import_vault_encrypted(session_key: bytes, file_path: str) -> int:
"""Decrypts and imports credentials from an encrypted .spvault backup file. Returns count of imported items."""
with open(file_path, 'rb') as f:
encrypted_data = f.read()
fernet = Fernet(session_key)
decrypted_data = fernet.decrypt(encrypted_data)
creds = json.loads(decrypted_data.decode('utf-8'))
count = 0
for item in creds:
add_credential(
session_key,
service=item.get("service", "Imported Service"),
username=item.get("username", ""),
password_plaintext=item.get("password", ""),
url=item.get("url", ""),
notes=item.get("notes", ""),
category=item.get("category", "General"),
totp_secret=item.get("totp_secret", "")
)
count += 1
return count
def export_vault_csv(session_key: bytes, file_path: str):
"""Exports all credentials to a plain text CSV file."""
creds = get_all_credentials(session_key)
fieldnames = ["service", "username", "password", "url", "category", "totp_secret", "notes"]
with open(file_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for c in creds:
writer.writerow({
"service": c["service"],
"username": c["username"],
"password": c["password"],
"url": c["url"],
"category": c["category"],
"totp_secret": c["totp_secret"],
"notes": c["notes"]
})
def import_vault_csv(session_key: bytes, file_path: str) -> int:
"""Imports credentials from a CSV file (supports standard column headers). Returns imported count."""
count = 0
with open(file_path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
service = row.get("service") or row.get("name") or row.get("title") or "Imported"
username = row.get("username") or row.get("login_username") or row.get("email") or ""
password = row.get("password") or row.get("login_password") or ""
url = row.get("url") or row.get("login_uri") or ""
notes = row.get("notes") or row.get("comment") or ""
category = row.get("category") or "General"
totp_secret = row.get("totp_secret") or row.get("totp") or ""
if service and password:
add_credential(
session_key,
service=service,
username=username,
password_plaintext=password,
url=url,
notes=notes,
category=category,
totp_secret=totp_secret
)
count += 1
return count