본문 바로가기

Blockchain

BSC Testnet Web3j 스터디 (2) Private Key로 Public Key 추출

회사에서 온보딩으로 진행한 Web3j 학습 내용을 정리한 글입니다.

- 네트워크: BSC Testnet
- 체인 ID: 97

---

1. 지갑 주소 생성 (Private Key, Public Key)
1.1 Private Key로 Public Key 추출 

 

이더리움 지갑의 핵심은 개인키(Private Key) 하나에 모든 정보가 담겨 있다는 점이다. 개인키만 있으면 언제든 지갑 전체를 복원할 수 있다. 이미 가지고 있는 개인키를 사용해 공개키(Public Key)와 지갑 주소를 복원할 수 있다.

1. 단방향 암호화의 원리

이더리움 지갑은 개인키로부터 공개키와 주소가 파생되는 단방향 구조를 가진다.

개인키 → 공개키 → 주소

이 암호학적 관계 덕분에 개인키만 안전하게 보관하면 언제든 지갑 전체를 복원할 수 있다. 이러한 관계는 일방향으로, 개인키에서 주소를 계산하는 것은 가능하지만, 주소나 공개키를 보고 개인키를 알아내는 것은 불가능하다.

지난 포스팅에서 ECKeyPair ecKeyPair = Keys.createEcKeyPair(); 를 통해 개인키와 공개키 페어를 생성했는데, 사실 내부적으로는 다음과 같은 순서로 진행된다.

1. 암호학적으로 안전한 아주 큰 무작위 숫자를 하나 생성 -> 개인키(Private Key)

2. 생성된 개인키를 타원곡선 암호화(ECDSA)라는 수학적 공식을 사용해 계산 -> 공개키(Public Key)

이 과정은 항상 '개인키 생성 → 공개키 계산' 순서이다.

 

2. Private Key로 Public Key 추출하기

web3j에서는 개인키로 공개키를 추출하는 두 가지 주요 방법을 제공한다. 하나는 고수준 API인 Credentials 객체를 이용하는 방법이고, 다른 하나는 저수준 API인 Sign 클래스를 직접 사용하는 방법이다.

Credentials를 사용하면 훨씬 간편하고, 복잡한 과정을 알아서 처리해주기 때문에 실수할 확률이 적다. Sign 클래스는 암호화의 동작 원리를 깊게 학습하고 싶을 때 유용하다.

방법 1: Credentials 클래스 사용 (고수준 API)

가장 간단하고 일반적인 방법이다. Credentials는 이름 그대로 지갑의 인증 정보를 종합적으로 관리하는 객체이다.

  1. Credentials.create(privateKey)를 호출하여 개인키 문자열로 Credentials 객체를 생성한다.
  2. 내부적으로 ECKeyPair가 생성되며, credentials.getEcKeyPair()로 접근할 수 있다.
  3. ecKeyPair.getPublicKey()를 통해 공개키를 BigInteger 형태로 얻는다.

이 방법은 복잡한 내부 과정을 추상화하여 개발자가 편리하게 사용할 수 있도록 돕는다.

방법 2: Sign 클래스 직접 사용 (저수준 API)

타원곡선 암호화(ECDSA) 계산을 직접 수행하여 공개키를 도출하는 저수준 방식이다.

  1. 개인키 문자열을 Numeric.toBigInt()를 사용해 BigInteger 객체로 변환한다.
  2. Sign.publicKeyFromPrivate(privateKeyBigInt) 메서드를 호출한다.
  3. 이 메서드는 ECDSA 알고리즘에 따라 개인키에 해당하는 공개키를 직접 계산하여 반환한다.

암호화의 원리를 좀 더 직접적으로 다루고 싶을 때 유용하며, 두 방법의 결과는 당연히 동일해야 한다.

코드 예제

아래 코드는 개인키를 사용하여 위 두 가지 방법으로 공개키를 각각 추출하고, 그 결과가 미리 알고 있던 공개키와 일치하는지 검증하는 전체 예제이다.

package wallet;

import java.math.BigInteger;
import org.web3j.crypto.Credentials;
import org.web3j.crypto.Sign;
import org.web3j.utils.Numeric;

public class PublicKeyExtract {

    /**
     * 1.1 Private Key로 Public Key 추출
     * * 암호화 기본 개념:
     * - Private Key: 비밀키 (절대 공개 금지)
     * - Public Key: 공개키 (Private Key에서 수학적으로 도출)
     * - 주소: Public Key에서 Keccak-256 해시로 생성
     */

    public static void main(String[] args) {

        // 검증을 위해 사용할, 이미 알고 있는 공개키
        String expectedPublicKey = "0x376db746fb37456556a2c0018d876fbc5e61f9d60dec7f9f21132b2d7a19e71d5a7b81bcd29ded77b7b489f2e0e043998e385809f9779133938065190f351a91";
        System.out.println("기대하는 Public Key: " + expectedPublicKey);
        
        // 실제 운영에서는 환경 변수나 보안 저장소에서 개인키를 로드해야 합니다.
        // 테스트를 위해 여기서는 미리 생성해 둔 개인키를 사용합니다.
        String privateKey = "0x5a1b..."; // 실제 개인키를 입력하세요.
        System.out.println("사용할 Private Key: " + privateKey);
        System.out.println();


        // 방법 1. Credentials 클래스 사용 (고수준 API)
        System.out.println("=== 방법 1. Credentials에서 추출 ===");

        // Private Key로 Credentials 객체 생성
        Credentials credentials = Credentials.create(privateKey);

        // EC 키페어에서 Public Key 추출
        BigInteger publicKeyBigInt = credentials.getEcKeyPair().getPublicKey();

        // BigInteger를 0x 접두사가 있는 16진수 문자열로 변환
        String extractedPublicKey = Numeric.toHexStringWithPrefix(publicKeyBigInt);

        System.out.println("추출된 Public Key: " + extractedPublicKey);

        if(extractedPublicKey.equals(expectedPublicKey)){
            System.out.println("방법 1 성공: Public Key 일치");
        } else {
            System.out.println("방법 1 실패: Public Key 불일치");
        }

        // ---------------------------------------------------------
        System.out.println();

        // 방법 2. Sign 클래스 직접 사용 (저수준 API)
        System.out.println("\n=== 방법 2. Sign.publicKeyFromPrivate 메서드 ===");

        // 16진수 문자열을 BigInteger로 변환
        BigInteger privateKeyBigInt = Numeric.toBigInt(privateKey);

        // ECDSA 알고리즘으로 Public Key 직접 계산
        BigInteger publicKeyBigInt2 = Sign.publicKeyFromPrivate(privateKeyBigInt);

        String extractedPublicKey2 = Numeric.toHexStringWithPrefix(publicKeyBigInt2);

        System.out.println("추출된 Public Key: " + extractedPublicKey2);

        if(extractedPublicKey.equals(extractedPublicKey2)){
            System.out.println("방법 2 성공: 두 방법 결과 일치");
        } else {
            System.out.println("방법 2 실패: 결과 불일치");
        }
        
        System.out.println("\n=== 최종 검증 ===");
        if(extractedPublicKey.equals(expectedPublicKey) && extractedPublicKey2.equals(expectedPublicKey)) {
            System.out.println("모든 방법으로 Public Key 추출 성공");
        }
    }
}

실행 결과 및 정리

위 코드를 실행하면 Credentials를 사용한 방법과 Sign 클래스를 사용한 방법 모두 동일한 공개키를 출력하며, 이 공개키는 우리가 기대했던 값과 정확히 일치하는 것을 확인할 수 있다.

실생활에서는 메타마스크 같은 지갑 앱에서 '계정 가져오기' 기능을 사용할 때 바로 이 원리가 적용된다. 사용자가 개인키를 입력하면, 지갑 앱은 이 코드가 하는 것과 똑같은 과정을 거쳐 공개키와 주소를 계산해내고, 해당 지갑의 자산을 화면에 보여주고 관리할 수 있게 해준다.

 

핵심 정리

  1. 지갑의 소유권과 모든 정보는 개인키 하나에서 비롯된다. 개인키만 있으면 핸드폰을 잃어버리거나 컴퓨터가 고장나도 언제든 내 자산을 복원할 수 있다.
  2. 개인키에서 주소로 가는 흐름은 일방통행이다. 덕분에 우리는 주소를 인터넷에 자유롭게 공개하면서도 안전하게 자산을 주고받을 수 있다.
  3. web3j는 복잡한 타원곡선 암호화 계산을 Credentials나 Sign 같은 클래스로 감싸주어 개발자가 비즈니스 로직에 더 집중할 수 있게 해준다.