10.24本来是普通的日子,但却有机会让你很容易的区分的两种人:程序猿与其他。
正好周六,于是把之前 side project 中的一个临时方案修改一下。其中一个关键问题点是:用 go 生成 Instagram 登录时需要用到的加密密码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public static string GenerateEncPassword(string password, string publicKey, string keyId, string version) { var time = DateTime.UtcNow.ToTimestamp(); // Unix timestamp var keyBytes = publicKey.HexToBytes(); // Convert a hex string to a byte array var key = new byte[32]; new Random().NextBytes(key); var iv = new byte[12]; var tag = new byte[16]; var plainText = password.ToBytes(); // ToBytes = Encoding.UTF8.GetBytes var cipherText = new byte[plainText.Length]; using (var cipher = new AesGcm(key)) { cipher.Encrypt(nonce: iv, plaintext: plainText, ciphertext: cipherText, tag: tag, associatedData: time.ToString().ToBytes()); } var encryptedKey = SealedPublicKeyBox.Create(key, keyBytes); var bytesOfLen = ((short)encryptedKey.Length).ToBytes(); // ToBytes = BitConverter.GetBytes(short); var info = new byte[] { 1, byte.Parse(keyId) }; var bytes = info.Concat(bytesOfLen).Concat(encryptedKey).Concat(tag).Concat(cipherText); // Concat means that concat two array // expected: #PWD_INSTAGRAM_BROWSER:10:1595671654:ARBQAFWLYGkTT9UU0dyUCkaGTRFu0PH5Ph5s86DUAbZ+B9xon8cKmnqQGaUo7bB4NHCMKQRY69b9LwaJZ1rDw1OFM0LEGtI+KbDuDC0QnfJM6o1no0XPOl73RJoUZ/OfN5nE2q/IdqX0NFinS0faRf8= var str = $"#PWD_INSTAGRAM_BROWSER:{version}:{time}:{bytes.ToBase64()}"; // ToBase64 = Convert.ToBase64String return str; } |
扫了一下整体结构,发现IG的这段加密设计还是很好的体现了大厂典范:
- 密码加密使用到了
aes-256-gcm
, 不是野生程序员上来就是base64或者来一个自欺欺人的哈希函数(MD5, SHA1等)。这两者本身不应该一起提,因为本身是用途完全不一样的东西,只是国内看过很多方案是后者,有时候还觉得自己加了salt更安全。AES结合GCM在解决数据加密问题的同时,也解决是数据完整性校验的问题。此外,加密操作中引入time作为附加数据,因此也可以校验数据的时效性,防止重放攻击。 - AES是一种对称加密,因此涉及的密钥分发的问题。上述方案中,使用了
SealedPublicKeyBox
这种常用且规范的源于。即通过接收者的公钥来解密AES的对称密钥,得到密钥的密文。而这个密钥密文只有接收者使用自己的私钥才能解密。 - 从私钥密钥去中心化以及管理维护成本出发,应该是同时启用了多对公私钥对(可通过
keyId
识别)。 - 密文
bytes
构造上采用经典简洁的length.content
设计。 - 最终构造的
enc_password
格式中包含了平台、版本、时间戳、密文。如果你也设计过密码加密的方案,你应该能看到这几个要素没有一点废话,基本就是教科书的标准格式示范。调研了一圈,发现的唯一业务上的槽点是版本为某个特定值时,其实是允许传输明文密码
的。这从语义上来说是矛盾的,推测是从前向兼容的一种妥协。 - golang中可以通过
NewGCM
实现aes-256-gcm
, 但是认证标签(authentication tag) 没有单独输出,是append在密文之后的。因此可以通过cipherText[len(cipherText)-16:]
获取。 - golang中没有
SealedPublicKeyBox
原语。可以用 packagegolang.org/x/crypto/nacl/box
中的SealAnonymous
代替。
其实密码加密方案有很多基本的概念和原则,国外大厂也有很多规范的设计。只要不是一开始就想当然,其实是能找到或者设计出一个符合业务需求且安全高效的加密方案的。
特殊的一天,在老家楼上分析了一段有意思的代码,完整了方案的重构,还开着遥控车在农村撒欢越野。 What a beautiful day!