API 전문 암호화

추가적인 보안을 위해 출금 API 호출 시 전문을 암호화할 수 있습니다. 기존 출금 API의 동일한 endpoint를 사용합니다. Header에 Octet-Access-Key가 포함되어 있다면 API 전문 암호화를 사용하는 것으로 판단합니다. 따라서 해당 기능 사용 시 반드시 AccessKey를 발급 받아 사용해야 합니다. API Payload를 암호화하여 출금 API를 호출하는 방법은 아래 가이드를 참고해 주세요.

TIP
해당 기능은 모든 키 관리 유형을 지원합니다.
싱글시그 지갑과 멀티시그 지갑 모두 API 전문을 암호화하여 출금할 수 있습니다.


지원 API

  • 출금 신청
  • NFT 컨트랙트 배포 신청
  • NFT 아이템 생성 신청
  • NFT 출금 신청
  • NFT contract method execution 신청




API 전문 암호화하기

  1. AES 키 생성 API를 호출해 양방향 암호화를 위한 키 쌍을 생성합니다.

    SecretKeyhashKey는 발급할 때만 확인이 가능하며, AccessKey는 언제든지 확인할 수 있습니다. 만약 SecretKeyhashKey를 분실했다면, 키를 재발급 받아야 합니다.


  1. API 호출을 위한 Body를 만듭니다.
    전문 암호화 기능을 이용하게 되면 유저키를 RSA 암호화 하여 전송할 필요가 없습니다. 평문으로 전송해도 되고, 기존처럼 RSA 암호화하여 전송해도 됩니다. 단, 평문으로 전송할 때에는 userKey 로 전송해야 합니다.
// 기존 멀티시그 지갑 출금 신청 API Payload
{
	...
	"encryptedUserKey": "rsa encrypted userkey",
	...
}

// 암호화 시 멀티시그 지갑 출금 신청 API Payload
{
	...
	"userKey": "802...",
	...
}

  1. 암호화 한 결과 값을 Body에 data로 넣습니다.
// API 호출 시 Body
{
	"data": "Owl9/MDqndW0jNpz2waUSO3Ed50ZGwTNEAjXTkhlLYVPi9e05cMzKeugYFKywPDNx/FCeUQOKzHeyHKKsKFLVC+7Eo5mrefC30t8E4tfjDMY+PRQJPLl4LsQICCiTDUr6n/s/y/6hchNlea1beobmSHY9IFDc39aQJmr1T3gYh0fhQOTHCv9GvyAmRwv18L7wOLvTT/jYySLq2uYa24hR4IcCtUe4yYLUmPMYqPi4Rh4aXOkooOWKJP0ARdtk09WQwKNAh1NbHpWkIa3CRpHUg=="
}

  1. API 호출하는 Header에는 AccessKey를 넣습니다.
HEADER
Authorization: 'Bearer {apiToken}'
Octet-Access-Key: '{Access Key}'

BODY
{
	"data": "Owl9/MDqndW0jNpz2waUSO3Ed50ZGwTNEAjXTkhlLYVPi9e05cMzKeugYFKywPDNx/FCeUQOKzHeyHKKsKFLVC+7Eo5mrefC30t8E4tfjDMY+PRQJPLl4LsQICCiTDUr6n/s/y/6hchNlea1beobmSHY9IFDc39aQJmr1T3gYh0fhQOTHCv9GvyAmRwv18L7wOLvTT/jYySLq2uYa24hR4IcCtUe4yYLUmPMYqPi4Rh4aXOkooOWKJP0ARdtk09WQwKNAh1NbHpWkIa3CRpHUg=="
}

  1. Payload의 무결성을 확인하기 위해 HMAC을 헤더에 포함하여 전송하여야 합니다.
    'SecretKey로 암호화 하기 전의 Body값'을 HashKey를 이용하여 SHA256으로 해싱합니다.
    해당 값을 헤더에 담아 전송합니다.
// HMAC 대상이 되는 Body
{
     "symbol": "ETH",
     "requestId": "user1-amount0.001-symbolETH-202302281830",
     "amount": "0.001",
     "senderAddress": "0x55Bc4ba7A86A00C8A1A09C59DcC32eA2E7FAbCd4",
     "receiverAddress": "0xee3c0f0AF227dA8DaccbF3Bc33bCf20f0a222aEF"
}
HEADER
Authorization: 'Bearer {apiToken}'
Octet-Access-Key: '{Access Key}'
Octet-Hmac: '5EJZMSFuZrtTle+O5FzuPvHZFnFWRFe5WZJ/VKWfaBY=' <-- HMAC-SHA256 
BODY
{
	"data": "Owl9/MDqndW0jNpz2waUSO3Ed50ZGwTNEAjXTkhlLYVPi9e05cMzKeugYFKywPDNx/FCeUQOKzHeyHKKsKFLVC+7Eo5mrefC30t8E4tfjDMY+PRQJPLl4LsQICCiTDUr6n/s/y/6hchNlea1beobmSHY9IFDc39aQJmr1T3gYh0fhQOTHCv9GvyAmRwv18L7wOLvTT/jYySLq2uYa24hR4IcCtUe4yYLUmPMYqPi4Rh4aXOkooOWKJP0ARdtk09WQwKNAh1NbHpWkIa3CRpHUg=="
}




예제 코드

고객사에서 수행해야 하는 암호화의 언어 별 예제 코드입니다.

Node.js

import { createHash, randomBytes, createCipheriv, createDecipheriv, generateKey, generateKeySync, createHmac } from 'crypto';

hashMAC(data: Record<string, any>, hashKey: string): string {
	const message = JSON.stringify(data);
	return createHmac('sha256', hashKey).update(message).digest('base64');
}

encryptMessage(data: Record<string, any>, secretKey: string): string {
	const AES_ALGORITHM = 'aes-256-cbc';
	const AES_IV_SIZE = 16;
	const AES_OUTPUT_ENCODING = 'base64';

	const message = JSON.stringify(data);
	const keyHash = createHash('sha256').update(secretKey);
 	const iv = randomBytes(AES_IV_SIZE);
 	const cipher = createCipheriv(AES_ALGORITHM, keyHash.digest(), iv);
 	const cipherText = cipher.update(Buffer.from(message));
 	const cipherFinal = cipher.final();
 	const encrypted = Buffer.concat([iv, cipherText, cipherFinal]);

 	return encrypted.toString(AES_OUTPUT_ENCODING);
}

Java

// gson
class JsonUtil {
    public static String stringify(Object obj) {
        Gson gson = new Gson();
        return gson.toJson(obj);
    }
}

// jackson
class JsonUtil {
    public static String stringify(Object obj) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        return objectMapper.writeValueAsString(obj);
    }
}

class AesEncryptionUtil {
    private static final String AES_ALGORITHM = "AES/CBC/PKCS5Padding";
    private static final int AES_IV_SIZE = 16;

    public static String aesEncrypt(Object data, String password) throws Exception {
        String message = JsonUtil.stringify(data);
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        byte[] keyBytes = md.digest(password.getBytes(StandardCharsets.UTF_8));
        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "AES");
        byte[] iv = new byte[AES_IV_SIZE];
        SecureRandom random = new SecureRandom();
        random.nextBytes(iv);
        IvParameterSpec ivParams = new IvParameterSpec(iv);
        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParams);
        byte[] cipherText = cipher.update(message.getBytes(StandardCharsets.UTF_8));
        byte[] cipherFinal = cipher.doFinal();
        byte[] encryptedBytes = new byte[iv.length + cipherText.length + cipherFinal.length];
        System.arraycopy(iv, 0, encryptedBytes, 0, iv.length);
        System.arraycopy(cipherText, 0, encryptedBytes, iv.length, cipherText.length);
        System.arraycopy(cipherFinal, 0, encryptedBytes, iv.length + cipherText.length, cipherFinal.length);
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }
}

class HmacUtil {
    public static String createHmac(Object data, String hashKey) throws Exception {
        String message = JsonUtil.stringify(data);
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec keySpec = new SecretKeySpec(hashKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
        mac.init(keySpec);
        byte[] hmacBytes = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(hmacBytes);
    }
}


// lombok
@Builder
@Getter
class Withdrawal {
    private String symbol;
    private String requestId;
    private String amount;
    private String senderAddress;
    private String receiverAddress;
}

그 외 언어 사용 시

위 언어 외 다른 언어로 암호화를 진행할 경우, 아래 값을 이용하여 테스트할 수 있습니다.

1. 테스트 데이터

secretKey : 5ba425e8473f74e246f393f1950f0509772c35d2cfc0c3dae8fdbe5db33daa51
hashKey   : 218471b0f4b1e4f8a01a8bd783462ef7a988569ecb1518263b129a10a910945d
iv        : HEXLANTOCTETV2.0
{ 
  "symbol": "ETH", 
  "requestId": "1ETH_20230511_1738_USER_000", 
  "amount": "1", 
  "senderAddress": "0x0CDAf02E737bF187c40bdd44f6f5230a4007A4d6", 
  "receiverAddress": "0x1317e2E91eb1f3161fad47D5a70A3C52E8E84f4D",
  "userKey": "rawUserKeyrawUserKeyrawUserKeyrawUserKeyrawUserKeyrawUserKeyrawUserKeyrawUserKeyrawUserKeyrawUserKeyrawUserKeyrawUserKeyrawUserKeyrawUserKey"
}

2. 검증

위 객체를 암호화 하였을 때 다음과 같은 값으로 출력되어야 합니다.

HMAC           : KQTd+eynbbyeDA1Hc+N75taYqCNc5Ln04HlXUOvg7qg=
Encrypted Body : SEVYTEFOVE9DVEVUVjIuMH4ftbMr9z+fYILoCWSOnUeRwPb2E8orqtKDEM3eSZ7WxrYIUH76Yp0FkA5i9sdBTUj48mdtlxQ1Hc2oPpQkAf5SZql3rdnaT5B4fC1csnkSopCg3cqFbknlVOThpUpF+d7Lrb708IEkWmmyOADAn67GSO9XP7lKkHBdzi4ueSAPg8JovNoVq27tjcINLhNMln+HS+gQp0t/HgfP5AC8sxgwMxuNoJ2i7qU3BFt8pPov8nBpY/4989kY1bE1r31GeEkHr30iiG5S3HsRoZRXeEMetVt7/4Vwk/FmoIBbO4tujIabsunNo5CRxMpoAHYFoGtGI+AqG2HdoZL70csNDdMAen0jjBaF4Q/W+PMgrPimmUjYTxpVDgVrKXFa1H5PeK1lncpE0CUnRA7v6kXptMyNVyaAR4xFYELRjSHt3aSFy4Do3Q8rERmEhfeAOJdIpD7iOC5wx3hr/XNEfn0mctw=

위 객체를 직렬화 하여 문자열로 출력했을 때 다음과 같은 값으로 출력되어야 합니다.

{"symbol":"ETH","requestId":"1ETH_20230511_1738_USER_000","amount":"1","senderAddress":"0x0CDAf02E737bF187c40bdd44f6f5230a4007A4d6","receiverAddress":"0x1317e2E91eb1f3161fad47D5a70A3C52E8E84f4D","userKey":"rawUserKeyrawUserKeyrawUserKeyrawUserKeyrawUserKeyrawUserKeyrawUserKeyrawUserKeyrawUserKeyrawUserKeyrawUserKeyrawUserKeyrawUserKeyrawUserKey"}