Skip to content
Closed
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
53 changes: 39 additions & 14 deletions keepercommander/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,8 +377,12 @@ def load_team_keys(params, team_uids): # type: (KeeperParams, List[str]
encrypted_team_key = t.get('encrypted_team_key')
if encrypted_team_key:
team_key = crypto.decrypt_aes_v2(utils.base64_url_decode(encrypted_team_key), tree_key)
params.key_cache[team_uid] = PublicKeys(aes=team_key)
s.remove(team_uid)
existing = params.key_cache.get(team_uid)
params.key_cache[team_uid] = PublicKeys(
aes=team_key,
rsa=getattr(existing, 'rsa', b'') or b'',
ec=getattr(existing, 'ec', b'') or b'')
# Still fetch asymmetric public keys via team_get_keys below.
except Exception as e:
logging.debug('Team UID \"%s\": Decrypt key error: %s', team_uid, str(e))

Expand All @@ -397,30 +401,51 @@ def load_team_keys(params, team_uids): # type: (KeeperParams, List[str]
}
rs = communicate(params, rq)
if 'keys' in rs:
merged = {} # team_uid -> PublicKeys fields
for tk in rs['keys']:
team_uid = tk.get('team_uid')
if not team_uid:
continue
if team_uid not in merged:
existing = params.key_cache.get(team_uid)
merged[team_uid] = {
'aes': getattr(existing, 'aes', b'') or b'',
'rsa': getattr(existing, 'rsa', b'') or b'',
'ec': getattr(existing, 'ec', b'') or b'',
}
# Read the symmetric/wrapped team key from the 'key' field
if 'key' in tk:
team_uid = tk['team_uid']
try:
aes = b''
rsa = b''
ec = b''
encrypted_key = utils.base64_url_decode(tk['key'])
key_type = tk['type']
key_type = tk.get('type')
if key_type == 1:
aes = crypto.decrypt_aes_v1(encrypted_key, params.data_key)
merged[team_uid]['aes'] = crypto.decrypt_aes_v1(encrypted_key, params.data_key)
elif key_type == 2:
aes = crypto.decrypt_rsa(encrypted_key, params.rsa_key2)
merged[team_uid]['aes'] = crypto.decrypt_rsa(encrypted_key, params.rsa_key2)
elif key_type == 3:
aes = crypto.decrypt_aes_v2(encrypted_key, params.data_key)
merged[team_uid]['aes'] = crypto.decrypt_aes_v2(encrypted_key, params.data_key)
elif key_type == 4:
aes = crypto.decrypt_ec(encrypted_key, params.ecc_key)
merged[team_uid]['aes'] = crypto.decrypt_ec(encrypted_key, params.ecc_key)
elif key_type == -1:
ec = encrypted_key
merged[team_uid]['ec'] = encrypted_key
elif key_type == -3:
rsa = encrypted_key
params.key_cache[team_uid] = PublicKeys(rsa=rsa, aes=aes, ec=ec)
merged[team_uid]['rsa'] = encrypted_key
except Exception as e:
logging.debug(e)
# Read the raw team public key from 'team_public_key' field (separate from 'key')
if 'team_public_key' in tk:
try:
pub_key_bytes = utils.base64_url_decode(tk['team_public_key'])
pub_key_type = tk.get('team_public_key_type')
if pub_key_type == -3:
merged[team_uid]['rsa'] = pub_key_bytes
elif pub_key_type == -1:
merged[team_uid]['ec'] = pub_key_bytes
except Exception as e:
logging.debug(e)
for team_uid, kd in merged.items():
params.key_cache[team_uid] = PublicKeys(
rsa=kd['rsa'], aes=kd['aes'], ec=kd['ec'])


def load_available_teams(params):
Expand Down
55 changes: 43 additions & 12 deletions keepercommander/commands/nested_share_folder/folder_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,10 @@ def execute(self, params, **kwargs):
check_folder_share_permission(params, folder_uid, 'nsf-share-folder')

targets = self._collect_targets(params, recipients, folder_uid, folder_arg)
if not targets:
raise CommandError(
'nsf-share-folder',
f'No valid recipients resolved for folder {folder_arg!r}')
for recipient, is_team in targets:
self._apply(params, action, folder_uid, recipient, role,
expiration, as_team=is_team)
Expand Down Expand Up @@ -408,24 +412,50 @@ def _expand_existing(params, folder_uid, folder_arg):
"""Expand ``@existing`` / ``@current`` into all users and teams currently
on the folder, excluding the caller. Mirrors legacy behaviour
(``shared_folder_cache[...]['users']`` + ``['teams']`` union).

Uses ``get_folder_access_v3`` so sub-folders (whose sync cache only
carries the current user's own row) still enumerate every accessor
that can be removed, including inherited access.
"""
from keepercommander.proto import folder_pb2
accesses = (getattr(params, 'nested_share_folder_accesses', {})
.get(folder_uid, []))
at_user = int(folder_pb2.AT_USER)
at_team = int(folder_pb2.AT_TEAM)

result = []
for a in accesses:
access_type = int(a.get('access_type', 0) or 0)
if access_type == at_user:
username = a.get('username')
if username and username != params.user:
result.append(('user', username))
elif access_type == at_team:
team_uid = a.get('access_type_uid')
if team_uid:
result.append(('team', team_uid))
try:
info = _nsf.get_folder_access_v3(params, [folder_uid], resolve_usernames=True)
for fr in info.get('results', []):
if not fr.get('success'):
continue
for accessor in fr.get('accessors', []):
if accessor.get('access_type') == 'AT_OWNER':
continue
if accessor.get('access_type') == 'AT_TEAM':
team_uid = accessor.get('accessor_uid')
if team_uid:
result.append(('team', team_uid))
elif accessor.get('access_type') == 'AT_USER':
username = accessor.get('username')
if username and username != params.user:
result.append(('user', username))
except Exception as exc:
logging.debug(
'nsf-share-folder: get_folder_access_v3 failed for %s: %s',
folder_arg, exc)

if not result:
accesses = (getattr(params, 'nested_share_folder_accesses', {})
.get(folder_uid, []))
for a in accesses:
access_type = int(a.get('access_type', 0) or 0)
if access_type == at_user:
username = a.get('username')
if username and username != params.user:
result.append(('user', username))
elif access_type == at_team:
team_uid = a.get('access_type_uid')
if team_uid:
result.append(('team', team_uid))

if not result:
logging.info("No existing users or teams found in folder '%s'", folder_arg)
Expand All @@ -447,6 +477,7 @@ def _apply(cls, params, action, folder_uid, recipient, role, expiration,
try:
result = api_func(**kw)
if result['success']:
params.sync_data = True
taken = result.get('action_taken', verb)
if taken == 'already_had_access':
logging.info("%s '%s' already has access", kind, recipient)
Expand Down
11 changes: 8 additions & 3 deletions keepercommander/commands/nested_share_folder/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,14 +201,16 @@ def classify_share_recipient(params, recipient):
Mirrors the legacy ``share-folder`` resolution exactly:
1. If *recipient* matches ``EMAIL_PATTERN`` → ``('user', email_lower)``.
2. Otherwise look it up in ``api.get_share_objects(params)['teams']``
(cap of 500 entries) and, if needed, ``params.available_team_cache``.
A match by team name *or* team UID returns ``('team', team_uid_b64)``.
(cap of 500 entries), ``params.available_team_cache``, and
``resolve_team_identifier`` (``team_cache``). A match by team name
*or* team UID returns ``('team', team_uid_b64)``.
3. No match → logs the same warning as legacy and returns ``None``.
4. Multiple matches → logs the same warning and returns ``None``.

Returns ``(kind, identifier)`` or ``None``.
"""
from ... import constants, api
from ...nested_share_folder.common import resolve_team_identifier

if re.match(constants.EMAIL_PATTERN, recipient):
return 'user', recipient.lower()
Expand All @@ -228,12 +230,15 @@ def classify_share_recipient(params, recipient):
pass

matches = [uid for uid, name in teams_map.items()
if recipient in (name, uid)]
if recipient == uid or (name and name.lower() == recipient.lower())]

if len(matches) == 1:
return 'team', matches[0]

if not matches:
resolved_team = resolve_team_identifier(params, recipient)
if resolved_team:
return 'team', resolved_team[0]
logger.warning('User "%s" could not be resolved as email or team',
recipient)
else:
Expand Down
70 changes: 53 additions & 17 deletions keepercommander/nested_share_folder/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ def get_team_keys(params, team_uid_b64: str):
"""
from ..params import PublicKeys

cached = params.key_cache.get(team_uid_b64)
has_asym = bool(cached and (getattr(cached, 'rsa', None) or getattr(cached, 'ec', None)))
if cached and has_asym:
return cached

api.load_team_keys(params, [team_uid_b64])
keys = params.key_cache.get(team_uid_b64)

Expand All @@ -186,21 +191,47 @@ def get_team_keys(params, team_uid_b64: str):
try:
rq = {'command': 'team_get_keys', 'teams': [team_uid_b64]}
rs = api.communicate(params, rq)
existing_aes = getattr(keys, 'aes', None) if keys else None
rsa_pub = b''
ec_pub = b''
merged = {
'aes': getattr(keys, 'aes', b'') or b'' if keys else b'',
'rsa': getattr(keys, 'rsa', b'') or b'' if keys else b'',
'ec': getattr(keys, 'ec', b'') or b'' if keys else b'',
}
for tk in (rs or {}).get('keys', []):
if tk.get('team_uid') != team_uid_b64 or 'key' not in tk:
if tk.get('team_uid') != team_uid_b64:
continue
key_type = tk.get('type')
encrypted_key = utils.base64_url_decode(tk['key'])
if key_type == -1:
ec_pub = encrypted_key
elif key_type == -3:
rsa_pub = encrypted_key
if rsa_pub or ec_pub:
# Symmetric/wrapped key in 'key' field
if 'key' in tk:
try:
key_type = tk.get('type')
encrypted_key = utils.base64_url_decode(tk['key'])
if key_type == 1:
merged['aes'] = crypto.decrypt_aes_v1(encrypted_key, params.data_key)
elif key_type == 2:
merged['aes'] = crypto.decrypt_rsa(encrypted_key, params.rsa_key2)
elif key_type == 3:
merged['aes'] = crypto.decrypt_aes_v2(encrypted_key, params.data_key)
elif key_type == 4:
merged['aes'] = crypto.decrypt_ec(encrypted_key, params.ecc_key)
elif key_type == -1:
merged['ec'] = encrypted_key
elif key_type == -3:
merged['rsa'] = encrypted_key
except Exception as e:
logger.debug('team_get_keys key decode failed: %s', e)
# Raw public key in 'team_public_key' field (separate from 'key')
if 'team_public_key' in tk:
try:
pub_key_bytes = utils.base64_url_decode(tk['team_public_key'])
pub_key_type = tk.get('team_public_key_type')
if pub_key_type == -3:
merged['rsa'] = pub_key_bytes
elif pub_key_type == -1:
merged['ec'] = pub_key_bytes
except Exception as e:
logger.debug('team_get_keys public key decode failed: %s', e)
if any(merged.values()):
params.key_cache[team_uid_b64] = PublicKeys(
aes=existing_aes, rsa=rsa_pub, ec=ec_pub)
aes=merged['aes'], rsa=merged['rsa'], ec=merged['ec'])
keys = params.key_cache[team_uid_b64]
except Exception as exc:
logger.debug("team_get_keys fallback failed for %s: %s",
Expand All @@ -215,6 +246,10 @@ def encrypt_for_team(plaintext_key: bytes, team_keys,
prefer_aes: bool = False,
forbid_rsa: bool = False) -> Tuple[bytes, int]:
"""Encrypt *plaintext_key* using the best available team key.

Mirrors legacy ``share-folder``: prefer the team AES key when
*prefer_aes* is set, otherwise try asymmetric keys first, then fall
back to AES when no public key is available (common for enterprise teams).
"""
aes = getattr(team_keys, 'aes', None)
ec_bytes = getattr(team_keys, 'ec', None)
Expand All @@ -237,7 +272,9 @@ def encrypt_for_team(plaintext_key: bytes, team_keys,
return (crypto.encrypt_ec(plaintext_key, ec_key),
folder_pb2.encrypted_by_public_key_ecc)

raise ValueError("No public key found for team")
raise ValueError(
"No public key found for team; NSF folder sharing requires the "
"team's RSA or ECC public key (server requires key type 2)")


def resolve_uid_email(params, user_identifier: str) -> Tuple[Optional[bytes], str]:
Expand Down Expand Up @@ -451,13 +488,12 @@ def parse_folder_access_result(response, folder_uid, user_uid, default_message):
if response.folderAccessResults:
result = response.folderAccessResults[0]
status_value = result.status
is_failure = (status_value != 0) or (result.message and len(result.message) > 0)
status_name = (folder_pb2.FolderModifyStatus.Name(status_value)
if status_value != 0 else 'SUCCESS')
is_failure = status_value != folder_pb2.SUCCESS
status_name = folder_pb2.FolderModifyStatus.Name(status_value)
return {
'folder_uid': folder_uid,
'user_uid': user_uid,
'status': 'ERROR' if is_failure and status_value == 0 else status_name,
'status': status_name,
'message': result.message if result.message else default_message,
'success': not is_failure,
}
Expand Down
Loading
Loading