Skip to content

Commit efbe668

Browse files
committed
Add functions for encrypting and decrypting junipter $9$ type passwords
1 parent 1a51547 commit efbe668

File tree

4 files changed

+142
-0
lines changed

4 files changed

+142
-0
lines changed

docs/user/include_jinja_list.md

+2
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@
5353
| mac_type | netutils.mac.mac_type |
5454
| compare_type5 | netutils.password.compare_type5 |
5555
| compare_type7 | netutils.password.compare_type7 |
56+
| decrypt_juniper | netutils.password.decrypt_juniper |
5657
| decrypt_type7 | netutils.password.decrypt_type7 |
58+
| encrypt_juniper | netutils.password.encrypt_juniper |
5759
| encrypt_type5 | netutils.password.encrypt_type5 |
5860
| encrypt_type7 | netutils.password.encrypt_type7 |
5961
| get_hash_salt | netutils.password.get_hash_salt |

netutils/password.py

+110
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,24 @@
7171
"0x37",
7272
]
7373

74+
JUNIPER_ENCODING = [
75+
[1, 4, 32],
76+
[1, 16, 32],
77+
[1, 8, 32],
78+
[1, 64],
79+
[1, 32],
80+
[1, 4, 16, 128],
81+
[1, 32, 64],
82+
]
83+
84+
JUNIPER_KEYS = ["QzF3n6/9CAtpu0O", "B1IREhcSyrleKvMW8LXx", "7N-dVbwsY2g4oaJZGUDj", "iHkq.mPf5T"]
85+
JUNIPER_KEYS_STRING = "".join(JUNIPER_KEYS)
86+
JUNIPER_KEYS_LENGTH = len(JUNIPER_KEYS_STRING)
87+
JUNIPER_CHARACTER_KEYS = dict()
88+
for idx, key in enumerate(JUNIPER_KEYS):
89+
for character in key:
90+
JUNIPER_CHARACTER_KEYS[character] = 3 - idx
91+
7492

7593
def _fail_on_mac(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
7694
"""There is an issue with Macintosh for encryption."""
@@ -252,3 +270,95 @@ def get_hash_salt(encrypted_password: str) -> str:
252270
if len(split_password) != 4:
253271
raise ValueError(f"Could not parse salt out password correctly from {encrypted_password}")
254272
return split_password[2]
273+
274+
275+
def decrypt_juniper(encrypted_password: str) -> str:
276+
"""Given an encrypted Junos $9$ type password, decrypt it.
277+
278+
Args:
279+
encrypted_password: A password that has been encrypted, and will be decrypted.
280+
281+
Returns:
282+
The unencrypted_password password.
283+
284+
Examples:
285+
>>> from netutils.password import decrypt_juniper
286+
>>> decrypt_juniper("$9$7YdwgGDkTz6oJz69A1INdb")
287+
'juniper'
288+
>>>
289+
"""
290+
# Strip $9$ from start of string
291+
password_characters = encrypted_password.split("$9$", 1)[1]
292+
293+
# Get first character and toss extra characters
294+
first_character = password_characters[0]
295+
stripped_password_characters = password_characters[JUNIPER_CHARACTER_KEYS[first_character] + 1 :]
296+
297+
previous_char = first_character
298+
decrypted_password = "" # nosec
299+
while stripped_password_characters:
300+
# Get encoding modulus
301+
decode = JUNIPER_ENCODING[len(decrypted_password) % len(JUNIPER_ENCODING)]
302+
303+
# Get nibble we will decode
304+
nibble = stripped_password_characters[0 : len(decode)]
305+
stripped_password_characters = stripped_password_characters[len(decode) :]
306+
307+
# Decode value for nibble and convert to character, append to decryped password
308+
value = 0
309+
for idx, char in enumerate(nibble):
310+
gap = (
311+
(JUNIPER_KEYS_STRING.index(char) - JUNIPER_KEYS_STRING.index(previous_char)) % JUNIPER_KEYS_LENGTH
312+
) - 1
313+
value += gap * decode[idx]
314+
previous_char = char
315+
decrypted_password += chr(value)
316+
317+
return decrypted_password
318+
319+
320+
def encrypt_juniper(unencrypted_password: str, salt: t.Optional[int] = None) -> str:
321+
"""Given an unencrypted password, encrypt to Juniper $9$ type password.
322+
323+
Args:
324+
unencrypted_password: A password that has not been encrypted, and will be compared against.
325+
salt: A integer that can be set by the operator. Defaults to random generated one.
326+
327+
Returns:
328+
The encrypted password.
329+
330+
Examples:
331+
>>> from netutils.password import encrypt_juniper
332+
>>> encrypt_juniper("juniper", 35) # doctest: +SKIP
333+
'$9$7YdwgGDkTz6oJz69A1INdb'
334+
>>>
335+
"""
336+
if not salt:
337+
salt = random.randint(0, JUNIPER_KEYS_LENGTH) - 1 # nosec
338+
339+
# Use salt to generate start of encrypted password
340+
first_character = JUNIPER_KEYS_STRING[salt]
341+
random_chars = "".join(
342+
[
343+
JUNIPER_KEYS_STRING[random.randint(0, JUNIPER_KEYS_LENGTH) - 1] # nosec
344+
for x in range(0, JUNIPER_CHARACTER_KEYS[first_character])
345+
]
346+
)
347+
encrypted_password = "$9$" + first_character + random_chars
348+
349+
previous_character = first_character
350+
for idx, char in enumerate(unencrypted_password):
351+
encode = JUNIPER_ENCODING[idx % len(JUNIPER_ENCODING)][::-1] # Get encoding modulus in reverse order
352+
char_ord = ord(char)
353+
gaps: t.List[int] = []
354+
for modulus in encode:
355+
gaps = [int(char_ord / modulus)] + gaps
356+
char_ord %= modulus
357+
358+
for gap in gaps:
359+
gap += JUNIPER_KEYS_STRING.index(previous_character) + 1
360+
new_character = JUNIPER_KEYS_STRING[gap % JUNIPER_KEYS_LENGTH]
361+
previous_character = new_character
362+
encrypted_password += new_character
363+
364+
return encrypted_password

netutils/utils.py

+2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
"encrypt_type5": "password.encrypt_type5",
5959
"encrypt_type7": "password.encrypt_type7",
6060
"get_hash_salt": "password.get_hash_salt",
61+
"encrypt_juniper": "password.encrypt_juniper",
62+
"decrypt_juniper": "password.decrypt_juniper",
6163
"tcp_ping": "ping.tcp_ping",
6264
"longest_prefix_match": "route.longest_prefix_match",
6365
"vlanlist_to_config": "vlan.vlanlist_to_config",

tests/unit/test_password.py

+28
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,21 @@
6969
},
7070
]
7171

72+
ENCRYPT_JUNIPER = [
73+
{
74+
"sent": {"unencrypted_password": "juniper", "salt": 35},
75+
"received_one": "$9$7",
76+
"received_two": "gGDkTz6oJz69A1INdb",
77+
},
78+
]
79+
80+
DECRYPT_JUNIPER = [
81+
{
82+
"sent": {"encrypted_password": "$9$7YdwgGDkTz6oJz69A1INdb"},
83+
"received": "juniper",
84+
}
85+
]
86+
7287

7388
@pytest.mark.parametrize("data", COMPARE_TYPE5)
7489
def test_compare_type5(data):
@@ -98,3 +113,16 @@ def test_encrypt_type7(data):
98113
@pytest.mark.parametrize("data", GET_HASH_SALT)
99114
def test_get_hash_salt(data):
100115
assert password.get_hash_salt(**data["sent"]) == data["received"]
116+
117+
118+
@pytest.mark.parametrize("data", ENCRYPT_JUNIPER)
119+
def test_encrypt_juniper(data):
120+
# Passwords include random padding, check only the non random sections
121+
decrypted_password = password.encrypt_juniper(**data["sent"])
122+
assert decrypted_password[0:4] == data["received_one"]
123+
assert decrypted_password[7:] == data["received_two"]
124+
125+
126+
@pytest.mark.parametrize("data", DECRYPT_JUNIPER)
127+
def test_decrypt_juniper(data):
128+
assert password.decrypt_juniper(**data["sent"]) == data["received"]

0 commit comments

Comments
 (0)