From f8b8ea6cbe30c0fbe0c3839739e087f5029a688d Mon Sep 17 00:00:00 2001 From: Hide_D Date: Sat, 19 Aug 2023 06:50:32 +0000 Subject: [PATCH] =?UTF-8?q?SecureToken=20=EA=B5=AC=ED=98=84=20-=20gateway?= =?UTF-8?q?=20-=20server=20=ED=86=B5=EC=8B=A0=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/SecureToken.ts | 171 ++++++++++++++++++++++++++++++++++++++++++ server/dotenv.d.ts | 2 + 2 files changed, 173 insertions(+) create mode 100644 server/SecureToken.ts diff --git a/server/SecureToken.ts b/server/SecureToken.ts new file mode 100644 index 0000000..87dd512 --- /dev/null +++ b/server/SecureToken.ts @@ -0,0 +1,171 @@ + +import 'dotenv/config'; +import { AES_GCM_Decrypt, AES_GCM_Encrypt } from "./util/aes.js"; +import { webcrypto } from "crypto"; +import { Buffer } from "buffer"; +import { sha512 } from "./util/sha2.js"; +import { BSON } from "bson"; + +/** + * Preshared Token Secret을 이용한 AES256-GCM 토큰 + * key, iv = SHA512(presharedSecret + nonce) 으로 생성 + */ + +export type SecureEncryptedToken = { + nonce: string; // [key, iv] = SHA512(presharedSecret + nonce) + 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) + type: string; // aad[0] + validUntil: string; // aad[1] + payload: T; // aad[2] + tag: string; //BASE64(AES(BSON(aad),null))) +} + +const staticPresharedTokenSecret: string | undefined = process.env.PRESHARED_TOKEN_SECRET; + +export async function generateSecureEncryptedToken(validUntil: Date, type: string, payload: T, presharedTokenSecret?: string): Promise { + if (!presharedTokenSecret) { + if (!staticPresharedTokenSecret) { + throw new Error("PRESHARED_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(webcrypto.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 = [type, validUntilText]; + + const aadRaw = BSON.serialize(aad); + const payloadRaw = BSON.serialize(payload); + + const ciphertext = Buffer.from(await AES_GCM_Encrypt(key, iv, payloadRaw, aadRaw)); + + return { + nonce: nonce.toString('base64'), + type, + validUntil: validUntilText, + payload: ciphertext.toString('base64'), + } +} + +export async function parseSecureEncryptedToken(secureToken: SecureEncryptedToken, presharedTokenSecret?: string): Promise { + 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_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 = [secureToken.type, 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(validUntil: Date, type: string, payload: T, presharedTokenSecret?: string): Promise> { + if (!presharedTokenSecret) { + if (!staticPresharedTokenSecret) { + throw new Error("PRESHARED_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(webcrypto.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 = [type, validUntilText, payload]; + 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'), + type, + validUntil: validUntilText, + payload, + tag: tag.toString('base64'), + } +} + +export async function parseSecurePlaintextToken(secureToken: SecurePlaintextToken, presharedTokenSecret?: string): Promise { + if (!presharedTokenSecret) { + if (!staticPresharedTokenSecret) { + throw new Error("PRESHARED_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 = [secureToken.type, secureToken.validUntil, secureToken.payload]; + const aadRaw = BSON.serialize(aad); + + const tag = Buffer.from(secureToken.tag, 'base64'); + + await AES_GCM_Decrypt(key, iv, tag, aadRaw); + + return secureToken.payload; +} \ No newline at end of file diff --git a/server/dotenv.d.ts b/server/dotenv.d.ts index 99f4201..1ccbc5a 100644 --- a/server/dotenv.d.ts +++ b/server/dotenv.d.ts @@ -26,5 +26,7 @@ declare namespace NodeJS { API_ROOT_PATH?: string; VITE_API_ROOT_PATH?: string; + + PRESHARED_SECURE_TOKEN_SECRET?: string; //길게 } } \ No newline at end of file