종단 간 필드 암호화
요청 body의 특정 필드를 옥텟이 발급한 1회용 공개키로 암호화해 전송할 수 있습니다.
옥텟이 복호화해 원래 필드로 되돌려 놓고 기존 로직을 그대로 수행합니다.
요청 body에 encryptedFields 필드가 있으면 종단간 필드 암호화를 사용하는 것으로 판단합니다.
암호화는 엔드유저 앱에서 하고 고객사 서버가 그대로 중계할 수도 있고, 고객사 서버가 직접 할 수도 있습니다. 두 경우 모두 암호화한 지점부터 옥텟까지 평문이 드러나지 않습니다.
TIP
이 기능은 선택 사항입니다.encryptedFields없이 호출하면 기존과 동일하게 동작합니다.
API 전문 암호화와 함께 사용할 수 있습니다. 전문 암호화는 고객사 서버가 장기 대칭키로 요청 전체를 암호화하는 기능이고, 종단간 필드 암호화는 1회용 공개키로 특정 필드만 암호화하는 기능입니다.
지원 API와 암호화할 수 있는 필드
암호화 대상 필드를 body로 받는 모든 API에서 사용할 수 있습니다.
| 암호화할 수 있는 필드 | 사용하는 API |
|---|---|
pin | 자식주소 생성, 자식주소 PIN 등록·수정, 출금 신청, NFT 출금 신청 등 PIN을 받는 모든 API |
recoveryPin | 자식주소 생성, 자식주소 PIN 등록·수정·삭제 |
위 목록 밖의 필드를 암호화하면 요청이 거절됩니다(ERR_0459004). 암호화 필드의 값은 문자열이어야 합니다. 대상 필드는 앞으로 확대될 예정입니다.
동작 흐름
| 단계 | 주체 | 하는 일 |
|---|---|---|
| 1 | 고객사 서버 | 종단간 필드 암호화 키 발급 API를 호출해 { kid, publicKey, alg, enc, expiresAt }를 받습니다. 키는 5분 만료, 1회용입니다. |
| 2 | 암호화 주체 (엔드유저 앱 또는 고객사 서버) | { "pin": "...", "recoveryPin": "..." } JSON을 publicKey로 JWE 암호화해 문자열 하나를 만듭니다. 엔드유저 앱이 암호화하는 경우 고객사 서버가 kid와 publicKey를 앱에 전달합니다. |
| 3 | 고객사 서버 | 기존 API body에서 pin, recoveryPin 대신 encryptedFields 필드로 전송합니다. |
| 4 | 옥텟 | kid로 개인키를 찾아 복호화하고 키를 즉시 폐기합니다. 복호화된 필드를 body에 되돌려 놓고 기존 로직을 수행합니다. |
종단간 필드 암호화하기
1. 키 발급
고객사 서버가 종단간 필드 암호화 키 발급 API(POST /2.0/e2e-field-encryption/keys)를 호출합니다.
// 응답
{
"kid": "7f3a2c9e-5b1d-4e8a-9f7d-2c5e8a1b4d6f",
"publicKey": {
"kty": "EC",
"crv": "P-256",
"x": "Vji7TZu5c-g8e7aH1CwaCRXrsaWNQCDOlc7rS6ZMcfg",
"y": "mI_nTSD-lRIad79h-ZvTHBaVgO0vMykRbocX7_QZ4DM",
"kid": "7f3a2c9e-5b1d-4e8a-9f7d-2c5e8a1b4d6f",
"alg": "ECDH-ES+A256KW",
"use": "enc"
},
"alg": "ECDH-ES+A256KW",
"enc": "A256GCM",
"expiresAt": "2026-09-03T10:05:00.000Z"
}publicKey는 JWK(RFC 7517) 형식의 EC P-256 공개키입니다. JOSE 라이브러리에 그대로 import합니다.- 키는
expiresAt(발급 후 5분)이 지나거나 한 번 사용되면 폐기됩니다. 요청 하나마다 키를 새로 발급받아야 합니다. - 엔드유저 앱에서 암호화하는 경우, PIN 입력 화면을 열 때 발급받아 앱에 전달하는 것을 권장합니다.
2. 암호화
암호화할 필드들을 JSON 객체로 묶어 JWE(RFC 7516) compact serialization으로 암호화합니다.
JSON의 키 이름이 곧 어떤 필드를 암호화했는지의 선언입니다.
| 항목 | 값 |
|---|---|
| 형식 | JWE compact serialization (헤더.암호화된키.IV.암호문.태그) |
alg | ECDH-ES+A256KW (P-256) |
enc | A256GCM |
kid | 키 발급 응답의 kid (protected header에 포함 필수) |
| 평문 | 문자열 값만 가진 JSON 객체. 예: {"pin":"123456","recoveryPin":"654321"} |
암호학을 직접 구현할 필요는 없습니다. 발급받은 JWK와 평문을 JOSE 라이브러리에 넣으면 됩니다.
라이브러리가 메시지마다 임시 EC 키를 만들어 옥텟 공개키와 키 합의를 하고(그 임시 공개키는 헤더 epk에 자동으로 들어갑니다), 본문은 AES-256-GCM으로 암호화합니다.
// jose: 4.13 이상 (4.15.9로 검증. 브라우저·React Native·Node.js 공통)
import { CompactEncrypt, importJWK } from 'jose';
// 키 발급 API 응답의 publicKey
const publicKey = {
kty: 'EC',
crv: 'P-256',
x: 'Vji7TZu5c-g8e7aH1CwaCRXrsaWNQCDOlc7rS6ZMcfg',
y: 'mI_nTSD-lRIad79h-ZvTHBaVgO0vMykRbocX7_QZ4DM',
kid: '7f3a2c9e-5b1d-4e8a-9f7d-2c5e8a1b4d6f',
alg: 'ECDH-ES+A256KW',
use: 'enc',
};
// 암호화할 필드만 넣는다 (자식주소 생성이면 pin + recoveryPin, 출금이면 pin)
const fields = { pin: '123456', recoveryPin: '654321' };
const key = await importJWK(publicKey, 'ECDH-ES+A256KW');
const encryptedFields = await new CompactEncrypt(new TextEncoder().encode(JSON.stringify(fields)))
.setProtectedHeader({ alg: 'ECDH-ES+A256KW', enc: 'A256GCM', kid: publicKey.kid })
.encrypt(key);
// eyJhbGciOiJFQ0RILUVTK0EyNTZLVyIsImVuYyI6IkEyNTZHQ00iLCJraWQiOiI3ZjNhMmM5ZS0... (약 440자)// nimbus-jose-jwt: 9.37.3 (JDK 17). Android에서도 동일하게 사용할 수 있습니다.
import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWEHeader;
import com.nimbusds.jose.JWEObject;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.ECDHEncrypter;
import com.nimbusds.jose.jwk.ECKey;
// 키 발급 API 응답의 publicKey(JWK) JSON 문자열
String publicKeyJson = "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"Vji7TZu5c-g8e7aH1CwaCRXrsaWNQCDOlc7rS6ZMcfg\",\"y\":\"mI_nTSD-lRIad79h-ZvTHBaVgO0vMykRbocX7_QZ4DM\",\"kid\":\"7f3a2c9e-5b1d-4e8a-9f7d-2c5e8a1b4d6f\",\"alg\":\"ECDH-ES+A256KW\",\"use\":\"enc\"}";
// 암호화할 필드만 넣는다 (자식주소 생성이면 pin + recoveryPin, 출금이면 pin)
String fieldsJson = "{\"pin\":\"123456\",\"recoveryPin\":\"654321\"}";
ECKey jwk = ECKey.parse(publicKeyJson);
JWEHeader header = new JWEHeader.Builder(JWEAlgorithm.ECDH_ES_A256KW, EncryptionMethod.A256GCM)
.keyID(jwk.getKeyID())
.build();
JWEObject jwe = new JWEObject(header, new Payload(fieldsJson));
jwe.encrypt(new ECDHEncrypter(jwk));
String encryptedFields = jwe.serialize();
// eyJlcGsiOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJt... (약 440자)같은 평문도 암호화할 때마다 결과가 다릅니다(메시지마다 임시 키·난수 AES 키·IV를 사용). TypeScript와 Java의 헤더 필드 순서가 달라 출력 형태가 다르지만, 옥텟은 둘 다 동일하게 처리합니다.
3. 옥텟으로 전송
encryptedFields 문자열을 기존 API body에 암호화한 필드 대신 넣어 호출합니다. endpoint와 나머지 필드는 기존과 같습니다.
// 자식주소 생성 API Payload
// 기존
{
"offset": 1,
"pin": "123456",
"recoveryPin": "654321"
}
// 종단간 필드 암호화 사용 시
{
"offset": 1,
"encryptedFields": "eyJhbGciOiJFQ0RILUVTK0EyNTZLVyIsImVuYyI6IkEyNTZHQ00iLCJraWQiOiI..."
}// 출금 신청 API Payload (종단간 필드 암호화 사용 시)
{
"symbol": "ETH",
"requestId": "user1-amount0.001-symbolETH-202609031830",
"amount": "0.001",
"senderAddress": "0x55Bc4ba7A86A00C8A1A09C59DcC32eA2E7FAbCd4",
"receiverAddress": "0xee3c0f0AF227dA8DaccbF3Bc33bCf20f0a222aEF",
"encryptedUserKey": "rsa encrypted userkey",
"encryptedFields": "eyJhbGciOiJFQ0RILUVTK0EyNTZLVyIsImVuYyI6IkEyNTZHQ00iLCJraWQiOiI..."
}- 암호화한 필드(
pin,recoveryPin)를 평문으로 함께 보내면 요청이 거절됩니다(ERR_0459005). encryptedUserKey는 고객사가 옥텟 RSA 공개키로 직접 암호화하는 값이며, 종단간 필드 암호화의 대상이 아닙니다. 이름이 비슷하지만 사용하는 키와 알고리즘이 다르므로 기존처럼 별도 필드로 전송합니다.- API 전문 암호화를 함께 사용하는 경우,
encryptedFields가 포함된 body를 전문 암호화하고 HMAC도 그 body로 계산합니다. - 옥텟은
encryptedFields가 어떤 요청에 쓰이는지는 검증하지 않습니다(키가 유효한 동안 어느 API 요청에든 붙일 수 있음). 엔드유저 앱에서 암호화하는 경우, 고객사 서버는 앱이 보낸encryptedFields를 그 유저가 요청한 API 호출에만 사용해야 합니다.
에러 코드
| 코드 | 상황 | 대응 |
|---|---|---|
ERR_0459001 | encryptedFields 형식이 올바르지 않음. JWE compact가 아니거나, alg/enc가 ECDH-ES+A256KW/A256GCM이 아니거나, kid 또는 epk가 없음 | 라이브러리 설정(alg, enc, kid)을 확인합니다. |
ERR_0459002 | 키를 찾을 수 없음. 만료(5분)되었거나 이미 사용된 키 | 키를 다시 발급받아 다시 암호화합니다. |
ERR_0459003 | 복호화 실패. 다른 키로 암호화했거나 값이 변조되었거나, 평문이 문자열 값만 가진 JSON 객체가 아님 | 발급받은 publicKey로 암호화했는지, 평문 형식을 확인합니다. |
ERR_0459004 | 암호화할 수 없는 필드가 포함됨 | 암호화 가능 필드(pin, recoveryPin)만 넣습니다. |
ERR_0459005 | 암호화한 필드를 평문으로도 함께 보냄 | body에서 평문 필드를 제거합니다. |
키는 옥텟이 encryptedFields를 읽는 즉시 폐기됩니다. 위 오류나 PIN 불일치로 요청이 실패한 뒤 다시 시도할 때는 키를 새로 발급받아 다시 암호화해야 합니다.
테스트
JWE는 매번 난수를 사용하므로 "이 값이 나와야 한다"는 고정 검증값을 제공하지 않습니다.
테스트 환경에서 키 발급, 암호화, API 호출을 순서대로 실제로 수행해 검증해 주세요. 구현이 잘못된 경우 위 에러 코드로 원인을 확인할 수 있습니다.
Updated about 6 hours ago