Skip to content

Commit a1feaef

Browse files
committed
add keyPair, exportPublicKey, and sharedKey to NamedGroup
1 parent cd06cc6 commit a1feaef

File tree

5 files changed

+252
-14
lines changed

5 files changed

+252
-14
lines changed

deno.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tls/enum",
3-
"version": "0.8.7",
3+
"version": "0.8.8",
44
"exports": "./src/mod.ts",
55
"publish": {
66
"exclude": ["dist/"]
@@ -18,6 +18,7 @@
1818
"@peculiar/x509": "npm:@peculiar/x509@^1.12.3",
1919
"@std/assert": "jsr:@std/assert@^1.0.2",
2020
"@tls/struct": "jsr:@tls/struct@^0.3.8",
21+
"ec-key": "npm:ec-key@^0.0.6",
2122
"elliptic": "npm:elliptic@^6.6.1"
2223
}
2324
}

src/namedgroup.js

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export class NamedGroup extends Enum {
1111
#keyGen;
1212
#privateKey;
1313
#publicKey;
14+
#keyPair;
1415

1516
/* Elliptic Curve Groups (ECDHE) */
1617
static SECP256R1 = new NamedGroup('SECP256R1', 0x0017);
@@ -124,10 +125,60 @@ export class NamedGroup extends Enum {
124125
*
125126
* @returns {KeyShareEntry} A new KeyShareEntry instance.
126127
*/
127-
keyShareEntry() {
128-
const key_exchange = KeyExchange.fromKey(this.publicKey);
128+
async keyShareEntry() {
129+
const key_exchange = KeyExchange.fromKey(await this.exportPublicKey());
129130
return new KeyShareEntry(this, key_exchange);
130131
}
132+
133+
async keyPair(){
134+
if(this.#keyPair)return this.#keyPair
135+
let algo
136+
switch (this) {
137+
case NamedGroup.X25519:{
138+
algo = "X25519"
139+
break;
140+
}
141+
case NamedGroup.SECP256R1: {
142+
algo = { name: "ECDH", namedCurve: "P-256" };
143+
break;
144+
}
145+
case NamedGroup.SECP384R1: {
146+
algo = { name: "ECDH", namedCurve: "P-384" };
147+
break;
148+
}
149+
}
150+
this.#keyPair||= await generateKey(algo);
151+
return this.#keyPair;
152+
}
153+
154+
async exportPublicKey(){
155+
const bits = await crypto.subtle.exportKey("raw", (await this.keyPair()).publicKey)
156+
return new Uint8Array(bits)
157+
}
158+
159+
async sharedKey(publicKey) {
160+
let algo, length
161+
switch (this) {
162+
case NamedGroup.X25519:
163+
algo = "X25519";
164+
length = 256;
165+
break;
166+
case NamedGroup.SECP256R1:
167+
algo = "ECDH";
168+
length = 256;
169+
break
170+
case NamedGroup.SECP384R1:
171+
algo = "ECDH";
172+
length = 384
173+
break;
174+
}
175+
const bits = await crypto.subtle.deriveBits(
176+
{ name: algo, public: publicKey },
177+
(await this.keyPair()).privateKey,
178+
length // Output key length (384 bits for P-384)
179+
)
180+
return new Uint8Array(bits)
181+
}
131182
}
132183

133184
/**
@@ -169,5 +220,11 @@ export class KeyShareEntry extends Struct {
169220
}
170221
}
171222

172-
223+
async function generateKey(algo) {
224+
return await crypto.subtle.generateKey(
225+
algo,
226+
true,
227+
["deriveKey", "deriveBits"]
228+
);
229+
}
173230
// npx -p typescript tsc ./src/namedgroup.js --declaration --allowJs --emitDeclarationOnly --lib ESNext --outDir ./dist

src/utils.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,102 @@ export function parseItems(copy, start, lengthOf, Fn) {
99
return items
1010
}
1111

12+
/**
13+
* Converts secp384r1 (P-384) private and public key bytes to JWK format
14+
*
15+
* @param {Uint8Array} privateKeyBytes - Raw private key bytes (optional)
16+
* @param {Uint8Array} publicKeyBytes - Raw public key bytes (uncompressed format with 0x04 prefix)
17+
* @returns {Object} JWK representation of the key
18+
*/
19+
export function secp384r1ToJwk(privateKeyBytes, publicKeyBytes) {
20+
// Ensure we have at least one of the keys
21+
if (!privateKeyBytes && !publicKeyBytes) {
22+
throw new Error('Either privateKeyBytes or publicKeyBytes must be provided');
23+
}
24+
25+
// Handle public key conversion
26+
const jwk = {};
27+
28+
if (publicKeyBytes) {
29+
// Public key should be in uncompressed form starting with 0x04 followed by
30+
// x and y coordinates (each 48 bytes for P-384)
31+
if (publicKeyBytes.length !== 97 || publicKeyBytes[0] !== 4) {
32+
throw new Error('Invalid public key format. Expected uncompressed format (0x04 + 96 bytes)');
33+
}
34+
35+
// Extract x and y coordinates (skipping the 0x04 prefix)
36+
const xCoord = publicKeyBytes.slice(1, 49);
37+
const yCoord = publicKeyBytes.slice(49, 97);
38+
39+
// Base64Url encode coordinates
40+
jwk.x = base64UrlEncode(xCoord);
41+
jwk.y = base64UrlEncode(yCoord);
42+
}
43+
44+
// Handle private key if provided
45+
if (privateKeyBytes) {
46+
if (privateKeyBytes.length !== 48) {
47+
throw new Error('Invalid private key length. Expected 48 bytes for P-384');
48+
}
49+
50+
jwk.d = base64UrlEncode(privateKeyBytes);
51+
}
52+
53+
// Set common parameters for secp384r1 curve
54+
jwk.kty = "EC";
55+
jwk.crv = "P-384";
56+
57+
// Set key use based on what's provided
58+
jwk.key_ops = privateKeyBytes ?
59+
["sign"] : // Private key can sign
60+
["verify"]; // Public key can verify
61+
62+
return jwk;
63+
}
64+
65+
/**
66+
* Helper function to convert bytes to base64url format
67+
*
68+
* @param {Uint8Array} bytes - Byte array to encode
69+
* @returns {string} base64url encoded string
70+
*/
71+
function base64UrlEncode(bytes) {
72+
// Convert to regular base64
73+
const base64 = btoa(String.fromCharCode.apply(null, bytes));
74+
75+
// Convert to base64url by replacing characters
76+
return base64
77+
.replace(/\+/g, '-')
78+
.replace(/\//g, '_')
79+
.replace(/=/g, '');
80+
}
81+
82+
export async function convertSecp384r1ToJWK(privateKeyBytes, publicKeyBytes) {
83+
//const crypto = globalThis.crypto || require("crypto").webcrypto; // Ensure WebCrypto API is available
84+
85+
// Import Private Key
86+
const privateKey = await crypto.subtle.importKey(
87+
"raw",
88+
privateKeyBytes,
89+
{ name: "ECDH", namedCurve: "P-384" },
90+
true,
91+
["deriveBits"]
92+
);
93+
94+
// Export Private Key to JWK
95+
const jwkPrivateKey = await crypto.subtle.exportKey("jwk", privateKey);
96+
97+
// Import Public Key
98+
const publicKey = await crypto.subtle.importKey(
99+
"raw",
100+
publicKeyBytes,
101+
{ name: "ECDH", namedCurve: "P-384" },
102+
true,
103+
[]
104+
);
105+
106+
// Export Public Key to JWK
107+
const jwkPublicKey = await crypto.subtle.exportKey("jwk", publicKey);
108+
109+
return { jwkPrivateKey, jwkPublicKey };
110+
}

test/namedgroup_test.js

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { NamedGroup } from "../src/namedgroup.js";
33
import { p384 } from "@noble/curves/p384";
44
import elliptic from "elliptic"
55
import { safeuint8array } from "../src/dep.ts";
6+
import ECKey from "ec-key"
67

78
Deno.test(
89
"NamedGroup",
@@ -22,18 +23,20 @@ Deno.test(
2223
}
2324
)
2425

26+
const x25519 = NamedGroup.X25519;
27+
28+
const x25519_keyPair = await x25519.keyPair();
29+
const x25519_publicKeyByte = await x25519.exportPublicKey();
30+
const x25519_keyShareEntry = await x25519.keyShareEntry();
31+
32+
2533
const secP384R1 = NamedGroup.SECP384R1;
26-
const pub = secP384R1.publicKey;
34+
const p384_keys = await secP384R1.keyPair();
35+
const p384_pub = await secP384R1.exportPublicKey();
2736
const pri = secP384R1.privateKey;
2837

29-
const keyPair = await crypto.subtle.generateKey(
30-
{ name: "ECDSA", namedCurve: "P-384" },
31-
true,
32-
["sign", "verify"]
33-
);
34-
35-
const publicKeyJwk = await crypto.subtle.exportKey("raw", keyPair.publicKey);
3638

39+
debugger;
3740

3841
var EC = elliptic.ec;
3942
var ec = new EC('p384');
@@ -45,10 +48,69 @@ var publicKey1 = key1.getPublic()
4548
var publicKey1Buffer = safeuint8array(0x04, publicKey1.getX().toBuffer(), publicKey1.getY().toBuffer());
4649
var publicKey2Buffer = publicKey1.encode('buffer');
4750

51+
var privateKey1 = key1.getPrivate().toBuffer();
52+
var publicKey2 = key2.getPublic()
4853

49-
var shared1 = key1.derive(key2.getPublic());
5054

5155
const priv = p384.utils.randomPrivateKey();
52-
const publ = p384.getPublicKey(priv, false)
56+
const publ = p384.getPublicKey(priv, false);
57+
const shar = p384.getSharedSecret(priv, p384_pub);
58+
const shar_p384 = await secP384R1.sharedKey(await importECPublicKey(publ));
59+
debugger;
60+
61+
//const key = ECKey;
62+
63+
const _null = null;
64+
debugger;
65+
66+
async function generateKey() {
67+
return await crypto.subtle.generateKey(
68+
{ name: "ECDH", namedCurve: "P-384" },
69+
true,
70+
["deriveKey", "deriveBits"]
71+
);
72+
}
73+
async function x25519Key() {
74+
return await crypto.subtle.generateKey(
75+
"X25519",
76+
true,
77+
["deriveKey", "deriveBits"]
78+
)
79+
}
80+
;
81+
82+
async function importEcPrivateKey(ecPrivKey) {
83+
return await crypto.subtle.importKey(
84+
"pkcs8", // Key format (PKCS#8 for private keys)
85+
ecPrivKey, // Key data (ArrayBuffer)
86+
{
87+
name: "ECDH", // Algorithm name (Elliptic Curve Diffie-Hellman)
88+
namedCurve: "P-384" // SECP384R1 curve
89+
},
90+
true, // Key is extractable (can be exported)
91+
["deriveKey", "deriveBits"] // Usages
92+
);
93+
}
94+
95+
async function importECPublicKey(keyBuffer) {
96+
// 2️⃣ Import Key
97+
return await crypto.subtle.importKey(
98+
"raw", // Public key format (SPKI)
99+
keyBuffer, // Key data (ArrayBuffer)
100+
{
101+
name: "ECDH", // Algorithm (Elliptic Curve Diffie-Hellman)
102+
namedCurve: "P-384" // SECP384R1 curve
103+
},
104+
true, // Key is extractable (can be exported)
105+
[] // No key usages (public key is used for verification/derivation)
106+
);
107+
}
53108

109+
async function deriveECKey(privateKey, publicKey) {
110+
return await crypto.subtle.deriveBits(
111+
{ name: "ECDH", public: publicKey },
112+
privateKey,
113+
384 // Output key length (384 bits for P-384)
114+
)
115+
}
54116

type/namedgroup.d.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,25 @@ export class NamedGroup extends Enum {
9393
* @returns {KeyShareEntry} A new KeyShareEntry instance.
9494
*/
9595
keyShareEntry(): KeyShareEntry;
96+
97+
/**
98+
* Generates or retrieves the key pair for this named group
99+
* @returns {Promise<CryptoKeyPair>} The generated or cached key pair
100+
*/
101+
keyPair(): Promise<CryptoKeyPair>;
102+
103+
/**
104+
* Exports the public key in raw format
105+
* @returns {Promise<Uint8Array>} The raw public key bytes
106+
*/
107+
exportPublicKey(): Promise<Uint8Array>;
108+
109+
/**
110+
* Derives a shared secret using the local private key and peer's public key
111+
* @param {CryptoKey} publicKey - The peer's public key
112+
* @returns {Promise<Uint8Array>} The derived shared secret
113+
*/
114+
sharedKey(publicKey: CryptoKey): Promise<Uint8Array>;
96115
}
97116

98117
/**

0 commit comments

Comments
 (0)