summaryrefslogtreecommitdiff
path: root/python/vyos/utils/auth.py
blob: 5d0e3464a90f4c9aca7caeb10cb928094ea942fb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# authutils -- miscelanneous functions for handling passwords and publis keys
#
# Copyright (C) 2023-2024 VyOS maintainers and contributors
#
# This library is free software; you can redistribute it and/or modify it under the terms of
# the GNU Lesser General Public License as published by the Free Software Foundation;
# either version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along with this library;
# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

import cracklib
import math
import re
import string

from enum import StrEnum
from decimal import Decimal
from vyos.utils.process import cmd


DEFAULT_PASSWORD: str = 'vyos'
LOW_ENTROPY_MSG: str = 'should be at least 8 characters long;'
WEAK_PASSWORD_MSG: str = 'The password complexity is too low - @MSG@'
CRACKLIB_ERROR_MSG: str = 'A following error occurred: @MSG@\n' \
    'Possibly the cracklib database is corrupted or is missing. ' \
    'Try reinstalling the python3-cracklib package.'

class EPasswdStrength(StrEnum):
    WEAK = 'Weak'
    DECENT = 'Decent'
    STRONG = 'Strong'
    ERROR = 'Cracklib Error'


def calculate_entropy(charset: str, passwd: str) -> float:
    """
    Calculate the entropy of a password based on the set of characters used
    Uses E = log2(R**L) formula, where
        - R is the range (length) of the character set
        - L is the length of password
    """
    return math.log(math.pow(len(charset), len(passwd)), 2)

def evaluate_strength(passwd: str) -> dict[str, str]:
    """ Evaluates password strength and returns a check result dict """
    charset = (cracklib.ASCII_UPPERCASE + cracklib.ASCII_LOWERCASE +
        string.punctuation + string.digits)

    result = {
        'strength': '',
        'error': '',
    }

    try:
        cracklib.FascistCheck(passwd)
    except ValueError as e:
        # The password is vulnerable to dictionary attack no matter the entropy
        if 'is' in str(e):
            msg = str(e).replace('is', 'should not be')
        else:
            msg = f'should not be {e}'
        result.update(strength=EPasswdStrength.WEAK)
        result.update(error=WEAK_PASSWORD_MSG.replace('@MSG@', msg))
    except Exception as e:
        result.update(strength=EPasswdStrength.ERROR)
        result.update(error=CRACKLIB_ERROR_MSG.replace('@MSG@', str(e)))
    else:
        # Now check the password's entropy
        # Cast to Decimal for more precise rounding
        entropy = Decimal.from_float(calculate_entropy(charset, passwd))

        match round(entropy):
            case e if e in range(0, 59):
                result.update(strength=EPasswdStrength.WEAK)
                result.update(
                    error=WEAK_PASSWORD_MSG.replace('@MSG@', LOW_ENTROPY_MSG)
                )
            case e if e in range(60, 119):
                result.update(strength=EPasswdStrength.DECENT)
            case e if e >= 120:
                result.update(strength=EPasswdStrength.STRONG)

    return result

def make_password_hash(password):
    """ Makes a password hash for /etc/shadow using mkpasswd """

    mkpassword = 'mkpasswd --method=sha-512 --stdin'
    return cmd(mkpassword, input=password, timeout=5)

def split_ssh_public_key(key_string, defaultname=""):
    """ Splits an SSH public key into its components """

    key_string = key_string.strip()
    parts = re.split(r'\s+', key_string)

    if len(parts) == 3:
        key_type, key_data, key_name = parts[0], parts[1], parts[2]
    else:
        key_type, key_data, key_name = parts[0], parts[1], defaultname

    if key_type not in ['ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519']:
        raise ValueError("Bad key type \'{0}\', must be one of must be one of ssh-rsa, ssh-dss, ecdsa-sha2-nistp<256|384|521> or ssh-ed25519".format(key_type))

    return({"type": key_type, "data": key_data, "name": key_name})

def get_current_user() -> str:
    import os
    current_user = 'nobody'
    # During CLI "owner" script execution we use SUDO_USER
    if 'SUDO_USER' in os.environ:
        current_user = os.environ['SUDO_USER']
    # During op-mode or config-mode interactive CLI we use USER
    elif 'USER' in os.environ:
        current_user = os.environ['USER']
    return current_user