Esempio n. 1
0
    def unlock(self, username: str, password: str,
               create_new: bool) -> typing.FilePath:
        user_data_dir = cast(typing.FilePath,
                             os.path.join(self.data_directory, username))
        if create_new:
            if os.path.exists(user_data_dir):
                raise AuthenticationError(
                    'User {} already exists'.format(username))
            else:
                os.mkdir(user_data_dir)
        else:
            if not os.path.exists(user_data_dir):
                raise AuthenticationError(
                    'User {} does not exist'.format(username))

            if not os.path.exists(os.path.join(user_data_dir,
                                               'rotkehlchen.db')):
                # This is bad. User directory exists but database is missing.
                # Make a backup of the directory that user should probably remove
                # on his own. At the same time delete the directory so that a new
                # user account can be created
                shutil.move(
                    user_data_dir,
                    os.path.join(self.data_directory, 'backup_%s' % username))

                raise AuthenticationError(
                    'User {} exists but DB is missing. Somehow must have been manually '
                    'deleted or is corrupt. Please recreate the user account.'.
                    format(username))

        self.db: DBHandler = DBHandler(user_data_dir, username, password)
        self.user_data_dir = user_data_dir
        return user_data_dir
Esempio n. 2
0
    def __init__(self, user_data_dir: FilePath, password: str, msg_aggregator: MessagesAggregator):
        self.msg_aggregator = msg_aggregator
        self.user_data_dir = user_data_dir
        self.sqlcipher_version = detect_sqlcipher_version()
        action = self.read_info_at_start()
        if action == DBStartupAction.UPGRADE_3_4:
            result, msg = self.upgrade_db_sqlcipher_3_to_4(password)
            if not result:
                log.error(
                    'dbinfo determined we need an upgrade from sqlcipher version '
                    '3 to version 4 but the upgrade failed.',
                    error=msg,
                )
                raise AuthenticationError(msg)
        elif action == DBStartupAction.STUCK_4_3:
            msg = (
                'dbinfo determined we are using sqlcipher version 3 but the '
                'database has already been upgraded to version 4. Please find a '
                'rotkehlchen binary that uses sqlcipher version 4 to open the '
                'database'
            )
            log.error(msg)
            raise AuthenticationError(msg)
        else:
            self.connect(password)

        try:
            self.conn.executescript(DB_SCRIPT_CREATE_TABLES)
        except sqlcipher.DatabaseError as e:  # pylint: disable=no-member
            migrated = False
            errstr = str(e)
            if self.sqlcipher_version == 4:
                migrated, errstr = self.upgrade_db_sqlcipher_3_to_4(password)

            if self.sqlcipher_version != 4 or not migrated:
                errstr = (
                    'Wrong password while decrypting the database or not a database. Perhaps '
                    'trying to use an sqlcipher 4 version DB with sqlciper 3?'
                )
                raise AuthenticationError(
                    f'SQLCipher version: {self.sqlcipher_version} - {errstr}',
                )

        # Run upgrades if needed
        DBUpgradeManager(self).run_upgrades()
        # Then make sure to always have latest version in the DB
        cursor = self.conn.cursor()
        cursor.execute(
            'INSERT OR REPLACE INTO settings(name, value) VALUES(?, ?)',
            ('version', str(ROTKEHLCHEN_DB_VERSION)),
        )
        self.conn.commit()
Esempio n. 3
0
    def __init__(self, user_data_dir: typing.FilePath, password: str):
        self.user_data_dir = user_data_dir
        self.sqlcipher_version = detect_sqlcipher_version()
        self.connect(password)
        try:
            self.conn.executescript(DB_SCRIPT_CREATE_TABLES)
        except sqlcipher.DatabaseError as e:  # pylint: disable=no-member
            migrated = False
            errstr = str(e)
            if self.sqlcipher_version == 4:
                migrated = True
                # if we are at version 4 perhaps we are just upgrading from an
                # sqlcipher3 database
                script = f'PRAGMA KEY="{password}";PRAGMA cipher_migrate;COMMIT;'
                try:
                    self.conn.executescript(script)
                    self.conn.executescript(DB_SCRIPT_CREATE_TABLES)
                except sqlcipher.DatabaseError as e:  # pylint: disable=no-member
                    errstr = str(e)
                    migrated = False

            if self.sqlcipher_version != 4 or not migrated:
                if errstr == 'file is not a database':
                    errstr = 'Wrong password while decrypting the database or not a database'
                raise AuthenticationError(errstr)

        self.run_updates()
        cursor = self.conn.cursor()
        cursor.execute(
            'INSERT OR REPLACE INTO settings(name, value) VALUES(?, ?)',
            ('version', str(ROTKEHLCHEN_DB_VERSION)),
        )
        self.conn.commit()
Esempio n. 4
0
    def try_premium_at_start(self, api_key, api_secret, username, create_new,
                             sync_approval):
        """Check if new user provided api pair or we already got one in the DB"""

        if api_key != '':
            assert create_new, 'We should never get here for an already existing account'
            try:
                self.premium = premium_create_and_verify(api_key, api_secret)
            except (IncorrectApiKeyFormat, AuthenticationError) as e:
                log.error('Given API key is invalid')
                # At this point we are at a new user trying to create an account with
                # premium API keys and we failed. But a directory was created. Remove it.
                # But create a backup of it in case something went really wrong
                # and the directory contained data we did not want to lose
                shutil.move(
                    self.user_directory,
                    os.path.join(
                        self.data_dir,
                        f'auto_backup_{username}_{ts_now()}',
                    ),
                )
                shutil.rmtree(self.user_directory)
                raise AuthenticationError(
                    'Could not verify keys for the new account. '
                    '{}'.format(str(e)), )

        # else, if we got premium initialize it and try to sync with the server
        premium_credentials = self.data.db.get_rotkehlchen_premium()
        if premium_credentials:
            api_key = premium_credentials[0]
            api_secret = premium_credentials[1]
            try:
                self.premium = premium_create_and_verify(api_key, api_secret)
            except (IncorrectApiKeyFormat, AuthenticationError) as e:
                log.error(
                    f'Could not authenticate with the rotkehlchen server with '
                    f'the API keys found in the Database. Error: {str(e)}', )
                del self.premium
                self.premium = None

        if not self.premium:
            return

        if self.can_sync_data_from_server():
            if sync_approval == 'unknown' and not create_new:
                log.info('DB data at server newer than local')
                raise RotkehlchenPermissionError(
                    'Rotkehlchen Server has newer version of your DB data. '
                    'Should we replace local data with the server\'s?', )
            elif sync_approval == 'yes' or sync_approval == 'unknown' and create_new:
                log.info('User approved data sync from server')
                if self.sync_data_from_server():
                    if create_new:
                        # if we successfully synced data from the server and this is
                        # a new account, make sure the api keys are properly stored
                        # in the DB
                        self.data.db.set_rotkehlchen_premium(
                            api_key, api_secret)
            else:
                log.debug('Could sync data from server but user refused')
Esempio n. 5
0
    def unlock(
        self,
        username: str,
        password: str,
        create_new: bool,
    ) -> FilePath:
        user_data_dir = FilePath(os.path.join(self.data_directory, username))
        if create_new:
            if os.path.exists(user_data_dir):
                raise AuthenticationError(
                    'User {} already exists'.format(username))
            else:
                os.mkdir(user_data_dir)
        else:
            if not os.path.exists(user_data_dir):
                raise AuthenticationError(
                    'User {} does not exist'.format(username))

            if not os.path.exists(os.path.join(user_data_dir,
                                               'rotkehlchen.db')):
                # This is bad. User directory exists but database is missing.
                # Make a backup of the directory that user should probably remove
                # on their own. At the same time delete the directory so that a new
                # user account can be created
                shutil.move(
                    user_data_dir,
                    os.path.join(
                        self.data_directory,
                        f'auto_backup_{username}_{ts_now()}',
                    ),
                )

                raise AuthenticationError(
                    'User {} exists but DB is missing. Somehow must have been manually '
                    'deleted or is corrupt. Please recreate the user account. '
                    'A backup of the user directory was created.'.format(
                        username))

        self.db: DBHandler = DBHandler(user_data_dir, password,
                                       self.msg_aggregator)
        self.user_data_dir = user_data_dir
        self.logged_in = True
        self.username = username
        return user_data_dir
Esempio n. 6
0
 def __init__(self, user_data_dir, username, password):
     self.user_data_dir = user_data_dir
     self.connect(password)
     cursor = self.conn.cursor()
     try:
         cursor.execute(
             'CREATE TABLE IF NOT EXISTS timed_balances ('
             '    time INTEGER, currency VARCHAR[12], amount DECIMAL, usd_value DECIMAL'
             ')')
     except sqlcipher.DatabaseError:
         raise AuthenticationError(
             'Wrong password while decrypting the database')
     cursor.execute(
         'CREATE TABLE IF NOT EXISTS timed_location_data ('
         '    time INTEGER, location VARCHAR[24], usd_value DECIMAL'
         ')')
     cursor.execute('CREATE TABLE IF NOT EXISTS timed_unique_data ('
                    '    time INTEGER, net_usd DECIMAL'
                    ')')
     cursor.execute(
         'CREATE TABLE IF NOT EXISTS user_credentials ('
         '    name VARCHAR[24] NOT NULL PRIMARY KEY, api_key TEXT, api_secret TEXT'
         ')')
     cursor.execute(
         'CREATE TABLE IF NOT EXISTS blockchain_accounts ('
         '    blockchain VARCHAR[24], account TEXT NOT NULL PRIMARY KEY'
         ')')
     cursor.execute('CREATE TABLE IF NOT EXISTS multisettings ('
                    '    name VARCHAR[24] NOT NULL, value TEXT,'
                    '    UNIQUE(name, value)'
                    ')')
     cursor.execute(
         'CREATE TABLE IF NOT EXISTS current_balances ('
         '    asset VARCHAR[24] NOT NULL PRIMARY KEY, amount DECIMAL'
         ')')
     cursor.execute('CREATE TABLE IF NOT EXISTS trades ('
                    '    id INTEGER PRIMARY KEY ASC,'
                    '    time INTEGER,'
                    '    location VARCHAR[24],'
                    '    pair VARCHAR[24],'
                    '    type VARCHAR[3],'
                    '    amount DECIMAL,'
                    '    rate DECIMAL,'
                    '    fee DECIMAL,'
                    '    fee_currency VARCHAR[6],'
                    '    link TEXT,'
                    '    notes TEXT'
                    ')')
     cursor.execute('CREATE TABLE IF NOT EXISTS settings ('
                    '  name VARCHAR[24] NOT NULL PRIMARY KEY, value TEXT,'
                    '  UNIQUE(name, value)'
                    ')')
     cursor.execute(
         'INSERT OR IGNORE INTO settings(name, value) VALUES(?, ?)',
         ('version', str(ROTKEHLCHEN_DB_VERSION)))
     self.conn.commit()
Esempio n. 7
0
    def try_premium_at_start(self, api_key, api_secret, create_new,
                             sync_approval):
        """Check if new user provided api pair or we already got one in the DB"""

        if api_key != '':
            self.premium, valid, empty_or_error = premium_create_and_verify(
                api_key, api_secret)
            if not valid:
                log.error('Given API key is invalid')
                # At this point we are at a new user trying to create an account with
                # premium API keys and we failed. But a directory was created. Remove it.
                shutil.rmtree(self.user_directory)
                raise AuthenticationError(
                    'Could not verify keys for the new account. '
                    '{}'.format(empty_or_error), )
        else:
            # If we got premium initialize it and try to sync with the server
            premium_credentials = self.data.db.get_rotkehlchen_premium()
            if premium_credentials:
                api_key = premium_credentials[0]
                api_secret = premium_credentials[1]
                self.premium, valid, empty_or_error = premium_create_and_verify(
                    api_key,
                    api_secret,
                )
                if not valid:
                    log.error(
                        'The API keys found in the Database are not valid. Perhaps '
                        'they expired?', )
                del self.premium
                self.premium = None
                return
            else:
                # no premium credentials in the DB
                return

        if self.can_sync_data_from_server():
            if sync_approval == 'unknown' and not create_new:
                log.info('DB data at server newer than local')
                raise PermissionError(
                    'Rotkehlchen Server has newer version of your DB data. '
                    'Should we replace local data with the server\'s?', )
            elif sync_approval == 'yes' or sync_approval == 'unknown' and create_new:
                log.info('User approved data sync from server')
                if self.sync_data_from_server():
                    if create_new:
                        # if we successfully synced data from the server and this is
                        # a new account, make sure the api keys are properly stored
                        # in the DB
                        self.data.db.set_rotkehlchen_premium(
                            api_key, api_secret)
            else:
                log.debug('Could sync data from server but user refused')
Esempio n. 8
0
def premium_create_and_verify(credentials: PremiumCredentials) -> Premium:
    """Create a Premium object with the key pairs and verify them.

    Returns the created premium object

    raises AuthenticationError if the given key is rejected by the server
    """
    premium = Premium(credentials)

    if premium.is_active():
        return premium

    raise AuthenticationError('Rotkehlchen API key was rejected by server')
Esempio n. 9
0
    def __init__(self, user_data_dir, username, password):
        self.user_data_dir = user_data_dir
        self.connect(password)
        try:
            self.conn.executescript(DB_SCRIPT_CREATE_TABLES)
        except sqlcipher.DatabaseError:
            raise AuthenticationError(
                'Wrong password while decrypting the database')

        cursor = self.conn.cursor()
        cursor.execute(
            'INSERT OR IGNORE INTO settings(name, value) VALUES(?, ?)',
            ('version', str(ROTKEHLCHEN_DB_VERSION)))
        self.conn.commit()
Esempio n. 10
0
def premium_create_and_verify(api_key: ApiKey, api_secret: ApiSecret):
    """Create a Premium object with the key pairs and verify them.

    Returns the created premium object

    raises IncorrectApiKeyFormat if the given key is in the wrong format
    raises AuthenticationError if the given key is rejected by the server
    """
    try:
        premium = Premium(api_key, api_secret)
    except BinasciiError:
        raise IncorrectApiKeyFormat('Rotkehlchen api key is not in the correct format')
    if premium.is_active():
        return premium

    raise AuthenticationError('Rotkehlchen API key was rejected by server')
Esempio n. 11
0
    def __init__(self, user_data_dir: typing.FilePath, username: str,
                 password: str):
        self.user_data_dir = user_data_dir
        self.connect(password)
        try:
            self.conn.executescript(DB_SCRIPT_CREATE_TABLES)
        except sqlcipher.DatabaseError as e:  # pylint: disable=no-member
            errstr = str(e)
            if errstr == 'file is not a database':
                errstr = 'Wrong password while decrypting the database or not a database'
            raise AuthenticationError(errstr)

        self.run_updates()
        cursor = self.conn.cursor()
        cursor.execute(
            'INSERT OR REPLACE INTO settings(name, value) VALUES(?, ?)',
            ('version', str(ROTKEHLCHEN_DB_VERSION)))
        self.conn.commit()
Esempio n. 12
0
    def set_credentials(self, api_key: ApiKey, api_secret: ApiSecret) -> None:
        """Try to set the credentials for a premium rotkehlchen subscription

        Raises IncorrectApiKeyFormat if the given key is not in a proper format
        Raises AuthenticationError if the given key is rejected by the Rotkehlchen server
        """
        old_api_key = self.api_key
        old_secret = ApiSecret(base64.b64encode(self.secret))

        # Forget the last active value since we are trying new credentials
        self.status = SubscriptionStatus.UNKNOWN

        # If what's given is not even valid b64 encoding then stop here
        try:
            self.reset_credentials(api_key, api_secret)
        except BinasciiError as e:
            raise IncorrectApiKeyFormat(f'Secret Key formatting error: {str(e)}')

        active = self.is_active()
        if not active:
            self.reset_credentials(old_api_key, old_secret)
            raise AuthenticationError('Rotkehlchen API key was rejected by server')
Esempio n. 13
0
    def set_credentials(self, credentials: PremiumCredentials) -> None:
        """Try to set the credentials for a premium rotkehlchen subscription

        Raises AuthenticationError if the given key is rejected by the Rotkehlchen server
        """
        old_credentials = self.credentials

        # Forget the last active value since we are trying new credentials
        self.status = SubscriptionStatus.UNKNOWN

        # If what's given is not even valid b64 encoding then stop here
        try:
            self.reset_credentials(credentials)
        except BinasciiError as e:
            raise IncorrectApiKeyFormat(
                f'Secret Key formatting error: {str(e)}')

        active = self.is_active()
        if not active:
            self.reset_credentials(old_credentials)
            raise AuthenticationError(
                'Rotkehlchen API key was rejected by server')
Esempio n. 14
0
    def unlock(
            self,
            username: str,
            password: str,
            create_new: bool,
            initial_settings: Optional[ModifiableDBSettings] = None,
    ) -> Path:
        """Unlocks a user, either logging them in or creating a new user

        May raise:
        - SystemPermissionError if there are permission errors when accessing the DB
        or a directory in the user's filesystem
        - AuthenticationError if the given user does not exist, or if
        sqlcipher version problems are detected
        - DBUpgradeError if the rotki DB version is newer than the software or
        there is a DB upgrade and there is an error.
        """
        user_data_dir = self.data_directory / username
        if create_new:
            try:
                if (user_data_dir / 'rotkehlchen.db').exists():
                    raise AuthenticationError(
                        f'User {username} already exists. User data dir: {user_data_dir}',
                    )
                else:
                    user_data_dir.mkdir(exist_ok=True)
            except PermissionError as e:
                raise SystemPermissionError(f'Failed to create directory for user: {str(e)}')

        else:
            try:
                if not user_data_dir.exists():
                    raise AuthenticationError('User {} does not exist'.format(username))

                if not (user_data_dir / 'rotkehlchen.db').exists():
                    raise PermissionError

            except PermissionError:
                # This is bad. User directory exists but database is missing.
                # Or either DB or user directory can't be accessed due to permissions
                # Make a backup of the directory that user should probably remove
                # on their own. At the same time delete the directory so that a new
                # user account can be created
                shutil.move(
                    user_data_dir,  # type: ignore
                    self.data_directory / f'auto_backup_{username}_{ts_now()}',
                )

                raise SystemPermissionError(
                    'User {} exists but DB is missing. Somehow must have been manually '
                    'deleted or is corrupt or access permissions do not allow reading. '
                    'Please recreate the user account. '
                    'A backup of the user directory was created.'.format(username))

        self.db: DBHandler = DBHandler(
            user_data_dir=user_data_dir,
            password=password,
            msg_aggregator=self.msg_aggregator,
            initial_settings=initial_settings,
        )
        self.user_data_dir = user_data_dir
        self.logged_in = True
        self.username = username
        self.password = password
        return user_data_dir
Esempio n. 15
0
    def try_premium_at_start(
        self,
        api_key: ApiKey,
        api_secret: ApiSecret,
        username: str,
        create_new: bool,
        sync_approval: Literal['yes', 'no', 'unknown'],
    ) -> Premium:
        """
        Check if new user provided api pair or we already got one in the DB

        Returns the created premium if user's premium credentials were fine.

        If not it will raise AuthenticationError.
        """

        if api_key != '':
            assert create_new, 'We should never get here for an already existing account'

            try:
                self.premium = premium_create_and_verify(api_key, api_secret)
            except (IncorrectApiKeyFormat, AuthenticationError) as e:
                log.error('Given API key is invalid')
                # At this point we are at a new user trying to create an account with
                # premium API keys and we failed. But a directory was created. Remove it.
                # But create a backup of it in case something went really wrong
                # and the directory contained data we did not want to lose
                shutil.move(
                    self.data.user_directory,
                    os.path.join(
                        self.data.data_directory,
                        f'auto_backup_{username}_{ts_now()}',
                    ),
                )
                shutil.rmtree(self.data.user_directory)
                raise AuthenticationError(
                    'Could not verify keys for the new account. '
                    '{}'.format(str(e)), )

        # else, if we got premium data in the DB initialize it and try to sync with the server
        premium_credentials = self.data.db.get_rotkehlchen_premium()
        if premium_credentials:
            assert not create_new, 'We should never get here for a new account'
            api_key = premium_credentials[0]
            api_secret = premium_credentials[1]
            try:
                self.premium = premium_create_and_verify(api_key, api_secret)
            except (IncorrectApiKeyFormat, AuthenticationError) as e:
                message = (
                    f'Could not authenticate with the rotkehlchen server with '
                    f'the API keys found in the Database. Error: {str(e)}')
                log.error(message)
                raise AuthenticationError(message)

        result = self._can_sync_data_from_server(new_account=create_new)
        if result.can_sync == CanSync.ASK_USER:
            if sync_approval == 'unknown':
                log.info('DB data at server newer than local')
                raise RotkehlchenPermissionError(result.message)
            elif sync_approval == 'yes':
                log.info('User approved data sync from server')
                if self._sync_data_from_server_and_replace_local():
                    if create_new:
                        # if we successfully synced data from the server and this is
                        # a new account, make sure the api keys are properly stored
                        # in the DB
                        self.data.db.set_rotkehlchen_premium(
                            api_key, api_secret)
            else:
                log.debug('Could sync data from server but user refused')
        elif result.can_sync == CanSync.YES:
            log.info('User approved data sync from server')
            if self._sync_data_from_server_and_replace_local():
                if create_new:
                    # if we successfully synced data from the server and this is
                    # a new account, make sure the api keys are properly stored
                    # in the DB
                    self.data.db.set_rotkehlchen_premium(api_key, api_secret)

        # else result.can_sync was no, so we do nothing

        # Success, return premium
        return self.premium
Esempio n. 16
0
    def unlock(
        self,
        username: str,
        password: str,
        create_new: bool,
    ) -> FilePath:
        """Unlocks a user, either logging them in or creating a new user

        May raise:
        - SystemPermissionError if there are permission errors when accessing the DB
        or a directory in the user's filesystem
        - AuthenticationError if the given user does not exist, or if
        sqlcipher version problems are detected
        - DBUpgradeError if the rotki DB version is newer than the software or
        there is a DB upgrade and there is an error.
        """
        user_data_dir = FilePath(os.path.join(self.data_directory, username))
        if create_new:
            if os.path.exists(user_data_dir):
                raise AuthenticationError(
                    'User {} already exists'.format(username))
            else:
                try:
                    os.mkdir(user_data_dir)
                except PermissionError as e:
                    raise SystemPermissionError(
                        f'Failed to create directory for user: {str(e)}')

        else:
            if not os.path.exists(user_data_dir):
                raise AuthenticationError(
                    'User {} does not exist'.format(username))

            if not os.path.exists(os.path.join(user_data_dir,
                                               'rotkehlchen.db')):
                # This is bad. User directory exists but database is missing.
                # Make a backup of the directory that user should probably remove
                # on their own. At the same time delete the directory so that a new
                # user account can be created
                shutil.move(
                    user_data_dir,
                    os.path.join(
                        self.data_directory,
                        f'auto_backup_{username}_{ts_now()}',
                    ),
                )

                raise SystemPermissionError(
                    'User {} exists but DB is missing. Somehow must have been manually '
                    'deleted or is corrupt or access permissions do not allow reading. '
                    'Please recreate the user account. '
                    'A backup of the user directory was created.'.format(
                        username))

        self.db: DBHandler = DBHandler(user_data_dir, password,
                                       self.msg_aggregator)
        self.user_data_dir = user_data_dir
        self.logged_in = True
        self.username = username
        self.password = password
        return user_data_dir