鍵生成と暗号化

鍵生成と暗号化

それは本当にランダムだろうか?

new Key() でコンストラクターを呼び出すと、内部ではPRNG(疑似乱数生成器)を使って秘密鍵を生成している。WindowsOS上では、Windows Crypto APIの.Netラッパーである RNGCryptoServiceProvider を使用している。

アンドロイドでは、私は SecureRandom を使用する。実際は RandomUtils.Random を使って自分独自の実装を使うことができる。

iOS上では、私はまだ実装したことはないが、自分で IRandom の実装クラスを作る必要がある。

コンピューターにとってランダムにすることは難しい。しかし、1番大きな問題は、ある一連の数値が本当にランダムかどうかを知ることが不可能だということである。

もし、マルウェアがあなたのPRNGを改ざんした場合(そうすると、あなたが生成する数値を予測できる)、手遅れになって初めて改ざんされたことに気づくことになる。

これは、クロスプラットフォームもしくはネイティブ実装のPRNG(コンピュータのクロックとCPUのスピードの組み合わせを利用)は危険であることを意味する。しかし、手遅れになるまで、それを知る由はない。

パフォーマンス上の理由から ほとんどのPRNGは同じように機能する。シード とよばれるランダムな数値が1つ選ばれ、呼び出す度に、結果が予測可能な式によって次の値が生成される。

シードのランダムさの度合は、エントロピー と呼ばれる計測値で定義されるが、エントロピー度は、監視している人にも依存する。

例えば、あなたが自分のクロック時間をもとにシード値を生成したとしよう。 そして、クロックが1ミリ秒の精度を持ったとしよう。(現実には15ミリ秒以上。)

もしあなたが先週鍵を生成したと、攻撃者が知っているとすると、シード値は、 1000 * 60 * 60 * 24 * 7 = 604800000 通りある。

そのような攻撃者にとって、エントロピーは、log2(604800000) = 29.17 ビットである。

その程度の回数だと順番に処理するとして、私の自宅のコンピュータで処理させても2秒以下しかかからない。このように順番に全可能性を当たってみる処理のことを”総当たり式”と呼ぶ。

でも、例えば、シード値を生成するのに、クロック時間とプロセスIDを使ったとする。 そして、1024個の個別のプロセスIDが存在すると想像してみよう。

今度は、攻撃者は、604800000 * 1024 回を順番に当たっていく必要があり、それには2000秒かかる。 さて、ここに、コンピューターを起動した日時の情報も足してみよう。攻撃者は私が今日起動したと知っているとしても、86400000 通りの可能性が追加できる。

これで、攻撃者は、604800000 * 1024 * 86400000 = 5,35088E+19通りの可能性に当たる必要がある。 しかし、覚えておいてほしいのは、もし攻撃者が私のコンピューターに侵入可能であれば、この追加の情報を取得できるので、可能性の数を減らし、エントロピーを下げることができる。

エントロピーは、log2(数値の取りえる可能性の数)で計算できるので、log2(5,35088E+19)= 65 ビットとなる。

これは十分だろうか。攻撃者が可能性を左右する情報をこれ以上持っていないと仮定するならば、多分そうだろう。

しかし、公開鍵のハッシュ値が20バイト = 160ビット ということは、すべてのアドレスの数よりまだ小さい。もう少し頑張れるかもしれない。

注意: エントロピーを増やすことは線形的に難しいが、エントロピーを解読するのは、指数関数的に難しくなる。

エントロピーを生成する面白い方法は、人間を介在させるやり方だ。(マウスを動かす。)

もし、あなたがプラットフォームのPRNGを完全に信頼できない(それはそれほどおかしな事ではない)なら、NBitcoinが使用するPRNGからの出力に対して、エントロピーを足すことができる。

RandomUtils.AddEntropy("hello");
RandomUtils.AddEntropy(new byte[] { 1, 2, 3 });
var nsaProofKey = new Key();

あなたが AddEntropy(data) を呼び出すときNBitcoinが処理するのは、 additionalEntropy = SHA(SHA(data) ^ additionalEntropy) だ。

そして、新しい数値を取得するときの結果は: result = SHA(PRNG() ^ additionalEntropy) だ。

鍵導出関数

しかし、最も重要なことは、可能性の数の多さではなく、攻撃者があなたの鍵を破ることに成功するのに、どれくらいの時間が必要かということである。そこでKDFの登場である。

KDF もしくは 鍵導出関数(Key Derivation Function) が、たとえエントロピーが低くても、より強い鍵をもつための方法である。

シード値を生成してみたいと想像してみよう。そして攻撃者が1千万個の可能性があることを知っていたとする。 そのようなシードであれば通常、クラックして破るのは非常に簡単だ。

しかし、もしあなたがその順番処理を遅くすることができたとしたらどうだろう? KDFはハッシュ関数で意図的にコンピューターの演算リソースを無駄に使わせることができる。 これが例だ:

var derived = SCrypt.BitcoinComputeDerivedKey("hello", new byte[] { 1, 2, 3 });
RandomUtils.AddEntropy(derived);

もし仮に、攻撃者があなたのエントロピーの元が5つの文字だと知っていたとしても、Scryptを実行して合っているか可能性を確認しないといけない。私のコンピューターでは5秒かかる。

結局のところ、どういうことかというと、疑似乱数発生器に悪さをされる事を極端に心配する必要はない。エントロピーの増加とKDFの使用、両方をすることで攻撃を弱めることができる。 覚えておいてほしいのは攻撃者は、あなたもしくはあなたのシステムの情報を集めることで、エントロピーを減らすことができるということだ。 もし、タイムスタンプを元にエントロピーを作るとして、あなたが先週キーを生成し、しかも午前9時から午後6時までしかコンピューターを使用しないということを攻撃者が知ってしまうと、エントロピーを減らすことになる。

前段で、特別なKDFであるScryptについて話した。そこで言ったとおり、KDFの目的は 総当たり攻撃をコストがかかるものにすることだ。

なので、KDFを使ってパスワードで、秘密鍵を暗号化する標準の方法がすでにあるとしても、別に驚くことではないだろう。それが、BIP38だ。

var privateKey = new Key();
var bitcoinPrivateKey = privateKey.GetWif(Network.Main);
Console.WriteLine(bitcoinPrivateKey); // L1tZPQt7HHj5V49YtYAMSbAmwN9zRjajgXQt9gGtXhNZbcwbZk2r
BitcoinEncryptedSecret encryptedBitcoinPrivateKey = bitcoinPrivateKey.Encrypt("password");
Console.WriteLine(encryptedBitcoinPrivateKey); // 6PYKYQQgx947Be41aHGypBhK6TA5Xhi9TdPBkatV3fHbbKrdDoBoXFCyLK
var decryptedBitcoinPrivateKey = encryptedBitcoinPrivateKey.GetSecret("password");
Console.WriteLine(decryptedBitcoinPrivateKey); // L1tZPQt7HHj5V49YtYAMSbAmwN9zRjajgXQt9gGtXhNZbcwbZk2r
Console.ReadLine();

このような暗号化は、2つのケースで使用される。:

  • 鍵の保存場所の提供者を信用していない。(ハッキングされるかもしれないから。)

  • 他の人のために鍵を保存しようとしている。(あなたはその人の鍵を知りたくない。)

もし、自分のストレージを持っているならデータベースが提供する暗号化で十分だろう。

もし、あなたのサーバーが復号化の機能を持つ場合、気を付けなければいけない。もしかすると、攻撃者が大量の鍵を復号化させるようなDDOS攻撃を仕掛けてくるかもしれないからだ。

可能であるならば、復号化は最終利用者に移譲すべきである。

古き良き時代のように

まず最初に、なぜ複数の鍵を生成しなければならないのだろうか。 一番の理由はプライバシーである。すべてのアドレスの残高を、だれでも見ることができるのだから、取引ごとに新しいアドレスを使う方が良い。

しかし、実際のところ、相手ごとにも鍵を生成することもできる。それにより、支払元を簡単に特定でき、プライバシーの露出も防げる。

これまでのコードでもやってきたように、以下のように鍵を生成できる:

var privateKey = new Key()

しかし、これには、2つの問題がある:

  • 新しい鍵を生成することで、自分の持つウォレットのバックアップが古くて使えなってしまう。

  • アドレスの作成処理を信用できない相手に委譲することができなくなる。

もし、ウェブウォレットを開発していて、ユーザーのためにキーを生成している場合、あるユーザーの鍵が破られたとすると、即座にその人はあなたを疑い始めるだろう。

BIP38 (パート2)

我々はBIP38をすでに見ているが、実は、このBIPは、2つのアイディアを1つにしたものだ。

2つ目については、どうやって鍵とアドレスの生成を、信頼できない相手に委譲することができるかを書いている。なので、2つの懸念点のうちの1つを解決する。

そのアイディアとは、鍵生成をする人へ渡すパスフレーズコードを作るというものだ。このパスフレーズコードで、その相手はあなたのために、暗号化された鍵を作ることができ、しかも、あなたのパスワードも秘密鍵も知ることはない。

この パスフレーズコード は WIFフォーマットにして鍵生成する人に渡すことができる。

注釈: NBitcoinでは、Bitcoinが頭についている型は全て Base58 (WIF)フォーマットのデータである。

なので、鍵の生成を委譲したいユーザーとして最初に、 パスフレーズコード を作ることになる。

var passphraseCode = new BitcoinPassphraseCode("my secret", Network.Main, null);

そしてこのパスフレーズコードを鍵を生成するサードパーティに渡す。

そのサードパーティは、暗号化された鍵をあなたに作ってくれる。

EncryptedKeyResult encryptedKeyResult = passphraseCode.GenerateEncryptedSecret();

この 暗号化された鍵 は、たくさんの情報を持っている。

まずは、生成されたビットコインアドレス。

var generatedAddress = encryptedKeyResult.GeneratedAddress; // 14KZsAVLwafhttaykXxCZt95HqadPXuz73

それから、暗号化された鍵自体(前段で述べた 鍵の暗号化 のレッスンでみたように)。

var encryptedKey = encryptedKeyResult.EncryptedKey; // 6PnWtBokjVKMjuSQit1h1Ph6rLMSFz2n4u3bjPJH1JMcp1WHqVSfr5ebNS

そして、最後だけれども重要である 確認コード。これにより、その第三者は、生成された鍵とアドレスが あなたのパスワードに対応していることを証明することができる。

var confirmationCode = encryptedKeyResult.ConfirmationCode; // cfrm38VUcrdt2zf1dCgf4e8gPNJJxnhJSdxYg6STRAEs7QuAuLJmT5W7uNqj88hzh9bBnU9GFkN

あなたは所有者として、この情報を手に入れたらすぐ、ConfirmationCode.Check を使って、鍵作成者がインチキをしていないか確認する必要がある。そして、パスワードを使って秘密鍵を取得する。

Console.WriteLine(confirmationCode.Check("my secret", generatedAddress)); // True
var bitcoinPrivateKey = encryptedKey.GetSecret("my secret");
Console.WriteLine(bitcoinPrivateKey.GetAddress() == generatedAddress); // True
Console.WriteLine(bitcoinPrivateKey); // KzzHhrkr39a7upeqHzYNNeJuaf1SVDBpxdFDuMvFKbFhcBytDF1R

我々は、第三者がどうやってあなたのパスワードと秘密鍵を知ることなしに暗号化された鍵を作成することができるかを見てきた。

しかし、まだ1つ問題が残っている:

  • 新しい鍵を生成することで、自分の持つウォレットのバックアップがすべて古いものになってしまう。

BIP 32、もしくは 階層的決定性ウォレット(HDウォレット)は、別の解決方法を提案しており、その方がより広くサポートされた方法だ。

HDウォレット (BIP 32)

それでは、我々が解決したい問題を再度心にとめておこう:

  • バックアップが古くなってしまうのを防ぎたい

  • 信頼してない相手に鍵やアドレスの生成を委譲したい

決定性ウォレットは、バックアップの問題を解決するだろう。そのようなウォレットでは、シード値だけは保存しなければならない。そしてこのシードから、一連の秘密鍵を何度も生成することができる。

これが、"決定性"の意味するところである。 以下のように、マスターキーから新しい鍵をどんどん生成できる。

ExtKey masterKey = new ExtKey();
Console.WriteLine("Master key : " + masterKey.ToString(Network.Main));
for (int i = 0; i < 5; i++)
{
ExtKey key = masterKey.Derive((uint)i);
Console.WriteLine("Key " + i + " : " + key.ToString(Network.Main));
}
Master key : xprv9s21ZrQH143K3JneCAiVkz46BsJ4jUdH8C16DccAgMVfy2yY5L8A4XqTvZqCiKXhNWFZXdLH6VbsCsqBFsSXahfnLajiB6ir46RxgdkNsFk
Key 0 : xprv9tvBA4Kt8UTuEW9Fiuy1PXPWWGch1cyzd1HSAz6oQ1gcirnBrDxLt8qsis6vpNwmSVtLZXWgHbqff9rVeAErb2swwzky82462r6bWZAW6Ty
Key 1 : xprv9tvBA4Kt8UTuHyzrhkRWh9xTavFtYoWhZTopNHGJSe3KomssRrQ9MTAhVWKFp4d7D8CgmT7TRzauoAZXp3xwHQfxr7FpXfJKpPDUtiLdmcF
Key 2 : xprv9tvBA4Kt8UTuLoEZPpW9fBEzC3gfTdj6QzMp8DzMbAeXgDHhSMmdnxSFHCQXycFu8FcqTJRm2kamjeE8CCKzbiXyoKWZ9ihiF7J5JicgaLU
Key 3 : xprv9tvBA4Kt8UTuPwJQyxuZoFj9hcEMCoz7DAWLkz9tRMwnBDiZghWePdD7etfi9RpWEWQjKCM8wHvKQwQ4uiGk8XhdKybzB8n2RVuruQ97Vna
Key 4 : xprv9tvBA4Kt8UTuQoh1dQeJTXsmmTFwCqi4RXWdjBp114rJjNtPBHjxAckQp3yeEFw7Gf4gpnbwQTgDpGtQgcN59E71D2V97RRDtxeJ4rVkw4E
Key 5 : xprv9tvBA4Kt8UTuTdiEhN8iVDr5rfAPSVsCKpDia4GtEsb87eHr8yRVveRhkeLEMvo3XWL3GjzZvncfWVKnKLWUMNqSgdxoNm7zDzzD63dxGsm

マスターキー だけを保存する必要がある。なぜなら、同じ一連の秘密鍵を何度も生成できるからである。

見ればわかるように、これらの鍵は、 ExtKey であり、使い慣れた Key ではない。しかし、内部に本物の秘密鍵を内包しているので、使うのを躊躇する必要はない。

KeyChainCodeExtKey のコンストラクターに渡すことで Key から ExtKey に戻すことも可能である。以下のようにする:

ExtKey extKey = new ExtKey();
byte[] chainCode = extKey.ChainCode;
Key key = extKey.PrivateKey;
ExtKey newExtKey = new ExtKey(key, chainCode);

base58 型の ExtKey は、 BitcoinExtKey という。

しかし、どうやって2つ目の問題を解決するのだろう: 潜在的にハッキングされる可能性のある相手(支払いサーバー)にアドレスの作成を委譲するには?

トリックは、マスターキーの無性化ができるということである。そして、秘密鍵を持たない公開可能なバージョンのマスターキーを持つことができる。無性化されたマスターキーから、第三者機関は、秘密鍵なしに公開鍵を作成できるのだ。

ExtPubKey masterPubKey = masterKey.Neuter();
for (int i = 0 ; i < 5 ; i++)
{
ExtPubKey pubkey = masterPubKey.Derive((uint)i);
Console.WriteLine("PubKey " + i + " : " + pubkey.ToString(Network.Main));
}
PubKey 0 : xpub67uQd5a6WCY6A7NZfi7yGoGLwXCTX5R7QQfMag8z1RMGoX1skbXAeB9JtkaTiDoeZPprGH1drvgYcviXKppXtEGSVwmmx4pAdisKv2CqoWS
PubKey 1 : xpub67uQd5a6WCY6CUeDMBvPX6QhGMoMMNKhEzt66hrH6sv7rxujt7igGf9AavEdLB73ZL6ZRJTRnhyc4BTiWeXQZFu7kyjwtDg9tjRcTZunfeR
PubKey 2 : xpub67uQd5a6WCY6Dxbqk9Jo9iopKZUqg8pU1bWXbnesppsR3Nem8y4CVFjKnzBUkSVLGK4defHzKZ3jjAqSzGAKoV2YH4agCAEzzqKzeUaWJMW
PubKey 3 : xpub67uQd5a6WCY6HQKya2Mwwb7bpSNB5XhWCR76kRaPxchE3Y1Y2MAiSjhRGftmeWyX8cJ3kL7LisJ3s4hHDWvhw3DWpEtkihPpofP3dAngh5M
PubKey 4 : xpub67uQd5a6WCY6JddPfiPKdrR49KYEuXUwwJJsL5rWGDDQkpPctdkrwMhXgQ2zWopsSV7buz61e5mGSYgDisqA3D5vyvMtKYP8S3EiBn5c1u4

では、支払いサーバーが公開鍵1を生成したと想定してみよう。すると対応した秘密鍵を、秘密のマスターキーを使って得ることができる。

masterKey = new ExtKey();
masterPubKey = masterKey.Neuter();
//The payment server generate pubkey1
ExtPubKey pubkey1 = masterPubKey.Derive((uint)1);
//You get the private key of pubkey1
ExtKey key1 = masterKey.Derive((uint)1);
//Check it is legit
Console.WriteLine("Generated address : " + pubkey1.PubKey.GetAddress(Network.Main));
Console.WriteLine("Expected address : " + key1.PrivateKey.PubKey.GetAddress(Network.Main));
Generated address : 1Jy8nALZNqpf4rFN9TWG2qXapZUBvquFfX
Expected address : 1Jy8nALZNqpf4rFN9TWG2qXapZUBvquFfX

ExtPubKeyExtKey に似ている。が、両者の違いは、 PubKey (公開鍵)を保持しているが Key (秘密鍵)は保持していない。

ここまで、決定性鍵がどうやって問題を解決してくれるのかを見てきた。では、次は、 階層的 が何を意味するかについて議論してみよう。

前段の演習では、マスターキーとインデックス番号の組み合わせにより 鍵を生成するのを見てきた。このプロセスを 派生 と呼び、マスターキーは 親の鍵 で、生成される鍵は、子供の鍵 である。

しかし、子供の鍵から派生した、さらに子供の鍵を複数作ることができる。これが 階層的 の意味である。

なので、概念的かつ一般的にこのように言うことができる: 親の鍵 + 鍵の階層パス → 子供の鍵

この図に書かれているように、親から2つのやり方で子供(1,1)を派生させることができる。

ExtKey parent = new ExtKey();
ExtKey child11 = parent.Derive(1).Derive(1);

もしくは、

ExtKey parent = new ExtKey();
ExtKey child11 = parent.Derive(new KeyPath("1/1"));

なので、まとめると:

ExtPubKey も同じように取り扱うことができる。

なぜ 階層的な鍵が必要になるのだろう?なぜなら、自分の複数の口座のための鍵を目的に別に分類するのは、良い方法だと思われるからだ。詳しくは、BIP44

そして、 組織の内部において口座の権限を分けて管理することができるようにもなる。

自分をある会社のCEOだとしてみよう。すると、会社のウォレットをすべて管理したいだろう。しかし、経理部に、マーケティング部のお金を使ってほしくない。

すると最初に思いつくアイディアは、1つの部署に1つの階層を生成することだろう。

しかし、この場合、経理部マーケティング部 はCEOの秘密鍵を逆生成できるかもしれない。

そのような子供鍵は 非強化 であると言われる。

ExtKey ceoKey = new ExtKey();
Console.WriteLine("CEO: " + ceoKey.ToString(Network.Main));
ExtKey accountingKey = ceoKey.Derive(0, hardened: false);
ExtPubKey ceoPubkey = ceoKey.Neuter();
//Recover ceo key with accounting private key and ceo public key
ExtKey ceoKeyRecovered = accountingKey.GetParentExtKey(ceoPubkey);
Console.WriteLine("CEO recovered: " + ceoKeyRecovered.ToString(Network.Main));
CEO: xprv9s21ZrQH143K2XcJU89thgkBehaMqvcj4A6JFxwPs6ZzGYHYT8dTchd87TC4NHSwvDuexuFVFpYaAt3gztYtZyXmy2hCVyVyxumdxfDBpoC
CEO recovered: xprv9s21ZrQH143K2XcJU89thgkBehaMqvcj4A6JFxwPs6ZzGYHYT8dTchd87TC4NHSwvDuexuFVFpYaAt3gztYtZyXmy2hCVyVyxumdxfDBpoC

別の言い方でいうと、 非強化鍵 は階層を"登る"ことができる。非強化鍵 は単一管理されるタイプの口座にだけ使用されるべきでる。

なので、我々のケースでは、CEOは 強化鍵 を作成しなければならない。そうすれば、経理部は、階層を登って秘密鍵を見つけることはできない。

ExtKey ceoKey = new ExtKey();
Console.WriteLine("CEO: " + ceoKey.ToString(Network.Main));
ExtKey accountingKey = ceoKey.Derive(0, hardened: true);
ExtPubKey ceoPubkey = ceoKey.Neuter();
ExtKey ceoKeyRecovered = accountingKey.GetParentExtKey(ceoPubkey); //Crash

ExtKey.Derivate(鍵階層パス) を使うときに、子のインデックス値の後ろにアポストロフィを付けることでも、強化鍵を作成できる。

var nonHardened = new KeyPath("1/2/3");
var hardened = new KeyPath("1/2/3'");

では経理部は、顧客ごとに1つの親鍵を生成したと想定してみよう。そして、顧客からの支払いごとにその子供鍵を使う。

CEOであるあなたは、そのうちの1つのアドレスから支払いたい場合、こうやって行う。

ceoKey = new ExtKey();
string accounting = "1'";
int customerId = 5;
int paymentId = 50;
KeyPath path = new KeyPath(accounting + "/" + customerId + "/" + paymentId);
//Path : "1'/5/50"
ExtKey paymentKey = ceoKey.Derive(path);

HD鍵のためのネモニック(記憶しやすい)コード(BIP39)

これまで見たように、HD鍵を作成することは簡単である。しかしもし、そんな鍵を電話や手書きで人に伝える簡単な方法があればどうだろうか?

Tresorのようなコールドウォレットは、簡単に書き留められるようなある文章をもとに、HD鍵を生成する。そういった文章は ザ・シード と言われたり ネモニック(記憶文) と言われる。そして、それはパスワードやPIN番号でさらに暗号化される。

書き留めやすい文章を生成するために使用される言葉は、単語リスト と呼ばれる。

Mnemonic mnemo = new Mnemonic(Wordlist.English, WordCount.Twelve);
ExtKey hdRoot = mnemo.DeriveExtKey("my password");
Console.WriteLine(mnemo);

minute put grant neglect anxiety case globe win famous correct turn link

ここでもし、ネモニックとパスワードを持っていたら、 階層ルート の鍵を見つけることができる。

mnemo = new Mnemonic("minute put grant neglect anxiety case globe win famous correct turn link",
Wordlist.English);
hdRoot = mnemo.DeriveExtKey("my password");

現時点でサポートされている 単語リスト の言語は、英語、日本語、スペイン語、中国語(簡体字と繁体字)である。

ダークウォレット

この名前がついたのは不運である。なぜなら、まったくもってこのウォレットはダークではないのに、不要な注目や懸念を生んでしまっている。ダークウォレットは、我々の最初の2つの問題に対する実践的な解決策である。

  • 古くなって使えなくなるウォレットのバックアップを防ぐ

  • 鍵とアドレスの生成を信用していない相手に移譲すること

しかし、このウォレットは、ボーナス的なすごい機能をもつのだ。

あなたは、唯一の( ステルスアドレス と呼ばれる)アドレスだけを、ほかの人たちにシェアするだけでよいのだ。よって、プライバシーを失うことがない。

ここで思い出してほしいのは、1つの ビットコインアドレス をみんなと共有すると、すべての人たちは、ブロックチェーンをみれば、あなたの持っているビットコインの残高を知ることができるということだ。しかし、ステルスアドレス を使えば、それには当たらない。

このウォレットがダークと命名されたのは、本当に残念なことである。なぜかというと、それは部分的にでもビットコインの疑似匿名性が原因として起きる、プライバシー問題を解決するからである。より良い名前があるとしたら、ワン・アドレス だったかもしれない。

ダークウォレットの用語を使うと、以下のように参加者を表現する:

  • 支払者 は、 受領者ステルスアドレス を知っている。

  • 受領者 は、 スペンド鍵 を知っていて、その秘密のコードで彼が受け取るコインを消費することができる。

  • スキャナー は、スキャン鍵 を知っていて、 その秘密のコードで、 その 受領者 に属する取引を特定することができる。

以降は、操作の詳細になる。内部的には、このステルスアドレスは、1つもしくは複数(複数署名の場合)の スペンド公開鍵 と1つの スキャン公開鍵 で構成されている。

var scanKey = new Key();
var spendKey = new Key();
BitcoinStealthAddress stealthAddress
= new BitcoinStealthAddress
(
scanKey: scanKey.PubKey,
pubKeys: new[] { spendKey.PubKey },
signatureCount: 1,
bitfield: null,
network: Network.Main);

支払者 は、あなたの ステルスアドレス を使って、一時的に使用する エフェム鍵 を作成し、そこから ステルス公開鍵 を生成する。それから、支払いが行われるビットコインアドレスが作成される。

そして、(最初のチャレンジでやったように)トランザクションのOP_RETURNに埋め込まれた ステルスメタデータ の中に エフェム公開鍵 をパッケージする。

さらに、トランザクション・アウトプットを生成されたビットコインアドレス( ステルス公開鍵 のアドレス)へ追加する。

var ephemKey = new Key();
Transaction transaction = new Transaction();
stealthAddress.SendTo(transaction, Money.Coins(1.0m), ephemKey);
Console.WriteLine(transaction);

エフェム鍵 の生成の実装については詳細すぎるので、無視して良いだろう。NBitcoinが自動的に生成してくれる:

Transaction transaction = new Transaction();
stealthAddress.SendTo(transaction, Money.Coins(1.0m));
Console.WriteLine(transaction);
{
"hash": "7772b0ad19acd1bd2b0330238a898fe021486315bd1e15f4154cd3931a4940f9",
"ver": 1,
"vin_sz": 0,
"vout_sz": 2,
"lock_time": 0,
"size": 93,
"in": [],
"out": [
{
"value": "0.00000000",
"scriptPubKey": "OP_RETURN 060000000002b9266f15e8c6598e7f25d3262969a774df32b9b0b50fea44fc8d914c68176f3e"
},
{
"value": "1.00000000",
"scriptPubKey": "OP_DUP OP_HASH16051f68af989f5bf24259c519829f46c7f2935b756 OP_EQUALVERIFY OP_CHECKSIG"
}
]
}

そして、支払者は、トランザクション・インプットをトランザクションに追加し署名をする。それからビットコインネットワークにトランザクションを送信する。

ステルスアドレススキャン鍵 を知る スキャナー は、ステルス公開鍵 を取得できるので、予定された ビットコインアドレス への支払いも確認できる。

それから スキャナー は、トランザクションのアウトプットの1つがそのアドレスに対するものかをチェックし、もしそうであるなら スキャナー は、 受領者 に対してトランザクションについて通知する。

受領者 は、スペンド鍵 を使うことでそのアドレスの秘密鍵を取得できる。

スキャナーとして、どうやってトランザクションをスキャンするか、そして受領者としてどうやって秘密鍵を見つけるかについてのコードは、あとの TransactionBuilderを使ってみる の章で説明する。

ここで知ってほしいのは、ステルスアドレス は、複数の スペンド公開鍵 を持つことができることだ。それを使う場合、そのアドレスはマルチシグ用(複数人による署名)のアドレスということになる。

ダークウォレットの唯一の制限事項は、OP_RETURN を使用するということである。すなわち、トランザクションに任意のデータを埋め込むことが簡単にはできない。(現在のビットコインのルールでは、OP_RETURNには40バイトしか許されていない。もうすぐ1トランザクションに対して80バイトになる予定だ。)

(Stackoverflow) 私が理解するに、"ステルスアドレス"とは、ある特定の問題に対応するために作られた。一般の人々からお金を集めたいと思ったら、例えば寄付のためのアドレスを自分のウェブサイトに載せるとかいうようにだが、そうすると、ブロックチェーン上ですべての支払いの情報が見られてしまう。もしかしたら、あなたがそのアドレスから何に支払ったかを追跡されるだろう。

ステルスアドレスを使うケースでは、支払者にユニークなアドレスを生成するように依頼する。そうすることで、そのトランザクションに内包された追加情報を使うだけで、対応した秘密鍵を推測することができる。そしてあなたは、1つのステルスアドレスをウェブサイトに公開したにもかかわらず、ブロックチェーン上では、すべて違ったアドレスに支払いが行われることになり、それぞれアドレスの関連性を見つけることは不可能だ。(もちろん支払者は、支払先アドレスを知っているので、そこからどこにお金を送ったかは見ることができるが、ほかの支払者には、それを見ることができない。)

しかし、ほかの方法でも同様の効果を得ることはできる:それぞれの支払者に対して、固有の別々のアドレスを渡せばよい。1つの公開アドレスをウェブサイトに載せる代わりに、例えば押されるごとに新しいアドレスを生成し、秘密鍵を保存するようなボタンを置いておくとか、事前に用意しておいた公開アドレスのリストから次々とアドレスを選んで使うようなボタンを配置するとか(もちろん秘密鍵は、安全な場所に保存しておく)。ステルスアドレスを使用するときと同様に、支払いはすべて個別のアドレスへ行われ、アドレス間の相関は全くないし、一人の支払者が、別の支払情報を見ることもできない。

なので、ステルスアドレスを使用することとの唯一の違いは、サーバーが面倒なアドレス作成の処理をしなくて良いということである。実は、ある意味でステルスアドレスを使用するのは悪いことかもしれない。なぜなら、ステルスアドレスを使用する人が少ないため、もしあなたがステルスアドレスを使用していると知られてしまうと、ステルスアドレスのトランザクションは、あなたのものであると分かってしまうからだ。

この方法は"100%の匿名性"を提供しない。ビットコインの基本的な匿名性に対する弱さは、残ってしまう。すなわち、だれでも支払いのつながりを見ることができるということだ。なので、もしあなたが、あるトランザクションそのもの、もしくは、そのトランザクションの関係者だと知るならば、そのビットコインがどこからきて、どこへいったのを見ることができるのだ。