Skip to main content
Archived

This project has been archived by its maintainers, and is no longer receiving any updates.

juniper8-crypt

tests PyPI Python versions License: MIT

[!IMPORTANT] This repository is read-only and no longer developed. Development has moved to network-secret, which brings juniper8-crypt, juniper9-crypt, and the Nokia SR OS custom-hash cipher together in a single package with a single CLI.

Migrating is an import path change and nothing more: pip install network-secret, then from network_secret import juniper8. The encrypt(), decrypt(), and check() signatures are identical.

Encrypt and decrypt Juniper $8$ (type 8) passwords, from the command line or Python.

The $8$ format is genuine authenticated encryption, keyed by the device master password (set system master-password). Unlike the reversible, keyless $9$ substitution cipher, a $8$ secret cannot be recovered without that master password: the same master password is required to both encrypt and decrypt.

Prefer a browser? Encode and decode $8$ (and $9$, and Nokia SR OS custom-hash) at network-secret.pages.dev. It runs the same algorithm fully client-side - nothing you type is ever sent to a server.

Juniper documents the $8$ format (AES256-GCM, PBKDF2, the field layout, the ASCII64/base64 encoding), but the documentation is incomplete in the one way that matters: it never states that the 16-byte iv field is truncated to its first 12 bytes for the GCM nonce. Implement it by the book and the authentication tag never verifies, which is why no public decoder existed. That missing detail was reverse-engineered and verified against a real JUNOS 23.2 device (the GCM authentication tag verifies). See Algorithm for the full details.

Run without installing

If you have uv installed, uvx runs the CLI without installing anything:

uvx juniper8-crypt --master 'MySecretMasterPw' --decrypt '$8$aes256-gcm$hmac-sha2-256$100$p8XEvHtxRNE$d/hqRmh5etkBzo7WSdtvjg$7w1eMTYXkz4RdzMF9CAkJQ$qVLunbFwBWwyxln2Vg'

Install

pip install juniper8-crypt

Or with uv:

uv add juniper8-crypt

Command-line usage

Every operation needs the master password. You can supply it three ways, in order of precedence:

  1. --master/-m on the command line,
  2. the JUNOS_MASTER_PASSWORD environment variable,
  3. an interactive no-echo prompt (used when neither of the above is set).
# Decrypt a $8$ value (master on the command line)
juniper8-crypt --master 'MyMaster' --decrypt '$8$aes256-gcm$hmac-sha2-256$100$...'

# Encrypt a plaintext
juniper8-crypt --master 'MyMaster' --encrypt 'LabBgpSecret1'

# Check a $8$ value against a plaintext or another $8$ value
juniper8-crypt --master 'MyMaster' --check '$8$...' 'LabBgpSecret1'
juniper8-crypt --master 'MyMaster' --check '$8$...' '$8$...'

# Master from the environment (keeps it out of shell history / the process list)
export JUNOS_MASTER_PASSWORD='MyMaster'
juniper8-crypt --decrypt '$8$aes256-gcm$hmac-sha2-256$100$...'

# Master from an interactive prompt (nothing on the command line or in the env)
juniper8-crypt --decrypt '$8$aes256-gcm$hmac-sha2-256$100$...'
# Master password: ‹typed without echo›

Passing --master on the command line is convenient but leaks the secret into your shell history and the process list (ps). Prefer JUNOS_MASTER_PASSWORD or the prompt for anything sensitive.

Always quote $8$ strings with single quotes - the shell expands $8 as a positional parameter otherwise.

Exit codes

Code Meaning
0 Success (or --check matched)
1 --check mismatched
2 Invalid input (malformed value, wrong master, etc.)

Example output

$ juniper8-crypt --master 'a3f8d9e112c04b7af1c3e8b92d057a4e' \
    --decrypt '$8$aes256-gcm$hmac-sha2-256$100$p8XEvHtxRNE$d/hqRmh5etkBzo7WSdtvjg$7w1eMTYXkz4RdzMF9CAkJQ$qVLunbFwBWwyxln2Vg'
LabBgpSecret1

$ juniper8-crypt --master 'a3f8d9e112c04b7af1c3e8b92d057a4e' --encrypt 'LabBgpSecret1'
$8$aes256-gcm$hmac-sha2-256$100$wh8cAoBCbnY$hON9pWdcoFECAJYdqwr3+A$IyYbHprOWFigR4titT+CxA$NWh8D/XOgwafCuK6TQ

$ juniper8-crypt --master 'a3f8d9e112c04b7af1c3e8b92d057a4e' \
    --check '$8$aes256-gcm$hmac-sha2-256$100$p8XEvHtxRNE$d/hqRmh5etkBzo7WSdtvjg$7w1eMTYXkz4RdzMF9CAkJQ$qVLunbFwBWwyxln2Vg' 'LabBgpSecret1'
Value 1   : 'LabBgpSecret1'
Value 2   : 'LabBgpSecret1'
Match     : YES

--encrypt output varies on every run: a fresh random salt and IV are generated each time, so the same plaintext produces a different $8$ string. They all decrypt back to the same plaintext with the same master password.

Python API

from juniper8_crypt import decrypt, encrypt, check

master = "a3f8d9e112c04b7af1c3e8b92d057a4e"

# Decrypt
plain = decrypt("$8$aes256-gcm$hmac-sha2-256$100$p8XEvHtxRNE$d/hqRmh5etkBzo7WSdtvjg$7w1eMTYXkz4RdzMF9CAkJQ$qVLunbFwBWwyxln2Vg", master)
# 'LabBgpSecret1'

# Encrypt (non-deterministic)
ciphertext = encrypt("LabBgpSecret1", master)
# '$8$aes256-gcm$hmac-sha2-256$100$...'  (a fresh value each call)

# Compare a $8$ value against a plaintext
plain_a, plain_b, match = check(ciphertext, "LabBgpSecret1", master)
assert match is True

# Compare two $8$ values
plain_a, plain_b, match = check(ciphertext, encrypt("LabBgpSecret1", master), master)
assert match is True

Error handling

decrypt() raises ValueError for malformed inputs (missing $8$ prefix, wrong field count, unsupported algorithm, invalid base64) and for authentication failure (wrong master password, or a value not produced by this scheme):

from juniper8_crypt import decrypt

try:
    decrypt("$8$...", "wrong-master")
except ValueError as e:
    print(f"bad input: {e}")

Tests

git clone https://github.com/antoinekh/juniper8-crypt
cd juniper8-crypt
uv run pytest -v

Algorithm

Juniper $8$ encryption overview

$8$ is the JUNOS "type 8" format used for secrets the device must be able to recover in cleartext (BGP/IS-IS authentication keys, RADIUS secrets, etc.) once a master password is configured. It is real authenticated encryption: PBKDF2 stretches the master password into an AES key, and AES-256-GCM encrypts the secret and authenticates it with a tag.

String layout

$8$<crypt-algo>$<hash-algo>$<iterations>$<salt>$<iv>$<tag>$<ciphertext>
Field Example Meaning
crypt-algo aes256-gcm Cipher. Only AES-256-GCM is currently emitted.
hash-algo hmac-sha2-256 PBKDF2 PRF (HMAC-SHA-256).
iterations 100 PBKDF2 iteration count (default 100, range 10-10000).
salt p8XEvHtxRNE 8 random bytes, the PBKDF2 salt.
iv d/hqRmh5e... 16 bytes; the GCM nonce is the first 12.
tag 7w1eMTYX... 16-byte GCM authentication tag.
ciphertext qVLunbFw... The encrypted secret (same length as the plaintext).

Every binary field is standard base64 (RFC 4648), with the = padding stripped.

Building blocks

1. Key derivation. The master password (as UTF-8 bytes) is stretched with PBKDF2-HMAC-SHA256 over the salt for iterations rounds, producing a 32-byte AES-256 key:

key = PBKDF2HMAC(SHA256(), length=32, salt=salt, iterations=iterations).derive(master.encode())

2. Encryption. AES-256-GCM encrypts the plaintext with no additional authenticated data (AAD), producing the ciphertext and a 16-byte tag:

nonce = iv[:12]                       # only the first 12 IV bytes are used
sealed = AESGCM(key).encrypt(nonce, plaintext, None)
ciphertext, tag = sealed[:-16], sealed[-16:]

3. The IV gotcha. This is the detail that defeats naive implementations. The iv field decodes to 16 bytes, but JUNOS uses only the first 12 as the GCM nonce (a standard 96-bit nonce); the trailing 4 bytes are random padding that is stored but never used. Feeding all 16 bytes to AES-GCM as the nonce produces a different counter stream and the tag never verifies.

Decryption

  1. Split the $8$ string and base64-decode salt, iv, tag, ciphertext.
  2. Re-derive the key with PBKDF2 from the master password and salt.
  3. AES-256-GCM-decrypt ciphertext with nonce iv[:12], verifying tag. A wrong master password (or any tampering) fails tag verification.

Security note

$8$ is real encryption, but its strength rests entirely on the master password. Treat the master password as a high-value secret; anyone holding it can decrypt every $8$ value in a config.

Credits

Juniper documents the $8$ format, but the documentation is incomplete: it omits that the 16-byte iv field is truncated to its first 12 bytes for the GCM nonce (and the tag/salt lengths, the exact PRF, and the no-AAD detail). A by-the-book implementation therefore fails authentication, and as far as I could find there was no public decoder. The missing pieces were reverse-engineered with AI help: Claude ran the known-plaintext search, spotted that AES-GCM's ciphertext is independent of the tag and AAD (which made the search tractable), and identified the iv[:12] nonce quirk that defeats naive implementations.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

juniper8_crypt-0.2.0.tar.gz (268.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

juniper8_crypt-0.2.0-py3-none-any.whl (10.4 kB view details)

Uploaded Python 3

File details

Details for the file juniper8_crypt-0.2.0.tar.gz.

File metadata

  • Download URL: juniper8_crypt-0.2.0.tar.gz
  • Upload date:
  • Size: 268.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for juniper8_crypt-0.2.0.tar.gz
Algorithm Hash digest
SHA256 2b849cddf9b58caf78820a5dacf85d8ff46766dd9dedd892b7c63bf3736adbc8
MD5 d53ceae8eba0bc9c224da0bbd56c7866
BLAKE2b-256 9126debe5ce83623ad4ad5c676381710b8267511c65cbaeded446b1d8d2fe215

See more details on using hashes here.

Provenance

The following attestation bundles were made for juniper8_crypt-0.2.0.tar.gz:

Publisher: publish.yml on antoinekh/juniper8-crypt

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file juniper8_crypt-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: juniper8_crypt-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 10.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for juniper8_crypt-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c2e4f035f57ce025bb6f025bda9762e342ec2fde96ae4d58fdeb6070426a4645
MD5 411f7fe1b354fde44b5c5d5cdea74371
BLAKE2b-256 1fd5b603f37333311d40688f4c6e4b8a93467a36eb575eb16896812f9120ffad

See more details on using hashes here.

Provenance

The following attestation bundles were made for juniper8_crypt-0.2.0-py3-none-any.whl:

Publisher: publish.yml on antoinekh/juniper8-crypt

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page