天天看點

剝開比原看代碼10:比原是如何通過/create-key接口建立密鑰的

作者:freewind

比原項目倉庫:

Github位址:

https://github.com/Bytom/bytom Gitee位址: https://gitee.com/BytomBlockchain/bytom

在前一篇,我們探讨了從浏覽器的dashboard中進行注冊的時候,密鑰、帳戶的别名以及密碼,是如何從前端傳到了後端。在這一篇,我們就要看一下,當比原背景收到了建立密鑰的請求之後,将會如何建立。

由于本文的問題比較具體,是以就不需要再細分,我們直接從代碼開始。

還記得在前一篇中,對應建立密鑰的web api的功能點的配置是什麼樣的嗎?

API.buildHandler

方法中: api/api.go#L164-L244

func (a *API) buildHandler() {
    // ...
    if a.wallet != nil {
        // ...
        m.Handle("/create-key", jsonHandler(a.pseudohsmCreateKey))
        // ...
      

可見,其路徑為

/create-key

,而相應的handler是

a.pseudohsmCreateKey

(外面套着的

jsonHandler

在之前已經讨論過,這裡不提):

api/hsm.go#L23-L32
func (a *API) pseudohsmCreateKey(ctx context.Context, in struct {
    Alias string `json:"alias"`
    Password string `json:"password"`
}) Response {
    xpub, err := a.wallet.Hsm.XCreate(in.Alias, in.Password)
    if err != nil {
        return NewErrorResponse(err)
    }
    return NewSuccessResponse(xpub)
}
      

它主要是調用了

a.wallet.Hsm.XCreate

,讓我們跟進去:

blockchain/pseudohsm/pseudohsm.go#L50-L66
// XCreate produces a new random xprv and stores it in the db.
func (h *HSM) XCreate(alias string, auth string) (*XPub, error) {
    // ...
    // 1.
    normalizedAlias := strings.ToLower(strings.TrimSpace(alias))
    // 2.
    if ok := h.cache.hasAlias(normalizedAlias); ok {
        return nil, ErrDuplicateKeyAlias
    }

    // 3.
    xpub, _, err := h.createChainKDKey(auth, normalizedAlias, false)
    if err != nil {
        return nil, err
    }
    // 4.
    h.cache.add(*xpub)
    return xpub, err
}
      

其中出現了

HSM

這個詞,它是指

Hardware-Security-Module

,原來比原還預留了跟硬體相關的子產品(暫不讨論)。

上面的代碼分成了4部分,分别是:

  1. 首先對傳進來的

    alias

    參數進行标準化操作,即去兩邊空白,并且轉換成小寫
  2. 檢查

    cache

    中有沒有,有的話就直接傳回并報個相應的錯,不會重複生成,因為私鑰和别名是一一對應的。在前端可以根據這個錯誤提醒使用者檢查或者換一個新的别名。
  3. 調用

    createChainKDKey

    生成相應的密鑰,并拿到傳回的公鑰

    xpub

  4. 把公鑰放入cache中。看起來公鑰和别名并不是同一個東西,那前面為什麼可以查詢alias呢?

是以我們進入

h.cache.hasAlias

看看:

blockchain/pseudohsm/keycache.go#L76-L84
func (kc *keyCache) hasAlias(alias string) bool {
    xpubs := kc.keys()
    for _, xpub := range xpubs {
        if xpub.Alias == alias {
            return true
        }
    }
    return false
}
      

通過

xpub.Alias

我們可以了解到,原來别名跟公鑰是綁定的,

alias

可以看作是公鑰的一個屬性(當然也屬于相應的私鑰)。是以前面把公鑰放進cache,之後就可以查詢别名了。

那麼第3步中的

createChainKDKey

又是如何生成密鑰的呢?

blockchain/pseudohsm/pseudohsm.go#L68-L86
func (h *HSM) createChainKDKey(auth string, alias string, get bool) (*XPub, bool, error) {
    // 1.
    xprv, xpub, err := chainkd.NewXKeys(nil)
    if err != nil {
        return nil, false, err
    }
    // 2.
    id := uuid.NewRandom()
    key := &XKey{
        ID: id,
        KeyType: "bytom_kd",
        XPub: xpub,
        XPrv: xprv,
        Alias: alias,
    }
    // 3.
    file := h.keyStore.JoinPath(keyFileName(key.ID.String()))
    if err := h.keyStore.StoreKey(file, key, auth); err != nil {
        return nil, false, errors.Wrap(err, "storing keys")
    }
    // 4.
    return &XPub{XPub: xpub, Alias: alias, File: file}, true, nil
}
      

這塊代碼内容比較清晰,我們可以把它分成4步,分别是:

  1. chainkd.NewXKeys

    生成密鑰。其中

    chainkd

    對應的是比原代碼庫中的另一個包

    "crypto/ed25519/chainkd"

    ,從名稱上來看,使用的是

    ed25519

    算法。如果對前面文章“如何連上一個比原節點”還有印象的話,會記得比原在有新節點連上的時候,就會使用該算法生成一對密鑰,用于當次連接配接進行加密通信。不過需要注意的是,雖然兩者都是

    ed25519

    算法,但是上次使用的代碼卻是來自第三方庫

    "github.com/tendermint/go-crypto"

    的。它跟這次的算法在細節上究竟有哪些不同,目前還不清楚,留待以後合适的機會研究。然後是傳入

    chainkd.NewXKeys(nil)

    的參數

    nil

    ,對應的是“随機數生成器”。如果傳的是

    nil

    NewXKeys

    就會在内部使用預設的随機數生成器生成随機數并生成密鑰。關于密鑰算法相關的内容,在本文中并不探讨。
  2. 給目前密鑰生成一個唯一的id,在後面用于生成檔案名,儲存在硬碟上。id使用的是uuid,生成的是一個形如

    62bc9340-f6a7-4d16-86f0-4be61920a06e

    這樣的全球唯一的随機數
  3. 把密鑰以檔案形式儲存在硬碟上。這塊内容比較多,下面詳細講。
  4. 把公鑰相關資訊組合在一起,供調用者使用。

我們再詳細講一下第3步,把密鑰儲存成檔案。首先是生成檔案名,

keyFileName

函數對應的代碼如下:

blockchain/pseudohsm/key.go#L96-L101
// keyFileName implements the naming convention for keyfiles:
// UTC--<created_at UTC ISO8601>-<address hex>
func keyFileName(keyAlias string) string {
    ts := time.Now().UTC()
    return fmt.Sprintf("UTC--%s--%s", toISO8601(ts), keyAlias)
}
      

注意這裡的參數

keyAlias

實際上應該是

keyID

,就是前面生成的uuid。寫成

alias

有點誤導,已經送出PR

#922

。最後生成的檔案名,形如:

UTC--2018-05-07T06-20-46.270917000Z--62bc9340-f6a7-4d16-86f0-4be61920a06e

生成檔案名之後,會通過

h.keyStore.JoinPath

把它放在合适的目錄下。通常來說,這個目錄是本機資料目錄下的

keystore

,如果你是OSX系統,它應該在你的

~/Library/Bytom/keystore

,如果是别的,你可以通過下面的代碼來确定

DefaultDataDir()

關于上面的儲存密鑰檔案的目錄,到底是怎麼确定的,在代碼中其實是有點繞的。不過如果你對這感興趣的話,我相信你應該能自行找到,這裡就不列出來了。如果找不到的話,可以試試以下關鍵字:

pseudohsm.New(config.KeysDir())

os.ExpandEnv(config.DefaultDataDir())

DefaultDataDir()

DefaultBaseConfig()

在第3步的最後,會調用

keyStore.StoreKey

方法,把它儲存成檔案。該方法代碼如下:

blockchain/pseudohsm/keystore_passphrase.go#L67-L73
func (ks keyStorePassphrase) StoreKey(filename string, key *XKey, auth string) error {
    keyjson, err := EncryptKey(key, auth, ks.scryptN, ks.scryptP)
    if err != nil {
        return err
    }
    return writeKeyFile(filename, keyjson)
}
      

EncryptKey

裡做了很多事情,把傳進來的密鑰及其它資訊利用起來生成了JSON格式的資訊,然後通過

writeKeyFile

把它儲存硬碟上。是以在你的

keystore

目錄下,會看到屬于你的密鑰檔案。它們很重要,千萬别誤删了。

a.wallet.Hsm.XCreate

看完了,讓我們回到

a.pseudohsmCreateKey

方法的最後一部分。可以看到,當成功生成key之後,會傳回一個

NewSuccessResponse(xpub)

,把與公鑰相關的資訊傳回給前端。它會被

jsonHandler

自動轉換成JSON格式,通過http傳回過去。

在這次的問題中,我們主要研究的是比原在通過web api接口

/create-key

接收到請求後,在内部做了哪些事,以及把密鑰檔案放在了哪裡。其中涉及到密鑰的算法(如

ed25519

)會在以後的文章中,進行詳細的讨論。

繼續閱讀