Example #1
0
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
Example #2
0
    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))
Example #3
0
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}
Example #4
0
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}
Example #5
0
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
Example #6
0
    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))
Example #7
0
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
Example #8
0
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)))
Example #9
0
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)
Example #10
0
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
Example #11
0
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']}
Example #12
0
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
    }
Example #13
0
    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