def find_name_index(name_address, master_privkey_hex, max_tries=25, start=0): """ Given a name's device-specific address and device-specific master key, find index from which it was derived. Return the index on success Return None on failure. """ hdwallet = HDWallet(master_privkey_hex) for i in xrange(start, max_tries): child_privkey = hdwallet.get_child_privkey(index=i) child_pubkey = get_pubkey_hex(child_privkey) child_addresses = [ keylib.public_key_to_address( keylib.key_formatting.compress(child_pubkey)), keylib.public_key_to_address( keylib.key_formatting.decompress(child_pubkey)) ] if str(name_address) in child_addresses: return i return None
def get_master_address(self): if self.master_address is not None: return self.master_address hex_privkey = self.get_master_privkey() hex_pubkey = get_pubkey_hex(hex_privkey) return virtualchain.address_reencode( keylib.public_key_to_address(hex_pubkey))
def app_make_session(blockchain_id, app_private_key, app_domain, methods, app_public_keys, requester_device_id, master_data_privkey, session_lifetime=None, config_path=CONFIG_PATH): """ Make a session JWT for this application. Verify with user private key Sign with master private key Return {'session': session jwt, 'session_token': session token} on success Return {'error': ...} on error """ conf = get_config(path=config_path) assert conf if session_lifetime is None: session_lifetime = conf.get('default_session_lifetime', 1e80) app_public_key = get_pubkey_hex(app_private_key) app_user_id = data.datastore_get_id(app_public_key) api_endpoint_host = conf.get('api_endpoint_host', DEFAULT_API_HOST) api_endpoint_port = conf.get('api_endpoint_port', DEFAULT_API_PORT) api_endpoint = '{}:{}'.format(api_endpoint_host, api_endpoint_port) ses = { 'version': 1, 'blockchain_id': blockchain_id, 'app_domain': app_domain, 'methods': methods, 'app_public_keys': app_public_keys, 'app_user_id': app_user_id, 'api_endpoint': api_endpoint, 'device_id': requester_device_id, 'storage': { 'classes': classify_storage_drivers(), 'preferences': {} }, 'timestamp': int(time.time()), 'expires': int(time.time() + session_lifetime), } jsonschema.validate(ses, APP_SESSION_SCHEMA) signer = jsontokens.TokenSigner() session_token = signer.sign(ses, master_data_privkey) session = jsontokens.decode_token(session_token) return {'session': session, 'session_token': session_token}
def app_delete_resource(blockchain_id, app_domain, res_name, app_config=None, data_privkey=None, proxy=None, wallet_keys=None, config_path=CONFIG_PATH): """ Remove data from a named application resource in mutable storage. data_privkey should be the publisher's private key name should be a blockchain ID that points to the public key if app_config is not None, then the driver hints will be honored. Return {'status': True, 'version': ...} on success Return {'error': ...} on error """ if data_privkey is None: assert wallet_keys, "No data private key or wallet given" data_privkey = wallet_keys.get('data_privkey', None) assert data_privkey, "Wallet does not contain a data private key" data_pubkey = get_pubkey_hex(data_privkey) proxy = get_default_proxy() if proxy is None else proxy res_data_id = storage.make_fq_data_id(app_domain, res_name) driver_hints = None if app_config is not None: # use driver hints driver_hints = app_config['driver_hints'] tombstone = storage.make_data_tombstone(res_data_id) signed_tombstone = storage.sign_data_tombstone(res_data_id, data_privkey) res = data.delete_mutable(res_data_id, [signed_tombstone], proxy=proxy, storage_drivers=driver_hints, blockchain_id=blockchain_id, is_fq_data_id=True, config_path=config_path) if 'error' in res: log.error("Failed to delete resource {}: {}".format( res_data_id, res['error'])) return {'error': 'Failed to delete resource'} return {'status': True}
def serialize_mutable_data(data_text_or_json, data_privkey=None, data_pubkey=None, data_signature=None, profile=False): """ Generate a serialized mutable data record from the given information. Sign it with privatekey. The signature will be generated over the netstring "len(payload):payload,". If given, the signature must be signed this way (i.e. via sign_data_payload) Return the serialized data (as a string) on success """ if profile: # private key required to generate signature assert data_privkey is not None # profiles must conform to a particular standard format tokenized_data = blockstack_profiles.sign_token_records( [data_text_or_json], data_privkey) del tokenized_data[0]['decodedToken'] serialized_data = json.dumps(tokenized_data, sort_keys=True) return serialized_data else: # version 2 format for mutable data assert data_privkey or (data_pubkey and data_signature) if data_signature is None: assert isinstance(data_text_or_json, (str, unicode)), "data must be a string" data_str = str(data_text_or_json) data_signature = sign_data_payload(data_str, data_privkey) # make sure it's compressed if data_pubkey is None: data_pubkey = get_pubkey_hex(data_privkey) pubkey_hex_compressed = keylib.key_formatting.compress(data_pubkey) data_payload = serialize_data_payload(data_text_or_json) res = "bsk2.{}.{}.{}".format(pubkey_hex_compressed, data_signature, data_payload) return res
def get_child_address(self, index=0): """ @index is the child index Returns: child address for given @index """ if self.child_addresses is not None: return self.child_addresses[index] # force decompressed... hex_privkey = self.get_child_privkey(index) hex_pubkey = get_pubkey_hex(hex_privkey) return virtualchain.address_reencode( keylib.public_key_to_address(hex_pubkey))
def put_profile(name, new_profile, blockchain_id=None, user_data_privkey=None, user_zonefile=None, proxy=None, wallet_keys=None, required_drivers=None, config_path=CONFIG_PATH): """ Set the new profile data. CLIENTS SHOULD NOT CALL THIS METHOD DIRECTLY. if user_data_privkey is given, then wallet_keys does not need to be given. Return {'status: True} on success Return {'error': ...} on failure. """ ret = {} proxy = get_default_proxy() if proxy is None else proxy config = proxy.conf # deduce storage drivers required_storage_drivers = None if required_drivers is not None: required_storage_drivers = required_drivers else: required_storage_drivers = config.get('storage_drivers_required_write', None) if required_storage_drivers is not None: required_storage_drivers = required_storage_drivers.split(',') else: required_storage_drivers = config.get('storage_drivers', '').split(',') # deduce private key if user_data_privkey is None: user_data_privkey = get_data_privkey_info(user_zonefile, wallet_keys=wallet_keys, config_path=config_path) if json_is_error(user_data_privkey): log.error("Failed to get data private key: {}".format( user_data_privkey['error'])) return {'error': 'No data key defined'} profile_payload = copy.deepcopy(new_profile) profile_payload = set_profile_timestamp(profile_payload) if BLOCKSTACK_DEBUG: # NOTE: don't calculate this string unless we're actually debugging... log.debug('Save updated profile for "{}" to {} at {} by {}'.format( name, ','.join(required_storage_drivers), get_profile_timestamp(profile_payload), get_pubkey_hex(user_data_privkey))) rc = storage.put_mutable_data(name, profile_payload, data_privkey=user_data_privkey, required=required_storage_drivers, profile=True, blockchain_id=blockchain_id) if rc: ret['status'] = True else: ret['error'] = 'Failed to update profile' return ret
def put_mutable_data(fq_data_id, data_text_or_json, sign=True, raw=False, data_privkey=None, data_pubkey=None, data_signature=None, profile=False, blockchain_id=None, required=None, skip=None, required_exclusive=False): """ Given the unserialized data, store it into our mutable data stores. Do so in a best-effort way. This method fails if all storage providers fail, or if a storage provider in required fails. @required: list of required drivers to use. All of them must succeed for this method to succeed. @skip: list of drivers we can skip. None of them will be tried. @required_exclusive: if True, then only the required drivers will be tried (none of the loaded but not-required drivers will be invoked) @sign: if True, then a private key is required. if False, then simply store the data without serializing it or including a public key and signature. @raw: If True, then the data will be put as-is without any ancilliary metadata. Requires sign=False Return True on success Return False on error """ global storage_handlers assert len(storage_handlers) > 0, "No storage handlers initialized" # sanity check: only take structured data if this is a profile if not isinstance(data_text_or_json, (str, unicode)): assert profile, "Structured data is only supported when profile=True" required = [] if required is None else required skip = [] if skip is None else skip assert len(set(required).intersection( set(skip))) == 0, "Overlap between required and skip driver lists" log.debug( 'put_mutable_data({}), required={}, skip={} required_exclusive={}'. format(fq_data_id, ','.join(required), ','.join(skip), required_exclusive)) # fully-qualified username hint fqu = None if blockchain_id is not None: fqu = blockchain_id # sanity check: only support single-sig private keys if data_privkey is not None: if not is_singlesig_hex(data_privkey): log.error('Only single-signature data private keys are supported') return False data_pubkey = get_pubkey_hex(data_privkey) elif sign: assert data_pubkey is not None assert data_signature is not None serialized_data = None if sign or not raw: serialized_data = serialize_mutable_data(data_text_or_json, data_privkey=data_privkey, data_pubkey=data_pubkey, data_signature=data_signature, profile=profile) else: serialized_data = data_text_or_json if BLOCKSTACK_TEST: log.debug("data ({}): {}".format(type(serialized_data), serialized_data)) successes = 0 required_successes = 0 for handler in storage_handlers: if handler.__name__ in skip: log.debug("Skipping {}: at caller's request".format( handler.__name__)) continue if not getattr(handler, 'put_mutable_handler', None): if handler.__name__ not in required: log.debug( "Skipping {}: it does not implement put_mutable_handler". format(handler.__name__)) continue log.debug( "Required storage provider {} does not implement put_mutable_handler" .format(handler.__name__)) return False if required_exclusive and handler.__name__ not in required: log.debug("Skipping {}: it is optional".format(handler.__name__)) continue rc = False log.debug('Try "{}"'.format(handler.__name__)) try: rc = handler.put_mutable_handler(fq_data_id, serialized_data, fqu=fqu, profile=profile) except Exception as e: log.exception(e) if handler.__name__ not in required: continue log.error("Failed to replicate data with '{}'".format( handler.__name__)) return None if rc: log.debug("Replicated {} bytes with {} (rc = {})".format( len(serialized_data), handler.__name__, rc)) successes += 1 if handler.__name__ in required: required_successes += 1 continue if handler.__name__ not in required: log.debug('Failed to replicate with "{}"'.format(handler.__name__)) continue # required driver failed log.error( "Failed to replicate to required storage provider '{}'".format( handler.__name__)) return False # failed everywhere or succeeded somewhere log.debug( "put_mutable_data: successes = {}, required_successes = {}, |required - skip| = {}" .format(successes, required_successes, len(set(required) - set(skip)))) return (successes > 0) and (required_successes >= len(set(required) - set(skip)))
def get_data_privkey(user_zonefile, wallet_keys=None, config_path=CONFIG_PATH): """ Get the data private key that matches this zonefile. * If the zonefile has a public key that this wallet does not have, then there is no data key. * If the zonefile does not have a public key, then: * if the data private key in the wallet matches the owner private key, then the wallet data key is the data key to use. (this is for legacy compatibility with onename.com, which does not create data keys for users) * otherwise, there is no data key Return the private key on success Return {'error': ...} if we could not find the key """ from .wallet import get_wallet from .user import user_zonefile_data_pubkey zonefile_data_pubkey = None try: # NOTE: uncompressed... zonefile_data_pubkey = user_zonefile_data_pubkey(user_zonefile) except ValueError: log.error('Multiple pubkeys defined in zone file') return {'error': 'Multiple data public keys in zonefile'} wallet_keys = {} if wallet_keys is None else wallet_keys if wallet_keys.get('data_privkey', None) is None: log.error('No data private key set') return {'error': 'No data private key in wallet keys'} wallet = get_wallet( config_path=CONFIG_PATH) if wallet_keys is None else wallet_keys assert wallet, 'Failed to get wallet' if not wallet.has_key('data_privkey'): log.error("No data private key in wallet") return {'error': 'No data private key in wallet'} data_privkey = wallet['data_privkey'] # NOTE: uncompresssed wallet_data_pubkey = keylib.key_formatting.decompress( get_pubkey_hex(str(data_privkey))) if zonefile_data_pubkey is None and wallet_data_pubkey is not None: # zone file does not have a data key set. # the wallet data key *must* match the owner key owner_privkey_info = wallet['owner_privkey'] owner_privkey = None if virtualchain.is_singlesig(owner_privkey_info): owner_privkey = owner_privkey_info elif virtualchain.is_multisig(owner_privkey_info): owner_privkey = owner_privkey_info['private_keys'][0] owner_pubkey = keylib.key_formatting.decompress( get_pubkey_hex(str(owner_privkey))) if owner_pubkey != wallet_data_pubkey: # doesn't match. no data key return { 'error': 'No zone file key, and data key does not match owner key ({} != {})' .format(owner_pubkey, wallet_data_pubkey) } return str(data_privkey)
def app_unpublish(blockchain_id, app_domain, force=False, data_privkey=None, app_config=None, wallet_keys=None, proxy=None, config_path=CONFIG_PATH): """ Unpublish an application Deletes its config and index. Does NOT delete its resources. Does NOT delete user data. if force is True, then we will try to delete the app state even if we can't load the app config WARNING: force can be dangerous, since it can delete data via drivers that were never meant for this app. Use with caution! Return {'status': True, 'app_config': ..., 'retry': ...} on success. If retry is True, then retry this method with the given app_config Return {'error': ...} on error """ proxy = get_default_proxy() if proxy is None else proxy # find out where to delete from data_pubkey = None if data_privkey is not None: data_pubkey = get_pubkey_hex(str(data_privkey)) if app_config is None: app_config = app_get_config(blockchain_id, app_domain, data_pubkey=data_pubkey, proxy=proxy, config_path=CONFIG_PATH) if 'error' in app_config: if not force: log.error("Failed to load app config for {}'s {}".format( blockchain_id, app_domain)) return {'error': 'Failed to load app config'} else: # keep going app_config = None log.warning( "Failed to load app config, but proceeding at caller request" ) config_data_id = storage.make_fq_data_id(app_domain, '.blockstack') index_data_id = storage.make_fq_data_id(app_domain, 'index.html') storage_drivers = None if app_config is not None: # only use the ones we have to urls = user_db.urls_from_uris(app_config['index_uris']) driver_names = [] for url in urls: drivers = storage.get_drivers_for_url(url) driver_names += [d.__name__ for d in drivers] storage_drivers = list(set(driver_names)) ret = {} # delete the index index_tombstone = storage.make_data_tombstone(index_data_id) signed_index_tombstone = storage.sign_data_tombstone( index_data_id, data_privkey) res = data.delete_mutable(index_data_id, [signed_index_tombstone], proxy=proxy, storage_drivers=storage_drivers, blockchain_id=blockchain_id, is_fq_data_id=True, config_path=config_path) if 'error' in res: log.warning("Failed to delete index file {}".format(index_data_id)) ret['app_config'] = app_config ret['retry'] = True # delete the config config_tombstone = storage.make_data_tombstone(config_data_id) signed_config_tombstone = storage.sign_data_tombstone( config_data_id, data_privkey) res = data.delete_mutable(config_data_id, [signed_config_tombstone], proxy=proxy, blockchain_id=blockchain_id, is_fq_data_id=True, config_path=config_path) if 'error' in res: log.warning("Failed to delete config file {}".format(config_data_id)) if not ret.has_key('app_config'): ret['app_config'] = app_config ret['retry'] = True ret['status'] = True return ret
def app_put_resource(blockchain_id, app_domain, res_name, res_data, app_config=None, data_privkey=None, proxy=None, wallet_keys=None, config_path=CONFIG_PATH): """ Store data to a named application resource in mutable storage. data_privkey should be the publisher's private key name should be a blockchain ID that points to the public key if app_config is not None, then the driver hints will be honored. Return {'status': True, 'version': ...} on success Return {'error': ...} on error """ assert isinstance(res_data, (str, unicode)), "Resource must be a string" try: json.dumps(res_data) except: raise AssertionError("Resource must be a JSON-serializable string") if data_privkey is None: assert wallet_keys, 'Missing both data private key and wallet keys' data_privkey = wallet_keys.get('data_privkey') assert data_privkey, "Wallet does not have a data private key" proxy = get_default_proxy() if proxy is None else proxy res_data_id = storage.make_fq_data_id(app_domain, res_name) data_pubkey = get_pubkey_hex(data_privkey) driver_hints = None if app_config is not None: # use driver hints driver_hints = app_config['driver_hints'] res_blob = data.make_mutable_data_info(res_data_id, res_data, is_fq_data_id=True) res_blob_str = data.data_blob_serialize(res_blob) res_sig = data.data_blob_sign(res_blob_str, data_privkey) res = data.put_mutable(res_data_id, res_blob_str, data_pubkey, res_sig, res_blob['version'], blockchain_id=blockchain_id, config_path=config_path, storage_drivers=driver_hints) if 'error' in res: log.error("Failed to store resource {}: {}".format( res_data_id, res['error'])) return {'error': 'Failed to store resource'} return {'status': True, 'version': res_blob['version']}
def app_publish(dev_blockchain_id, app_domain, app_method_list, app_index_uris, app_index_file, app_driver_hints=[], data_privkey=None, proxy=None, wallet_keys=None, config_path=CONFIG_PATH): """ Instantiate an application. * replicate the (opaque) app index file to "index.html" to each URL in app_uris * replicate the list of URIs and the list of methods to ".blockstack" via each of the client's storage drivers. This succeeds even if the app already exists (in which case, it will be overwritten). This method is idempotent, so it can be retried on failure. data_privkey should be the publisher's private key (i.e. their data key) name should be the blockchain ID that points to data_pubkey Return {'status': True, 'config_fq_data_id': config's fully-qualified data ID, 'index_fq_data_id': index file's fully-qualified data ID} on success Return {'error': ...} on error """ if data_privkey is None: assert wallet_keys, 'Missing both data private key and wallet keys' data_privkey = wallet_keys.get('data_privkey') assert data_privkey, "Wallet does not have a data private key" proxy = get_default_proxy() if proxy is None else proxy # replicate configuration data (method list and app URIs) app_cfg = { 'blockchain_id': dev_blockchain_id, 'app_domain': app_domain, 'index_uris': app_index_uris, 'api_methods': app_method_list, 'driver_hints': app_driver_hints, } jsonschema.validate(app_cfg, APP_CONFIG_SCHEMA) data_pubkey = get_pubkey_hex(data_privkey) config_data_id = storage.make_fq_data_id(app_domain, '.blockstack') app_cfg_blob = data.make_mutable_data_info(config_data_id, app_cfg, is_fq_data_id=True) app_cfg_str = data.data_blob_serialize(app_cfg_blob) app_cfg_sig = data.data_blob_sign(app_cfg_str, data_privkey) res = data.put_mutable(config_data_id, app_cfg_str, data_pubkey, app_cfg_sig, app_cfg_blob['version'], blockchain_id=dev_blockchain_id, config_path=config_path) if 'error' in res: log.error( 'Failed to replicate application configuration {}: {}'.format( config_data_id, res['error'])) return {'error': 'Failed to replicate application config'} # what drivers to use for the index file? urls = user_db.urls_from_uris(app_index_uris) driver_names = [] for url in urls: drivers = storage.get_drivers_for_url(url) driver_names += [d.__name__ for d in drivers] driver_names = list(set(driver_names)) index_data_id = storage.make_fq_data_id(app_domain, 'index.html') # replicate app index file (at least one must succeed) # NOTE: the publisher is free to use alternative URIs that are not supported; they'll just be ignored. app_index_blob = data.make_mutable_data_info(index_data_id, app_index_file, is_fq_data_id=True) app_index_blob_str = data.data_blob_serialize(app_index_blob) app_index_sig = data.data_blob_sign(app_index_blob_str, data_privkey) res = data.put_mutable(index_data_id, app_index_blob_str, data_pubkey, app_index_sig, app_index_blob['version'], blockchain_id=dev_blockchain_id, config_path=config_path, storage_drivers=app_driver_hints) if 'error' in res: log.error( "Failed to replicate application index file to {}: {}".format( ",".join(urls), res['error'])) return {'error': 'Failed to replicate index file'} return { 'status': True, 'config_fq_data_id': config_data_id, 'index_fq_data_id': index_data_id }
try: jsonschema.validate(new_wallet, WALLET_SCHEMA_CURRENT) except ValidationError, ve: # maybe one without a data key? try: jsonschema.validate(new_wallet, WALLET_SCHEMA_CURRENT_NODATAKEY) except ValidationError, ve: if BLOCKSTACK_DEBUG: log.exception(ve) return {'error': 'Wallet secrets do not match wallet schema'} # no data key. Give one and revalidate. # data key defaults to owner private key data_privkey = get_data_key_from_owner_key_LEGACY(new_wallet['owner_privkey']) new_wallet['data_privkey'] = data_privkey new_wallet['data_pubkey'] = get_pubkey_hex(data_privkey) new_wallet['data_pubkeys'] = [new_wallet['data_pubkey']] jsonschema.validate(new_wallet, WALLET_SCHEMA_CURRENT) return {'status': True, 'wallet': new_wallet} def inspect_wallet_data(data): """ Inspect the encrypted wallet structure. Determine: * which format it has * whether or not it needs to be migrated Return {'status': True, 'format': ..., 'migrate': True/False} on success Return {'error': ...} on failure