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

Release #1264

Merged
merged 6 commits into from
Jul 3, 2024
Merged

Release #1264

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
2 changes: 1 addition & 1 deletion keepercommander/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
# Contact: ops@keepersecurity.com
#

__version__ = '16.11.2'
__version__ = '16.11.3'
5 changes: 5 additions & 0 deletions keepercommander/auth/console_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 14 additions & 2 deletions keepercommander/commands/audit_alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,6 @@ def execute(self, params, **kwargs):
},
'filter': {}
}

self.apply_alert_options(params, alert, **kwargs)

rq = {
Expand All @@ -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))
Expand All @@ -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 = {
Expand Down
6 changes: 3 additions & 3 deletions keepercommander/commands/record_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
59 changes: 48 additions & 11 deletions keepercommander/commands/security_audit.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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():
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]
Expand All @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -266,6 +302,9 @@ def get_node_id(name_or_id):

rows.append(row)

if vault_errors_lookup.keys():
return report_errors()

if save_report:
self.save_updated_security_reports(params, updated_security_reports)

Expand All @@ -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]
Expand Down
7 changes: 7 additions & 0 deletions keepercommander/importer/keepass/keepass.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]):([^\}]+)\}'

Expand Down Expand Up @@ -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}'
Expand Down
40 changes: 39 additions & 1 deletion keepercommander/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Loading