Help us understand the problem. What is going on with this article?

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: {} } }
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした