
웹 브라우저에서 개발자모드에 들어가면 해당 화면이 어떤 방식으로 구성되어 있는지 어떤 함수를 호출하는지 다 확인할 수 있습니다.
이번에 확장앱을 개발하면서 확장앱도 브라우저 기반으로 동작하기 때문에 background.js 즉 비즈니스 로직이 모두 노출된다는 걸 알았는데요. 따라서 비즈니스 로직을 감추기 위한 방법을 찾아보았습니다.
사실 .js 에 무슨 짓을 해도 사용자가 까보려고 하면 까볼 수 있다지만, 최소한의 노력은 하기 위해 로직을 파악하기 어렵게 만드는 법을 찾다 난독화를 진행하게 되었어요.
다행히도 관련 라이브러리가 있었고 (요즘 시대 당연한가?🤔), 해당 라이브러리를 활용하는 방법을 정리해두려고 합니다.
제가 난독화를 진행하면서 원했던 건 딱 한가지 "사용자가 비즈니스로직을 파악하기 어려울 것"이었습니다.
사실 코드 최소화만 진행해도 인간이라면, 개발자일지라도 코드를 파악하기가 굉장히 어렵다는걸 알지만, 요즘은 대 인공지능의 시대인만큼... 왠만한 코드는 ChatGpt에 넣고 돌리면 ChatGpt가 파악하기 쉽게 변환해주더라구요.
따라서 ChatGpt가 파악할 수 없게 하기 위해 몇가지 조건을 생각해야했습니다.
1. 모든 변수 및 함수명을 의도를 파악하기 어려운 값으로 변환할 것
2. 코드 순서를 섞을 것
3. 의미 없는 코드를 삽입할 것
결론적으로 이 3가지를 만족했을 때 chatGpt도 해당 코드가 어떤 동작을 원하는지 알기 어려워했고 정리도 못해줬습니다만,
그 와중에 열심히 알려주더라구요.. 몇 년뒤면 정리해줄지도 모르겠습니다..🥹
난독화를 위한 라이브러리로 저는 javascript-obfuscator를 선택했습니다.
처음에는 난독화에 대한 지식이 부족하여 ChatGPT가 추천한 terser를 사용해 진행했지만, 요구 사항 중 2번과 3번을 terser가 지원하지 않는다는 것을 알게 되었습니다.
그래서 다른 대안을 찾던 중, javascript-obfuscator가 모든 요구 조건을 충족한다는 것을 확인하고 해당 라이브러리를 사용하기로 결정했습니다.
해당 진행상황을 따라하기 전에
node및npm이 설치되어 있어야 합니다.
0. 작업을 진행할 Directory 생성하기
난독화 작업을 진행할 Directory를 생성합니다. 저는 obfuscator라는 이름으로 디렉토리를 만들었습니다. 이 디렉토리 안에 난독화와 관련된 파일들을 저장할 예정입니다.
cd ..
mkdir obfuscator
최상위 디렉토리로 이동한 후, obfuscator 디렉토리를 생성합니다.
1. javascript-obfuscator 설치하기
먼저 javascript-obfuscator를 프로젝트에 설치해야 합니다. npm을 통해 간단히 설치할 수 있습니다:
npm install --save-dev javascript-obfuscator
2. 난독화 코드(minify.js) 작성하기
난독화를 위해 파일을 읽고, 난독화한 후 결과를 파일로 저장하는 코드를 작성합니다. fs와 path 모듈을 활용하여 파일 시스템을 처리하고, javascript-obfuscator를 이용해 코드를 난독화합니다.
저는 minify.js라고 이름지었습니다.
const fs = require('fs');
const path = require('path');
const obfuscator = require('javascript-obfuscator');
// 난독화할 파일 경로 설정
const inputFilePath = path.join(__dirname, 'input.js');
const outputFilePath = path.join(__dirname, 'output.js');
// 코드 난독화 함수
function obfuscateCode(filePath) {
const code = fs.readFileSync(filePath, 'utf8'); // 파일 읽기
const obfuscatedCode = obfuscator.obfuscate(code, {
compact: true, // 코드 압축
controlFlowFlattening: true, // 제어 흐름 플래튼화
controlFlowFlatteningThreshold: 1, // 제어 흐름 플래튼화 적용 확률
deadCodeInjection: true, // 죽은 코드 삽입
deadCodeInjectionThreshold: 1, // 죽은 코드 삽입 확률
stringArray: true, // 문자열을 배열로 변환
stringArrayEncoding: ['base64'], // Base64로 문자열 인코딩
stringArrayThreshold: 1, // 모든 문자열을 배열로 변환
disableConsoleOutput: true, // console.log 등 출력 제거
renameGlobals: true, // 전역 변수 이름 변경
identifierNamesGenerator: 'mangled' // 변수 및 함수명을 짧고 의미 없는 이름으로 변경
}).getObfuscatedCode();
fs.writeFileSync(outputFilePath, obfuscatedCode); // 난독화된 코드 파일로 저장
console.log(`Obfuscated code saved to: ${outputFilePath}`);
}
obfuscateCode(inputFilePath);
inputFilePath와 outputFilePath는 난독화할 파일과 결과를 저장할 파일의 경로를 설정하는 부분입니다. 각자의 프로젝트 구조에 맞게 이 경로를 수정해야 합니다.
위 예시에서는 파일과 디렉토리 위치를 임의로 설정했습니다.
3. javascript-obfuscator 옵션 설명
난독화에 사용할 수 있는 다양한 옵션들이 있습니다. 각 옵션은 난독화의 강도를 조절할 수 있습니다.
compact: true: 코드 내 공백과 주석을 제거하여 파일 크기를 줄입니다.
controlFlowFlattening: true: 제어 흐름 플래튼화를 활성화합니다. 이는 코드의 실행 흐름을 더 복잡하게 만들어 코드 분석을 어렵게 합니다.
controlFlowFlatteningThreshold: 1: 제어 흐름 플래튼화를 모든 코드에 적용합니다. 이 값을 0.5로 설정하면 코드의 절반만 제어 흐름 플래튼화가 적용됩니다.
deadCodeInjection: true: 의미 없는 "죽은 코드"를 삽입하여 코드 분석을 더 복잡하게 만듭니다.
deadCodeInjectionThreshold: 1: 죽은 코드를 100% 삽입합니다. 이 값은 0과 1 사이의 값을 설정하여 적용할 비율을 조절할 수 있습니다.
stringArray: true: 코드에 있는 모든 문자열을 배열로 변환하여 인덱스로 접근하게 합니다.
stringArrayEncoding: ['base64']: 문자열을 Base64로 인코딩하여 가독성을 더 떨어뜨립니다.
disableConsoleOutput: true: console.log()와 같은 디버깅 출력을 제거합니다.
renameGlobals: true: 전역 변수의 이름을 바꿔서 코드가 어떤 역할을 하는지 분석하기 어렵게 만듭니다.
identifierNamesGenerator: 'mangled': 변수와 함수명을 짧고 의미 없는 이름으로 변경합니다. 이는 코드를 해독하는 과정을 더 복잡하게 만듭니다.
4. 코드 난독화 실행
위 코드를 작성한 후, input.js 파일에 있는 JavaScript 코드를 난독화하려면 다음 명령을 실행합니다:
node minify.js
이 명령을 실행하면 output.js 파일이 생성되고, 난독화된 코드가 저장됩니다.
5. 난독화된 코드 확인(예시 코드)
var _0x23b8 = ['log', 'hello world'];
(function(_0x5689ca, _0x23b8bc) {
var _0x437c93 = function(_0x1c741a) {
while (--_0x1c741a) {
_0x5689ca['push'](_0x5689ca['shift']());
}
};
_0x437c93(++_0x23b8bc);
}(_0x23b8, 0x1f4));
var _0x437c = function(_0x5689ca, _0x23b8bc) {
_0x5689ca = _0x5689ca - 0x0;
var _0x437c93 = _0x23b8[_0x5689ca];
return _0x437c93;
};
console[_0x437c('0x0')](_0x437c('0x1'));
이번 프로젝트에서 javascript-obfuscator를 사용해 난독화를 적용한 방법과 그 과정에서 발생한 이슈를 공유합니다.
프로젝트의 주요 디렉토리 구조는 다음과 같습니다:
📂 root
├── 📂 build
│ ├── 📂 module
│ │ └── ... (module 내 여러 파일들)
│ ├── 📂 page
│ │ └── ... (page 내 여러 파일들)
│ ├── background.js
│ └── manifest.json
│
│
├── 📂 obfuscator
│ └── minify.js
├── 📂 deploy
│ └── ... (배포를 위한 파일들)
build 디렉토리: 개발을 위한 디렉토리로, 비즈니스 로직을 도메인 별로 모듈화하여 관리합니다. background.js는 이 모듈들을 불러와서 동작합니다.obfuscator 디렉토리: 난독화 작업을 위한 파일들이 들어있는 디렉토리입니다.deploy 디렉토리: 난독화된 파일들을 배포하기 위한 디렉토리입니다.다음과 같은 간단한 코드를 예시로 들어보겠습니다:
import { getTest } from './module/test.js';;
chrome.alarms.create('updateTest', {delayInMinutes: 360, periodInMinutes: 360});
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === 'updateTest') {
console.log("test 동작");
}
}
});
해당 코드의 난독화를 진행했을 때, import 문이 난독화될 경우 파일을 찾지 못하는 문제가 발생했습니다. 하지만 이를 난독화하지 않으면 어떤 파일이 어떤 역할을 하는지 쉽게 파악할 수 있어, 비즈니스 로직이 노출될 가능성이 있었습니다.
이를 해결하기 위해 import 할 파일명을 난수화 한 이후 import 문을 해당 파일명으로 변경하는 방법을 생각해 보았습니다. 하지만 여러 가지 문제로 인해 이 방법은 실패했습니다.
여러 시행착오 끝에, 최종적으로 배포 시 모든 코드를 background.js에 통합하여 난독화하는 방법으로 전환했습니다. 모든 코드를 background.js에 통합하면, import 문과 관련된 이슈를 더 이상 신경 쓸 필요가 없었기 때문입니다.
이를 구현하기 위해 minify.js 파일에 다음과 같은 코드를 추가했습니다:
const inputDirPath = path.join(__dirname, '..', 'build');
// import 및 export 문, use strict 제거 함수
function removeImportsAndExports(code) {
return code
.replace(/^(import\s.*?;|export\s)/gm, '') // import/export 문 제거
.replace(/^\s*['"]use strict['"];\s*/gm, '') // 'use strict'; 제거
.trim();
}
// 코드 합치기 및 난독화 함수
async function processFiles(inputDirPath) {
try {
let combinedCode = '';
const moduleDirPath = path.join(inputDirPath, 'module');
const moduleFiles = fs.readdirSync(moduleDirPath).filter(file => file.endsWith('.js'));
// module 디렉토리 내 파일 합치기
for (const file of moduleFiles) {
const filePath = path.join(moduleDirPath, file);
const code = fs.readFileSync(filePath, 'utf8');
combinedCode += removeImportsAndExports(code) + '\n';
}
// background.js 파일 내용 추가
const backgroundFilePath = path.join(inputDirPath, 'background.js');
const backgroundCode = fs.readFileSync(backgroundFilePath, 'utf8');
combinedCode += removeImportsAndExports(backgroundCode) + '\n';
// 난독화된 코드 생성 및 저장
const obfuscatedCode = obfuscateCode(combinedCode);
fs.writeFileSync(outputFilePath, obfuscatedCode, 'utf8');
console.log(`Obfuscated code saved to: ${outputFilePath}`);
} catch (error) {
console.error('Error processing files:', error);
}
이 코드는 module 디렉토리의 모든 JS 파일을 가져와 import 문을 제거하고, background.js와 결합한 뒤 난독화합니다.
결과적으로, 개발 단계에서는 모듈화를 유지하고, 배포 단계에서는 모든 코드를 하나로 합쳐 난독화함으로써 클라이언트가 코드를 파악하기 어렵게 만들 수 있었습니다.
난독화를 완료한 후, deploy 디렉토리에 build 파일 전체를 복사하고, 마지막으로 난독화 된 background.js만 업데이트한 후 배포를 진행했습니다.
저는 이 과정을 이해하고 있었기 때문에 문제되지 않았지만, 다른 사람에게 설명하기에는 복잡하고 개발자답지 않다고 느껴져 이 과정을 자동화하기로 했습니다.
이를 위해 minify.js에 자동화 코드를 추가했습니다:
// 디렉토리 및 파일 복사 함수
function copyDirectory(src, dest) {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true });
}
const entries = fs.readdirSync(src);
for (const entry of entries) {
const srcPath = path.join(src, entry);
const destPath = path.join(dest, entry);
if (fs.statSync(srcPath).isDirectory()) {
copyDirectory(srcPath, destPath); // 디렉토리일 경우 재귀적으로 복사
} else {
fs.copyFileSync(srcPath, destPath); // 파일일 경우 복사
}
}
}
// 코드 합치기 및 난독화 함수
async function processFiles(inputDirPath) {
try {
// ../build에서 module 디렉토리 제외한 모든 파일 및 디렉토리 복사
const allFiles = fs.readdirSync(inputDirPath);
for (const file of allFiles) {
const filePath = path.join(inputDirPath, file);
const isModuleDir = fs.statSync(filePath).isDirectory() && file === 'module';
// module 디렉토리와 background.js 파일 건너뛰기
if (isModuleDir || file === 'background.js') continue;
const destFilePath = path.join(outputDirPath, file);
if (fs.statSync(filePath).isDirectory()) {
copyDirectory(filePath, destFilePath);
console.log(`Copied directory ${file} to ${outputDirPath}`);
} else {
fs.copyFileSync(filePath, destFilePath);
console.log(`Copied file ${file} to ${outputDirPath}`);
}
}
} catch (error) {
console.error('Error processing files:', error);
}
}
위 코드에서 copyDirectory 함수는 깊은 복사를 위해 사용됩니다. 이 함수는 재귀적으로 디렉토리 내 모든 파일을 끝까지 탐색하여 복사할 수 있도록 도와줍니다.
모든 요구사항을 만족한 최종 minify.js 파일은 아래와 같습니다.
const fs = require('fs');
const path = require('path');
const obfuscator = require('javascript-obfuscator');
// 경로 설정
const inputDirPath = path.join(__dirname, '..', 'build');
const outputFilePath = path.join(__dirname, '..', 'deploy', 'background.js');
const outputDirPath = path.dirname(outputFilePath);
// output 디렉토리 생성
if (!fs.existsSync(outputDirPath)) {
fs.mkdirSync(outputDirPath, { recursive: true });
}
// import 및 export 문, use strict 제거 함수
function removeImportsAndExports(code) {
return code
.replace(/^(import\s.*?;|export\s)/gm, '') // import/export 문 제거
.replace(/^\s*['"]use strict['"];\s*/gm, '') // 'use strict'; 제거
.trim();
}
// 코드 난독화 함수
function obfuscateCode(code) {
return obfuscator.obfuscate(code, {
compact: true,
controlFlowFlattening: true,
controlFlowFlatteningThreshold: 1,
deadCodeInjection: true,
deadCodeInjectionThreshold: 1,
stringArray: true,
stringArrayEncoding: ['base64'],
stringArrayThreshold: 1,
disableConsoleOutput: false, // console 출력 제거 여부
renameGlobals: true,
identifierNamesGenerator: 'mangled'
}).getObfuscatedCode();
}
// 디렉토리 및 파일 복사 함수
function copyDirectory(src, dest) {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true });
}
const entries = fs.readdirSync(src);
for (const entry of entries) {
const srcPath = path.join(src, entry);
const destPath = path.join(dest, entry);
if (fs.statSync(srcPath).isDirectory()) {
copyDirectory(srcPath, destPath); // 디렉토리일 경우 재귀적으로 복사
} else {
fs.copyFileSync(srcPath, destPath); // 파일일 경우 복사
}
}
}
// 코드 합치기 및 난독화 함수
async function processFiles(inputDirPath) {
try {
let combinedCode = '';
const moduleDirPath = path.join(inputDirPath, 'module');
const moduleFiles = fs.readdirSync(moduleDirPath).filter(file => file.endsWith('.js'));
// module 디렉토리 내 파일 합치기
for (const file of moduleFiles) {
const filePath = path.join(moduleDirPath, file);
const code = fs.readFileSync(filePath, 'utf8');
combinedCode += removeImportsAndExports(code) + '\n';
}
// background.js 파일 내용 추가
const backgroundFilePath = path.join(inputDirPath, 'background.js');
const backgroundCode = fs.readFileSync(backgroundFilePath, 'utf8');
combinedCode += removeImportsAndExports(backgroundCode) + '\n';
// 난독화된 코드 생성 및 저장
const obfuscatedCode = obfuscateCode(combinedCode);
fs.writeFileSync(outputFilePath, obfuscatedCode, 'utf8');
console.log(`Obfuscated code saved to: ${outputFilePath}`);
// ../build에서 module 디렉토리 제외한 모든 파일 및 디렉토리 복사
const allFiles = fs.readdirSync(inputDirPath);
for (const file of allFiles) {
const filePath = path.join(inputDirPath, file);
const isModuleDir = fs.statSync(filePath).isDirectory() && file === 'module';
// module 디렉토리와 background.js 파일 건너뛰기
if (isModuleDir || file === 'background.js') continue;
const destFilePath = path.join(outputDirPath, file);
if (fs.statSync(filePath).isDirectory()) {
copyDirectory(filePath, destFilePath);
console.log(`Copied directory ${file} to ${outputDirPath}`);
} else {
fs.copyFileSync(filePath, destFilePath);
console.log(`Copied file ${file} to ${outputDirPath}`);
}
}
} catch (error) {
console.error('Error processing files:', error);
}
}
// 메인 실행 함수
(async () => {
await processFiles(inputDirPath);
})();
모든 자동화 작업이 그렇듯, 할 때는 귀찮고 번거롭지만, 완료하고 보니 정말 편리하네요. 난독화 작업이 처음이라 시간이 꽤 걸렸지만, 다음 번에는 수월하게 진행할 수 있을 것 같습니다.

결과를 확인해보니, 해당 JS 파일만으로는 비즈니스 로직을 파악하는 것이 쉽지 않아 보입니다.
JS 파일에 중요한 비즈니스 로직이 포함되어 있고 이를 숨겨야 한다면,
이 방법을 추천드립니다!👍