作者: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-L32func (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部分,分别是:
- 首先對傳進來的
參數進行标準化操作,即去兩邊空白,并且轉換成小寫alias
- 檢查
中有沒有,有的話就直接傳回并報個相應的錯,不會重複生成,因為私鑰和别名是一一對應的。在前端可以根據這個錯誤提醒使用者檢查或者換一個新的别名。cache
- 調用
生成相應的密鑰,并拿到傳回的公鑰createChainKDKey
xpub
- 把公鑰放入cache中。看起來公鑰和别名并不是同一個東西,那前面為什麼可以查詢alias呢?
是以我們進入
h.cache.hasAlias
看看:
blockchain/pseudohsm/keycache.go#L76-L84func (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-L86func (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步,分别是:
-
生成密鑰。其中chainkd.NewXKeys
對應的是比原代碼庫中的另一個包chainkd
,從名稱上來看,使用的是"crypto/ed25519/chainkd"
算法。如果對前面文章“如何連上一個比原節點”還有印象的話,會記得比原在有新節點連上的時候,就會使用該算法生成一對密鑰,用于當次連接配接進行加密通信。不過需要注意的是,雖然兩者都是ed25519
算法,但是上次使用的代碼卻是來自第三方庫ed25519
的。它跟這次的算法在細節上究竟有哪些不同,目前還不清楚,留待以後合适的機會研究。然後是傳入"github.com/tendermint/go-crypto"
的參數chainkd.NewXKeys(nil)
,對應的是“随機數生成器”。如果傳的是nil
,nil
就會在内部使用預設的随機數生成器生成随機數并生成密鑰。關于密鑰算法相關的内容,在本文中并不探讨。NewXKeys
- 給目前密鑰生成一個唯一的id,在後面用于生成檔案名,儲存在硬碟上。id使用的是uuid,生成的是一個形如
這樣的全球唯一的随機數62bc9340-f6a7-4d16-86f0-4be61920a06e
- 把密鑰以檔案形式儲存在硬碟上。這塊内容比較多,下面詳細講。
- 把公鑰相關資訊組合在一起,供調用者使用。
我們再詳細講一下第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-L73func (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
)會在以後的文章中,進行詳細的讨論。