98d815e659
/manage/crypto 하위에 등록된 암호화모듈 목록/단건 조회, DYNAMIC 키 전략(KeyDerivationStrategy) 로드 상태 확인, 동적 키 캐시 키 목록 조회, Base64 기반 암복호화 라운드트립 테스트 엔드포인트를 제공한다. 원본 암호키(encKeyHex/decKeyHex/ivHex)는 응답에 포함하지 않는다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
392 lines
18 KiB
JSON
392 lines
18 KiB
JSON
{
|
|
"info": {
|
|
"_postman_id": "b7e2e1d0-3c1a-4b0a-9d2e-crypto-manage-0001",
|
|
"name": "CryptoModuleManage API (/manage/crypto)",
|
|
"description": "CryptoModuleManager 기반 암호화모듈 진단 및 암복호화 테스트 API 샘플 모음.\n\n사용 전 확인사항:\n1. Collection Variables 의 baseUrl 을 실제 서버 주소로 변경\n2. cryptoName / dynamicCryptoName 을 DB(crypto_module_config)에 실제 등록된 모듈명으로 변경\n3. DYNAMIC 모듈은 keyDerivParams 의 contextKey 값과 dynamicContextKey 변수가 일치해야 함 (예: X-Api-Enc-Key)\n\n주의: 이 API는 원본 암호키(encKeyHex/decKeyHex/ivHex)를 절대 응답에 포함하지 않는다. 진단 응답은 메타데이터와 키 도출 전략(KeyDerivationStrategy) 로드 상태만 노출한다.",
|
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
|
},
|
|
"variable": [
|
|
{ "key": "baseUrl", "value": "http://localhost:8080", "type": "string" },
|
|
{ "key": "cryptoName", "value": "SAMPLE_STATIC", "type": "string", "description": "STATIC 키 방식으로 등록된 실제 cryptoName으로 교체" },
|
|
{ "key": "dynamicCryptoName", "value": "SAMPLE_DYNAMIC", "type": "string", "description": "DYNAMIC 키 방식으로 등록된 실제 cryptoName으로 교체" },
|
|
{ "key": "dynamicContextKey", "value": "X-Api-Enc-Key", "type": "string", "description": "DYNAMIC 모듈의 key_deriv_params.contextKey 값과 일치해야 함" },
|
|
{ "key": "dynamicContextValue", "value": "clientKeyValue01", "type": "string" },
|
|
{ "key": "plainText", "value": "테스트 평문입니다", "type": "string" },
|
|
{ "key": "plainTextBase64", "value": "", "type": "string" },
|
|
{ "key": "staticCipherTextBase64", "value": "", "type": "string" },
|
|
{ "key": "dynamicCipherTextBase64", "value": "", "type": "string" }
|
|
],
|
|
"item": [
|
|
{
|
|
"name": "1. 진단 (Diagnostics)",
|
|
"item": [
|
|
{
|
|
"name": "전체 암호화모듈 목록 조회",
|
|
"request": {
|
|
"method": "GET",
|
|
"header": [],
|
|
"url": {
|
|
"raw": "{{baseUrl}}/manage/crypto/list",
|
|
"host": ["{{baseUrl}}"],
|
|
"path": ["manage", "crypto", "list"]
|
|
},
|
|
"description": "등록된 전체 암호화모듈의 진단정보(메타데이터)를 반환한다. 원본 키는 포함되지 않는다."
|
|
},
|
|
"event": [
|
|
{
|
|
"listen": "test",
|
|
"script": {
|
|
"type": "text/javascript",
|
|
"exec": [
|
|
"const json = pm.response.json();",
|
|
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
|
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
|
"pm.test('data는 배열', () => pm.expect(json.data).to.be.an('array'));",
|
|
"pm.test('원본 키 필드가 응답에 없어야 함', () => {",
|
|
" const body = pm.response.text();",
|
|
" pm.expect(body).to.not.include('encKeyHex');",
|
|
" pm.expect(body).to.not.include('decKeyHex');",
|
|
"});"
|
|
]
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "단일 암호화모듈 조회",
|
|
"request": {
|
|
"method": "GET",
|
|
"header": [],
|
|
"url": {
|
|
"raw": "{{baseUrl}}/manage/crypto/{{cryptoName}}",
|
|
"host": ["{{baseUrl}}"],
|
|
"path": ["manage", "crypto", "{{cryptoName}}"]
|
|
},
|
|
"description": "cryptoName 하나의 진단정보를 조회한다. STATIC 모듈은 keyDerivStrategy가 null, strategyLoaded가 false로 내려온다."
|
|
},
|
|
"event": [
|
|
{
|
|
"listen": "test",
|
|
"script": {
|
|
"type": "text/javascript",
|
|
"exec": [
|
|
"const json = pm.response.json();",
|
|
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
|
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
|
"pm.test('cryptoName 일치', () => pm.expect(json.data.cryptoName).to.eql(pm.collectionVariables.get('cryptoName')));"
|
|
]
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "DYNAMIC 모듈 조회 (전략 로드 확인)",
|
|
"request": {
|
|
"method": "GET",
|
|
"header": [],
|
|
"url": {
|
|
"raw": "{{baseUrl}}/manage/crypto/{{dynamicCryptoName}}",
|
|
"host": ["{{baseUrl}}"],
|
|
"path": ["manage", "crypto", "{{dynamicCryptoName}}"]
|
|
},
|
|
"description": "DYNAMIC 모듈은 resolveStrategy를 통해 키 도출 전략(KeyDerivationStrategy) 클래스가 정상 로드되는지 strategyLoaded/strategyClassName으로 확인할 수 있다."
|
|
},
|
|
"event": [
|
|
{
|
|
"listen": "test",
|
|
"script": {
|
|
"type": "text/javascript",
|
|
"exec": [
|
|
"const json = pm.response.json();",
|
|
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
|
"pm.test('keySourceType = DYNAMIC', () => pm.expect(json.data.keySourceType).to.eql('DYNAMIC'));",
|
|
"pm.test('strategyLoaded = true', () => pm.expect(json.data.strategyLoaded).to.eql(true));",
|
|
"pm.test('strategyClassName 존재', () => pm.expect(json.data.strategyClassName).to.be.a('string'));"
|
|
]
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "동적 키 캐시 목록 조회",
|
|
"request": {
|
|
"method": "GET",
|
|
"header": [],
|
|
"url": {
|
|
"raw": "{{baseUrl}}/manage/crypto/cache/dynamic-keys",
|
|
"host": ["{{baseUrl}}"],
|
|
"path": ["manage", "crypto", "cache", "dynamic-keys"]
|
|
},
|
|
"description": "DYNAMIC 키 방식 모듈이 캐시한 키의 캐시-키 목록(예: 'cryptoName:contextValue')을 반환한다. 원본 키 값이 아니다. 아래 'DYNAMIC 키' 폴더의 암호화 요청을 먼저 실행한 뒤 호출하면 목록이 채워진다."
|
|
},
|
|
"event": [
|
|
{
|
|
"listen": "test",
|
|
"script": {
|
|
"type": "text/javascript",
|
|
"exec": [
|
|
"const json = pm.response.json();",
|
|
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
|
"pm.test('data는 배열', () => pm.expect(json.data).to.be.an('array'));"
|
|
]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "2. STATIC 키 암복호화 테스트",
|
|
"item": [
|
|
{
|
|
"name": "암호화 (encrypt)",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
|
"body": {
|
|
"mode": "raw",
|
|
"raw": "{\n \"plainTextBase64\": \"{{plainTextBase64}}\"\n}"
|
|
},
|
|
"url": {
|
|
"raw": "{{baseUrl}}/manage/crypto/{{cryptoName}}/test/encrypt",
|
|
"host": ["{{baseUrl}}"],
|
|
"path": ["manage", "crypto", "{{cryptoName}}", "test", "encrypt"]
|
|
},
|
|
"description": "STATIC 키 방식 모듈로 평문(Base64)을 암호화한다. runtimeContext 없이 호출한다."
|
|
},
|
|
"event": [
|
|
{
|
|
"listen": "prerequest",
|
|
"script": {
|
|
"type": "text/javascript",
|
|
"exec": [
|
|
"const plainText = pm.collectionVariables.get('plainText');",
|
|
"pm.collectionVariables.set('plainTextBase64', btoa(unescape(encodeURIComponent(plainText))));"
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"listen": "test",
|
|
"script": {
|
|
"type": "text/javascript",
|
|
"exec": [
|
|
"const json = pm.response.json();",
|
|
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
|
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
|
"pm.test('cipherTextBase64 존재', () => pm.expect(json.data.cipherTextBase64).to.be.a('string'));",
|
|
"pm.test('평문 base64와 달라야 함', () => pm.expect(json.data.cipherTextBase64).to.not.eql(pm.collectionVariables.get('plainTextBase64')));",
|
|
"pm.collectionVariables.set('staticCipherTextBase64', json.data.cipherTextBase64);"
|
|
]
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "복호화 (decrypt) — 위 암호화 결과 사용",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
|
"body": {
|
|
"mode": "raw",
|
|
"raw": "{\n \"cipherTextBase64\": \"{{staticCipherTextBase64}}\"\n}"
|
|
},
|
|
"url": {
|
|
"raw": "{{baseUrl}}/manage/crypto/{{cryptoName}}/test/decrypt",
|
|
"host": ["{{baseUrl}}"],
|
|
"path": ["manage", "crypto", "{{cryptoName}}", "test", "decrypt"]
|
|
},
|
|
"description": "바로 위 '암호화 (encrypt)' 요청에서 저장한 staticCipherTextBase64를 복호화하여 원문과 일치하는지 확인한다. 반드시 암호화 요청을 먼저 실행할 것."
|
|
},
|
|
"event": [
|
|
{
|
|
"listen": "test",
|
|
"script": {
|
|
"type": "text/javascript",
|
|
"exec": [
|
|
"const json = pm.response.json();",
|
|
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
|
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
|
"const decoded = decodeURIComponent(escape(atob(json.data.plainTextBase64)));",
|
|
"pm.test('원문과 일치 (라운드트립)', () => pm.expect(decoded).to.eql(pm.collectionVariables.get('plainText')));"
|
|
]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "3. DYNAMIC 키 암복호화 테스트",
|
|
"item": [
|
|
{
|
|
"name": "암호화 (encrypt, runtimeContext 포함)",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
|
"body": {
|
|
"mode": "raw",
|
|
"raw": "{\n \"plainTextBase64\": \"{{plainTextBase64}}\",\n \"runtimeContext\": {\n \"{{dynamicContextKey}}\": \"{{dynamicContextValue}}\"\n }\n}"
|
|
},
|
|
"url": {
|
|
"raw": "{{baseUrl}}/manage/crypto/{{dynamicCryptoName}}/test/encrypt",
|
|
"host": ["{{baseUrl}}"],
|
|
"path": ["manage", "crypto", "{{dynamicCryptoName}}", "test", "encrypt"]
|
|
},
|
|
"description": "DYNAMIC 키 방식 모듈로 runtimeContext(예: 헤더값)를 전달하여 키를 도출한 뒤 암호화한다. dynamicContextKey는 DB에 등록된 key_deriv_params.contextKey와 일치해야 한다."
|
|
},
|
|
"event": [
|
|
{
|
|
"listen": "prerequest",
|
|
"script": {
|
|
"type": "text/javascript",
|
|
"exec": [
|
|
"const plainText = pm.collectionVariables.get('plainText');",
|
|
"pm.collectionVariables.set('plainTextBase64', btoa(unescape(encodeURIComponent(plainText))));"
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"listen": "test",
|
|
"script": {
|
|
"type": "text/javascript",
|
|
"exec": [
|
|
"const json = pm.response.json();",
|
|
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
|
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
|
"pm.test('cipherTextBase64 존재', () => pm.expect(json.data.cipherTextBase64).to.be.a('string'));",
|
|
"pm.collectionVariables.set('dynamicCipherTextBase64', json.data.cipherTextBase64);"
|
|
]
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "복호화 (decrypt, 동일 runtimeContext 필요)",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
|
"body": {
|
|
"mode": "raw",
|
|
"raw": "{\n \"cipherTextBase64\": \"{{dynamicCipherTextBase64}}\",\n \"runtimeContext\": {\n \"{{dynamicContextKey}}\": \"{{dynamicContextValue}}\"\n }\n}"
|
|
},
|
|
"url": {
|
|
"raw": "{{baseUrl}}/manage/crypto/{{dynamicCryptoName}}/test/decrypt",
|
|
"host": ["{{baseUrl}}"],
|
|
"path": ["manage", "crypto", "{{dynamicCryptoName}}", "test", "decrypt"]
|
|
},
|
|
"description": "암호화 시 사용한 것과 동일한 runtimeContext를 전달해야 동일한 키가 도출되어 복호화에 성공한다. dynamicContextValue를 바꿔서 보내면 키가 달라져 복호화가 실패(예외)하는 것도 확인해볼 수 있다."
|
|
},
|
|
"event": [
|
|
{
|
|
"listen": "test",
|
|
"script": {
|
|
"type": "text/javascript",
|
|
"exec": [
|
|
"const json = pm.response.json();",
|
|
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
|
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
|
"const decoded = decodeURIComponent(escape(atob(json.data.plainTextBase64)));",
|
|
"pm.test('원문과 일치 (라운드트립)', () => pm.expect(decoded).to.eql(pm.collectionVariables.get('plainText')));"
|
|
]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "4. 에러 케이스",
|
|
"item": [
|
|
{
|
|
"name": "미등록 cryptoName 조회 → success=false",
|
|
"request": {
|
|
"method": "GET",
|
|
"header": [],
|
|
"url": {
|
|
"raw": "{{baseUrl}}/manage/crypto/NOT_REGISTERED_MODULE",
|
|
"host": ["{{baseUrl}}"],
|
|
"path": ["manage", "crypto", "NOT_REGISTERED_MODULE"]
|
|
},
|
|
"description": "존재하지 않는 cryptoName 조회 시 HTTP 200 + success=false + message에 사유가 담겨 반환되는지 확인한다 (예외가 그대로 500으로 노출되지 않음)."
|
|
},
|
|
"event": [
|
|
{
|
|
"listen": "test",
|
|
"script": {
|
|
"type": "text/javascript",
|
|
"exec": [
|
|
"const json = pm.response.json();",
|
|
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
|
"pm.test('success = false', () => pm.expect(json.success).to.eql(false));",
|
|
"pm.test('message에 모듈명 포함', () => pm.expect(json.message).to.include('NOT_REGISTERED_MODULE'));"
|
|
]
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "잘못된 Base64로 복호화 → success=false",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
|
"body": {
|
|
"mode": "raw",
|
|
"raw": "{\n \"cipherTextBase64\": \"not-valid-base64!!\"\n}"
|
|
},
|
|
"url": {
|
|
"raw": "{{baseUrl}}/manage/crypto/{{cryptoName}}/test/decrypt",
|
|
"host": ["{{baseUrl}}"],
|
|
"path": ["manage", "crypto", "{{cryptoName}}", "test", "decrypt"]
|
|
},
|
|
"description": "Base64 형식이 아닌 문자열을 전달하면 success=false로 처리되고 서버가 500으로 죽지 않는지 확인한다."
|
|
},
|
|
"event": [
|
|
{
|
|
"listen": "test",
|
|
"script": {
|
|
"type": "text/javascript",
|
|
"exec": [
|
|
"const json = pm.response.json();",
|
|
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
|
"pm.test('success = false', () => pm.expect(json.success).to.eql(false));",
|
|
"pm.test('message 존재', () => pm.expect(json.message).to.be.a('string'));"
|
|
]
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "다른 runtimeContext로 DYNAMIC 복호화 시도 → 실패 확인",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
|
"body": {
|
|
"mode": "raw",
|
|
"raw": "{\n \"cipherTextBase64\": \"{{dynamicCipherTextBase64}}\",\n \"runtimeContext\": {\n \"{{dynamicContextKey}}\": \"differentContextValue\"\n }\n}"
|
|
},
|
|
"url": {
|
|
"raw": "{{baseUrl}}/manage/crypto/{{dynamicCryptoName}}/test/decrypt",
|
|
"host": ["{{baseUrl}}"],
|
|
"path": ["manage", "crypto", "{{dynamicCryptoName}}", "test", "decrypt"]
|
|
},
|
|
"description": "'3. DYNAMIC 키' 폴더의 암호화를 먼저 실행해 dynamicCipherTextBase64를 채운 뒤, 암호화 때와 다른 runtimeContext 값으로 복호화를 시도한다. 키가 달라 padding/복호화 오류로 success=false가 되는 것을 확인한다 (패딩 모드에 따라 우연히 성공할 수도 있으니 참고용)."
|
|
},
|
|
"event": [
|
|
{
|
|
"listen": "test",
|
|
"script": {
|
|
"type": "text/javascript",
|
|
"exec": [
|
|
"const json = pm.response.json();",
|
|
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
|
"console.log('다른 컨텍스트 복호화 결과:', JSON.stringify(json));"
|
|
]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|