NKN公链源码学习[1]:webgui的加密通信
2021-05-29

背景

NKN的web-gui即dashboard模块,在通信过程中,有参数的HTTP包都会进过一次AES对称加密。

像这样

POST /api/wallet/open HTTP/1.1
Host: 192.168.1.100:30000
Content-Length: 63
Accept: application/json, text/plain, */*
Unix: 1588221168
Origin: http://192.168.1.100:30000
Content-Type: application/json;charset=UTF-8
Cookie: i18n_redirected=en; session=MTU4ODIyMTE2M3xRY2VaUnlSclQ1V3lOUllOSzAzSHZnMWhaZkJPbWpELUVjSGZNcTdBVWpHNnZLa1lZakF6b08xbXlHUklLanVCWU9fWkJueThRY0VnVU1mWjBrbHNmWjVjTFY1bnAzZVBJZUFtM1l0ay02WjZtTkNmaTRTR1pCNkgtcWVZN2ZRd1B4TGZ8FlyZ4mfYPDAvX_64WpEYZxPLBMNqm4Zq9LwtHU3v0AI=
Connection: close

{"data":"3ddc98eb25e7f572a0294533187e0d24b0e71c76897f300ec8d6"}

这里加密的data内容,其实是wallet的密码,即webgui的登陆密码。
阅读源码后总结了一下加密密钥的协商和使用的实现方法。

解密函数

我们只研究接收者逻辑

DecryptData

func DecryptData(context *gin.Context, hasSeed bool) string {
	...
	seed := ""
	wallet, exists := context.Get("wallet")
	if exists && hasSeed {
		passwordKeyHash := wallet.(*vault.WalletImpl).Data.PasswordHash
		seedByte := sha256.Sum256([]byte(passwordKeyHash))
		seed = BytesToHexString(seedByte[:])
	}

	tick := time.Now().Unix()
	padding := int64(serviceConfig.UnixRange)
	session := sessions.Default(context)
	token := session.Get("token")
	...

	for i := tick - padding; i < tick+padding; i++ {
		seedHash := BytesToHexString(HmacSha256([]byte(seed), []byte(token.(string)+strconv.FormatInt(i, 10))))
		jsonData, err := AesDecrypt(body.Data, seedHash)
		if err != nil {
			continue
		}
		var data map[string]interface{}
		err = json.Unmarshal([]byte(jsonData), &data)
		if err != nil {
			continue
		}
		return jsonData
	}
	...
	return ""
}

解释一下代码

记 AES解密密钥为decrypt_key
如果函数传入了hasSeed为true,
decrypt_key = passwordHash + token
如果hasSeed为false
decrypt_key = token

可见这个token是从session中取出的,所以token值是与服务器同步的,客户端不可控。
客户端在加密的时候要使用"正确的"token

token

服务器的token 每60秒更新一次,是一个uuid。
token的定义和更新代码如下:

app.Use(func(context *gin.Context) {
		session := sessions.Default(context)

		now := time.Now().Unix()
		if serviceConfig.TokenExp == 0 || serviceConfig.TokenExp+serviceConfig.TokenExpSec < now {
			token := uuid.NewUUID().String()
			serviceConfig.Token = token
			serviceConfig.TokenExp = now + serviceConfig.TokenExpSec

			session.Set("token", token)
			session.Save()
		}

		if session.Get("token") == nil {
			session.Set("token", serviceConfig.Token)
			session.Save()
		}

		context.Next()
	})

只要在规定时间范围内使用token加密,就可以在服务器端进行解密
范围是 token的时间戳 加减padding

padding := int64(serviceConfig.UnixRange)

PasswordHash

解密密钥的另一个部分是PasswordHash
这个在设计的时候被设计成 secret token
解密密钥的秘密性就体现在PasswordHash的不可知

对于敏感接口的密文,需要PasswordHash和token都正确才能解密

总结

在NKN中 token被理解为可公开的部分密钥,PasswordHash为不可公开的部分。
两部分组合才能解密。

但问题在于,wallet/open接口默认hasSeed为空,解密不需要PasswordHash。
也就是说wallet/open接口发送的密文,本质上是可以破解的。