종단 간 필드 암호화

요청 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 암호화해 문자열 하나를 만듭니다. 엔드유저 앱이 암호화하는 경우 고객사 서버가 kidpublicKey를 앱에 전달합니다.
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.암호문.태그)
algECDH-ES+A256KW (P-256)
encA256GCM
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_0459001encryptedFields 형식이 올바르지 않음. JWE compact가 아니거나, alg/encECDH-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 호출을 순서대로 실제로 수행해 검증해 주세요. 구현이 잘못된 경우 위 에러 코드로 원인을 확인할 수 있습니다.


Did this page help you?