Spaces:
Running
Running
| /** | |
| * 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; | |