Edited at

NEM のコールドウォレットの自作は難しいのか検証してみた

More than 1 year has passed since last update.


まえがき

Coincheck(コインチェック) の XEM盗難騒動で 技術的に難しかった というお話があったようだ。

今回はコールドウォレットを自作してみて、本当に難しいのかを検証してみた。


設計

ネットワークにつながっていない端末で PrivateKey を保有する(または入力する)

上記端末で署名済みのトランザクションを生成する

署名済みのトランザクションを ネットワークにつながっている端末でNEMネットワークに送信する

こうすることで PrivateKey がネットワーク上に露出することがなくなる

ここで検討しないといけないのが 署名済みのトランザクション をどのようにネットワークにつながっている端末 に受け渡すか

最も単純な方法として、 QRコード に変換して画像として渡してしまえば、 カメラが有る端末で容易に送金できる

まとめるとこんな感じ



  • ネットワークにつながっていない端末


    1. オンラインウォレット宛の 署名済みトランザクション を生成する


    2. 署名済みトランザクション を QRコードへ変換




  • スマートフォン等


    1. QRコードを読み込み、署名済みトランザクションをNEMネットワークに送信する




実装

実装は nodejs + nem-sdk を利用するのが良いようだ


環境

nodejs: v8.9.1


dependencies-package

"nem-sdk": "^1.6.2",

"qrcode": "^1.2.0",
"readline-sync": "^1.4.7"


自作コールドウォレットのコード


transaction.js

const readlineSync = require('readline-sync');

const nem = require("nem-sdk").default;
const QRCode = require('qrcode');

class NemOfflineTransaction {
constructor(networkId) {
this.networkId = networkId;
}

start() {
// [1] 入力
this.keyIn();
// [2] 確認
this.confirm();
// [3] 署名付きのトランザクション生成
const tx = this.generateTransaction();
// [4] QRコードへ変換
this.generateQRCode(tx);
}

keyIn() {
this.recipient = readlineSync.question('Recipient: ');
this.amount = readlineSync.question('Amount: ');
// 便宜上 標準入力で privateKey を渡す
this.privateKey = readlineSync.question('PrivateKey: ', { hideEchoBack: true });
this.keyPair = nem.crypto.keyPair.create(this.privateKey);
this.address = nem.model.address.toAddress(this.keyPair.publicKey.toString(), this.networkId);
}

confirm() {
console.log(`\n---------------------------------------------------------------`);
console.log(`[1] Your address: ${this.address}`);
console.log(`[2] Recipient address: ${this.recipient}`);
console.log(`[3] amount: ${this.amount} XEM`);
console.log(`---------------------------------------------------------------`);
if (!readlineSync.keyInYN('Are you sure?:')) {
process.exit(0);
}
}

generateTransaction() {
// [1] 未署名のトランザクション生成
const txEntity = this.generateUnsignedTransaction();
const serializeTx = nem.utils.serialization.serializeTransaction(txEntity);
// [2] トランザクションに署名
const signature = this.generateSignature(this.keyPair, serializeTx);
// [3] announce で送信できる形式に変換
return JSON.stringify({
'data': nem.utils.convert.ua2hex(serializeTx),
'signature': signature.toString()
});
}

// トランザクションへの署名
generateSignature(keyPair, unsignedTx) {
return keyPair.sign(unsignedTx);
}

// 未署名のTransactionの生成
generateUnsignedTransaction() {
const tx = nem.model.objects.create("transferTransaction")(this.recipient, this.amount, null);
const common = nem.model.objects.create("common")("", this.privateKey);
return nem.model.transactions.prepare("transferTransaction")(common, tx, this.networkId);
}

// QRコードをファイルとして書き出す
generateQRCode(tx) {
QRCode.toFile(`${this.address}_${this.recipient}_${this.amount}.png`, tx);
}
}

// 本番系でやるには `testnet` => `mainnet` に変換
const network = nem.model.network.data.testnet.id;
offlineTx = new NemOfflineTransaction(network);
offlineTx.start();



実行例

$ node transaction.js

Recipient: TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Amount: 1
PrivateKey: ****************************************************************

---------------------------------------------------------------
[1] Your address: TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
[2] Recipient address: TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
[3] amount: 1 XEM
---------------------------------------------------------------
Are you sure? [y/n]: y

これで実行したディレクトリ直下に以下のようなQRコードのファイルが出来上がる

TB2SBFRLW7TVFDRUYY5T6J7MQX6POSEV3AYCWCFB_TDWWYDGQNBKSAJBSHZX7QWVX7WNVAWWB7HGPWRB2_1.png

ちなみに中身は以下の形式

data が送金トランザクション本体(hex)

signature が 署名(hex)

{

"data":"010100000100...",
"signature":"c87c7443730d8..."
}


署名済みトランザクションの送信部分

const tx = "{ data: .... }"; // QRコードの中身

const nem = require("nem-sdk").default;
const endpoint = nem.model.objects.create("endpoint")(nem.model.nodes.defaultTestnet, nem.model.nodes.defaultPort);
nem.com.requests.transaction.announce(endpoint, tx).then((res) => {
console.log("Transaction announced!");
});

コード中に PrivateKey がない状態なのでオンラインの端末でも安全に利用できる


あとがき

nem-sdk の存在は知っていたが、使ったことはなかった

仮想通貨の基本的な知識はあった

nodejs はぼちぼち触れる

こんな状態で、プロトタイプの実装は4時間程度

Bitcoin-Core 等をいれて、同期して~みたいなことをしなくて良いので、

結構簡単にいじれるイメージとなった


補足


プライベートキーの作り方

const networkId = nem.model.network.data.testnet.id;

const nem = require("nem-sdk").default;
const rBytes = nem.crypto.nacl.randomBytes(32);
const privateKey = nem.utils.convert.ua2hex(rBytes);
const keyPair = nem.crypto.keyPair.create(privateKey);
const address = nem.model.address.toAddress(keyPair.publicKey.toString(), networkId);

console.log(`Address : ${address}`);
console.log(`PrivateKey: ${privateKey}`);
console.log(`PublicKey : ${keyPair.publicKey.toString()}`);


アドレスの残高確認

const nem = require("nem-sdk").default;

const address = 'NC3BI3DNMR2PGEOOMP2NKXQGSAKMS7GYRKVA5CSZ';
const endpoint = nem.model.objects.create("endpoint")(nem.model.nodes.defaultMainnet, nem.model.nodes.defaultPort);

nem.com.requests.account.data(endpoint, address).then((res) => {
console.log(res);
});


実行結果

{ meta:

{ cosignatories: [],
cosignatoryOf: [],
status: 'LOCKED',
remoteStatus: 'INACTIVE' },
account:
{ address: 'NC3BI3DNMR2PGEOOMP2NKXQGSAKMS7GYRKVA5CSZ',
harvestedBlocks: 0,
balance: 9211155333,
importance: 0.06940358968348674,
vestedBalance: 921764202,
publicKey: 'e40d7a1c74f173cde9a1d19369da59d53017e799143f969a4e788a6f972016af',
label: null,
multisigInfo: {} } }