이전에 니모닉 지갑 만드는 방법을 배우며 실습해본적이 있긴하지만 그저 따라하면서 '오~ 된다~'하는 것에서 그쳤기 때문에... 이번에는 공식문서도 한번 읽어보면서 다시 한 번 정리해볼 생각이다.
일단 니모닉 지갑을 만들려면 니모닉이 뭔지 알아야한다.
복잡한 개인 키를 쉽게 입력할 수 있도록 12개 또는 24개의 랜덤한 영어 단어로 지갑 문구를 생성해 암호화폐 지갑 사용자가 난해한 기술에 대한 이해 없이도 지갑을 편리하게 사용할 수 있도록 돕는다.
개인 키를 대신하는 것이기 때문에 지갑을 복구할 때에도 쓰이고, 개인 키와 마찬가지로 유출될 경우 지갑 내 암호화폐를 모두 잃을 수 있다. 따라서 암호화해서 저장해두거나, 종이 등에 적어 금고 등으로 물리적으로 보관하는 것이 안전하다.
니모닉 단어들을 순서대로 맞추는 것으로 암호화된 보안 비밀번호 방식인데 이 니모닉의 문자 배열을 순서대로 맞추지 못하면 어느 누구도 지갑을 열 수 없다. 처음 내 지갑 만들기를 실행했을 때 지갑에서 영단어 그룹을 사용해 고유의 지갑 문구를 생성하게 되고, 이 문구가 내 지갑의 니모닉이 된다.
BIP-39 형식으로 정리된 니모닉 코드는 임의의 값을 사전에 정의된 일상적 단어 리스트에 매핑한다. 니모닉 코드는 해시 함수를 재귀적으로 반복하는 키 스트레칭 과정을 거쳐 마스터 시드를 생성하고 그 마스터 시드는 HD지갑 동작의 바탕이 된다.
결정적 지갑을 파생하기 위한 시드로 사용되는 난수를 인코딩한 단어 시퀀스이다. 단어 시퀀스를 통해 시드를 다시 만들 수 있고 그것으로 지갑과 모든 파생된 키들을 다시 만들 수 있다.
니모닉 단어는 128~256비트 길이의 엔트로피를 표현한다. 엔트로피는 키 스트레칭 함수 PBKDF2를 통해서 512비트의 시드를 만드는데 사용되며 생성된 시드는 결정론적 지갑을 구축하고 키를 파생하는데 사용된다.
키 스트레칭 함수는 니모닉과 솔트라는 두 개의 파라미터를 사용한다. 솔트의 목적은 무차별 대입 공격을 가능하게 하는 조회 테이블 생성을 힘들게 하는 것이다. PBKDF2 키 스트레칭 함수의 첫 번째 파라미터는 니모닉이고, 두 번째 파라미터는 솔트이다. 솔트는 니모닉에 사용자가 지정한 암호문을 붙인 것이다. PBKDF2는 니모닉과 솔트를 HMAC-SHA512로 2048번 해싱해서 512 bits 값을 만들어내는데, 이 값이 시드이다.
먼저 니모닉을 생성해주는 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개의 단어 시드로 구성된 문자열을 생성하고 반환해준다.
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 를 포함한 객체가 들어간다.
두 번째 인자에는 키스토어를 인자로 사용하는 콜백 함수가 들어간다.
이쯤에서 키스토어가 뭔지 한 번 찾아보는게 좋겠다.
키스토어는 암호화폐 지갑을 사용하기 위한 프라이빗 키를 비밀번호로 암호화한 텍스트 또는 파일이다. 텍스트나 파일을 보관하고 있다가 다른 지갑에 불러올 때 이를 입력한 후 알맞은 비밀번호를 입력하면 복호화되어 프라이빗 키를 도출할 수 있게 되고 한 번 암호화된 파일이므로 키스토어 파일은 컴퓨터나 메모장 등에 보관하기 안전하다. 단, 비밀번호가 너무 쉬우면 무차별대입 공격으로 쉽게 뚫릴 수 있다.
다시 이어서, 콜백 함수에서는 또 keystore.keyFromPassword() 함수를 사용하고 있다. 이 함수는 내부적으로 구성된 솔트를 사용해 적절한 pwDerivedKey를 반환한다고 한다. 사용자가 입력한 password를 입력으로 사용하고 키스토어를 암호화/복호화하는데 사용되는 Uint8Array 타입의 대칭 키를 생성한다.
두 번째 인자에는 pwDerivedKey를 인자로 사용하는 콜백 함수를 만든다. 이 콜백 함수에서는 keystore.generateNewAddress()를 이용해 새로운 주소 생성 함수를 실행한다. 인자로 pwDerivedKey를 받으며 address/private key 쌍을 생성한다. 두 번째 인자로 숫자를 넣으면 해당 숫자만큼의 쌍을 생성한다. 기본 값은 1이다.
keystore.generateNewAddress()를 실행하고나면 keystore.getAddresses() 호출로 생성된 주소/개인 키 쌍을 불러올 수 있다.
address 변수에는 getAddresses()로 불러온 값을 문자열로 바꾸어 저장하고, keystore 변수에는 현재 키스토어 객체를 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을 실행하고 엔드포인트를 http://localhost:3000/wallet/newMnemonic 로 입력해 POST 요청을 보낸다. 응답으로 니모닉이 생성되어 돌아오는 것을 볼 수 있다.
생성된 니모닉을 복사해 이번에는 http://localhost:3000/wallet/newWallet 로 POST 요청을 보낼 것이다. 니모닉과 패스워드를 body에 입력하고 요청을 보내면 성공 메시지가 응답으로 온다.
또 동시에 wallet.json이 로컬에 생성되는 것을 볼 수 있다.
https://github.com/be-kid/MnemonicWallet
확실히 공식문서를 보면서 함수들 하나하나 역할을 제대로 확인하며 코드를 살펴보니 내용과 흐름이 잘 이해되었다..
영어로 된 문서를 두려워해서 늘 한글로 설명해준 블로그 같은 곳들을 찾아보거나 얼렁뚱땅 넘어가는 경우도 있었는데, 시간을 들여 차근차근 살펴보면 나에게 훨씬 도움이 많이 될 것 같다..