Skip to content

Commit 296cb3e

Browse files
authored
feat(pq-key-encoder): add post-quantum key encoding utilities (#3)
1 parent 11a98d3 commit 296cb3e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+2624
-10
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Node
22
node_modules/
33
dist/
4+
*.tsbuildinfo
45
package-lock.json
56
bun.lockb
67

Lines changed: 143 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# pq-key-encoder
22

3-
Post-quantum key encoding utilities.
3+
Post-quantum key encoding utilities for NIST PQC algorithms.
44

55
## Installation
66

@@ -10,12 +10,152 @@ npm install pq-key-encoder
1010

1111
## Usage
1212

13+
### Basic Encoding/Decoding
14+
15+
```typescript
16+
import {
17+
fromDER,
18+
fromPEM,
19+
fromJWK,
20+
fromSPKI,
21+
fromPKCS8,
22+
toDER,
23+
toPEM,
24+
toJWK,
25+
toSPKI,
26+
toPKCS8,
27+
type KeyData,
28+
type PQJwk,
29+
} from 'pq-key-encoder';
30+
31+
// KeyData is the core type: { alg, type, bytes }
32+
const publicKey: KeyData = {
33+
alg: 'ML-KEM-768',
34+
type: 'public',
35+
bytes: publicKeyBytes, // Uint8Array from key generation
36+
};
37+
38+
const privateKey: KeyData = {
39+
alg: 'ML-KEM-768',
40+
type: 'private',
41+
bytes: privateKeyBytes,
42+
};
43+
```
44+
45+
### DER (SPKI / PKCS8)
46+
47+
```typescript
48+
// Encode to DER (auto-selects SPKI for public, PKCS8 for private)
49+
const publicDer = toDER(publicKey);
50+
const privateDer = toDER(privateKey);
51+
52+
// Decode from DER (auto-detects key type)
53+
const parsedPublic = fromDER(publicDer);
54+
const parsedPrivate = fromDER(privateDer);
55+
56+
// Explicit SPKI/PKCS8 functions
57+
const spki = toSPKI(publicKey);
58+
const pkcs8 = toPKCS8(privateKey);
59+
const fromSpki = fromSPKI(spki);
60+
const fromPkcs8 = fromPKCS8(pkcs8);
61+
```
62+
63+
### PEM
64+
1365
```typescript
14-
import { } from 'pq-key-encoder';
66+
// Encode to PEM (PUBLIC KEY / PRIVATE KEY labels)
67+
const publicPem = toPEM(publicKey);
68+
const privatePem = toPEM(privateKey);
1569

16-
// Coming soon
70+
// Decode from PEM
71+
const parsedPublicPem = fromPEM(publicPem);
72+
const parsedPrivatePem = fromPEM(privatePem);
1773
```
1874

75+
### JWK
76+
77+
```typescript
78+
// Public key to JWK
79+
const publicJwk = toJWK(publicKey);
80+
// { kty: 'PQC', alg: 'ML-KEM-768', x: '<base64url>' }
81+
82+
// Private key to JWK (requires public key bytes)
83+
const privateJwk = toJWK(privateKey, {
84+
includePrivate: true,
85+
publicKey: publicKey.bytes,
86+
});
87+
// { kty: 'PQC', alg: 'ML-KEM-768', x: '<base64url>', d: '<base64url>' }
88+
89+
// Decode from JWK
90+
const fromPublicJwk = fromJWK(publicJwk);
91+
const fromPrivateJwk = fromJWK(privateJwk);
92+
```
93+
94+
## Supported Algorithms
95+
96+
Algorithms are validated against `pq-oid` metadata (OID + key sizes):
97+
98+
| Family | Algorithms |
99+
|--------|------------|
100+
| ML-KEM | ML-KEM-512, ML-KEM-768, ML-KEM-1024 |
101+
| ML-DSA | ML-DSA-44, ML-DSA-65, ML-DSA-87 |
102+
| SLH-DSA | SHA2/SHAKE variants (128s/f, 192s/f, 256s/f) |
103+
104+
## Error Handling
105+
106+
```typescript
107+
import {
108+
KeyEncoderError,
109+
InvalidInputError,
110+
InvalidEncodingError,
111+
UnsupportedAlgorithmError,
112+
KeySizeMismatchError,
113+
} from 'pq-key-encoder';
114+
115+
try {
116+
const key = fromDER(malformedData);
117+
} catch (error) {
118+
if (error instanceof KeySizeMismatchError) {
119+
console.error(`Expected ${error.expected} bytes, got ${error.actual}`);
120+
} else if (error instanceof UnsupportedAlgorithmError) {
121+
console.error('Unknown algorithm OID');
122+
} else if (error instanceof InvalidEncodingError) {
123+
console.error('Malformed DER structure');
124+
}
125+
}
126+
```
127+
128+
## Types
129+
130+
```typescript
131+
type AlgorithmName = 'ML-KEM-512' | 'ML-KEM-768' | ... ;
132+
type KeyType = 'public' | 'private';
133+
134+
interface KeyData {
135+
alg: AlgorithmName;
136+
type: KeyType;
137+
bytes: Uint8Array;
138+
}
139+
140+
interface PQJwk {
141+
kty: 'PQC';
142+
alg: AlgorithmName;
143+
x: string; // base64url public key
144+
d?: string; // base64url private key (optional)
145+
}
146+
```
147+
148+
## Notes
149+
150+
- Inputs are validated for correct key size and encoding structure
151+
- DER encoding uses AlgorithmIdentifier with absent parameters (per NIST PQ specs); decoder accepts both absent and NULL for interoperability
152+
- JWK uses non-standard `kty: 'PQC'` for post-quantum keys
153+
- `fromJWK` returns only private key bytes when both `x` and `d` are present (public key is validated but not returned). To get both keys, parse separately:
154+
```typescript
155+
const privateKey = fromJWK(jwk);
156+
const publicKey = fromJWK({ kty: jwk.kty, alg: jwk.alg, x: jwk.x });
157+
```
158+
19159
## License
20160

21161
MIT

packages/pq-key-encoder/ts/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
"dist"
1010
],
1111
"scripts": {
12-
"build": "tsc",
13-
"prepublishOnly": "npm run build"
12+
"build": "tsc -b",
13+
"prepublishOnly": "npm run build",
14+
"test": "bun test"
1415
},
1516
"keywords": [
1617
"post-quantum",
@@ -20,6 +21,9 @@
2021
],
2122
"author": "",
2223
"license": "MIT",
24+
"dependencies": {
25+
"pq-oid": "0.0.1"
26+
},
2327
"devDependencies": {
2428
"typescript": "^5.0.0"
2529
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { OID } from 'pq-oid';
2+
import type { AlgorithmName } from '../types';
3+
import { encodeObjectIdentifier, encodeSequence } from './primitives';
4+
5+
/** Build an AlgorithmIdentifier SEQUENCE for a PQ algorithm. */
6+
export function encodeAlgorithmIdentifier(alg: AlgorithmName): Uint8Array {
7+
const oid = OID.fromName(alg);
8+
return encodeSequence([encodeObjectIdentifier(oid)]);
9+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { InvalidEncodingError, InvalidInputError } from '../errors';
2+
3+
export type DecodedLength = {
4+
length: number;
5+
bytesRead: number;
6+
};
7+
8+
/** Ensure the length is a valid non-negative integer. */
9+
function assertValidLength(length: number): void {
10+
if (!Number.isInteger(length) || length < 0) {
11+
throw new InvalidInputError('Length must be a non-negative integer.');
12+
}
13+
}
14+
15+
/** Encode a length using DER short/long form. */
16+
export function encodeLength(length: number): Uint8Array {
17+
assertValidLength(length);
18+
19+
if (length < 0x80) {
20+
return Uint8Array.of(length);
21+
}
22+
23+
const bytes: number[] = [];
24+
let remaining = length;
25+
while (remaining > 0) {
26+
bytes.unshift(remaining & 0xff);
27+
remaining = Math.floor(remaining / 256);
28+
}
29+
30+
return Uint8Array.from([0x80 | bytes.length, ...bytes]);
31+
}
32+
33+
/** Decode a DER length value from a byte array. */
34+
export function decodeLength(input: Uint8Array, offset = 0): DecodedLength {
35+
if (offset < 0 || offset >= input.length) {
36+
throw new InvalidEncodingError('Missing length octet.');
37+
}
38+
39+
const first = input[offset];
40+
if (first === 0x80) {
41+
throw new InvalidEncodingError('Indefinite length encoding is not allowed in DER.');
42+
}
43+
44+
if ((first & 0x80) === 0) {
45+
return { length: first, bytesRead: 1 };
46+
}
47+
48+
const lengthBytes = first & 0x7f;
49+
if (lengthBytes === 0) {
50+
throw new InvalidEncodingError('Indefinite length encoding is not allowed in DER.');
51+
}
52+
53+
const start = offset + 1;
54+
const end = start + lengthBytes;
55+
if (end > input.length) {
56+
throw new InvalidEncodingError('Truncated length encoding.');
57+
}
58+
59+
if (input[start] === 0x00) {
60+
throw new InvalidEncodingError('Length encoding must be minimal.');
61+
}
62+
63+
if (lengthBytes > 6) {
64+
throw new InvalidEncodingError('Length encoding exceeds safe integer range.');
65+
}
66+
67+
let length = 0;
68+
for (let i = start; i < end; i += 1) {
69+
length = length * 256 + input[i];
70+
}
71+
72+
if (length < 0x80) {
73+
throw new InvalidEncodingError('Length encoding must use short form.');
74+
}
75+
76+
return { length, bytesRead: 1 + lengthBytes };
77+
}

0 commit comments

Comments
 (0)