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
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()
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()
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')
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
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()
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')
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')
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()
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')
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()
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')
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')
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
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
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