nest-encrypt-cycle 1.1.7 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +6 -2
- package/CHANGELOG.md +110 -0
- package/CLAUDE.md +3 -2
- package/PERFORMANCE_ANALYSIS.md +181 -0
- package/README.md +44 -6
- package/benchmark.js +157 -0
- package/dist/src/encrypt.interceptor.js +3 -2
- package/dist/src/encrypt.interceptor.js.map +1 -1
- package/dist/src/encrypt.service.d.ts +5 -0
- package/dist/src/encrypt.service.js +62 -14
- package/dist/src/encrypt.service.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -2
- package/test-optimized.js +119 -0
- package/test-query-params.js +99 -0
- package/test-wildcard.js +154 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nest-encrypt-cycle",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "A NestJS interceptor that encrypts/decrypts request and response bodies",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@nestjs/common": "^10.0.0",
|
|
22
|
-
"crypto-js": "^4.2.0",
|
|
23
22
|
"reflect-metadata": "^0.1.13"
|
|
24
23
|
},
|
|
25
24
|
"peerDependencies": {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 최적화 후 기능 검증 테스트
|
|
3
|
+
* native crypto로 변경 후 암복호화가 정상 작동하는지 확인
|
|
4
|
+
*/
|
|
5
|
+
const { EncryptService } = require('./dist/src/encrypt.service');
|
|
6
|
+
|
|
7
|
+
// 테스트 데이터
|
|
8
|
+
const testCases = [
|
|
9
|
+
{ name: 'Small JSON', data: { id: 1, name: 'test' } },
|
|
10
|
+
{ name: 'Medium JSON', data: { id: 1, items: Array(10).fill({ field: 'value' }) } },
|
|
11
|
+
{ name: 'Large JSON', data: { id: 1, items: Array(100).fill({ field: 'value' }) } },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const KEY = '0123456789abcdef0123456789abcdef'; // 32 bytes for AES-256
|
|
15
|
+
|
|
16
|
+
// EncryptService 인스턴스 생성
|
|
17
|
+
const encryptService = new EncryptService({
|
|
18
|
+
key: KEY,
|
|
19
|
+
whiteList: [
|
|
20
|
+
{ method: 'GET', pathname: '/api/skip' },
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
console.log('🧪 Testing Optimized EncryptService\n');
|
|
25
|
+
console.log('='.repeat(80));
|
|
26
|
+
|
|
27
|
+
// 기본 기능 테스트
|
|
28
|
+
console.log('\n📝 Basic Functionality Tests:\n');
|
|
29
|
+
|
|
30
|
+
let allPassed = true;
|
|
31
|
+
|
|
32
|
+
for (const testCase of testCases) {
|
|
33
|
+
const jsonString = JSON.stringify(testCase.data);
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// 암호화
|
|
37
|
+
const encrypted = encryptService.encrypt(jsonString, '/api/test', 'POST');
|
|
38
|
+
|
|
39
|
+
// 복호화
|
|
40
|
+
const decrypted = encryptService.decrypt(encrypted, '/api/test', 'POST');
|
|
41
|
+
|
|
42
|
+
// 검증
|
|
43
|
+
const decryptedData = JSON.parse(decrypted);
|
|
44
|
+
const isValid = JSON.stringify(decryptedData) === jsonString;
|
|
45
|
+
|
|
46
|
+
if (isValid) {
|
|
47
|
+
console.log(`✅ ${testCase.name}: PASSED`);
|
|
48
|
+
console.log(` Original: ${jsonString.slice(0, 50)}...`);
|
|
49
|
+
console.log(` Encrypted: ${encrypted.slice(0, 50)}...`);
|
|
50
|
+
console.log(` Decrypted: ${decrypted.slice(0, 50)}...`);
|
|
51
|
+
} else {
|
|
52
|
+
console.log(`❌ ${testCase.name}: FAILED`);
|
|
53
|
+
console.log(` Expected: ${jsonString}`);
|
|
54
|
+
console.log(` Got: ${decrypted}`);
|
|
55
|
+
allPassed = false;
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.log(`❌ ${testCase.name}: ERROR - ${error.message}`);
|
|
59
|
+
allPassed = false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Whitelist 테스트
|
|
66
|
+
console.log('='.repeat(80));
|
|
67
|
+
console.log('\n🔓 Whitelist Functionality Test:\n');
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const testData = 'plain text data';
|
|
71
|
+
const result = encryptService.encrypt(testData, '/api/skip', 'GET');
|
|
72
|
+
|
|
73
|
+
if (result === testData) {
|
|
74
|
+
console.log('✅ Whitelist: PASSED');
|
|
75
|
+
console.log(' Data was NOT encrypted (as expected for whitelisted route)');
|
|
76
|
+
} else {
|
|
77
|
+
console.log('❌ Whitelist: FAILED');
|
|
78
|
+
console.log(` Expected: ${testData}`);
|
|
79
|
+
console.log(` Got: ${result}`);
|
|
80
|
+
allPassed = false;
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.log(`❌ Whitelist: ERROR - ${error.message}`);
|
|
84
|
+
allPassed = false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log();
|
|
88
|
+
|
|
89
|
+
// 성능 간단 테스트
|
|
90
|
+
console.log('='.repeat(80));
|
|
91
|
+
console.log('\n⚡ Quick Performance Check:\n');
|
|
92
|
+
|
|
93
|
+
const iterations = 1000;
|
|
94
|
+
const testData = JSON.stringify({ id: 1, data: Array(10).fill({ field: 'value' }) });
|
|
95
|
+
|
|
96
|
+
const start = process.hrtime.bigint();
|
|
97
|
+
for (let i = 0; i < iterations; i++) {
|
|
98
|
+
const enc = encryptService.encrypt(testData, '/api/test', 'POST');
|
|
99
|
+
encryptService.decrypt(enc, '/api/test', 'POST');
|
|
100
|
+
}
|
|
101
|
+
const end = process.hrtime.bigint();
|
|
102
|
+
|
|
103
|
+
const duration = Number(end - start) / 1000000; // ms
|
|
104
|
+
const opsPerSec = (iterations / duration) * 1000;
|
|
105
|
+
|
|
106
|
+
console.log(`${iterations} iterations completed in ${duration.toFixed(2)}ms`);
|
|
107
|
+
console.log(`Performance: ${opsPerSec.toFixed(2)} ops/sec`);
|
|
108
|
+
console.log(`Average: ${(duration / iterations).toFixed(4)}ms per operation`);
|
|
109
|
+
|
|
110
|
+
console.log();
|
|
111
|
+
console.log('='.repeat(80));
|
|
112
|
+
console.log();
|
|
113
|
+
|
|
114
|
+
if (allPassed) {
|
|
115
|
+
console.log('✅ All tests PASSED! Native crypto optimization is working correctly.');
|
|
116
|
+
} else {
|
|
117
|
+
console.log('❌ Some tests FAILED. Please review the errors above.');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Parameter 처리 테스트
|
|
3
|
+
* Whitelist에 /api/test가 있으면 /api/test?id=1도 매칭되어야 함
|
|
4
|
+
*/
|
|
5
|
+
const { EncryptService } = require('./dist/src/encrypt.service');
|
|
6
|
+
|
|
7
|
+
const KEY = '0123456789abcdef0123456789abcdef';
|
|
8
|
+
|
|
9
|
+
// Whitelist에 /api/users 추가
|
|
10
|
+
const encryptService = new EncryptService({
|
|
11
|
+
key: KEY,
|
|
12
|
+
whiteList: [
|
|
13
|
+
{ method: 'GET', pathname: '/api/users' },
|
|
14
|
+
{ method: 'POST', pathname: '/api/data' },
|
|
15
|
+
],
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
console.log('🧪 Query Parameter Whitelist Test\n');
|
|
19
|
+
console.log('='.repeat(80));
|
|
20
|
+
|
|
21
|
+
const testCases = [
|
|
22
|
+
{
|
|
23
|
+
name: 'Whitelisted path without query params',
|
|
24
|
+
pathname: '/api/users',
|
|
25
|
+
method: 'GET',
|
|
26
|
+
data: 'test data',
|
|
27
|
+
shouldEncrypt: false,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'Whitelisted path WITH query params (should still be whitelisted)',
|
|
31
|
+
pathname: '/api/users?id=123',
|
|
32
|
+
method: 'GET',
|
|
33
|
+
data: 'test data',
|
|
34
|
+
shouldEncrypt: false,
|
|
35
|
+
note: '❌ This will FAIL with old code, ✅ PASS with new code',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'Whitelisted path with multiple query params',
|
|
39
|
+
pathname: '/api/users?id=123&page=2&limit=10',
|
|
40
|
+
method: 'GET',
|
|
41
|
+
data: 'test data',
|
|
42
|
+
shouldEncrypt: false,
|
|
43
|
+
note: '❌ This will FAIL with old code, ✅ PASS with new code',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'Non-whitelisted path',
|
|
47
|
+
pathname: '/api/secret',
|
|
48
|
+
method: 'GET',
|
|
49
|
+
data: 'test data',
|
|
50
|
+
shouldEncrypt: true,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'Non-whitelisted path with query params',
|
|
54
|
+
pathname: '/api/secret?id=1',
|
|
55
|
+
method: 'GET',
|
|
56
|
+
data: 'test data',
|
|
57
|
+
shouldEncrypt: true,
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
let allPassed = true;
|
|
62
|
+
|
|
63
|
+
for (const testCase of testCases) {
|
|
64
|
+
console.log(`\n📋 ${testCase.name}`);
|
|
65
|
+
if (testCase.note) {
|
|
66
|
+
console.log(` ${testCase.note}`);
|
|
67
|
+
}
|
|
68
|
+
console.log(` Path: ${testCase.pathname}`);
|
|
69
|
+
console.log(` Method: ${testCase.method}`);
|
|
70
|
+
|
|
71
|
+
// Note: interceptor에서 pathname을 split('?')[0]으로 처리하므로
|
|
72
|
+
// 여기서도 동일하게 처리
|
|
73
|
+
const cleanPathname = testCase.pathname.split('?')[0];
|
|
74
|
+
const result = encryptService.encrypt(testCase.data, cleanPathname, testCase.method);
|
|
75
|
+
|
|
76
|
+
const wasEncrypted = result !== testCase.data;
|
|
77
|
+
const expectedBehavior = testCase.shouldEncrypt;
|
|
78
|
+
|
|
79
|
+
if (wasEncrypted === expectedBehavior) {
|
|
80
|
+
console.log(` ✅ PASSED: Data was ${wasEncrypted ? 'encrypted' : 'NOT encrypted'} (as expected)`);
|
|
81
|
+
} else {
|
|
82
|
+
console.log(` ❌ FAILED: Data was ${wasEncrypted ? 'encrypted' : 'NOT encrypted'} (expected ${expectedBehavior ? 'encrypted' : 'NOT encrypted'})`);
|
|
83
|
+
allPassed = false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log('\n' + '='.repeat(80));
|
|
88
|
+
console.log();
|
|
89
|
+
|
|
90
|
+
if (allPassed) {
|
|
91
|
+
console.log('✅ All query parameter tests PASSED!');
|
|
92
|
+
console.log('\nNow whitelist works correctly with query parameters:');
|
|
93
|
+
console.log(' - /api/users → whitelisted');
|
|
94
|
+
console.log(' - /api/users?id=1 → whitelisted (query params ignored)');
|
|
95
|
+
console.log(' - /api/users?page=2 → whitelisted (query params ignored)');
|
|
96
|
+
} else {
|
|
97
|
+
console.log('❌ Some tests FAILED.');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
package/test-wildcard.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wildcard Pattern Matching Test
|
|
3
|
+
* Test various wildcard patterns in whitelist
|
|
4
|
+
*/
|
|
5
|
+
const { EncryptService } = require('./dist/src/encrypt.service');
|
|
6
|
+
|
|
7
|
+
const KEY = '0123456789abcdef0123456789abcdef';
|
|
8
|
+
|
|
9
|
+
console.log('🧪 Wildcard Pattern Matching Test\n');
|
|
10
|
+
console.log('='.repeat(80));
|
|
11
|
+
|
|
12
|
+
// Test Case 1: Simple wildcard at the end
|
|
13
|
+
console.log('\n📋 Test 1: /api/users/* pattern');
|
|
14
|
+
console.log('-'.repeat(80));
|
|
15
|
+
|
|
16
|
+
const service1 = new EncryptService({
|
|
17
|
+
key: KEY,
|
|
18
|
+
whiteList: [
|
|
19
|
+
{ method: 'GET', pathname: '/api/users/*' },
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const testCases1 = [
|
|
24
|
+
{ pathname: '/api/users/123', expected: false, desc: 'Match: /api/users/123' },
|
|
25
|
+
{ pathname: '/api/users/abc', expected: false, desc: 'Match: /api/users/abc' },
|
|
26
|
+
{ pathname: '/api/users/uuid-1234', expected: false, desc: 'Match: /api/users/uuid-1234' },
|
|
27
|
+
{ pathname: '/api/users', expected: true, desc: 'No match: /api/users (no trailing segment)' },
|
|
28
|
+
{ pathname: '/api/users/123/profile', expected: true, desc: 'No match: /api/users/123/profile (too deep)' },
|
|
29
|
+
{ pathname: '/api/posts/1', expected: true, desc: 'No match: /api/posts/1 (different path)' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
let allPassed = true;
|
|
33
|
+
|
|
34
|
+
for (const test of testCases1) {
|
|
35
|
+
const result = service1.encrypt('test data', test.pathname, 'GET');
|
|
36
|
+
const wasEncrypted = result !== 'test data';
|
|
37
|
+
const passed = wasEncrypted === test.expected;
|
|
38
|
+
|
|
39
|
+
console.log(`${passed ? '✅' : '❌'} ${test.desc}`);
|
|
40
|
+
if (!passed) {
|
|
41
|
+
console.log(` Expected: ${test.expected ? 'encrypted' : 'NOT encrypted'}, Got: ${wasEncrypted ? 'encrypted' : 'NOT encrypted'}`);
|
|
42
|
+
allPassed = false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Test Case 2: Wildcard in the middle
|
|
47
|
+
console.log('\n📋 Test 2: /api/*/profile pattern');
|
|
48
|
+
console.log('-'.repeat(80));
|
|
49
|
+
|
|
50
|
+
const service2 = new EncryptService({
|
|
51
|
+
key: KEY,
|
|
52
|
+
whiteList: [
|
|
53
|
+
{ method: 'GET', pathname: '/api/*/profile' },
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const testCases2 = [
|
|
58
|
+
{ pathname: '/api/users/profile', expected: false, desc: 'Match: /api/users/profile' },
|
|
59
|
+
{ pathname: '/api/123/profile', expected: false, desc: 'Match: /api/123/profile' },
|
|
60
|
+
{ pathname: '/api/admin/profile', expected: false, desc: 'Match: /api/admin/profile' },
|
|
61
|
+
{ pathname: '/api/profile', expected: true, desc: 'No match: /api/profile (missing middle segment)' },
|
|
62
|
+
{ pathname: '/api/users/123/profile', expected: true, desc: 'No match: /api/users/123/profile (too many segments)' },
|
|
63
|
+
{ pathname: '/api/users/settings', expected: true, desc: 'No match: /api/users/settings (wrong ending)' },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
for (const test of testCases2) {
|
|
67
|
+
const result = service2.encrypt('test data', test.pathname, 'GET');
|
|
68
|
+
const wasEncrypted = result !== 'test data';
|
|
69
|
+
const passed = wasEncrypted === test.expected;
|
|
70
|
+
|
|
71
|
+
console.log(`${passed ? '✅' : '❌'} ${test.desc}`);
|
|
72
|
+
if (!passed) {
|
|
73
|
+
console.log(` Expected: ${test.expected ? 'encrypted' : 'NOT encrypted'}, Got: ${wasEncrypted ? 'encrypted' : 'NOT encrypted'}`);
|
|
74
|
+
allPassed = false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Test Case 3: Multiple wildcards
|
|
79
|
+
console.log('\n📋 Test 3: /api/*/posts/* pattern');
|
|
80
|
+
console.log('-'.repeat(80));
|
|
81
|
+
|
|
82
|
+
const service3 = new EncryptService({
|
|
83
|
+
key: KEY,
|
|
84
|
+
whiteList: [
|
|
85
|
+
{ method: 'POST', pathname: '/api/*/posts/*' },
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const testCases3 = [
|
|
90
|
+
{ pathname: '/api/users/posts/123', method: 'POST', expected: false, desc: 'Match: /api/users/posts/123' },
|
|
91
|
+
{ pathname: '/api/admin/posts/456', method: 'POST', expected: false, desc: 'Match: /api/admin/posts/456' },
|
|
92
|
+
{ pathname: '/api/users/posts', method: 'POST', expected: true, desc: 'No match: /api/users/posts (missing last segment)' },
|
|
93
|
+
{ pathname: '/api/users/posts/123', method: 'GET', expected: true, desc: 'No match: wrong method (GET instead of POST)' },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
for (const test of testCases3) {
|
|
97
|
+
const result = service3.encrypt('test data', test.pathname, test.method);
|
|
98
|
+
const wasEncrypted = result !== 'test data';
|
|
99
|
+
const passed = wasEncrypted === test.expected;
|
|
100
|
+
|
|
101
|
+
console.log(`${passed ? '✅' : '❌'} ${test.desc}`);
|
|
102
|
+
if (!passed) {
|
|
103
|
+
console.log(` Expected: ${test.expected ? 'encrypted' : 'NOT encrypted'}, Got: ${wasEncrypted ? 'encrypted' : 'NOT encrypted'}`);
|
|
104
|
+
allPassed = false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Test Case 4: Mix of exact and wildcard patterns
|
|
109
|
+
console.log('\n📋 Test 4: Mixed exact and wildcard patterns');
|
|
110
|
+
console.log('-'.repeat(80));
|
|
111
|
+
|
|
112
|
+
const service4 = new EncryptService({
|
|
113
|
+
key: KEY,
|
|
114
|
+
whiteList: [
|
|
115
|
+
{ method: 'GET', pathname: '/api/health' }, // Exact
|
|
116
|
+
{ method: 'GET', pathname: '/api/users/*' }, // Wildcard
|
|
117
|
+
{ method: 'POST', pathname: '/api/*/create' }, // Wildcard
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const testCases4 = [
|
|
122
|
+
{ pathname: '/api/health', method: 'GET', expected: false, desc: 'Match (exact): /api/health' },
|
|
123
|
+
{ pathname: '/api/users/123', method: 'GET', expected: false, desc: 'Match (wildcard): /api/users/123' },
|
|
124
|
+
{ pathname: '/api/posts/create', method: 'POST', expected: false, desc: 'Match (wildcard): /api/posts/create' },
|
|
125
|
+
{ pathname: '/api/users/create', method: 'POST', expected: false, desc: 'Match (wildcard): /api/users/create' },
|
|
126
|
+
{ pathname: '/api/unknown', method: 'GET', expected: true, desc: 'No match: /api/unknown' },
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
for (const test of testCases4) {
|
|
130
|
+
const result = service4.encrypt('test data', test.pathname, test.method);
|
|
131
|
+
const wasEncrypted = result !== 'test data';
|
|
132
|
+
const passed = wasEncrypted === test.expected;
|
|
133
|
+
|
|
134
|
+
console.log(`${passed ? '✅' : '❌'} ${test.desc}`);
|
|
135
|
+
if (!passed) {
|
|
136
|
+
console.log(` Expected: ${test.expected ? 'encrypted' : 'NOT encrypted'}, Got: ${wasEncrypted ? 'encrypted' : 'NOT encrypted'}`);
|
|
137
|
+
allPassed = false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log('\n' + '='.repeat(80));
|
|
142
|
+
console.log();
|
|
143
|
+
|
|
144
|
+
if (allPassed) {
|
|
145
|
+
console.log('✅ All wildcard pattern tests PASSED!\n');
|
|
146
|
+
console.log('Wildcard patterns now supported:');
|
|
147
|
+
console.log(' - /api/users/* → matches /api/users/123, /api/users/abc');
|
|
148
|
+
console.log(' - /api/*/profile → matches /api/users/profile, /api/admin/profile');
|
|
149
|
+
console.log(' - /api/*/posts/* → matches /api/users/posts/1, /api/admin/posts/2');
|
|
150
|
+
console.log(' - Can mix exact + wildcard patterns in same whitelist');
|
|
151
|
+
} else {
|
|
152
|
+
console.log('❌ Some tests FAILED.');
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|