From 925484ca9eb404e12c893e10d8ea8ecb54432a99 Mon Sep 17 00:00:00 2001 From: Ayrris Aunario Date: Thu, 27 Jun 2024 17:28:54 -0500 Subject: [PATCH 1/6] KeePass export: add support for Pleasant Password's TOTP-data schema (KC-786) --- keepercommander/importer/keepass/keepass.py | 7 ++++ keepercommander/utils.py | 40 ++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/keepercommander/importer/keepass/keepass.py b/keepercommander/importer/keepass/keepass.py index 652f88c92..a1040281f 100644 --- a/keepercommander/importer/keepass/keepass.py +++ b/keepercommander/importer/keepass/keepass.py @@ -28,6 +28,7 @@ Record, Folder, SharedFolder, BytesAttachment, RecordField from ... import utils from ...display import bcolors +from ...utils import parse_totp_uri _REFERENCE = r'\{REF:([TUPAN])@([IT]):([^\}]+)\}' @@ -326,6 +327,12 @@ def do_export(self, filename, records, file_password=None, **kwargs): for cf in r.fields: if cf.type == 'oneTimeCode': entry.otp = cf.value + # Set custom fields for Pleasant Password TOTP compatibility + totp_props = parse_totp_uri(cf.value) + for key in ['secret', 'period', 'issuer', 'digits']: + val = totp_props.get(key) + val and entry.set_custom_property(f'TOTP{key.capitalize()}', str(val)) + continue if cf.type and cf.label: title = f'${cf.type}:{cf.label}' diff --git a/keepercommander/utils.py b/keepercommander/utils.py index 5d70a8918..1278dc76c 100644 --- a/keepercommander/utils.py +++ b/keepercommander/utils.py @@ -14,7 +14,7 @@ import math import re import time -from urllib.parse import urlparse +from urllib.parse import urlparse, parse_qs, unquote from . import crypto from .constants import EMAIL_PATTERN @@ -320,3 +320,41 @@ def size_to_str(size): # type: (int) -> str return f'{size:.2f} Mb' size = size / 1024 return f'{size:,.2f} Gb' + +def parse_totp_uri(uri): # type: (str) -> Dict[str, Union[str, int, None]] + def parse_int(val): + return val and int(val) + + def decode_uri_component(component): # type: (str) -> str + return unquote(component or '').strip() + + result = dict() + + if not uri: + return result + + parsed = urlparse(uri) + if parsed.scheme == 'otpauth': + label = re.sub(r'^/+', '', parsed.path or '') + parts = re.split(r':|%3A', label) + parts = [part for part in parts if part] + account_name = len(parts) and parts.pop() + issuer = len(parts) and parts.pop() + + parsed = parse_qs(parsed.query) + + issuers = parsed.get('issuer') + secrets = parsed.get('secret') + algorithms = parsed.get('algorithm') + digits_vals = parsed.get('digits') + periods = parsed.get('period') + result = { + 'issuer': decode_uri_component(issuers and next(iter(issuers)) or issuer), + 'account': decode_uri_component(account_name), + 'secret': secrets and next(iter(secrets)), + 'algorithm': algorithms and next(iter(algorithms)) or 'SHA1', + 'digits': parse_int(digits_vals and next(iter(digits_vals))) or 6, + 'period': parse_int(periods and next(iter(periods))) or 30 + } + + return result From c6b274ed7c097e27d4c522836a88ffb4df0781a1 Mon Sep 17 00:00:00 2001 From: Ayrris Aunario Date: Fri, 28 Jun 2024 18:54:42 -0500 Subject: [PATCH 2/6] SSO Login: fix app hanging when browser proxy/manager fails to launch due to missing requirements (KC-787) --- keepercommander/auth/console_ui.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/keepercommander/auth/console_ui.py b/keepercommander/auth/console_ui.py index e10bdba2d..1b397019f 100644 --- a/keepercommander/auth/console_ui.py +++ b/keepercommander/auth/console_ui.py @@ -262,6 +262,11 @@ def on_password(self, step): def on_sso_redirect(self, step): try: wb = webbrowser.get() + wrappers = set('xdg-open|gvfs-open|gnome-open|x-www-browser|www-browser'.split('|')) + browsers = set(webbrowser._browsers if hasattr(webbrowser, '_browsers') else {}) + standalones = browsers - wrappers + if browsers and not standalones: # show browser-launch option only if effectively supported + wb = None except: wb = None From bf5aeaed160f77270cea81db6e8ee5e2651d8b50 Mon Sep 17 00:00:00 2001 From: Sergey Kolupaev Date: Mon, 1 Jul 2024 11:18:57 -0700 Subject: [PATCH 3/6] audit-alert add: active argument support --- keepercommander/commands/audit_alerts.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/keepercommander/commands/audit_alerts.py b/keepercommander/commands/audit_alerts.py index a0597fff6..27d97a6da 100644 --- a/keepercommander/commands/audit_alerts.py +++ b/keepercommander/commands/audit_alerts.py @@ -706,7 +706,6 @@ def execute(self, params, **kwargs): }, 'filter': {} } - self.apply_alert_options(params, alert, **kwargs) rq = { @@ -715,6 +714,20 @@ def execute(self, params, **kwargs): 'settings': alert, } api.communicate(params, rq) + + active = kwargs.get('active') + if isinstance(active, str): + if active == 'off': + rq = { + 'command': 'put_enterprise_setting', + 'type': 'AuditAlertContext', + 'settings': { + 'id': alert_id, + 'disabled': True + } + } + api.communicate(params, rq) + self.invalidate_alerts() command = AuditAlertView() command.execute(params, target=str(alert_id)) @@ -726,7 +739,6 @@ def get_parser(self): def execute(self, params, **kwargs): alert = AuditSettingMixin.get_alert_configuration(params, kwargs.get('target')) - self.apply_alert_options(params, alert, **kwargs) rq = { From 4807449146dc946067baeceb474c09faf1155e8f Mon Sep 17 00:00:00 2001 From: Ayrris Aunario Date: Wed, 3 Jul 2024 16:54:03 -0500 Subject: [PATCH 4/6] `security-audit-report`: Output detailed error report when security data parsing/updating fails; `record-update`: Fix for error thrown when editing simple record field value (KC-788) --- keepercommander/commands/record_edit.py | 6 +-- keepercommander/commands/security_audit.py | 59 ++++++++++++++++++---- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/keepercommander/commands/record_edit.py b/keepercommander/commands/record_edit.py index 55ba0abbb..a31803883 100644 --- a/keepercommander/commands/record_edit.py +++ b/keepercommander/commands/record_edit.py @@ -601,11 +601,11 @@ def assign_typed_fields(self, record, fields): else: if isinstance(value, dict) and isinstance(record_field.value[0], dict): record_field.value[0].update(value) + noneKeys = [k for k,v in record_field.value[0].items() if v is None] + for k in noneKeys: + del record_field.value[0][k] else: record_field.value[0] = value - noneKeys = [k for k,v in record_field.value[0].items() if v is None] - for k in noneKeys: - del record_field.value[0][k] else: if is_field: record_field.value.clear() diff --git a/keepercommander/commands/security_audit.py b/keepercommander/commands/security_audit.py index 48035e8de..63a15856f 100644 --- a/keepercommander/commands/security_audit.py +++ b/keepercommander/commands/security_audit.py @@ -1,6 +1,7 @@ import argparse import json import logging +from json import JSONDecodeError from typing import Dict, List, Optional, Any from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey @@ -173,6 +174,26 @@ def get_node_id(name_or_id): node = next(iter(matches)) if matches else {} return node.get('node_id') + def report_errors(emails): + title = 'Security Audit Report - Problems Found\nSecurity data could not be parsed for the following vaults:' + output_fmt = kwargs.get('format', 'table') + headers = ['vault_owner', 'error_message'] + if output_fmt == 'table': + headers = [field_to_title(x) for x in headers] + vault_errors = [] + for username, errors in vault_errors_lookup.items(): + vault_errors.append([username, errors]) + + # Place errors not associated w/ a specific vault at the top + vault_errors.sort(key=lambda error_row: error_row[0] != 'Enterprise') + return dump_report_data(vault_errors, headers, fmt=output_fmt, filename=kwargs.get('output'), title=title) + + vault_errors_lookup = dict() + def update_vault_errors(username, error): + errors = vault_errors_lookup.get(username) or [] + errors.append(error) + vault_errors_lookup[username] = errors + nodes = kwargs.get('node') or [] node_ids = [get_node_id(n) for n in nodes] node_ids = [n for n in node_ids if n] @@ -192,7 +213,12 @@ def get_node_id(name_or_id): to_page = security_report_data_rs.toPage complete = security_report_data_rs.complete from_page = to_page + 1 - rsa_key = self.get_enterprise_private_rsa_key(params, security_report_data_rs.enterprisePrivateKey) + try: + rsa_key = self.get_enterprise_private_rsa_key(params, security_report_data_rs.enterprisePrivateKey) + except: + update_vault_errors('Enterprise', 'Invalid enterprise private key') + continue + for sr in security_report_data_rs.securityReport: user_info = self.resolve_user_info(params, sr.enterpriseUserId) node_id = user_info.get('node_id', 0) @@ -221,13 +247,23 @@ def get_node_id(name_or_id): master_pw_strength = 1 if sr.encryptedReportData: - sri = crypto.decrypt_aes_v2(sr.encryptedReportData, tree_key) - data = json.loads(sri) + try: + sri = crypto.decrypt_aes_v2(sr.encryptedReportData, tree_key) + data = json.loads(sri) + except Exception as ex: + update_vault_errors(email, ex) + continue else: data = {dk: 0 for dk in self.score_data_keys} if show_updated: - data = self.get_updated_security_report_row(sr, rsa_key, data) + try: + data = self.get_updated_security_report_row(sr, rsa_key, data) + except Exception as e: + reason = f"Invalid JSON: {e.doc}" if isinstance(e, JSONDecodeError) else e + update_vault_errors(email, reason) + continue + if save_report: updated_sr = APIRequest_pb2.SecurityReport() @@ -266,6 +302,9 @@ def get_node_id(name_or_id): rows.append(row) + if vault_errors_lookup.keys(): + return report_errors(vault_errors_lookup) + if save_report: self.save_updated_security_reports(params, updated_security_reports) @@ -292,15 +331,13 @@ def get_updated_security_report_row(self, sr, rsa_key, last_saved_data): # type: (APIRequest_pb2.SecurityReport, RSAPrivateKey, Dict[str, int]) -> Dict[str, int] def apply_incremental_data(old_report_data, incremental_dataset, key): # type: (Dict[str, int], List[APIRequest_pb2.SecurityReportIncrementalData], RSAPrivateKey) -> Dict[str, int] + def decrypt_security_data(sec_data, k): # type: (bytes, RSAPrivateKey) -> Dict[str, int] or None + decrypted = None if sec_data: - decrypted = None - try: - decrypted = crypto.decrypt_rsa(sec_data, k) - finally: - return json.loads(decrypted.decode()) if decrypted else None - else: - return None + decrypted = crypto.decrypt_rsa(sec_data, k) + decrypted = json.loads(decrypted.decode()) + return decrypted def decrypt_incremental_data(inc_data): # type: (APIRequest_pb2.SecurityReportIncrementalData) -> Dict[str, Dict[str, int] or None] From 1da3bf1dcefdc6992ff8cccf924b2aac2fdd1c4a Mon Sep 17 00:00:00 2001 From: Ayrris Aunario Date: Wed, 3 Jul 2024 17:24:55 -0500 Subject: [PATCH 5/6] clean-up for KC-788 (`security-audit-report`): remove unused variable --- keepercommander/commands/security_audit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keepercommander/commands/security_audit.py b/keepercommander/commands/security_audit.py index 63a15856f..2b1da0390 100644 --- a/keepercommander/commands/security_audit.py +++ b/keepercommander/commands/security_audit.py @@ -174,7 +174,7 @@ def get_node_id(name_or_id): node = next(iter(matches)) if matches else {} return node.get('node_id') - def report_errors(emails): + def report_errors(): title = 'Security Audit Report - Problems Found\nSecurity data could not be parsed for the following vaults:' output_fmt = kwargs.get('format', 'table') headers = ['vault_owner', 'error_message'] @@ -303,7 +303,7 @@ def update_vault_errors(username, error): rows.append(row) if vault_errors_lookup.keys(): - return report_errors(vault_errors_lookup) + return report_errors() if save_report: self.save_updated_security_reports(params, updated_security_reports) From e84a5771d46f06717afe11fa92ac5c57cfa771fc Mon Sep 17 00:00:00 2001 From: Ayrris Aunario Date: Wed, 3 Jul 2024 17:43:39 -0500 Subject: [PATCH 6/6] release version update --- keepercommander/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keepercommander/__init__.py b/keepercommander/__init__.py index ceb298931..2a05c3711 100644 --- a/keepercommander/__init__.py +++ b/keepercommander/__init__.py @@ -10,4 +10,4 @@ # Contact: ops@keepersecurity.com # -__version__ = '16.11.2' +__version__ = '16.11.3'