From ef7d1b6247bb4adf5a882b16d23c8cf48f1b4b9d Mon Sep 17 00:00:00 2001 From: jamshale Date: Thu, 25 Jul 2024 17:52:13 +0000 Subject: [PATCH 1/3] Add rekey feature with blank key support Signed-off-by: jamshale --- aries_cloudagent/askar/store.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/aries_cloudagent/askar/store.py b/aries_cloudagent/askar/store.py index 763ea4b077..6f4a1eea11 100644 --- a/aries_cloudagent/askar/store.py +++ b/aries_cloudagent/askar/store.py @@ -6,7 +6,7 @@ from aries_askar import AskarError, AskarErrorCode, Store -from ..core.error import ProfileError, ProfileDuplicateError, ProfileNotFoundError +from ..core.error import ProfileDuplicateError, ProfileError, ProfileNotFoundError from ..core.profile import Profile from ..utils.env import storage_path @@ -49,9 +49,7 @@ def __init__(self, config: dict = None): f"With key derivation method '{self.KEY_DERIVATION_RAW}'," "key should also be provided" ) - # self.rekey = config.get("rekey") - # self.rekey_derivation_method = config.get("rekey_derivation_method") - + self.rekey = config.get("rekey") self.name = config.get("name") or Profile.DEFAULT_NAME self.in_memory = self.name == ":memory:" if self.in_memory: @@ -156,6 +154,10 @@ async def open_store(self, provision: bool = False) -> "AskarOpenStore": self.key_derivation_method, self.key, ) + + if self.rekey: + await Store.rekey(store, self.key_derivation_method, self.rekey) + except AskarError as err: if err.code == AskarErrorCode.DUPLICATE: raise ProfileDuplicateError( @@ -165,6 +167,17 @@ async def open_store(self, provision: bool = False) -> "AskarOpenStore": raise ProfileNotFoundError( f"Store '{self.name}' not found", ) + if self.rekey: + # Attempt to rekey the store with a default key in the case the key + # was created with a blank key before version 0.12.0 + store = await Store.open( + self.get_uri(), + self.key_derivation_method, + AskarStoreConfig.DEFAULT_KEY, + ) + await Store.rekey(store, self.key_derivation_method, self.rekey) + return AskarOpenStore(self, provision, store) + raise ProfileError("Error opening store") from err return AskarOpenStore(self, provision, store) From 0d16ac4e7e5bafdea254de959c63d4ed3697241a Mon Sep 17 00:00:00 2001 From: jamshale Date: Thu, 25 Jul 2024 18:37:45 +0000 Subject: [PATCH 2/3] Refactor / Remove other key exists check / unit tests Signed-off-by: jamshale --- aries_cloudagent/askar/store.py | 63 +++++++++------- aries_cloudagent/askar/tests/test_store.py | 88 +++++++++++++++++++++- aries_cloudagent/config/argparse.py | 16 +++- 3 files changed, 134 insertions(+), 33 deletions(-) diff --git a/aries_cloudagent/askar/store.py b/aries_cloudagent/askar/store.py index 6f4a1eea11..82fb6ee996 100644 --- a/aries_cloudagent/askar/store.py +++ b/aries_cloudagent/askar/store.py @@ -36,20 +36,17 @@ def __init__(self, config: dict = None): config = {} self.auto_recreate = config.get("auto_recreate", False) self.auto_remove = config.get("auto_remove", False) + self.key = config.get("key", self.DEFAULT_KEY) self.key_derivation_method = ( config.get("key_derivation_method") or self.DEFAULT_KEY_DERIVATION ) - if ( - self.key_derivation_method.lower() == self.KEY_DERIVATION_RAW.lower() - and self.key == "" - ): - raise ProfileError( - f"With key derivation method '{self.KEY_DERIVATION_RAW}'," - "key should also be provided" - ) self.rekey = config.get("rekey") + self.rekey_derivation_method = ( + config.get("rekey_derivation_method") or self.DEFAULT_KEY_DERIVATION + ) + self.name = config.get("name") or Profile.DEFAULT_NAME self.in_memory = self.name == ":memory:" if self.in_memory: @@ -131,6 +128,20 @@ async def remove_store(self): ) raise ProfileError("Error removing store") from err + def _handle_open_error(self, err: AskarError, retry=False): + if err.code == AskarErrorCode.DUPLICATE: + raise ProfileDuplicateError( + f"Duplicate store '{self.name}'", + ) + if err.code == AskarErrorCode.NOT_FOUND: + raise ProfileNotFoundError( + f"Store '{self.name}' not found", + ) + if retry and self.rekey: + return + + raise ProfileError("Error opening store") from err + async def open_store(self, provision: bool = False) -> "AskarOpenStore": """Open a store, removing and/or creating it if so configured. @@ -154,32 +165,28 @@ async def open_store(self, provision: bool = False) -> "AskarOpenStore": self.key_derivation_method, self.key, ) - - if self.rekey: - await Store.rekey(store, self.key_derivation_method, self.rekey) + if self.rekey: + await Store.rekey(store, self.rekey_derivation_method, self.rekey) except AskarError as err: - if err.code == AskarErrorCode.DUPLICATE: - raise ProfileDuplicateError( - f"Duplicate store '{self.name}'", - ) - if err.code == AskarErrorCode.NOT_FOUND: - raise ProfileNotFoundError( - f"Store '{self.name}' not found", - ) + self._handle_open_error(err, retry=True) + if self.rekey: # Attempt to rekey the store with a default key in the case the key - # was created with a blank key before version 0.12.0 - store = await Store.open( - self.get_uri(), - self.key_derivation_method, - AskarStoreConfig.DEFAULT_KEY, - ) - await Store.rekey(store, self.key_derivation_method, self.rekey) + # was created with a blank key before version 0.12.0. This can be removed + # in a future version or when 0.11.0 is no longer supported. + try: + store = await Store.open( + self.get_uri(), + self.key_derivation_method, + AskarStoreConfig.DEFAULT_KEY, + ) + except AskarError as err: + self._handle_open_error(err) + + await Store.rekey(store, self.rekey_derivation_method, self.rekey) return AskarOpenStore(self, provision, store) - raise ProfileError("Error opening store") from err - return AskarOpenStore(self, provision, store) diff --git a/aries_cloudagent/askar/tests/test_store.py b/aries_cloudagent/askar/tests/test_store.py index 5d128d1840..441dc416c7 100644 --- a/aries_cloudagent/askar/tests/test_store.py +++ b/aries_cloudagent/askar/tests/test_store.py @@ -1,8 +1,11 @@ from unittest import IsolatedAsyncioTestCase -from ...core.error import ProfileError +from aries_askar import AskarError, AskarErrorCode, Store -from ..store import AskarStoreConfig +from aries_cloudagent.tests import mock + +from ...core.error import ProfileDuplicateError, ProfileError, ProfileNotFoundError +from ..store import AskarOpenStore, AskarStoreConfig class TestStoreConfig(IsolatedAsyncioTestCase): @@ -31,3 +34,84 @@ async def test_init_should_fail_when_key_missing(self): with self.assertRaises(ProfileError): askar_store = AskarStoreConfig(config) + + +class TestStoreOpen(IsolatedAsyncioTestCase): + key_derivation_method = "Raw" + key = "key" + storage_type = "default" + + @mock.patch.object(Store, "open", autospec=True) + async def test_open_store(self, mock_store_open): + config = { + "key_derivation_method": self.key_derivation_method, + "key": self.key, + "storage_type": self.storage_type, + } + + store = await AskarStoreConfig(config).open_store() + assert isinstance(store, AskarOpenStore) + assert mock_store_open.called + + @mock.patch.object(Store, "open") + async def test_open_store_fails(self, mock_store_open): + config = { + "key_derivation_method": self.key_derivation_method, + "key": self.key, + "storage_type": self.storage_type, + } + + mock_store_open.side_effect = [ + AskarError(AskarErrorCode.NOT_FOUND, message="testing"), + AskarError(AskarErrorCode.DUPLICATE, message="testing"), + AskarError(AskarErrorCode.ENCRYPTION, message="testing"), + ] + + with self.assertRaises(ProfileNotFoundError): + await AskarStoreConfig(config).open_store() + with self.assertRaises(ProfileDuplicateError): + await AskarStoreConfig(config).open_store() + with self.assertRaises(ProfileError): + await AskarStoreConfig(config).open_store() + + @mock.patch.object(Store, "open") + @mock.patch.object(Store, "rekey") + async def test_open_store_fail_retry_with_rekey(self, mock_store_open, mock_rekey): + config = { + "key_derivation_method": self.key_derivation_method, + "key": self.key, + "storage_type": self.storage_type, + "rekey": "rekey", + } + + mock_store_open.side_effect = [ + AskarError(AskarErrorCode.ENCRYPTION, message="testing"), + mock.AsyncMock(auto_spec=True), + ] + + store = await AskarStoreConfig(config).open_store() + + assert isinstance(store, AskarOpenStore) + assert mock_rekey.called + + @mock.patch.object(Store, "open") + @mock.patch.object(Store, "rekey") + async def test_open_store_fail_retry_with_rekey_fails( + self, mock_store_open, mock_rekey + ): + config = { + "key_derivation_method": self.key_derivation_method, + "key": self.key, + "storage_type": self.storage_type, + "rekey": "rekey", + } + + mock_store_open.side_effect = [ + AskarError(AskarErrorCode.ENCRYPTION, message="testing"), + mock.AsyncMock(auto_spec=True), + ] + + store = await AskarStoreConfig(config).open_store() + + assert isinstance(store, AskarOpenStore) + assert mock_rekey.called diff --git a/aries_cloudagent/config/argparse.py b/aries_cloudagent/config/argparse.py index cf94e1a0a4..34427e9431 100644 --- a/aries_cloudagent/config/argparse.py +++ b/aries_cloudagent/config/argparse.py @@ -1632,10 +1632,16 @@ def add_arguments(self, parser: ArgumentParser): type=str, metavar="", env_var="ACAPY_WALLET_KEY_DERIVATION_METHOD", + help=("Specifies the key derivation method used for wallet encryption."), + ) + parser.add_argument( + "--wallet-rekey-derivation-method", + type=str, + metavar="", + env_var="ACAPY_WALLET_REKEY_DERIVATION_METHOD", help=( - "Specifies the key derivation method used for wallet encryption." - "If RAW key derivation method is used, also --wallet-key parameter" - " is expected." + "Specifies the key derivation method used for the replacement" + "rekey encryption." ), ) parser.add_argument( @@ -1694,6 +1700,10 @@ def get_settings(self, args: Namespace) -> dict: settings["wallet.type"] = args.wallet_type if args.wallet_key_derivation_method: settings["wallet.key_derivation_method"] = args.wallet_key_derivation_method + if args.wallet_rekey_derivation_method: + settings["wallet.rekey_derivation_method"] = ( + args.wallet_rekey_derivation_method + ) if args.wallet_storage_config: settings["wallet.storage_config"] = args.wallet_storage_config if args.wallet_storage_creds: From d42b0122eaca1508c8c7caae25ace57c666dc47d Mon Sep 17 00:00:00 2001 From: jamshale Date: Mon, 29 Jul 2024 16:13:59 +0000 Subject: [PATCH 3/3] Remove no wallet key test from store config Signed-off-by: jamshale --- aries_cloudagent/askar/tests/test_store.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/aries_cloudagent/askar/tests/test_store.py b/aries_cloudagent/askar/tests/test_store.py index 441dc416c7..6923f5f29e 100644 --- a/aries_cloudagent/askar/tests/test_store.py +++ b/aries_cloudagent/askar/tests/test_store.py @@ -26,15 +26,6 @@ async def test_init_success(self): assert askar_store.key == self.key assert askar_store.storage_type == self.storage_type - async def test_init_should_fail_when_key_missing(self): - config = { - "key_derivation_method": self.key_derivation_method, - "storage_type": self.storage_type, - } - - with self.assertRaises(ProfileError): - askar_store = AskarStoreConfig(config) - class TestStoreOpen(IsolatedAsyncioTestCase): key_derivation_method = "Raw"