|
71 | 71 | "0x37",
|
72 | 72 | ]
|
73 | 73 |
|
| 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 | + |
74 | 92 |
|
75 | 93 | def _fail_on_mac(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
|
76 | 94 | """There is an issue with Macintosh for encryption."""
|
@@ -252,3 +270,95 @@ def get_hash_salt(encrypted_password: str) -> str:
|
252 | 270 | if len(split_password) != 4:
|
253 | 271 | raise ValueError(f"Could not parse salt out password correctly from {encrypted_password}")
|
254 | 272 | 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 |
0 commit comments