diff --git a/pegleg/engine/util/cryptostring.py b/pegleg/engine/util/cryptostring.py index 424c40cf..8a1b73fb 100644 --- a/pegleg/engine/util/cryptostring.py +++ b/pegleg/engine/util/cryptostring.py @@ -25,9 +25,80 @@ class CryptoString(object): self._pool = string.ascii_letters + string.digits + punctuation self._random = random.SystemRandom() + def has_upper(self, crypto_str): + """Check if string contains an uppercase letter + + :param str crypto_str: The string to test. + :returns: True if string contains at least one uppercase letter. + :rtype: boolean + """ + + return any(char in string.ascii_uppercase for char in crypto_str) + + def has_lower(self, crypto_str): + """Check if string contains a lowercase letter + + :param str crypto_str: The string to test. + :returns: True if string contains at least one lowercase letter. + :rtype: boolean + """ + + return any(char in string.ascii_lowercase for char in crypto_str) + + def has_number(self, crypto_str): + """Check if string contains a number + + :param str crypto_str: The string to test. + :returns: True if string contains at least one number. + :rtype: boolean + """ + + return any(char in string.digits for char in crypto_str) + + def has_symbol(self, crypto_str): + """Check if string contains a symbol + + :param str crypto_str: The string to test. + :returns: True if string contains at least one symbol. + :rtype: boolean + """ + + return any(char in string.punctuation for char in crypto_str) + + def validate_crypto_str(self, crypto_str): + """Ensure cryptostring contains characters from all sets + + :param str crypto_str: The string to test. + :returns: True if string contains at least one each: uppercase letter, + lowercase letter, number and symbol + :rtype: boolean + """ + + for test in [self.has_upper, self.has_lower, self.has_number, + self.has_symbol]: + if not test(crypto_str): + return False + + return True + def get_crypto_string(self, length=24): + """Create and return a random cryptographic string. + + When the string is generated, it will be checked to determine if it + contains uppercase letters, lowercase letters, numbers and symbols. + If it does not contain at least one character from each set it will + be re-generated until it does. + + :param int length: Length of crypto string to generate. If this length + is smaller than 24, or not defined, the length will default to 24. + :returns: The generated cryptographic string + :rtype: string """ - Create and return a random cryptographic string of length ``length``. - """ - return ''.join(self._random.choice(self._pool) - for _ in range(max(24, length))) + + while True: + crypto_str = ''.join(self._random.choice(self._pool) + for _ in range(max(24, length))) + if self.validate_crypto_str(crypto_str): + break + + return crypto_str diff --git a/tests/unit/engine/test_generate_cryptostring.py b/tests/unit/engine/test_generate_passphrases.py similarity index 92% rename from tests/unit/engine/test_generate_cryptostring.py rename to tests/unit/engine/test_generate_passphrases.py index d46de908..85a72dbd 100644 --- a/tests/unit/engine/test_generate_cryptostring.py +++ b/tests/unit/engine/test_generate_passphrases.py @@ -19,11 +19,9 @@ import uuid from cryptography import fernet import mock import pytest -import string from testfixtures import log_capture import yaml -from pegleg.engine.util.cryptostring import CryptoString from pegleg.engine.generators.passphrase_generator import PassphraseGenerator from pegleg.engine.util import encryption from pegleg.engine import util @@ -122,32 +120,6 @@ TEST_SITE_DOCUMENTS = [TEST_SITE_DEFINITION, TEST_PASSPHRASES_CATALOG] TEST_GLOBAL_SITE_DOCUMENTS = [TEST_SITE_DEFINITION, TEST_GLOBAL_PASSPHRASES_CATALOG] -def test_cryptostring_default_len(): - s_util = CryptoString() - s = s_util.get_crypto_string() - assert len(s) == 24 - alphabet = set(string.punctuation + string.ascii_letters + string.digits) - assert any(c in alphabet for c in s) - - -def test_cryptostring_short_len(): - s_util = CryptoString() - s = s_util.get_crypto_string(0) - assert len(s) == 24 - s = s_util.get_crypto_string(23) - assert len(s) == 24 - s = s_util.get_crypto_string(-1) - assert len(s) == 24 - - -def test_cryptostring_long_len(): - s_util = CryptoString() - s = s_util.get_crypto_string(25) - assert len(s) == 25 - s = s_util.get_crypto_string(128) - assert len(s) == 128 - - @mock.patch.object( util.definition, 'documents_for_site', diff --git a/tests/unit/engine/util/test_cryptostring.py b/tests/unit/engine/util/test_cryptostring.py new file mode 100644 index 00000000..3a8f7d21 --- /dev/null +++ b/tests/unit/engine/util/test_cryptostring.py @@ -0,0 +1,89 @@ +# Copyright 2018 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pegleg.engine.util.cryptostring import CryptoString +import string + + +def test_cryptostring_default_len(): + s_util = CryptoString() + s = s_util.get_crypto_string() + assert len(s) == 24 + +def test_cryptostring_short_len(): + s_util = CryptoString() + s = s_util.get_crypto_string(0) + assert len(s) == 24 + s = s_util.get_crypto_string(23) + assert len(s) == 24 + s = s_util.get_crypto_string(-1) + assert len(s) == 24 + +def test_cryptostring_long_len(): + s_util = CryptoString() + s = s_util.get_crypto_string(25) + assert len(s) == 25 + s = s_util.get_crypto_string(128) + assert len(s) == 128 + +def test_cryptostring_has_upper(): + s_util = CryptoString() + crypto_string = 'Th1sP@sswordH4sUppers!' + assert s_util.has_upper(crypto_string) is True + crypto_string = 'THISPASSWORDHASONLYUPPERS' + assert s_util.has_upper(crypto_string) is True + crypto_string = 'th1sp@sswordh4snouppers!' + assert s_util.has_upper(crypto_string) is False + +def test_cryptostring_has_lower(): + s_util = CryptoString() + crypto_string = 'Th1sP@sswordH4sLowers!' + assert s_util.has_lower(crypto_string) is True + crypto_string = 'thispasswordhasonlylowers' + assert s_util.has_lower(crypto_string) is True + crypto_string = 'TH1SP@SSWORDH4SNOLOWERS!' + assert s_util.has_lower(crypto_string) is False + +def test_cryptostring_has_number(): + s_util = CryptoString() + crypto_string = 'Th1sP@sswordH4sNumbers!' + assert s_util.has_number(crypto_string) is True + crypto_string = '123456789012345678901234567890' + assert s_util.has_number(crypto_string) is True + crypto_string = 'ThisP@sswordHasNoNumbers!' + assert s_util.has_number(crypto_string) is False + +def test_cryptostring_has_symbol(): + s_util = CryptoString() + crypto_string = 'Th1sP@sswordH4sSymbols!' + assert s_util.has_symbol(crypto_string) is True + crypto_string = '!@#$%^&*()[]\}{|<>?,./~`' + assert s_util.has_symbol(crypto_string) is True + crypto_string = 'ThisPasswordH4sNoSymbols' + assert s_util.has_symbol(crypto_string) is False + +def test_cryptostring_has_all(): + s_util = CryptoString() + crypto_string = s_util.get_crypto_string() + assert s_util.validate_crypto_str(crypto_string) is True + crypto_string = 'Th1sP@sswordH4sItAll!' + assert s_util.validate_crypto_str(crypto_string) is True + crypto_string = 'th1sp@sswordh4snouppers!' + assert s_util.validate_crypto_str(crypto_string) is False + crypto_string = 'TH1SP@SSWORDH4SNOLOWERS!' + assert s_util.validate_crypto_str(crypto_string) is False + crypto_string = 'ThisP@sswordHasNoNumbers!' + assert s_util.validate_crypto_str(crypto_string) is False + crypto_string = 'ThisPasswordH4sNoSymbols' + assert s_util.validate_crypto_str(crypto_string) is False \ No newline at end of file