Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement view-key management through CLI. #290

Merged
merged 1 commit into from
May 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 131 additions & 31 deletions cli/mobilecoin/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ def _create_parsers(self):
self.export_args.add_argument('account_id', help='ID of the account to export.')
self.export_args.add_argument('-s', '--show', action='store_true',
help='Only show the secret entropy mnemonic, do not write it to file.')
self.export_args.add_argument('-V', '--view', action='store_true',
help='Show the view-private-key only.')

# Remove account.
self.remove_args = command_sp.add_parser('remove', help='Remove an account from local storage.')
Expand Down Expand Up @@ -168,10 +170,14 @@ def _create_parsers(self):

def _load_account_prefix(self, prefix):
accounts = self.client.get_all_accounts()
view_accounts = self.client.get_all_view_only_accounts()
accounts.update(view_accounts)

matching_ids = [
a_id for a_id in accounts.keys()
if a_id.startswith(prefix)
]

if len(matching_ids) == 0:
print('Could not find account starting with', prefix)
exit(1)
Expand Down Expand Up @@ -269,10 +275,11 @@ def status(self):
))
print('Network fee is {}'.format(_format_mob(fee)))

def list(self, **args):
accounts = self.client.get_all_accounts(**args)
def list(self):
accounts = self.client.get_all_accounts()
view_accounts = self.client.get_all_view_only_accounts()

if len(accounts) == 0:
if len(accounts) + len(view_accounts) == 0:
print('No accounts.')
return

Expand All @@ -281,7 +288,12 @@ def list(self, **args):
balance = self.client.get_balance_for_account(account_id)
account_list.append((account_id, account, balance))

for (account_id, account, balance) in account_list:
view_account_list = []
for account_id, view_account in view_accounts.items():
balance = self.client.get_balance_for_view_only_account(account_id)
view_account_list.append((account_id, view_account, balance))

for (account_id, account, balance) in account_list + view_account_list:
print()
_print_account(account, balance)

Expand Down Expand Up @@ -320,6 +332,8 @@ def import_(self, backup, name=None, block=None, key_derivation_version=2):
account = self.client.import_account(**data)
elif 'legacy_root_entropy' in data:
account = self.client.import_account_from_legacy_root_entropy(**data)
elif 'view_private_key' in data:
account = self.client.import_view_only_account(**data)
else:
raise ValueError('Could not import account from {}'.format(backup))

Expand All @@ -328,7 +342,11 @@ def import_(self, backup, name=None, block=None, key_derivation_version=2):
_print_account(account)
print()

def export(self, account_id, show=False):
def export(self, account_id, show=False, view=False):
if view:
self._export_view_key(account_id, show)
return

account = self._load_account_prefix(account_id)
account_id = account['account_id']
balance = self.client.get_balance_for_account(account_id)
Expand Down Expand Up @@ -369,22 +387,76 @@ def export(self, account_id, show=False):
else:
print(f'Wrote {filename}.')

def remove(self, account_id):
def _export_view_key(self, account_id, show):
account = self._load_account_prefix(account_id)
account_id = account['account_id']
balance = self.client.get_balance_for_account(account_id)

print('You are about to remove this account:')
print('You are about to export the private view key for this account:')
print()
_print_account(account, balance)

print()
print('You will lose access to the funds in this account unless you')
print('restore it from the mnemonic phrase.')
if show:
print('The private view key will display on your screen.')
print('Make sure your screen is not being viewed or recorded.')
else:
print('Keep the view key file safe and private!')
print('Anyone who has access to the view key can see all transactions for the account.')

if show:
confirm_message = 'Really show account view key? (Y/N) '
else:
confirm_message = 'Really write private view key to a file? (Y/N) '

if not self.confirm(confirm_message):
print('Cancelled.')
return

secrets = self.client.export_account_secrets(account_id)
if show:
print()
print(secrets['account_key']['view_private_key'])
print()
else:
filename = 'mobilecoin_view_key_{}.json'.format(account_id[:16])
try:
_save_view_key_export(account, secrets, filename)
except OSError as e:
print('Could not write file: {}'.format(e))
exit(1)
else:
print(f'Wrote {filename}.')

def remove(self, account_id):
account = self._load_account_prefix(account_id)
account_id = account['account_id']

if account['object'] == 'view_only_account':
balance = self.client.get_balance_for_view_only_account(account_id)
print('You are about to remove this view key:')
print()
_print_account(account, balance)
print()
print('You will lose the ability to see related transactions unless you')
print('restore it from backup.')
else:
balance = self.client.get_balance_for_account(account_id)
print('You are about to remove this account:')
print()
_print_account(account, balance)
print()
print('You will lose access to the funds in this account unless you')
print('restore it from the mnemonic phrase.')

if not self.confirm('Continue? (Y/N) '):
print('Cancelled.')
return

self.client.remove_account(account_id)
if account['object'] == 'view_only_account':
self.client.remove_view_only_account(account_id)
else:
self.client.remove_account(account_id)
print('Removed.')

def history(self, account_id):
Expand Down Expand Up @@ -735,7 +807,12 @@ def _format_decimal(d):


def _format_account_header(account):
return '{} {}'.format(account['account_id'][:6], account['name'])
output = account['account_id'][:6]
if account['name']:
output += ' ' + account['name']
if account.get('object') == 'view_only_account':
output += ' [view-only]'
return output


def _format_balance(balance):
Expand All @@ -745,11 +822,11 @@ def _format_balance(balance):
offline = True
network_block = int(balance['local_block_height'])

orphaned = pmob2mob(balance['orphaned_pmob'])
if orphaned > 0:
orphaned_status = ', {} orphaned'.format(_format_mob(orphaned))
else:
orphaned_status = ''
orphaned_status = ''
if 'orphaned_pmob' in balance:
orphaned = pmob2mob(balance['orphaned_pmob'])
if orphaned > 0:
orphaned_status = ', {} orphaned'.format(_format_mob(orphaned))

account_block = int(balance['account_block_height'])
if account_block == network_block:
Expand All @@ -762,8 +839,13 @@ def _format_balance(balance):
else:
offline_status = ''

if 'unspent_pmob' in balance:
amount = balance['unspent_pmob']
elif 'balance' in balance:
amount = balance['balance']

result = '{}{} ({}){}'.format(
_format_mob(pmob2mob(balance['unspent_pmob'])),
_format_mob(pmob2mob(amount)),
orphaned_status,
sync_status,
offline_status,
Expand All @@ -781,10 +863,11 @@ def _format_gift_code_status(status):

def _print_account(account, balance=None):
print(_format_account_header(account))
print(indent(
'address {}'.format(account['main_address']),
' '*2,
))
if 'main_address' in account:
print(indent(
'address {}'.format(account['main_address']),
' '*2,
))
if balance is not None:
print(indent(
_format_balance(balance),
Expand Down Expand Up @@ -856,20 +939,22 @@ def _load_import_file(filename):
'name',
'first_block_index',
'next_subaddress_index',
'view_private_key',
]:
value = data.get(field)
if value is not None:
result[field] = value

result['fog_keys'] = {}
for field in [
'fog_report_url',
'fog_report_id',
'fog_authority_spki',
]:
value = data['account_key'].get(field)
if value is not None:
result['fog_keys'][field] = value
if 'account_key' in data:
result['fog_keys'] = {}
for field in [
'fog_report_url',
'fog_report_id',
'fog_authority_spki',
]:
value = data['account_key'].get(field)
if value is not None:
result['fog_keys'][field] = value

return result

Expand All @@ -893,9 +978,24 @@ def _save_export(account, secrets, filename):
'next_subaddress_index': account['next_subaddress_index'],
})

_save_json_file(filename, export_data)


def _save_view_key_export(account, secrets, filename):
_save_json_file(
filename,
{
'account_name': account['name'],
'view_private_key': secrets['account_key']['view_private_key'],
'first_block_index': account['first_block_index'],
}
)


def _save_json_file(filename, data):
path = Path(filename)
if path.exists():
raise OSError('File exists.')
with path.open('w') as f:
json.dump(export_data, f, indent=2)
json.dump(data, f, indent=2)
f.write('\n')
35 changes: 34 additions & 1 deletion cli/mobilecoin/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ def _req(self, request_data):

if self.verbose:
print(r.status, http.client.responses[r.status])
print(repr(raw_response))
print(len(raw_response), 'bytes')
print(json.dumps(response_data, indent=2))
print()
Expand Down Expand Up @@ -117,10 +116,29 @@ def import_account_from_legacy_root_entropy(self, legacy_root_entropy, name=None
})
return r['account']

def import_view_only_account(self, view_private_key, name=None, first_block_index=None):
params = {
"view_private_key": view_private_key,
}
if name is not None:
params['name'] = name
if first_block_index is not None:
params['first_block_index'] = str(int(first_block_index))

r = self._req({
"method": "import_view_only_account",
"params": params
})
return r['view_only_account']

def get_all_accounts(self):
r = self._req({"method": "get_all_accounts"})
return r['account_map']

def get_all_view_only_accounts(self):
r = self._req({"method": "get_all_view_only_accounts"})
return r['account_map']

def get_account(self, account_id):
r = self._req({
"method": "get_account",
Expand All @@ -144,6 +162,12 @@ def remove_account(self, account_id):
"params": {"account_id": account_id}
})

def remove_view_only_account(self, account_id):
return self._req({
"method": "remove_view_only_account",
"params": {"account_id": account_id}
})

def export_account_secrets(self, account_id):
r = self._req({
"method": "export_account_secrets",
Expand Down Expand Up @@ -186,6 +210,15 @@ def get_balance_for_account(self, account_id):
})
return r['balance']

def get_balance_for_view_only_account(self, account_id):
r = self._req({
"method": "get_balance_for_view_only_account",
"params": {
"account_id": account_id,
}
})
return r['balance']

def get_balance_for_address(self, address):
r = self._req({
"method": "get_balance_for_address",
Expand Down