建立數字錢包(一)賬號生成

weixin_33711641發表於2019-01-23

橢圓曲線數字簽名演算法生成私鑰

Secp256k1
通過橢圓曲線數字簽名演算法生成私鑰和公鑰,其中SEC(Standards for Efficient Cryptography)是專門利用ECDSA或者其可選項Schnorr演算法來產生高效的加密方法。
特點是生成金鑰很快。

Scep256k1 基本特性

  • secp256k1 ECDSA signing/verification and key generation.
  • Adding/multiplying private/public keys.
  • Serialization/parsing of private keys, public keys, signatures.
  • Constant time, constant memory access signing and pubkey generation.
  • Derandomized DSA (via RFC6979 or with a caller provided function.)
  • Very efficient implementation.

講解程式碼

步驟

  1. 生成私鑰
  2. 加密私鑰
  3. 生成 keyObject 物件
  4. 從keyObject物件中恢復私鑰

生成私鑰

下面利用 keythereum[1] 產生符合以太坊的金鑰,併產生keyObject檔案

const params = { keyBytes: 32, ivBytes: 16 };
let {privateKey, salt, iv} = keythereum.create(params);

keythereum可以產生私鑰,以及後面加密私鑰所用的PBKDF2演算法需要的salt,和加密aes-128-ctr私鑰的iv值。

得到私鑰之後,我們可以通過私鑰生成公鑰。

let privateKeyBuffer = Buffer.from(privateKey, "hex") // or "base64"
let publicKey = secp256k1.publicKeyCreate(privateKeyBuffer, false).slice(1);
let address = "0x" + keccak256(publicKey).slice(-20).toString("hex");

加密私鑰

利用KDF演算法基於password派生出金鑰,然後利用這個金鑰加密我們的私鑰。

const password = "Hello,Ethereum"
const options = {
    kdf: "pbkdf2",
    cipher: "aes-128-ctr",
    kdfparams: {
        c: 262144,
        dklen: 32,
        prf: "hmac-sha256"
    }
};
const keyObject = keythereum.dump(password, privateKey, salt, iv, options);

這就是產生keyObject基本思路。我們在看看dump函式到底做了什麼

this.marshal(this.deriveKey(password, salt, options), privateKey, salt, iv, options);

deriveKey(...) 的原始碼如下:

this.crypto.pbkdf2Sync(
        password,
        salt,
        options.kdfparams.c || this.constants.pbkdf2.c,
        options.kdfparams.dklen || this.constants.pbkdf2.dklen,
        prf //hmac-sha256
      );

這裡基於password生成的derivedKey,這個金鑰並不是我們要用的私鑰,而是用來加密先前生成的privateKey的,加密的過程在marshal函式中呼叫的encrypt函式裡。

let ciphertext = this.encrypt(privateKey, derivedKey.slice(0, 16), iv, algo).toString("hex");

encrypt函式,如下:

var cipher, ciphertext;
algo = algo || this.constants.cipher;
if (!this.isCipherAvailable(algo)) throw new Error(algo + " is not available");

//加密過程
cipher = this.crypto.createCipheriv(algo, this.str2buf(key), this.str2buf(iv));
ciphertext = cipher.update(this.str2buf(plaintext));

return Buffer.concat([ciphertext, cipher.final()]);

此處的ciphertext代表的是privateKey,而key則是derivedKey

生成 keyObject 物件

得到了加密後的ciphertext之後,開始組裝keyObject物件並返回。

keyObject = {
      address: this.privateKeyToAddress(privateKey).slice(2),
      crypto: {
        cipher: options.cipher || this.constants.cipher,
        ciphertext: ciphertext,
        cipherparams: { iv: iv.toString("hex") },
        mac: this.getMAC(derivedKey, ciphertext)
      },
      id: uuid.v4(), // random 128-bit UUID
      version: 3
    };
keyObject.crypto.kdf = "pbkdf2";
      keyObject.crypto.kdfparams = {
        c: options.kdfparams.c || this.constants.pbkdf2.c,
        dklen: options.kdfparams.dklen || this.constants.pbkdf2.dklen,
        prf: options.kdfparams.prf || this.constants.pbkdf2.prf,
        salt: salt.toString("hex")
      };    

privateKeyToAddress(...)方法裡首先通過privateKey產生publicKey,然後使用keccak256雜湊publicKey得到地址。

具體實現如下:

let privateKeyBuffer = Buffer.from(privateKey);
let publicKey = secp256k1.publicKeyCreate(privateKeyBuffer, false).slice(1);
let address = "0x" + keccak256(publicKey).slice(-20).toString("hex");

keccak256(publicKey) 產生了32bytes,擷取尾部20bytes轉換成十六進位制之後就是40字元,加上前導0x之後,就是42個字元的以太坊地址,比如:0x0f645438395206b408e52be4fcf4bc21c330bfa2

從keyObject物件中恢復私鑰

有了keyObject和密碼就可以恢復原來的私鑰

let privateKey = keythereum.recover(password, keyObject)

可以想到,recover方法中,首先會利用password和keyObject中的salt派生出當初的金鑰derivedKey,然後把加密過的私鑰ciphertext和derivedKey, iv作為原來加密演算法aes-128-ctr的輸入引數,成功解密後返回明文的私鑰。

具體程式碼如下:

verifyAndDecrypt(this.deriveKey(password, salt, keyObjectCrypto), salt, iv, ciphertext, algo)

這裡首先得到了derivedKey,然後驗證並解密kyeObject中的ciphertext,如下:

function verifyAndDecrypt(derivedKey, salt, iv, ciphertext, algo) {
  var key;
  if (self.getMAC(derivedKey, ciphertext) !== keyObjectCrypto.mac) {
    throw new Error("message authentication code mismatch");
  }
  if (keyObject.version === "1") {
    key = keccak256(derivedKey.slice(0, 16)).slice(0, 16);
  } else {
    key = derivedKey.slice(0, 16);
  }
  return self.decrypt(ciphertext, key, iv, algo);
}

注意這裡的mac值比較,確保了ciphertext沒有被人篡改才有解密的必要。

參考實現

  1. NodeJS
  2. Bitcoin-core

  1. Keythereum

相關文章