· 6 years ago · Apr 16, 2019, 02:46 AM
1import sqlite3
2import hashlib
3import base64
4from tkinter import *
5
6from cryptography.fernet import Fernet
7
8DB_FILE_NAME = 'my.db'
9
10
11
12class AuthenticationError(ValueError):
13 pass
14
15
16class EncryptedDB():
17
18 class ToEncrypt(bytes):
19 '''
20 A wrapper class around bytes for passing to sqlite.
21 '''
22
23 @staticmethod
24 def bytes_check(s, encoding='UTF-8'):
25 if isinstance(s, str):
26 s = bytes(s, encoding)
27 if not isinstance(s, bytes):
28 raise ValueError(
29 'must be bytes or str, not {}'.format(type(s))
30 )
31 return s
32 @staticmethod
33 def derive_password(p):
34 p = EncryptedDB.bytes_check(p)
35 return hashlib.sha256(p).digest()
36
37
38
39 def __init__(self):
40 self.db_file = DB_FILE_NAME
41 self.conn = sqlite3.connect(
42 self.db_file
43 ,detect_types=sqlite3.PARSE_DECLTYPES)
44 self.conn.row_factory = sqlite3.Row
45 self.conn.executescript('''
46 CREATE TABLE IF NOT EXISTS version_info (
47 version INTEGER
48 );
49 CREATE TABLE IF NOT EXISTS encryptedstring (
50 id INTEGER PRIMARY KEY AUTOINCREMENT
51 ,owner TEXT
52 ,data ENCRYPTED
53 ,description TEXT
54 ,FOREIGN KEY(owner) REFERENCES login(username)
55 );
56 CREATE TABLE IF NOT EXISTS login (
57 username TEXT PRIMARY KEY
58 ,password SHA256
59 ,salt BLOB
60 ,userkey ENCRYPTED
61 );
62 ''')
63
64 self.fernet = None
65 sqlite3.register_converter('SHA256', EncryptedDB.derive_password)
66
67
68 def requires_login(f):
69 def wrap(self, *args, **kwargs):
70 if hasattr(self, 'logged_in_user'):
71 r = f(self, *args, **kwargs)
72 else:
73 raise AuthenticationError(
74 'This method requires a prior call to login.'
75 )
76 return r
77 return wrap
78
79
80 def make_login(self, username, password):
81 self.conn.execute('''
82 insert into login(username, password) values(?,?)
83 ''', (username, password))
84 self.save()
85
86 def login(self, user, password):
87 r = self.conn.execute('''
88 select * from login
89 where username = ? and password = ?
90 ''', (user, password)).fetchone()
91 if r is None:
92 raise AuthenticationError(
93 'No such credential combination exists'
94 )
95
96 password = EncryptedDB.derive_password(password)
97 password = base64.urlsafe_b64encode(password)
98 self.fernet = Fernet(password)
99 sqlite3.register_converter('ENCRYPTED', self.fernet.decrypt)
100 sqlite3.register_adapter(EncryptedDB.ToEncrypt, self.fernet.encrypt)
101 self.logged_in_user = user
102
103
104
105 @requires_login
106 def insert(self, data, description=None):
107 data = EncryptedDB.bytes_check(data)
108 data = EncryptedDB.ToEncrypt(data)
109 if len(description) == 0:
110 description = None
111 self.conn.execute('''
112 insert into encryptedstring(owner, data, description) values(?,?,?)
113 ''', (self.logged_in_user, data, description) )
114
115 @requires_login
116 def get(self, *rowids):
117 return self.conn.execute('''
118 select * from encryptedstring
119 where id in ({})
120 '''.format(
121 ','.join('?'*len(rowids))
122 )
123 , rowids).fetchall()
124
125
126 @requires_login
127 def list(self):
128 return self.conn.execute('''
129 select id, description
130 from encryptedstring
131 where owner = ?
132 order by id
133 ''', (self.logged_in_user,)).fetchall()
134
135 def save(self):
136 try:
137 self.conn.execute('commit')
138 return True
139 except sqlite3.OperationalError:
140 pass
141 return False
142
143 def __str__(self):
144 if not hasattr(self, 'logged_in_user'):
145 return repr(self)
146 largest_id = self.conn.execute('''
147 select id
148 from encryptedstring
149
150 order by id desc
151 ''').fetchone()
152 if largest_id is None:
153 return ''
154 largest_id = largest_id['id']
155 fmt = f'{{:{largest_id}}}: {{}}\r'
156 s = ''
157 for r in self.list():
158 s += fmt.format(*r)
159 return s
160
161
162class App(Tk):
163 def __init__(self, *args, **kwargs):
164 super().__init__(*args, **kwargs)
165 self.db = EncryptedDB()
166 self.render_login()
167
168 def render_login(self):
169 self.num_failed_logins = 0
170 f = Frame(self)
171 self.nentry = Entry(f, width=20)
172 self.pentry = Entry(f, width=20, show=b'\xe2\x80\xa2'.decode())
173 self.errorlabel = Label(self, fg='red')
174 self.new_user = Button(self, text='New Login', command=self.add_creds)
175
176 f.pack()
177 self.nentry.grid(row=0,column=1)
178 self.pentry.grid(row=1,column=1)
179 Label(f, text='Username').grid(row=0,column=0)
180 Label(f, text='Password').grid(row=1,column=0)
181 Button(self, text='Submit', command=self.verify_creds).pack(anchor='center')
182 Button(self, text='New Login', command=self.add_creds).pack(side=RIGHT,anchor='e')
183 self.errorlabel.pack()
184
185 def add_creds(self):
186 u = self.nentry.get()
187 p = self.pentry.get()
188 if len(u) == 0 or len(p) == 0:
189 self.errorlabel.config(text='cant be blank')
190 return
191
192 try:
193 self.db.make_login(u, p)
194 except sqlite3.IntegrityError:
195 self.errorlabel.config(text='username taken')
196 return
197
198 self.verify_creds()
199
200
201 def verify_creds(self):
202 u = self.nentry.get()
203 p = self.pentry.get()
204 try:
205 self.db.login(u,p)
206 except AuthenticationError:
207 if self.num_failed_logins == 0:
208 self.errorlabel.config(text='invalid credentials')
209 self.num_failed_logins += 1
210 return
211
212 # login correct, do whatever
213 [w.pack_forget() for w in self.winfo_children()]
214 self.render_main()
215
216 def render_main(self):
217 self.geometry('800x600')
218 self.main_frame = Frame(self)
219 self.main_frame.pack(expand=True, fill=BOTH)
220 self.update_main_display()
221 Button(self, text='New Entry', command=self.insert_popup).pack()
222
223 def insert_popup(self):
224 def do_insert():
225 self.db.insert(
226 data.get(1.0, END)
227 ,description=desc.get()
228 )
229 self.db.save()
230 self.update_main_display()
231 w.destroy()
232
233
234 w = Toplevel(self)
235 f = Frame(w)
236 f.pack()
237 Label(f, text='Description:').pack()
238 desc = Entry(f)
239 desc.pack(expand=True, fill=X)
240 Label(f, text='Secret Text:').pack()
241 data = Text(f)
242 data.pack(expand=True, fill=BOTH)
243 Button(f, text='Submit', command=do_insert).pack()
244
245
246 def update_main_display(self):
247 for w in self.main_frame.winfo_children():
248 w.pack_forget()
249 for i, row in enumerate(self.db.list()):
250 d = row['description']
251 if d is None:
252 d = '< No description >'
253 Label(self.main_frame, text=str(i+1)).grid(row=i, column=0)
254 l = Label(self.main_frame, text=d)
255 l.id = row['id']
256 l.bind('<Button-1>', lambda cb: self.show_secret(l.id))
257 l.grid(row=i, column=1)
258
259 def show_secret(self, id):
260 s = self.db.get(id)
261 # get returns a list but we are just getting 1, so extract from list
262 if len(s) == 1:
263 s = s[0]['data'].decode()
264 else:
265 #this should never happen
266 s = '< error: this should never happen >'
267 raise RuntimeError(
268 'record id from update_main_display returned '
269 'more than one record. uh oh :)'
270 )
271
272 if s is not None:
273 w = Toplevel(self)
274 t = Text(w)
275 t.insert(0.0, s)
276 t.config(state=DISABLED)
277 t.pack()
278
279
280
281if __name__ == '__main__':
282 App().mainloop()