Implement basic reading of all notes
This commit is contained in:
		
							
								
								
									
										149
									
								
								api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								api.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | |||||||
|  | import hashlib, hmac, json, requests, time | ||||||
|  | from base64 import b64encode, b64decode | ||||||
|  | from binascii import hexlify, unhexlify | ||||||
|  | from Crypto.Cipher import AES | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RESTAPI: | ||||||
|  |     def __init__(self, base_url): | ||||||
|  |         self.base_url = base_url | ||||||
|  |         self.headers = {} | ||||||
|  |  | ||||||
|  |     def get(self, route, params=None): | ||||||
|  |         url = self.base_url + route | ||||||
|  |         return requests.get(url, params, headers=self.headers).json() | ||||||
|  |  | ||||||
|  |     def post(self, route, data=None): | ||||||
|  |         url = self.base_url + route | ||||||
|  |         return requests.post(url, data, headers=self.headers).json() | ||||||
|  |  | ||||||
|  |     def addHeader(self, header): | ||||||
|  |         self.headers.update(header) | ||||||
|  |  | ||||||
|  | class EncryptionHelper: | ||||||
|  |     def pure_generatePasswordAndKey(self, password, pw_salt, pw_cost): | ||||||
|  |         output = hashlib.pbkdf2_hmac('sha512', password.encode(), pw_salt.encode(), pw_cost, dklen=96) | ||||||
|  |         output = hexlify(output).decode() | ||||||
|  |  | ||||||
|  |         output_length = len(output) | ||||||
|  |         split_length = output_length // 3 | ||||||
|  |         pw = output[0 : split_length] | ||||||
|  |         mk = output[split_length : split_length * 2] | ||||||
|  |         ak = output[split_length * 2 : split_length * 3] | ||||||
|  |         return {'pw': pw, 'mk': mk, 'ak': ak} | ||||||
|  |  | ||||||
|  |     def pure_decryptResponseItems(self, response_items, keys): | ||||||
|  |         return [self.pure_decryptItem(item, keys) for item in response_items] | ||||||
|  |  | ||||||
|  |     def pure_decryptItem(self, item, keys): | ||||||
|  |         uuid = item['uuid'] | ||||||
|  |         content = item['content'] | ||||||
|  |         enc_item_key = item['enc_item_key'] | ||||||
|  |  | ||||||
|  |         if not content: | ||||||
|  |             return item | ||||||
|  |  | ||||||
|  |         if content[:3] == '002': | ||||||
|  |             item_key = self.pure_decryptString002(enc_item_key, keys['mk'], keys['ak'], uuid) | ||||||
|  |             item_key_length = len(item_key) | ||||||
|  |             item_ek = item_key[:item_key_length//2] | ||||||
|  |             item_ak = item_key[item_key_length//2:] | ||||||
|  |  | ||||||
|  |             dec_content = self.pure_decryptString002(content, item_ek, item_ak, uuid) | ||||||
|  |         else: | ||||||
|  |             print('Invalid protocol version.') | ||||||
|  |  | ||||||
|  |         dec_item = item | ||||||
|  |         dec_item['content'] = json.loads(dec_content) | ||||||
|  |         return dec_item | ||||||
|  |  | ||||||
|  |     def pure_decryptString002(self, string_to_decrypt, encryption_key, auth_key, uuid): | ||||||
|  |         components = string_to_decrypt.split(':') | ||||||
|  |         version = components[0] | ||||||
|  |         auth_hash = components[1] | ||||||
|  |         local_uuid = components[2] | ||||||
|  |         IV = components[3] | ||||||
|  |         ciphertext = components[4] | ||||||
|  |  | ||||||
|  |         if local_uuid != uuid: | ||||||
|  |             print('UUID does not match.') | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         string_to_auth = ':'.join([version, uuid, IV, ciphertext]) | ||||||
|  |         local_auth_hash = hmac.new(unhexlify(auth_key), string_to_auth.encode(), 'sha256').digest() | ||||||
|  |         local_auth_hash = hexlify(local_auth_hash).decode() | ||||||
|  |  | ||||||
|  |         if local_auth_hash != auth_hash: | ||||||
|  |             print('Message has been tampered with.') | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         cipher = AES.new(unhexlify(encryption_key), AES.MODE_CBC, unhexlify(IV)) | ||||||
|  |         result = cipher.decrypt(b64decode(ciphertext)) | ||||||
|  |         result = result[:-result[-1]] # remove PKCS#7 padding | ||||||
|  |  | ||||||
|  |         return result.decode() | ||||||
|  |  | ||||||
|  | class ItemManager: | ||||||
|  |     items = {} | ||||||
|  |  | ||||||
|  |     def mapResponseItemsToLocalItems(self, response_items): | ||||||
|  |         for response_item in response_items: | ||||||
|  |             uuid = response_item['uuid'] | ||||||
|  |  | ||||||
|  |             if response_item['deleted']: | ||||||
|  |                 if uuid in self.items: | ||||||
|  |                     del self.items[uuid] | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             self.items[uuid] = response_item | ||||||
|  |  | ||||||
|  |     def getNotes(self): | ||||||
|  |         notes = {} | ||||||
|  |         for key, value in self.items.items(): | ||||||
|  |             if value['content_type'] == 'Note': | ||||||
|  |                 note = value['content'] | ||||||
|  |                 notes[note['title']] = note['text'] + '\n' | ||||||
|  |         return notes | ||||||
|  |  | ||||||
|  | class StandardNotesAPI: | ||||||
|  |     encryption_helper = EncryptionHelper() | ||||||
|  |     item_manager = ItemManager() | ||||||
|  |     base_url = 'https://sync.standardnotes.org' | ||||||
|  |     sync_token = None | ||||||
|  |  | ||||||
|  |     def getAuthParamsForEmail(self): | ||||||
|  |         return self.api.get('/auth/params', {'email': self.username}) | ||||||
|  |  | ||||||
|  |     def signIn(self, password): | ||||||
|  |         pw_info = self.getAuthParamsForEmail() | ||||||
|  |         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']}) | ||||||
|  |         self.api.addHeader({'Authorization': 'Bearer ' + res['token']}) | ||||||
|  |  | ||||||
|  |     def refreshItems(self): | ||||||
|  |         res = self.api.post('/items/sync', {'sync_token': self.sync_token}) | ||||||
|  |         print(json.dumps(res)) | ||||||
|  |         self.sync_token = res['sync_token'] | ||||||
|  |         self.handleResponseItems(res['retrieved_items']) | ||||||
|  |  | ||||||
|  |     def handleResponseItems(self, response_items): | ||||||
|  |         decrypted_items = self.encryption_helper.pure_decryptResponseItems(response_items, self.keys) | ||||||
|  |         self.item_manager.mapResponseItemsToLocalItems(decrypted_items) | ||||||
|  |  | ||||||
|  |     def getNotes(self): | ||||||
|  |         self.refreshItems() | ||||||
|  |         return self.item_manager.getNotes() | ||||||
|  |  | ||||||
|  |     def __init__(self, username, password): | ||||||
|  |         self.api = RESTAPI(self.base_url) | ||||||
|  |         self.username = username | ||||||
|  |         self.signIn(password) | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     notes = StandardNotesAPI('tanner@domain.com', 'complexpass') | ||||||
|  |  | ||||||
|  |     while True: | ||||||
|  |         notes.refreshItems() | ||||||
|  |         print(notes.getNotes()) | ||||||
|  |  | ||||||
|  |         time.sleep(1) | ||||||
							
								
								
									
										14
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | cement==2.10.2 | ||||||
|  | certifi==2017.7.27.1 | ||||||
|  | chardet==3.0.4 | ||||||
|  | fusepy==2.0.4 | ||||||
|  | idna==2.6 | ||||||
|  | inflection==0.3.1 | ||||||
|  | Jinja2==2.9.4 | ||||||
|  | MarkupSafe==1.0 | ||||||
|  | peewee==2.8.5 | ||||||
|  | pycryptodome==3.4.7 | ||||||
|  | requests==2.18.4 | ||||||
|  | semantic-version==2.6.0 | ||||||
|  | six==1.10.0 | ||||||
|  | urllib3==1.22 | ||||||
							
								
								
									
										105
									
								
								sn-fs.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								sn-fs.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | |||||||
|  | from __future__ import print_function, absolute_import, division | ||||||
|  |  | ||||||
|  | import errno | ||||||
|  | import logging | ||||||
|  | import os | ||||||
|  |  | ||||||
|  | from stat import S_IFDIR, S_IFREG | ||||||
|  | from sys import argv, exit | ||||||
|  | from time import time | ||||||
|  |  | ||||||
|  | from fuse import FUSE, FuseOSError, Operations, LoggingMixIn | ||||||
|  | from api import StandardNotesAPI | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class StandardNotesFS(LoggingMixIn, Operations): | ||||||
|  |     def __init__(self, path='.'): | ||||||
|  |         self.standard_notes = StandardNotesAPI('tanner@domain.com', 'complexpass') | ||||||
|  |         self.notes = self.standard_notes.getNotes() | ||||||
|  |  | ||||||
|  |         self.uid = os.getuid() | ||||||
|  |         self.gid = os.getgid() | ||||||
|  |  | ||||||
|  |         now = time() | ||||||
|  |         self.dir_stat = dict(st_mode=(S_IFDIR | 0o755), st_ctime=now, | ||||||
|  |                             st_mtime=now, st_atime=now, st_nlink=2, | ||||||
|  |                             st_uid=self.uid, st_gid=self.gid) | ||||||
|  |  | ||||||
|  |         self.note_stat = dict(st_mode=(S_IFREG | 0o644), st_ctime=now, | ||||||
|  |                             st_mtime=now, st_atime=now, st_nlink=1, | ||||||
|  |                             st_uid=self.uid, st_gid=self.gid) | ||||||
|  |  | ||||||
|  |     def getattr(self, path, fh=None): | ||||||
|  |         st = self.note_stat | ||||||
|  |  | ||||||
|  |         path_parts = path.split('/') | ||||||
|  |         note_name = path_parts[1] | ||||||
|  |  | ||||||
|  |         if path == '/': | ||||||
|  |             return self.dir_stat | ||||||
|  |         elif note_name in self.notes: | ||||||
|  |             st['st_size'] = len(self.notes[note_name]) | ||||||
|  |             return st | ||||||
|  |         else: | ||||||
|  |             raise FuseOSError(errno.ENOENT) | ||||||
|  |  | ||||||
|  |     def readdir(self, path, fh): | ||||||
|  |         dirents = ['.', '..'] | ||||||
|  |  | ||||||
|  |         if path == '/': | ||||||
|  |             dirents.extend(list(self.notes.keys())) | ||||||
|  |         return dirents | ||||||
|  |  | ||||||
|  |     def read(self, path, size, offset, fh): | ||||||
|  |         path_parts = path.split('/') | ||||||
|  |         note_name = path_parts[1] | ||||||
|  |  | ||||||
|  |         return self.notes[note_name][offset : offset + size].encode() | ||||||
|  |  | ||||||
|  |     def chmod(self, path, mode): | ||||||
|  |         return 0 | ||||||
|  |  | ||||||
|  |     def chown(self, path, uid, gid): | ||||||
|  |         return 0 | ||||||
|  |  | ||||||
|  |     def create(self, path, mode): | ||||||
|  |         return 0 | ||||||
|  |  | ||||||
|  |     def destroy(self, path): | ||||||
|  |         return 0 | ||||||
|  |  | ||||||
|  |     def mkdir(self, path, mode): | ||||||
|  |         return 0 | ||||||
|  |  | ||||||
|  |     def readlink(self, path): | ||||||
|  |         return 0 | ||||||
|  |  | ||||||
|  |     def rename(self, old, new): | ||||||
|  |         return 0 | ||||||
|  |  | ||||||
|  |     def rmdir(self, path): | ||||||
|  |         return 0 | ||||||
|  |  | ||||||
|  |     def symlink(self, target, source): | ||||||
|  |         return 0 | ||||||
|  |  | ||||||
|  |     def truncate(self, path, length, fh=None): | ||||||
|  |         return 0 | ||||||
|  |  | ||||||
|  |     def unlink(self, path): | ||||||
|  |         return 0 | ||||||
|  |  | ||||||
|  |     def utimens(self, path, times=None): | ||||||
|  |         return 0 | ||||||
|  |  | ||||||
|  |     def write(self, path, data, offset, fh): | ||||||
|  |         return 0 | ||||||
|  |  | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     if len(argv) != 2: | ||||||
|  |         print('usage: %s <mountpoint>' % argv[0]) | ||||||
|  |         exit(1) | ||||||
|  |  | ||||||
|  |     logging.basicConfig(level=logging.DEBUG) | ||||||
|  |  | ||||||
|  |     fuse = FUSE(StandardNotesFS(), argv[1], foreground=True, nothreads=True) | ||||||
		Reference in New Issue
	
	Block a user