# QSPICE® Encryption Specification

**Version**: 1.0.0 ([changelog](#changelog))\
**Author**: Joe T. Sylve, Ph.D. \<joe.sylve@gmail.com\> \
**Repository**: https://github.com/jtsylve/spice-crypt

This document describes the encryption scheme that QSPICE uses to protect proprietary sub-circuit model files (the `.prot` / `.unprot` "protected" block found in `.qsch`, `.lib`, `.sub`, and similar text files).  The scheme combines a randomized base-16 text encoding, a dual stream cipher keyed by a per-file random seed stored in the clear, DEFLATE compression, and a Windows-1252 keyword tokenization of the netlist.  Each stage is documented so that the protected models can be read by alternative tools.

[SpiceCrypt](https://github.com/jtsylve/spice-crypt) is a reference implementation of this specification, available as a command-line tool and Python library under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later).


## Purpose

Many third-party component vendors distribute SPICE simulation models exclusively as QSPICE-encrypted files.  This encryption locks the models to a single proprietary simulator, preventing their use in open-source and alternative tools such as NGSpice, Xyce, PySpice, and others.

This specification is published in service of two goals:

- **Interoperability**: Documenting the encryption scheme allows developers of alternative SPICE simulators to support lawfully obtained encrypted models.  The accompanying [SpiceCrypt](../README.md) reference implementation demonstrates working decryption based on this specification.
- **Encryption research**: The scheme relies on security through obscurity — the key material is derived entirely from a seed stored in the clear alongside the ciphertext, so it provides no meaningful cryptographic protection (see [Section 7](#7-security-assessment)).  Documenting these properties illustrates how proprietary encryption schemes deviate from established standards.

Both activities are specifically permitted by law:

- **United States**: [17 U.S.C. § 1201(f)](https://www.law.cornell.edu/uscode/text/17/1201) permits circumvention of technological protection measures for the purpose of achieving interoperability between independently created programs, and Section 1201(f)(2) explicitly allows distributing the tools developed for this purpose.  [§ 1201(g)](https://www.law.cornell.edu/uscode/text/17/1201) further permits circumvention conducted in good-faith encryption research and allows dissemination of the findings.
- **European Union**: [Article 6 of the Software Directive (2009/24/EC)](https://eur-lex.europa.eu/eli/dir/2009/24/oj) permits decompilation and reverse engineering when indispensable to achieve interoperability with independently created programs.  Article 6(3) provides that this right cannot be overridden by contract.


## License

Copyright © 2026 Joe T. Sylve, Ph.D. <joe.sylve@gmail.com>

This document is licensed under the [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/) (CC-BY-4.0).  You are free to share and adapt this material for any purpose, including commercial use, provided appropriate credit is given.

QSPICE is a registered trademark of Qorvo US, Inc.  This specification is an independent work of interoperability research and is not affiliated with, endorsed by, or sponsored by Qorvo.


## Notation

- Byte values are written in hexadecimal with a `0x` prefix; multi-byte integers are little-endian unless stated otherwise.
- `a ^ b` is bitwise XOR, `a >> n` / `a << n` are logical shifts, `a & b` is bitwise AND, and `a % n` is the non-negative remainder.
- All 32-bit arithmetic is performed modulo 2³² (`& 0xFFFFFFFF`).


## 1. Encrypted File Format

A QSPICE protected model is ordinary SPICE text in which one or more sub-circuit bodies are replaced by a *protected block*.  A protected block is delimited by a line containing only `.prot` and a line containing only `.unprot`:

```
* Vendor copyright header
*
.subckt EXAMPLE A B C
.prot
l2VOxFvU,vvx,mUYUVYmQnYQlnlUQlmFYvVYwJvlvn2w
...more encoded lines...
.unprot
.ends EXAMPLE
```

**Delimiters**: The `.prot` and `.unprot` markers are matched against the whitespace-trimmed line, case-insensitively.  Everything between them is the encoded payload.  Lines outside any protected block — including the enclosing `.subckt` / `.ends` and any comments — are plain text and are passed through unchanged.

**Payload**: Between the delimiters, the payload is a stream of glyphs from a fixed 64-character alphabet ([Section 2.1](#21-encoding-alphabet)), wrapped across lines (vendor files use a width of 76 characters).  Whitespace between glyphs is not significant and is ignored when decoding.

**Decryption replaces** each protected block with the recovered plaintext sub-circuit body, yielding a complete, usable netlist.  The `.prot` and `.unprot` markers are not part of the plaintext and are discarded.

The remaining sections describe how the glyph stream is turned back into plaintext.  The procedure has five stages, applied in order: decode ([Section 2](#2-text-decoding)), generate keystreams ([Section 3](#3-keystream-generation)), decrypt ([Section 4](#4-payload-decryption)), decompress ([Section 5.1](#51-decompression)), and detokenize ([Section 5.2](#52-keyword-tokenization)).


## 2. Text Decoding

### 2.1 Encoding Alphabet

The payload is a randomized base-16 encoding: every plaintext byte is written as two glyphs, one per 4-bit nibble (most-significant nibble first).  The 64-glyph alphabet is arranged as four rows of sixteen:

| Row | Glyphs (nibble value 0x0 … 0xF)               |
|-----|-----------------------------------------------|
| 0   | `,` `v` `U` `x` `F` `K` `n` `m` `J` `w` `V` `O` `l` `2` `Y` `Q` |
| 1   | `Z` `R` `r` `D` `P` `H` `8` `f` `0` `u` `e` `d` `I` `T` `N` `7` |
| 2   | `z` `q` `M` `c` `y` `i` `h` `3` `W` `p` `E` `o` `j` `A` `1` `k` |
| 3   | `5` `6` `L` `t` `s` `&` `b` `9` `X` `g` `G` `a` `S` `4` `C` `B` |

As a single string in alphabet order (index `0` … `63`):

```
,vUxFKnmJwVOl2YQZRrDPH8f0uedITN7zqMcyih3WpEojA1k56Lts&b9XgGaS4CB
```

A glyph encodes the nibble value equal to **its index modulo 16**.  Each nibble value therefore has four interchangeable glyphs (one per row); the encoder selects a row pseudo-randomly so that re-encoding the same model produces different-looking output.  The row selection carries no information and is ignored when decoding — only `index % 16` matters.

### 2.2 Nibbles to Bytes

To decode the payload:

1. Discard any character not in the alphabet (this removes line breaks and inter-glyph whitespace).
2. Take the remaining glyphs in pairs.  For a pair `(g_hi, g_lo)`, the decoded byte is `(nibble(g_hi) << 4) | nibble(g_lo)`, where `nibble(g)` is the glyph's index modulo 16.
3. If an odd glyph remains at the end, drop it.

Let the full decoded byte string be `D`.

### 2.3 Seed and Payload Split

The first four bytes of `D` are a 32-bit **seed**, stored little-endian and **not** encrypted:

```
seed = D[0] | (D[1] << 8) | (D[2] << 16) | (D[3] << 24)
```

The remaining bytes, `P = D[4:]`, are the encrypted payload.  The seed is the only key material; both keystreams in [Section 3](#3-keystream-generation) are derived from it.


## 3. Keystream Generation

The payload is decrypted by XOR with the byte-wise sum (XOR) of two independent keystreams, both seeded by `seed`.

### 3.1 Mersenne Twister Keystream

The first keystream is the low byte of each output word of an MT19937 generator (Matsumoto–Nishimura) that uses **non-standard seeding** but the **standard** recurrence and tempering.

**Parameters** (standard MT19937): `n = 624`, `m = 397`, `MATRIX_A = 0x9908B0DF`, `UPPER_MASK = 0x80000000`, `LOWER_MASK = 0x7FFFFFFF`.

**Seeding** (non-standard).  The 624-word state is a geometric sequence in the seed with multiplier `6069`:

```
state[0] = seed                                    (if seed is 0, use 1)
state[k] = (6069 * state[k-1]) & 0xFFFFFFFF        for k = 1 … 623
```

A `seed` of `0` is replaced by `1` for the purpose of seeding `state[0]` only; the table-walk keystream in [Section 3.2](#32-keystream-table-walk) continues to use the raw `seed` (so its first index is `0`).

The generator index is initialized to `n` so that a full regeneration ("twist") runs before the first word is produced.

**Regeneration** (standard twist):

```
for i in 0 … 623:
    y          = (state[i] & UPPER_MASK) | (state[(i+1) % 624] & LOWER_MASK)
    state[i]   = state[(i+397) % 624] ^ (y >> 1)
    if y & 1:
        state[i] ^= MATRIX_A
index = 0
```

**Tempering** (standard).  To produce the next word, take `y = state[index]`, advance `index`, regenerating first if `index >= 624`, then:

```
y ^= y >> 11
y ^= (y << 7)  & 0x9D2C5680
y ^= (y << 15) & 0xEFC60000
y ^= y >> 18
```

The MT keystream byte for payload position `i` (0-based) is the low 8 bits (`y & 0xFF`) of the `i`-th tempered word.

### 3.2 Keystream Table Walk

The second keystream indexes a fixed 9973-byte table `T` ([Appendix A](#appendix-a-keystream-table)).  The walk is a constant-stride sequence modulo the table length (9973 is prime):

```
stride  = (seed >> 3) & 0xFF
index_0 = seed % 9973
index_{i+1} = (index_i + stride) % 9973
```

The table keystream byte for payload position `i` is `T[index_i]`.  When `stride` is `0` (i.e. `(seed >> 3) & 0xFF == 0`), every position uses `T[seed % 9973]`.


## 4. Payload Decryption

Each encrypted payload byte is decrypted by XOR with both keystream bytes for that position:

```
plaintext_compressed[i] = P[i] ^ mt_byte(i) ^ T[index_i]
```

where `mt_byte(i)` is from [Section 3.1](#31-mersenne-twister-keystream) and `T[index_i]` is from [Section 3.2](#32-keystream-table-walk).  XOR is its own inverse, so the same operation encrypts and decrypts.


## 5. Decompression and Keyword Tokenization

### 5.1 Decompression

The decrypted payload is a single zlib stream (RFC 1950: a two-byte header, a raw DEFLATE body per RFC 1951, and a trailing Adler-32 checksum; vendor files begin with the bytes `0x78 0x9C`).  Inflating it yields the QSPICE netlist body — the lines that belong between the enclosing `.subckt` and `.ends`.

### 5.2 Keyword Tokenization

QSPICE stores its netlists in the Windows-1252 (CP1252) code page.  Standard ASCII SPICE content (component lines, `.model` statements, node names, numeric values) is recovered verbatim, but certain device prefixes and operators are carried as single bytes with the high bit set.  These bytes are ordinary CP1252 characters, so decoding the inflated body as Windows-1252 expands every token to the character QSPICE documents for it:

| Byte | Char | QSPICE meaning |
|------|------|----------------|
| `0xC3` | `Ã` | Gm-block device prefix (stored lowercased as `0xE3` `ã`) |
| `0xD8` | `Ø` | .DLL device prefix — C++/Verilog modules (stored lowercased as `0xF8` `ø`) |
| `0xA5` | `¥` | gate/flip-flop device prefix; also the reserved-/unconnected-pin marker |
| `0x80` | `€` | 12-bit DAC device prefix |
| `0xA3` | `£` | (de)multiplexer / gate-driver device prefix |
| `0xD7` | `×` | saturating-transformer device prefix |
| `0xAB` / `0xBB` | `«` / `»` | node-group delimiters (`Ø` and `×` devices) |
| `0xB4` | `´` | separator between a device-type prefix and the instance name |
| `0xB5` | `µ` | micro (1e-6) SI prefix on numeric values |

The device prefixes are written uppercase in QSPICE's documentation but appear lowercased in the payload, because QSPICE lower-cases the netlist before protecting it.  Of these characters, only the micro sign has a standard-SPICE ASCII equivalent: `u`.  Other SPICE tools (NGSpice, Xyce, PySpice, PSpice) expect `u` and will mis-parse `µ`, so a tool targeting interoperability should rewrite `µ` → `u`.  The remaining characters name QSPICE-only behavioral devices and bus syntax with no equivalent in other simulators; they are preserved as the characters QSPICE itself documents.


## 6. Decryption Procedure (Summary)

Given the text between `.prot` and `.unprot`:

1. **Decode** the glyphs to bytes `D` ([Section 2.2](#22-nibbles-to-bytes)).
2. **Split** `seed = D[0:4]` (little-endian) and `P = D[4:]` ([Section 2.3](#23-seed-and-payload-split)).
3. **Generate** the MT keystream ([Section 3.1](#31-mersenne-twister-keystream)) and table-walk keystream ([Section 3.2](#32-keystream-table-walk)) from `seed`.
4. **XOR** each byte of `P` with both keystream bytes to recover the compressed payload ([Section 4](#4-payload-decryption)).
5. **Inflate** the result as a zlib stream to recover the netlist body ([Section 5.1](#51-decompression)).
6. **Detokenize** the body by decoding it as Windows-1252, expanding the high-bit device-prefix and operator tokens (and, for interoperability, rewriting `µ` → `u`) ([Section 5.2](#52-keyword-tokenization)).
7. **Substitute** the plaintext body in place of the protected block, discarding the `.prot` / `.unprot` markers.


## 7. Security Assessment

The QSPICE protected-block scheme is obfuscation, not encryption, and provides no confidentiality against an informed party:

- **No secret key.**  All key material is the 32-bit `seed`, which is stored in the clear as the first four bytes of every protected block.  Anyone holding the file holds the key.
- **Public construction.**  The alphabet, the keystream table, the Mersenne Twister parameters, and the seeding rule are fixed constants of the scheme, identical across all files; only the seed varies.  Recovering them once (as in this document) is sufficient to decrypt every protected block ever produced.
- **Keystream reuse.**  The 32-bit seed space is small, and the table walk repeats with period at most 9973.  Two blocks sharing a seed share both keystreams, exposing the XOR of their plaintexts.
- **Integrity.**  The format carries no authentication or signature over the protected block; the only consistency check is that the inner payload must inflate as a valid zlib stream.

These properties are inherent to storing the key alongside the ciphertext and are documented here for interoperability and research purposes, consistent with the legal provisions cited under [Purpose](#purpose).


## Appendix A: Keystream Table

The 9973-byte keystream table `T` from [Section 3.2](#32-keystream-table-walk), encoded as base64.  Decoding yields exactly 9973 bytes; verify with:

```
SHA-256 = bfd3ff9339f28056922c167e92daffbb10668e993aef4ac7ff3dd6d662004df3
```

```

VxHG4eotYRLVIHbtbJDeA43fMOvUWA4CLoeUDUShAq/vyDcgwA==
```


## Changelog

- **1.0.0** — Initial specification of the QSPICE `.prot` / `.unprot` protected-block scheme: randomized base-16 encoding, seed-derived Mersenne Twister and table-walk keystreams, zlib decompression, and Windows-1252 keyword tokenization.

---
QSPICE is a registered trademark of Qorvo US, Inc.
