니모닉 지갑 만들기

JooYong Lee·2022년 4월 6일
0

블록체인

목록 보기
8/9

이전에 니모닉 지갑 만드는 방법을 배우며 실습해본적이 있긴하지만 그저 따라하면서 '오~ 된다~'하는 것에서 그쳤기 때문에... 이번에는 공식문서도 한번 읽어보면서 다시 한 번 정리해볼 생각이다.

일단 니모닉 지갑을 만들려면 니모닉이 뭔지 알아야한다.

니모닉(Mnemonic)이란

복잡한 개인 키를 쉽게 입력할 수 있도록 12개 또는 24개의 랜덤한 영어 단어로 지갑 문구를 생성해 암호화폐 지갑 사용자가 난해한 기술에 대한 이해 없이도 지갑을 편리하게 사용할 수 있도록 돕는다.

개인 키를 대신하는 것이기 때문에 지갑을 복구할 때에도 쓰이고, 개인 키와 마찬가지로 유출될 경우 지갑 내 암호화폐를 모두 잃을 수 있다. 따라서 암호화해서 저장해두거나, 종이 등에 적어 금고 등으로 물리적으로 보관하는 것이 안전하다.

니모닉 단어들을 순서대로 맞추는 것으로 암호화된 보안 비밀번호 방식인데 이 니모닉의 문자 배열을 순서대로 맞추지 못하면 어느 누구도 지갑을 열 수 없다. 처음 내 지갑 만들기를 실행했을 때 지갑에서 영단어 그룹을 사용해 고유의 지갑 문구를 생성하게 되고, 이 문구가 내 지갑의 니모닉이 된다.

BIP-39 형식으로 정리된 니모닉 코드는 임의의 값을 사전에 정의된 일상적 단어 리스트에 매핑한다. 니모닉 코드는 해시 함수를 재귀적으로 반복하는 키 스트레칭 과정을 거쳐 마스터 시드를 생성하고 그 마스터 시드는 HD지갑 동작의 바탕이 된다.

니모닉 코드 단어

결정적 지갑을 파생하기 위한 시드로 사용되는 난수를 인코딩한 단어 시퀀스이다. 단어 시퀀스를 통해 시드를 다시 만들 수 있고 그것으로 지갑과 모든 파생된 키들을 다시 만들 수 있다.

니모닉 단어 생성

  1. 암호학적으로 랜덤한 128~256 bits의 시퀀스 S를 만든다.
  2. S의 SHA-256 해시값 중에서 앞에서 S의 길이 / 32비트 만큼을 체크섬으로 만든다.
  3. 2번에서 만든 체크섬을 S의 끝에 추가한다.
  4. 3번에서 만든 시퀀스와 체크섬의 연결을 11 bits 단위로 자른다.
  5. 각 11비트를 2048(2^11)개의 미리 정의된 단어로 치환한다.
  6. 단어 시퀀스로부터 순서를 유지하면서 니모닉 코드를 생성한다.

시드 생성

니모닉 단어는 128~256비트 길이의 엔트로피를 표현한다. 엔트로피는 키 스트레칭 함수 PBKDF2를 통해서 512비트의 시드를 만드는데 사용되며 생성된 시드는 결정론적 지갑을 구축하고 키를 파생하는데 사용된다.

키 스트레칭 함수는 니모닉과 솔트라는 두 개의 파라미터를 사용한다. 솔트의 목적은 무차별 대입 공격을 가능하게 하는 조회 테이블 생성을 힘들게 하는 것이다. PBKDF2 키 스트레칭 함수의 첫 번째 파라미터는 니모닉이고, 두 번째 파라미터는 솔트이다. 솔트는 니모닉에 사용자가 지정한 암호문을 붙인 것이다. PBKDF2는 니모닉과 솔트를 HMAC-SHA512로 2048번 해싱해서 512 bits 값을 만들어내는데, 이 값이 시드이다.

니모닉 지갑 만들기

먼저 니모닉을 생성해주는 API를 만들고, 이를 통해 생성된 니모닉 코드와 패스워드를 통해 지갑을 만드는 API를 만든다.

새 니모닉 생성 API 만들기

router.post('/newMnemonic', async(req, res) => {
  let mnemonic;
  try {
    mnemonic = lightwallet.keystore.generateRandomSeed();
    res.json({mnemonic});
  } catch(err) {
    console.log(err);
  }
});

eth-lightwallet 모듈을 사용하여 개발한다.
위에서 사용된 keystore.generateRandomSeed() 함수는 랜덤한 12개의 단어 시드로 구성된 문자열을 생성하고 반환해준다.

니모닉 코드와 패스워드를 이용한 지갑 생성 API 만들기

router.post('/newWallet', async(req, res) => {
  let password = req.body.password;
  let mnemonic = req.body.mnemonic;
  
  try {
    lightwallet.keystore.createVault(
      {
        password: password,
        seedPhrase: mnemonic,
        hdPathString: "m/0'/0'/0'"
      },
      (err, ks) => {
        ks.keyFromPassword(password, (err, pwDerivedKey) => {
          ks.generateNewAddress(pwDerivedKey, 1);
          
          let address = ks.getAddresses().toString();
          let keystore = ks.serialize();
          
          res.json({keystore: keystore, address: address});
        });
      }
    );
  } catch (exception) {
    console.log("NewWallet => " + exception);
  }
});       

먼저 keystore.createVault() 함수를 사용해 키스토어를 생성한다.
첫 번째 인자에는 password, seedPhrase, hdPathString, salt 를 포함한 객체가 들어간다.

  • password는 serialize(직렬화)할 때 vault를 암호화할 때 사용하는 문자열이다.
  • seedPhrase는 계정 생성에 사용되는 12단어 니모닉이다.
  • hdPathString에는 사용자가 BIP39 호환 HD 경로 문자열을 제공해야한다. 기본값은 m/0'/0'/0'이다.
  • salt는 위 3가지와는 다르게 선택 사항이다. vault를 암호화 및 해독하는데 사용되는데, 입력하지 않으면 임의의 솔트가 생성된다.

두 번째 인자에는 키스토어를 인자로 사용하는 콜백 함수가 들어간다.

이쯤에서 키스토어가 뭔지 한 번 찾아보는게 좋겠다.

키스토어는 암호화폐 지갑을 사용하기 위한 프라이빗 키를 비밀번호로 암호화한 텍스트 또는 파일이다. 텍스트나 파일을 보관하고 있다가 다른 지갑에 불러올 때 이를 입력한 후 알맞은 비밀번호를 입력하면 복호화되어 프라이빗 키를 도출할 수 있게 되고 한 번 암호화된 파일이므로 키스토어 파일은 컴퓨터나 메모장 등에 보관하기 안전하다. 단, 비밀번호가 너무 쉬우면 무차별대입 공격으로 쉽게 뚫릴 수 있다.

다시 이어서, 콜백 함수에서는 또 keystore.keyFromPassword() 함수를 사용하고 있다. 이 함수는 내부적으로 구성된 솔트를 사용해 적절한 pwDerivedKey를 반환한다고 한다. 사용자가 입력한 password를 입력으로 사용하고 키스토어를 암호화/복호화하는데 사용되는 Uint8Array 타입의 대칭 키를 생성한다.

두 번째 인자에는 pwDerivedKey를 인자로 사용하는 콜백 함수를 만든다. 이 콜백 함수에서는 keystore.generateNewAddress()를 이용해 새로운 주소 생성 함수를 실행한다. 인자로 pwDerivedKey를 받으며 address/private key 쌍을 생성한다. 두 번째 인자로 숫자를 넣으면 해당 숫자만큼의 쌍을 생성한다. 기본 값은 1이다.

keystore.generateNewAddress()를 실행하고나면 keystore.getAddresses() 호출로 생성된 주소/개인 키 쌍을 불러올 수 있다.
address 변수에는 getAddresses()로 불러온 값을 문자열로 바꾸어 저장하고, keystore 변수에는 현재 키스토어 객체를 JSON 인코딩 문자열로 직렬화해 저장한다.

생성된 키스토어를 json 파일로 만들어 로컬 서버에 저장하기

위 코드에서 keystore와 address를 담아 보내는 응답 대신에 아래 코드를 작성한다.

// res.json({keystore: keystore, address: address});
// 대신에

fs.writeFile('wallet.json', keystore, (err, data) => {
  if (err) {
    res.json({code: 999, message: "실패"});
  } else {
    res.json({code: 1, message: "성공"});
  }
});

wallet.json이라는 이름의 파일로 저장하고 응답으로는 성공/실패 여부만 돌려준다.

postman으로 결과 확인

일단 서버를 실행시켜준다.

postman을 실행하고 엔드포인트를 http://localhost:3000/wallet/newMnemonic 로 입력해 POST 요청을 보낸다. 응답으로 니모닉이 생성되어 돌아오는 것을 볼 수 있다.

생성된 니모닉을 복사해 이번에는 http://localhost:3000/wallet/newWallet 로 POST 요청을 보낼 것이다. 니모닉과 패스워드를 body에 입력하고 요청을 보내면 성공 메시지가 응답으로 온다.

또 동시에 wallet.json이 로컬에 생성되는 것을 볼 수 있다.

https://github.com/be-kid/MnemonicWallet

마치면서..

확실히 공식문서를 보면서 함수들 하나하나 역할을 제대로 확인하며 코드를 살펴보니 내용과 흐름이 잘 이해되었다..
영어로 된 문서를 두려워해서 늘 한글로 설명해준 블로그 같은 곳들을 찾아보거나 얼렁뚱땅 넘어가는 경우도 있었는데, 시간을 들여 차근차근 살펴보면 나에게 훨씬 도움이 많이 될 것 같다..

profile
21.11.01~ 기록

0개의 댓글