코드 이식
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@sammo/api_def",
|
||||
"version": "1.0.1",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./def": "./dist/def/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --build"
|
||||
},
|
||||
"author": "",
|
||||
"type": "module",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@sammo/crypto": "workspace:^",
|
||||
"@sammo/secure_token": "workspace:^",
|
||||
"@sammo/util": "workspace:^",
|
||||
"@strpc/def": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"zod": "^3.22.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.22.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const hello = "hello";
|
||||
@@ -0,0 +1,37 @@
|
||||
import type {
|
||||
DefAPINamespace,
|
||||
} from "@strpc/def";
|
||||
|
||||
//굳이 할 필요는 없지만, d.ts가 깔끔해짐
|
||||
import type {
|
||||
InvalidResponse, ValidResponse
|
||||
} from "@strpc/def";
|
||||
import type {
|
||||
ArgDeleteAPI,
|
||||
ArgGetAPI,
|
||||
ArgHeadAPI,
|
||||
ArgPatchAPI,
|
||||
ArgPostAPI,
|
||||
ArgPutAPI,
|
||||
EmptyDeleteAPI,
|
||||
EmptyGetAPI,
|
||||
EmptyHeadAPI,
|
||||
EmptyPatchAPI,
|
||||
EmptyPostAPI,
|
||||
EmptyPutAPI,
|
||||
} from "@strpc/def/types";
|
||||
import { GET, POST } from '@strpc/def';
|
||||
|
||||
import type { Base64String, WrappedBuffer } from "@sammo/util";
|
||||
import type { ECDSASignatureBSON, ECDSASignatureJSON, ECDSASignatureRaw, ECDSA_P384_VerifyKey, ECDSA_PKCS8_P384_SignKey } from "@sammo/crypto/RawTypes";
|
||||
import type { PEMString } from "@sammo/crypto";
|
||||
|
||||
export type {
|
||||
InferResponse,
|
||||
InferError,
|
||||
InferQuery,
|
||||
} from "@strpc/def";
|
||||
export type * as def from './def/index.js';
|
||||
|
||||
export const structure = {
|
||||
} satisfies DefAPINamespace;
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
|
||||
"path": "../util"
|
||||
},
|
||||
{
|
||||
"path": "../secure_token"
|
||||
},
|
||||
{
|
||||
"path": "../../@strpc/def"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
root: false,
|
||||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': 'off'
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
const jestConfig = {
|
||||
preset: 'ts-jest/presets/default-esm',
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
testEnvironment: 'node',
|
||||
rootDir: '.',
|
||||
transform: {
|
||||
'^.+\\.tsx?$': [
|
||||
'ts-jest',
|
||||
{
|
||||
useESM: true,
|
||||
tsconfig: 'tsconfig.jest.json',
|
||||
},
|
||||
],
|
||||
},
|
||||
moduleFileExtensions: [
|
||||
'js',
|
||||
'mjs',
|
||||
'ts',
|
||||
'mts',
|
||||
'tsx',
|
||||
'jsx',
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'^(\\.{1,2}/.*)\\.js$': '$1',
|
||||
"^lodash-es$": "lodash",
|
||||
"^lodash-es/(.*)$": "<rootDir>/node_modules/lodash/$1",
|
||||
},
|
||||
modulePathIgnorePatterns: [
|
||||
'<rootDir>/dist/',
|
||||
],
|
||||
moduleDirectories: [
|
||||
"node_modules",
|
||||
".*/@sammo/util"
|
||||
],
|
||||
};
|
||||
|
||||
export default jestConfig
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "@sammo/crypto",
|
||||
"version": "1.0.1",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./AES": "./dist/AES.js",
|
||||
"./ECDSA": "./dist/ECDSA.js",
|
||||
"./ECDHe": "./dist/ECDHe.js",
|
||||
"./ECDHe_AES": "./dist/ECDHe_AES.js",
|
||||
"./ECKey": "./dist/ECKey.js",
|
||||
"./PBKDF2": "./dist/PBKDF2.js",
|
||||
"./SHA2": "./dist/SHA2.js",
|
||||
"./RawTypes": "./dist/RawTypes.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"test": "jest"
|
||||
},
|
||||
"author": "",
|
||||
"type": "module",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@sammo/util": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.4",
|
||||
"@types/lodash": "^4.14.198",
|
||||
"@types/lodash-es": "^4.17.9",
|
||||
"@types/node": "^20.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.0",
|
||||
"@typescript-eslint/parser": "^6.7.0",
|
||||
"bson": "^5.4.0",
|
||||
"buffer": "^6.0.3",
|
||||
"eslint": "^8.49.0",
|
||||
"jest": "^29.7.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"ts-jest": "^29.1.1",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bson": "^5.4.0",
|
||||
"buffer": "^6.0.3",
|
||||
"lodash-es": "^4.17.21"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { AES_GCM_Decrypt, AES_GCM_Encrypt } from "./AES.js";
|
||||
|
||||
type TestType = {
|
||||
key: Buffer,
|
||||
IV: Buffer,
|
||||
PT: Buffer,
|
||||
AAD?: Buffer,
|
||||
CT: Buffer,
|
||||
failed?: boolean,
|
||||
};
|
||||
|
||||
type RawTestType = {
|
||||
key: string,
|
||||
IV: string,
|
||||
PT: string,
|
||||
AAD?: string,
|
||||
CT: string,
|
||||
failed?: boolean,
|
||||
}
|
||||
|
||||
function convertItem(obj: RawTestType): TestType {
|
||||
return {
|
||||
key: Buffer.from(obj.key, 'hex'),
|
||||
IV: Buffer.from(obj.IV, 'hex'),
|
||||
PT: Buffer.from(obj.PT, 'hex'),
|
||||
AAD: obj.AAD ? Buffer.from(obj.AAD, 'hex') : undefined,
|
||||
CT: Buffer.from(obj.CT, 'hex'),
|
||||
failed: obj.failed,
|
||||
}
|
||||
}
|
||||
|
||||
const tests: TestType[] = [
|
||||
{
|
||||
key: '92e11dcdaa866f5ce790fd24501f92509aacf4cb8b1339d50c9c1240935dd08b',
|
||||
IV : 'ac93a1a6145299bde902f21a',
|
||||
PT : '2d71bcfa914e4ac045b2aa60955fad24',
|
||||
AAD: '1e0889016f67601c8ebea4943bc23ad6',
|
||||
CT : '8995ae2e6df3dbf96fac7b7137bae67feca5aa77d51d4a0a14d9c51e1da474ab',
|
||||
failed: false,
|
||||
},
|
||||
{
|
||||
key: 'b52c505a37d78eda5dd34f20c22540ea1b58963cf8e5bf8ffa85f9f2492505b4',
|
||||
IV : '516c33929df5a3284ff463d7',
|
||||
PT : '',
|
||||
AAD: '',
|
||||
CT : 'bdc1ac884d332457a1d2664f168c76f0',
|
||||
failed: false,
|
||||
},
|
||||
{
|
||||
key: '886cff5f3e6b8d0e1ad0a38fcdb26de97e8acbe79f6bed66959a598fa5047d65',
|
||||
IV : '3a8efa1cd74bbab5448f9945',
|
||||
PT : '',
|
||||
AAD: '519fee519d25c7a304d6c6aa1897ee1eb8c59655',
|
||||
CT : 'f6d47505ec96c98a42dc3ae719877b87',
|
||||
failed: false,
|
||||
},
|
||||
{
|
||||
key: '460fc864972261c2560e1eb88761ff1c992b982497bd2ac36c04071cbb8e5d99',
|
||||
IV : '8a4a16b9e210eb68bcb6f58d',
|
||||
PT : '99e4e926ffe927f691893fb79a96b067',
|
||||
AAD: '',
|
||||
CT : '133fc15751621b5f325c7ff71ce08324ec4e87e0cf74a13618d0b68636ba9fa7',
|
||||
failed: false,
|
||||
},
|
||||
].map(convertItem)
|
||||
|
||||
|
||||
|
||||
|
||||
test.each(tests)('encrypt(%#)', async (item) => {
|
||||
for (const item of tests) {
|
||||
|
||||
if(item.failed){
|
||||
expect(() => AES_GCM_Encrypt(item.key, item.IV, item.PT, item.AAD)).rejects.toThrow('Invalid');
|
||||
return;
|
||||
}
|
||||
const ct = await AES_GCM_Encrypt(item.key, item.IV, item.PT, item.AAD);
|
||||
expect(ct).toEqual(item.CT.buffer);
|
||||
}
|
||||
})
|
||||
|
||||
test.each(tests)('decrypt(%#)', async (item) => {
|
||||
for (const item of tests) {
|
||||
|
||||
if(item.failed){
|
||||
expect(() => AES_GCM_Decrypt(item.key, item.IV, item.CT, item.AAD)).rejects.toThrow('Invalid');
|
||||
return;
|
||||
}
|
||||
const pt = await AES_GCM_Decrypt(item.key, item.IV, item.CT, item.AAD);
|
||||
expect(pt).toEqual(item.PT.buffer);
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { BufferSource } from "./types.js";
|
||||
|
||||
const subtle = globalThis.crypto.subtle;
|
||||
|
||||
|
||||
export async function AES_GCM_Encrypt(key: BufferSource | CryptoKey, iv: BufferSource, msg: BufferSource, aad?: BufferSource): Promise<ArrayBuffer> {
|
||||
const keyObj = key instanceof CryptoKey ? key : await subtle.importKey("raw", key, {
|
||||
name: "AES-GCM",
|
||||
}, false, ["encrypt"]);
|
||||
|
||||
const ciphertext = await subtle.encrypt({
|
||||
name: "AES-GCM",
|
||||
iv,
|
||||
additionalData: aad
|
||||
}, keyObj, msg);
|
||||
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
|
||||
export async function AES_GCM_Decrypt(key: BufferSource | CryptoKey, iv: BufferSource, ciphertext: BufferSource, aad?: BufferSource): Promise<ArrayBuffer> {
|
||||
const keyObj = key instanceof CryptoKey ? key : await subtle.importKey("raw", key, {
|
||||
name: "AES-GCM",
|
||||
}, false, ["decrypt"]);
|
||||
|
||||
const plaintext = await subtle.decrypt({
|
||||
name: "AES-GCM",
|
||||
iv: iv,
|
||||
additionalData: aad
|
||||
}, keyObj, ciphertext);
|
||||
//아마도 tag 검증 실패시 예외가 발생할 것으로 예상
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
export async function AES_CBC_Encrypt(key: BufferSource | CryptoKey, iv: BufferSource, msg: BufferSource): Promise<ArrayBuffer> {
|
||||
const keyObj = key instanceof CryptoKey ? key : await subtle.importKey("raw", key, {
|
||||
name: "AES-CBC",
|
||||
}, false, ["encrypt"]);
|
||||
|
||||
const ciphertext = await subtle.encrypt({
|
||||
name: "AES-CBC",
|
||||
iv,
|
||||
}, keyObj, msg);
|
||||
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
|
||||
export async function AES_CBC_Decrypt(key: BufferSource | CryptoKey, iv: BufferSource, ciphertext: BufferSource): Promise<ArrayBuffer> {
|
||||
const keyObj = key instanceof CryptoKey ? key : await subtle.importKey("raw", key, {
|
||||
name: "AES-CBC",
|
||||
}, false, ["decrypt"]);
|
||||
|
||||
const plaintext = await subtle.decrypt({
|
||||
name: "AES-CBC",
|
||||
iv: iv,
|
||||
}, keyObj, ciphertext);
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { wrapBuffer } from "@sammo/util";
|
||||
import { ECDSA_sign, ECDSA_verify } from "./ECDSA.js";
|
||||
import { verifyKeyFromSignKey } from "./ECKey.js";
|
||||
import type { ECDHe_P384_PublicKey, ECDHe_P384_KeyPair, ECDSA_PKCS8_P384_SignKey, ECDHe_P384_PrivateKey, ECDHe_P384_PublicKeyInfo, ECDHe_P384_LiteKeyPair } from "./RawTypes.js";
|
||||
|
||||
const subtle = globalThis.crypto.subtle;
|
||||
|
||||
export const curveName: EcKeyGenParams | EcKeyImportParams = {
|
||||
name: 'ECDH',
|
||||
namedCurve: 'P-384'
|
||||
}
|
||||
|
||||
export async function genECDHeKey(signKey: ECDSA_PKCS8_P384_SignKey): Promise<ECDHe_P384_KeyPair> {
|
||||
const keyPairP = subtle.generateKey(curveName, true, ['deriveKey', 'deriveBits']);
|
||||
const verifyKeyP = verifyKeyFromSignKey(signKey);
|
||||
|
||||
const rawKeyPair = await keyPairP;
|
||||
const publicKey = wrapBuffer<ECDHe_P384_PublicKey>(await subtle.exportKey('spki', rawKeyPair.publicKey));
|
||||
const privateKey = wrapBuffer<ECDHe_P384_PrivateKey>(await subtle.exportKey('pkcs8', rawKeyPair.privateKey));
|
||||
|
||||
const signatureP = ECDSA_sign(signKey, privateKey);
|
||||
|
||||
return {
|
||||
publicInfo: {
|
||||
publicKey,
|
||||
verifyKey: await verifyKeyP,
|
||||
sign: await signatureP
|
||||
},
|
||||
privateKey,
|
||||
}
|
||||
}
|
||||
|
||||
export async function genECDHeLiteKey(): Promise<ECDHe_P384_LiteKeyPair> {
|
||||
const keyPair = await subtle.generateKey(curveName, true, ['deriveKey', 'deriveBits']);
|
||||
const publicKey = wrapBuffer<ECDHe_P384_PublicKey>(await subtle.exportKey('spki', keyPair.publicKey));
|
||||
const privateKey = wrapBuffer<ECDHe_P384_PrivateKey>(await subtle.exportKey('pkcs8', keyPair.privateKey));
|
||||
|
||||
return {
|
||||
publicKey,
|
||||
privateKey
|
||||
};
|
||||
}
|
||||
|
||||
export async function deriveKey(other: ECDHe_P384_PublicKeyInfo, me: ECDHe_P384_LiteKeyPair | ECDHe_P384_KeyPair, keySizeBit?: number): Promise<ArrayBuffer>;
|
||||
export async function deriveKey(other: ECDHe_P384_PublicKeyInfo | ECDHe_P384_PublicKey, me: ECDHe_P384_KeyPair, keySizeBit?: number): Promise<ArrayBuffer>;
|
||||
|
||||
export async function deriveKey(other: ECDHe_P384_PublicKeyInfo | ECDHe_P384_PublicKey, me: ECDHe_P384_KeyPair | ECDHe_P384_LiteKeyPair, keySizeBit = 256): Promise<ArrayBuffer> {
|
||||
|
||||
const rawPublicKey = other instanceof Uint8Array ? other : other.publicKey;
|
||||
const rawPrivateKey = me.privateKey;
|
||||
const publicKeyP = subtle.importKey('spki', rawPublicKey, curveName, true, ['deriveBits']);
|
||||
const privateKeyP = subtle.importKey('pkcs8', rawPrivateKey, curveName, true, ['deriveBits']);
|
||||
|
||||
if (!(other instanceof Uint8Array) && !(await ECDSA_verify(other.verifyKey, other.publicKey, other.sign))) {
|
||||
throw new Error("Invalid signature");
|
||||
}
|
||||
|
||||
return await subtle.deriveBits({
|
||||
name: 'ECDH',
|
||||
public: await publicKeyP
|
||||
|
||||
}, await privateKeyP, keySizeBit);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { AES_GCM_Decrypt, AES_GCM_Encrypt } from "./AES.js";
|
||||
import { deriveKey } from "./ECDHe.js";
|
||||
import type { ECDHe_P384_KeyPair, ECDHe_P384_LiteKeyPair, ECDHe_P384_PublicKey, ECDHe_P384_PublicKeyInfo } from "./RawTypes.js";
|
||||
|
||||
export type EncryptedItem = {
|
||||
ct: Uint8Array;
|
||||
}
|
||||
|
||||
const keySizeBit = 256;
|
||||
|
||||
export async function ECDHe_AES_GCM_Encrypt(other: ECDHe_P384_PublicKeyInfo, me: ECDHe_P384_LiteKeyPair | ECDHe_P384_KeyPair, iv: BufferSource, msg: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>;
|
||||
export async function ECDHe_AES_GCM_Encrypt(other: ECDHe_P384_PublicKey, me: ECDHe_P384_KeyPair, iv: BufferSource, msg: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>;
|
||||
export async function ECDHe_AES_GCM_Encrypt(other: ECDHe_P384_PublicKeyInfo, me: ECDHe_P384_LiteKeyPair, iv: BufferSource, msg: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>;
|
||||
|
||||
export async function ECDHe_AES_GCM_Encrypt(other: ECDHe_P384_PublicKeyInfo | ECDHe_P384_PublicKey, me: ECDHe_P384_KeyPair | ECDHe_P384_LiteKeyPair, iv: BufferSource, msg: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>{
|
||||
let key: ArrayBuffer;
|
||||
if('publicInfo' in me){
|
||||
key = await deriveKey(other, me, keySizeBit);
|
||||
}
|
||||
else if('publicKey' in other){
|
||||
key = await deriveKey(other, me, keySizeBit);
|
||||
}
|
||||
else{
|
||||
throw new Error("Invalid argument: may be trying to do ECDH, not ECDHe.");
|
||||
}
|
||||
|
||||
return AES_GCM_Encrypt(key, iv, msg, aad);
|
||||
}
|
||||
|
||||
export async function ECDHe_AES_GCM_Decrypt(other: ECDHe_P384_PublicKeyInfo, me: ECDHe_P384_LiteKeyPair | ECDHe_P384_KeyPair, iv: BufferSource, ciphertext: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>;
|
||||
export async function ECDHe_AES_GCM_Decrypt(other: ECDHe_P384_PublicKey, me: ECDHe_P384_KeyPair, iv: BufferSource, ciphertext: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>;
|
||||
export async function ECDHe_AES_GCM_Decrypt(other: ECDHe_P384_PublicKeyInfo, me: ECDHe_P384_LiteKeyPair, iv: BufferSource, ciphertext: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>;
|
||||
|
||||
|
||||
export async function ECDHe_AES_GCM_Decrypt(other: ECDHe_P384_PublicKeyInfo | ECDHe_P384_PublicKey, me: ECDHe_P384_KeyPair | ECDHe_P384_LiteKeyPair, iv: BufferSource, ciphertext: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>{
|
||||
let key: ArrayBuffer;
|
||||
if('publicInfo' in me){
|
||||
key = await deriveKey(other, me, keySizeBit);
|
||||
}
|
||||
else if('publicKey' in other){
|
||||
key = await deriveKey(other, me, keySizeBit);
|
||||
}
|
||||
else{
|
||||
throw new Error("Invalid argument: may be trying to do ECDH, not ECDHe.");
|
||||
}
|
||||
|
||||
return AES_GCM_Decrypt(key, iv, ciphertext, aad);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ECDSA_sign_bson, ECDSA_verify_bson } from "./ECDSA.js";
|
||||
import { genKey } from "./ECKey.js";
|
||||
import { randomBytes } from "./utils.js";
|
||||
test('basic sign', async () => {
|
||||
const [signKey, verifyKey] = await genKey();
|
||||
|
||||
const misVerifyKey = Buffer.alloc(verifyKey.length);
|
||||
verifyKey.copy(misVerifyKey);
|
||||
misVerifyKey[misVerifyKey.length - 2] ^= 1; //1bit
|
||||
|
||||
const msg = randomBytes(100);
|
||||
const misMsg = Buffer.alloc(msg.length);
|
||||
msg.copy(misMsg);
|
||||
misMsg[1] ^= 1;
|
||||
|
||||
const signature = await ECDSA_sign_bson(signKey, msg);
|
||||
expect(signature.length).toEqual(96);
|
||||
|
||||
const misSignature = Buffer.alloc(signature.length);
|
||||
signature.copy(misSignature);
|
||||
misSignature[misSignature.length - 2] ^= 3;
|
||||
|
||||
const t0P = ECDSA_verify_bson(verifyKey, msg, signature);
|
||||
const f1P = ECDSA_verify_bson(misVerifyKey, msg, signature);
|
||||
const f2P = ECDSA_verify_bson(verifyKey, misMsg, signature);
|
||||
const f3P = ECDSA_verify_bson(verifyKey, msg, misSignature);
|
||||
|
||||
expect(await t0P).toEqual(true);
|
||||
expect(await f1P).toEqual(false);
|
||||
expect(await f2P).toEqual(false);
|
||||
expect(await f3P).toEqual(false);
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
import { BSON } from "bson";
|
||||
import { type Bsonifiable, bsonify, type Jsonifiable, jsonify } from "@sammo/util";
|
||||
import { importSignKey, importVerifyKey } from "./ECKey.js";
|
||||
import type { ECDSASignature, ECDSA_PKCS8_P384_SignKey, ECDSA_P384_VerifyKey, ECDSASignatureBSON, ECDSASignatureJSON, ECDSASignatureRaw } from "./RawTypes.js";
|
||||
import { isBufferSource } from './types.js';
|
||||
import { isArray, isDate, isObject } from "lodash-es";
|
||||
|
||||
// 간편하게 저장하기 위해서 그냥! 매번 복잡한 일을 하도록 하자
|
||||
const subtle = globalThis.crypto.subtle;
|
||||
|
||||
const hashName: EcdsaParams = {
|
||||
name: 'ECDSA',
|
||||
hash: 'SHA-512'
|
||||
}
|
||||
|
||||
export async function ECDSA_sign(sign_key: ECDSA_PKCS8_P384_SignKey, msg: BufferSource): Promise<ECDSASignatureRaw> {
|
||||
const subtleSignKeyP = importSignKey(sign_key);
|
||||
return Buffer.from(await subtle.sign(hashName, await subtleSignKeyP, msg));
|
||||
}
|
||||
|
||||
|
||||
export async function ECDSA_sign_bson(sign_key: ECDSA_PKCS8_P384_SignKey, msg: Bsonifiable): Promise<ECDSASignatureBSON>
|
||||
export async function ECDSA_sign_bson(sign_key: ECDSA_PKCS8_P384_SignKey, msg: BufferSource): Promise<ECDSASignatureRaw>
|
||||
export async function ECDSA_sign_bson(sign_key: ECDSA_PKCS8_P384_SignKey, msg: Bsonifiable | BufferSource): Promise<ECDSASignatureRaw | ECDSASignatureBSON> {
|
||||
if (!isBufferSource(msg)) {
|
||||
msg = BSON.serialize(bsonify(msg as Record<string, string>));
|
||||
return await ECDSA_sign(sign_key, msg) as ECDSASignatureBSON;
|
||||
}
|
||||
return ECDSA_sign(sign_key, msg);
|
||||
}
|
||||
|
||||
export async function ECDSA_sign_json(sign_key: ECDSA_PKCS8_P384_SignKey, msg: Bsonifiable): Promise<ECDSASignatureJSON>
|
||||
export async function ECDSA_sign_json(sign_key: ECDSA_PKCS8_P384_SignKey, msg: BufferSource): Promise<ECDSASignatureRaw>
|
||||
export async function ECDSA_sign_json(sign_key: ECDSA_PKCS8_P384_SignKey, msg: Jsonifiable | BufferSource): Promise<ECDSASignatureRaw | ECDSASignatureJSON> {
|
||||
if (!isBufferSource(msg)) {
|
||||
msg = Buffer.from(JSON.stringify(jsonify(msg as Record<string, string>)), 'utf-8');
|
||||
return await ECDSA_sign(sign_key, msg) as ECDSASignatureJSON;
|
||||
}
|
||||
return ECDSA_sign(sign_key, msg);
|
||||
}
|
||||
|
||||
export async function ECDSA_verify(verify_key: ECDSA_P384_VerifyKey, msg: BufferSource, sign: ECDSASignature): Promise<boolean> {
|
||||
try {
|
||||
const subtleVerifyKeyP = importVerifyKey(verify_key);
|
||||
return await subtle.verify(hashName, await subtleVerifyKeyP, sign, msg);
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function ECDSA_verify_bson(verify_key: ECDSA_P384_VerifyKey, msg: Bsonifiable, sign: ECDSASignatureBSON): Promise<boolean>;
|
||||
export async function ECDSA_verify_bson(verify_key: ECDSA_P384_VerifyKey, msg: BufferSource, sign: ECDSASignatureRaw): Promise<boolean>;
|
||||
export async function ECDSA_verify_bson(verify_key: ECDSA_P384_VerifyKey, msg: Bsonifiable | BufferSource, sign: ECDSASignatureBSON | ECDSASignatureRaw): Promise<boolean> {
|
||||
|
||||
try {
|
||||
if (!isBufferSource(msg)) {
|
||||
msg = BSON.serialize(bsonify(msg as Record<string, string>));
|
||||
}
|
||||
return ECDSA_verify(verify_key, msg, sign);
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function ECDSA_verify_json(verify_key: ECDSA_P384_VerifyKey, msg: Bsonifiable, sign: ECDSASignatureJSON): Promise<boolean>;
|
||||
export async function ECDSA_verify_json(verify_key: ECDSA_P384_VerifyKey, msg: BufferSource, sign: ECDSASignatureRaw): Promise<boolean>;
|
||||
export async function ECDSA_verify_json(verify_key: ECDSA_P384_VerifyKey, msg: Jsonifiable | BufferSource, sign: ECDSASignatureJSON | ECDSASignatureRaw): Promise<boolean> {
|
||||
|
||||
try {
|
||||
if (!isBufferSource(msg)) {
|
||||
const x = JSON.stringify(jsonify(msg as Record<string, string>));
|
||||
msg = Buffer.from(x, 'utf-8');
|
||||
}
|
||||
return ECDSA_verify(verify_key, msg, sign);
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { genKey, verifyKeyFromSignKey, pemFromVerifyKey, importVerifyKey, importSignKey, pemFromSignKey } from "./ECKey.js";
|
||||
import { TypeLength } from "./TypeLength.js";
|
||||
import { decodePEM } from "./utils.js";
|
||||
|
||||
test('gen_key', async () => {
|
||||
const [signKey, verifyKey] = await genKey();
|
||||
|
||||
expect(signKey.length).toEqual(TypeLength.ECDSA_PKCS8_P384_SignKey);
|
||||
expect(verifyKey.length).toEqual(TypeLength.ECDSA_SPKI_P384_VerifyKey);
|
||||
|
||||
const pemVerifyKey = await pemFromVerifyKey(verifyKey);
|
||||
const verifyKey2 = decodePEM(pemVerifyKey, 'PUBLIC KEY')[0];
|
||||
expect(verifyKey).toEqual(verifyKey2);
|
||||
|
||||
});
|
||||
|
||||
test('convert', async () => {
|
||||
const [signKey, verifyKey] = await genKey();
|
||||
|
||||
const newVerifyKey = await verifyKeyFromSignKey(signKey);
|
||||
|
||||
expect(verifyKey).toEqual(newVerifyKey);
|
||||
})
|
||||
|
||||
test('import sign_key', async() => {
|
||||
const [signKey, verifyKey] = await genKey();
|
||||
|
||||
const pemSignKey = pemFromSignKey(signKey);
|
||||
const signKey2 = decodePEM(pemSignKey, 'EC PRIVATE KEY')[0];
|
||||
|
||||
const signKeyObj = await importSignKey(signKey, true);
|
||||
const signKeyObj2 = await importSignKey(signKey2, true);
|
||||
|
||||
expect(signKeyObj).toEqual(signKeyObj2);
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
|
||||
import type { ECDSA_PKCS8_P384_SignKey, ECDSA_P384_VerifyKey } from "./RawTypes.js";
|
||||
import { encodePEM } from "./utils.js";
|
||||
|
||||
// 간편하게 저장하기 위해서 그냥! 매번 복잡한 일을 하도록 하자
|
||||
|
||||
const subtle = globalThis.crypto.subtle;
|
||||
|
||||
export const curveName: EcKeyGenParams | EcKeyImportParams = {
|
||||
name: 'ECDSA',
|
||||
namedCurve: 'P-384'
|
||||
}
|
||||
|
||||
export async function importSignKey(signKey: ECDSA_PKCS8_P384_SignKey, allowExport?: boolean): Promise<CryptoKey> {
|
||||
return await subtle.importKey('pkcs8', signKey, curveName, allowExport ?? false, ['sign']);
|
||||
}
|
||||
|
||||
export async function importVerifyKeyRaw(verifyKey: ECDSA_P384_VerifyKey, allowExport?: boolean): Promise<CryptoKey> {
|
||||
return await subtle.importKey('raw', verifyKey, curveName, allowExport ?? false, ['verify'])
|
||||
}
|
||||
|
||||
export async function exportVerifyKeyRaw(verifyKey: ECDSA_P384_VerifyKey): Promise<Buffer>{
|
||||
const key = await importVerifyKeyRaw(verifyKey);
|
||||
return Buffer.from(await subtle.exportKey('raw', key));
|
||||
}
|
||||
|
||||
export async function importVerifyKey(verifyKey: ECDSA_P384_VerifyKey, allowExport?: boolean): Promise<CryptoKey> {
|
||||
return await subtle.importKey('spki', verifyKey, curveName, allowExport ?? false, ['verify'])
|
||||
}
|
||||
|
||||
|
||||
export async function genKey(): Promise<[ECDSA_PKCS8_P384_SignKey, ECDSA_P384_VerifyKey]> {
|
||||
const keyPair = await subtle.generateKey(curveName, true, ['sign', 'verify']);
|
||||
const signKey = subtle.exportKey('pkcs8', keyPair.privateKey);
|
||||
const verifyKey = subtle.exportKey('spki', keyPair.publicKey);
|
||||
return [Buffer.from(await signKey), Buffer.from(await verifyKey)];
|
||||
}
|
||||
|
||||
export async function pemFromVerifyKey(verifyKey: ECDSA_P384_VerifyKey): Promise<string> {
|
||||
const subtleVerifyKey = await importVerifyKey(verifyKey, true);
|
||||
const spkiRaw = await subtle.exportKey('spki', subtleVerifyKey)
|
||||
return encodePEM(Buffer.from(spkiRaw), 'PUBLIC KEY');
|
||||
}
|
||||
|
||||
export function pemFromSignKey(signKey: ECDSA_PKCS8_P384_SignKey): string {
|
||||
return encodePEM(signKey, 'EC PRIVATE KEY');
|
||||
}
|
||||
|
||||
export async function verifyKeyFromSignKey(signKey: ECDSA_PKCS8_P384_SignKey): Promise<ECDSA_P384_VerifyKey> {
|
||||
const subtleSignKey = await importSignKey(signKey, true);
|
||||
const jwk = await subtle.exportKey('jwk', subtleSignKey);
|
||||
delete jwk.d;
|
||||
delete jwk.dp;
|
||||
delete jwk.dq;
|
||||
delete jwk.q;
|
||||
delete jwk.qi;
|
||||
jwk.key_ops = ["verify"];
|
||||
|
||||
const subtleVerifyKey = await subtle.importKey('jwk', jwk, curveName, true, ['verify']);
|
||||
const verifyKey = await subtle.exportKey('spki', subtleVerifyKey);
|
||||
return Buffer.from(verifyKey);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
const subtle = globalThis.crypto.subtle;
|
||||
|
||||
export type ValidEncAlg = 'AES256-GCM' | 'AES128-GCM' | 'AES256-CBC' | 'AES128-CBC';
|
||||
|
||||
const encAlgMap: Record<ValidEncAlg, AesDerivedKeyParams> = {
|
||||
'AES256-GCM': {
|
||||
name: 'AES-GCM',
|
||||
length: 256,
|
||||
},
|
||||
'AES128-GCM' : {
|
||||
name: 'AES-GCM',
|
||||
length: 128,
|
||||
},
|
||||
'AES256-CBC' : {
|
||||
name: 'AES-CBC',
|
||||
length: 256,
|
||||
},
|
||||
'AES128-CBC' : {
|
||||
name: 'AES-CBC',
|
||||
length: 128,
|
||||
},
|
||||
}
|
||||
|
||||
export async function PBKDF2_SHA512(password: string, salt: BufferSource, alg: ValidEncAlg = 'AES256-GCM', iterations = 600000): Promise<CryptoKey> {
|
||||
const param: Pbkdf2Params = {
|
||||
name: 'PBKDF2',
|
||||
hash: 'SHA-512',
|
||||
salt: salt,
|
||||
iterations,
|
||||
};
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const baseKey = await subtle.importKey(
|
||||
"raw",
|
||||
encoder.encode(password),
|
||||
{ name: "PBKDF2" },
|
||||
false,
|
||||
["deriveBits", "deriveKey"]
|
||||
);
|
||||
|
||||
return await subtle.deriveKey(
|
||||
param,
|
||||
baseKey,
|
||||
encAlgMap[alg],
|
||||
true,
|
||||
["encrypt", "decrypt"]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { WrappedBuffer } from "@sammo/util";
|
||||
|
||||
export interface ECDSA_PKCS8_P384_SignKey extends WrappedBuffer{
|
||||
_w_type?: "ECDSA_PKCS8_P384_SignKey";
|
||||
}
|
||||
|
||||
export interface ECDSA_P384_VerifyKey extends WrappedBuffer{
|
||||
_w_type?: "ECDSA_P384_VerifyKey";
|
||||
}
|
||||
|
||||
export interface ECDSASignatureRaw extends WrappedBuffer{
|
||||
_w_type?: "ECDSASignature";
|
||||
}
|
||||
|
||||
export interface ECDSASignatureBSON extends WrappedBuffer{
|
||||
_w_type?: "ECDSASignatureBSON";
|
||||
}
|
||||
|
||||
export interface ECDSASignatureJSON extends WrappedBuffer{
|
||||
_w_type?: "ECDSASignatureJSON";
|
||||
}
|
||||
|
||||
export type ECDSASignature = ECDSASignatureRaw | ECDSASignatureBSON | ECDSASignatureJSON;
|
||||
|
||||
export interface ECDHe_P384_PublicKey extends WrappedBuffer{
|
||||
_w_type?: "ECDHe_P384_PublicKey";
|
||||
}
|
||||
|
||||
export interface ECDHe_P384_PrivateKey extends WrappedBuffer{
|
||||
_w_type?: "ECDHe_P384_PrivateKey";
|
||||
}
|
||||
|
||||
export interface ECDHe_P384_LiteKeyPair {
|
||||
publicKey: ECDHe_P384_PublicKey;
|
||||
privateKey: ECDHe_P384_PrivateKey;
|
||||
}
|
||||
|
||||
export interface ECDHe_P384_PublicKeyInfo {
|
||||
publicKey: ECDHe_P384_PublicKey;
|
||||
verifyKey: ECDSA_P384_VerifyKey;
|
||||
sign: ECDSASignatureRaw; //sign of publicKey
|
||||
}
|
||||
export interface ECDHe_P384_KeyPair{
|
||||
publicInfo: ECDHe_P384_PublicKeyInfo;
|
||||
privateKey: ECDHe_P384_PrivateKey;
|
||||
}
|
||||
|
||||
export {
|
||||
TypeLength
|
||||
} from "./TypeLength.js";
|
||||
@@ -0,0 +1,10 @@
|
||||
import { sha256, sha512 } from "./SHA2.js";
|
||||
|
||||
test('sha2', async () => {
|
||||
const msg = Buffer.from('hello');
|
||||
const answer256 = Buffer.from('2CF24DBA5FB0A30E26E83B2AC5B9E29E1B161E5C1FA7425E73043362938B9824', 'hex');
|
||||
const answer512 = Buffer.from('9B71D224BD62F3785D96D46AD3EA3D73319BFBC2890CAADAE2DFF72519673CA72323C3D99BA5C11D7C7ACC6E14B8C5DA0C4663475C2E5C3ADEF46F73BCDEC043', 'hex');
|
||||
|
||||
expect(await sha256(msg)).toEqual(answer256.buffer);
|
||||
expect(await sha512(msg)).toEqual(answer512.buffer);
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { isBufferSource } from "./types.js";
|
||||
import { BSON } from "bson";
|
||||
import { type Bsonifiable, bsonify } from "@sammo/util";
|
||||
import { isArray, isDate, isObject } from "lodash-es";
|
||||
const subtle = globalThis.crypto.subtle;
|
||||
|
||||
export async function sha256(msg: Bsonifiable | BufferSource): Promise<ArrayBuffer> {
|
||||
if (!isBufferSource(msg)) {
|
||||
msg = BSON.serialize(bsonify(msg));
|
||||
}
|
||||
return await subtle.digest('SHA-256', msg);
|
||||
}
|
||||
|
||||
export async function sha512(msg: Bsonifiable | BufferSource): Promise<ArrayBuffer> {
|
||||
if (!isBufferSource(msg)) {
|
||||
msg = BSON.serialize(bsonify(msg));
|
||||
}
|
||||
return await subtle.digest('SHA-512', msg);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum TypeLength {
|
||||
//ECDSA P384 서명
|
||||
ECDSA_PKCS8_P384_SignKey = 185,
|
||||
ECDSA_SPKI_P384_VerifyKey = 120,
|
||||
ECDSASignature = 96,
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
export class NotYetImplemented extends Error {
|
||||
public override name = 'NotYetImplemented';
|
||||
override toString(): string {
|
||||
if (this.message) {
|
||||
return this.name + ': ' + this.message;
|
||||
}
|
||||
else {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class InvalidArgument extends Error {
|
||||
public override name = 'InvalidArgument';
|
||||
override toString(): string {
|
||||
if (this.message) {
|
||||
return this.name + ': ' + this.message;
|
||||
}
|
||||
else {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidVType extends InvalidArgument {
|
||||
public override name = 'InvalidVType';
|
||||
}
|
||||
|
||||
export class InvalidArgumentBufferSize extends InvalidArgument {
|
||||
public override name = 'InvalidArgumentBufferSize';
|
||||
}
|
||||
|
||||
export class RuntimeError extends Error {
|
||||
public override name = 'RuntimeError';
|
||||
constructor(public override message: string = '') {
|
||||
super(message);
|
||||
}
|
||||
override toString(): string {
|
||||
if (this.message) {
|
||||
return this.name + ': ' + this.message;
|
||||
}
|
||||
else {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NotNullExpected extends RuntimeError {
|
||||
public override name = 'NotNullExpected';
|
||||
}
|
||||
|
||||
export class PrivilegedGenViolation extends RuntimeError {
|
||||
public override name = 'PrivilegedGenViolation';
|
||||
}
|
||||
|
||||
export type MergeableBuffer = MergeableBuffer[] | Buffer;
|
||||
|
||||
export function calcMergeableBuffer(item: MergeableBuffer): number {
|
||||
if (item instanceof Buffer) {
|
||||
return item.byteLength;
|
||||
}
|
||||
let bufferSize = 0;
|
||||
for (const subItem of item) {
|
||||
bufferSize += calcMergeableBuffer(subItem);
|
||||
}
|
||||
return bufferSize;
|
||||
}
|
||||
|
||||
export function mergeBuffer(...buffers: MergeableBuffer[]): Buffer {
|
||||
if (buffers.length == 0) {
|
||||
return Buffer.alloc(0);
|
||||
}
|
||||
if (buffers.length == 1) {
|
||||
const item = buffers[0];
|
||||
if (item instanceof Buffer) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
const fillBuffer = function (item: MergeableBuffer, bufferIdx: number): number {
|
||||
if (item instanceof Buffer) {
|
||||
result.set(item, bufferIdx);
|
||||
bufferIdx += item.byteLength;
|
||||
return bufferIdx;
|
||||
}
|
||||
for (const subItem of item) {
|
||||
bufferIdx = fillBuffer(subItem, bufferIdx);
|
||||
}
|
||||
return bufferIdx;
|
||||
}
|
||||
|
||||
const bufferSize = calcMergeableBuffer(buffers);
|
||||
const result = Buffer.alloc(bufferSize);
|
||||
|
||||
fillBuffer(buffers, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
type ErrType<T> = { new(msg?: string): T }
|
||||
type Nullable<T> = T | null | undefined
|
||||
|
||||
export function unwrap<T>(result: Nullable<T>): T {
|
||||
if (result === null || result === undefined) {
|
||||
throw new NotNullExpected();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function unwrap_err<T, ErrT extends Error>(result: Nullable<T>, errType: ErrType<ErrT>, errMsg?: string): T {
|
||||
if (result === null || result === undefined) {
|
||||
throw new errType(errMsg);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export * as AES from './AES.js';
|
||||
export * as ECDSA from './ECDSA.js';
|
||||
export * as ECKey from './ECKey.js';
|
||||
export * as PBKDF2 from './PBKDF2.js';
|
||||
export * as SHA2 from './SHA2.js';
|
||||
export * as ECDHe from './ECDHe.js';
|
||||
export * as ECDHe_AES from './ECDHe_AES.js';
|
||||
|
||||
export * as RawTypes from './RawTypes.js';
|
||||
|
||||
export * from './utils.js';
|
||||
export * from './types.js';
|
||||
export { TypeLength } from './TypeLength.js';
|
||||
@@ -0,0 +1,14 @@
|
||||
export type BufferSource = ArrayBufferView | ArrayBuffer | SharedArrayBuffer;
|
||||
|
||||
export function isBufferSource(obj: unknown): obj is BufferSource {
|
||||
if (obj instanceof ArrayBuffer){
|
||||
return true;
|
||||
}
|
||||
if('SharedArrayBuffer' in globalThis && obj instanceof SharedArrayBuffer){
|
||||
return true;
|
||||
}
|
||||
if (ArrayBuffer.isView(obj)){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { WrappedBuffer } from "@sammo/util";
|
||||
|
||||
const crypto = globalThis.crypto;
|
||||
|
||||
export function randomBytes(length: number): Buffer {
|
||||
const buffer = Buffer.alloc(length);
|
||||
crypto.getRandomValues(buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
//TODO: 필요할때마다 확장
|
||||
export type ValidPEMType = 'PUBLIC KEY' | 'EC PRIVATE KEY' | 'CERTIFICATE';
|
||||
|
||||
/**
|
||||
* PEM string에 내부 타입으로 WrappedBuffer를 보관한 형태
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export type PEMString<T extends Buffer, S extends ValidPEMType> = string & {
|
||||
/** 타입구분자. 항상 undefined일 것이다 */
|
||||
_pem_b_type?: T;
|
||||
_pem_type?: S;
|
||||
}
|
||||
|
||||
export function encodePEM<T extends WrappedBuffer, S extends ValidPEMType>(data: T, pemType: S): PEMString<T, S>;
|
||||
export function encodePEM(data: Buffer, pemType: ValidPEMType): string;
|
||||
export function encodePEM(data: Buffer, pemType: ValidPEMType): string {
|
||||
const base64text = data.toString('base64');
|
||||
const splitText = base64text.match(/.{1,64}/g)?.join('\n') ?? '';
|
||||
|
||||
return `-----BEGIN ${pemType}-----
|
||||
${splitText}
|
||||
-----END ${pemType}-----
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type InferPEMType<T extends PEMString<any, any>> = Exclude<undefined, T['_pem_type']>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type InferPEMBuffer<T extends PEMString<any, any>> = Exclude<undefined, T['_pem_b_type']>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function decodePEM<T extends PEMString<any, any>>(pem: T, pemType: InferPEMType<T>): InferPEMBuffer<T>[];
|
||||
export function decodePEM(pem: string, pemType?: ValidPEMType): Buffer[];
|
||||
export function decodePEM(pem: string, pemType?: ValidPEMType): Buffer[] {
|
||||
const tag = pemType ?? "[A-Z0-9 ]+";
|
||||
const pattern = new RegExp(`-{5}BEGIN ${tag}-{5}([a-zA-Z0-9=+\\/\\n\\r]+)-{5}END ${tag}-{5}`, "g");
|
||||
|
||||
const res: Buffer[] = [];
|
||||
let matches: RegExpExecArray | null = null;
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while (matches = pattern.exec(pem)) {
|
||||
const base64 = matches[1]
|
||||
.replace(/\r/g, "")
|
||||
.replace(/\n/g, "");
|
||||
res.push(Buffer.from(base64, 'base64'));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function decodeSinglePEM<T extends PEMString<any, any>>(pem: T, pemType: InferPEMType<T>): InferPEMBuffer<T>;
|
||||
export function decodeSinglePEM(pem: string, pemType?: ValidPEMType): Buffer;
|
||||
export function decodeSinglePEM(pem: string, pemType?: ValidPEMType): Buffer {
|
||||
const res = decodePEM(pem, pemType);
|
||||
if (res.length != 1) {
|
||||
throw new Error("invalid pem");
|
||||
}
|
||||
return res[0];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"verbatimModuleSyntax": false,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../util"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@sammo/secure_token",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc --build"
|
||||
},
|
||||
"author": "",
|
||||
"type": "module",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@sammo/crypto": "workspace:^",
|
||||
"@sammo/util": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bson": "^5.4.0",
|
||||
"buffer": "^6.0.3",
|
||||
"zod": "^3.22.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bson": "^5.4.0",
|
||||
"buffer": "^6.0.3",
|
||||
"zod": "^3.22.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { sha512 } from "@sammo/crypto/SHA2";
|
||||
import { BSON } from "bson";
|
||||
import type { PlainBson } from '@sammo/util';
|
||||
import { calcBase64Len } from "@sammo/util";
|
||||
import { AES_GCM_Decrypt, AES_GCM_Encrypt } from "@sammo/crypto/AES";
|
||||
import { z } from "zod";
|
||||
|
||||
const nonceLen = 16;
|
||||
const tagLen = 16;
|
||||
|
||||
export type EncryptedSimpleItem<T> = {
|
||||
nonce: string;
|
||||
ct: string;
|
||||
}
|
||||
|
||||
export const zodEncryptedSimpleItem = z.object({
|
||||
nonce: z.string().length(calcBase64Len(nonceLen)),
|
||||
ct: z.string().min(calcBase64Len(tagLen)),
|
||||
});
|
||||
|
||||
export type DecryptedSimpleItem<T extends EncryptedSimpleItem<unknown>> = T extends EncryptedSimpleItem<infer U> ? U : never;
|
||||
|
||||
const staticPresharedTokenSecret: string | undefined = process.env.PRESHARED_SECURE_TOKEN_SECRET;
|
||||
|
||||
export async function encryptSimpleItem<T extends PlainBson>(data: T, presharedTokenSecret?: string): Promise<EncryptedSimpleItem<T>> {
|
||||
if (!presharedTokenSecret) {
|
||||
if (!staticPresharedTokenSecret) {
|
||||
throw new Error("PRESHARED_SECURE_TOKEN_SECRET is not set");
|
||||
}
|
||||
presharedTokenSecret = staticPresharedTokenSecret;
|
||||
}
|
||||
|
||||
const secretLength = Buffer.byteLength(presharedTokenSecret, 'utf8');
|
||||
const secretBuffer = Buffer.alloc(secretLength + nonceLen);
|
||||
secretBuffer.write(presharedTokenSecret, 0, secretLength, 'utf8');
|
||||
|
||||
//secretBuffer에서 Buffer를 바로 준비해도 되지만, 혹시모를 안전상의 이유로 별도로 할당하고 복사
|
||||
const nonce = Buffer.from(crypto.getRandomValues(new Uint8Array(nonceLen)));
|
||||
secretBuffer.set(nonce, secretLength);
|
||||
|
||||
const keyBuffer = Buffer.from(await sha512(secretBuffer));
|
||||
|
||||
const key = new Uint8Array(keyBuffer.buffer, 0, 32);
|
||||
const iv = new Uint8Array(keyBuffer.buffer, 32, 12);
|
||||
|
||||
const payload = BSON.serialize(data);
|
||||
|
||||
const ciphertext = Buffer.from(await AES_GCM_Encrypt(key, iv, payload));
|
||||
|
||||
return {
|
||||
nonce: nonce.toString('base64'),
|
||||
ct: ciphertext.toString('base64'),
|
||||
};
|
||||
}
|
||||
|
||||
export async function decryptSimpleItem<T extends PlainBson>(data: EncryptedSimpleItem<T>, presharedTokenSecret?: string): Promise<T> {
|
||||
if (!presharedTokenSecret) {
|
||||
if (!staticPresharedTokenSecret) {
|
||||
throw new Error("PRESHARED_SECURE_TOKEN_SECRET is not set");
|
||||
}
|
||||
presharedTokenSecret = staticPresharedTokenSecret;
|
||||
}
|
||||
|
||||
const secretLength = Buffer.byteLength(presharedTokenSecret, 'utf8');
|
||||
const secretBuffer = Buffer.alloc(secretLength + 16);
|
||||
secretBuffer.write(presharedTokenSecret, 0, secretLength, 'utf8');
|
||||
|
||||
const nonce = Buffer.from(data.nonce, 'base64');
|
||||
secretBuffer.set(nonce, secretLength);
|
||||
|
||||
const keyBuffer = Buffer.from(await sha512(secretBuffer));
|
||||
const key = new Uint8Array(keyBuffer.buffer, 0, 32);
|
||||
const iv = new Uint8Array(keyBuffer.buffer, 32, 12);
|
||||
|
||||
const ct = Buffer.from(data.ct, 'base64');
|
||||
|
||||
const payload = BSON.deserialize(Buffer.from(await AES_GCM_Decrypt(key, iv, ct))) as T satisfies DecryptedSimpleItem<EncryptedSimpleItem<T>>;
|
||||
return payload;
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import { AES_GCM_Decrypt, AES_GCM_Encrypt } from "@sammo/crypto/AES";
|
||||
import { Buffer } from "buffer";
|
||||
import { sha512 } from "@sammo/crypto/SHA2";
|
||||
import { BSON } from "bson";
|
||||
import { type Bsonifiable, bsonify, type JsonifiableObj } from '@sammo/util';
|
||||
import { type Jsonifiable, jsonify } from '@sammo/util';
|
||||
|
||||
const crypto = globalThis.crypto;
|
||||
|
||||
/**
|
||||
* Preshared Token Secret을 이용한 AES256-GCM 토큰
|
||||
* key, iv = SHA512(presharedSecret + nonce) 으로 생성
|
||||
*/
|
||||
|
||||
export type SecureEncryptedToken = {
|
||||
nonce: string; // [key, iv] = SHA512(presharedSecret + nonce)
|
||||
encrypted: 1;
|
||||
type: string; // aad[0]
|
||||
validUntil: string; // aad[1]
|
||||
payload: string; //BASE64(AES(BSON(aad),BSON(payload)))
|
||||
}
|
||||
|
||||
export type SecurePlaintextToken = {
|
||||
nonce: string; // [key, iv] = SHA512(presharedSecret + nonce)
|
||||
encrypted: 0;
|
||||
type: string; // aad[0]
|
||||
validUntil: string; // aad[1]
|
||||
payload: string; // JSON => aad[2]
|
||||
tag: string; //BASE64(AES(BSON(aad),null)))
|
||||
}
|
||||
|
||||
export type SecureToken = SecureEncryptedToken | SecurePlaintextToken;
|
||||
|
||||
const staticPresharedTokenSecret: string | undefined = process.env.PRESHARED_SECURE_TOKEN_SECRET;
|
||||
|
||||
type secureTokenAAD = {
|
||||
type: string;
|
||||
validUntilText: string;
|
||||
}
|
||||
type plaintextTokenAAD = {
|
||||
type: string;
|
||||
validUntilText: string;
|
||||
jsonPayload: string;
|
||||
}
|
||||
|
||||
export async function generateSecureEncryptedToken<T extends Bsonifiable>(validUntil: Date, type: string, payload: T, presharedTokenSecret?: string): Promise<SecureEncryptedToken> {
|
||||
if (!presharedTokenSecret) {
|
||||
if (!staticPresharedTokenSecret) {
|
||||
throw new Error("PRESHARED_SECURE_TOKEN_SECRET is not set");
|
||||
}
|
||||
presharedTokenSecret = staticPresharedTokenSecret;
|
||||
}
|
||||
|
||||
const secretLength = Buffer.byteLength(presharedTokenSecret, 'utf8');
|
||||
const secretBuffer = Buffer.alloc(secretLength + 16);
|
||||
secretBuffer.write(presharedTokenSecret, 0, secretLength, 'utf8');
|
||||
|
||||
//secretBuffer에서 Buffer를 바로 준비해도 되지만, 혹시모를 안전상의 이유로 별도로 할당하고 복사
|
||||
const nonce = Buffer.from(crypto.getRandomValues(new Uint8Array(16)));
|
||||
secretBuffer.set(nonce, secretLength);
|
||||
|
||||
const keyBuffer = Buffer.from(await sha512(secretBuffer));
|
||||
|
||||
const key = new Uint8Array(keyBuffer.buffer, 0, 32);
|
||||
const iv = new Uint8Array(keyBuffer.buffer, 32, 12);
|
||||
|
||||
const validUntilText = validUntil.toISOString();
|
||||
|
||||
const aad: secureTokenAAD = {
|
||||
type, validUntilText
|
||||
};
|
||||
|
||||
const aadRaw = BSON.serialize(aad);
|
||||
const payloadRaw = BSON.serialize(bsonify(payload as Record<string, string>));
|
||||
|
||||
const ciphertext = Buffer.from(await AES_GCM_Encrypt(key, iv, payloadRaw, aadRaw));
|
||||
|
||||
return {
|
||||
nonce: nonce.toString('base64'),
|
||||
encrypted: 1,
|
||||
type,
|
||||
validUntil: validUntilText,
|
||||
payload: ciphertext.toString('base64'),
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseSecureEncryptedToken<T extends object>(secureToken: SecureEncryptedToken, presharedTokenSecret?: string): Promise<T> {
|
||||
const now = new Date();
|
||||
const validUntil = new Date(secureToken.validUntil);
|
||||
if (now > validUntil) {
|
||||
throw new Error("token expired");
|
||||
}
|
||||
|
||||
if (!secureToken.encrypted) {
|
||||
throw new Error("token is not encrypted");
|
||||
}
|
||||
|
||||
if (!presharedTokenSecret) {
|
||||
if (!staticPresharedTokenSecret) {
|
||||
throw new Error("PRESHARED_SECURE_TOKEN_SECRET is not set");
|
||||
}
|
||||
presharedTokenSecret = staticPresharedTokenSecret;
|
||||
}
|
||||
|
||||
const secretLength = Buffer.byteLength(presharedTokenSecret, 'utf8');
|
||||
const secretBuffer = Buffer.alloc(secretLength + 16);
|
||||
secretBuffer.write(presharedTokenSecret, 0, secretLength, 'utf8');
|
||||
|
||||
const nonce = Buffer.from(secureToken.nonce, 'base64');
|
||||
secretBuffer.set(nonce, secretLength);
|
||||
|
||||
const keyBuffer = Buffer.from(await sha512(secretBuffer));
|
||||
const key = new Uint8Array(keyBuffer.buffer, 0, 32);
|
||||
const iv = new Uint8Array(keyBuffer.buffer, 32, 12);
|
||||
|
||||
const aad: secureTokenAAD = { type: secureToken.type, validUntilText: secureToken.validUntil };
|
||||
|
||||
const aadRaw = BSON.serialize(aad);
|
||||
const payloadCiphertext = Buffer.from(secureToken.payload, 'base64');
|
||||
|
||||
const payloadRaw = new Uint8Array(await AES_GCM_Decrypt(key, iv, payloadCiphertext, aadRaw));
|
||||
const payload = BSON.deserialize(payloadRaw);
|
||||
|
||||
return payload as T;
|
||||
}
|
||||
|
||||
export async function generateSecurePlaintextToken<T extends Jsonifiable>(validUntil: Date, type: string, payload: T, presharedTokenSecret?: string): Promise<SecurePlaintextToken> {
|
||||
if (!presharedTokenSecret) {
|
||||
if (!staticPresharedTokenSecret) {
|
||||
throw new Error("PRESHARED_SECURE_TOKEN_SECRET is not set");
|
||||
}
|
||||
presharedTokenSecret = staticPresharedTokenSecret;
|
||||
}
|
||||
|
||||
const secretLength = Buffer.byteLength(presharedTokenSecret, 'utf8');
|
||||
const secretBuffer = Buffer.alloc(secretLength + 16);
|
||||
secretBuffer.write(presharedTokenSecret, 0, secretLength, 'utf8');
|
||||
|
||||
//secretBuffer에서 Buffer를 바로 준비해도 되지만, 혹시모를 안전상의 이유로 별도로 할당하고 복사
|
||||
const nonce = Buffer.from(crypto.getRandomValues(new Uint8Array(16)));
|
||||
secretBuffer.set(nonce, secretLength);
|
||||
|
||||
const keyBuffer = Buffer.from(await sha512(secretBuffer));
|
||||
|
||||
const key = new Uint8Array(keyBuffer.buffer, 0, 32);
|
||||
const iv = new Uint8Array(keyBuffer.buffer, 32, 12);
|
||||
|
||||
const validUntilText = validUntil.toISOString();
|
||||
const jsonPayload = JSON.stringify(jsonify(payload as Record<string, string>));
|
||||
|
||||
const aad: plaintextTokenAAD = { type, validUntilText, jsonPayload };
|
||||
const aadRaw = BSON.serialize(aad);
|
||||
|
||||
const dummyPayload = new ArrayBuffer(0);
|
||||
|
||||
const tag = Buffer.from(await AES_GCM_Encrypt(key, iv, dummyPayload, aadRaw));
|
||||
|
||||
return {
|
||||
nonce: nonce.toString('base64'),
|
||||
encrypted: 0,
|
||||
type,
|
||||
validUntil: validUntilText,
|
||||
payload: jsonPayload,
|
||||
tag: tag.toString('base64'),
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseSecurePlaintextToken<T extends object>(secureToken: SecurePlaintextToken, presharedTokenSecret?: string): Promise<T> {
|
||||
const now = new Date();
|
||||
const validUntil = new Date(secureToken.validUntil);
|
||||
if (now > validUntil) {
|
||||
throw new Error("token expired");
|
||||
}
|
||||
|
||||
if (!presharedTokenSecret) {
|
||||
if (!staticPresharedTokenSecret) {
|
||||
throw new Error("PRESHARED_SECURE_TOKEN_SECRET is not set");
|
||||
}
|
||||
presharedTokenSecret = staticPresharedTokenSecret;
|
||||
}
|
||||
|
||||
if (secureToken.encrypted) {
|
||||
throw new Error("token is encrypted");
|
||||
}
|
||||
|
||||
const secretLength = Buffer.byteLength(presharedTokenSecret, 'utf8');
|
||||
const secretBuffer = Buffer.alloc(secretLength + 16);
|
||||
secretBuffer.write(presharedTokenSecret, 0, secretLength, 'utf8');
|
||||
|
||||
const nonce = Buffer.from(secureToken.nonce, 'base64');
|
||||
secretBuffer.set(nonce, secretLength);
|
||||
|
||||
const keyBuffer = Buffer.from(await sha512(secretBuffer));
|
||||
const key = new Uint8Array(keyBuffer.buffer, 0, 32);
|
||||
const iv = new Uint8Array(keyBuffer.buffer, 32, 12);
|
||||
|
||||
const aad: plaintextTokenAAD = {
|
||||
type: secureToken.type,
|
||||
validUntilText: secureToken.validUntil,
|
||||
jsonPayload: secureToken.payload
|
||||
};
|
||||
const aadRaw = BSON.serialize(aad);
|
||||
|
||||
const tag = Buffer.from(secureToken.tag, 'base64');
|
||||
|
||||
await AES_GCM_Decrypt(key, iv, tag, aadRaw);
|
||||
|
||||
return JSON.parse(secureToken.payload);
|
||||
}
|
||||
|
||||
export async function generateSecureToken<T extends JsonifiableObj>(encrypted: boolean, validUntil: Date, type: string, payload: T, presharedTokenSecret?: string): Promise<SecureToken> {
|
||||
if (encrypted) {
|
||||
return await generateSecureEncryptedToken(validUntil, type, payload, presharedTokenSecret);
|
||||
} else {
|
||||
return await generateSecurePlaintextToken(validUntil, type, payload, presharedTokenSecret);
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseSecureToken<T extends object>(secureToken: SecureToken, presharedTokenSecret?: string): Promise<T> {
|
||||
if (secureToken.encrypted) {
|
||||
return await parseSecureEncryptedToken(secureToken, presharedTokenSecret);
|
||||
} else {
|
||||
return await parseSecurePlaintextToken(secureToken, presharedTokenSecret);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./SecureToken.js"
|
||||
export * from "./zodSecureToken.js"
|
||||
|
||||
export * from "./EncryptedSimple.js"
|
||||
@@ -0,0 +1,52 @@
|
||||
import { z } from "zod";
|
||||
import type { SecureEncryptedToken, SecurePlaintextToken } from "./SecureToken.js";
|
||||
|
||||
export function zodEncryptedSecureToken(type?: string) {
|
||||
const obj = z.object({
|
||||
nonce: z.string(),
|
||||
encrypted: z.literal(1),
|
||||
type: z.string(),
|
||||
validUntil: z.string().datetime(),
|
||||
payload: z.string(),
|
||||
}).refine((v) => {
|
||||
const validUntil = new Date(v.validUntil);
|
||||
const now = new Date();
|
||||
return validUntil > now;
|
||||
|
||||
}, "token expired"
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) satisfies z.ZodType<any, any, SecureEncryptedToken>;
|
||||
|
||||
if (type) {
|
||||
return obj.refine((v) => {
|
||||
return v.type === type;
|
||||
}, "token type mismatch")
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function zodPlaintextSecureToken(type: string) {
|
||||
const obj = z.object({
|
||||
nonce: z.string(),
|
||||
encrypted: z.literal(0),
|
||||
type: z.string(),
|
||||
validUntil: z.string().datetime(),
|
||||
payload: z.string(),
|
||||
tag: z.string(),
|
||||
}).refine((v) => {
|
||||
const validUntil = new Date(v.validUntil);
|
||||
const now = new Date();
|
||||
return validUntil > now;
|
||||
}, "token expired"
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) satisfies z.ZodType<any, any, SecurePlaintextToken>;
|
||||
|
||||
if (type) {
|
||||
return obj.refine((v) => {
|
||||
return v.type === type;
|
||||
}, "token type mismatch")
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../util"
|
||||
},
|
||||
{
|
||||
"path": "../crypto"
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@sammo/util",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc --build"
|
||||
},
|
||||
"type": "module",
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"lodash-es": "^4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"lodash-es": "^4.17.21"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import { every, isString } from "lodash-es";
|
||||
import type { JsonifiableClassObj, PlainJson, PlainJsonObj } from "./jsonify.js";
|
||||
|
||||
type BsonifiableLite = string | number | boolean | bigint | null | undefined;
|
||||
|
||||
export type PlainBsonItem =
|
||||
string | number | boolean | null | undefined |
|
||||
Date | ArrayBuffer | SharedArrayBuffer | ArrayBufferView |
|
||||
PlainBsonItem[] |
|
||||
{ [key: string]: PlainBsonItem };
|
||||
|
||||
export type PlainBson =
|
||||
{ [key: string]: PlainBsonItem };
|
||||
|
||||
export type BsonifiableItem =
|
||||
BsonifiableLite |
|
||||
Date | ArrayBuffer | SharedArrayBuffer | ArrayBufferView |
|
||||
Array<BsonifiableItem> |
|
||||
Map<BsonifiableLite, BsonifiableItem> |
|
||||
Set<BsonifiableLite> |
|
||||
BsonifiableClassObj<PlainBson> |
|
||||
JsonifiableClassObj<PlainJson> |
|
||||
{ [key: string]: BsonifiableItem } |
|
||||
ReadonlyArray<BsonifiableItem>;
|
||||
|
||||
export type Bsonifiable =
|
||||
Map<string, BsonifiableItem> |
|
||||
BsonifiableClassObj<PlainBson> |
|
||||
JsonifiableClassObj<PlainJsonObj> |
|
||||
{ [key: string]: BsonifiableItem };
|
||||
|
||||
|
||||
export interface BsonifiableClassObj<T extends PlainBson> {
|
||||
bsonify(): T;
|
||||
}
|
||||
|
||||
type MaybeBsonifiedItem<T> = T extends BsonifiableItem ? BsonifiedItem<T> : never;
|
||||
type MaybeBsonified<T> = T extends Bsonifiable ? Bsonified<T> : never;
|
||||
|
||||
export type BsonifiedItem<T extends BsonifiableItem | unknown> =
|
||||
T extends string ? T :
|
||||
T extends number ? T :
|
||||
T extends boolean ? T :
|
||||
T extends bigint ? ReturnType<T['toString']> :
|
||||
T extends null ? T :
|
||||
T extends undefined ? T :
|
||||
T extends Date ? T :
|
||||
T extends ArrayBuffer ? T :
|
||||
T extends SharedArrayBuffer ? T :
|
||||
T extends ArrayBufferView ? T :
|
||||
T extends Map<infer K extends string, infer V> ? { [key in K]: MaybeBsonified<V> } :
|
||||
T extends Map<infer K, infer V> ? [MaybeBsonified<K>, MaybeBsonified<V>][] :
|
||||
T extends Set<infer V> ? MaybeBsonified<V>[] :
|
||||
T extends BsonifiableClassObj<PlainBson> ? ReturnType<T['bsonify']> :
|
||||
T extends JsonifiableClassObj<PlainJson> ? ReturnType<T['jsonify']> :
|
||||
T extends object ? { [key in keyof T]: MaybeBsonified<T[key]> } :
|
||||
T extends unknown ? unknown :
|
||||
never;
|
||||
|
||||
export type Bsonified<T extends Bsonifiable | unknown> =
|
||||
T extends Map<infer K extends string, infer V> ? { [key in K]: MaybeBsonifiedItem<V> } :
|
||||
T extends BsonifiableClassObj<PlainBson> ? ReturnType<T['bsonify']> :
|
||||
T extends JsonifiableClassObj<PlainJsonObj> ? ReturnType<T['jsonify']> :
|
||||
T extends object ? { [key in keyof T]: MaybeBsonifiedItem<T[key]> } :
|
||||
T extends unknown ? unknown :
|
||||
never;
|
||||
|
||||
/**
|
||||
* Convert any object to BSON-safe object
|
||||
*/
|
||||
export function bsonifyItem<T extends BsonifiableItem>(item: T): BsonifiedItem<T> {
|
||||
if (typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean') {
|
||||
return item as BsonifiedItem<T>;
|
||||
}
|
||||
|
||||
if (typeof item === 'bigint') {
|
||||
return item.toString() as BsonifiedItem<T>;
|
||||
}
|
||||
|
||||
if (typeof item === 'undefined') {
|
||||
return item as BsonifiedItem<typeof item>;
|
||||
}
|
||||
|
||||
if (item === null) {
|
||||
return item as BsonifiedItem<typeof item>;
|
||||
}
|
||||
|
||||
if (item instanceof Date) {
|
||||
return item as BsonifiedItem<typeof item>;
|
||||
}
|
||||
|
||||
if (item instanceof ArrayBuffer) {
|
||||
return item as BsonifiedItem<typeof item>;
|
||||
}
|
||||
|
||||
if ('SharedArrayBuffer' in globalThis && item instanceof SharedArrayBuffer) {
|
||||
return item as BsonifiedItem<typeof item>;
|
||||
}
|
||||
|
||||
if (ArrayBuffer.isView(item)) {
|
||||
return item as BsonifiedItem<typeof item>;
|
||||
}
|
||||
|
||||
//HACK: depth hack, escape ts(2589)
|
||||
if (item instanceof Map) {
|
||||
const onlyStringKey = every(item.keys, isString);
|
||||
if (onlyStringKey) {
|
||||
const result: { [key: string]: string } = {};
|
||||
for (const [k, v] of item.entries()) {
|
||||
result[k as string] = bsonifyItem(v as string);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return result as any;
|
||||
}
|
||||
|
||||
const result: [string, string][] = [];
|
||||
for (const [k, v] of item.entries()) {
|
||||
result.push([bsonifyItem(k as string), bsonifyItem(v as string)]);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return result as any;
|
||||
}
|
||||
|
||||
if (item instanceof Set) {
|
||||
const result: string[] = [];
|
||||
for (const v of item.values()) {
|
||||
result.push(bsonifyItem(v as string));
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return result as any;
|
||||
}
|
||||
|
||||
if (Array.isArray(item)) {
|
||||
const result: string[] = [];
|
||||
for (const v of item) {
|
||||
result.push(bsonifyItem(v as string));
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return result as any;
|
||||
}
|
||||
|
||||
if (typeof item !== 'object') {
|
||||
throw new Error(`jsonify: invalid type ${typeof item}`);
|
||||
}
|
||||
|
||||
if ('bsonify' in item && typeof item.bsonify === 'function') {
|
||||
return item.bsonify() as BsonifiedItem<typeof item>;
|
||||
}
|
||||
|
||||
if ('jsonify' in item && typeof item.jsonify === 'function') {
|
||||
return item.jsonify() as BsonifiedItem<typeof item>;
|
||||
}
|
||||
|
||||
const result: { [key: string]: unknown } = {};
|
||||
for (const [k, v] of Object.entries(item)) {
|
||||
if (typeof k !== 'string') {
|
||||
continue;
|
||||
}
|
||||
if (k === 'prototype') {
|
||||
continue;
|
||||
}
|
||||
if (k === '__proto__ ') {
|
||||
continue;
|
||||
}
|
||||
if (typeof v === 'function') {
|
||||
continue;
|
||||
}
|
||||
result[k] = bsonifyItem(v as BsonifiableItem);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return result as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert any object to BSON-document object
|
||||
*/
|
||||
export function bsonify<T extends Bsonifiable>(item: T): Bsonified<T> {
|
||||
if (typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean') {
|
||||
throw new Error(`bsonify: invalid type ${typeof item}`);
|
||||
}
|
||||
|
||||
if (typeof item === 'bigint') {
|
||||
throw new Error(`bsonify: invalid type ${typeof item}`);
|
||||
}
|
||||
|
||||
if (typeof item === 'undefined') {
|
||||
throw new Error(`bsonify: invalid type ${typeof item}`);
|
||||
}
|
||||
|
||||
if (item === null) {
|
||||
throw new Error(`bsonify: invalid type ${typeof item}`);
|
||||
}
|
||||
|
||||
if (item instanceof Date) {
|
||||
throw new Error(`bsonify: invalid type ${typeof item}`);
|
||||
}
|
||||
|
||||
if (item instanceof ArrayBuffer) {
|
||||
throw new Error(`bsonify: invalid type ${typeof item}`);
|
||||
}
|
||||
|
||||
if ('SharedArrayBuffer' in globalThis && item instanceof SharedArrayBuffer) {
|
||||
throw new Error(`bsonify: invalid type ${typeof item}`);
|
||||
}
|
||||
|
||||
if (ArrayBuffer.isView(item)) {
|
||||
throw new Error(`bsonify: invalid type ${typeof item}`);
|
||||
}
|
||||
|
||||
//HACK: depth hack, escape ts(2589)
|
||||
if (item instanceof Map) {
|
||||
const onlyStringKey = every(item.keys, isString);
|
||||
if (onlyStringKey) {
|
||||
const result: { [key: string]: unknown } = {};
|
||||
for (const [k, v] of item.entries()) {
|
||||
result[k as string] = bsonifyItem(v as string);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return result as any;
|
||||
}
|
||||
|
||||
throw new Error(`bsonify: invalid type ${typeof item}`);
|
||||
}
|
||||
|
||||
if (item instanceof Set) {
|
||||
throw new Error(`bsonify: invalid type ${typeof item}`);
|
||||
}
|
||||
|
||||
if (Array.isArray(item)) {
|
||||
throw new Error(`bsonify: invalid type ${typeof item}`);
|
||||
}
|
||||
|
||||
if (typeof item !== 'object') {
|
||||
throw new Error(`jsonify: invalid type ${typeof item}`);
|
||||
}
|
||||
|
||||
if ('bsonify' in item && typeof item.bsonify === 'function') {
|
||||
return item.bsonify() as Bsonified<typeof item>;
|
||||
}
|
||||
|
||||
if ('jsonify' in item && typeof item.jsonify === 'function') {
|
||||
return item.jsonify() as Bsonified<typeof item>;
|
||||
}
|
||||
|
||||
const result: { [key: string]: unknown } = {};
|
||||
for (const [k, v] of Object.entries(item)) {
|
||||
if (typeof k !== 'string') {
|
||||
continue;
|
||||
}
|
||||
if (k === 'prototype') {
|
||||
continue;
|
||||
}
|
||||
if (k === '__proto__ ') {
|
||||
continue;
|
||||
}
|
||||
if (typeof v === 'function') {
|
||||
continue;
|
||||
}
|
||||
result[k] = bsonifyItem(v);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return result as any;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export class NotNullExpected extends TypeError {
|
||||
public override name = 'NotNullExpected';
|
||||
}
|
||||
|
||||
export class RuntimeError extends Error {
|
||||
public override name = 'RuntimeError';
|
||||
constructor(public override message: string = '') {
|
||||
super(message);
|
||||
}
|
||||
override toString(): string {
|
||||
if (this.message) {
|
||||
return this.name + ': ' + this.message;
|
||||
}
|
||||
else {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
export * from "./bsonify.js"
|
||||
export * from "./jsonify.js"
|
||||
export * from "./error.js"
|
||||
export * from "./unwrap.js"
|
||||
export * from "./types.js"
|
||||
|
||||
export function isEmail(addr: string): boolean {
|
||||
return String(addr)
|
||||
.toLowerCase()
|
||||
.match(
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
) !== null;
|
||||
}
|
||||
|
||||
export function isIPAddr4(addr: string): boolean {
|
||||
if (/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(addr)) {
|
||||
return (true)
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function calcBase64Len(length: number) {
|
||||
return ((4 * length / 3) + 3) & ~3;
|
||||
}
|
||||
|
||||
/** Buffer이지만 ts에서 타입 구분 편의를 제공 */
|
||||
export interface WrappedBuffer extends Buffer {
|
||||
/** 타입구분자. 항상 undefined일 것이다 */
|
||||
_w_type?: string;
|
||||
}
|
||||
|
||||
/** ArrayBuffer,Uint8Array,Buffer를 WrappedBuffer로 변환
|
||||
*/
|
||||
export function wrapBuffer<T extends WrappedBuffer>(buffer: BufferSource): T {
|
||||
/**
|
||||
* Buffer라면 wrapBuffer없이 그대로 대입 가능
|
||||
* ArrayBuffer, Uint8Array 등이라면 Buffer.from을 사용 가능
|
||||
* DataView 등인 경우는 ArrayBufferView 활용
|
||||
* 그냥 써도 되지만, 위 방법보다 아무렇게나 쓰기 좋다는 장점은 있음
|
||||
*/
|
||||
if (buffer instanceof Buffer) {
|
||||
return buffer as T;
|
||||
}
|
||||
if (buffer instanceof ArrayBuffer) {
|
||||
return Buffer.from(buffer) as T;
|
||||
}
|
||||
if (buffer instanceof SharedArrayBuffer) {
|
||||
return Buffer.from(buffer) as T;
|
||||
}
|
||||
return Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength) as T;
|
||||
}
|
||||
|
||||
/** 호출할 필요 없이 바로 Buffer에 대입해도 되지만, 누군가 찾을 수 있어서. */
|
||||
export function unwrapBuffer<T extends WrappedBuffer>(buffer: T): Buffer {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
export function delay(time: number): Promise<void>;
|
||||
export function delay<T>(time: number, result: T): Promise<T>;
|
||||
export function delay<T = undefined>(time: number, result?: T): Promise<T | void> {
|
||||
return new Promise(resolve =>
|
||||
setTimeout(() => {
|
||||
resolve(result);
|
||||
}, time)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* base64 string에 내부 타입으로 WrappedBuffer를 보관한 형태
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export type Base64String<T extends Buffer> = string & {
|
||||
/** 타입구분자. 항상 undefined일 것이다 */
|
||||
_b_type?: T;
|
||||
}
|
||||
|
||||
export function base64FromWrappedBuffer<T extends WrappedBuffer>(buffer: T | ArrayBuffer | Uint8Array): Base64String<T> {
|
||||
return Buffer.from(buffer).toString('base64');
|
||||
}
|
||||
|
||||
export function wrappedBufferFromBase64<T extends WrappedBuffer>(base64: Base64String<T>): T {
|
||||
return Buffer.from(base64, 'base64') as T;
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
import { every, isString } from "lodash-es";
|
||||
import type { BsonifiableClassObj, PlainBson } from "./bsonify.js";
|
||||
|
||||
export type PlainJson =
|
||||
string | number | boolean | null | PlainJson[] | { [key: string]: PlainJson };
|
||||
|
||||
//엄밀히는 PlainJsonItem 스스로가 PlainJson이어야 하지만, Bson의 document와 호환을 맞추기 위해 object만 허용
|
||||
export type PlainJsonObj =
|
||||
{ [key: string]: PlainJson };
|
||||
|
||||
type JsonifiableLite =
|
||||
string | number | boolean | bigint | null | undefined;
|
||||
|
||||
export type Jsonifiable =
|
||||
JsonifiableLite |
|
||||
Date | ArrayBuffer | SharedArrayBuffer | ArrayBufferView |
|
||||
Array<Jsonifiable> |
|
||||
Map<JsonifiableLite, Jsonifiable> |
|
||||
Set<JsonifiableLite> |
|
||||
JsonifiableClassObj<PlainJson> |
|
||||
BsonifiableClassObj<PlainBson> |
|
||||
{ [key: string]: Jsonifiable } |
|
||||
ReadonlyArray<Jsonifiable>;
|
||||
|
||||
export type JsonifiableObj =
|
||||
Map<string, Jsonifiable> |
|
||||
JsonifiableClassObj<PlainJsonObj> |
|
||||
BsonifiableClassObj<PlainBson> |
|
||||
{ [key: string]: Jsonifiable };
|
||||
|
||||
export interface JsonifiableClassObj<T extends PlainJson> {
|
||||
jsonify(): T;
|
||||
}
|
||||
|
||||
type MaybeJsonified<T> = T extends Jsonifiable ? Jsonified<T> : never;
|
||||
type MaybeJsonifiedObj<T> = T extends JsonifiableObj ? JsonifiedObj<T> : never;
|
||||
|
||||
export type Jsonified<T extends Jsonifiable|unknown> =
|
||||
T extends string ? T :
|
||||
T extends number ? T :
|
||||
T extends boolean ? T :
|
||||
T extends bigint ? ReturnType<T['toString']> :
|
||||
T extends null ? T :
|
||||
T extends undefined ? T :
|
||||
T extends Date ? string :
|
||||
T extends ArrayBuffer ? string :
|
||||
T extends SharedArrayBuffer ? string :
|
||||
T extends ArrayBufferView ? string :
|
||||
T extends Map<infer K extends string, infer V> ? { [key in K]: MaybeJsonified<V> } :
|
||||
T extends Map<infer K, infer V> ? [MaybeJsonified<K>, MaybeJsonified<V>][] :
|
||||
T extends Set<infer V> ? MaybeJsonified<V>[] :
|
||||
T extends JsonifiableClassObj<PlainJson> ? ReturnType<T['jsonify']> :
|
||||
T extends BsonifiableClassObj<PlainBson> ? Jsonified<ReturnType<T['bsonify']>> :
|
||||
T extends object ? { [key in keyof T]: MaybeJsonified<T[key]> } :
|
||||
T extends unknown ? unknown :
|
||||
never;
|
||||
|
||||
export type JsonifiedObj<T extends JsonifiableObj|unknown> =
|
||||
T extends Map<infer K extends string, infer V> ? { [key in K]: MaybeJsonifiedObj<V> } :
|
||||
T extends JsonifiableClassObj<PlainJson> ? ReturnType<T['jsonify']> :
|
||||
T extends BsonifiableClassObj<PlainBson> ? Jsonified<ReturnType<T['bsonify']>> :
|
||||
T extends object ? { [key in keyof T]: MaybeJsonified<T[key]> } :
|
||||
T extends unknown ? unknown :
|
||||
never;
|
||||
|
||||
/**
|
||||
* Convert any object to JSON-safe object
|
||||
*/
|
||||
export function jsonify<T extends Jsonifiable>(item: T): Jsonified<T> {
|
||||
if (typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean') {
|
||||
return item as Jsonified<typeof item>;
|
||||
}
|
||||
|
||||
if (typeof item === 'bigint') {
|
||||
return item.toString() as Jsonified<typeof item>;
|
||||
}
|
||||
|
||||
if (typeof item === 'undefined') {
|
||||
return item as Jsonified<typeof item>;
|
||||
}
|
||||
|
||||
if (item === null) {
|
||||
return item as Jsonified<typeof item>;
|
||||
}
|
||||
|
||||
if (item instanceof Date) {
|
||||
return item.toISOString() as Jsonified<typeof item>;
|
||||
}
|
||||
|
||||
if (item instanceof ArrayBuffer) {
|
||||
return Buffer.from(item).toString('base64') as Jsonified<typeof item>;
|
||||
}
|
||||
|
||||
if ('SharedArrayBuffer' in globalThis && item instanceof SharedArrayBuffer) {
|
||||
return Buffer.from(item).toString('base64') as Jsonified<typeof item>;
|
||||
}
|
||||
|
||||
if (ArrayBuffer.isView(item)) {
|
||||
return Buffer.from(item.buffer, item.byteOffset, item.byteLength).toString('base64') as Jsonified<typeof item>;
|
||||
}
|
||||
|
||||
//HACK: depth hack, escape ts(2589)
|
||||
if (item instanceof Map) {
|
||||
const onlyStringKey = every(item.keys, isString);
|
||||
if (onlyStringKey) {
|
||||
const result: { [key: string]: string } = {};
|
||||
for (const [k, v] of item.entries()) {
|
||||
result[k as string] = jsonify(v as string);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return result as any;
|
||||
}
|
||||
|
||||
const result: [string, string][] = [];
|
||||
for (const [k, v] of item.entries()) {
|
||||
result.push([jsonify(k as string), jsonify(v as string)]);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return result as any;
|
||||
}
|
||||
|
||||
if (item instanceof Set) {
|
||||
const result: string[] = [];
|
||||
for (const v of item.values()) {
|
||||
result.push(jsonify(v as string));
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return result as any;
|
||||
}
|
||||
|
||||
if (Array.isArray(item)) {
|
||||
const result: string[] = [];
|
||||
for (const v of item) {
|
||||
result.push(jsonify(v as string));
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return result as any;
|
||||
}
|
||||
|
||||
if (typeof item !== 'object') {
|
||||
throw new Error(`jsonify: invalid type ${typeof item}`);
|
||||
}
|
||||
|
||||
if ('jsonify' in item && typeof item.jsonify === 'function') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return item.jsonify() as any;
|
||||
}
|
||||
|
||||
if ('bsonify' in item && typeof item.bsonify === 'function') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return jsonify(item.bsonify() as Record<string,string>) as any;
|
||||
}
|
||||
|
||||
const result: { [key: string]: string } = {};
|
||||
for (const [k, v] of Object.entries(item)) {
|
||||
if (typeof k !== 'string') {
|
||||
continue;
|
||||
}
|
||||
if (k === 'prototype') {
|
||||
continue;
|
||||
}
|
||||
if (k === '__proto__ ') {
|
||||
continue;
|
||||
}
|
||||
if (typeof v === 'function') {
|
||||
continue;
|
||||
}
|
||||
result[k] = jsonify(v as string);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return result as any;
|
||||
}
|
||||
|
||||
export function jsonifyObj<T extends JsonifiableObj>(item: T): JsonifiedObj<T> {
|
||||
if (item === null) {
|
||||
throw new Error(`jsonifyObj: invalid type ${typeof item}`);
|
||||
}
|
||||
|
||||
if (item === undefined) {
|
||||
throw new Error(`jsonifyObj: invalid type ${typeof item}`);
|
||||
}
|
||||
|
||||
//HACK: depth hack, escape ts(2589)
|
||||
if (item instanceof Map) {
|
||||
const onlyStringKey = every(item.keys, isString);
|
||||
if (onlyStringKey) {
|
||||
const result: { [key: string]: unknown } = {};
|
||||
for (const [k, v] of item.entries()) {
|
||||
result[k as string] = jsonify(v as Record<string,string>);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return result as any;
|
||||
}
|
||||
|
||||
throw new Error(`jsonifyObj: invalid type ${typeof item}`);
|
||||
}
|
||||
|
||||
if (Array.isArray(item)) {
|
||||
throw new Error(`jsonifyObj: invalid type ${typeof item}`);
|
||||
}
|
||||
|
||||
if (item instanceof Set) {
|
||||
throw new Error(`jsonifyObj: invalid type ${typeof item}`);
|
||||
}
|
||||
|
||||
if (typeof item !== 'object') {
|
||||
throw new Error(`jsonify: invalid type ${typeof item}`);
|
||||
}
|
||||
|
||||
if ('jsonify' in item && typeof item.jsonify === 'function') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return item.jsonify() as any;
|
||||
}
|
||||
|
||||
if ('bsonify' in item && typeof item.bsonify === 'function') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return jsonify(item.bsonify() as Record<string,string>) as any;
|
||||
}
|
||||
|
||||
const result: { [key: string]: unknown } = {};
|
||||
for (const [k, v] of Object.entries(item)) {
|
||||
if (typeof k !== 'string') {
|
||||
continue;
|
||||
}
|
||||
if (k === 'prototype') {
|
||||
continue;
|
||||
}
|
||||
if (k === '__proto__ ') {
|
||||
continue;
|
||||
}
|
||||
if (typeof v === 'function') {
|
||||
continue;
|
||||
}
|
||||
result[k] = jsonify(v as Record<string,string>);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return result as any;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type Nullable<T> = T | null | undefined;
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Nullable } from './types.js';
|
||||
import { NotNullExpected } from "./error.js";
|
||||
|
||||
export function unwrap<T>(result: Nullable<T>): T {
|
||||
if (result === null || result === undefined) {
|
||||
throw new NotNullExpected();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function unwrap_any<T>(result: Nullable<unknown>): T {
|
||||
if (result === null || result === undefined) {
|
||||
throw new NotNullExpected();
|
||||
}
|
||||
return result as T;
|
||||
}
|
||||
|
||||
type ErrType<T> = { new(msg?: string): T }
|
||||
|
||||
export function unwrap_err<T, ErrT extends Error>(result: Nullable<T>, errType: ErrType<ErrT>, errMsg?: string): T {
|
||||
if (result === null || result === undefined) {
|
||||
throw new errType(errMsg);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "node16",
|
||||
"moduleResolution": "node16",
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"skipLibCheck": true,
|
||||
"incremental": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"composite": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user