기사 메일전송
자바(Java)로 미니 블록체인 개발해봐요(8) - SECTION 06. 파일 기반의 신원 인증 시스템 구현하기(2) - 필자는 안경잡이 개발자로 유명하며 현재 프리랜서로 활동중에 있다
  • 기사등록 2018-05-09 00:56:15
기사수정



이제 단순히 개인키와 공개키 정보를 파일 형태로 내보내기 하는 것뿐만 아니라 파일에서부터 키 데이터를 읽어와 개인키와 공개키를 추출하는 역할을 수행하는 함수 또한 개발해보자. 이를 위해 EC.java 클래스의 소스코드를 다음과 같이 수정한다.


package util;


import java.io.BufferedReader;

import java.io.FileNotFoundException;

import java.io.FileReader;

import java.io.IOException;

import java.security.Key;

import java.security.KeyFactory;

import java.security.KeyPair;

import java.security.KeyPairGenerator;

import java.security.NoSuchAlgorithmException;

import java.security.PrivateKey;

import java.security.PublicKey;

import java.security.SecureRandom;

import java.security.spec.ECGenParameterSpec;

import java.security.spec.InvalidKeySpecException;

import java.security.spec.PKCS8EncodedKeySpec;

import java.security.spec.X509EncodedKeySpec;


import org.bouncycastle.util.encoders.Base64;


public class EC {


  // 세부 알고리즘으로 sect163k1을 사용합니다.

  private final String ALGORITHM = "sect163k1";

  

  public void generate(String privateKeyName, String publicKeyName) throws Exception {

    // 바운시 캐슬의 타원 곡선 표준 알고리즘(ECDSA)을 사용합니다.

    KeyPairGenerator generator = KeyPairGenerator.getInstance("ECDSA", "BC");

    

    // 타원 곡선의 세부 알고리즘으로 sect163k1을 사용합니다.

    ECGenParameterSpec ecsp;

    ecsp = new ECGenParameterSpec(ALGORITHM);

    generator.initialize(ecsp, new SecureRandom());

    

    // 해당 알고리즘으로 랜덤의 키 한 쌍을 생성합니다.

    KeyPair keyPair = generator.generateKeyPair();

    System.out.println("타원곡선 암호키 한 쌍을 생성했습니다.");

    

    // 생성한 키 한 쌍에서 개인키와 공개키를 추출합니다.

    PrivateKey priv = keyPair.getPrivate();

    PublicKey pub = keyPair.getPublic();

    

    // 개인키와 공개키를 특정한 파일 이름으로 저장합니다.

    writePemFile(priv, "EC PRIVATE KEY", privateKeyName);

    writePemFile(pub, "EC PUBLIC KEY", publicKeyName);

  }

  

  // Pem 클래스를 이용해 생성된 암호키를 파일로 저장하는 함수입니다.

  private void writePemFile(Key key, String description, String filename)

      throws FileNotFoundException, IOException {

    Pem pemFile = new Pem(key, description);

    pemFile.write(filename);

    System.out.println(String.format("EC 암호키 %s을(를) %s 파일로 내보냈습니다.", description, filename));

  }

  

  // 문자열 형태의 인증서에서 개인키를 추출하는 함수입니다.

  public PrivateKey readPrivateKeyFromPemFile(String privateKeyName)

      throws FileNotFoundException, IOException, NoSuchAlgorithmException, 

      InvalidKeySpecException {

    String data = readString(privateKeyName);

    System.out.println("EC 개인키를 " + privateKeyName + "로부터 불러왔습니다.");

    System.out.print(data);


    // 불필요한 설명 구문을 제거합니다.

    data = data.replaceAll("-----BEGIN EC PRIVATE KEY-----", "");

    data = data.replaceAll("-----END EC PRIVATE KEY-----", "");


    // PEM 파일은 Base64로 인코딩 되어있으므로 디코딩해서 읽을 수 있도록 합니다.

    byte[] decoded = Base64.decode(data);

    PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decoded);

    KeyFactory factory = KeyFactory.getInstance("ECDSA");

    PrivateKey privateKey = factory.generatePrivate(spec);

    return privateKey;

  }


  // 문자열 형태의 인증서에서 공개키를 추출하는 함수입니다.

  public PublicKey readPublicKeyFromPemFile(String publicKeyName)

      throws FileNotFoundException, IOException, NoSuchAlgorithmException, 

      InvalidKeySpecException {

    String data = readString(publicKeyName);

    System.out.println("EC 개인키를 " + publicKeyName + "로부터 불러왔습니다.");

    System.out.print(data);


    // 불필요한 설명 구문을 제거합니다.

    data = data.replaceAll("-----BEGIN EC PUBLIC KEY-----", "");

    data = data.replaceAll("-----END EC PUBLIC KEY-----", "");


    // PEM 파일은 Base64로 인코딩 되어있으므로 디코딩해서 읽을 수 있도록 합니다.

    byte[] decoded = Base64.decode(data);

    X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded);

    KeyFactory factory = KeyFactory.getInstance("ECDSA");

    PublicKey publicKey = factory.generatePublic(spec);

    return publicKey;

  }


  // 특정한 파일에 작성되어 있는 문자열을 그대로 읽어오는 함수입니다.

  private String readString(String filename) throws FileNotFoundException, IOException {

    String pem = "";

    BufferedReader br = new BufferedReader(new FileReader(filename));

    String line;

    while ((line = br.readLine()) != null)

      pem += line + "\n";

    br.close();

    return pem;

  }  

}


  이제 BlockChainStart.java 클래스를 수정하여 실제로 특정한 개인키와 공개키를 생성해 파일 형태로 저장하고, 이후에 해당 파일을 읽어서 개인키와 공개키를 추출하도록 만들어보자.


package core;


import java.security.PrivateKey;

import java.security.PublicKey;

import java.security.Security;


import org.bouncycastle.jce.provider.BouncyCastleProvider;


import util.EC;


public class BlockChainStarter {

  public static void main(String[] args) throws Exception {

    // 바운시 캐슬의 암호화 라이브러리를 사용하도록 설정합니다.

    Security.addProvider(new BouncyCastleProvider());


    // 타원 곡선 객체를 생성해 개인키와 공개키를 각각 private.pem, public.pem으로 저장합니다.

    EC ec = new EC();

    ec.generate("private.pem", "public.pem");

    

    // 파일로 저장한 개인키와 공개키를 다시 프로그램으로 불러옵니다.

    PrivateKey privateKey = ec.readPrivateKeyFromPemFile("private.pem");

    PublicKey publicKey = ec.readPublicKeyFromPemFile("public.pem");

  }

}


  프로그램 실행 결과는 다음과 같으며 파일에서 불러온 키 데이터가 그대로 출력되는 것을 확인할 수 있다. 물론 실제 지갑 소프트웨어에서는 이렇게 키 데이터가 노출되어서는 안 된다. 개인키는 비밀번호(Password)와 같은 기능을 수행하므로 외부인이 알지 못하도록 보호해야 한다.



  마지막으로 키를 두 쌍 생성해 서명 및 검증하는 실습을 통해 ‘특정한 개인키로 암호화한 데이터는 그 개인키와 한 쌍이 되는 공개키를 이용했을 때만 복호화가 가능’하다는 것을 직접 확인해보자.


package core;


import java.math.BigInteger;

import java.security.PrivateKey;

import java.security.PublicKey;

import java.security.Security;

import java.security.Signature;


import org.bouncycastle.jce.provider.BouncyCastleProvider;


import util.EC;


public class BlockChainStarter {

  public static void main(String[] args) throws Exception {

    Security.addProvider(new BouncyCastleProvider());

    EC ec = new EC();

    

    // 총 두 쌍의 키를 생성해 파일 형태로 저장합니다.

    ec.generate("private1.pem", "public1.pem");

    ec.generate("private2.pem", "public2.pem");

    // 파일 형태로 저장한 키 데이터를 프로그램으로 불러옵니다.

    PrivateKey privateKey1 = ec.readPrivateKeyFromPemFile("private1.pem");

    PublicKey publicKey1 = ec.readPublicKeyFromPemFile("public1.pem");

    PrivateKey privateKey2 = ec.readPrivateKeyFromPemFile("private2.pem");

    PublicKey publicKey2 = ec.readPublicKeyFromPemFile("public2.pem");

    Signature ecdsa;

    ecdsa = Signature.getInstance("SHA1withECDSA");

    // 개인키 1을 이용해 암호화(서명)합니다.

    ecdsa.initSign(privateKey1);


    String text = "평문입니다.";

    System.out.println("평문 정보: " + text);

    byte[] baText = text.getBytes("UTF-8");


    // 평문 데이터를 암호화하여 서명한 데이터를 출력합니다.

    ecdsa.update(baText);

    byte[] baSignature = ecdsa.sign();

    System.out.println("서명된 값: 0x" + (new BigInteger(1, baSignature).toString(16)).toUpperCase());


    Signature signature;

    signature = Signature.getInstance("SHA1withECDSA");

    

    // 검증할 때는 공개키 2를 이용해 복호화를 수행합니다.

    signature.initVerify(publicKey2);

    signature.update(baText);

    boolean result = signature.verify(baSignature);

    

    // 개인키와 매칭되는 공개키가 아니므로 복호화에 실패합니다.

    System.out.println("신원 검증: " + result);

  }

}




  수정된 소스코드의 실행 결과는 위와 같다. 개인키 private1.pem은 공개키 public1.pem와 한 쌍이므로 public2.pem으로 private1.pem으로 암호화한 서명을 복호화하려고 했을 때 서명에 실패(False)하는 것을 알 수 있다. 반대로 public1.pem을 이용해 복호화하도록 소스코드를 수정하면 서명에 성공하는 것 또한 확인할 수 있을 것이다. 이제 독자들은 이렇게 작성된 소스코드를 자유롭게 수정해보며 자바(Java) 안에서 타원 곡선 암호화 기법이 적용되는 원리를 자세히 이해하도록 하자.


  우리는 이처럼 2주에 걸쳐서 자바(Java)에서 타원 곡선 암호화 기법을 사용하는 방법에 대해 공부하는 시간을 가졌다. 우리가 다루어 본 예제는 실무자들도 구현에 어려움을 겪는 난이도이지만, 실제 상용 소프트웨어에서 사용할 수 있는 수준으로 정석적으로 구현했으므로 여러 번 따라 입력해보면서 동작 원리에 대해 확실히 이해하는 것을 추천한다. 또한 핵심 원리로 개인키로 서명한 암호문이 있을 때 오직 그와 한 쌍이 되는 공개키로 해당 암호문을 검증할 수 있다는 점만 확실히 기억하자.


다음호에 계속...


-기사 속 코드 작성에 문제가 있으면 비트웹 대표메일로 메일을 주시면 필자의 원본 파일을 전달해드리겠습니다-


비트웹(bitweb.co.kr)

0
기사수정

다른 곳에 퍼가실 때는 아래 고유 링크 주소를 출처로 사용해주세요.

http://www.bitweb.co.kr/news/view.php?idx=921
기자프로필
프로필이미지
나도 한마디
※ 로그인 후 의견을 등록하시면, 자신의 의견을 관리하실 수 있습니다. 0/1000
실시간 암호화폐 순위 확인하기
코인마켓캡
모바일 버전 바로가기