password.py 5.9 KB
Newer Older
N
Nicolargo 已提交
1 2 3 4
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
5
# Copyright (C) 2015 Nicolargo <nicolas@nicolargo.com>
N
Nicolargo 已提交
6 7 8 9 10 11 12 13 14 15 16 17 18 19
#
# Glances 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 3 of the License, or
# (at your option) any later version.
#
# Glances 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 program. If not, see <http://www.gnu.org/licenses/>.

A
PEP 257  
Alessio Sergi 已提交
20 21
"""Manage password."""

A
Alessio Sergi 已提交
22 23
import getpass
import hashlib
N
Nicolargo 已提交
24 25 26
import os
import sys
import uuid
A
Alessio Sergi 已提交
27
from io import open
N
Nicolargo 已提交
28

29
from glances import __appname__
30
from glances.compat import b, input
31
from glances.globals import BSD, LINUX, OSX, WINDOWS
32
from glances.logger import logger
N
Nicolargo 已提交
33 34


A
Alessio Sergi 已提交
35
class GlancesPassword(object):
A
PEP 257  
Alessio Sergi 已提交
36 37

    """This class contains all the methods relating to password."""
N
Nicolargo 已提交
38

39 40
    def __init__(self, username='glances'):
        self.username = username
N
Nicolargo 已提交
41
        self.password_path = self.get_password_path()
42
        self.password_filename = self.username + '.pwd'
N
Nicolargo 已提交
43 44 45
        self.password_filepath = os.path.join(self.password_path, self.password_filename)

    def get_password_path(self):
A
PEP 257  
Alessio Sergi 已提交
46 47
        r"""Get the path where the password file will be stored.

A
Alessio Sergi 已提交
48 49 50
        * Linux and BSD: ~/.config/glances
        * OS X: ~/Library/glances
        * Windows: %APPDATA%\glances
N
Nicolargo 已提交
51
        """
A
Alessio Sergi 已提交
52
        if LINUX or BSD:
N
Nicolargo 已提交
53
            app_path = os.environ.get('XDG_CONFIG_HOME') or os.path.expanduser('~/.config')
A
Alessio Sergi 已提交
54
        elif OSX:
N
Nicolargo 已提交
55
            app_path = os.path.join(os.environ.get('HOME'), 'Library')
A
Alessio Sergi 已提交
56
        elif WINDOWS:
N
Nicolargo 已提交
57 58 59 60 61
            app_path = os.environ.get('APPDATA')
        else:
            app_path = '.'

        # Append the Glances folder
62
        app_path = os.path.join(app_path, __appname__)
N
Nicolargo 已提交
63 64 65

        return app_path

66 67
    def sha256_hash(self, plain_password):
        """Return the SHA-256 of the given password."""
A
Alessio Sergi 已提交
68
        return hashlib.sha256(b(plain_password)).hexdigest()
69

N
Nicolargo 已提交
70
    def get_hash(self, salt, plain_password):
A
PEP 257  
Alessio Sergi 已提交
71
        """Return the hashed password, salt + SHA-256."""
N
Nicolargo 已提交
72
        return hashlib.sha256(salt.encode() + plain_password.encode()).hexdigest()
A
Alessio Sergi 已提交
73

N
Nicolargo 已提交
74
    def hash_password(self, plain_password):
A
PEP 257  
Alessio Sergi 已提交
75
        """Hash password with a salt based on UUID (universally unique identifier)."""
N
Nicolargo 已提交
76 77 78
        salt = uuid.uuid4().hex
        encrypted_password = self.get_hash(salt, plain_password)
        return salt + '$' + encrypted_password
A
Alessio Sergi 已提交
79

N
Nicolargo 已提交
80
    def check_password(self, hashed_password, plain_password):
A
PEP 257  
Alessio Sergi 已提交
81 82 83
        """Encode the plain_password with the salt of the hashed_password.

        Return the comparison with the encrypted_password.
N
Nicolargo 已提交
84 85 86 87 88 89
        """
        salt, encrypted_password = hashed_password.split('$')
        re_encrypted_password = self.get_hash(salt, plain_password)
        return encrypted_password == re_encrypted_password

    def get_password(self, description='', confirm=False, clear=False):
A
PEP 257  
Alessio Sergi 已提交
90 91 92 93 94
        """Get the password from a Glances client or server.

        For Glances server, get the password (confirm=True, clear=False):
            1) from the password file (if it exists)
            2) from the CLI
A
Alessio Sergi 已提交
95
        Optionally: save the password to a file (hashed with salt + SHA-256)
N
Nicolargo 已提交
96

A
PEP 257  
Alessio Sergi 已提交
97 98 99 100
        For Glances client, get the password (confirm=False, clear=True):
            1) from the CLI
            2) the password is hashed with SHA-256 (only SHA string transit
               through the network)
N
Nicolargo 已提交
101 102 103
        """
        if os.path.exists(self.password_filepath) and not clear:
            # If the password file exist then use it
104
            logger.info("Read password from file {}".format(self.password_filepath))
N
Nicolargo 已提交
105 106
            password = self.load_password()
        else:
107
            # password_sha256 is the plain SHA-256 password
A
Alessio Sergi 已提交
108
            # password_hashed is the salt + SHA-256 password
109
            password_sha256 = self.sha256_hash(getpass.getpass(description))
110
            password_hashed = self.hash_password(password_sha256)
N
Nicolargo 已提交
111 112
            if confirm:
                # password_confirm is the clear password (only used to compare)
113
                password_confirm = self.sha256_hash(getpass.getpass('Password (confirm): '))
N
Nicolargo 已提交
114 115

                if not self.check_password(password_hashed, password_confirm):
A
Alessio Sergi 已提交
116
                    logger.critical("Sorry, passwords do not match. Exit.")
N
Nicolargo 已提交
117 118
                    sys.exit(1)

119
            # Return the plain SHA-256 or the salted password
N
Nicolargo 已提交
120
            if clear:
121
                password = password_sha256
N
Nicolargo 已提交
122 123 124 125 126
            else:
                password = password_hashed

            # Save the hashed password to the password file
            if not clear:
A
Alessio Sergi 已提交
127 128
                save_input = input('Do you want to save the password? [Yes/No]: ')
                if len(save_input) > 0 and save_input[0].upper() == 'Y':
N
Nicolargo 已提交
129 130 131 132 133
                    self.save_password(password_hashed)

        return password

    def save_password(self, hashed_password):
A
PEP 257  
Alessio Sergi 已提交
134
        """Save the hashed password to the Glances folder."""
A
Alessio Sergi 已提交
135
        # Check if the Glances folder already exists
N
Nicolargo 已提交
136
        if not os.path.exists(self.password_path):
A
Alessio Sergi 已提交
137
            # Create the Glances folder
N
Nicolargo 已提交
138
            try:
A
Alessio Sergi 已提交
139 140
                os.makedirs(self.password_path)
            except OSError as e:
141
                logger.error("Cannot create Glances directory: {}".format(e))
N
Nicolargo 已提交
142 143
                return

A
Alessio Sergi 已提交
144
        # Create/overwrite the password file
145
        with open(self.password_filepath, 'wb') as file_pwd:
146
            file_pwd.write(b(hashed_password))
N
Nicolargo 已提交
147 148

    def load_password(self):
A
PEP 257  
Alessio Sergi 已提交
149
        """Load the hashed password from the Glances folder."""
A
Alessio Sergi 已提交
150 151 152
        # Read the password file, if it exists
        with open(self.password_filepath, 'r') as file_pwd:
            hashed_password = file_pwd.read()
N
Nicolargo 已提交
153 154

        return hashed_password