mubi-613 commited on
Commit
54f146a
·
0 Parent(s):

Clean push without binary history

Browse files
Files changed (9) hide show
  1. .gitattributes +2 -0
  2. .github/workflows/auto_sync.yml +20 -0
  3. .gitignore +16 -0
  4. LICENSE +21 -0
  5. README.md +69 -0
  6. main.py +269 -0
  7. requirements.txt +11 -0
  8. simple_notes.json +1 -0
  9. vault_logic.py +290 -0
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ fonts/dejavu-sans/*.ttf filter=lfs diff=lfs merge=lfs -text
2
+ fonts/dejavu-sans/*.pkl filter=lfs diff=lfs merge=lfs -text
.github/workflows/auto_sync.yml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to Hugging Face hub
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ workflow_dispatch:
6
+
7
+ jobs:
8
+ sync-to-hub:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v3
12
+ with:
13
+ fetch-depth: 0
14
+ lfs: true
15
+ - name: Push to hub
16
+ env:
17
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
18
+ run: git push --force https://mubi0613:$HF_TOKEN@huggingface.co/spaces/mubi0613/SecureVault-AI main
19
+
20
+ ----------------------------------------------------------------------------------------------------------------
.gitignore ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Local Vault Data
2
+ notes.json
3
+ vault_config.json
4
+ usage_stats.json
5
+ feedback_log.json
6
+ .env
7
+
8
+ # Python Environment
9
+ __pycache__/
10
+ *.py[cod]
11
+ *$py.class
12
+ venv/
13
+ .env
14
+
15
+ # Streamlit logs
16
+ .streamlit/
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mubi0613
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🛡️ SecureVault AI: Privacy-First RAG Knowledge Base
2
+
3
+ A production-grade, secure note-taking application that integrates **Retrieval-Augmented Generation (RAG)** with **Zero-Knowledge Encryption**. Built for users who need AI-powered insights without sacrificing data privacy.
4
+
5
+ ## 🚀 Key Engineering Features
6
+ * **Hybrid Security Model:** Uses **PBKDF2** key derivation and **AES-256 (Fernet)** encryption. Data is only decrypted in-memory during an active session.
7
+ * **Local RAG Engine:** Utilizes **FAISS** (Facebook AI Similarity Search) and `all-MiniLM-L6-v2` embeddings for lightning-fast, local semantic search—no private data ever leaves your machine.
8
+ * **AI Observability:** Integrated **Token & Cost Tracker** and a **Feedback Loop** (Correct/Wrong logging) to audit retrieval accuracy and identify gaps in the knowledge base.
9
+ * **Fail-Safe Recovery:** Implements a hashed **Recovery Key** system for account resets while maintaining the mathematical integrity of encrypted data.
10
+ * **Smart Exports:** Unicode-compliant PDF and DOCX generation with specialized font embedding for multi-language support.
11
+
12
+ ## 🛠️ Technical Stack
13
+ * **Frontend:** Streamlit (Custom CSS & Session Management)
14
+ * **AI/NLP:** Sentence-Transformers, TextBlob, FAISS
15
+ * **Cryptography:** Python `cryptography` library (SHA-256 & PBKDF2)
16
+ * **Data:** JSON-based persistent storage
17
+
18
+ ---
19
+
20
+ ## 🔐 Security Architecture
21
+ 1. **PIN Hashing:** The Master PIN is never stored; only its **SHA-256** hash is kept for authentication.
22
+ 2. **Zero-Knowledge:** The PIN is used to derive the encryption key. If the PIN is lost, "Secret" notes remain mathematically inaccessible.
23
+ 3. **Auto-Lock Engine:** A session-based activity monitor automatically wipes the memory and locks the vault after 150 seconds of inactivity.
24
+
25
+
26
+
27
+ ---
28
+
29
+ ## 📦 Installation & Setup
30
+
31
+ ### 1. Clone the Repository
32
+ ```bash
33
+ git clone [https://github.com/mubi0613/SecureVault-AI.git](https://github.com/mubi0613/SecureVault-AI.git)
34
+ cd SecureVault-AI
35
+
36
+ 2. Install Dependencies
37
+ Ensure you have Python 3.9+ installed, then run:
38
+ ```bash
39
+ pip install -r requirements.txt
40
+
41
+ 3. Run the Application
42
+ ```bash
43
+ streamlit run main.py
44
+
45
+ 📖 Usage Guide
46
+ Initialize: Set your Master PIN and save your Recovery Key.
47
+
48
+ Create Notes: Use the sidebar to add notes. Toggle "Mark as Secret" to apply AES-256 encryption.
49
+
50
+ AI Search: Open the "Ask Your Vault" expander to query your notes using natural language.
51
+
52
+ Manage: Edit or delete notes, and export them as PDF/DOCX for offline use.
53
+
54
+ Monitor: Check the "AI Resources" section in the sidebar to view estimated token usage and costs.
55
+
56
+ 📂 Project Structure
57
+ Plaintext
58
+ SecureVault-AI/
59
+ ├── main.py # Streamlit UI & Session Management
60
+ ├── vault_logic.py # Cryptography, RAG, & File IO Logic
61
+ ├── requirements.txt # Project Dependencies
62
+ ├── .gitignore
63
+ ├── fonts/ # Custom fonts for PDF Unicode support
64
+ └── README.md # Documentation
65
+ ⚠️ Important Note on Security
66
+ This application is designed with a Zero-Knowledge philosophy. The developers cannot recover your notes if you lose both your Master PIN and your Recovery Key. Data integrity is the sole responsibility of the user.
67
+
68
+ 📜 License
69
+ Distributed under the MIT License. See LICENSE for more information.
main.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from datetime import datetime
3
+ import vault_logic as vl
4
+ import time
5
+ import json
6
+ import os
7
+
8
+ # --- CONFIGURATION ---
9
+ AUTO_LOCK_SECONDS = 150 # 2 Minutes
10
+
11
+ st.set_page_config(page_title="AI Vault", layout="wide")
12
+
13
+ # --- 0. FIRST TIME SETUP ---
14
+ if not vl.is_vault_initialized():
15
+ st.title("🔒 Initialize Your Secure Vault")
16
+
17
+ # 1. Primary Action: The PIN Box
18
+ st.info("👋 Welcome! To get started, please set your Master PIN below.")
19
+
20
+ with st.form("setup_form"):
21
+ new_pin = st.text_input("Create Master PIN", type="password", help="Minimum 4 digits")
22
+ conf_pin = st.text_input("Confirm Master PIN", type="password")
23
+ submit = st.form_submit_button("Setup Vault", type="primary")
24
+
25
+ # 2. Professional Spacing
26
+ st.write("")
27
+ st.write("")
28
+ st.divider()
29
+
30
+ # 3. Security & Recovery Information
31
+ if 'generated_recovery' not in st.session_state:
32
+ st.session_state.generated_recovery = vl.generate_recovery_key()
33
+
34
+ st.warning("⚠️ **CRITICAL: Save Your Recovery Key**")
35
+ st.caption("RECOVERY - KEY : This is the ONLY way to reset your vault access if you forget your PIN.")
36
+ st.code(st.session_state.generated_recovery, language=None)
37
+
38
+ # Important Security Note (Liability Waiver)
39
+ st.markdown("""
40
+ <div style="background-color: #ff4b4b11; padding: 20px; border-radius: 10px; border: 1px solid #ff4b4b;">
41
+ <p style="color: #ff4b4b; font-weight: bold; margin-bottom: 5px;">🛑 Important Security Notice:</p>
42
+ <p style="font-size: 0.9rem; margin-bottom: 10px;">
43
+ This vault utilizes <b>Zero-Knowledge AES-Encryption</b>. Your PIN is never stored; it is only hashed.
44
+ While the Recovery Key can reset app access, it <b>cannot decrypt</b> existing secret data
45
+ locked with a different PIN.
46
+ </p>
47
+ <p style="font-size: 0.8rem; opacity: 0.8;">
48
+ <i>The developer holds no responsibility for data loss resulting from forgotten credentials.</i>
49
+ </p>
50
+ </div>
51
+ """, unsafe_allow_html=True)
52
+
53
+ if submit:
54
+ if new_pin == conf_pin and len(new_pin) >= 4:
55
+ vl.initialize_vault(new_pin, st.session_state.generated_recovery)
56
+ st.success("Vault Securely Initialized! Refreshing...")
57
+ time.sleep(1.5)
58
+ st.rerun()
59
+ else:
60
+ st.error("PINs must match and be at least 4 digits.")
61
+ st.stop()
62
+
63
+ # --- 1. SESSION STATE ---
64
+ if 'notes' not in st.session_state:
65
+ st.session_state.notes = vl.load_notes()
66
+ if 'edit_note_id' not in st.session_state:
67
+ st.session_state.edit_note_id = None
68
+ if 'vault_unlocked' not in st.session_state:
69
+ st.session_state.vault_unlocked = False
70
+ if 'master_pin' not in st.session_state:
71
+ st.session_state.master_pin = "" # We store the actual PIN in session only while unlocked
72
+ if 'temp_content' not in st.session_state:
73
+ st.session_state.temp_content = ""
74
+ if 'form_iteration' not in st.session_state:
75
+ st.session_state.form_iteration = 0
76
+ if 'last_activity' not in st.session_state:
77
+ st.session_state.last_activity = time.time()
78
+ if 'show_lock_alert' not in st.session_state:
79
+ st.session_state.show_lock_alert = False
80
+
81
+ # --- 2. AUTO-LOCK ENGINE ---
82
+ def update_activity():
83
+ st.session_state.last_activity = time.time()
84
+ st.session_state.show_lock_alert = False
85
+
86
+ if st.session_state.vault_unlocked:
87
+ elapsed_time = time.time() - st.session_state.last_activity
88
+ if elapsed_time > AUTO_LOCK_SECONDS:
89
+ st.session_state.vault_unlocked = False
90
+ st.session_state.edit_note_id = None
91
+ st.session_state.temp_content = ""
92
+ st.session_state.show_lock_alert = True
93
+ st.rerun()
94
+
95
+ # --- 3. CSS & ALERTS ---
96
+ st.markdown("""
97
+ <style>
98
+ .block-container {padding-top: 2rem !important;}
99
+ [data-testid="stSidebarNav"] {display: none;}
100
+ </style>
101
+ """, unsafe_allow_html=True)
102
+
103
+ if st.session_state.show_lock_alert:
104
+ st.error("🚨 **THE VAULT IS AUTO LOCKED**")
105
+
106
+ # --- 4. SIDEBAR ---
107
+ with st.sidebar:
108
+ st.title("➕ Add/Edit Note")
109
+
110
+ c_title, c_content, c_secret = "", "", False
111
+ if st.session_state.edit_note_id:
112
+ n = next((x for x in st.session_state.notes if x['id'] == st.session_state.edit_note_id), None)
113
+ if n:
114
+ c_title = n['title']
115
+ c_content = vl.decrypt_data(n['content'], st.session_state.master_pin) if (n.get('secret') and st.session_state.vault_unlocked) else n['content']
116
+ c_secret = n.get('secret', False)
117
+
118
+ default_content = st.session_state.temp_content if st.session_state.temp_content else c_content
119
+
120
+ with st.form(key=f"note_form_{st.session_state.form_iteration}", clear_on_submit=False):
121
+ new_t = st.text_input("Title", value=c_title)
122
+ new_c = st.text_area("Content", value=default_content, height=150)
123
+ m_secret = st.checkbox("🤫 Mark as Secret", value=c_secret) if st.session_state.vault_unlocked else False
124
+
125
+ if st.form_submit_button("✨ AI Summarize"):
126
+ update_activity()
127
+ if new_c:
128
+ summary_result = vl.ai_summarize_text(new_c)
129
+ st.session_state.temp_content = summary_result
130
+ st.rerun()
131
+
132
+ if st.form_submit_button("Save", type="primary", use_container_width=True):
133
+ update_activity()
134
+ if new_t or new_c:
135
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M")
136
+ final_content = vl.encrypt_data(new_c, st.session_state.master_pin) if m_secret else new_c
137
+ if st.session_state.edit_note_id:
138
+ for n in st.session_state.notes:
139
+ if n['id'] == st.session_state.edit_note_id:
140
+ n.update({"title": new_t, "content": final_content, "timestamp": ts, "secret": m_secret})
141
+ st.session_state.edit_note_id = None
142
+ else:
143
+ st.session_state.notes.insert(0, {"id": int(datetime.now().timestamp()), "title": new_t, "content": final_content, "timestamp": ts, "secret": m_secret})
144
+
145
+ vl.save_notes(st.session_state.notes)
146
+ st.session_state.temp_content = ""
147
+ st.session_state.form_iteration += 1
148
+ st.rerun()
149
+
150
+ st.divider()
151
+ st.write("### 🔐 Vault Security")
152
+ if not st.session_state.vault_unlocked:
153
+ pin_input = st.text_input("Enter Stealth PIN", type="password", key="pin_entry")
154
+ if pin_input:
155
+ if vl.verify_pin(pin_input):
156
+ update_activity()
157
+ st.session_state.vault_unlocked = True
158
+ st.session_state.master_pin = pin_input
159
+ st.rerun()
160
+ else: st.error("Incorrect PIN")
161
+
162
+ # NEW: Recovery Logic
163
+ with st.expander("Forgot PIN?"):
164
+ recovery_in = st.text_input("Enter Recovery Key")
165
+ if st.button("Reset Vault PIN"):
166
+ # Logic: If recovery key matches, delete config so they can start over
167
+ # Note: Old encrypted notes stay encrypted (this is the cost of security!)
168
+ if vl.verify_recovery_key(recovery_in): # You'll add this to vault_logic
169
+ os.remove("vault_config.json")
170
+ st.warning("Vault Identity Reset. Please refresh to set a new PIN.")
171
+ st.rerun()
172
+
173
+ else:
174
+ st.success("🔓 Vault is Open")
175
+ time_left = int(AUTO_LOCK_SECONDS - (time.time() - st.session_state.last_activity))
176
+ st.caption(f"Auto-locking in {max(0, time_left)}s")
177
+ if st.button("🔒 Close Vault", use_container_width=True):
178
+ st.session_state.vault_unlocked = False
179
+ st.session_state.master_pin = ""
180
+ st.rerun()
181
+
182
+ st.divider()
183
+ st.subheader("📊 AI Resources")
184
+ if os.path.exists("usage_stats.json"):
185
+ with open("usage_stats.json", "r") as f:
186
+ stats = json.load(f)
187
+ col1, col2 = st.columns(2)
188
+ col1.metric("Tokens", f"{int(stats['total_tokens'])}")
189
+ col2.metric("Cost", f"${stats['total_cost']:.5f}")
190
+ else:
191
+ st.info("No AI usage data yet.")
192
+
193
+ # --- 5. MAIN PAGE ---
194
+ st.title(vl.get_page_heading(st.session_state.vault_unlocked))
195
+
196
+ # --- 6. RAG CHAT INTERFACE ---
197
+ if st.session_state.vault_unlocked:
198
+ with st.expander("💬 Ask Your Vault (AI Search)", expanded=False):
199
+ user_query = st.text_input("Ask a question about your notes:", placeholder="e.g., What are my goals for 2026?")
200
+
201
+ if user_query:
202
+ update_activity()
203
+ # 1. Prepare Decrypted Context
204
+ decrypted_texts = []
205
+ for n in st.session_state.notes:
206
+ content = vl.decrypt_data(n['content'], st.session_state.master_pin) if n.get('secret') else n['content']
207
+ decrypted_texts.append(content)
208
+
209
+ # 2. RAG Logic
210
+ index, text_data = vl.create_vector_index([{"content": t} for t in decrypted_texts])
211
+ context_chunks = vl.query_vault(user_query, index, text_data)
212
+
213
+ # 3. AI Answer
214
+ context_str = "\n".join(context_chunks)
215
+ prompt = f"Using ONLY these notes:\n{context_str}\n\nAnswer this question: {user_query}"
216
+
217
+ with st.spinner("Searching Vault..."):
218
+ answer = vl.get_gemini_response(user_query, context_str)
219
+ st.info(f"**AI Answer:**\n{answer}")
220
+
221
+ # 4. Feedback System ---
222
+ st.write("---")
223
+ st.write("Was this answer helpful?")
224
+ f_col1, f_col2 = st.columns([1, 5])
225
+
226
+ if f_col1.button("✅ Yes", key="fb_yes"):
227
+ vl.log_feedback(user_query, answer, context_str, "Correct")
228
+ st.success("Thanks for the feedback!")
229
+
230
+ if f_col2.button("❌ No / Wrong", key="fb_no"):
231
+ vl.log_feedback(user_query, answer, context_str, "Wrong")
232
+ st.warning("Logged as a failure.")
233
+
234
+ st.divider()
235
+ search = st.text_input("🔍 Search...", placeholder="Filter notes...")
236
+
237
+ # --- 7. Display Grid ---
238
+ cols = st.columns(3)
239
+ filtered = vl.get_filtered_notes(st.session_state.notes, st.session_state.vault_unlocked, search)
240
+
241
+ for idx, note in enumerate(filtered):
242
+ with cols[idx % 3]:
243
+ with st.container(border=True):
244
+ display_content = vl.decrypt_data(note['content'], st.session_state.master_pin) if (note.get('secret') and st.session_state.vault_unlocked) else note['content']
245
+
246
+ st.subheader(f"🔒 {note['title']}" if note.get('secret') else note['title'])
247
+ st.write(display_content[:200] + "..." if len(display_content) > 200 else display_content)
248
+ st.caption(f"🕒 {note['timestamp']}")
249
+
250
+ eb, db = st.columns(2)
251
+ with eb:
252
+ if st.button("✏️ Edit", key=f"e_{note['id']}"):
253
+ update_activity()
254
+ st.session_state.edit_note_id = note['id']
255
+ st.rerun()
256
+ with db:
257
+ if st.button("🗑️ Delete", key=f"d_{note['id']}"):
258
+ st.session_state.notes = [x for x in st.session_state.notes if x['id'] != note['id']]
259
+ vl.save_notes(st.session_state.notes)
260
+ st.rerun()
261
+
262
+ st.divider()
263
+ p_col, d_col = st.columns(2)
264
+ with p_col:
265
+ pdf_bytes = vl.create_pdf(note['title'], display_content)
266
+ st.download_button("📄 Download PDF", data=pdf_bytes, file_name=f"{note['title']}.pdf", key=f"pdf_{note['id']}", use_container_width=True)
267
+ with d_col:
268
+ docx_bytes = vl.create_docx(note['title'], display_content)
269
+ st.download_button("📝 Download DOCX", data=docx_bytes, file_name=f"{note['title']}.docx", key=f"docx_{note['id']}", use_container_width=True)
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit>=1.42.0
2
+ cryptography
3
+ textblob
4
+ fpdf2
5
+ python-docx
6
+ faiss-cpu
7
+ numpy
8
+ sentence-transformers
9
+ google-genai
10
+ python-dotenv
11
+ nltk
simple_notes.json ADDED
@@ -0,0 +1 @@
 
 
1
+ []
vault_logic.py ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import base64
4
+ import hashlib
5
+ import time
6
+ from datetime import datetime
7
+ from cryptography.fernet import Fernet
8
+ from cryptography.hazmat.primitives import hashes
9
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
10
+ from textblob import TextBlob
11
+ from fpdf import FPDF
12
+ from docx import Document
13
+ import io
14
+ import faiss
15
+ import numpy as np
16
+ from sentence_transformers import SentenceTransformer
17
+ import secrets
18
+ import string
19
+ from google import genai # Gemini
20
+ from dotenv import load_dotenv
21
+ import nltk
22
+
23
+ try:
24
+ nltk.data.find('tokenizers/punkt_tab')
25
+ except LookupError:
26
+ nltk.download('punkt_tab')
27
+
28
+ # This line reads the .env file and makes the API key available
29
+ load_dotenv(override=True) # 'override=True' forces it to refresh the key if you changed .env
30
+
31
+ NOTES_FILE = "notes.json"
32
+ CONFIG_FILE = "vault_config.json"
33
+
34
+ # Load a small, fast model for embeddings (runs locally)
35
+ embed_model = SentenceTransformer('all-MiniLM-L6-v2')
36
+
37
+ # --- 0. PIN & KEY LOGIC ---
38
+ def get_pin_hash(pin: str):
39
+ """Creates a secure SHA-256 hash of the PIN"""
40
+ return hashlib.sha256(pin.encode()).hexdigest()
41
+
42
+ def is_vault_initialized():
43
+ """Checks if a user has set a PIN yet"""
44
+ return os.path.exists(CONFIG_FILE)
45
+
46
+ def generate_recovery_key():
47
+ """Generates a random 16-character recovery string"""
48
+ chars = string.ascii_uppercase + string.digits
49
+ return ''.join(secrets.choice(chars) for _ in range(16))
50
+
51
+ def initialize_vault(pin: str, recovery_key: str):
52
+ """Saves the user's initial PIN hash and recovery key hash"""
53
+ with open(CONFIG_FILE, "w") as f:
54
+ json.dump({"pin_hash": get_pin_hash(pin),
55
+ "recovery_hash": get_pin_hash(recovery_key) # We hash the recovery key too!
56
+ }, f)
57
+
58
+ def verify_recovery_key(input_key: str):
59
+ """Verifies the recovery key against the stored hash"""
60
+ if not is_vault_initialized():
61
+ return False
62
+ with open(CONFIG_FILE, "r") as f:
63
+ stored_recovery_hash = json.load(f).get("recovery_hash")
64
+ return get_pin_hash(input_key) == stored_recovery_hash
65
+
66
+ def verify_pin(input_pin: str):
67
+ """Verifies input against stored hash"""
68
+ if not is_vault_initialized():
69
+ return False
70
+ with open(CONFIG_FILE, "r") as f:
71
+ stored_hash = json.load(f).get("pin_hash")
72
+ return get_pin_hash(input_pin) == stored_hash
73
+
74
+ # --- 1. KEY GENERATION (From your PIN) ---
75
+ def generate_key(pin: str):
76
+ """Derives a functional encryption key from the user's PIN"""
77
+ password = pin.encode()
78
+ # We use a static salt for this local project (Professional apps use random salts)
79
+ salt = b'stable_salt_123'
80
+ kdf = PBKDF2HMAC(
81
+ algorithm=hashes.SHA256(),
82
+ length=32,
83
+ salt=salt,
84
+ iterations=100000,
85
+ )
86
+ key = base64.urlsafe_b64encode(kdf.derive(password))
87
+ return Fernet(key)
88
+
89
+ # --- 2. ENCRYPTION / DECRYPTION ---
90
+ def encrypt_data(data_string, pin):
91
+ """Turns readable text into scrambled code"""
92
+ f = generate_key(pin)
93
+ return f.encrypt(data_string.encode()).decode()
94
+
95
+ def decrypt_data(encrypted_string, pin):
96
+ """Turns scrambled code back into readable text"""
97
+ try:
98
+ f = generate_key(pin)
99
+ return f.decrypt(encrypted_string.encode()).decode()
100
+ except Exception:
101
+ return "[Decryption Error: Check PIN]"
102
+
103
+ # --- 3. UPDATED LOAD/SAVE ---
104
+ def load_notes():
105
+ if os.path.exists(NOTES_FILE):
106
+ with open(NOTES_FILE, "r") as f:
107
+ return json.load(f)
108
+ return []
109
+
110
+ def save_notes(notes_list):
111
+ with open(NOTES_FILE, "w") as f:
112
+ json.dump(notes_list, f, indent=4)
113
+
114
+ def get_page_heading(is_unlocked):
115
+ return "🛡️ Safe Vault" if is_unlocked else "📝 My Notes"
116
+
117
+ def get_filtered_notes(all_notes, is_unlocked, search_query):
118
+ visible = all_notes
119
+ if not is_unlocked:
120
+ visible = [n for n in all_notes if not n.get('secret', False)]
121
+
122
+ query = search_query.lower()
123
+ return [n for n in visible if query in n['title'].lower() or query in n['content'].lower()]
124
+
125
+ # --- 4. AI FEATURES ---
126
+ def ai_summarize_text(text):
127
+ """Uses NLP to extract key points from long notes."""
128
+ if len(text) < 50:
129
+ return text # Don't summarize very short notes
130
+
131
+ blob = TextBlob(text)
132
+ # Extracting sentences and picking the top 2 for a summary
133
+ sentences = [str(s) for s in blob.sentences]
134
+
135
+ if len(sentences) > 2:
136
+ summary = "✨ AI Summary:\n" + "\n".join([f"• {s}" for s in sentences[:2]])
137
+
138
+ # NEW: Log the usage here!
139
+ track_usage(text, type="input")
140
+ track_usage(summary, type="output")
141
+ return summary
142
+ return text
143
+
144
+ # --- 5. EXPORTS ---
145
+ # The Logic Functions : These handle the actual file creation.
146
+ def create_pdf(title, content):
147
+ try:
148
+ pdf = FPDF()
149
+ pdf.add_page()
150
+
151
+ # Updated to match your folder: fonts -> dejavu-sans -> DejaVuSans.ttf
152
+ font_path = os.path.join("fonts", "dejavu-sans", "DejaVuSans.ttf")
153
+
154
+ if os.path.exists(font_path):
155
+ pdf.add_font('DejaVu', '', font_path, uni=True)
156
+ pdf.set_font('DejaVu', '', 14)
157
+ else:
158
+ # This is where it was falling back before, causing the "?" symbols
159
+ pdf.set_font("Arial", "B", 16)
160
+
161
+ # Write Title
162
+ pdf.multi_cell(0, 10, title)
163
+ pdf.ln(5)
164
+
165
+ # Write Content
166
+ if os.path.exists(font_path):
167
+ pdf.set_font('DejaVu', '', 12)
168
+ else:
169
+ pdf.set_font("Arial", "", 12)
170
+
171
+ pdf.multi_cell(0, 10, content)
172
+
173
+ # Output as bytes
174
+ return pdf.output(dest='S').encode('latin-1')
175
+ except Exception as e:
176
+ return f"PDF Error: {str(e)}".encode('latin-1')
177
+
178
+ def create_docx(title, content):
179
+ doc = Document()
180
+ doc.add_heading(title, 0)
181
+ doc.add_paragraph(content)
182
+ bio = io.BytesIO()
183
+ doc.save(bio)
184
+ return bio.getvalue()
185
+
186
+ # --- 6. AI & RESOURCE TRACKER ---
187
+ def track_usage(text, type="input"):
188
+ # Rough estimation: 1 token approx 4 characters
189
+ tokens = len(text) / 4
190
+ cost = (tokens / 1000) * 0.000125 # Estimate for Gemini 1.5 Flash
191
+
192
+ # Save to a local JSON file to persist data
193
+ usage_file = "usage_stats.json"
194
+ stats = {"total_tokens": 0, "total_cost": 0.0}
195
+
196
+ if os.path.exists(usage_file):
197
+ with open(usage_file, "r") as f:
198
+ stats = json.load(f)
199
+
200
+ stats["total_tokens"] += tokens
201
+ stats["total_cost"] += cost
202
+
203
+ with open(usage_file, "w") as f:
204
+ json.dump(stats, f)
205
+
206
+ return stats
207
+
208
+ # --- 7. RAG & FEEDBACK ---
209
+ def create_vector_index(notes):
210
+ """Turns notes into a searchable mathematical index"""
211
+ if not notes:
212
+ return None, []
213
+
214
+ # Extract only the text content
215
+ text_data = [n['content'] for n in notes]
216
+
217
+ # Convert text to vectors (embeddings)
218
+ embeddings = embed_model.encode(text_data)
219
+
220
+ # Create the FAISS index
221
+ dimension = embeddings.shape[1]
222
+ index = faiss.IndexFlatL2(dimension)
223
+ index.add(np.array(embeddings).astype('float32'))
224
+
225
+ return index, text_data
226
+
227
+ def query_vault(query, index, text_data, top_k=2):
228
+ """Finds the most relevant notes for a question"""
229
+ if index is None:
230
+ return "No notes found to search."
231
+
232
+ # Convert question to vector
233
+ query_vector = embed_model.encode([query])
234
+
235
+ # Search the index
236
+ distances, indices = index.search(np.array(query_vector).astype('float32'), top_k)
237
+
238
+ # Pull the relevant text chunks
239
+ results = [text_data[i] for i in indices[0] if i != -1]
240
+ return results
241
+
242
+ def log_feedback(query, answer, context, status):
243
+ log_file = "feedback_log.json"
244
+ entry = {
245
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
246
+ "query": query,
247
+ "answer": answer,
248
+ "context_used": context,
249
+ "status": status # "Correct" or "Wrong"
250
+ }
251
+
252
+ logs = []
253
+ if os.path.exists(log_file):
254
+ with open(log_file, "r") as f:
255
+ logs = json.load(f)
256
+
257
+ logs.append(entry)
258
+ with open(log_file, "w") as f:
259
+ json.dump(logs, f, indent=4)
260
+
261
+ # --- 8. GEMINI AI ENGINE ---
262
+ def get_gemini_response(user_query, context_str):
263
+ """Connects to Google Gemini API for free AI logic"""
264
+ api_key = os.getenv("GEMINI_API_KEY")
265
+ if not api_key:
266
+ return "⚠️ Error: GEMINI_API_KEY not found in Hugging Face Secrets."
267
+
268
+ try:
269
+ # New initialization style
270
+ client = genai.Client(api_key=api_key)
271
+
272
+ prompt = f"""
273
+ You are a secure vault assistant. Use the following retrieved notes to answer.
274
+ Notes Context: {context_str}
275
+ User Question: {user_query}
276
+ """
277
+
278
+ # Inside your loop or before you call the model
279
+ time.sleep(2) # Wait 2 seconds between requests
280
+ # Simplified model call
281
+ response = client.models.generate_content(
282
+ model="gemini-2.5-flash-lite",
283
+ contents=prompt
284
+ )
285
+
286
+ track_usage(prompt, type="input")
287
+ track_usage(response.text, type="output")
288
+ return response.text
289
+ except Exception as e:
290
+ return f"❌ AI Engine Error: {str(e)}"