class BukuCrypt for encrypt and decrypt functions.

Nasty hack, but saves ~100ms. Given that encrypt/decrypt need
to be done manually, late loading doesn't seem outrageous here.
This commit is contained in:
Arun Prakash Jana 2016-06-01 22:21:55 +05:30
parent 219304cc68
commit 237f8857bc
No known key found for this signature in database
GPG Key ID: C0A712ED95043DCB

358
buku
View File

@ -29,21 +29,6 @@ import gzip
import io
import signal
# Import libraries needed for encryption
try:
import getpass
import hashlib
import struct
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
no_crypto = False
BLOCKSIZE = 65536
SALT_SIZE = 32
CHUNKSIZE = 0x80000 # Read/write 512 KB chunks
except ImportError:
no_crypto = True
# Globals
update = False # Update a bookmark in DB
@ -62,6 +47,12 @@ DELIMITER = ',' # Delimiter used to store tags in DB
_VERSION_ = '2.1' # Program version
# Crypto globals
BLOCKSIZE = 65536
SALT_SIZE = 32
CHUNKSIZE = 0x80000 # Read/write 512 KB chunks
class BMHTMLParser(HTMLParser.HTMLParser):
"""Class to parse and fetch the title from a HTML page, if available"""
@ -94,6 +85,187 @@ class BMHTMLParser(HTMLParser.HTMLParser):
pass
class BukuCrypt:
""" Class to handle encryption and decryption
of the database file. Functionally a separate entity.
Involves late imports in the static functions but it
saves ~100ms each time. Given that encrypt/decrypt are
not done automatically and any one should be called at
a time, this doesn't seem to be an outrageous approach.
"""
@staticmethod
def get_filehash(filepath):
"""Get the SHA256 hash of a file
Params: path to the file
"""
from hashlib import sha256
with open(filepath, 'rb') as f:
hasher = sha256()
buf = f.read(BLOCKSIZE)
while len(buf) > 0:
hasher.update(buf)
buf = f.read(BLOCKSIZE)
return hasher.digest()
@staticmethod
def encrypt_file(iterations):
"""Encrypt the bookmarks database file"""
try:
from getpass import getpass
import struct
from hashlib import sha256
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
except Exception:
print('cryptography missing')
sys.exit(1)
if iterations < 1:
printmsg('Iterations must be >= 1', 'ERROR')
sys.exit(1)
dbpath = os.path.join(BukuDb.get_dbdir_path(), 'bookmarks.db')
encpath = '%s.enc' % dbpath
if not os.path.exists(dbpath):
print('%s missing. Already encrypted?' % dbpath)
sys.exit(1)
# If both encrypted file and flat file exist, error out
if os.path.exists(dbpath) and os.path.exists(encpath):
printmsg('Both encrypted and flat DB files exist!', 'ERROR')
sys.exit(1)
password = ''
password = getpass()
passconfirm = getpass()
if password == '':
print('Empty password')
sys.exit(1)
if password != passconfirm:
print("Passwords don't match")
sys.exit(1)
# Get SHA256 hash of DB file
dbhash = BukuCrypt.get_filehash(dbpath)
# Generate random 256-bit salt and key
salt = os.urandom(SALT_SIZE)
key = ('%s%s' % (password, salt.decode('utf-8', 'replace'))).encode('utf-8')
for _ in range(iterations):
key = sha256(key).digest()
iv = os.urandom(16)
encryptor = Cipher(
algorithms.AES(key),
modes.CBC(iv),
backend=default_backend()
).encryptor()
filesize = os.path.getsize(dbpath)
with open(dbpath, 'rb') as infile:
with open(encpath, 'wb') as outfile:
outfile.write(struct.pack('<Q', filesize))
outfile.write(salt)
outfile.write(iv)
# Embed DB file hash in encrypted file
outfile.write(dbhash)
while True:
chunk = infile.read(CHUNKSIZE)
if len(chunk) == 0:
break
elif len(chunk) % 16 != 0:
chunk = '%s%s' % (chunk, ' ' * (16 - len(chunk) % 16))
outfile.write(encryptor.update(chunk) + encryptor.finalize())
os.remove(dbpath)
print('File encrypted')
sys.exit(0)
@staticmethod
def decrypt_file(iterations):
"""Decrypt the bookmarks database file"""
try:
from getpass import getpass
import struct
from hashlib import sha256
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
except Exception:
print('cryptography missing')
sys.exit(1)
if iterations < 1:
printmsg('Decryption failed', 'ERROR')
sys.exit(1)
dbpath = os.path.join(BukuDb.get_dbdir_path(), 'bookmarks.db')
encpath = '%s.enc' % dbpath
if not os.path.exists(encpath):
printmsg('%s missing' % encpath, 'ERROR')
sys.exit(1)
# If both encrypted file and flat file exist, error out
if os.path.exists(dbpath) and os.path.exists(encpath):
printmsg('Both encrypted and flat DB files exist!', 'ERROR')
sys.exit(1)
password = ''
password = getpass()
if password == '':
printmsg('Decryption failed', 'ERROR')
sys.exit(1)
with open(encpath, 'rb') as infile:
origsize = struct.unpack('<Q', infile.read(struct.calcsize('Q')))[0]
# Read 256-bit salt and generate key
salt = infile.read(32)
key = ('%s%s' % (password, salt.decode('utf-8', 'replace'))).encode('utf-8')
for _ in range(iterations):
key = sha256(key).digest()
iv = infile.read(16)
decryptor = Cipher(
algorithms.AES(key),
modes.CBC(iv),
backend=default_backend(),
).decryptor()
# Get original DB file's SHA256 hash from encrypted file
enchash = infile.read(32)
with open(dbpath, 'wb') as outfile:
while True:
chunk = infile.read(CHUNKSIZE)
if len(chunk) == 0:
break
outfile.write(decryptor.update(chunk) + decryptor.finalize())
outfile.truncate(origsize)
# Match hash of generated file with that of original DB file
dbhash = BukuCrypt.get_filehash(dbpath)
if dbhash != enchash:
os.remove(dbpath)
printmsg('Decryption failed', 'ERROR')
sys.exit(1)
else:
os.remove(encpath)
print('File decrypted')
class BukuDb:
def __init__(self, *args, **kwargs):
@ -1047,146 +1219,6 @@ def browser_open(url):
os.dup2(_stdout, 1)
def get_filehash(filepath):
"""Get the SHA256 hash of a file
Params: path to the file
"""
with open(filepath, 'rb') as f:
hasher = hashlib.sha256()
buf = f.read(BLOCKSIZE)
while len(buf) > 0:
hasher.update(buf)
buf = f.read(BLOCKSIZE)
return hasher.digest()
def encrypt_file(iterations):
"""Encrypt the bookmarks database file"""
dbpath = os.path.join(BukuDb.get_dbdir_path(), 'bookmarks.db')
encpath = '%s.enc' % dbpath
if not os.path.exists(dbpath):
print('%s missing. Already encrypted?' % dbpath)
sys.exit(1)
# If both encrypted file and flat file exist, error out
if os.path.exists(dbpath) and os.path.exists(encpath):
printmsg('Both encrypted and flat DB files exist!', 'ERROR')
sys.exit(1)
password = ''
password = getpass.getpass()
passconfirm = getpass.getpass()
if password == '':
print('Empty password')
sys.exit(1)
if password != passconfirm:
print("Passwords don't match")
sys.exit(1)
# Get SHA256 hash of DB file
dbhash = get_filehash(dbpath)
# Generate random 256-bit salt and key
salt = os.urandom(SALT_SIZE)
key = ('%s%s' % (password, salt.decode('utf-8', 'replace'))).encode('utf-8')
for _ in range(iterations):
key = hashlib.sha256(key).digest()
iv = os.urandom(16)
encryptor = Cipher(
algorithms.AES(key),
modes.CBC(iv),
backend=default_backend()
).encryptor()
filesize = os.path.getsize(dbpath)
with open(dbpath, 'rb') as infile:
with open(encpath, 'wb') as outfile:
outfile.write(struct.pack('<Q', filesize))
outfile.write(salt)
outfile.write(iv)
# Embed DB file hash in encrypted file
outfile.write(dbhash)
while True:
chunk = infile.read(CHUNKSIZE)
if len(chunk) == 0:
break
elif len(chunk) % 16 != 0:
chunk = '%s%s' % (chunk, ' ' * (16 - len(chunk) % 16))
outfile.write(encryptor.update(chunk) + encryptor.finalize())
os.remove(dbpath)
print('File encrypted')
sys.exit(0)
def decrypt_file(iterations):
"""Decrypt the bookmarks database file"""
dbpath = os.path.join(BukuDb.get_dbdir_path(), 'bookmarks.db')
encpath = '%s.enc' % dbpath
if not os.path.exists(encpath):
printmsg('%s missing' % encpath, 'ERROR')
sys.exit(1)
# If both encrypted file and flat file exist, error out
if os.path.exists(dbpath) and os.path.exists(encpath):
printmsg('Both encrypted and flat DB files exist!', 'ERROR')
sys.exit(1)
password = ''
password = getpass.getpass()
if password == '':
printmsg('Decryption failed', 'ERROR')
sys.exit(1)
with open(encpath, 'rb') as infile:
origsize = struct.unpack('<Q', infile.read(struct.calcsize('Q')))[0]
# Read 256-bit salt and generate key
salt = infile.read(32)
key = ('%s%s' % (password, salt.decode('utf-8', 'replace'))).encode('utf-8')
for _ in range(iterations):
key = hashlib.sha256(key).digest()
iv = infile.read(16)
decryptor = Cipher(
algorithms.AES(key),
modes.CBC(iv),
backend=default_backend(),
).decryptor()
# Get original DB file's SHA256 hash from encrypted file
enchash = infile.read(32)
with open(dbpath, 'wb') as outfile:
while True:
chunk = infile.read(CHUNKSIZE)
if len(chunk) == 0:
break
outfile.write(decryptor.update(chunk) + decryptor.finalize())
outfile.truncate(origsize)
# Match hash of generated file with that of original DB file
dbhash = get_filehash(dbpath)
if dbhash != enchash:
os.remove(dbpath)
printmsg('Decryption failed', 'ERROR')
sys.exit(1)
else:
os.remove(encpath)
print('File decrypted')
def sigint_handler(signum, frame):
"""Custom SIGINT handler"""
@ -1451,22 +1483,10 @@ if __name__ == '__main__':
# Handle encrypt/decrypt options at top priority
if args.encrypt is not None:
if no_crypto:
printmsg('cryptography missing', 'ERROR')
sys.exit(1)
if args.encrypt < 1:
printmsg('Iterations must be >= 1', 'ERROR')
sys.exit(1)
encrypt_file(args.encrypt)
BukuCrypt.encrypt_file(args.encrypt)
if args.decrypt is not None:
if no_crypto:
printmsg('cryptography missing', 'ERROR')
sys.exit(1)
if args.decrypt < 1:
printmsg('Decryption failed', 'ERROR')
sys.exit(1)
decrypt_file(args.decrypt)
BukuCrypt.decrypt_file(args.decrypt)
# Initialize the database and get handles
bdb = BukuDb()