Skip to content

Commit 73055af

Browse files
committed
Update PEM file creation and parsing
1 parent 4913876 commit 73055af

File tree

4 files changed

+107
-38
lines changed

4 files changed

+107
-38
lines changed

src/types/keypair/PrivateKey.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,15 @@ export class PrivateKey {
119119
* @param algorithm - The cryptographic algorithm to use.
120120
* @returns A promise resolving to a PrivateKey instance.
121121
*/
122-
public static async fromPem(
122+
public static fromPem(
123123
content: string,
124124
algorithm: KeyAlgorithm
125-
): Promise<PrivateKey> {
126-
const priv = await PrivateKeyFactory.createPrivateKeyFromPem(
125+
): PrivateKey {
126+
const priv = PrivateKeyFactory.createPrivateKeyFromPem(
127127
content,
128128
algorithm
129129
);
130-
const pubBytes = await priv.publicKeyBytes();
130+
const pubBytes = priv.publicKeyBytes();
131131
const algBytes = Uint8Array.of(algorithm);
132132
const pub = PublicKey.fromBuffer(concat([algBytes, pubBytes]));
133133
return new PrivateKey(algorithm, pub, priv);
@@ -185,10 +185,10 @@ class PrivateKeyFactory {
185185
* @returns A promise resolving to a PrivateKeyInternal instance.
186186
* @throws Error if the algorithm is unsupported.
187187
*/
188-
public static async createPrivateKeyFromPem(
188+
public static createPrivateKeyFromPem(
189189
content: string,
190190
algorithm: KeyAlgorithm
191-
): Promise<PrivateKeyInternal> {
191+
): PrivateKeyInternal {
192192
switch (algorithm) {
193193
case KeyAlgorithm.ED25519:
194194
return Ed25519PrivateKey.fromPem(content);

src/types/keypair/ed25519/PrivateKey.ts

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import * as ed25519 from '@noble/ed25519';
2-
import { PrivateKeyInternal } from "../PrivateKey";
2+
import { PrivateKeyInternal } from '../PrivateKey';
33
import { sha512 } from '@noble/hashes/sha512';
4+
import { Conversions } from '../../Conversions';
5+
import { readBase64WithPEM } from '../utils';
46

57
ed25519.utils.sha512Sync = (...m) => sha512(ed25519.utils.concatBytes(...m));
68

9+
const ED25519_PEM_SECRET_KEY_TAG = 'PRIVATE KEY';
10+
711
/**
812
* Represents an Ed25519 private key, supporting key generation, signing, and PEM encoding.
913
* Provides methods for creating instances from byte arrays, hexadecimal strings, and PEM format.
@@ -89,13 +93,33 @@ export class PrivateKey implements PrivateKeyInternal {
8993
* @returns A PEM-encoded string of the private key.
9094
*/
9195
toPem(): string {
92-
const seed = this.key.slice(0, 32);
93-
94-
const prefix = Buffer.alloc(PrivateKey.PemFramePrivateKeyPrefixSize);
95-
const fullKey = Buffer.concat([prefix, Buffer.from(seed)]);
96-
97-
const pemString = fullKey.toString('base64');
98-
return `-----BEGIN PRIVATE KEY-----\n${pemString}\n-----END PRIVATE KEY-----`;
96+
const derPrefix = Buffer.from([
97+
48,
98+
46,
99+
2,
100+
1,
101+
0,
102+
48,
103+
5,
104+
6,
105+
3,
106+
43,
107+
101,
108+
112,
109+
4,
110+
34,
111+
4,
112+
32
113+
]);
114+
const encoded = Conversions.encodeBase64(
115+
Buffer.concat([derPrefix, Buffer.from(this.key)])
116+
);
117+
118+
return (
119+
`-----BEGIN ${ED25519_PEM_SECRET_KEY_TAG}-----\n` +
120+
`${encoded}\n` +
121+
`-----END ${ED25519_PEM_SECRET_KEY_TAG}-----\n`
122+
);
99123
}
100124

101125
/**
@@ -106,18 +130,27 @@ export class PrivateKey implements PrivateKeyInternal {
106130
* @throws Error if the content cannot be properly parsed.
107131
*/
108132
static fromPem(content: string): PrivateKey {
109-
const base64Content = content
110-
.replace('-----BEGIN PRIVATE KEY-----', '')
111-
.replace('-----END PRIVATE KEY-----', '')
112-
.replace(/\n/g, '');
113-
const fullKey = Buffer.from(base64Content, 'base64');
133+
const privateKeyBytes = readBase64WithPEM(content);
114134

115-
const data = fullKey.slice(PrivateKey.PemFramePrivateKeyPrefixSize);
135+
return new PrivateKey(
136+
new Uint8Array(Buffer.from(PrivateKey.parsePrivateKey(privateKeyBytes)))
137+
);
138+
}
116139

117-
const seed = data.slice(-32);
118-
const privateEdDSA = ed25519.utils.randomPrivateKey();
119-
privateEdDSA.set(seed);
140+
private static parsePrivateKey(bytes: Uint8Array) {
141+
const len = bytes.length;
142+
143+
// prettier-ignore
144+
const key =
145+
(len === 32) ? bytes :
146+
(len === 64) ? Buffer.from(bytes).slice(0, 32) :
147+
(len > 32 && len < 64) ? Buffer.from(bytes).slice(len % 32) :
148+
null;
149+
150+
if (key == null || key.length !== 32) {
151+
throw Error(`Unexpected key length: ${len}`);
152+
}
120153

121-
return new PrivateKey(privateEdDSA);
154+
return key;
122155
}
123156
}

src/types/keypair/secp256k1/PrivateKey.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import * as secp256k1 from '@noble/secp256k1';
22
import { sha256 } from '@noble/hashes/sha256';
33
import { hmac } from '@noble/hashes/hmac';
4-
import { PrivateKeyInternal } from "../PrivateKey";
4+
import { PrivateKeyInternal } from '../PrivateKey';
5+
import KeyEncoder from 'key-encoder';
6+
import { Conversions } from '../../Conversions';
7+
import { readBase64WithPEM } from '../utils';
58

69
secp256k1.utils.hmacSha256Sync = (k, ...m) =>
710
hmac(sha256, k, secp256k1.utils.concatBytes(...m));
811

9-
/** PEM prefix for a private key. */
10-
const PemPrivateKeyPrefix = '-----BEGIN PRIVATE KEY-----';
11-
12-
/** PEM suffix for a private key. */
13-
const PemPrivateKeySuffix = '-----END PRIVATE KEY-----';
12+
const keyEncoder = new KeyEncoder('secp256k1');
1413

1514
/**
1615
* Represents a secp256k1 private key, supporting key generation, signing, and PEM encoding.
@@ -111,8 +110,11 @@ export class PrivateKey implements PrivateKeyInternal {
111110
* @returns A PEM-encoded string of the private key.
112111
*/
113112
toPem(): string {
114-
const keyBase64 = Buffer.from(this.key).toString('base64');
115-
return `${PemPrivateKeyPrefix}\n${keyBase64}\n${PemPrivateKeySuffix}`;
113+
return keyEncoder.encodePrivate(
114+
Conversions.encodeBase16(this.key),
115+
'raw',
116+
'pem'
117+
);
116118
}
117119

118120
/**
@@ -122,11 +124,14 @@ export class PrivateKey implements PrivateKeyInternal {
122124
* @throws Error if the content cannot be properly parsed.
123125
*/
124126
static fromPem(content: string): PrivateKey {
125-
const base64Key = content
126-
.replace(PemPrivateKeyPrefix, '')
127-
.replace(PemPrivateKeySuffix, '')
128-
.replace(/\s+/g, '');
129-
const keyBuffer = Buffer.from(base64Key, 'base64');
130-
return new PrivateKey(new Uint8Array(keyBuffer));
127+
const privateKeyBytes = readBase64WithPEM(content);
128+
129+
const rawKeyHex = keyEncoder.encodePrivate(
130+
Buffer.from(privateKeyBytes),
131+
'der',
132+
'raw'
133+
);
134+
135+
return new PrivateKey(new Uint8Array(Buffer.from(rawKeyHex, 'hex')));
131136
}
132137
}

src/types/keypair/utils.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Conversions } from '../Conversions';
2+
3+
/**
4+
* Reads in a base64 private key, ignoring the header: `-----BEGIN PUBLIC KEY-----`
5+
* and footer: `-----END PUBLIC KEY-----`
6+
* @param {string} content A .pem private key string with a header and footer
7+
* @returns A base64 private key as a `Uint8Array`
8+
* @remarks
9+
* If the provided base64 `content` string does not include a header/footer,
10+
* it will pass through this function unaffected
11+
* @example
12+
* Example PEM:
13+
*
14+
* ```
15+
* -----BEGIN PUBLIC KEY-----\r\n
16+
* MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEj1fgdbpNbt06EY/8C+wbBXq6VvG+vCVD\r\n
17+
* Nl74LvVAmXfpdzCWFKbdrnIlX3EFDxkd9qpk35F/kLcqV3rDn/u3dg==\r\n
18+
* -----END PUBLIC KEY-----\r\n
19+
* ```
20+
*/
21+
export function readBase64WithPEM(content: string): Uint8Array {
22+
const base64 = content
23+
// there are two kinks of line-endings, CRLF(\r\n) and LF(\n)
24+
// we need handle both
25+
.split(/\r?\n/)
26+
.filter(x => !x.startsWith('---'))
27+
.join('')
28+
// remove the line-endings in the end of content
29+
.trim();
30+
return Conversions.decodeBase64(base64);
31+
}

0 commit comments

Comments
 (0)