15
3

More than 5 years have passed since last update.

コンソーシアムのPoCにおけるウォレット管理とTXサイン実装例

Last updated at Posted at 2018-12-04

こんにちは。SKILL Inc. CTOの田坂です。

コンソーシアムのPoCにおけるウォレット管理とTXサイン実装例という表題に関して溜まった知見を一部アウトプットしたいと思います

注意 この記事はSKILL Inc.としての記事であり、紐づいているOrganizationは関連ありません。

1. Dappを作りPoCをする上で気になること

  • UX
  • Gas代
  • ウォレット管理
  • 基本的なセキュリティ
  • 仮説検証を成り立たせるためのサイズ感(これは今度書く)

Dappを作るとき外部ウォレットを使うと現状ブラウザ拡張を入れたり、専用ブラウザを使ったりとUX的ハードルがあることは周知の事実だと思います。
今後技術の発展でウォレットやブロックチェーンを支える技術はより使いやすいものになって行く思いますが、今のタイミングでUXの障壁にならずDappと必要最低限なサービス仮説検証をガンガン行って行きたいと思っており。微力ながら知見を共有できればと思い書いていきます。

2. 全体像

68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f32393033372f35356362396266612d613435322d616330392d373765612d3837636363663161336365312e706e67.png

Ethereum環境はAzure Ethereum PoAを利用しています。
具体的な技術選択に関する記事は別途書こうと思っていますので、ここではあまり触れません。
サマるとPoCでスモールにやるならPoA使ってGas代ゼロにした方が色々面倒なことを気にせず仮説検証ポイント絞れていいですよって話です。もちろんLayer1,2周りの技術キャッチアップは並行して行っています。一つのPoCで多くのことを検証しようとすると破綻するので、切り分けが大事だと思っています。

追記
ネットワークアクセスをIP制限しきながら、Ethノードへの接続もVPNでアクセス制御することで
よりセキュアに実証実験ができます。

3. ウォレット

僕たちのPoCではリアルマネーは現状BC上で扱わず、基本的なWebサービスにBCのバリューを加える形になっています。
なので既存ウォレットは使える必要がない前提の話で進めていきます。

ウォレットはサーバで生成管理せず、クライアントで保管しています。
また、別のブラウザなどでも同じウォレットを使えるように、サーバに暗号化したウォレットを保管しています
ウォレットの暗号化は1Passwordのセキュリティ実装を参考に、秘密鍵とマスターキーのセットで二回暗号化しています。

参考
1Password Security Design

3.1. ウォレット生成し、暗号化したものをサーバに保管
3.2. ユーザの暗号化されたウォレットがサーバにあればDLして復号化

下の説明でBCで使う秘密鍵とそれを暗号化する秘密鍵二つあり、混乱するので
BCで使う秘密鍵を ウォレット秘密鍵 、暗号化のキーをウォレット暗号化秘密鍵と表現しています。

3.1. ウォレット生成し、暗号化したものをサーバに保管

ウォレット暗号化秘密鍵
 複雑な文字列。ブラウザにキャッシュしておく。
 キャッシュにない場合はユーザに入力してもらう。
  → 基本新しいブラウザを使い始める時以外入力しない。
 例 SK-AAAA-AAAA-AAAA-AAAA-AAAA-AAAA
  prefixは正しいものをわかりやすくするため

マスタキーサンプル
 上と比較すると簡単な文字列
 ブラウザでキャッシュ。セッション切れるとユーザに再入力してもらう。
 例 password1234&&

手順

  1. ethereumjs-wallet でウォレット生成
  2. ウォレット暗号化秘密鍵マスターキーウォレット秘密鍵を暗号化
  3. サーバに暗号化されたウォレット秘密鍵を保管
  4. ブラウザのキャッシュに各種キーを保管

実装例

  • ウォレット暗号化秘密鍵 walletPrivateKey
  • マスターキー password
const Wallet = require("ethereumjs-wallet")
const CryptoJS = require("crypto-js")

const prefix = "SK"

let password = encodeURI(passwordString)

let wallet = Wallet.generate()
let address = wallet.getAddressString()
let privateKey = wallet.getPrivateKeyString()
let walletPrivateKey =
  prefix +
  "-" +
  code() +
  "-" +
  code() +
  "-" +
  code() +
  "-" +
  code() +
  "-" +
  code() +
  "-" +
  code()
var ciperPrivateKey1 = CryptoJS.AES.encrypt(
  privateKey,
  password
).toString()
var ciperPrivateKey2 = CryptoJS.AES.encrypt(
  ciperPrivateKey1,
  walletPrivateKey
).toString()


3.2 ユーザの暗号化されたウォレットがサーバにあればDLして復号化

手順

  1. ウォレット暗号化秘密鍵で復号
  2. 1の復号データをマスターキーで復号 ← web3で使えるウォレット秘密鍵になる
let masterKey = encodeURI('****')

var privateKey = null
var address = null

let response = await this.$axios.get("/v1/wallets/mine", {})
if(response.data != null) {
  address = response.data.wallet.address
  let encodedPrivateKey = response.data.wallet.encoded_private_key
  let a1 = CryptoJS.AES.decrypt(encodedPrivateKey, masterKey).toString(
    CryptoJS.enc.Utf8
  )
  privateKey = CryptoJS.AES.decrypt(a1, password).toString(
    CryptoJS.enc.Utf8
  )
}

4. 別ブラウザでの利用方法

この方法は議論中で使いやす形がまだ定まっていませんが
1Passwordのエマージェンシーキットが運用実績があり、参考事例としてあげさせて頂きます。

1Passwordではアカウントを作成するとき、マスターパスワードを任意で入力が求められます。
同時に秘密鍵が生成され、秘密鍵とマスターキー以外のログイン情報がPDFで生成され、ユーザが保管します。

ユーザは別環境で利用するときそのエマージェンシーキットを元に、自分が保管した情報を復号化する仕組みになっています。

詳しくは以下のページなどわかりやすいと思います

5. TXサイン

TXはクライアントでサインするが、RPC endpoint直接叩かない。

多分言葉で書くよりコードの方がわかりやすいと思うので実装ほぼそのまま掲載

5.1 トランザクション呼び出し部分

コントラクトのメソッドそのまま呼びだすが、返り値をmakeTransactionでRawTransactionに変換する

let txHash = await this.$makeTransaction(
  this,
  this.$contract.methods.makeReference(
    this.summary.registered_summary_id,
    response.reference.hash
  )
).catch(e => {
  console.log(e)
  throw "TransactionError"
})

5.2 トランザクションのサインとAPIに投げる部分

  1. addressに対する、transactionカウントを取得
  2. transactionに署名し、RawTransactionに変換する
  3. APIサーバにRawTransactionを投げる
  4. RawTransactionを検証し、署名した人がサービスにいていい人かチェックする。またDDoSなどはAPIサーバで弾く

以下Nuxt.jsの実装例

default

abiをstaticファイルから引っ張ってきて、 Web3.Contract インスタンスを生成

getTransactionCount

APIサーバ経由でtransactionカウントを取得する関数

sendTransaction

APIサーバ経由でRawTransactionを実行する

import abi from "~/static/contracts/dapp_contract_v1.json"

const getTransactionCount = async (app, address) => {
  const res = await app.$axios.get("/v1/transactions/count?address=" + address)
  return res.data
}

const sendTransaction = async (app, rawTransaction, address) => {
  const res = await app.$axios.post("/v1/transactions", {
    raw_transaction: rawTransaction,
    address: address
  })
  return res.data
}

async function send(app, transaction) {
  let privateKey = app.$store.state.wallet.privateKey
  let address = app.$store.state.wallet.address
  let nonceRes = await getTransactionCount(app, address)
  let options = {
    to: transaction._parent._address,
    data: transaction.encodeABI(),
    gas: 4700000,
    chainId: process.env.NUXT_ENV_ETH_NETWORK_ID,
    nonce: nonceRes.nonce,
    gasPrice: "0x0"
  }
  let signedTransaction = await window.web3.eth.accounts.signTransaction(
    options,
    privateKey
  )
  let res = await sendTransaction(
    app,
    signedTransaction.rawTransaction,
    address
  )
  return res.tx_id
}

export default ({ store }, inject) => {
  var Web3 = require("web3")
  var web3 = new Web3()
  var FakeProvider = require("web3-fake-provider")

  web3.setProvider(new FakeProvider())
  window.web3 = web3
  const contract = new window.web3.eth.Contract(
    abi,
    process.env.NUXT_ENV_DAPP_CONTRACT_ADDRESS,
    {
      gasPrice: "0x00",
      gas: 4700000
    }
  )
  inject("contract", contract)
  inject("makeTransaction", send)
}

Gas

gasPriceは0にし、gasの単位は設定する

6. おわりに

今回コンソーシアムにおけるPoCでのウォレット管理とサイン周りの話を書きました。
PoCにおける仮説検証するときに多くのことを検証しすぎて、検証にめっちゃお金かかったり時間かかったり
そもそもコアなバリュー検証できたんだっけ?という状況って全然あるなと思っていて、検証ポイントをはっきりさせて
価値ある知見を世の中に残していければと思っています。

インプットとかまさかりとかあれば是非よろしくお願いします!

15
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
3