├── README.md └── ncipfs.py /README.md: -------------------------------------------------------------------------------- 1 | **Note: THIS REPO IS NOT MAINTAINED - the NuCypher dep has had breaking changes and this repo should only be used for refernce purposes** 2 | 3 | # NCIPFS 4 | 5 | [![Python 3.6](https://img.shields.io/badge/python-3.7-blue.svg)](https://www.python.org/downloads/release/python-370/) [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://opensource.org/licenses/MIT) 6 | 7 | ## Welcome to NCIPFS 8 | 9 | This project makes it easy to secure data on IPFS with NuCyphers awesome proxy re encryption. This libaray is under development and are finializing the API. 10 | 11 | ### Example Scenario: 12 | 13 | `David` wants to upload a 1 TB file and allow `Kathy` and `Joe` access. 14 | 15 | Traditionally he would ask `Kathy` and `Joe` for their respecive public keys and then he would encrypt the data twice; once for each key. Then store the encrypted value somewhere they could retrived it. He'll use IPFS as this datastore. 16 | 17 | However NuCyphers proxy re-encryption allows us to encrypt the data once, then re-encrypt new keys that will allow the two parties access. In this system we only encrypt the 1 TB once, and only store 1 TB on IPFS, this saves us storage space, encryption time, and ecryption computations. 18 | 19 | ### Resources 20 | 21 | https://medium.com/@david.richard.holtz/hako-3825c3a033d7 22 | 23 | https://www.youtube.com/watch?v=_0Jl836ETLo 24 | 25 | https://github.com/drbh/hako 26 | 27 | 28 | 29 | ### Benefits 30 | 31 | ✅ **Y**ou **O**nly **E**ncrypt **O**nce **(Y.O.E.O kinda like Y.O.L.O)** 32 | ✅ Verified (immutable data store) 33 | ✅ Only re-encrypt keys for viewers 34 | ✅ Distributed - no single point of failure 35 | ✅ Can run federated (centralized) or distributed (ECR20 token incentivized) 36 | 37 | ### Use cases 38 | 39 | 💡 Distributing large datasets 40 | 💡 Distributing data to many people 41 | 💡 IOT datastored (check out the original hearbeat example) 42 | 43 | ### Implementations 44 | 45 | 🐥 Python 3 (stable IPFS and NuCypher codebases) 46 | 🥚 Node JS (waiting on stable NyCypher codebase) 47 | 🥚 Golang (waiting on stable NyCypher codebase) 48 | 49 | 50 | ## Installation 51 | 52 | ### Library 53 | 54 | ``` 55 | pip install ncipfs 56 | ``` 57 | 58 | ### Development 59 | 60 | ```python 61 | git clone https://github.com/drbh/ncipfs.git 62 | cd ncipfs.git 63 | ``` 64 | 65 | We use `pipenv` to manage any of the deps 66 | ``` 67 | # install deps and access virtual env 68 | pipenv install 69 | pipenv shell 70 | ``` 71 | 72 | ## Implementation 73 | 74 | On the top level there is a class named **NCIPFS**. This class handles the connection to an **IPFS Gateway** a **NuCypher Network** and access to **Local File Store** that handles all of the users keys. 75 | 76 | 77 | Top level diagram -- 78 | 79 | ## API 80 | 81 | ### NCIPFS Methods 82 | 83 | connect(self, nucypher_network, ipfs_api_gateway) 84 | 85 | create_new_user(self, name, password) 86 | 87 | act_as_alice(self, name, password) 88 | 89 | act_as_bob(self, name) 90 | 91 | add_contents(self, alicia, my_label, contents): 92 | 93 | add_data_and_grant_self_access(self, username, password, label, contents) 94 | 95 | grant_others_access(self, username, password, cid, label, recp_enc_b58_key, recp_sig_b58_key) 96 | 97 | fetch_and_decrypt_nucid(self, username, nucid) 98 | 99 | decrypt(self, bob, item_cid, pol, sig, lab) 100 | 101 | ### Top Level Functions 102 | 103 | creat_nucid(alice, cid, enc_pubkey, sig_pubkey, label) 104 | 105 | get_users_public_keys(name, serialized=False) 106 | 107 | ncipfs_to_policy(store_url) 108 | 109 | -------------------------------------------------------------------------------- /ncipfs.py: -------------------------------------------------------------------------------- 1 | import maya 2 | import msgpack 3 | import ipfsapi 4 | import shutil 5 | import os 6 | import time 7 | import datetime 8 | import json 9 | 10 | import base58 11 | import base64 12 | 13 | from timeit import default_timer as timer 14 | 15 | from nucypher.characters.lawful import Enrico 16 | from nucypher.characters.lawful import Bob, Ursula 17 | from nucypher.config.characters import AliceConfiguration 18 | from nucypher.crypto.powers import DecryptingPower, SigningPower 19 | from nucypher.network.middleware import RestMiddleware 20 | from nucypher.utilities.logging import SimpleObserver 21 | from nucypher.crypto.kits import UmbralMessageKit 22 | from nucypher.keystore.keypairs import DecryptingKeypair, SigningKeypair 23 | from umbral.keys import UmbralPublicKey, UmbralPrivateKey 24 | 25 | 26 | 27 | 28 | class ncipfs(object): 29 | """ 30 | docstring for ncipfs 31 | """ 32 | def __init__(self): 33 | self.name = "David" 34 | pass 35 | 36 | def connect(self, nucypher_network, ipfs_api_gateway): 37 | """ 38 | client = ncipfs.Connect( 39 | nucypher_network="localhost:11500", 40 | ipfs_api_gateway="localhost:5001" 41 | ) 42 | """ 43 | self.nucypher_network = nucypher_network 44 | self.ipfs_api_gateway = ipfs_api_gateway 45 | 46 | try: 47 | self.ipfs_gateway_api = ipfsapi.connect('127.0.0.1', 5001) 48 | except Exception as e: # should be more specific ConnectionRefusedError, NewConnectionError, MaxRetryError not sure 49 | print("Automatic Mode A Public Gateway will be used as a fallback") 50 | self.ipfs_gateway_api = ipfsapi.connect('https://ipfs.infura.io', 5001) 51 | 52 | 53 | # SEEDNODE_URL = self.nucypher_network 54 | POLICY_FILENAME = "policy-metadata.json" 55 | 56 | # # FOR LOCAL RUNNING NET 57 | # self.ursula = Ursula.from_seed_and_stake_info( 58 | # seed_uri=SEEDNODE_URL, 59 | # federated_only=True, 60 | # minimum_stake=0 61 | # ) 62 | 63 | self.ursula =urs = Ursula.from_teacher_uri( 64 | teacher_uri=self.nucypher_network, 65 | federated_only=True, 66 | min_stake=0 67 | ) 68 | return True 69 | 70 | def create_new_user(self, name, password): 71 | passphrase = password 72 | direco = "accounts/"+ name 73 | # alice_config = AliceConfiguration( 74 | # config_root=os.path.join(direco), 75 | # is_me=True, known_nodes={self.ursula}, start_learning_now=True, 76 | # federated_only=True, learn_on_same_thread=True, 77 | # ) 78 | # alice_config.initialize(password=passphrase) 79 | # alice_config.keyring.unlock(password=passphrase) 80 | # alice_config_file = alice_config.to_configuration_file() 81 | alice_config = AliceConfiguration( 82 | config_root=os.path.join(direco), 83 | is_me=True, 84 | known_nodes={self.ursula}, 85 | start_learning_now=False, 86 | federated_only=True, 87 | learn_on_same_thread=True, 88 | ) 89 | alice_config.initialize(password=passphrase) 90 | alice_config.keyring.unlock(password=passphrase) 91 | alice = alice_config.produce() 92 | alice_config_file = alice_config.to_configuration_file() 93 | alice.start_learning_loop(now=True) 94 | 95 | 96 | enc_privkey = UmbralPrivateKey.gen_key() 97 | sig_privkey = UmbralPrivateKey.gen_key() 98 | 99 | doctor_privkeys = { 100 | 'enc': enc_privkey.to_bytes().hex(), 101 | 'sig': sig_privkey.to_bytes().hex(), 102 | } 103 | 104 | DOCTOR_PUBLIC_JSON = direco + '/recipent.public.json' 105 | DOCTOR_PRIVATE_JSON = direco + '/recipent.private.json' 106 | 107 | with open(DOCTOR_PRIVATE_JSON, 'w') as f: 108 | json.dump(doctor_privkeys, f) 109 | 110 | enc_pubkey = enc_privkey.get_pubkey() 111 | sig_pubkey = sig_privkey.get_pubkey() 112 | doctor_pubkeys = { 113 | 'enc': enc_pubkey.to_bytes().hex(), 114 | 'sig': sig_pubkey.to_bytes().hex() 115 | } 116 | with open(DOCTOR_PUBLIC_JSON, 'w') as f: 117 | json.dump(doctor_pubkeys, f) 118 | 119 | return alice 120 | 121 | def act_as_alice(self, name, password): 122 | dirname = "accounts/" + name + "/" 123 | congifloc = dirname + "alice.config" 124 | alice_config = AliceConfiguration( 125 | config_root=os.path.join(dirname), 126 | is_me=True, 127 | known_nodes={self.ursula}, 128 | start_learning_now=False, 129 | federated_only=True, 130 | learn_on_same_thread=True, 131 | ) 132 | 133 | cfg = alice_config.from_configuration_file(congifloc) 134 | cfg.keyring.unlock(password) 135 | alice = cfg.produce() 136 | # alice.start_learning_loop(now=True) 137 | return alice 138 | 139 | 140 | def act_as_bob(self, name): 141 | dirname = "accounts/" + name + "/" 142 | fname = dirname+"recipent.private.json" 143 | with open(fname) as data_file: 144 | data = json.load(data_file) 145 | enc_privkey = UmbralPrivateKey.from_bytes(bytes.fromhex(data["enc"])) 146 | sig_privkey = UmbralPrivateKey.from_bytes(bytes.fromhex(data["sig"])) 147 | 148 | bob_enc_keypair = DecryptingKeypair(private_key=enc_privkey) 149 | bob_sig_keypair = SigningKeypair(private_key=sig_privkey) 150 | enc_power = DecryptingPower(keypair=bob_enc_keypair) 151 | sig_power = SigningPower(keypair=bob_sig_keypair) 152 | power_ups = [enc_power, sig_power] 153 | bob = Bob( 154 | is_me=True, 155 | federated_only=True, 156 | crypto_power_ups=power_ups, 157 | start_learning_now=True, 158 | abort_on_learning_error=True, 159 | known_nodes=[self.ursula], 160 | save_metadata=False, 161 | network_middleware=RestMiddleware(), 162 | ) 163 | return bob 164 | 165 | def add_contents(self, alicia, my_label, contents): 166 | """ 167 | cid = client.add_contents( 168 | policy_pubkey=policy_pub_key 169 | ) 170 | """ 171 | policy_pubkey = alicia.get_policy_pubkey_from_label(my_label) 172 | 173 | data_source = Enrico(policy_encrypting_key=policy_pubkey) 174 | data_source_public_key = bytes(data_source.stamp) 175 | heart_rate = 80 176 | now = time.time() 177 | kits = list() 178 | heart_rate = contents 179 | now += 3 180 | heart_rate_data = { 'heart_rate': heart_rate, 'timestamp': now, } 181 | plaintext = msgpack.dumps(heart_rate_data, use_bin_type=True) 182 | message_kit, _signature = data_source.encrypt_message(plaintext) 183 | kit_bytes = message_kit.to_bytes() 184 | kits.append(kit_bytes) 185 | data = { 'data_source': data_source_public_key, 'kits': kits, } 186 | # print("🚀 ADDING TO IPFS D-STORAGE NETWORK 🚀") 187 | d = msgpack.dumps(data, use_bin_type=True) 188 | 189 | ### NETWORK ERROR OUT ON FALLBACK 190 | cid = self.ipfs_gateway_api.add_bytes(d) 191 | # print("File Address:\t%s" % cid) 192 | return cid 193 | 194 | def add_data_and_grant_self_access(self, username, password, label, contents): 195 | alice = self.act_as_alice(username, password) 196 | cid = self.add_contents(alice, label.encode("utf-8"), contents) 197 | enc, sig = get_users_public_keys(username) 198 | nucid = creat_nucid(alice, cid, enc, sig, label.encode("utf-8")) 199 | return nucid 200 | 201 | def grant_others_access(self, username, password, cid, label, recp_enc_b58_key, recp_sig_b58_key): 202 | alice = self.act_as_alice(username, password) 203 | enc = UmbralPublicKey.from_bytes(base58.b58decode(recp_enc_b58_key)) 204 | sig = UmbralPublicKey.from_bytes(base58.b58decode(recp_sig_b58_key)) 205 | nucid = creat_nucid(alice, cid, enc, sig, label.encode("utf-8")) 206 | return nucid 207 | 208 | 209 | def fetch_and_decrypt_nucid(self, username, nucid): 210 | item_cid, pol, sig, lab = ncipfs_to_policy(nucid) 211 | bob = self.act_as_bob(username) 212 | try: 213 | out = self.decrypt(bob, item_cid, pol, sig, lab) 214 | except: 215 | out = "Failed to decrypt" 216 | return out 217 | 218 | 219 | def decrypt(self, bob, item_cid, pol, sig, lab): 220 | policy_pubkey = UmbralPublicKey.from_bytes(bytes.fromhex(pol)) 221 | alices_sig_pubkey = UmbralPublicKey.from_bytes(bytes.fromhex(sig)) 222 | label = lab.encode() 223 | dat = self.ipfs_gateway_api.cat(item_cid) 224 | doctor = bob 225 | doctor.join_policy(label, alices_sig_pubkey) 226 | data = msgpack.loads(dat, raw=False) 227 | message_kits = (UmbralMessageKit.from_bytes(k) for k in data['kits']) 228 | data_source = Enrico.from_public_keys( 229 | {SigningPower: data['data_source']}, 230 | policy_encrypting_key=policy_pubkey 231 | ) 232 | message_kit = next(message_kits) 233 | start = timer() 234 | retrieved_plaintexts = doctor.retrieve( 235 | label=label, 236 | message_kit=message_kit, 237 | data_source=data_source, 238 | alice_verifying_key=alices_sig_pubkey 239 | ) 240 | end = timer() 241 | plaintext = msgpack.loads(retrieved_plaintexts[0], raw=False) 242 | heart_rate = plaintext['heart_rate'] 243 | timestamp = maya.MayaDT(plaintext['timestamp']) 244 | terminal_size = shutil.get_terminal_size().columns 245 | max_width = min(terminal_size, 120) 246 | columns = max_width - 12 - 27 247 | scale = columns / 40 248 | retrieval_time = "Retrieval time: {:8.2f} ms".format(1000 * (end - start)) 249 | line = heart_rate# + " " + retrieval_time 250 | return line 251 | 252 | 253 | def creat_nucid(alice, cid, enc_pubkey, sig_pubkey, label): 254 | powers_and_material = { DecryptingPower: enc_pubkey, SigningPower: sig_pubkey } 255 | doctor_strange = Bob.from_public_keys(powers_and_material=powers_and_material, federated_only=True) 256 | policy_end_datetime = maya.now() + datetime.timedelta(days=5) 257 | # m, n = 1, 2 258 | m, n = 2, 3 259 | print(doctor_strange, label, m, n, policy_end_datetime) 260 | print(alice) 261 | policy = alice.grant(bob=doctor_strange, label=label, m=m, n=n, expiration=policy_end_datetime) 262 | policy_info = { 263 | "policy_pubkey": base58.b58encode(policy.public_key.to_bytes()).decode("utf-8"), 264 | "alice_sig_pubkey": base58.b58encode(bytes(alice.stamp)).decode("utf-8"), 265 | "label": label.decode("utf-8"), 266 | } 267 | 268 | store_url = "%s_%s_%s_%s"%(cid, 269 | base58.b58encode(policy.public_key.to_bytes()).decode("utf-8"), 270 | base58.b58encode(bytes(alice.stamp)).decode("utf-8"), 271 | label.decode("utf-8")) 272 | 273 | store_url = "nucid://" + store_url 274 | 275 | return store_url 276 | 277 | 278 | def get_users_public_keys(name, serialized=False): 279 | dirname = "accounts/" + name + "/" 280 | fname = dirname+"recipent.public.json" 281 | with open(fname) as data_file: 282 | data = json.load(data_file) 283 | enc_pubkey = UmbralPublicKey.from_bytes(bytes.fromhex(data["enc"])) 284 | sig_pubkey = UmbralPublicKey.from_bytes(bytes.fromhex(data["sig"])) 285 | print(enc_pubkey, sig_pubkey) 286 | if serialized: 287 | return (base58.b58encode(bytes.fromhex(data["enc"])).decode("utf-8"), 288 | base58.b58encode(bytes.fromhex(data["sig"])).decode("utf-8")) 289 | return (enc_pubkey, sig_pubkey) 290 | 291 | 292 | 293 | def ncipfs_to_policy(store_url): 294 | print(store_url) 295 | if len(store_url.split("//")[1].split("_")) == 2: 296 | return [ 297 | elem 298 | for idx, elem in enumerate(store_url.split("//")[1].split("_")) 299 | ] 300 | else: 301 | return [ 302 | base58.b58decode(elem).hex() 303 | if idx != 0 and idx != 3 else elem 304 | for idx, elem in enumerate(store_url.split("//")[1].split("_")) 305 | ] 306 | # n = ncipfs() 307 | 308 | # n.connect("localhost:11501","localhost:5001") 309 | 310 | # # n.create_new_user("person", "password password 123") 311 | 312 | # my_nucid = add_data_and_grant_self_access("dholtz1", "12345678901234567890", 313 | # "example.txt", "hello world") 314 | 315 | # ## share with PERSON's keys 316 | 317 | # shareable_nucid = grant_others_access("dholtz1", "12345678901234567890", 318 | # "QmSWioSfDQA2tx8FNQEYPtXER7BTrFNwtuoAXUdCFp2gFi","example.txt", 319 | # '24ig8sngFYbCu4eZQakU4RQhDdCJxccC28GV1E3m9d4E2', 320 | # '28E9RhzxtZypGLMGgSAyxPy3Wdu1upaF7Bti6fynEz3iQ') 321 | 322 | 323 | # fetch_and_decrypt_nucid("dholtz1", 'ncipfs://QmSWioSfDQA2tx8FNQEYPtXER7BTrFNwtuoAXUdCFp2gFi_pmkFc7WMN6KqUwK5U83sfMW5LBF1XbibsiQ2qR1XDWRg_gU7tg6RpWRAbtb57EuUErdj1npn1FiGF5wxfUeX3RuzZ_example.txt') 324 | 325 | 326 | # fetch_and_decrypt_nucid("person", shareable_nucid) 327 | 328 | 329 | # fetch_and_decrypt_nucid("philburt", shareable_nucid) 330 | 331 | --------------------------------------------------------------------------------