Fix SNI lock, update deps, ESM refactor

This commit is contained in:
Wildan M 2023-04-21 10:46:24 +07:00
commit 447b1b735d
13 changed files with 1204 additions and 1144 deletions

View file

@ -1,494 +1,388 @@
const fs = require('fs')
const path = require('path')
const {
promisify
} = require('util')
const {
fromKeyLike
} = require('jose-node-cjs-runtime/jwk/from_key_like')
const {
generateKeyPair
} = require('jose-node-cjs-runtime/util/generate_key_pair')
const {
calculateThumbprint
} = require('jose-node-cjs-runtime/jwk/thumbprint')
const {
SignJWT
} = require('jose-node-cjs-runtime/jwt/sign')
const {
CompactSign
} = require('jose-node-cjs-runtime/jws/compact/sign')
const pem = require('pem')
const common = require('./common')
const request = require('./request')
const createCsr = promisify(pem.createCSR)
import fs from "fs";
import path from "path";
import { promisify } from "util";
import fromKeyLike from "jose/jwk/from_key_like";
import generateKeyPair from "jose/util/generate_key_pair";
import calculateThumbprint from "jose/jwk/thumbprint";
import SignJWT from "jose/jwt/sign";
import CompactSign from "jose/jws/compact/sign";
import pem from "pem";
import * as common from "./common.js";
import request from "./request.js";
const createCsr = promisify(pem.createCSR);
/**
* Represents a Let's Encrypt account and
* sends requests to get valid TLS certificates.
*/
class Client {
/**
* @param {String} [directoryUrl]
*/
constructor(directoryUrl = common.DIRECTORY_URL) {
this.accountPrivateJwk = null
/** @type {import('crypto').KeyObject} */
this.accountPrivateKey = null
this.accountPublicJwk = null
/** @type {import('crypto').KeyObject} */
this.accountPublicKey = null
this.directoryUrl = directoryUrl
this.challengeCallbacks = null
this.hasDirectory = false
this.myAccountUrl = ''
this.newAccountUrl = ''
this.newNonceUrl = ''
this.newOrderUrl = ''
this.replayNonce = ''
this.thumbprint = ''
}
/**
* Export account public and private keys to a directory.
*
* @param {String} dirname - name of directory to write key files to
* @param {String} [passphrase] - optional passphrase to encrypt private key with
*
* @return {Promise}
*/
exportAccountKeyPair(dirname, passphrase) {
const privateKeyFile = path.join(dirname, 'privateKey.pem')
const publicKeyFile = path.join(dirname, 'publicKey.pem')
return Promise.all([
common.writeKeyToFile(privateKeyFile, this.accountPrivateKey, passphrase),
common.writeKeyToFile(publicKeyFile, this.accountPublicKey)
])
}
/**
* Generate new account public and private keys.
*
* @return {Promise}
*/
async generateAccountKeyPair() {
const {
privateKey,
publicKey
} = await generateKeyPair(common.ACCOUNT_KEY_ALGORITHM)
// @ts-ignore
this.accountPrivateKey = privateKey
// @ts-ignore
this.accountPublicKey = publicKey
await this.initAccountJwks()
}
/**
* Generate a certificate from Let's Encrypt for your domain.
*
* @param {String} domain - the domain you want a certificate for
*
* @return {Promise}
*/
async generateCertificate(domain) {
await this.directory()
await this.newNonce()
if (!this.myAccountUrl)
await this.newAccount()
const {
authzUrls,
finalizeUrl
} = await this.newOrder(domain)
const {
challenge
} = await this.authz(authzUrls[0])
await this.completeChallenge(challenge, domain)
await this.pollAuthz(authzUrls[0])
const {
certificate,
privateKeyData
} = await this.finalizeOrder(finalizeUrl, domain)
return {
certificate,
privateKeyData
/**
* @param {String} [directoryUrl]
*/
constructor(directoryUrl = common.DIRECTORY_URL) {
this.accountPrivateJwk = null;
/** @type {import('crypto').KeyObject|null} */
this.accountPrivateKey = null;
/** @type {import("jose/types.js").JWK | undefined} */
this.accountPublicJwk = undefined;
/** @type {import('crypto').KeyObject|null} */
this.accountPublicKey = null;
this.directoryUrl = directoryUrl;
this.challengeCallbacks = null;
this.hasDirectory = false;
this.myAccountUrl = '';
this.newAccountUrl = '';
this.newNonceUrl = '';
this.newOrderUrl = '';
this.replayNonce = '';
this.thumbprint = '';
}
}
/**
* Import account public and private keys from a directory.
*
* @param {String} dirname - name of directory to read key files from
* @param {String} [passphrase] - optional passphrase to decrypt private key with
*
* @return {Promise}
*/
async importAccountKeyPair(dirname, passphrase) {
const [privateKeyData, publicKeyData] = await Promise.all([
fs.promises.readFile(path.join(dirname, 'privateKey.pem'), 'utf8'),
fs.promises.readFile(path.join(dirname, 'publicKey.pem'), 'utf8')
])
this.accountPrivateKey = common.importPrivateKey(privateKeyData, passphrase)
this.accountPublicKey = common.importPublicKey(publicKeyData)
await this.initAccountJwks()
}
async authz(authzUrl) {
const data = await this.sign({
kid: this.myAccountUrl,
nonce: this.replayNonce,
url: authzUrl
})
const res = await request(authzUrl, {
method: 'POST',
headers: {
'content-type': 'application/jose+json'
},
data
})
this.setReplayNonce(res)
if (res.statusCode !== 200) {
throw new Error(`authz() Status Code: ${res.statusCode} Data: ${res.data}`)
/**
* Export account public and private keys to a directory.
*
* @param {String} dirname - name of directory to write key files to
* @param {String} [passphrase] - optional passphrase to encrypt private key with
*
* @return {Promise}
*/
exportAccountKeyPair(dirname, passphrase) {
if (this.accountPrivateKey == null || this.accountPublicKey == null) {
return Promise.reject(new Error('Account key pair not generated'));
}
const privateKeyFile = path.join(dirname, 'privateKey.pem');
const publicKeyFile = path.join(dirname, 'publicKey.pem');
return Promise.all([
common.writeKeyToFile(privateKeyFile, this.accountPrivateKey, passphrase),
common.writeKeyToFile(publicKeyFile, this.accountPublicKey)
]);
}
const {
challenges,
identifier,
...rest
} = res.data
const challenge = challenges.find(({
type
}) => type === 'http-01')
return {
challenge,
domain: identifier.value,
...rest
/**
* Generate new account public and private keys.
*
* @return {Promise}
*/
async generateAccountKeyPair() {
const { privateKey, publicKey } = await generateKeyPair(common.ACCOUNT_KEY_ALGORITHM);
// @ts-ignore
this.accountPrivateKey = privateKey;
// @ts-ignore
this.accountPublicKey = publicKey;
await this.initAccountJwks();
}
}
async completeChallenge(challenge, domain) {
await this.readyChallenge(challenge)
await this.receiveServerRequest(challenge, domain)
}
async directory() {
if (this.hasDirectory) return false
const res = await request(this.directoryUrl)
if (res.statusCode !== 200) {
throw new Error(`directory() Status Code: ${res.statusCode} Data: ${res.data}`)
/**
* Generate a certificate from Let's Encrypt for your domain.
*
* @param {String} domain - the domain you want a certificate for
*
* @return {Promise}
*/
async generateCertificate(domain) {
await this.directory();
await this.newNonce();
if (!this.myAccountUrl)
await this.newAccount();
const { authzUrls, finalizeUrl } = await this.newOrder(domain);
const { challenge } = await this.authz(authzUrls[0]);
await this.completeChallenge(challenge, domain);
await this.pollAuthz(authzUrls[0]);
const { certificate, privateKeyData } = await this.finalizeOrder(finalizeUrl, domain);
return {
certificate,
privateKeyData
};
}
this.hasDirectory = true
this.newAccountUrl = res.data.newAccount
this.newNonceUrl = res.data.newNonce
this.newOrderUrl = res.data.newOrder
return true
}
async fetchCertificate(certificateUrl) {
const data = await this.sign({
kid: this.myAccountUrl,
nonce: this.replayNonce,
url: certificateUrl
})
const res = await request(certificateUrl, {
method: 'POST',
headers: {
accept: 'application/pem-certificate-chain',
'content-type': 'application/jose+json'
},
data
})
this.setReplayNonce(res)
if (res.statusCode !== 200) {
throw new Error(`fetchCertificate() Status Code: ${res.statusCode} Data: ${res.data}`)
/**
* Import account public and private keys from a directory.
*
* @param {String} dirname - name of directory to read key files from
* @param {String} [passphrase] - optional passphrase to decrypt private key with
*
* @return {Promise}
*/
async importAccountKeyPair(dirname, passphrase) {
const [privateKeyData, publicKeyData] = await Promise.all([
fs.promises.readFile(path.join(dirname, 'privateKey.pem'), 'utf8'),
fs.promises.readFile(path.join(dirname, 'publicKey.pem'), 'utf8')
]);
this.accountPrivateKey = common.importPrivateKey(privateKeyData, passphrase);
this.accountPublicKey = common.importPublicKey(publicKeyData);
await this.initAccountJwks();
}
return res.data
}
async finalizeOrder(finalizeUrl, domain) {
const {
privateKey
} = await generateKeyPair(common.CERTIFICATE_KEY_ALGORITHM)
// @ts-ignore
const clientKey = common.exportPrivateKey(privateKey)
let {
csr
// @ts-ignore
} = await createCsr({
clientKey,
commonName: domain,
})
// "The CSR is sent in the base64url-encoded version of the DER format.
// (Note: Because this field uses base64url, and does not include headers,
// it is different from PEM.)"
csr = csr
.split('\n')
.filter(Boolean)
.slice(1, -1)
.join('')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '')
const data = await this.sign({
kid: this.myAccountUrl,
nonce: this.replayNonce,
url: finalizeUrl
}, {
csr
})
const res = await request(finalizeUrl, {
method: 'POST',
headers: {
'content-type': 'application/jose+json'
},
data
})
this.setReplayNonce(res)
if (res.statusCode !== 200) {
throw new Error(`finalizeOrder() Status Code: ${res.statusCode} Data: ${res.data}`)
async authz(authzUrl) {
const data = await this.sign({
kid: this.myAccountUrl,
nonce: this.replayNonce,
url: authzUrl
});
const res = await request(authzUrl, {
method: 'POST',
headers: {
'content-type': 'application/jose+json'
},
data
});
this.setReplayNonce(res);
if (res.statusCode !== 200) {
throw new Error(`authz() Status Code: ${res.statusCode} Data: ${res.data}`);
}
const { challenges, identifier, ...rest } = res.data;
const challenge = challenges.find(({ type }) => type === 'http-01');
return {
challenge,
domain: identifier.value,
...rest
};
}
const certificate = await this.fetchCertificate(res.data.certificate)
return {
certificate,
privateKeyData: clientKey
async completeChallenge(challenge, domain) {
await this.readyChallenge(challenge);
await this.receiveServerRequest(challenge, domain);
}
}
async initAccountJwks() {
const [publicJwk, accountPrivateJwk] = await Promise.all([
fromKeyLike(this.accountPublicKey),
fromKeyLike(this.accountPrivateKey)
])
this.accountPublicJwk = publicJwk
this.accountPrivateJwk = accountPrivateJwk
this.thumbprint = await calculateThumbprint(publicJwk)
}
async newAccount(...emails) {
const data = await this.sign({
jwk: this.accountPublicJwk,
nonce: this.replayNonce,
url: this.newAccountUrl
}, {
termsOfServiceAgreed: true
})
const res = await request(this.newAccountUrl, {
method: 'POST',
headers: {
'content-type': 'application/jose+json'
},
data
})
this.setReplayNonce(res)
if (![200, 201].includes(res.statusCode)) {
throw new Error(`newAccount() Status Code: ${res.statusCode} Data: ${res.data}`)
async directory() {
if (this.hasDirectory)
return false;
const res = await request(this.directoryUrl);
if (res.statusCode !== 200) {
throw new Error(`directory() Status Code: ${res.statusCode} Data: ${res.data}`);
}
this.hasDirectory = true;
this.newAccountUrl = res.data.newAccount;
this.newNonceUrl = res.data.newNonce;
this.newOrderUrl = res.data.newOrder;
return true;
}
this.myAccountUrl = res.headers.location
return res.statusCode === 201
}
async newNonce() {
if (this.replayNonce) return false
const res = await request(this.newNonceUrl, {
method: 'HEAD'
})
if (res.statusCode !== 200) {
throw new Error(`newNonce() Status Code: ${res.statusCode} Data: ${res.data}`)
async fetchCertificate(certificateUrl) {
const data = await this.sign({
kid: this.myAccountUrl,
nonce: this.replayNonce,
url: certificateUrl
});
const res = await request(certificateUrl, {
method: 'POST',
headers: {
accept: 'application/pem-certificate-chain',
'content-type': 'application/jose+json'
},
data
});
this.setReplayNonce(res);
if (res.statusCode !== 200) {
throw new Error(`fetchCertificate() Status Code: ${res.statusCode} Data: ${res.data}`);
}
return res.data;
}
this.setReplayNonce(res)
return true
}
async newOrder(...domains) {
const identifiers = domains.map(domain => ({
type: 'dns',
value: domain
}))
const data = await this.sign({
kid: this.myAccountUrl,
nonce: this.replayNonce,
url: this.newOrderUrl
}, {
identifiers
})
const res = await request(this.newOrderUrl, {
method: 'POST',
headers: {
'content-type': 'application/jose+json'
},
data
})
this.setReplayNonce(res)
if (res.statusCode !== 201) {
throw new Error(`newOrder() Status Code: ${res.statusCode} Data: ${res.data}`)
async finalizeOrder(finalizeUrl, domain) {
const { privateKey } = await generateKeyPair(common.CERTIFICATE_KEY_ALGORITHM);
// @ts-ignore
const clientKey = common.exportPrivateKey(privateKey);
let { csr
// @ts-ignore
} = await createCsr({
clientKey,
commonName: domain,
});
// "The CSR is sent in the base64url-encoded version of the DER format.
// (Note: Because this field uses base64url, and does not include headers,
// it is different from PEM.)"
csr = csr
.split('\n')
.filter(Boolean)
.slice(1, -1)
.join('')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
const data = await this.sign({
kid: this.myAccountUrl,
nonce: this.replayNonce,
url: finalizeUrl
}, {
csr
});
const res = await request(finalizeUrl, {
method: 'POST',
headers: {
'content-type': 'application/jose+json'
},
data
});
this.setReplayNonce(res);
if (res.statusCode !== 200) {
throw new Error(`finalizeOrder() Status Code: ${res.statusCode} Data: ${res.data}`);
}
const certificate = await this.fetchCertificate(res.data.certificate);
return {
certificate,
privateKeyData: clientKey
};
}
const orderUrl = res.headers.location
const {
authorizations: authzUrls,
finalize: finalizeUrl
} = res.data
return {
authzUrls,
domains,
finalizeUrl,
orderUrl
async initAccountJwks() {
if (this.accountPrivateKey == null || this.accountPublicKey == null) {
return Promise.reject(new Error('Account key pair not generated'));
}
const [publicJwk, accountPrivateJwk] = await Promise.all([
fromKeyLike(this.accountPublicKey),
fromKeyLike(this.accountPrivateKey)
]);
this.accountPublicJwk = publicJwk;
this.accountPrivateJwk = accountPrivateJwk;
this.thumbprint = await calculateThumbprint(publicJwk);
}
}
async pollAuthz(authzUrl) {
for (let i = 0; i < 10; i++) {
const result = await this.authz(authzUrl)
if (result.status === 'pending') {
await new Promise(resolve => setTimeout(resolve, 1e3))
continue
}
if (result.status === 'invalid') {
throw new Error('pollAuthz() authorization is invalid: ' + JSON.stringify(result, null, 2))
}
return result
async newAccount(...emails) {
const data = await this.sign({
jwk: this.accountPublicJwk,
nonce: this.replayNonce,
url: this.newAccountUrl
}, {
termsOfServiceAgreed: true
});
const res = await request(this.newAccountUrl, {
method: 'POST',
headers: {
'content-type': 'application/jose+json'
},
data
});
this.setReplayNonce(res);
if (![200, 201].includes(res.statusCode)) {
throw new Error(`newAccount() Status Code: ${res.statusCode} Data: ${res.data}`);
}
this.myAccountUrl = res.headers.location;
return res.statusCode === 201;
}
throw new Error('pollAuthz() timed out')
}
async readyChallenge(challenge) {
const data = await this.sign({
kid: this.myAccountUrl,
nonce: this.replayNonce,
url: challenge.url
}, {})
const res = await request(challenge.url, {
method: 'POST',
headers: {
'content-type': 'application/jose+json'
},
data
})
this.setReplayNonce(res)
if (res.statusCode !== 200) {
throw new Error(`readyChallenge() Status Code: ${res.statusCode} Data: ${res.data}`)
async newNonce() {
if (this.replayNonce)
return false;
const res = await request(this.newNonceUrl, {
method: 'HEAD'
});
if (res.statusCode !== 200) {
throw new Error(`newNonce() Status Code: ${res.statusCode} Data: ${res.data}`);
}
this.setReplayNonce(res);
return true;
}
}
receiveServerRequest(challenge, domain) {
return new Promise((resolve, reject) => {
const time = setTimeout(() => {
reject(new Error('Timed out waiting for server request'))
}, 10e3);
let hasResolved = false;
this.challengeCallbacks = () => {
if (!hasResolved)
setTimeout(resolve, 100);
else
return challenge.token + '.' + this.thumbprint;
hasResolved = true;
clearTimeout(time);
// wanted to clear callbacks here but LE does the call multiple times.
// remember we're in mutex lock so no worries for racing.
return challenge.token + '.' + this.thumbprint;
}
});
}
setReplayNonce(res) {
const replayNonce = (res.headers['replay-nonce'] || '').trim()
if (!replayNonce) {
throw new Error('No Replay-Nonce header in response')
async newOrder(...domains) {
const identifiers = domains.map(domain => ({
type: 'dns',
value: domain
}));
const data = await this.sign({
kid: this.myAccountUrl,
nonce: this.replayNonce,
url: this.newOrderUrl
}, {
identifiers
});
const res = await request(this.newOrderUrl, {
method: 'POST',
headers: {
'content-type': 'application/jose+json'
},
data
});
this.setReplayNonce(res);
if (res.statusCode !== 201) {
throw new Error(`newOrder() Status Code: ${res.statusCode} Data: ${res.data}`);
}
const orderUrl = res.headers.location;
const { authorizations: authzUrls, finalize: finalizeUrl } = res.data;
return {
authzUrls,
domains,
finalizeUrl,
orderUrl
};
}
this.replayNonce = replayNonce
}
async sign(header, payload) {
let data
if (payload) {
data = await new SignJWT(payload)
.setProtectedHeader({
alg: common.ACCOUNT_KEY_ALGORITHM,
...header
})
.sign(this.accountPrivateKey)
} else {
// SignJWT constructor only accepts object but RFC8555 requires empty payload
// Workaround: manually pass empty Uint8Array to CompactSign constructor
const sig = new CompactSign(new Uint8Array())
sig.setProtectedHeader({
alg: common.ACCOUNT_KEY_ALGORITHM,
...header
})
data = await sig.sign(this.accountPrivateKey)
async pollAuthz(authzUrl) {
for (let i = 0; i < 10; i++) {
const result = await this.authz(authzUrl);
if (result.status === 'pending') {
await new Promise(resolve => setTimeout(resolve, 1e3));
continue;
}
if (result.status === 'invalid') {
throw new Error('pollAuthz() authorization is invalid: ' + JSON.stringify(result, null, 2));
}
return result;
}
throw new Error('pollAuthz() timed out');
}
async readyChallenge(challenge) {
const data = await this.sign({
kid: this.myAccountUrl,
nonce: this.replayNonce,
url: challenge.url
}, {});
const res = await request(challenge.url, {
method: 'POST',
headers: {
'content-type': 'application/jose+json'
},
data
});
this.setReplayNonce(res);
if (res.statusCode !== 200) {
throw new Error(`readyChallenge() Status Code: ${res.statusCode} Data: ${res.data}`);
}
}
receiveServerRequest(challenge, domain) {
return new Promise((resolve, reject) => {
const time = setTimeout(() => {
reject(new Error('Timed out waiting for server request'));
}, 10e3);
let hasResolved = false;
this.challengeCallbacks = () => {
if (!hasResolved)
setTimeout(resolve, 100);
else
return challenge.token + '.' + this.thumbprint;
hasResolved = true;
clearTimeout(time);
// wanted to clear callbacks here but LE does the call multiple times.
// remember we're in mutex lock so no worries for racing.
return challenge.token + '.' + this.thumbprint;
};
});
}
setReplayNonce(res) {
const replayNonce = (res.headers['replay-nonce'] || '').trim();
if (!replayNonce) {
throw new Error('No Replay-Nonce header in response');
}
this.replayNonce = replayNonce;
}
/**
* @param {import("jose/types.js").JWSHeaderParameters} header
* @param {import("jose/types.js").JWTPayload | undefined} [payload]
*/
async sign(header, payload) {
if (this.accountPrivateKey == null) {
return Promise.reject(new Error('Account key pair not generated'));
}
let data;
if (payload) {
data = await new SignJWT(payload)
.setProtectedHeader({
alg: common.ACCOUNT_KEY_ALGORITHM,
...header
})
.sign(this.accountPrivateKey);
}
else {
// SignJWT constructor only accepts object but RFC8555 requires empty payload
// Workaround: manually pass empty Uint8Array to CompactSign constructor
const sig = new CompactSign(new Uint8Array());
sig.setProtectedHeader({
alg: common.ACCOUNT_KEY_ALGORITHM,
...header
});
data = await sig.sign(this.accountPrivateKey);
}
const [b64Header, b64Payload, b64Signature] = data.split('.');
return JSON.stringify({
protected: b64Header,
payload: b64Payload,
signature: b64Signature
});
}
const [b64Header, b64Payload, b64Signature] = data.split('.')
return JSON.stringify({
protected: b64Header,
payload: b64Payload,
signature: b64Signature
})
}
}
module.exports = Client
export default Client;

View file

@ -1,55 +1,45 @@
const crypto = require('crypto')
const fs = require('fs')
const ACCOUNT_KEY_ALGORITHM = 'ES256'
const CERTIFICATE_KEY_ALGORITHM = 'RS256'
const env = (process.env.NODE_ENV || '').trim().toLowerCase()
import crypto from "crypto";
import fs from "fs";
const ACCOUNT_KEY_ALGORITHM = 'ES256';
const CERTIFICATE_KEY_ALGORITHM = 'RS256';
const env = (process.env.NODE_ENV || '').trim().toLowerCase();
const DIRECTORY_URL = ['development', 'test'].includes(env)
? 'https://acme-staging-v02.api.letsencrypt.org/directory'
: 'https://acme-v02.api.letsencrypt.org/directory'
const PRIVATE_KEY_CIPHER = 'aes-256-cbc'
const PRIVATE_KEY_FORMAT = 'pem'
const PRIVATE_KEY_PERMISSIONS = 0o600
const PRIVATE_KEY_TYPE = 'pkcs8'
const PUBLIC_KEY_FORMAT = 'pem'
const PUBLIC_KEY_PERMISSIONS = 0o666
const PUBLIC_KEY_TYPE = 'spki'
? 'https://acme-staging-v02.api.letsencrypt.org/directory'
: 'https://acme-v02.api.letsencrypt.org/directory';
const PRIVATE_KEY_CIPHER = 'aes-256-cbc';
const PRIVATE_KEY_FORMAT = 'pem';
const PRIVATE_KEY_PERMISSIONS = 0o600;
const PRIVATE_KEY_TYPE = 'pkcs8';
const PUBLIC_KEY_FORMAT = 'pem';
const PUBLIC_KEY_PERMISSIONS = 0o666;
const PUBLIC_KEY_TYPE = 'spki';
/**
* @param {crypto.KeyObject} privateKey
* @param {String} [passphrase]
*
*/
const exportPrivateKey = (privateKey, passphrase) => {
/** @type {crypto.KeyExportOptions<'pem'>} */
const privateKeyOpts = {
type: PRIVATE_KEY_TYPE,
format: PRIVATE_KEY_FORMAT
}
if (passphrase) {
privateKeyOpts.cipher = PRIVATE_KEY_CIPHER
privateKeyOpts.passphrase = passphrase
}
return privateKey.export(privateKeyOpts)
}
/** @type {crypto.KeyExportOptions<'pem'>} */
const privateKeyOpts = {
type: PRIVATE_KEY_TYPE,
format: PRIVATE_KEY_FORMAT
};
if (passphrase) {
privateKeyOpts.cipher = PRIVATE_KEY_CIPHER;
privateKeyOpts.passphrase = passphrase;
}
return privateKey.export(privateKeyOpts);
};
/**
* @param {crypto.KeyObject} publicKey
*/
const exportPublicKey = publicKey => {
/** @type {crypto.KeyExportOptions<'pem'>} */
return publicKey.export({
type: PUBLIC_KEY_TYPE,
format: PUBLIC_KEY_FORMAT
})
}
/** @type {crypto.KeyExportOptions<'pem'>} */
return publicKey.export({
type: PUBLIC_KEY_TYPE,
format: PUBLIC_KEY_FORMAT
});
};
/**
* @param {String} privateKeyData
* @param {String} [passphrase]
@ -57,41 +47,39 @@ const exportPublicKey = publicKey => {
* @return {crypto.KeyObject}
*/
const importPrivateKey = (privateKeyData, passphrase) => {
/** @type {crypto.KeyExportOptions<'pem'>} */
const privateKeyOpts = {
key: privateKeyData,
format: PRIVATE_KEY_FORMAT,
type: PRIVATE_KEY_TYPE
}
if (passphrase) {
privateKeyOpts.passphrase = passphrase
}
try {
return crypto.createPrivateKey(privateKeyOpts)
} catch {
throw new Error('Failed to import private key')
}
}
/** @type {crypto.PrivateKeyInput} */
const privateKeyOpts = {
key: privateKeyData,
format: PRIVATE_KEY_FORMAT,
type: PRIVATE_KEY_TYPE
};
if (passphrase) {
privateKeyOpts.passphrase = passphrase;
}
try {
return crypto.createPrivateKey(privateKeyOpts);
}
catch {
throw new Error('Failed to import private key');
}
};
/**
* @param {String} publicKeyData
*
* @return {crypto.KeyObject}
*/
const importPublicKey = publicKeyData => {
try {
return crypto.createPublicKey({
key: publicKeyData,
format: PUBLIC_KEY_FORMAT,
type: PUBLIC_KEY_TYPE
})
} catch {
throw new Error('Failed to import public key')
}
}
try {
return crypto.createPublicKey({
key: publicKeyData,
format: PUBLIC_KEY_FORMAT,
type: PUBLIC_KEY_TYPE
});
}
catch {
throw new Error('Failed to import public key');
}
};
/**
* @param {String} filename
* @param {crypto.KeyObject|string} key
@ -100,40 +88,19 @@ const importPublicKey = publicKeyData => {
* @return {Promise}
*/
const writeKeyToFile = async (filename, key, passphrase) => {
if (typeof key === 'string') {
key = key.includes('PRIVATE KEY')
? importPrivateKey(key, passphrase)
: importPublicKey(key)
} else if (!(key instanceof crypto.KeyObject)) {
throw new Error('Expected "key" to be crypto.KeyObject or string')
}
const isPrivateKey = key.type === 'private'
const keyData = isPrivateKey
? exportPrivateKey(key, passphrase)
: exportPublicKey(key)
const mode = isPrivateKey ? PRIVATE_KEY_PERMISSIONS : PUBLIC_KEY_PERMISSIONS
await fs.promises.writeFile(filename, keyData, { mode })
}
module.exports = {
ACCOUNT_KEY_ALGORITHM,
CERTIFICATE_KEY_ALGORITHM,
DIRECTORY_URL,
PRIVATE_KEY_CIPHER,
PRIVATE_KEY_FORMAT,
PRIVATE_KEY_PERMISSIONS,
PRIVATE_KEY_TYPE,
PUBLIC_KEY_FORMAT,
PUBLIC_KEY_PERMISSIONS,
PUBLIC_KEY_TYPE,
env,
exportPrivateKey,
exportPublicKey,
importPrivateKey,
importPublicKey,
writeKeyToFile
}
if (typeof key === 'string') {
key = key.includes('PRIVATE KEY')
? importPrivateKey(key, passphrase)
: importPublicKey(key);
}
else if (!(key instanceof crypto.KeyObject)) {
throw new Error('Expected "key" to be crypto.KeyObject or string');
}
const isPrivateKey = key.type === 'private';
const keyData = isPrivateKey
? exportPrivateKey(key, passphrase)
: exportPublicKey(key);
const mode = isPrivateKey ? PRIVATE_KEY_PERMISSIONS : PUBLIC_KEY_PERMISSIONS;
await fs.promises.writeFile(filename, keyData, { mode });
};
export { ACCOUNT_KEY_ALGORITHM, CERTIFICATE_KEY_ALGORITHM, DIRECTORY_URL, PRIVATE_KEY_CIPHER, PRIVATE_KEY_FORMAT, PRIVATE_KEY_PERMISSIONS, PRIVATE_KEY_TYPE, PUBLIC_KEY_FORMAT, PUBLIC_KEY_PERMISSIONS, PUBLIC_KEY_TYPE, env, exportPrivateKey, exportPublicKey, importPrivateKey, importPublicKey, writeKeyToFile };

View file

@ -1,4 +1,3 @@
module.exports = {
...require('./common'),
Client: require('./client')
}
import Client from './client.js';
export * from './common.js';
export { Client };

View file

@ -1,46 +1,40 @@
const https = require('https')
import https from "https";
const request = (/** @type {string | import("url").URL} */ url, /** @type {https.RequestOptions&{data?: string}} */ { data = '', ...options } = {}, /** @type {() => any} */ cb) => {
return new Promise((resolve, reject) => {
try {
url = new URL(url)
} catch (err) {
return reject(err)
}
https.request(url, options, res => {
const { statusCode, headers } = res
let data = ''
res
.on('data', chunk => {
data += chunk
return new Promise((resolve, reject) => {
try {
url = new URL(url);
}
catch (err) {
return reject(err);
}
https.request(url, options, res => {
const { statusCode, headers } = res;
let data = '';
res
.on('data', chunk => {
data += chunk;
})
.once('end', () => {
if (headers['content-type']?.includes('application/json')) {
try {
data = JSON.parse(data);
}
catch (err) {
reject(err);
return;
}
}
resolve({ data, headers, statusCode });
})
.once('error', reject);
})
.once('end', () => {
if (headers['content-type']?.includes('application/json')) {
try {
data = JSON.parse(data)
} catch (err) {
reject(err)
return
}
}
resolve({ data, headers, statusCode })
})
.once('error', reject)
})
.once('error', reject)
.end(data)
setTimeout(() => {
const method = options.method || 'GET'
reject(new Error(`${method} request to "${url}" timed out`))
}, 10e3)
cb && cb()
})
}
module.exports = request
.once('error', reject)
.end(data);
setTimeout(() => {
const method = options.method || 'GET';
reject(new Error(`${method} request to "${url}" timed out`));
}, 10e3);
cb && cb();
});
};
export default request;