Skip to content

Commit

Permalink
Use legacycrypt instead of crypt on Python >= 3.13 (#3070)
Browse files Browse the repository at this point in the history
* Use legacycrypt instead of crypt on Python >= 3.13

* remove ModuleNotFound

---------

Co-authored-by: narrieta <narrieta>
  • Loading branch information
narrieta authored Feb 27, 2024
1 parent 5af6621 commit 6de4652
Show file tree
Hide file tree
Showing 9 changed files with 47 additions and 38 deletions.
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):
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
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
# 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()
File renamed without changes.
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

0 comments on commit 6de4652

Please sign in to comment.