Skip to content

Commit 09a9e9e

Browse files
committed
chore: wip
1 parent 0ba8d48 commit 09a9e9e

10 files changed

Lines changed: 613 additions & 943 deletions

File tree

.vscode/dictionary.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ codecov
1717
commitlint
1818
commitlintrc
1919
composables
20+
csrs
2021
davidanson
2122
dbaeumer
2223
degit

packages/pem/src/pem.ts

Lines changed: 236 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,255 @@
1+
import { decode64, encode64 } from 'ts-security-utils'
2+
import type { PEMMessage, PEMHeader, ProcType, DEKInfo, PEMEncodeOptions } from './types'
3+
4+
/**
5+
* TypeScript implementation of basic PEM (Privacy Enhanced Mail) algorithms.
6+
*
7+
* See: RFC 1421.
8+
*
9+
* @author Dave Longley
10+
* @author Chris Breuer
11+
*
12+
* A PEM object has the following fields:
13+
*
14+
* type: identifies the type of message (eg: "RSA PRIVATE KEY").
15+
*
16+
* procType: identifies the type of processing performed on the message,
17+
* it has two subfields: version and type, eg: 4,ENCRYPTED.
18+
*
19+
* contentDomain: identifies the type of content in the message, typically
20+
* only uses the value: "RFC822".
21+
*
22+
* dekInfo: identifies the message encryption algorithm and mode and includes
23+
* any parameters for the algorithm, it has two subfields: algorithm and
24+
* parameters, eg: DES-CBC,F8143EDE5960C597.
25+
*
26+
* headers: contains all other PEM encapsulated headers -- where order is
27+
* significant (for pairing data like recipient ID + key info).
28+
*
29+
* body: the binary-encoded body.
30+
*/
31+
132
/**
2-
* Converts an RSA private key from PEM format.
33+
* Encodes (serializes) the given PEM object.
334
*
4-
* @param pem the PEM-formatted private key.
35+
* @param msg the PEM message object to encode.
36+
* @param options the options to use: maxline the maximum characters per line for the body, (default: 64).
537
*
6-
* @return the private key.
38+
* @return the PEM-formatted string.
739
*/
8-
export function privateKeyFromPem(pem: string) {
9-
var msg = forge.pem.decode(pem)[0];
40+
export function encode(msg: PEMMessage, options: PEMEncodeOptions = {}): string {
41+
let rval = `-----BEGIN ${msg.type}-----\r\n`;
42+
43+
// encode special headers
44+
let header: PEMHeader;
45+
if (msg.procType) {
46+
header = {
47+
name: 'Proc-Type',
48+
values: [String(msg.procType.version), msg.procType.type]
49+
};
50+
rval += foldHeader(header);
51+
}
52+
if (msg.contentDomain) {
53+
header = { name: 'Content-Domain', values: [msg.contentDomain] };
54+
rval += foldHeader(header);
55+
}
56+
if (msg.dekInfo) {
57+
header = { name: 'DEK-Info', values: [msg.dekInfo.algorithm] };
58+
if (msg.dekInfo.parameters) {
59+
header.values.push(msg.dekInfo.parameters);
60+
}
61+
rval += foldHeader(header);
62+
}
1063

11-
if (msg.type !== 'PRIVATE KEY' && msg.type !== 'RSA PRIVATE KEY') {
12-
var error = new Error('Could not convert private key from PEM; PEM ' +
13-
'header type is not "PRIVATE KEY" or "RSA PRIVATE KEY".');
14-
error.headerType = msg.type;
15-
throw error;
64+
if (msg.headers) {
65+
// encode all other headers
66+
for (let i = 0; i < msg.headers.length; ++i) {
67+
rval += foldHeader(msg.headers[i]);
68+
}
1669
}
17-
if (msg.procType && msg.procType.type === 'ENCRYPTED') {
18-
throw new Error('Could not convert private key from PEM; PEM is encrypted.');
70+
71+
// terminate header
72+
if (msg.procType) {
73+
rval += '\r\n';
1974
}
2075

21-
// convert DER to ASN.1 object
22-
var obj = asn1.fromDer(msg.body);
76+
// add body
77+
rval += encode64(msg.body as unknown as string, options.maxline || 64) + '\r\n';
2378

24-
return pki.privateKeyFromAsn1(obj);
25-
};
79+
rval += `-----END ${msg.type}-----\r\n`;
80+
return rval;
81+
}
2682

2783
/**
28-
* Converts an RSA private key to PEM format.
84+
* Decodes (deserializes) all PEM messages found in the given string.
2985
*
30-
* @param key the private key.
31-
* @param maxline the maximum characters per line, defaults to 64.
86+
* @param str the PEM-formatted string to decode.
3287
*
33-
* @return the PEM-formatted private key.
88+
* @return the PEM message objects in an array.
3489
*/
35-
export function privateKeyToPem(key: any, maxline: number) {
36-
// convert to ASN.1, then DER, then PEM-encode
37-
var msg = {
38-
type: 'RSA PRIVATE KEY',
39-
body: asn1.toDer(pki.privateKeyToAsn1(key)).getBytes()
40-
};
41-
return forge.pem.encode(msg, { maxline: maxline });
42-
};
90+
export function decode(str: string): PEMMessage[] {
91+
const rval: PEMMessage[] = [];
92+
93+
// split string into PEM messages (be lenient w/EOF on BEGIN line)
94+
const rMessage = /\s*-----BEGIN ([A-Z0-9- ]+)-----\r?\n?([\x21-\x7e\s]+?(?:\r?\n\r?\n))?([:A-Za-z0-9+\/=\s]+?)-----END \1-----/g;
95+
const rHeader = /([\x21-\x7e]+):\s*([\x21-\x7e\s^:]+)/;
96+
const rCRLF = /\r?\n/;
97+
let match: RegExpExecArray | null;
98+
99+
while ((match = rMessage.exec(str)) !== null) {
100+
// accept "NEW CERTIFICATE REQUEST" as "CERTIFICATE REQUEST"
101+
// https://datatracker.ietf.org/doc/html/rfc7468#section-7
102+
let type = match[1];
103+
if (type === 'NEW CERTIFICATE REQUEST') {
104+
type = 'CERTIFICATE REQUEST';
105+
}
106+
107+
const msg: PEMMessage = {
108+
type: type,
109+
procType: null,
110+
contentDomain: null,
111+
dekInfo: null,
112+
headers: [],
113+
body: decode64(match[3]) as unknown as Uint8Array
114+
};
115+
rval.push(msg);
116+
117+
// no headers
118+
if (!match[2]) {
119+
continue;
120+
}
121+
122+
// parse headers
123+
const lines = match[2].split(rCRLF);
124+
let li = 0;
125+
let headerMatch: RegExpExecArray | null;
126+
127+
while (li < lines.length) {
128+
// get line, trim any rhs whitespace
129+
let line = lines[li].replace(/\s+$/, '');
130+
131+
// RFC2822 unfold any following folded lines
132+
for (let nl = li + 1; nl < lines.length; ++nl) {
133+
const next = lines[nl];
134+
if (!/\s/.test(next[0])) {
135+
break;
136+
}
137+
line += next;
138+
li = nl;
139+
}
140+
141+
// parse header
142+
headerMatch = rHeader.exec(line);
143+
if (headerMatch) {
144+
const header: PEMHeader = { name: headerMatch[1], values: [] };
145+
const values = headerMatch[2].split(',');
146+
for (let vi = 0; vi < values.length; ++vi) {
147+
header.values.push(ltrim(values[vi]));
148+
}
149+
150+
// Proc-Type must be the first header
151+
if (!msg.procType) {
152+
if (header.name !== 'Proc-Type') {
153+
throw new Error('Invalid PEM formatted message. The first ' +
154+
'encapsulated header must be "Proc-Type".');
155+
} else if (header.values.length !== 2) {
156+
throw new Error('Invalid PEM formatted message. The "Proc-Type" ' +
157+
'header must have two subfields.');
158+
}
159+
msg.procType = { version: values[0], type: values[1] };
160+
} else if (!msg.contentDomain && header.name === 'Content-Domain') {
161+
// special-case Content-Domain
162+
msg.contentDomain = values[0] || '';
163+
} else if (!msg.dekInfo && header.name === 'DEK-Info') {
164+
// special-case DEK-Info
165+
if (header.values.length === 0) {
166+
throw new Error('Invalid PEM formatted message. The "DEK-Info" ' +
167+
'header must have at least one subfield.');
168+
}
169+
msg.dekInfo = { algorithm: values[0], parameters: values[1] || null };
170+
} else {
171+
msg.headers.push(header);
172+
}
173+
}
174+
175+
++li;
176+
}
177+
178+
if (msg.procType?.type === 'ENCRYPTED' && !msg.dekInfo) {
179+
throw new Error('Invalid PEM formatted message. The "DEK-Info" ' +
180+
'header must be present if "Proc-Type" is "ENCRYPTED".');
181+
}
182+
}
183+
184+
if (rval.length === 0) {
185+
throw new Error('Invalid PEM formatted message.');
186+
}
187+
188+
return rval;
189+
}
43190

44191
/**
45-
* Converts a PrivateKeyInfo to PEM format.
46-
*
47-
* @param pki the PrivateKeyInfo.
48-
* @param maxline the maximum characters per line, defaults to 64.
192+
* Folds a PEM header according to RFC 1421 rules.
49193
*
50-
* @return the PEM-formatted private key.
194+
* @param header The header to fold
195+
* @returns The folded header string
51196
*/
52-
export function privateKeyInfoToPem(pki: any, maxline: number) {
53-
// convert to DER, then PEM-encode
54-
var msg = {
55-
type: 'PRIVATE KEY',
56-
body: asn1.toDer(pki).getBytes()
197+
function foldHeader(header: PEMHeader): string {
198+
let rval = `${header.name}: `;
199+
200+
// ensure values with CRLF are folded
201+
const values: string[] = [];
202+
const insertSpace = (match: string, $1: string): string => {
203+
return ' ' + $1;
57204
};
58-
return forge.pem.encode(msg, { maxline: maxline });
59-
};
205+
206+
for (let i = 0; i < header.values.length; ++i) {
207+
values.push(header.values[i].replace(/^(\S+\r\n)/, insertSpace));
208+
}
209+
rval += values.join(',') + '\r\n';
210+
211+
// do folding
212+
let length = 0;
213+
let candidate = -1;
214+
for (let i = 0; i < rval.length; ++i, ++length) {
215+
if (length > 65 && candidate !== -1) {
216+
const insert = rval[candidate];
217+
if (insert === ',') {
218+
++candidate;
219+
rval = rval.substring(0, candidate) + '\r\n ' + rval.substring(candidate);
220+
} else {
221+
rval = rval.substring(0, candidate) +
222+
'\r\n' + insert + rval.substring(candidate + 1);
223+
}
224+
length = (i - candidate - 1);
225+
candidate = -1;
226+
++i;
227+
} else if (rval[i] === ' ' || rval[i] === '\t' || rval[i] === ',') {
228+
candidate = i;
229+
}
230+
}
231+
232+
return rval;
233+
}
234+
235+
/**
236+
* Removes leading whitespace from a string.
237+
*
238+
* @param str The string to trim
239+
* @returns The trimmed string
240+
*/
241+
function ltrim(str: string): string {
242+
return str.replace(/^\s+/, '');
243+
}
244+
245+
export interface PEM {
246+
encode: (msg: PEMMessage, options?: PEMEncodeOptions) => string
247+
decode: (str: string) => PEMMessage[]
248+
}
249+
250+
export const pem: PEM = {
251+
encode,
252+
decode
253+
}
254+
255+
export default pem

packages/pem/src/types.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* TypeScript types for PEM (Privacy Enhanced Mail) implementation.
3+
*/
4+
5+
/**
6+
* Represents a PEM header with a name and array of values.
7+
*/
8+
export interface PEMHeader {
9+
name: string;
10+
values: string[];
11+
}
12+
13+
/**
14+
* Represents the processing type information in a PEM message.
15+
*/
16+
export interface ProcType {
17+
version: string;
18+
type: string;
19+
}
20+
21+
/**
22+
* Represents the Data Encryption Key information in a PEM message.
23+
*/
24+
export interface DEKInfo {
25+
algorithm: string;
26+
parameters: string | null;
27+
}
28+
29+
/**
30+
* Represents a decoded PEM message.
31+
*/
32+
export interface PEMMessage {
33+
type: string;
34+
procType: ProcType | null;
35+
contentDomain: string | null;
36+
dekInfo: DEKInfo | null;
37+
headers: PEMHeader[];
38+
body: Uint8Array;
39+
}
40+
41+
/**
42+
* Options for encoding PEM messages.
43+
*/
44+
export interface PEMEncodeOptions {
45+
maxline?: number;
46+
}

0 commit comments

Comments
 (0)