def test_reencrypt_user_secret_on_set_password(empty_data_folder): """ Should re-encrypt the user_secret when the user changes their password. """ specter = Specter(data_folder=empty_data_folder) password = "******" user = User.from_json( user_dict={ "id": "someuser", "username": "******", "password": hash_password(password), "config": {}, "is_admin": False, "services": None, }, specter=specter, ) # Force generation of a new user_secret user.decrypt_user_secret(password) assert user.encrypted_user_secret is not None assert user.plaintext_user_secret is not None first_encrypted_user_secret = user.encrypted_user_secret first_plaintext_user_secret = user.plaintext_user_secret new_password = "******" user.set_password(new_password) # The new encrypted_user_secret will be different... assert first_encrypted_user_secret != user.encrypted_user_secret # ...but the plaintext_user_secret remains unchanged assert first_plaintext_user_secret == user.plaintext_user_secret
def remove_all_services_from_user(self, user: User): """ Clears User.services and `user_secret`; wipes the User's ServiceEncryptedStorage. """ # Don't show any Services on the sidebar for the admin user user.services.clear() # Reset as if we never had any encrypted storage user.delete_user_secret(autosave=False) user.save_info() if self.user_has_encrypted_storage(user=user): # Encrypted Service data is now orphaned since there is no # password. So wipe it from the disk. ServiceEncryptedStorageManager.get_instance().delete_all_service_data(user)
def test_generate_user_secret_on_set_password(empty_data_folder): """ Should generate a user_secret if one does not yet exist when the User's password is changed via set_password. """ specter = Specter(data_folder=empty_data_folder) password = "******" user = User.from_json( user_dict={ "id": "someuser", "username": "******", "password": hash_password(password), "config": {}, "is_admin": False, "services": None, }, specter=specter, ) assert user.encrypted_user_secret is None assert user.plaintext_user_secret is None new_password = "******" user.set_password(new_password) assert user.encrypted_user_secret is not None assert user.plaintext_user_secret is not None # Reset the plaintext user_secret and test decryption user.plaintext_user_secret = None user.decrypt_user_secret(new_password) assert user.plaintext_user_secret is not None
def test_generate_user_secret_on_decrypt_user_secret(empty_data_folder): """ Should generate a user_secret if one does not yet exist when decrypt_user_secret is called (happens during the login flow). """ specter = Specter(data_folder=empty_data_folder) password = "******" user = User.from_json( user_dict={ "id": "someuser", "username": "******", "password": hash_password(password), "config": {}, "is_admin": False, "services": None, }, specter=specter, ) assert user.encrypted_user_secret is None assert user.plaintext_user_secret is None # Even though there's no user_secret yet, the flow calls decrypt anyway... user.decrypt_user_secret(password) # ...and a new user_secret is created and stored encrypted and plaintext assert user.encrypted_user_secret is not None assert user.plaintext_user_secret is not None
def test_ServiceEncryptedStorage(empty_data_folder): specter_mock = Mock() specter_mock.config = {"uid": ""} specter_mock.user_manager = Mock() specter_mock.user_manager.users = [""] user1 = User.from_json( user_dict={ "id": "user1", "username": "******", "password": hash_password("somepassword"), "config": {}, "is_admin": False, "services": None, }, specter=specter_mock, ) user2 = User.from_json( user_dict={ "id": "user2", "username": "******", "password": hash_password("somepassword"), "config": {}, "is_admin": False, "services": None, }, specter=specter_mock, ) user1._generate_user_secret("muh") # Can set and get service storage fields service_storage = ServiceEncryptedStorage(empty_data_folder, user1) service_storage.set_service_data("a_service_id", {"somekey": "green"}) assert service_storage.get_service_data("a_service_id") == { "somekey": "green" } assert service_storage.get_service_data("another_service_id") == {} # We expect a call for a user that isn't logged in to fail with pytest.raises(ServiceEncryptedStorageError) as execinfo: ServiceEncryptedStorage(empty_data_folder, user2) assert "must be authenticated with password" in str(execinfo.value)
def test_storage_field_encrypt_decrypt(empty_data_folder): """ Storage class should be able to use the associated User's decrypted user_secret to encrypt and decrypt the specified encrypted_fields to and from on-disk json. When loaded into memory, all fields -- whether encrypted or not -- should be plaintext readable. """ specter = Specter(data_folder=empty_data_folder) password = "******" user = User.from_json( user_dict={ "id": "someuser", "username": "******", "password": hash_password("somepassword"), "config": {}, "is_admin": False, "services": None, }, specter=specter, ) # User must provide their password in order to decrypt their user_secret which is # then used to decrypt/encrypt their service storage user.decrypt_user_secret(password) storage = ExampleStorage( data_folder=specter.data_folder, encryption_key=user.plaintext_user_secret ) storage.data["testfield1"] = "This data is not encrypted" storage.data["testfield2"] = "This data WILL BE encrypted" storage._save() # Read the resulting storage file with open(storage.data_file, "r") as storage_json_file: data_on_disk = json.load(storage_json_file) print(data_on_disk) # Plaintext fields are readable... assert data_on_disk["testfield1"] == storage.data["testfield1"] # ...while encrypted fields are not assert data_on_disk["testfield2"] != storage.data["testfield2"] # Re-instantiate the storage so it has to load from the saved file... storage_2 = ExampleStorage( data_folder=specter.data_folder, encryption_key=user.plaintext_user_secret ) # ...and verify the field decryption assert storage_2.data["testfield2"] == storage.data["testfield2"] assert data_on_disk["testfield2"] != storage_2.data["testfield2"]
def test_WalletImporter_integration(specter_regtest_configured, bitcoin_regtest): """ WalletImporter can load a wallet from a backup json with unknown devices and initialize a watch-only wallet that can receive funds and update its balance. """ specter = specter_regtest_configured someuser: User = specter.user_manager.add_user( User.from_json( { "id": "someuser", "username": "******", "password": "******", "config": {}, "is_admin": False, }, specter, )) specter.user_manager.save() specter.check() # Create a Wallet wallet_json = '{"label": "another_simple_wallet", "blockheight": 0, "descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/0/*)#xp8lv5nr", "devices": [{"type": "trezor", "label": "trezor"}]} ' wallet_importer = WalletImporter(wallet_json, specter, device_manager=someuser.device_manager) wallet_importer.create_nonexisting_signers( someuser.device_manager, { "unknown_cosigner_0_name": "trezor", "unknown_cosigner_0_type": "trezor" }, ) dm: DeviceManager = someuser.device_manager wallet = wallet_importer.create_wallet(someuser.wallet_manager) # fund it with some coins bitcoin_regtest.testcoin_faucet(address=wallet.getnewaddress(), confirm_payment=False) # There can be a delay in the node generating the faucet deposit tx so keep # rechecking until it's done (or we timeout). for i in range(0, 15): wallet.update() if wallet.update_balance()["untrusted_pending"] != 0: break else: time.sleep(2) wallet = someuser.wallet_manager.get_by_alias("another_simple_wallet") assert wallet.update_balance()["untrusted_pending"] == 20
def remove_swan_integration(cls, user: User): # Unreserve unused addresses in all wallets for wallet_name, wallet in user.wallet_manager.wallets.items(): SwanService.unreserve_addresses(wallet=wallet) # If an autowithdrawal setup is active, remove pending addrs from Swan try: service_data = SwanService.get_current_user_service_data() if service_data.get(cls.SPECTER_WALLET_ALIAS) and service_data.get( cls.SWAN_WALLET_ID): # Import here to prevent circular dependency from . import client as swan_client cls.client().delete_autowithdrawal_addresses( service_data[cls.SWAN_WALLET_ID]) except Exception as e: # Note the exception but proceed with clearing local data logger.exception(e) # Wipe the on-disk encrypted service data (refresh_token, etc) SwanService.set_current_user_service_data({}) # Remove Swan from User's list of active Services user.remove_service(SwanService.id)
def test_reencrypt_user_secret_on_iterations_increase(empty_data_folder): """ Should re-encrypt the user_secret when the User.encryption_iterations is increased """ specter = Specter(data_folder=empty_data_folder) password = "******" user = User.from_json( user_dict={ "id": "someuser", "username": "******", "password": hash_password(password), "config": {}, "is_admin": False, "services": None, }, specter=specter, ) # Override current default iterations setting original_encryption_iterations = user.encryption_iterations user.encryption_iterations -= 10000 # Force generation of a new user_secret user.decrypt_user_secret(password) assert user.encrypted_user_secret is not None assert user.plaintext_user_secret is not None assert user.encrypted_user_secret["iterations"] < original_encryption_iterations first_encrypted_user_secret = user.encrypted_user_secret first_plaintext_user_secret = user.plaintext_user_secret # Reset iterations to default user.encryption_iterations = original_encryption_iterations # On decrypt, should automatically re-encrypt the `user_secret` user.decrypt_user_secret(password) assert user.encrypted_user_secret["iterations"] == original_encryption_iterations # The new encrypted_user_secret will be different... assert first_encrypted_user_secret != user.encrypted_user_secret # ...but the plaintext_user_secret remains unchanged assert first_plaintext_user_secret == user.plaintext_user_secret
def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder): assert bitcoin_regtest.get_rpc().test_connection() config = { "rpc": { "autodetect": False, "datadir": "", "user": bitcoin_regtest.rpcconn.rpcuser, "password": bitcoin_regtest.rpcconn.rpcpassword, "port": bitcoin_regtest.rpcconn.rpcport, "host": bitcoin_regtest.rpcconn.ipaddress, "protocol": "http", }, "auth": { "method": "rpcpasswordaspin", }, } specter = Specter(data_folder=devices_filled_data_folder, config=config) assert specter.chain == "regtest" # Create a User someuser = specter.user_manager.add_user( User.from_json( user_dict={ "id": "someuser", "username": "******", "password": hash_password("somepassword"), "config": {}, "is_admin": False, "services": None, }, specter=specter, )) specter.user_manager.save() specter.check() assert not someuser.wallet_manager.working_folder is None # Create a Wallet wallet_json = '{"label": "a_simple_wallet", "blockheight": 0, "descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/0/*)#xp8lv5nr", "devices": [{"type": "trezor", "label": "trezor"}]} ' wallet_importer = WalletImporter(wallet_json, specter, device_manager=someuser.device_manager) wallet_importer.create_nonexisting_signers( someuser.device_manager, { "unknown_cosigner_0_name": "trezor", "unknown_cosigner_0_type": "trezor" }, ) dm: DeviceManager = someuser.device_manager wallet = wallet_importer.create_wallet(someuser.wallet_manager) try: # fund it with some coins bitcoin_regtest.testcoin_faucet(address=wallet.getnewaddress()) # make sure it's confirmed bitcoin_regtest.mine() # Realize that the wallet has funds: wallet.update() except SpecterError as se: if str(se).startswith("Timeout"): pytest.fail( "We got a Bitcoin-RPC timeout while setting up the test, minting some coins. Test Error! Check cpu/mem utilastion and btc/elem logs!" ) return else: raise se assert wallet.fullbalance >= 20 assert not specter.wallet_manager.working_folder is None try: yield specter finally: # Deleting all Wallets (this will also purge them on core) for user in specter.user_manager.users: for wallet in list(user.wallet_manager.wallets.values()): user.wallet_manager.delete_wallet( wallet, bitcoin_datadir=bitcoin_regtest.datadir, chain="regtest")