Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Node
node_modules/
dist/
*.tsbuildinfo
package-lock.json
bun.lockb

Expand Down
146 changes: 143 additions & 3 deletions packages/pq-key-encoder/ts/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# pq-key-encoder

Post-quantum key encoding utilities.
Post-quantum key encoding utilities for NIST PQC algorithms.

## Installation

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

## Usage

### Basic Encoding/Decoding

```typescript
import {
fromDER,
fromPEM,
fromJWK,
fromSPKI,
fromPKCS8,
toDER,
toPEM,
toJWK,
toSPKI,
toPKCS8,
type KeyData,
type PQJwk,
} from 'pq-key-encoder';

// KeyData is the core type: { alg, type, bytes }
const publicKey: KeyData = {
alg: 'ML-KEM-768',
type: 'public',
bytes: publicKeyBytes, // Uint8Array from key generation
};

const privateKey: KeyData = {
alg: 'ML-KEM-768',
type: 'private',
bytes: privateKeyBytes,
};
```

### DER (SPKI / PKCS8)

```typescript
// Encode to DER (auto-selects SPKI for public, PKCS8 for private)
const publicDer = toDER(publicKey);
const privateDer = toDER(privateKey);

// Decode from DER (auto-detects key type)
const parsedPublic = fromDER(publicDer);
const parsedPrivate = fromDER(privateDer);

// Explicit SPKI/PKCS8 functions
const spki = toSPKI(publicKey);
const pkcs8 = toPKCS8(privateKey);
const fromSpki = fromSPKI(spki);
const fromPkcs8 = fromPKCS8(pkcs8);
```

### PEM

```typescript
import { } from 'pq-key-encoder';
// Encode to PEM (PUBLIC KEY / PRIVATE KEY labels)
const publicPem = toPEM(publicKey);
const privatePem = toPEM(privateKey);

// Coming soon
// Decode from PEM
const parsedPublicPem = fromPEM(publicPem);
const parsedPrivatePem = fromPEM(privatePem);
```

### JWK

```typescript
// Public key to JWK
const publicJwk = toJWK(publicKey);
// { kty: 'PQC', alg: 'ML-KEM-768', x: '<base64url>' }

// Private key to JWK (requires public key bytes)
const privateJwk = toJWK(privateKey, {
includePrivate: true,
publicKey: publicKey.bytes,
});
// { kty: 'PQC', alg: 'ML-KEM-768', x: '<base64url>', d: '<base64url>' }

// Decode from JWK
const fromPublicJwk = fromJWK(publicJwk);
const fromPrivateJwk = fromJWK(privateJwk);
```

## Supported Algorithms

Algorithms are validated against `pq-oid` metadata (OID + key sizes):

| Family | Algorithms |
|--------|------------|
| ML-KEM | ML-KEM-512, ML-KEM-768, ML-KEM-1024 |
| ML-DSA | ML-DSA-44, ML-DSA-65, ML-DSA-87 |
| SLH-DSA | SHA2/SHAKE variants (128s/f, 192s/f, 256s/f) |

## Error Handling

```typescript
import {
KeyEncoderError,
InvalidInputError,
InvalidEncodingError,
UnsupportedAlgorithmError,
KeySizeMismatchError,
} from 'pq-key-encoder';

try {
const key = fromDER(malformedData);
} catch (error) {
if (error instanceof KeySizeMismatchError) {
console.error(`Expected ${error.expected} bytes, got ${error.actual}`);
} else if (error instanceof UnsupportedAlgorithmError) {
console.error('Unknown algorithm OID');
} else if (error instanceof InvalidEncodingError) {
console.error('Malformed DER structure');
}
}
```

## Types

```typescript
type AlgorithmName = 'ML-KEM-512' | 'ML-KEM-768' | ... ;
type KeyType = 'public' | 'private';

interface KeyData {
alg: AlgorithmName;
type: KeyType;
bytes: Uint8Array;
}

interface PQJwk {
kty: 'PQC';
alg: AlgorithmName;
x: string; // base64url public key
d?: string; // base64url private key (optional)
}
```

## Notes

- Inputs are validated for correct key size and encoding structure
- DER encoding uses AlgorithmIdentifier with absent parameters (per NIST PQ specs); decoder accepts both absent and NULL for interoperability
- JWK uses non-standard `kty: 'PQC'` for post-quantum keys
- `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:
```typescript
const privateKey = fromJWK(jwk);
const publicKey = fromJWK({ kty: jwk.kty, alg: jwk.alg, x: jwk.x });
```

## License

MIT
8 changes: 6 additions & 2 deletions packages/pq-key-encoder/ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
"dist"
],
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
"build": "tsc -b",
"prepublishOnly": "npm run build",
"test": "bun test"
},
"keywords": [
"post-quantum",
Expand All @@ -20,6 +21,9 @@
],
"author": "",
"license": "MIT",
"dependencies": {
"pq-oid": "0.0.1"
},
"devDependencies": {
"typescript": "^5.0.0"
}
Expand Down
9 changes: 9 additions & 0 deletions packages/pq-key-encoder/ts/src/asn1/algorithm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { OID } from 'pq-oid';
import type { AlgorithmName } from '../types';
import { encodeObjectIdentifier, encodeSequence } from './primitives';

/** Build an AlgorithmIdentifier SEQUENCE for a PQ algorithm. */
export function encodeAlgorithmIdentifier(alg: AlgorithmName): Uint8Array {
const oid = OID.fromName(alg);
return encodeSequence([encodeObjectIdentifier(oid)]);
}
77 changes: 77 additions & 0 deletions packages/pq-key-encoder/ts/src/asn1/length.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { InvalidEncodingError, InvalidInputError } from '../errors';

export type DecodedLength = {
length: number;
bytesRead: number;
};

/** Ensure the length is a valid non-negative integer. */
function assertValidLength(length: number): void {
if (!Number.isInteger(length) || length < 0) {
throw new InvalidInputError('Length must be a non-negative integer.');
}
}

/** Encode a length using DER short/long form. */
export function encodeLength(length: number): Uint8Array {
assertValidLength(length);

if (length < 0x80) {
return Uint8Array.of(length);
}

const bytes: number[] = [];
let remaining = length;
while (remaining > 0) {
bytes.unshift(remaining & 0xff);
remaining = Math.floor(remaining / 256);
}

return Uint8Array.from([0x80 | bytes.length, ...bytes]);
}

/** Decode a DER length value from a byte array. */
export function decodeLength(input: Uint8Array, offset = 0): DecodedLength {
if (offset < 0 || offset >= input.length) {
throw new InvalidEncodingError('Missing length octet.');
}

const first = input[offset];
if (first === 0x80) {
throw new InvalidEncodingError('Indefinite length encoding is not allowed in DER.');
}

if ((first & 0x80) === 0) {
return { length: first, bytesRead: 1 };
}

const lengthBytes = first & 0x7f;
if (lengthBytes === 0) {
throw new InvalidEncodingError('Indefinite length encoding is not allowed in DER.');
}

const start = offset + 1;
const end = start + lengthBytes;
if (end > input.length) {
throw new InvalidEncodingError('Truncated length encoding.');
}

if (input[start] === 0x00) {
throw new InvalidEncodingError('Length encoding must be minimal.');
}

if (lengthBytes > 6) {
throw new InvalidEncodingError('Length encoding exceeds safe integer range.');
}

let length = 0;
for (let i = start; i < end; i += 1) {
length = length * 256 + input[i];
}

if (length < 0x80) {
throw new InvalidEncodingError('Length encoding must use short form.');
}

return { length, bytesRead: 1 + lengthBytes };
}
Loading