async def fetch(self, limit: int = None) -> Sequence[StorageRecord]: """ Fetch next batch of search results. Raise BadSearch if search is closed, WalletState if wallet is closed. :param limit: maximum number of records to return (default value Wallet.DEFAULT_CHUNK) :return: next batch of records found """ LOGGER.debug('StorageRecordSearch.fetch >>> limit: %s', limit) if not self.opened: LOGGER.debug('StorageRecordSearch.fetch <!< Storage record search is closed') raise BadSearch('Storage record search is closed') if not self._wallet.opened: LOGGER.debug('StorageRecordSearch.fetch <!< Wallet %s is closed', self._wallet.name) raise WalletState('Wallet {} is closed'.format(self._wallet.name)) records = json.loads(await non_secrets.fetch_wallet_search_next_records( self._wallet.handle, self.handle, limit or Wallet.DEFAULT_CHUNK))['records'] or [] # at exhaustion results['records'] = None rv = [StorageRecord(typ=rec['type'], value=rec['value'], tags=rec['tags'], ident=rec['id']) for rec in records] LOGGER.debug('StorageRecordSearch.fetch <<< %s', rv) return rv
async def decrypt(self, ciphertext: bytes, sender: str = None) -> (bytes, str): """ Decrypt ciphertext and optionally authenticate sender. Raise BadKey if authentication operation reveals sender key distinct from current verification key of owner of input DID. Raise WalletState if wallet is closed. :param ciphertext: ciphertext, as bytes :param sender: DID or verification key of sender, None for anonymously encrypted ciphertext :return: decrypted bytes and sender verification key (None for anonymous decryption) """ LOGGER.debug('BaseAnchor.decrypt >>> ciphertext: %s, sender: %s', ciphertext, sender) if not self.wallet.handle: # don't risk fetching verkey from ledger only to fail encryption on closed wallet LOGGER.debug('BaseAnchor.decrypt <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) from_verkey = None if sender: from_verkey = await self._verkey_for(sender) rv = await self.wallet.decrypt(ciphertext, True if from_verkey else None, to_verkey=None, from_verkey=from_verkey) LOGGER.debug('BaseAnchor.decrypt <<< %s', rv) return rv
async def delete_non_secret(self, typ: str, ident: str) -> None: """ Remove a non-secret record by its type and identifier. Silently return if no such record is present. Raise WalletState for closed wallet. :param typ: non-secret storage record type :param ident: non-secret storage record identifier """ LOGGER.debug('Wallet.delete_non_secret >>> typ: %s, ident: %s', typ, ident) if not self.handle: LOGGER.debug('Wallet.delete_non_secret <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) try: await non_secrets.delete_wallet_record(self.handle, typ, ident) except IndyError as x_indy: if x_indy.error_code == ErrorCode.WalletItemNotFound: LOGGER.info('Wallet.delete_non_secret <!< no record for type %s on identifier %s', typ, ident) else: LOGGER.debug( 'Wallet.delete_non_secret <!< deletion of %s record on identifier %s raised indy error code %s', typ, ident, x_indy.error_code) raise LOGGER.debug('Wallet.delete_non_secret <<<')
async def encrypt(self, message: bytes, authn: bool = False, recip: str = None) -> bytes: """ Encrypt plaintext for owner of DID or verification key, anonymously or via authenticated encryption scheme. If given DID, first check wallet and then pool for corresponding verification key. Raise WalletState if the wallet is closed. Given a recipient DID not in the wallet, raise AbsentPool if the instance has no pool or ClosedPool if its pool is closed. :param message: plaintext, as bytes :param authn: whether to use authenticated encryption scheme :param recip: DID or verification key of recipient, None for anchor's own :return: ciphertext, as bytes """ LOGGER.debug( 'BaseAnchor.encrypt >>> message: %s, authn: %s, recip: %s', message, authn, recip) if not self.wallet.handle: # don't risk fetching verkey from ledger only to fail encryption on closed wallet LOGGER.debug('BaseAnchor.encrypt <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) rv = await self.wallet.encrypt(message, authn, await self._verkey_for(recip)) LOGGER.debug('BaseAnchor.auth_encrypt <<< %s', rv) return rv
async def create_link_secret(self, label: str) -> None: """ Create link secret (a.k.a. master secret) used in proofs by HolderProver, if the current link secret does not already correspond to the input link secret label. Raise WalletState if wallet is closed, or any other IndyError causing failure to set link secret in wallet. :param label: label for link secret; indy-sdk uses label to generate link secret """ LOGGER.debug('Wallet.create_link_secret >>> label: %s', label) if not self.handle: LOGGER.debug('Wallet.create_link_secret <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) try: await anoncreds.prover_create_master_secret(self.handle, label) await self._write_link_secret_label(label) except IndyError as x_indy: if x_indy.error_code == ErrorCode.AnoncredsMasterSecretDuplicateNameError: LOGGER.warning( 'Wallet %s link secret already current: abstaining from updating label record', self.name) await self._write_link_secret_label(label) else: LOGGER.debug( 'Wallet.create_link_secret <!< cannot create link secret for wallet %s, indy error code %s', self.name, x_indy.error_code) raise LOGGER.debug('Wallet.create_link_secret <<<')
async def get_signing_key(self, verkey: str) -> KeyInfo: """ Get signing key pair for input verification key. Raise WalletState if wallet is closed, AbsentRecord for no such key pair. :param verkey: verification key of key pair :return: KeyInfo for key pair """ LOGGER.debug('Wallet.get_signing_key >>> seed: [SEED], verkey: %s', verkey) if not self.handle: LOGGER.debug('Wallet.get_signing_key <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) try: metadata = await crypto.get_key_metadata(self.handle, verkey) except IndyError as x_indy: if x_indy.error_code == ErrorCode.WalletItemNotFound: LOGGER.debug('Wallet.get_signing_key <!< Verification key %s not in wallet %s', verkey, self.name) raise AbsentRecord('Verification key not in wallet {}'.format(self.name)) LOGGER.debug('Wallet.get_signing_key <!< indy-sdk raised error %s', x_indy.error_code) raise rv = KeyInfo(verkey, json.loads(metadata) if metadata else {}) LOGGER.debug('Wallet.get_signing_key <<< %s', rv) return rv
async def replace_signing_key_metadata(self, verkey: str, metadata: dict) -> KeyInfo: """ Replace the metadata associated with a signing key pair. Raise WalletState if wallet is closed, AbsentRecord for no such key pair. :param verkey: verification key of key pair :param metadata: new metadata to store :return: resulting KeyInfo for key pair """ LOGGER.debug('Wallet.replace_signing_key_metadata >>> verkey: %s, metadata: %s', verkey, metadata) if not self.handle: LOGGER.debug('Wallet.replace_signing_key_metadata <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) try: await crypto.get_key_metadata(self.handle, verkey) except IndyError as x_indy: if x_indy.error_code == ErrorCode.WalletItemNotFound: LOGGER.debug( 'Wallet.replace_signing_key_metadata <!< Verification key %s not in wallet %s', verkey, self.name) raise AbsentRecord('Verification key not in wallet {}'.format(self.name)) LOGGER.debug('Wallet.replace_signing_key_metadata <!< indy-sdk raised error %s', x_indy.error_code) raise await crypto.set_key_metadata(self.handle, verkey, json.dumps(metadata or {})) rv = await self.get_signing_key(verkey) LOGGER.debug('Wallet.replace_signing_key_metadata <<< %s', rv) return rv
async def verify(self, message: bytes, signature: bytes, verkey: str = None) -> bool: """ Verify signature against input signer verification key (default anchor's own). Raise AbsentMessage for missing message or signature, or WalletState if wallet is closed. :param message: Content to sign, as bytes :param signature: signature, as bytes :param verkey: signer verification key (default for anchor's own) :return: whether signature is valid """ LOGGER.debug('Wallet.verify >>> message: %s, signature: %s, verkey: %s', message, signature, verkey) if not message: LOGGER.debug('Wallet.verify <!< No message to verify') raise AbsentMessage('No message to verify') if not signature: LOGGER.debug('Wallet.verify <!< No signature to verify') raise AbsentMessage('No signature to verify') if not self.handle: LOGGER.debug('Wallet.verify <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) rv = await crypto.crypto_verify(verkey or self.verkey, message, signature) LOGGER.debug('Wallet.verify <<< %s', rv) return rv
async def reseed_apply(self) -> None: """ Replace verification key with new verification key from reseed operation. Raise WalletState if wallet is closed. """ LOGGER.debug('Wallet.reseed_apply >>>') if not self.handle: LOGGER.debug('Wallet.reseed_init <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) await did.replace_keys_apply(self.handle, self.did) self.verkey = await did.key_for_local_did(self.handle, self.did) await did.set_did_metadata( self.handle, self.did, json.dumps({ 'anchor': True, 'since': int(time()) })) LOGGER.info('Wallet %s set seed hash metadata for DID %s', self.name, self.did) self._next_seed = None LOGGER.debug('Wallet.reseed_apply <<<')
async def verify(self, message: bytes, signature: bytes, signer: str = None) -> bool: """ Verify signature with input signer verification key (via lookup by DID first if need be). Raise WalletState if wallet is closed. :param message: Content to sign, as bytes :param signature: signature, as bytes :param signer: signer DID or verification key; omit for anchor's own :return: whether signature is valid """ LOGGER.debug( 'BaseAnchor.verify >>> signer: %s, message: %s, signature: %s', signer, message, signature) if not self.wallet.handle: LOGGER.debug('BaseAnchor.verify <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) verkey = None if signer: verkey = await self._verkey_for(signer) rv = await self.wallet.verify(message, signature, verkey) LOGGER.debug('BaseAnchor.verify <<< %s', rv) return rv
async def get_public_did(self) -> str: """ Get current public DID by metadata, None for not yet set. :return: DID """ LOGGER.debug('Wallet.get_public_did >>>') if not self.handle: LOGGER.debug('Wallet.get_public_did <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) rv = None dids_with_meta = json.loads(await did.list_my_dids_with_meta(self.handle)) # list latest = 0 for did_with_meta in dids_with_meta: try: meta = json.loads(did_with_meta['metadata']) if did_with_meta['metadata'] else {} if not meta.get('anchor', False): continue if isinstance(meta, dict) and meta.get('since', -1) > latest: rv = did_with_meta.get('did') except json.decoder.JSONDecodeError: continue # it's not an public DID, carry on LOGGER.debug('Wallet.get_public_did <<< %s', rv) return rv
async def sign(self, message: bytes, verkey: str = None) -> bytes: """ Derive signing key and Sign message; return signature. Raise WalletState if wallet is closed. Raise AbsentMessage for missing message, or WalletState if wallet is closed. :param message: Content to sign, as bytes :param verkey: verification key corresponding to private signing key (default anchor's own) :return: signature, as bytes """ LOGGER.debug('Wallet.sign >>> message: %s, verkey: %s', message, verkey) if not message: LOGGER.debug('Wallet.sign <!< No message to sign') raise AbsentMessage('No message to sign') if not self.handle: LOGGER.debug('Wallet.sign <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) rv = await crypto.crypto_sign(self.handle, verkey or self.verkey, message) LOGGER.debug('Wallet.sign <<< %s', rv) return rv
async def decrypt(self, ciphertext: bytes, sender: str = None) -> bytes: """ Decrypt ciphertext and optionally authenticate sender. Raise BadKey if authentication operation reveals sender key distinct from current verification key of owner of input DID. Raise WalletState if wallet is closed. :param ciphertext: ciphertext, as bytes :param sender: DID or verification key of sender, None for anonymously encrypted ciphertext :return: decrypted bytes """ LOGGER.debug('BaseAnchor.decrypt >>> ciphertext: %s, sender: %s', ciphertext, sender) if not self.wallet.handle: LOGGER.debug('BaseAnchor.decrypt <!< Wallet %s is closed', self.wallet.name) raise WalletState('Wallet {} is closed'.format(self.wallet.name)) verkey = None if sender: verkey = await self._verkey_for(sender) rv = await self.wallet.decrypt(ciphertext, verkey) LOGGER.debug('BaseAnchor.decrypt <<< %s', rv) return rv
async def get_nym(self, target_did: str = None) -> str: """ Get json cryptonym (including current verification key) for input (anchor) DID from ledger. Return empty production {} if the ledger has no such cryptonym. Raise BadLedgerTxn on failure. Raise WalletState if target DID is default (own DID) value but wallet does not have it (neither created nor opened since initialization). :param target_did: DID of cryptonym to fetch (default own DID) :return: cryptonym json """ LOGGER.debug('BaseAnchor.get_nym >>> target_did: %s', target_did) if target_did and not ok_did(target_did): LOGGER.debug('BaseAnchor.get_nym <!< Bad DID %s', target_did) raise BadIdentifier('Bad DID {}'.format(target_did)) if not (target_did or self.did): LOGGER.debug('BaseAnchor.get_nym <!< Bad wallet state: DID for %s unavailable', self.name) raise WalletState('Bad wallet state: DID for {} unavailable'.format(self.name)) rv = json.dumps({}) get_nym_req = await ledger.build_get_nym_request(self.did, target_did or self.did) resp_json = await self._submit(get_nym_req) data_json = (json.loads(resp_json))['result']['data'] # it's double-encoded on the ledger if data_json: rv = data_json LOGGER.debug('BaseAnchor.get_nym <<< %s', rv) return rv
async def remove(self) -> bool: """ Remove serialized wallet, best effort, if it exists. Return whether wallet absent after operation (removal successful or else not present a priori). Raise WalletState if wallet is open. :return: whether wallet gone from persistent storage """ LOGGER.debug('Wallet.remove >>>') if self.handle: LOGGER.debug('Wallet.remove <!< Wallet %s is open', self.name) raise WalletState('Wallet {} is open'.format(self.name)) rv = True try: LOGGER.info('Attempting to remove wallet: %s', self.name) await wallet.delete_wallet( json.dumps(self.config), json.dumps(self.access_creds)) except IndyError as x_indy: if x_indy.error_code == ErrorCode.WalletNotFoundError: LOGGER.info('Wallet %s not present; abstaining from removal', self.name) else: LOGGER.info('Failed wallet %s removal; indy-sdk error code %s', self.name, x_indy.error_code) rv = False LOGGER.debug('Wallet.remove <<< %s', rv) return rv
async def encrypt(self, message: bytes, authn: bool = False, verkey: str = None) -> bytes: """ Encrypt plaintext for owner of DID, anonymously or via authenticated encryption scheme. Raise AbsentMessage for missing message, or WalletState if wallet is closed. :param message: plaintext, as bytes :param authn: whether to use authenticated encryption scheme :param verkey: verification key of recipient, None for anchor's own :return: ciphertext, as bytes """ LOGGER.debug('Wallet.encrypt >>> message: %s, authn: %s, verkey: %s', message, authn, verkey) if not message: LOGGER.debug('Wallet.encrypt <!< No message to encrypt') raise AbsentMessage('No message to encrypt') if not self.handle: LOGGER.debug('Wallet.encrypt <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) if authn: rv = await crypto.auth_crypt(self.handle, self.verkey, verkey or self.verkey, message) else: rv = await crypto.anon_crypt(verkey or self.verkey, message) LOGGER.debug('Wallet.auth_encrypt <<< %s', rv) return rv
async def get_pairwise(self, pairwise_filt: str = None) -> dict: """ Return dict mapping each pairwise DID of interest in wallet to its pairwise info, or, for no filter specified, mapping them all. If wallet has no such item, return empty dict. :param pairwise_filt: remote DID of interest, or WQL json (default all) :return: dict mapping remote DIDs to PairwiseInfo """ LOGGER.debug('Wallet.get_pairwise >>> pairwise_filt: %s', pairwise_filt) if not self.handle: LOGGER.debug('Wallet.get_pairwise <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) non_secs = await self.get_non_secret( TYPE_PAIRWISE, pairwise_filt if ok_did(pairwise_filt) or not pairwise_filt else json.loads(pairwise_filt), canon_pairwise_wql) rv = {k: non_secret2pairwise_info(non_secs[k]) for k in non_secs} # touch up tags, mute leading ~ LOGGER.debug('Wallet.get_pairwise <<< %s', rv) return rv
async def create_signing_key(self, seed: str = None, metadata: dict = None) -> KeyInfo: """ Create a new signing key pair. Raise WalletState if wallet is closed, ExtantRecord if verification key already exists. :param seed: optional seed allowing deterministic key creation :param metadata: optional metadata to store with key pair :return: KeyInfo for new key pair """ LOGGER.debug('Wallet.create_signing_key >>> seed: [SEED], metadata: %s', metadata) if not self.handle: LOGGER.debug('Wallet.create_signing_key <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) try: verkey = await crypto.create_key(self.handle, json.dumps({'seed': seed} if seed else {})) except IndyError as x_indy: if x_indy.error_code == ErrorCode.WalletItemAlreadyExists: LOGGER.debug('Wallet.create_signing_key <!< Verification key already present in wallet %s', self.name) raise ExtantRecord('Verification key already present in wallet {}'.format(self.name)) LOGGER.debug('Wallet.create_signing_key <!< indy-sdk raised error %s', x_indy.error_code) raise await crypto.set_key_metadata(self.handle, verkey, json.dumps(metadata or {})) # coerce None to empty rv = KeyInfo(verkey, metadata or {}) LOGGER.debug('Wallet.create_signing_key <<< %s', rv) return rv
async def get_local_did_info(self, loc: str) -> DIDInfo: """ Get local DID info by local DID or verification key. Raise AbsentRecord for no such local DID. :param loc: DID or verification key of interest :return: DIDInfo for local DID """ LOGGER.debug('Wallet.get_local_did_info >>> loc: %s', loc) if not self.handle: LOGGER.debug('Wallet.get_local_did_info <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) if ok_did(loc): # it's a DID try: did_with_meta = json.loads(await did.get_my_did_with_meta( self.handle, loc)) rv = DIDInfo( did_with_meta['did'], did_with_meta['verkey'], json.loads(did_with_meta['metadata']) if did_with_meta['metadata'] else {}) # nudge None to empty except IndyError as x_indy: if x_indy.error_code == ErrorCode.WalletItemNotFound: LOGGER.debug( 'Wallet.get_local_did_info <!< DID %s not present in wallet %s', loc, self.name) raise AbsentRecord( 'Local DID {} not present in wallet {}'.format( loc, self.name)) else: LOGGER.debug( 'Wallet.get_local_did_info <!< indy-sdk raised error %s', x_indy.error_code) raise else: # it's a verkey dids_with_meta = json.loads(await did.list_my_dids_with_meta(self.handle )) # list for did_with_meta in dids_with_meta: if did_with_meta['verkey'] == loc: rv = DIDInfo( did_with_meta['did'], did_with_meta['verkey'], json.loads(did_with_meta['metadata']) if did_with_meta['metadata'] else {}) break else: LOGGER.debug( 'Wallet.get_local_did_info <!< Wallet %s has no local DID for verkey %s', self.name, loc) raise AbsentRecord( 'Wallet {} has no local DID for verkey {}'.format( self.name, loc)) LOGGER.debug('Wallet.get_local_did_info <<< %s', rv) return rv
async def revoke_cred(self, rr_id: str, cr_id) -> int: """ Revoke credential that input revocation registry identifier and credential revocation identifier specify. Return (epoch seconds) time of revocation. Raise AbsentTails if no tails file is available for input revocation registry identifier. Raise WalletState for closed wallet. Raise BadRevocation if issuer cannot revoke specified credential for any other reason (e.g., did not issue it, already revoked it). :param rr_id: revocation registry identifier :param cr_id: credential revocation identifier :return: time of revocation, in epoch seconds """ LOGGER.debug('Issuer.revoke_cred >>> rr_id: %s, cr_id: %s', rr_id, cr_id) if not self.wallet.handle: LOGGER.debug('Issuer.revoke_cred <!< Wallet %s is closed', self.wallet.name) raise WalletState('Wallet {} is closed'.format(self.wallet.name)) if not ok_rev_reg_id(rr_id): LOGGER.debug('Issuer.revoke_cred <!< Bad rev reg id %s', rr_id) raise BadIdentifier('Bad rev reg id {}'.format(rr_id)) tails_reader_handle = (await Tails( self._dir_tails, *rev_reg_id2cred_def_id_tag(rr_id)).open()).reader_handle try: rrdelta_json = await anoncreds.issuer_revoke_credential( self.wallet.handle, tails_reader_handle, rr_id, cr_id) except IndyError as x_indy: LOGGER.debug( 'Issuer.revoke_cred <!< Could not revoke revoc reg id %s, cred rev id %s: indy error code %s', rr_id, cr_id, x_indy.error_code) raise BadRevocation( 'Could not revoke revoc reg id {}, cred rev id {}: indy error code {}'.format( rr_id, cr_id, x_indy.error_code)) rr_ent_req_json = await ledger.build_revoc_reg_entry_request(self.did, rr_id, 'CL_ACCUM', rrdelta_json) resp_json = await self._sign_submit(rr_ent_req_json) # raises AbsentPool or ClosedPool if applicable resp = json.loads(resp_json) rv = self.pool.protocol.txn2epoch(resp) LOGGER.debug('Issuer.revoke_cred <<< %s', rv) return rv
async def _sign_submit(self, req_json: str) -> str: """ Sign and submit (json) request to ledger; return (json) result. Raise: * AbsentPool for no pool * ClosedPool if pool is not yet open * CorruptWallet if existing wallet appears not to pertain to the anchor's pool * WalletState if wallet is closed * BadLedgerTxn on ledger rejection of transaction. :param req_json: json of request to sign and submit :return: json response """ LOGGER.debug('BaseAnchor._sign_submit >>> req_json: %s', req_json) if not self.pool: LOGGER.debug('BaseAnchor._sign_submit <!< absent pool') raise AbsentPool('Cannot sign and submit request: absent pool') if not self.pool.handle: LOGGER.debug('BaseAnchor._submit <!< closed pool %s', self.pool.name) raise ClosedPool('Cannot sign and submit request to closed pool {}'.format(self.pool.name)) if not self.wallet.handle: LOGGER.debug('BaseAnchor._sign_submit <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) try: rv_json = await ledger.sign_and_submit_request(self.pool.handle, self.wallet.handle, self.did, req_json) await asyncio.sleep(0) except IndyError as x_indy: if x_indy.error_code == ErrorCode.WalletIncompatiblePoolError: LOGGER.debug( 'BaseAnchor._sign_submit <!< Corrupt wallet %s is not compatible with pool %s', self.name, self.pool.name) raise CorruptWallet('Corrupt wallet {} is not compatible with pool {}'.format( self.name, self.pool.name)) LOGGER.debug( 'BaseAnchor._sign_submit <!< cannot sign/submit request for ledger: indy error code %s', x_indy.error_code) raise BadLedgerTxn('Cannot sign/submit request for ledger: indy error code {}'.format( x_indy.error_code)) resp = json.loads(rv_json) if resp.get('op', '') in ('REQNACK', 'REJECT'): LOGGER.debug('BaseAnchor._sign_submit: ledger rejected request: %s', resp['reason']) raise BadLedgerTxn('Ledger rejected transaction request: {}'.format(resp['reason'])) LOGGER.debug('BaseAnchor._sign_submit <<< %s', rv_json) return rv_json
async def create_local_did(self, seed: str = None, loc_did: str = None, metadata: dict = None) -> DIDInfo: """ Create and store a new local DID for use in pairwise DID relations. :param seed: seed from which to create (default random) :param loc_did: local DID value (default None to let indy-sdk generate) :param metadata: metadata to associate with the local DID (operation always sets 'since' epoch timestamp) :return: DIDInfo for new local DID """ LOGGER.debug( 'Wallet.create_local_did >>> seed: [SEED] loc_did: %s metadata: %s', loc_did, metadata) cfg = {} if seed: cfg['seed'] = seed if loc_did: cfg['did'] = loc_did if not self.handle: LOGGER.debug('Wallet.create_local_did <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) try: (created_did, verkey) = await did.create_and_store_my_did( self.handle, json.dumps(cfg)) except IndyError as x_indy: if x_indy.error_code == ErrorCode.DidAlreadyExistsError: LOGGER.debug( 'Wallet.create_local_did <!< DID %s already present in wallet %s', loc_did, self.name) raise ExtantRecord( 'Local DID {} already present in wallet {}'.format( loc_did, self.name)) else: LOGGER.debug( 'Wallet.create_local_did <!< indy-sdk raised error %s', x_indy.error_code) raise loc_did_metadata = {**(metadata or {}), 'since': int(time())} await did.set_did_metadata(self.handle, created_did, json.dumps(loc_did_metadata)) rv = DIDInfo(created_did, verkey, loc_did_metadata) LOGGER.debug('Wallet.create_local_did <<< %s', rv) return rv
async def decrypt( self, ciphertext: bytes, authn_check: bool = None, to_verkey: str = None, from_verkey: str = None) -> (bytes, str): """ Decrypt ciphertext and optionally authenticate sender. Raise BadKey if authentication operation checks and reveals sender key distinct from input sender verification key. Raise AbsentMessage for missing ciphertext, or WalletState if wallet is closed. :param ciphertext: ciphertext, as bytes :param authn_check: True to authenticate and check sender verification key, False to authenticate and return sender verification key for client to decide fitness, or None to use anonymous decryption :param to_verkey: recipient verification key, default anchor's own :param from_verkey: sender verification key, ignored for anonymous decryption, default anchor's own for authenticated decryption :return: decrypted bytes and sender verification key (None for anonymous decryption) """ LOGGER.debug( 'Wallet.decrypt >>> ciphertext: %s, authn_check: %s, to_verkey: %s, from_verkey: %s', ciphertext, authn_check, to_verkey, from_verkey) if not ciphertext: LOGGER.debug('Wallet.decrypt <!< No ciphertext to decrypt') raise AbsentMessage('No ciphertext to decrypt') if not self.handle: LOGGER.debug('Wallet.decrypt <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) sender_verkey = None if authn_check is None: plaintext = await crypto.anon_decrypt(self.handle, to_verkey or self.verkey, ciphertext) else: (sender_verkey, plaintext) = await crypto.auth_decrypt(self.handle, to_verkey or self.verkey, ciphertext) if authn_check and sender_verkey != (from_verkey or self.verkey): LOGGER.debug('Wallet.decrypt <!< Authentication revealed unexpected sender key on decryption') raise BadKey('Authentication revealed unexpected sender key on decryption') rv = (plaintext, sender_verkey) LOGGER.debug('Wallet.decrypt <<< %s', rv) return rv
async def open(self) -> 'Wallet': """ Explicit entry. Open wallet as configured, for later closure via close(). For use when keeping wallet open across multiple calls. Raise any IndyError causing failure to open wallet, WalletState if wallet already open, or AbsentWallet on attempt to enter wallet not yet created. :return: current object """ LOGGER.debug('Wallet.open >>>') created = False while True: try: self._handle = await wallet.open_wallet( json.dumps(self.config), json.dumps(self.access_creds)) LOGGER.info('Opened wallet %s on handle %s', self.name, self.handle) break except IndyError as x_indy: if x_indy.error_code == ErrorCode.WalletNotFoundError: if created: LOGGER.debug('Wallet.open() <!< Wallet %s not found after creation', self.name) raise AbsentWallet('Wallet {} not found after creation'.format(self.name)) if self.auto_create: await self.create() continue else: LOGGER.debug('Wallet.open() <!< Wallet %s not found', self.name) raise AbsentWallet('Wallet {} not found'.format(self.name)) elif x_indy.error_code == ErrorCode.WalletAlreadyOpenedError: LOGGER.debug('Wallet.open() <!< Wallet %s is already open', self.name) raise WalletState('Wallet {} is already open'.format(self.name)) elif x_indy.error_code == ErrorCode.WalletAccessFailed: LOGGER.debug('Wallet.open() <!< Bad access credentials value for wallet %s', self.name) raise BadAccess('Bad access credentials value for wallet {}'.format(self.name)) LOGGER.debug('Wallet %s open raised indy error %s', self.name, x_indy.error_code) raise self.did = await self.get_public_did() self.verkey = await did.key_for_local_did(self.handle, self.did) if self.did else None LOGGER.info('Wallet %s got verkey %s for existing DID %s', self.name, self.verkey, self.did) LOGGER.debug('Wallet.open <<<') return self
async def create_cred_offer(self, schema_seq_no: int) -> str: """ Create credential offer as Issuer for given schema. Raise CorruptWallet if the wallet has no private key for the corresponding credential definition. Raise WalletState for closed wallet. :param schema_seq_no: schema sequence number :return: credential offer json for use in storing credentials at HolderProver. """ LOGGER.debug('Issuer.create_cred_offer >>> schema_seq_no: %s', schema_seq_no) if not self.wallet.handle: LOGGER.debug('Issuer.create_cred_offer <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) if not self.pool: LOGGER.debug('Issuer.create_cred_offer <!< issuer %s has no pool', self.name) raise AbsentPool( 'Issuer {} has no pool: cannot create cred offer'.format( self.name)) rv = None cd_id = cred_def_id(self.did, schema_seq_no, self.pool.protocol) try: rv = await anoncreds.issuer_create_credential_offer( self.wallet.handle, cd_id) except IndyError as x_indy: if x_indy.error_code == ErrorCode.WalletItemNotFound: LOGGER.debug( 'Issuer.create_cred_offer <!< did not issue cred definition from wallet %s', self.name) raise CorruptWallet( 'Cannot create cred offer: did not issue cred definition from wallet {}' .format(self.name)) LOGGER.debug( 'Issuer.create_cred_offer <!< cannot create cred offer, indy error code %s', x_indy.error_code) raise LOGGER.debug('Issuer.create_cred_offer <<< %s', rv) return rv
async def reseed_init(self, next_seed: str = None) -> str: """ Begin reseed operation: generate new key. Raise WalletState if wallet is closed. :param next_seed: incoming replacement seed (default random) :return: new verification key """ LOGGER.debug('Wallet.reseed_init >>> next_seed: [SEED]') if not self.handle: LOGGER.debug('Wallet.reseed_init <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) rv = await did.replace_keys_start(self.handle, self.did, json.dumps({'seed': next_seed} if next_seed else {})) LOGGER.debug('Wallet.reseed_init <<< %s', rv) return rv
async def sign(self, message: bytes) -> bytes: """ Sign message; return signature. Raise WalletState if wallet is closed. :param message: Content to sign, as bytes :return: signature, as bytes """ LOGGER.debug('BaseAnchor.sign >>> message: %s', message) if not self.wallet.handle: LOGGER.debug('BaseAnchor.sign <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) rv = await self.wallet.sign(message) LOGGER.debug('BaseAnchor.sign <<< %s', rv) return rv
async def reset(self, von_wallet: Wallet, seed: str = None) -> Wallet: """ Close and delete (open) VON anchor wallet and then create, open, and return replacement on current link secret. Note that this operation effectively destroys private keys for keyed data structures such as credential offers or credential definitions. Raise WalletState if the wallet is closed. :param von_wallet: open wallet :param seed: seed to use for new wallet (default random) :return: replacement wallet """ LOGGER.debug('WalletManager.reset >>> von_wallet %s', von_wallet) if not von_wallet.handle: LOGGER.debug('WalletManager.reset <!< Wallet %s is closed', von_wallet.name) raise WalletState('Wallet {} is closed'.format(von_wallet.name)) w_config = von_wallet.config # wallet under reset, no need to make copy w_config['did'] = von_wallet.did w_config['seed'] = seed w_config[ 'auto_create'] = von_wallet.auto_create # in case both auto_remove+auto_create set (create every open) w_config['auto_remove'] = von_wallet.auto_remove label = await von_wallet.get_link_secret_label() if label: w_config['link_secret_label'] = label await von_wallet.close() if not von_wallet.auto_remove: await self.remove(von_wallet) rv = await self.create(w_config, von_wallet.access) await rv.open() LOGGER.debug('WalletManager.reset <<< %s', rv) return rv
async def get_link_secret_label(self) -> str: """ Get current link secret label from non-secret storage records; return None for no match. :return: latest non-secret storage record for link secret label """ LOGGER.debug('Wallet.get_link_secret_label >>>') if not self.handle: LOGGER.debug('Wallet.get_link_secret <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) rv = None records = await self.get_non_secret(TYPE_LINK_SECRET_LABEL) if records: rv = records[str(max(int(k) for k in records))].value # str to int, max, and back again LOGGER.debug('Wallet.get_link_secret_label <<< %s', rv) return rv
async def decrypt(self, ciphertext: bytes, verkey: str = None) -> bytes: """ Decrypt ciphertext and optionally authenticate sender. Raise BadKey if authentication operation reveals sender key distinct from input verification key. Raise AbsentMessage for missing ciphertext, or WalletState if wallet is closed. :param ciphertext: ciphertext, as bytes :param verkey: sender's verification, or None for anonymously encrypted ciphertext :return: decrypted bytes """ LOGGER.debug('Wallet.decrypt >>> ciphertext: %s, verkey: %s', ciphertext, verkey) if not ciphertext: LOGGER.debug('Wallet.decrypt <!< No ciphertext to decrypt') raise AbsentMessage('No ciphertext to decrypt') if not self.handle: LOGGER.debug('Wallet.decrypt <!< Wallet %s is closed', self.name) raise WalletState('Wallet {} is closed'.format(self.name)) if verkey: (sender_verkey, rv) = await crypto.auth_decrypt(self.handle, self.verkey, ciphertext) if sender_verkey != verkey: LOGGER.debug( 'Wallet.decrypt <!< Authentication revealed unexpected sender key on decryption' ) raise BadKey( 'Authentication revealed unexpected sender key on decryption' ) else: rv = await crypto.anon_decrypt(self.handle, self.verkey, ciphertext) LOGGER.debug('Wallet.decrypt <<< %s', rv) return rv