剥开比原看代码10:比原是如何通过/create-key接口创建密钥的

简介: 作者:freewind比原项目仓库:Github地址:https://github.com/Bytom/bytomGitee地址:https://gitee.com/BytomBlockchain/bytom在前一篇,我们探讨了从浏览器的dashboard中进行注册的时候,密钥、帐户的别名以及密码,是如何从前端传到了后端。

作者: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,对应的是“随机数生成器”。如果传的是nilNewXKeys就会在内部使用默认的随机数生成器生成随机数并生成密钥。关于密钥算法相关的内容,在本文中并不探讨。
  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)会在以后的文章中,进行详细的讨论。

相关文章
|
测试技术 数据库 Python
软件测试面试题:不可逆的操作,如何处理,比如删除一个订单这种接口如何测试
软件测试面试题:不可逆的操作,如何处理,比如删除一个订单这种接口如何测试
165 0
|
前端开发
前端工作总结132-根据id传向把对象里面的整个数据传向下个接口
前端工作总结132-根据id传向把对象里面的整个数据传向下个接口
80 0
|
机器学习/深度学习 存储
1719. 重构一棵树的方案数 : 构造 + 验证(合法性 + 非唯一性)
1719. 重构一棵树的方案数 : 构造 + 验证(合法性 + 非唯一性)
|
测试技术
测试用例(包含测经典试点全集图解,强烈建议保存收藏)(二)
测试用例(Test Case)是指对一项特定的软件产品进行测试任务的描述,体现测试方案、方法、技术和策略。其内容包括测试目标、测试环境、输入数据、测试步骤、预期结果、测试脚本等,最终形成文档。简单地认为,测试用例是为某个特殊目标而编制的一组测试输入、执行条件以及预期结果,用于核实是否满足某个特定软件需求。
115 0
测试用例(包含测经典试点全集图解,强烈建议保存收藏)(二)
|
测试技术 数据库
测试用例(包含测经典试点全集图解,强烈建议保存收藏)(一)
测试用例(Test Case)是指对一项特定的软件产品进行测试任务的描述,体现测试方案、方法、技术和策略。其内容包括测试目标、测试环境、输入数据、测试步骤、预期结果、测试脚本等,最终形成文档。简单地认为,测试用例是为某个特殊目标而编制的一组测试输入、执行条件以及预期结果,用于核实是否满足某个特定软件需求。
411 1
测试用例(包含测经典试点全集图解,强烈建议保存收藏)(一)
|
Linux 开发工具 Docker
比原链(Bytom)节点接入文档
系统要求 我们建议选择主要的几家云主机平台的VPS服务,运行比原链节点对算力没有要求,但是请配置尽可能大的磁盘空间以适应区块链数据未来增长的需要。 节点服务器最小配置: 操作系统: Windows/Linux/Docker CPU: 2核 内存: 2G 硬盘: 40G 网络: 独立IP,2MB带宽 防火墙: 开启46657端口 Ubuntu接入文档 1 节点服务器部署 1.
1419 0
|
JSON 前端开发 API
剥开比原看代码12:比原是如何通过/create-account-receiver创建地址的?
作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 在比原的dashboard中,我们可以为一个帐户创建地址(address),这样就可以在两个地址之间转帐了。
1373 0
|
JSON 前端开发 API
剥开比原看代码11:比原是如何通过接口/create-account创建帐户的
作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 在前面,我们探讨了从浏览器的dashboard中进行注册的时候,数据是如何从前端发到后端的,并且后端是如何创建密钥的。
1278 0
|
JSON 前端开发 API
剥开比原看代码16:比原是如何通过/list-transactions显示交易信息的
作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 在前一篇文章中,我们试图理解比原是如何交易的,但是由于内容太多,我们把它分成了几个小问题,并在前一篇解决了“在dashboard中如何提交交易信息”,以及“比原后台是如何操作的”。
1182 0
|
网络协议 NoSQL Go
剥开比原看代码01:初始化时生成的配置文件在哪儿
作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 人们常说,“阅读源代码”是学习编程的一种重要方法。
1604 0