Implement writing of notes

This commit is contained in:
Tanner Collin 2017-10-04 20:36:27 -06:00
parent 8b82f96d4a
commit 1b816d971b
3 changed files with 127 additions and 24 deletions

87
api.py
View File

@ -2,6 +2,8 @@ import hashlib, hmac, json, requests, time
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
from Crypto.Cipher import AES from Crypto.Cipher import AES
from Crypto.Random import random
from copy import deepcopy
class RESTAPI: class RESTAPI:
@ -15,7 +17,10 @@ class RESTAPI:
def post(self, route, data=None): def post(self, route, data=None):
url = self.base_url + route url = self.base_url + route
return requests.post(url, data, headers=self.headers).json() print(data)
res = requests.post(url, json=data, headers=self.headers)
print(res.text)
return res.json()
def addHeader(self, header): def addHeader(self, header):
self.headers.update(header) self.headers.update(header)
@ -30,11 +35,29 @@ class EncryptionHelper:
pw = output[0 : split_length] pw = output[0 : split_length]
mk = output[split_length : split_length * 2] mk = output[split_length : split_length * 2]
ak = output[split_length * 2 : split_length * 3] ak = output[split_length * 2 : split_length * 3]
return {'pw': pw, 'mk': mk, 'ak': ak} return dict(pw=pw, mk=mk, ak=ak)
def pure_decryptResponseItems(self, response_items, keys): def encryptDirtyItems(self, dirty_items, keys):
return [self.pure_encryptItem(item, keys) for item in dirty_items]
def decryptResponseItems(self, response_items, keys):
return [self.pure_decryptItem(item, keys) for item in response_items] return [self.pure_decryptItem(item, keys) for item in response_items]
def pure_encryptItem(self, item, keys):
uuid = item['uuid']
content = json.dumps(item['content'])
item_key = hex(random.getrandbits(512))
item_key = item_key[2:].rjust(128, '0') # remove '0x', pad to 128
item_key_length = len(item_key)
item_ek = item_key[:item_key_length//2]
item_ak = item_key[item_key_length//2:]
enc_item = deepcopy(item)
enc_item['content'] = self.pure_encryptString002(content, item_ek, item_ak, uuid)
enc_item['enc_item_key'] = self.pure_encryptString002(item_key, keys['mk'], keys['ak'], uuid)
return enc_item
def pure_decryptItem(self, item, keys): def pure_decryptItem(self, item, keys):
uuid = item['uuid'] uuid = item['uuid']
content = item['content'] content = item['content']
@ -53,10 +76,28 @@ class EncryptionHelper:
else: else:
print('Invalid protocol version.') print('Invalid protocol version.')
dec_item = item dec_item = deepcopy(item)
dec_item['content'] = json.loads(dec_content) dec_item['content'] = json.loads(dec_content)
return dec_item return dec_item
def pure_encryptString002(self, string_to_encrypt, encryption_key, auth_key, uuid):
IV = hex(random.getrandbits(128))
IV = IV[2:].rjust(32, '0') # remove '0x', pad to 32
cipher = AES.new(unhexlify(encryption_key), AES.MODE_CBC, unhexlify(IV))
pt = string_to_encrypt.encode()
pad = 16 - len(pt) % 16
padded_pt = pt + pad * bytes([pad])
ciphertext = b64encode(cipher.encrypt(padded_pt)).decode()
string_to_auth = ':'.join(['002', uuid, IV, ciphertext])
auth_hash = hmac.new(unhexlify(auth_key), string_to_auth.encode(), 'sha256').digest()
auth_hash = hexlify(auth_hash).decode()
result = ':'.join(['002', auth_hash, uuid, IV, ciphertext])
return result
def pure_decryptString002(self, string_to_decrypt, encryption_key, auth_key, uuid): def pure_decryptString002(self, string_to_decrypt, encryption_key, auth_key, uuid):
components = string_to_decrypt.split(':') components = string_to_decrypt.split(':')
version = components[0] version = components[0]
@ -89,26 +130,44 @@ class StandardNotesAPI:
sync_token = None sync_token = None
def getAuthParamsForEmail(self): def getAuthParamsForEmail(self):
return self.api.get('/auth/params', {'email': self.username}) return self.api.get('/auth/params', dict(email=self.username))
def signIn(self, password): def signIn(self, password):
pw_info = self.getAuthParamsForEmail() pw_info = self.getAuthParamsForEmail()
self.keys = self.encryption_helper.pure_generatePasswordAndKey(password, pw_info['pw_salt'], pw_info['pw_cost']) self.keys = self.encryption_helper.pure_generatePasswordAndKey(password, pw_info['pw_salt'], pw_info['pw_cost'])
res = self.api.post('/auth/sign_in', {'email': self.username, 'password': self.keys['pw']}) res = self.api.post('/auth/sign_in', dict(email=self.username, password=self.keys['pw']))
self.api.addHeader({'Authorization': 'Bearer ' + res['token']}) self.api.addHeader(dict(Authorization='Bearer ' + res['token']))
def sync(self, dirty_items): def sync(self, dirty_items):
res = self.api.post('/items/sync', {'sync_token': self.sync_token}) items = self.handleDirtyItems(dirty_items)
print(json.dumps(res)) if items:
json_items = items
print(json_items)
else:
json_items = []
response = self.api.post('/items/sync', dict(sync_token=self.sync_token, items=json_items))
print(json.dumps(response))
self.sync_token = res['sync_token'] self.sync_token = response['sync_token']
return self.handleResponseItems(res['retrieved_items']) return self.handleResponseItems(response)
def handleResponseItems(self, response_items): def handleDirtyItems(self, dirty_items):
decrypted_items = self.encryption_helper.pure_decryptResponseItems(response_items, self.keys) items = self.encryption_helper.encryptDirtyItems(dirty_items, self.keys)
return decrypted_items return items
def handleResponseItems(self, response):
response_items = self.encryption_helper.decryptResponseItems(response['retrieved_items'], self.keys)
saved_items = self.encryption_helper.decryptResponseItems(response['saved_items'], self.keys)
return dict(response_items=response_items, saved_items=saved_items)
def __init__(self, username, password): def __init__(self, username, password):
self.api = RESTAPI(self.base_url) self.api = RESTAPI(self.base_url)
self.username = username self.username = username
self.signIn(password) self.signIn(password)
if __name__ == '__main__':
standard_notes = StandardNotesAPI('tanner@domain.com', 'complexpass')
test_item = standard_notes.encryption_helper.pure_encryptItem(dict(content=dict(hello='world'), uuid='1234'), standard_notes.keys)
print(test_item)
test_item = standard_notes.encryption_helper.pure_decryptItem(test_item, standard_notes.keys)
print(test_item)

View File

@ -3,7 +3,9 @@ from api import StandardNotesAPI
class ItemManager: class ItemManager:
items = {} items = {}
def mapResponseItemsToLocalItems(self, response_items): def mapResponseItemsToLocalItems(self, response_items, metadata_only=False):
DATA_KEYS = ['content', 'enc_item_key', 'auth_hash']
for response_item in response_items: for response_item in response_items:
uuid = response_item['uuid'] uuid = response_item['uuid']
@ -12,7 +14,28 @@ class ItemManager:
del self.items[uuid] del self.items[uuid]
continue continue
self.items[uuid] = response_item response_item['dirty'] = False
if uuid not in self.items:
self.items[uuid] = {}
for key, value in response_item.items():
if metadata_only and key in DATA_KEYS:
continue
self.items[uuid][key] = value
def syncItems(self):
dirty_items = [item for uuid, item in self.items.items() if item['dirty']]
# remove keys (note: this removed them from self.items as well)
for item in dirty_items:
item.pop('dirty', None)
item.pop('updated_at', None)
response = self.standard_notes.sync(dirty_items)
print('info: ', response)
self.mapResponseItemsToLocalItems(response['response_items'])
self.mapResponseItemsToLocalItems(response['saved_items'], metadata_only=True)
def getNotes(self): def getNotes(self):
notes = {} notes = {}
@ -37,7 +60,13 @@ class ItemManager:
uuid=item['uuid']) uuid=item['uuid'])
return notes return notes
def writeNote(self, uuid, text):
item = self.items[uuid]
item['content']['text'] = text.strip()
item['dirty'] = True
self.syncItems()
def __init__(self, username, password): def __init__(self, username, password):
self.standard_notes = StandardNotesAPI(username, password) self.standard_notes = StandardNotesAPI(username, password)
response_items = self.standard_notes.sync(None) self.syncItems()
self.mapResponseItemsToLocalItems(response_items)

View File

@ -4,10 +4,10 @@ import errno
import iso8601 import iso8601
import logging import logging
import os import os
import time
from stat import S_IFDIR, S_IFREG from stat import S_IFDIR, S_IFREG
from sys import argv, exit from sys import argv, exit
from datetime import datetime
from fuse import FUSE, FuseOSError, Operations, LoggingMixIn from fuse import FUSE, FuseOSError, Operations, LoggingMixIn
from itemmanager import ItemManager from itemmanager import ItemManager
@ -16,12 +16,11 @@ from itemmanager import ItemManager
class StandardNotesFS(LoggingMixIn, Operations): class StandardNotesFS(LoggingMixIn, Operations):
def __init__(self, path='.'): def __init__(self, path='.'):
self.item_manager = ItemManager('tanner@domain.com', 'complexpass') self.item_manager = ItemManager('tanner@domain.com', 'complexpass')
self.notes = self.item_manager.getNotes()
self.uid = os.getuid() self.uid = os.getuid()
self.gid = os.getgid() self.gid = os.getgid()
now = time.time() now = datetime.now().timestamp()
self.dir_stat = dict(st_mode=(S_IFDIR | 0o755), st_ctime=now, self.dir_stat = dict(st_mode=(S_IFDIR | 0o755), st_ctime=now,
st_mtime=now, st_atime=now, st_nlink=2, st_mtime=now, st_atime=now, st_nlink=2,
st_uid=self.uid, st_gid=self.gid) st_uid=self.uid, st_gid=self.gid)
@ -31,6 +30,8 @@ class StandardNotesFS(LoggingMixIn, Operations):
st_uid=self.uid, st_gid=self.gid) st_uid=self.uid, st_gid=self.gid)
def getattr(self, path, fh=None): def getattr(self, path, fh=None):
self.notes = self.item_manager.getNotes()
st = self.note_stat st = self.note_stat
path_parts = path.split('/') path_parts = path.split('/')
@ -48,6 +49,8 @@ class StandardNotesFS(LoggingMixIn, Operations):
raise FuseOSError(errno.ENOENT) raise FuseOSError(errno.ENOENT)
def readdir(self, path, fh): def readdir(self, path, fh):
self.notes = self.item_manager.getNotes()
dirents = ['.', '..'] dirents = ['.', '..']
if path == '/': if path == '/':
@ -55,12 +58,27 @@ class StandardNotesFS(LoggingMixIn, Operations):
return dirents return dirents
def read(self, path, size, offset, fh): def read(self, path, size, offset, fh):
self.notes = self.item_manager.getNotes()
path_parts = path.split('/') path_parts = path.split('/')
note_name = path_parts[1] note_name = path_parts[1]
note = self.notes[note_name] note = self.notes[note_name]
return note['text'][offset : offset + size].encode() return note['text'][offset : offset + size].encode()
def write(self, path, data, offset, fh):
self.notes = self.item_manager.getNotes()
path_parts = path.split('/')
note_name = path_parts[1]
note = self.notes[note_name]
text = note['text'][:offset] + data.decode()
uuid = note['uuid']
self.item_manager.writeNote(uuid, text)
return len(data)
def chmod(self, path, mode): def chmod(self, path, mode):
return 0 return 0
@ -97,9 +115,6 @@ class StandardNotesFS(LoggingMixIn, Operations):
def utimens(self, path, times=None): def utimens(self, path, times=None):
return 0 return 0
def write(self, path, data, offset, fh):
return 0
if __name__ == '__main__': if __name__ == '__main__':
if len(argv) != 2: if len(argv) != 2:
print('usage: %s <mountpoint>' % argv[0]) print('usage: %s <mountpoint>' % argv[0])