first commit
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
# `@suaveplan/license` — codec/base32
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Crockford-style Base32 codec used to serialise the packed payload and
|
||||
signature into a printable license-key string. The alphabet
|
||||
(`0-9 A-Z` minus `I`, `L`, `O`, `U`) is shared with upstream so any key
|
||||
generated by upstream parses under suaveplan and vice versa. The encoder
|
||||
inserts `-` separators every 5 characters for legibility; the decoder
|
||||
ignores them.
|
||||
|
||||
## Features
|
||||
|
||||
- 32-character Crockford alphabet, omitting `I`/`L`/`O`/`U`.
|
||||
- Dash-grouped output, `XXXXX-XXXXX-...`.
|
||||
- Case-insensitive decode (uppercase canonical).
|
||||
- Round-trips arbitrary `Uint8Array` data.
|
||||
- Raises `LicenseError(LICENSE.INVALID_ENCODING)` on any non-alphabet
|
||||
character, with the offending character in `error.context.character`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { base32Decode, base32Encode } from "@suaveplan/license/codec/base32/base32";
|
||||
|
||||
const encoded = base32Encode(new Uint8Array([1, 2, 3, 4, 5]));
|
||||
// e.g. "0426-1850-A"
|
||||
const decoded = base32Decode(encoded);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Catching invalid input:
|
||||
|
||||
```ts
|
||||
import { LicenseError } from "@suaveplan/license";
|
||||
import { base32Decode } from "@suaveplan/license/codec/base32/base32";
|
||||
|
||||
try {
|
||||
base32Decode("INVALID-CHAR-!");
|
||||
} catch (e) {
|
||||
if (e instanceof LicenseError && e.code === "LICENSE.INVALID_ENCODING") {
|
||||
console.warn("Bad character:", e.context?.character);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `base32Encode(buf: Uint8Array): string`
|
||||
|
||||
Returns the Base32 encoding of `buf`, grouped into 5-character chunks
|
||||
separated by `-`. Empty input returns the empty string.
|
||||
|
||||
### `base32Decode(str: string): Uint8Array`
|
||||
|
||||
Strips `-`, uppercases, and decodes the resulting characters. Throws
|
||||
`LicenseError(LICENSE.INVALID_ENCODING)` for any unknown character.
|
||||
Input is trimmed to a byte boundary — trailing partial-byte bits are
|
||||
discarded (matching upstream behaviour).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The implementation uses string concatenation of binary digits rather
|
||||
than bit shifts because the 5-bit-to-8-bit boundary makes manual
|
||||
shifting verbose; the perf cost is negligible for license-key-sized
|
||||
inputs (≤73 bytes). For larger inputs, prefer
|
||||
`@suaveplan/crypto-utils`' base32 implementation. The trailing-bit
|
||||
truncation in `base32Decode` matches upstream's `Math.floor(bits / 8)`
|
||||
so identical inputs decode to identical bytes across both codebases.
|
||||
@@ -0,0 +1,90 @@
|
||||
# `@suaveplan/license` — codec/bitpack
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Bit-precise pack/unpack primitives used to serialise the 72-bit license
|
||||
payload into 9 bytes. Plain `DataView` / typed-array slicing can only
|
||||
write whole bytes, so we walk the buffer bit-by-bit. Two thin classes
|
||||
hold the cursor: `SimpleBitPacker` writes MSB-first, `SimpleBitUnpacker`
|
||||
reads MSB-first. Both round-trip identically to upstream's
|
||||
`@suaveplan/license` bitpacker.
|
||||
|
||||
## Features
|
||||
|
||||
- `packUint(value, bits)` — writes an unsigned integer in `[0, 2^bits)`
|
||||
using `bits` bits; raises `LicenseError(LICENSE.PAYLOAD_OUT_OF_RANGE)`
|
||||
when the value exceeds the declared width or is negative.
|
||||
- `unpackUint(bits)` — reads the next `bits`-wide unsigned integer;
|
||||
raises `LicenseError(LICENSE.MALFORMED)` when the read would underflow
|
||||
the buffer.
|
||||
- `getBytes()` / `getBitCount()` / `getBitPosition()` introspection
|
||||
helpers used by the spec-conformance tests.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
SimpleBitPacker,
|
||||
SimpleBitUnpacker,
|
||||
} from "@suaveplan/license/codec/bitpack/bitpack";
|
||||
|
||||
const p = new SimpleBitPacker();
|
||||
p.packUint(2, 3); // 3-bit version
|
||||
p.packUint(17, 5); // 5-bit signingKeyId
|
||||
const bytes = p.getBytes();
|
||||
|
||||
const u = new SimpleBitUnpacker(bytes);
|
||||
const version = u.unpackUint(3); // 2
|
||||
const keyId = u.unpackUint(5); // 17
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Round-tripping a full 72-bit payload:
|
||||
|
||||
```ts
|
||||
import { FIELD_BITS } from "@suaveplan/license/constants/constants";
|
||||
import {
|
||||
SimpleBitPacker,
|
||||
SimpleBitUnpacker,
|
||||
} from "@suaveplan/license/codec/bitpack/bitpack";
|
||||
|
||||
const p = new SimpleBitPacker();
|
||||
for (const [_field, bits] of Object.entries(FIELD_BITS)) {
|
||||
p.packUint(0, bits);
|
||||
}
|
||||
const bytes = p.getBytes(); // 9 bytes
|
||||
|
||||
const u = new SimpleBitUnpacker(bytes);
|
||||
for (const [_field, bits] of Object.entries(FIELD_BITS)) {
|
||||
u.unpackUint(bits); // === 0
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class SimpleBitPacker`
|
||||
|
||||
- `packUint(value: number, bits: number): void` — write an unsigned
|
||||
integer; throws on overflow / negative.
|
||||
- `getBytes(): Uint8Array` — snapshot the packed bytes.
|
||||
- `getBitCount(): number` — total bits written.
|
||||
|
||||
### `class SimpleBitUnpacker`
|
||||
|
||||
- `constructor(buffer: Uint8Array)` — wrap a byte buffer.
|
||||
- `unpackUint(bits: number): number` — read the next integer; throws
|
||||
`MALFORMED` on underflow.
|
||||
- `getBitPosition(): number` — current read cursor.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The packer grows its internal `number[]` buffer one byte at a time
|
||||
because the upstream layout is small (9 bytes) and pre-sizing would
|
||||
require knowing the total bit count in advance. The `1 << bits` overflow
|
||||
check is safe for the bit widths used by the license schema (≤19 bits);
|
||||
for wider integers, callers should use a dedicated bigint bitpacker.
|
||||
@@ -0,0 +1,98 @@
|
||||
# `@suaveplan/license` — codec/obfuscation
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Per-key character obfuscation pass run after Base32 encoding. The first
|
||||
15 non-dash characters of the encoded key (which carry the bitpacked
|
||||
payload) are rotated within the Base32 alphabet by per-character offsets
|
||||
drawn from the license's signature. Two different licenses with
|
||||
different signatures produce two completely different obfuscation
|
||||
patterns, so attackers cannot pattern-match payload structure across
|
||||
keys.
|
||||
|
||||
The seed used to drive the rotation is derived from the unobfuscated
|
||||
chars 16-30 of the key (the signature region), which breaks the
|
||||
circular "decode needs the seed, the seed comes from the decode"
|
||||
dependency: the seed can be recovered from the obfuscated key alone.
|
||||
|
||||
## Features
|
||||
|
||||
- `obfuscateLicenseKey` / `deobfuscateLicenseKey` round-trip a key
|
||||
given the same seed.
|
||||
- `extractSignatureFromKey` recovers the seed from any obfuscated key
|
||||
in O(n) without touching the bitpacker.
|
||||
- Non-Base32 characters (dashes, accidental whitespace) pass through
|
||||
unchanged so the dash-grouped formatting survives obfuscation.
|
||||
- Short-key passthrough: keys with fewer than 15 non-dash characters
|
||||
return unmodified, matching upstream's "no signature region to seed
|
||||
from" fallback.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
deobfuscateLicenseKey,
|
||||
extractSignatureFromKey,
|
||||
obfuscateLicenseKey,
|
||||
} from "@suaveplan/license/codec/obfuscation/obfuscation";
|
||||
|
||||
const rawKey = "0426-1850-A8BC-NPQR-..."; // Base32 of payload || signature
|
||||
const seed = extractSignatureFromKey(rawKey);
|
||||
const obfuscated = obfuscateLicenseKey(rawKey, seed);
|
||||
const original = deobfuscateLicenseKey(obfuscated, seed);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Round-tripping with the exact scheme `generateLicense` uses:
|
||||
|
||||
```ts
|
||||
import {
|
||||
deobfuscateLicenseKey,
|
||||
extractSignatureFromKey,
|
||||
obfuscateLicenseKey,
|
||||
} from "@suaveplan/license/codec/obfuscation/obfuscation";
|
||||
|
||||
function obfuscate(rawKey: string): string {
|
||||
const seed = extractSignatureFromKey(rawKey);
|
||||
return obfuscateLicenseKey(rawKey, seed);
|
||||
}
|
||||
|
||||
function deobfuscate(obfKey: string): string {
|
||||
const seed = extractSignatureFromKey(obfKey);
|
||||
return deobfuscateLicenseKey(obfKey, seed);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `obfuscateLicenseKey(licenseKey: string, signatureBytes: Uint8Array): string`
|
||||
|
||||
Rotate the first 15 non-dash characters of `licenseKey` within
|
||||
`BASE32_ALPHABET`. Per-char offsets cycle through `signatureBytes`.
|
||||
Returns the input unchanged if it has fewer than 15 non-dash chars.
|
||||
|
||||
### `deobfuscateLicenseKey(obfuscatedKey: string, signatureBytes: Uint8Array): string`
|
||||
|
||||
Inverse of `obfuscateLicenseKey` — rotates by the negation of each
|
||||
offset.
|
||||
|
||||
### `extractSignatureFromKey(obfuscatedKey: string): Uint8Array`
|
||||
|
||||
Recover up to 15 seed bytes from chars 16-30 of the obfuscated key by
|
||||
reading their Base32-alphabet indices (0-31). Returns an empty array
|
||||
when the key has no signature region.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The rotation arithmetic uses double-modulo (`((x % len) + len) % len`)
|
||||
so negative offsets (from `deobfuscateLicenseKey`) wrap correctly.
|
||||
Non-Base32 characters return early in `rotateChar`, leaving dashes and
|
||||
any defensive whitespace intact — this matches upstream's
|
||||
permissive-pass behaviour and means an externally re-formatted key
|
||||
(e.g. paste with extra spaces) still deobfuscates correctly once the
|
||||
spaces are stripped by the Base32 decoder.
|
||||
@@ -0,0 +1,89 @@
|
||||
# `@suaveplan/license` — codec/payload
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`packPayload` and `unpackPayload` are the 72-bit payload codec on top of
|
||||
the bitpack primitives. They emit exactly 9 bytes (the binary contract
|
||||
shared with upstream `@suaveplan/license`) and walk the 9 schema fields
|
||||
in the canonical order declared by `FIELD_BITS`.
|
||||
|
||||
`packPayload` raises `LicenseError(LICENSE.PAYLOAD_OUT_OF_RANGE)` when a
|
||||
field exceeds its declared bit width (via the underlying bitpacker).
|
||||
`unpackPayload` raises `LicenseError(LICENSE.MALFORMED)` for any input
|
||||
length other than `PAYLOAD_BYTES` (9).
|
||||
|
||||
## Features
|
||||
|
||||
- Bit-for-bit wire compatibility with upstream's `packPayload` /
|
||||
`unpackPayload`.
|
||||
- Field-bound validation via `SimpleBitPacker.packUint`, surfaced as
|
||||
typed `LicenseError`s.
|
||||
- Strict 9-byte length invariant on both sides — no implicit truncation
|
||||
or padding.
|
||||
- Returns a fresh `Uint8Array`; safe to mutate the caller's buffer.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
packPayload,
|
||||
unpackPayload,
|
||||
} from "@suaveplan/license/codec/payload/payload";
|
||||
import { PackageTier, TimeEncodingType } from "@suaveplan/license/types/license";
|
||||
|
||||
const bytes = packPayload({
|
||||
version: 2,
|
||||
signingKeyId: 0,
|
||||
productId: 1,
|
||||
packageTier: PackageTier.PRO,
|
||||
flags: 0,
|
||||
timeEncodingType: TimeEncodingType.NONE,
|
||||
timeValue: 0,
|
||||
featureMask: 0,
|
||||
nonce: 0x123,
|
||||
});
|
||||
console.log(bytes.length); // 9
|
||||
const round = unpackPayload(bytes);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Catching a field-overflow at the generation boundary:
|
||||
|
||||
```ts
|
||||
import { LicenseError } from "@suaveplan/license";
|
||||
import { packPayload } from "@suaveplan/license/codec/payload/payload";
|
||||
|
||||
try {
|
||||
packPayload({ /* ...valid */ productId: 64 } as never); // 6 bits, max 63
|
||||
} catch (err) {
|
||||
if (err instanceof LicenseError && err.code === "LICENSE.PAYLOAD_OUT_OF_RANGE") {
|
||||
// surface to the user as a form-validation failure
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `packPayload(payload: LicensePayload): Uint8Array`
|
||||
|
||||
Pack a payload to its 9-byte representation. Throws
|
||||
`LicenseError(LICENSE.PAYLOAD_OUT_OF_RANGE)` on field overflow.
|
||||
|
||||
### `unpackPayload(data: Uint8Array): LicensePayload`
|
||||
|
||||
Unpack a 9-byte buffer. Throws `LicenseError(LICENSE.MALFORMED)` for
|
||||
any other length.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The unpacker casts `timeEncodingType` to the `TimeEncodingType` enum
|
||||
even though the bit width (2) admits any value in `[0, 3]` — all four
|
||||
values map to legitimate enum members, so the cast is safe. The
|
||||
constant `PAYLOAD_BYTES = 9` lives in `constants/constants.ts` so any
|
||||
future schema change (which would require a `LICENSE_VERSION` bump)
|
||||
updates a single source of truth.
|
||||
Reference in New Issue
Block a user