코드 이식

This commit is contained in:
2023-09-22 20:46:23 +09:00
parent 0f9d5c2dc9
commit b8d57a014a
41 changed files with 4202 additions and 7 deletions
+28
View File
@@ -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"
}
}
+1
View File
@@ -0,0 +1 @@
export const hello = "hello";
+37
View File
@@ -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;
+20
View File
@@ -0,0 +1,20 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
},
"references": [
{
"path": "../util"
},
{
"path": "../secure_token"
},
{
"path": "../../@strpc/def"
}
]
}
+12
View File
@@ -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'
},
}
+38
View File
@@ -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
+48
View File
@@ -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"
}
}
+92
View File
@@ -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);
}
})
+61
View File
@@ -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;
}
+63
View File
@@ -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);
}
+48
View File
@@ -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);
}
+32
View File
@@ -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);
})
+81
View File
@@ -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;
}
}
+35
View File
@@ -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);
})
+62
View File
@@ -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);
}
+48
View File
@@ -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"]
);
}
+50
View File
@@ -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";
+10
View File
@@ -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);
});
+19
View File
@@ -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);
}
+6
View File
@@ -0,0 +1,6 @@
export enum TypeLength {
//ECDSA P384 서명
ECDSA_PKCS8_P384_SignKey = 185,
ECDSA_SPKI_P384_VerifyKey = 120,
ECDSASignature = 96,
}
+130
View File
@@ -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';
+14
View File
@@ -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;
}
+73
View File
@@ -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];
}
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"verbatimModuleSyntax": false,
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
},
"references": [
{
"path": "../util"
}
]
}
+26
View File
@@ -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;
}
+225
View File
@@ -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);
}
}
+4
View File
@@ -0,0 +1,4 @@
export * from "./SecureToken.js"
export * from "./zodSecureToken.js"
export * from "./EncryptedSimple.js"
+52
View File
@@ -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;
}
+15
View File
@@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
},
"references": [
{
"path": "../util"
},
{
"path": "../crypto"
},
]
}
+19
View File
@@ -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"
}
}
+263
View File
@@ -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;
}
+18
View File
@@ -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;
}
}
}
+83
View File
@@ -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;
}
+238
View File
@@ -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;
}
+1
View File
@@ -0,0 +1 @@
export type Nullable<T> = T | null | undefined;
+25
View File
@@ -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;
}
+25
View File
@@ -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
}
}