Skip to content
Open
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
59 changes: 56 additions & 3 deletions keepercli-package/src/keepercli/commands/password_generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@

from . import base
from .. import api
from keepersdk import generator

from ..helpers.password_utils import (
PasswordGenerationService, GenerationRequest, GeneratedPassword,
BreachStatus
BreachStatus, DEFAULT_PASSWORD_LENGTH,
)
from ..params import KeeperParams

Expand Down Expand Up @@ -86,6 +88,37 @@ def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None:
help='Generate crypto-style strong password')
special_group.add_argument('--recoveryphrase', dest='recoveryphrase', action='store_true',
help='Generate 24-word recovery phrase')

passphrase_group = parser.add_argument_group('Keeper Passphrase')
passphrase_group.add_argument(
'--passphrase', dest='passphrase', action='store_true',
help='Generate a vault-style passphrase from the EFF word list',
)
passphrase_group.add_argument(
'--pp-separator', '-pps', dest='pp_separator', action='store',
help=(
f'Word separator (single character, or "space"). Allowed: '
f'{generator.PASSPHRASE_SEPARATOR_HELP}. '
'Overrides enterprise policy for this command.'
),
)
passphrase_group.add_argument(
'--pp-capitalize', '-ppc', dest='pp_capitalize', action='store_true',
help='Capitalize the first letter of each word. Overrides enterprise policy for this command.',
)
passphrase_group.add_argument(
'--pp-no-capitalize', dest='pp_capitalize', action='store_false',
help='Do not capitalize words. Overrides enterprise policy for this command.',
)
passphrase_group.add_argument(
'--pp-number', '-ppn', dest='pp_number', action='store_true',
help='Append a digit (0-9) to the first word only. Overrides enterprise policy for this command.',
)
passphrase_group.add_argument(
'--pp-no-number', dest='pp_number', action='store_false',
help='Do not append a digit to the first word. Overrides enterprise policy for this command.',
)
passphrase_group.set_defaults(pp_capitalize=None, pp_number=None)

diceware_group = parser.add_argument_group('Diceware Options')
diceware_group.add_argument('--dice-rolls', '-dr', dest='dice_rolls', type=int,
Expand Down Expand Up @@ -139,13 +172,17 @@ def _generate_passwords(self, service: PasswordGenerationService, request: Gener
def _create_generation_request(self, **kwargs) -> GenerationRequest:
"""Create a GenerationRequest from command line arguments."""
count = self._validate_count(kwargs.get('number', 1))
length = self._validate_length(kwargs.get('length', 20))
length = self._validate_length(kwargs.get('length', DEFAULT_PASSWORD_LENGTH))
algorithm = self._determine_algorithm(kwargs)

symbols, digits, uppercase, lowercase = self._validate_complexity_parameters(kwargs)
rules = self._validate_rules(kwargs.get('rules'))
dice_rolls = self._validate_dice_rolls(kwargs.get('dice_rolls'))

pp_separator = self._parse_passphrase_separator(kwargs.get('pp_separator'))
passphrase_word_count = None
if algorithm == 'passphrase' and length != DEFAULT_PASSWORD_LENGTH:
passphrase_word_count = length

return GenerationRequest(
length=length,
count=count,
Expand All @@ -158,6 +195,10 @@ def _create_generation_request(self, **kwargs) -> GenerationRequest:
dice_rolls=dice_rolls,
delimiter=kwargs.get('delimiter', ' '),
word_list_file=kwargs.get('word_list'),
pp_separator=pp_separator,
pp_capitalize=kwargs.get('pp_capitalize'),
pp_number=kwargs.get('pp_number'),
passphrase_word_count=passphrase_word_count,
enable_breach_scan=not kwargs.get('no_breachwatch', False)
# max_breach_attempts uses GenerationRequest default value
)
Expand All @@ -182,12 +223,24 @@ def _determine_algorithm(self, kwargs: Dict[str, Any]) -> str:
"""Determine password generation algorithm from arguments."""
if kwargs.get('crypto'):
return 'crypto'
elif kwargs.get('passphrase'):
return 'passphrase'
elif kwargs.get('recoveryphrase'):
return 'recovery'
elif kwargs.get('dice_rolls'):
return 'diceware'
else:
return 'random' # default

@staticmethod
def _parse_passphrase_separator(pp_separator: Optional[str]) -> Optional[str]:
"""Parse and validate passphrase separator from CLI."""
if not isinstance(pp_separator, str) or not pp_separator.strip():
return None
separator, error = generator._parse_passphrase_separator_token(pp_separator.strip())
if error:
raise base.CommandError(error)
return separator

def _validate_complexity_parameters(self, kwargs: Dict[str, Any]) -> tuple:
"""Validate complexity parameters (symbols, digits, uppercase, lowercase)."""
Expand Down
63 changes: 52 additions & 11 deletions keepercli-package/src/keepercli/commands/record_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import itertools
import json
import os
from typing import Iterable, Optional, List, Any, Sequence, Union, Dict
from typing import Iterable, Optional, List, Any, Sequence, Union, Dict, Tuple

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ed25519
Expand Down Expand Up @@ -118,8 +118,9 @@ class ParsedFieldValue:
Value Field type Description Example
==================== =============== =================== ==============
$GEN:[alg],[n] password Generates a random password $GEN:dice,5
Default algorith is rand alg: [rand | dice | crypto]
Default algorith is rand alg: [rand | dice | crypto | passphrase]
Optional: password length
passphrase: $GEN:passphrase[,word_count][,separator][,capitalize][,number]
$GEN oneTimeCode Generates TOTP URL
$GEN:[alg,][enc] keyPair Generates a key pair and $GEN:ec,enc
optional passcode alg: [rsa | ec | ed25519], enc
Expand Down Expand Up @@ -183,8 +184,13 @@ def assign_legacy_fields(self, record: vault_record.PasswordRecord, fields: List
if parsed_field.type == 'login':
record.login = parsed_field.value
elif parsed_field.type == 'password':
action_params.clear()
if self.is_generate_value(parsed_field.value, action_params):
record.password = self.generate_password(action_params)
password, gen_error = self.generate_password(action_params)
if gen_error:
self.on_warning(gen_error)
elif password is not None:
record.password = password
else:
record.password = parsed_field.value
elif parsed_field.type == 'url':
Expand Down Expand Up @@ -260,22 +266,49 @@ def generate_key_pair(key_type: str, passphrase: str) -> Dict:
}

@staticmethod
def generate_password(parameters: Optional[Sequence[str]]=None) -> str:
def generate_password(parameters: Optional[Sequence[str]] = None,
policy: Optional[dict] = None) -> Tuple[Optional[str], Optional[str]]:
algorithm, error = generator.resolve_gen_password_algorithm(
parameters if isinstance(parameters, (tuple, list, set)) else None)
if error:
return None, error

length = None
if isinstance(parameters, (tuple, list, set)):
algorithm = next((x for x in parameters if x in ('rand', 'dice', 'crypto')), 'rand')
length = next((x for x in parameters if x.isnumeric()), None)
if isinstance(length, str) and len(length) > 0:
try:
length = int(length)
except ValueError:
pass
else:
algorithm = 'rand'
length = None

gen: generator.PasswordGenerator
if algorithm == 'crypto':
gen = generator.CryptoPassphraseGenerator()
elif algorithm == 'passphrase':
pp_opts, pp_error = generator.parse_passphrase_gen_parameters(parameters)
if pp_error:
return None, pp_error
if policy and policy.get('passphrase-allow') is False:
logger.warning(
'Passphrase generation is disabled by enterprise policy; using random password.')
fallback_length = pp_opts.word_count or length
if isinstance(fallback_length, int):
if fallback_length < 4:
fallback_length = 4
elif fallback_length > 200:
fallback_length = 200
else:
fallback_length = 20
gen = generator.KeeperPasswordGenerator(length=fallback_length)
else:
gen = generator.KeeperPassphraseGenerator.create_with_options(
policy,
word_count=pp_opts.word_count if pp_opts.word_count is not None else length,
separator=pp_opts.separator,
capitalize=pp_opts.capitalize,
append_number=pp_opts.append_number,
)
elif algorithm == 'dice':
if isinstance(length, int):
if length < 1:
Expand All @@ -294,7 +327,7 @@ def generate_password(parameters: Optional[Sequence[str]]=None) -> str:
else:
length = 20
gen = generator.KeeperPasswordGenerator(length=length)
return gen.generate()
return gen.generate(), None

@staticmethod
def generate_totp_url() -> str:
Expand Down Expand Up @@ -456,12 +489,20 @@ def assign_typed_fields(self, record: vault_record.TypedRecord, fields: List[Par
value: Any = None
if self.is_generate_value(parsed_field.value, action_params):
if record_field.type == 'password':
value = self.generate_password(action_params)
value, gen_error = self.generate_password(action_params)
if gen_error:
self.on_warning(gen_error)
value = None
elif record_field.type in ('oneTimeCode', 'otp'):
value = self.generate_totp_url()
elif record_field.type in ('keyPair', 'privateKey'):
should_encrypt = 'enc' in action_params
passphrase = self.generate_password() if should_encrypt else ''
passphrase = ''
if should_encrypt:
passphrase, gen_error = self.generate_password()
if gen_error:
self.on_warning(gen_error)
continue
key_type = next((x for x in action_params if x in ('rsa', 'ec', 'ed25519')), 'rsa')
value = self.generate_key_pair(key_type, passphrase)
if passphrase:
Expand Down
13 changes: 13 additions & 0 deletions keepercli-package/src/keepercli/helpers/password_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ class GenerationRequest:
dice_rolls: Optional[int] = None
delimiter: str = ' '
word_list_file: Optional[str] = None

pp_separator: Optional[str] = None
pp_capitalize: Optional[bool] = None
pp_number: Optional[bool] = None
passphrase_word_count: Optional[int] = None

enable_breach_scan: bool = True
max_breach_attempts: int = BREACHWATCH_MAX
Expand Down Expand Up @@ -278,6 +283,14 @@ def _create_generator(self, request: GenerationRequest) -> generator.PasswordGen
word_list_file=request.word_list_file,
delimiter=request.delimiter
)
elif algorithm == 'passphrase':
return generator.KeeperPassphraseGenerator.create_with_options(
None,
word_count=request.passphrase_word_count,
separator=request.pp_separator,
capitalize=request.pp_capitalize,
append_number=request.pp_number,
)
else:
if request.rules and all(i is None for i in (request.symbols, request.digits, request.uppercase, request.lowercase)):
kpg = generator.KeeperPasswordGenerator.create_from_rules(request.rules, request.length)
Expand Down
Loading