背景
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接口發送的密文,本質上是可以破解的。