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

Use legacycrypt instead of crypt on Python >= 3.13 #3070

Merged
merged 2 commits into from
Feb 27, 2024
Merged
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
27 changes: 24 additions & 3 deletions azurelinuxagent/common/osutil/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# Requires Python 2.6+ and Openssl 1.0+
#

import array
import base64
import datetime
import errno
Expand All @@ -26,23 +27,33 @@
import os
import platform
import pwd
import random
import re
import shutil
import socket
import string
import struct
import sys
import time
from pwd import getpwall

import array
from azurelinuxagent.common.exception import OSUtilError
# 'crypt' was removed in Python 3.13; use legacycrypt instead
if sys.version_info[0] == 3 and sys.version_info[1] >= 13 or sys.version_info[0] > 3:
try:
from legacycrypt import crypt
except ImportError:
def crypt(password, salt):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We provide this stub implementation in case the module is not present, otherwise the entire process will crash

raise OSUtilError("Please install the legacycrypt Python module to use this feature.")
else:
from crypt import crypt # pylint: disable=deprecated-module

from azurelinuxagent.common import conf
from azurelinuxagent.common import logger
from azurelinuxagent.common.utils import fileutil
from azurelinuxagent.common.utils import shellutil
from azurelinuxagent.common.utils import textutil

from azurelinuxagent.common.exception import OSUtilError
from azurelinuxagent.common.future import ustr, array_to_bytes
from azurelinuxagent.common.utils.cryptutil import CryptUtil
from azurelinuxagent.common.utils.flexible_version import FlexibleVersion
Expand Down Expand Up @@ -433,11 +444,21 @@ def chpasswd(self, username, password, crypt_id=6, salt_len=10):
if self.is_sys_user(username):
raise OSUtilError(("User {0} is a system user, "
"will not set password.").format(username))
passwd_hash = textutil.gen_password_hash(password, crypt_id, salt_len)
passwd_hash = DefaultOSUtil.gen_password_hash(password, crypt_id, salt_len)

self._run_command_raising_OSUtilError(["usermod", "-p", passwd_hash, username],
err_msg="Failed to set password for {0}".format(username))

@staticmethod
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved here from textutils.py; this is a better place for this function

def gen_password_hash(password, crypt_id, salt_len):
collection = string.ascii_letters + string.digits
salt = ''.join(random.choice(collection) for _ in range(salt_len))
salt = "${0}${1}".format(crypt_id, salt)
if sys.version_info[0] == 2:
# if python 2.*, encode to type 'str' to prevent Unicode Encode Error from crypt.crypt
password = password.encode('utf-8')
return crypt(password, salt)

def get_users(self):
return getpwall()

Expand Down
2 changes: 1 addition & 1 deletion azurelinuxagent/common/osutil/freebsd.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def chpasswd(self, username, password, crypt_id=6, salt_len=10):
if self.is_sys_user(username):
raise OSUtilError(("User {0} is a system user, "
"will not set password.").format(username))
passwd_hash = textutil.gen_password_hash(password, crypt_id, salt_len)
passwd_hash = DefaultOSUtil.gen_password_hash(password, crypt_id, salt_len)
self._run_command_raising_OSUtilError(['pw', 'usermod', username, '-H', '0'], cmd_input=passwd_hash,
err_msg="Failed to set password for {0}".format(username))

Expand Down
3 changes: 1 addition & 2 deletions azurelinuxagent/common/osutil/gaia.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
from azurelinuxagent.common.utils.cryptutil import CryptUtil
import azurelinuxagent.common.utils.fileutil as fileutil
import azurelinuxagent.common.utils.shellutil as shellutil
import azurelinuxagent.common.utils.textutil as textutil


class GaiaOSUtil(DefaultOSUtil):
Expand Down Expand Up @@ -64,7 +63,7 @@ def useradd(self, username, expiration=None, comment=None):

def chpasswd(self, username, password, crypt_id=6, salt_len=10):
logger.info('chpasswd')
passwd_hash = textutil.gen_password_hash(password, crypt_id, salt_len)
passwd_hash = DefaultOSUtil.gen_password_hash(password, crypt_id, salt_len)
ret, out = self._run_clish(
'set user admin password-hash ' + passwd_hash)
if ret != 0:
Expand Down
14 changes: 0 additions & 14 deletions azurelinuxagent/common/utils/textutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,8 @@
# Requires Python 2.6+ and Openssl 1.0+

import base64
# W4901: Deprecated module 'crypt' (deprecated-module)
import crypt # pylint: disable=deprecated-module
import hashlib
import random
import re
import string
import struct
import sys
import traceback
Expand Down Expand Up @@ -288,16 +284,6 @@ def remove_bom(c):
return c


def gen_password_hash(password, crypt_id, salt_len):
collection = string.ascii_letters + string.digits
salt = ''.join(random.choice(collection) for _ in range(salt_len))
salt = "${0}${1}".format(crypt_id, salt)
if sys.version_info[0] == 2:
# if python 2.*, encode to type 'str' to prevent Unicode Encode Error from crypt.crypt
password = password.encode('utf-8')
return crypt.crypt(password, salt)


def get_bytes_from_pem(pem_str):
base64_bytes = ""
for line in pem_str.split('\n'):
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
distro; python_version >= '3.8'
pyasn1
pyasn1
legacycrypt; python_version >= '3.13'
17 changes: 10 additions & 7 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,13 +314,16 @@ def run(self):


# Note to packagers and users from source.
# In version 3.5 of Python distribution information handling in the platform
# module was deprecated. Depending on the Linux distribution the
# implementation may be broken prior to Python 3.7 wher the functionality
Copy link
Member Author

@narrieta narrieta Feb 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was actually removed on 3.8

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question for my understanding, what is this requires does? will setup fail if these modules not present?

# will be removed from Python 3
requires = [] # pylint: disable=invalid-name
if sys.version_info[0] >= 3 and sys.version_info[1] >= 7:
requires = ['distro'] # pylint: disable=invalid-name
# * In version 3.5 of Python distribution information handling in the platform
# module was deprecated. Depending on the Linux distribution the
# implementation may be broken prior to Python 3.8 where the functionality
# will be removed from Python 3.
# * In version 3.13 of Python, the crypt module was removed and legacycrypt is
# required instead.
requires = [
"distro;python_version>='3.8'",
"legacycrypt;python_version>='3.13'",
]

modules = [] # pylint: disable=invalid-name

Expand Down
9 changes: 9 additions & 0 deletions tests/common/osutil/test_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,15 @@ def test_get_hostname_record_should_initialize_the_host_name_using_cloud_init_in
self.assertEqual(expected, actual, "get_hostname_record returned an incorrect hostname")
self.assertEqual(expected, self.__get_published_hostname_contents(), "get_hostname_record returned an incorrect hostname")

def test_get_password_hash(self):
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'test_passwords.txt'), 'rb') as in_file:
for data in in_file:
# Remove bom on bytes data before it is converted into string.
data = textutil.remove_bom(data)
data = ustr(data, encoding='utf-8')
password_hash = osutil.DefaultOSUtil.gen_password_hash(data, 6, 10)
self.assertNotEqual(None, password_hash)


if __name__ == '__main__':
unittest.main()
10 changes: 0 additions & 10 deletions tests/common/utils/test_text_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
#

import hashlib
import os
import unittest
from azurelinuxagent.common.future import LooseVersion as Version

Expand All @@ -26,15 +25,6 @@


class TestTextUtil(AgentTestCase):
def test_get_password_hash(self):
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'test_passwords.txt'), 'rb') as in_file:
for data in in_file:
# Remove bom on bytes data before it is converted into string.
data = textutil.remove_bom(data)
data = ustr(data, encoding='utf-8')
password_hash = textutil.gen_password_hash(data, 6, 10)
self.assertNotEqual(None, password_hash)

def test_replace_non_ascii(self):
data = ustr(b'\xef\xbb\xbfhehe', encoding='utf-8')
self.assertEqual('hehe', textutil.replace_non_ascii(data))
Expand Down
Loading