'use strict' /** * Copyright (c) 2017~2024, OBCon Inc. * All rights reserved. */ /** * @file * @copyright 2017~2024, OBCon Inc. * @author gye hyun james kim [pnuskgh@gmail.com] */ const crypto = require('crypto'); class Security { constructor() { this._initialize(); } _initialize() { //--- Hash this._hashTypes = [ 'sha512', 'sha256', 'md5', 'sha1' ]; this._hashType = this._hashTypes[0]; this._digestTypes = [ 'base64', 'hex', 'latin1' ]; this._digestType = this._digestTypes[0]; this._hash = crypto.createHash(this._hashType); //--- RSA 암복호화의 Public key와 Private key const publicKey = '-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAxDjsWgMik7ZbfSe6frDTRmYT001/m3Yi+6UU4Vljev3/uNcf5YJO\neXFzL2bqzAqtA7mw7aJzvlyMVUXB/N2i3vz0bHc0SRJMfx1PI3C/TF3yVMRwyPrA\nTchP8g/SoXwymU4modVpe5hUVYUuJuS3zrT+YrFNoDiGRdzeZW+eAlPHyD/W9Rix\nSiJ8uEPdkkcGtw7PgmuCcNt3x5LAoIcjsBfdLBi0/ee7sgo3SZDWfyk7CrC6tCRA\nykMVeg9x9SmmBbRKeb5+38J3B0djkVudsOz5xWR+ETqhfmH9VDLQjNmcA6LdB1vA\nioW7AL/a4flW9gPd653JiFgaEhbP/sddzwIDAQAB\n-----END RSA PUBLIC KEY-----\n'; const privateKey = '-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,6EA33C8A9B1B435FB4D70734DF67774C\n\nzL40sX2LURVT8uszk5CFgIVhrIBXxHoWlflmQvGD0RK4A2w+c4pj5gCnCmGEQHed\nLC6k84OMw0STRs+T74frLby1KDaWh538yGs/aU7ZmD8zpa5X/gguORDJ+G8c+UcM\nlQ5dy2oUPG+QVcFNn428Tfr3V0EvhCdjtd+LQzNWlboQdXv+nSSUoAUKLMO9eJ6L\nokAoFeZlOVb777ELHaOVxJ2FuGcmGQwlJCJqbrTmWM2NIw4WzNtQ3FZ+hAd7mEgN\nzorYHtat2LdYCyvzjzdyMR6KGudcrn/DwiQI1ScfdMSqgpjAJWtsGYnIWX4mRpmR\nucjlo2YhWAocoa2mq7M1YDtALEAgAOeL0pYRY/Vr/qy0QF5pPcA16dSyeoFEppAh\nd41iHFofw+a+/NAApZPIGszuUAbZVziWnD+bkShsC10Vtnot42ctnDILOy5mgMZ5\nIXQMd1gN75nJrfvmffZGkiXsae4hjHj9KtM1ftTQE7mr5NMBdpQH+J1g+eBPGL6P\n0o0zVEHULSEIhAlkIT9Pcz6xG+5PX+wpKgWUjwmtMBIo3bgXbu5r9qQmei+w/Px3\nOC2TsJtjDpRrbVDmRGadmlKjWY+14VWsEGCesF5ouKCvMkjMaszSLVHfnayv8buZ\n5r3+usK4tA5a11USyhV1jQ2gap5D2hDyCmld3l0RhHfPLhIz3nO9i1wtw39lxQTe\nwWnYJrHgsWLglzrjp2rebqFcRcZxoI+4fuG2G1JjgQJaYZ5rLLe9iN2WIslKkscH\nMhqEzxuIX3cMcQ0hTK+hENDyvU9+DmPzY2E9f8ALWKScmcT5EAwkZK0jetZusLe+\nrbUtYnNuaoZ6e0blW+iwnWPqoCDHo3lGZnerZWD37l0cbTl7u+rjdLj9hdPqUgmc\nATPBvI9jxlM6neWRKUQR1GIBfafI+cK3SbNp6m0bigaz+F6snNez6Qk/9Ft7a+8h\ndNRiDaeSAU+NJd/X9JXtek3SfG/N2vYnqyY2KmJKjVI6wexu5DlY6kFOAjDvvccJ\nRR8DO6HjVKkXBtt3pW29DoFt8QjsM0fxGeSnUKXP5AJIYuran7jnmqIZYBgG0aOV\nCVjFWI8BKtkzdQcsRMC6fE/1zdZNJQjosWKYtlWXwbdx+H3lKtcZUOJre8YZs+4l\nZZ06IgNphTgSiJYuV20x7SGo4h/cgHFmPqd7vwezMj+I13SKujQW0yzwfs2TgdW4\nK3nNaWtqlfaMRAeK41k7qintmDycj/Vdilji+xgZXKGS99xEnAp5iiomSUkwlJ8h\nFO2YsHSGh6BdmnI4xeHnAZ9WW2JLsnQINhBofgOdhUxaTxWpB7YoJs/wqKX/uS8H\nrCJC/s8lu2Qe9EZmXpJqQsI6SlcZFxEK5p5knpdSR1KCSOBSqpdHViaHwb+q8TxB\nZB5c5FhSBz19MMqwLHiR94C56nnQd08VKftL3tBJszO9gWfzllz8CZQZm4PFA7E/\nuMC+fbCJsUD7nV09JgdEsaf/uddRi9obWMfZ5MH+94KKIpJNEQEM0IQwv50CIEwf\nuO39SowXxCYqvyKqjIWCt+bL84kKurX4aX8I7fo83FRNuASEFNNzopAxzmjERK5s\n-----END RSA PRIVATE KEY-----\n'; this._passphrase = 'pnuskgh@gmail.com'; this._publicKey = config.security.publicKey || publicKey; this._privateKey = config.security.privateKey || privateKey; //--- 블록 암호화/복호화 키 //--- AES (Advanced Encryption Standard) this._cipherTypes = [ 'aes-256-cbc', 'aes-192-cbc', 'aes-128-cbc', // 'aes-256-ccm', 'aes-192-ccm', 'aes-128-ccm', //--- 검증 필요 // 'aes-128-gcm', 'aes-192-gcm', 'aes-256-gcm','chacha20-poly1305' //--- 검증 필요 ]; this._cipherType = this._cipherTypes[0]; this._IV_LENGTH = 16; //--- IV (Initialize Vector) this._encryptKey = this.decrypt(config.security.encryptKey || this._initEncryptKey()); } get encryptKey() { return this._encryptKey; } set encryptKey(theValue) { this._encryptKey = theValue; } get publicKey() { return this._publicKey; } set publicKey(theValue) { this._publicKey = theValue; } _initEncryptKey(name = null) { const hash = crypto.createHash('sha512'); const hostname = config.http.baseUrl.replace(/\//g, '').replace(/http:/, '').replace(/https:/, '').split(':')[0]; hash.update(name || `${config.service.name}:${hostname}`); return hash.digest('hex').toLowerCase().substring(0, 32); } hash(plainText) { this._hash.update(plainText); return this._hash.digest(this._digestType).toLowerCase(); } hashWithSalt(plainText, hash=null, callback) { let salt = null; if (hash == null) { salt = crypto.randomBytes(this._IV_LENGTH).toString(this._digestType); } else { salt = hash.split(':')[0]; } crypto.pbkdf2(plainText, salt, 1000, 64, this._hashType, function(err, key) { if (err) { callback(null); } else { callback(`${salt}:${key.toString(this._digestType)}`); } }.bind(this)); } // this._encryptKey = 'f36ff20a60'; //--- 10자, 40 bits 키 // this._encryptKey = 'af0af00a9ea2c23c403485994ce78b4e'; //--- 32자, 128 bits 키 // this._encryptKey = '14889b604f7d72880e71734373ef11639f0ac1b3cb40955fade3a8848aae8407'; //--- 64자, 256 bits 키 _getEncryptKey() { let key = null; try { switch (this._cipherType) { case 'aes-128-cbc': case 'aes-128-ccm': case 'aes-128-gcm': key = crypto.scryptSync(this._encryptKey, 'GfG', 16); break; case 'aes-192-cbc': case 'aes-192-ccm': case 'aes-192-gcm': key = crypto.scryptSync(this._encryptKey, 'GfG', 24); break; case 'aes-256-cbc': case 'aes-256-ccm': case 'aes-256-gcm': default: if (this._encryptKey.length == 32) { key = Buffer.from(this._encryptKey); } else { //--- this._encryptKey가 32 bytes가 아니면 오류 발생 key = crypto.scryptSync(this._encryptKey, 'GfG', 32); } break; } } catch (e) { utils.obj.loggerError(e); } return key; } //--- encrypt:{IV(Initialize Vector)}:{encryptText} //--- rsa:{encryptText} encrypt(plainText, key=null, isPublic=true) { return (key == null) ? this._encrypt(plainText):this.encryptRSA(plainText, key, isPublic); } decrypt(encryptedText, key=this._privateKey, isPublic=false) { let planText = encryptedText; if (typeof(encryptedText) == 'string') { //--- string, number if (encryptedText.startsWith('encrypt:')) { planText = this._decrypt(encryptedText); } if (encryptedText.startsWith('rsa:')) { planText = this.decryptRSA(encryptedText, key, isPublic); } if ((planText.startsWith('encrypt:')) || (planText.startsWith('rsa:'))) { planText = this.decrypt(planText, key, isPublic); } } return planText; } _encrypt(plainText) { try { const key = this._getEncryptKey(); let iv = null; let cipher = null; if (this._cipherType.endsWith('-cbc')) { iv = crypto.randomBytes(this._IV_LENGTH); cipher = crypto.createCipheriv(this._cipherType, key, iv); } else if (this._cipherType.endsWith('-ccm')) { iv = crypto.randomBytes(12); cipher = crypto.createCipheriv(this._cipherType, key, iv, { authTagLength: 16 }); cipher.setAAD(Buffer.from('0123456789', 'hex'), { plaintextLength: Buffer.byteLength(plainText) }); } else { iv = crypto.randomBytes(this._IV_LENGTH); cipher = crypto.createCipheriv(this._cipherType, key, iv); } const encrypted = cipher.update(plainText); //--- 32 bytes : 32 bytes * n return `encrypt:${iv.toString(this._digestType)}:${Buffer.concat([encrypted, cipher.final()]).toString(this._digestType)}`; } catch (e) { utils.obj.loggerError(e); return plainText; } } _decrypt(encryptedText) { try { if (encryptedText.startsWith('encrypt:') == false) { return encryptedText; } encryptedText = encryptedText.substring(8); const key = this._getEncryptKey(); const textParts = encryptedText.split(':'); const iv = Buffer.from(textParts.shift(), this._digestType); const encrypted = Buffer.from(textParts.join(':'), this._digestType); let decipher = null; if (this._cipherType.endsWith('-cbc')) { decipher = crypto.createDecipheriv(this._cipherType, key, iv); } else if (this._cipherType.endsWith('-ccm')) { decipher = crypto.createDecipheriv(this._cipherType, key, iv, { authTagLength: 16 }); decipher.setAAD(Buffer.from('0123456789', 'hex'), { plaintextLength: encrypted.length }); } else { decipher = crypto.createDecipheriv(this._cipherType, key, iv); } const decrypted = decipher.update(encrypted); return Buffer.concat([decrypted, decipher.final()]).toString(); } catch (e) { utils.obj.loggerError(e); return encryptedText; } } //--- publicKey, privateKey 반환 getKeyPair() { // return crypto.generateKeyPairSync('rsa', { modulusLength: 2048 }); return crypto.generateKeyPairSync( 'rsa', //--- rsa, dsa, ec, ed25519, ed448, x25519, x448, dh { modulusLength: 2048, publicKeyEncoding: { type: 'pkcs1', //--- pkcs1, spki format: 'pem' //--- pem, der }, privateKeyEncoding: { type: 'pkcs1', //--- sec1, pkcs8 format: 'pem', cipher: 'aes-256-cbc', passphrase: this._passphrase } }, // (err, publicKey, privateKey) => { } ); } encryptRSA(plainText, key=this._publicKey, isPublic=true) { try { let cipher = null; if (isPublic) { cipher = crypto.publicEncrypt( { key: key, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: this._hashType //--- sha256, sha512 }, Buffer.from(plainText) ); } else { cipher = crypto.privateEncrypt( { key: key, cipher: 'aes-256-cbc', passphrase: this._passphrase }, Buffer.from(plainText) ); } return `rsa:${cipher.toString(this._digestType)}`; } catch (e) { utils.obj.loggerError(e); return plainText; } } decryptRSA(encryptedText, key=this._privateKey, isPublic=false) { try { if (encryptedText.startsWith('rsa:') == false) { return encryptedText; } encryptedText = encryptedText.substring(4); let decipher = null; if (isPublic) { decipher = crypto.publicDecrypt( { key: key, }, Buffer.from(encryptedText, this._digestType) ); } else { decipher = crypto.privateDecrypt( { key: key, cipher: 'aes-256-cbc', passphrase: this._passphrase, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: this._hashType //--- sha256, sha512 }, Buffer.from(encryptedText, this._digestType) ); } return decipher.toString(); } catch (e) { utils.obj.loggerError(e); return encryptedText; } } //--- 서명 반환 signature(text, privateKey=this._privateKey) { try { const signer = crypto.sign( this._hashType, Buffer.from(text), { key: privateKey, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, cipher: 'aes-256-cbc', passphrase: this._passphrase } ); return signer.toString(this._digestType); } catch (e) { utils.obj.loggerError(e); return text; } } //--- 서명 검증 verify(text, sign, publicKey=this._publicKey) { try { return crypto.verify( this._hashType, Buffer.from(text), { key: publicKey, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, }, Buffer.from(sign, this._digestType) ); } catch (e) { utils.obj.loggerError(e); return false; } } } module.exports = Security;