', methods=['POST', 'GET'])
def fetch_url(random_url):
"""Return the decrypted note. Begin short destruction timer.
@@ -77,6 +85,7 @@ def fetch_url(random_url):
else:
return render_template('keyerror.html', random=note.url)
+
if __name__ == '__main__':
DNOTE.debug = True
DNOTE.run()
diff --git a/dnote/note.py b/dnote/note.py
index 89a63a3..10124e6 100644
--- a/dnote/note.py
+++ b/dnote/note.py
@@ -1,52 +1,79 @@
"""Encrypts and decrypts notes."""
import base64
+import codecs
+import configparser
import os
import sys
import zlib
from Crypto.Cipher import AES
from Crypto.Hash import HMAC, SHA512
-from Crypto.Protocol import KDF
+from Crypto.Protocol.KDF import PBKDF2
from Crypto.Util import Counter
-
-import ConfigParser
+from . import utils
# copy the config file from conf dir to either /etc/dnote or ~/.dnote,
# then run this script.
-config = ConfigParser.SafeConfigParser()
+try:
+
+ config = configparser.ConfigParser()
+
+ for path in ['/etc/dnote', '~/.dnote']:
+ expanded_path = os.path.join(os.path.expanduser(path), 'd-note.ini')
+ if os.path.exists(expanded_path):
+ try:
+ config.read(expanded_path)
+ print(f"Using config file: {expanded_path}")
+ break
+ except configparser.InterpolationSyntaxError as e:
+ raise EOFError(f"Unable to parse configuration file properly: {e}")
+ else:
+ raise ValueError("Config file not found")
-for path in ['/etc/dnote', '~/.dnote']:
- expanded_path = "{0}/{1}".format(os.path.expanduser(path), 'd-note.ini')
- if os.path.exists(expanded_path):
- try:
- f = open(expanded_path)
- config.readfp(f)
- f.close()
- except ConfigParser.InterpolationSyntaxError as e:
- raise EOFError("Unable to parse configuration file properly: {0}".format(e))
+ cfgs = config.defaults()
-cfgs = {}
+ for section in config.sections():
+ if section not in cfgs:
+ cfgs[section] = {}
-for section in config.sections():
- if not cfgs.has_key(section):
- cfgs[section] = {}
+ for k, v in config.items(section):
+ cfgs[section][k] = v
- for k, v in config.items(section):
- cfgs[section][k] = v
+ dconfig_path = os.path.expanduser(cfgs.get('dnote', {}).get('config_path'))
+ dconfig = os.path.join(dconfig_path, "dconfig.py")
-dconfig_path = os.path.expanduser(cfgs['dnote']['config_path'])
-dconfig = dconfig_path + "/dconfig.py"
+ # add dconfig.py to the sys.path
+ sys.path.append(dconfig_path)
-# add dconfig.py to the sys.path
-sys.path.append(dconfig_path)
+except Exception as e:
+ raise Exception(f"unable to load config: {e}")
try:
import dconfig
except ImportError:
- print "You need to run 'generate_dnote_hashes' as part of the installation."
+ print("You need to run 'generate_dnote_hashes' as part of the installation.")
os.sys.exit(1)
-data_dir = os.path.expanduser(cfgs['dnote']['data_dir'])
+data_dir = cfgs.get('dnote').get('data_dir')
+
+
+def cleave_hash(hash: str, remove=True):
+ """ Generic function that can either strip off trailing equals signs from a base64 encoded
+ byte string or figures out how to put them back"""
+ if remove:
+ return hash.replace('=', '')
+ else:
+ # 3 goes to identify the right ending for the hash
+ for i in range(3):
+ padding = '=' * i
+ try:
+ tmp_hash = f"{hash}{padding}"
+ tmp_b64dec = base64.urlsafe_b64decode(utils.enc(tmp_hash, "utf-8"))
+ return tmp_hash
+ except Exception as e:
+ continue
+ raise ValueError("The partial hash you passed in is not compatible with this function")
+
class Note(object):
"""Note Model"""
@@ -59,9 +86,13 @@ class Note(object):
passphrase = None # User provided passphrase
dkey = None # Duress passphrase
plaintext = None # Plain text note
- ciphertext = None # Encrypted text
+ ciphertext = None # encrypted text
+ byte_size = None # Used to calculate the size of the URL and duress key
def __init__(self, url=None):
+ """initialise for Note object, url is optional"""
+ # load the byte_size from the config
+ self.byte_size = max(int(cfgs['dnote'].get('byte_size', 16)), 4)
if url is None:
self.create_url()
else:
@@ -73,10 +104,13 @@ def exists(self):
def path(self, kind=None):
"""Return the file path to the note file"""
+ if isinstance(self.fname, bytes):
+ self.fname = utils.dec(self.fname, "utf-8")
+ file_path = os.path.join(os.path.expanduser(data_dir), self.fname)
if kind is None:
- return '%s/%s' % (data_dir, self.fname)
+ return file_path
else:
- return '%s/%s.%s' % (data_dir, self.fname, kind)
+ return f'{file_path}.{kind}'
def create_url(self):
"""Create a cryptographic nonce for our URL, and use PBKDF2 with our
@@ -86,16 +120,10 @@ def create_url(self):
- 128-bits for file name
- 256-bits for AES-256 key
- 512-bits for HMAC-SHA512 key"""
-
- self.nonce = os.urandom(16)
- self.f_key = KDF.PBKDF2(
- self.nonce, dconfig.nonce_salt.decode("hex"), 16)
- self.aes_key = KDF.PBKDF2(
- self.nonce, dconfig.aes_salt.decode("hex"), 32)
- self.mac_key = KDF.PBKDF2(
- self.nonce, dconfig.mac_salt.decode("hex"), 64)
- self.url = base64.urlsafe_b64encode(self.nonce)[:22]
- self.fname = base64.urlsafe_b64encode(self.f_key)[:22]
+ self.nonce = utils.get_rand_bytes(self.byte_size)
+ self.fname_and_fkey()
+ self.aes_and_mac(self.nonce)
+ self.url = cleave_hash(utils.dec(base64.urlsafe_b64encode(self.nonce), "utf-8"))
if self.exists():
return self.create_url()
@@ -105,35 +133,33 @@ def decode_url(self, url):
keyword arguments:
url -- the url after the FQDN provided by the client"""
-
self.url = url
- url = url + "==" # add the padding back
- self.nonce = base64.urlsafe_b64decode(url.encode("utf-8"))
- self.f_key = KDF.PBKDF2(
- self.nonce, dconfig.nonce_salt.decode("hex"), 16)
- self.aes_key = KDF.PBKDF2(
- self.nonce, dconfig.aes_salt.decode("hex"), 32)
- self.mac_key = KDF.PBKDF2(
- self.nonce, dconfig.mac_salt.decode("hex"), 64)
- duress = KDF.PBKDF2(
- self.nonce, dconfig.duress_salt.decode("hex"), 16)
- self.dkey = base64.urlsafe_b64encode(duress)[:22]
- self.fname = base64.urlsafe_b64encode(self.f_key)[:22]
+ obj = cleave_hash(url, remove=False)
+ self.nonce = base64.urlsafe_b64decode(utils.enc(obj, "utf-8"))
+ self.fname_and_fkey()
+ self.aes_and_mac(self.nonce)
+ self.duress_key()
+
+ def fname_and_fkey(self):
+ """Set filename and f_key"""
+ self.f_key = PBKDF2(self.nonce, utils.dec(dconfig.nonce_salt), 16)
+ self.fname = cleave_hash(utils.dec(base64.urlsafe_b64encode(self.f_key), "utf-8"))
+
+ def aes_and_mac(self, obj):
+ """Set AES and HMAC keys"""
+ self.aes_key = PBKDF2(obj, utils.dec(dconfig.aes_salt), 32)
+ self.mac_key = PBKDF2(obj, utils.dec(dconfig.mac_salt), 64)
def set_passphrase(self, passphrase):
"""Set a user defined passphrase to override the AES and HMAC keys"""
self.passphrase = passphrase
- self.aes_key = KDF.PBKDF2(
- passphrase, dconfig.aes_salt.decode("hex"), 32)
- self.mac_key = KDF.PBKDF2(
- passphrase, dconfig.mac_salt.decode("hex"), 64)
+ self.aes_and_mac(passphrase)
def duress_key(self):
"""Generates a duress key for Big Brother. It is stored on disk in
plaintext."""
- duress_key = KDF.PBKDF2(
- self.nonce, dconfig.duress_salt.decode('hex'), 16)
- self.dkey = base64.urlsafe_b64encode(duress_key)[:22]
+ duress_key = PBKDF2(self.nonce, utils.dec(dconfig.duress_salt), self.byte_size)
+ self.dkey = cleave_hash(utils.dec(base64.urlsafe_b64encode(duress_key), "utf-8"))
def secure_remove(self):
"""Securely overwrite any file, then remove the file. Do not make any
@@ -143,51 +169,53 @@ def secure_remove(self):
for kind in (None, 'key', 'dkey'):
if not os.path.exists(self.path(kind)):
continue
- with open(self.path(kind), "r+") as note:
- for char in xrange(os.stat(note.name).st_size):
- note.seek(char)
- note.write(str(os.urandom(1)))
+ with open(self.path(kind), "r+b") as note:
+ for byt_idx in range(os.stat(note.name).st_size):
+ note.seek(byt_idx)
+ note.write(utils.get_rand_bytes(1))
os.remove(self.path(kind))
def encrypt(self):
- """Encrypt a plaintext to a URI file.
+ """encrypt a plaintext to a URI file.
All files are encrypted with AES in CTR mode. HMAC-SHA512 is used
to provide authenticated encryption ( encrypt then mac ). No private
keys are stored on the server."""
- plain = zlib.compress(self.plaintext.encode('utf-8'))
- with open(self.path(), 'w') as note:
- init_value = os.urandom(12)
- ctr = Counter.new(128,
- initial_value=long(init_value.encode('hex'), 16))
+ plain = zlib.compress(utils.enc(self.plaintext, 'utf-8'))
+ with open(self.path(), 'wb') as note:
+ init_value = utils.get_rand_bytes(12)
+ ctr = Counter.new(128, initial_value=int(utils.enc(init_value), 16))
aes = AES.new(self.aes_key, AES.MODE_CTR, counter=ctr)
ciphertext = aes.encrypt(plain)
- ciphertext = init_value + ciphertext
- hmac = HMAC.new(self.mac_key, ciphertext, SHA512)
- ciphertext = hmac.digest() + ciphertext
- note.write(ciphertext)
+ ciphertext_with_init = init_value + ciphertext
+ hmac = HMAC.new(self.mac_key, ciphertext_with_init, SHA512)
+ # ciphertext = hmac.digest() + ciphertext
+ hmac_dig = hmac.digest()
+ ciphertext_with_init_and_hmac = hmac_dig + ciphertext_with_init
+ note.write(ciphertext_with_init_and_hmac)
def decrypt(self):
- """Decrypt the ciphertext from a given URI file."""
+ """decrypt the ciphertext from a given URI file."""
- with open(self.path(), 'r') as note:
+ with open(self.path(), 'rb') as note:
message = note.read()
tag = message[:64]
data = message[64:]
init_value = data[:12]
body = data[12:]
- ctr = Counter.new(128, initial_value=long(init_value.encode('hex'), 16))
+ ctr = Counter.new(128, initial_value=int(utils.enc(init_value), 16))
aes = AES.new(self.aes_key, AES.MODE_CTR, counter=ctr)
plaintext = aes.decrypt(body)
# check the message tags, return True if is good
# constant time comparison
tag2 = HMAC.new(self.mac_key, data, SHA512).digest()
hmac_check = 0
- for char1, char2 in zip(tag, tag2):
- hmac_check |= ord(char1) ^ ord(char2)
+ for byte1, byte2 in zip(tag, tag2):
+ # hmac_check |= ord(char1) ^ ord(char2)
+ hmac_check |= byte1 ^ byte2
if hmac_check == 0:
- self.plaintext = zlib.decompress(plaintext).decode('utf-8')
+ self.plaintext = utils.dec(zlib.decompress(plaintext), 'utf-8')
else:
return False
return True
diff --git a/dnote/templates/keyerror.html b/dnote/templates/keyerror.html
index 71c7ed1..80b6de0 100644
--- a/dnote/templates/keyerror.html
+++ b/dnote/templates/keyerror.html
@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% block title %}Note Error{% endblock %}
{% block content %}
- Error
- Your password was entered in error. Please try again.
+Error
+Your password was entered in error. Please
+ try again.
{% endblock %}
diff --git a/dnote/templates/note.html b/dnote/templates/note.html
index 290c213..a3b01c9 100644
--- a/dnote/templates/note.html
+++ b/dnote/templates/note.html
@@ -1,15 +1,15 @@
{% extends "base.html" %}
{% block title %}Private Note{% endblock %}
{% block head %}
-
-
+
{% endblock %}
{% block content %}
- Reading Secret Note
- Here is your private note, decrypted only for your browser. This note has been securely destroyed on the server. In 3 minutes, you will be redirected back to the home page.
-
+Reading Secret Note
+Here is your private note, decrypted only for your browser. This note has been securely destroyed on the server. In 3
+ minutes, you will be redirected back to the home page.
+
{% endblock %}
diff --git a/dnote/templates/post.html b/dnote/templates/post.html
index c3c24dc..eb25512 100644
--- a/dnote/templates/post.html
+++ b/dnote/templates/post.html
@@ -1,63 +1,71 @@
{% extends "base.html" %}
{% block title %}Note Created{% endblock %}
{% block head %}
-
-
+
{% endblock %}
{% block destroy %}
- Destroy note
+Destroy
+ note
{% endblock%}
{% block content %}
- Your Secret Note Details
- If you submitted this note in error, you can destroy the note here.
- Your private url:
- {{ url_for('fetch_url', random_url=random, _external=True) }}
-
- Your private ID:
- {{ random }}
-
- ?
-
-
-
- {% if passphrase %}
- Your passphrase:
- {{ passphrase}}
-
- {% endif %}
- {% if duress %}
- Your duress key:
- {{ duress }}
-
- ?
-
-
-
- {% endif %}
- Alternatively, you can scan this QR code, and "share" it using your mobile device via text message, email, etc. NOTE: ZXing Barcode users: disable "Retrieve more info" from the settings, otherwise your recipient will NOT be able to view the post.
- Show QR Code
-
-
+ }
+
{% endblock %}
diff --git a/dnote/utils.py b/dnote/utils.py
index 7bdda52..7b456d4 100644
--- a/dnote/utils.py
+++ b/dnote/utils.py
@@ -1,19 +1,38 @@
"""Utility functions for d-note."""
+import codecs
+import os
import random
from Crypto.Hash import SHA
-from note import data_dir
+from .note import data_dir
+
+
+def dec(obj, encoding: str = 'hex'):
+ return codecs.decode(obj, encoding)
+
+
+def enc(obj, encoding: str = 'hex'):
+ return codecs.encode(obj, encoding)
+
+
+def get_rand_bytes(length: int = 16) -> bytes:
+ return os.urandom(length)
+
+
+def logit(name, var):
+ print(f"var: {name}, value: {var}, type: {type(var)}")
+
def duress_text():
"""Return 5 random sentences of the Zen of Python."""
import subprocess
text = ''
- python = subprocess.Popen(('python', '-c', 'import this'),
- stdout=subprocess.PIPE)
- sentence = [x for x in python.communicate()[0].splitlines() if x != '']
- for _ in range(5):
- text = text + random.choice(sentence) + ' '
+ python = subprocess.Popen(('python3', '-c', 'import this'), stdout=subprocess.PIPE)
+ lines = dec(python.communicate()[0], "utf-8").splitlines()
+ sentence = [x for x in lines if x != '']
+ text = ' '.join(random.choices(sentence, k=5))
return text
+
def verify_hashcash(token):
"""Return True or False based on the Hashcash token
@@ -26,10 +45,10 @@ def verify_hashcash(token):
Keyword arguments:
token -- a proposed Hashcash token to validate."""
- digest = SHA.new(token)
- with open('%s/hashcash.db' % data_dir, 'a+') as database:
+ digest = SHA.new(enc(token, "utf-8"))
+ with open(os.path.join(data_dir, 'hashcash.db'), 'a+') as database:
if digest.hexdigest()[:4] == '0000' and token not in database.read():
- database.write(token+'\n')
+ database.write(f'{token}\n')
return True
else:
return False
diff --git a/scripts/generate_dnote_hashes b/scripts/generate_dnote_hashes
index eb6f2aa..0aa9301 100755
--- a/scripts/generate_dnote_hashes
+++ b/scripts/generate_dnote_hashes
@@ -1,27 +1,29 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
import os
-import ConfigParser
+import configparser
+import codecs
# copy the config file from conf dir to either /etc/dnote or ~/.dnote,
# then run this script.
-config = ConfigParser.SafeConfigParser()
+config = configparser.ConfigParser()
for path in ['/etc/dnote', '~/.dnote']:
- expanded_path = "{0}/{1}".format(os.path.expanduser(path), 'd-note.ini')
+ expanded_path = os.path.join(os.path.expanduser(path), 'd-note.ini')
if os.path.exists(expanded_path):
- try:
- f = open(expanded_path)
- config.readfp(f)
- f.close()
- except ConfigParser.InterpolationSyntaxError as e:
- raise EOFError("Unable to parse configuration file properly: {0}".format(e))
+ print(f"Using config file: {expanded_path}")
+ try:
+ config.read(expanded_path)
+ print(f"Using config file: {expanded_path}")
+ break
+ except configparser.InterpolationSyntaxError as e:
+ raise EOFError(f"Unable to parse configuration file properly: {e}")
-cfgs = {}
+cfgs = config.defaults()
for section in config.sections():
- if not cfgs.has_key(section):
+ if section not in cfgs:
cfgs[section] = {}
for k, v in config.items(section):
@@ -31,18 +33,31 @@ for section in config.sections():
data_dir = os.path.expanduser(cfgs['dnote']['data_dir'])
if not os.path.isdir(data_dir):
- os.makedirs(data_dir, 0755)
+ os.makedirs(data_dir, mode=0o755)
dconfig_path = os.path.expanduser(cfgs['dnote']['config_path'])
-dconfig = dconfig_path + "/dconfig.py"
+dconfig = os.path.join(dconfig_path, "dconfig.py")
if not os.path.isdir(dconfig_path):
- os.makedirs(dconfig_path, 0755)
+ os.makedirs(dconfig_path, mode=0o755)
+
+
+def enc(obj, encoding: str = 'hex'):
+ return codecs.encode(obj, encoding)
+
+
+def get_rand_bytes(length: int = 16) -> bytes:
+ return os.urandom(length)
+
+
+def get_new_hash(length: int = 16):
+ return enc(get_rand_bytes(length))
+
if not os.path.exists(dconfig):
with open(dconfig, 'w') as f:
- f.write('aes_salt = "%s"\n' % os.urandom(16).encode('hex'))
- f.write('mac_salt = "%s"\n' % os.urandom(16).encode('hex'))
- f.write('nonce_salt = "%s"\n' % os.urandom(16).encode('hex'))
- f.write('duress_salt = "%s"\n' % os.urandom(16).encode('hex'))
- os.chmod(dconfig, 0440)
+ f.write(f'aes_salt = "{get_new_hash()}"\n')
+ f.write(f'mac_salt = "{get_new_hash()}"\n')
+ f.write(f'nonce_salt = "{get_new_hash()}"\n')
+ f.write(f'duress_salt = "{get_new_hash()}"\n')
+ os.chmod(dconfig, mode=0o440)
diff --git a/setup.py b/setup.py
index eb689b8..b665db1 100644
--- a/setup.py
+++ b/setup.py
@@ -1,26 +1,29 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
from setuptools import setup, find_packages
-
import os
import glob
+
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
+
setup(
name='dnote',
- version='1.0.1',
+ version='2.0.0',
description='d-note is a self-destructing notes web application',
packages=find_packages(),
- install_requires=['Flask'],
+ install_requires=['Flask', 'pycrypto'],
zip_safe=False,
include_package_data=True,
license='GPLv3',
author_email='aaron.toponce@gmail.com',
author='Aaron Toponce',
- maintainer='Clint Savage',
- url='http://github.com/atoponce/d-note',
+ maintainer='Jarrod Price',
+ url='http://github.com/Pyroseza/d-note',
long_description=read('README'),
scripts=['scripts/generate_dnote_hashes'],
+ data_files=[('/etc/dnote', ['d-note.ini']), ('/var/www/dnote', ['wsgi.py'])],
+ python_requires='==3.6.8',
)
diff --git a/wsgi.py b/wsgi.py
new file mode 100755
index 0000000..3f4ae87
--- /dev/null
+++ b/wsgi.py
@@ -0,0 +1,5 @@
+#!/usr/bin/env python3
+import sys
+import logging
+logging.basicConfig(stream=sys.stderr)
+from dnote import DNOTE as application