双因素认证
电报 Telegram 使用安全远程密码协议版本 6a 实现双因素认证。
示例实现:tdlib。
用SRP检查密码
要登录带有2FA密码保护的账户或执行其他操作(如更改频道所有者),你需要验证用户对当前2FA账户密码的了解。
为此,客户端首先需要获取SRP参数和KDF算法,通过account.getPassword方法检查密码的有效性。目前,只有密码KdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow算法被支持,所以我们只会解释这个。
然后,在用户提供密码后,客户端应使用SRP和如下所示的特定KDF算法生成InputCheckPasswordSRP对象,并将其传递给相应的方法(例如授权时使用auth.checkPassword)。
该SRP协议扩展使用基于密码的PBKDF2,使用sha512()进行100000次迭代。 PBKDF2 还用于额外重写参数,采用类似于 RFC 2945 中描述的方法(代替 )(见下文)。PBKDF2HMACSHA512iter100000xH(s | H ( I | password | I) | s)H(s | H ( I | ":" | password)
这里, 表示串联, 表示算术算子 。 在所有将数字串接到哈希函数的情况下,数字必须以大端序形式使用,填充至2048位;所有数学都是模的。 取代 ,将使用 (参见 SRP 协议)。 取代 ,将使用 (参见 SRP 协议)。|++pIsalt1ssalt2
主要的哈希函数是 sha256:H
- H(data) := sha256(data)
加盐哈希函数定义如下:SH
- SH(data, salt) := H(salt | data | salt)
主要密码哈希函数定义如下:
- PH1(password, salt1, salt2) := SH(SH(password, salt1), salt2)
次级密码哈希函数定义如下:
- PH2(password, salt1, salt2) := SH(pbkdf2(sha512, PH1(password, salt1, salt2), salt1, 100000), salt2)
客户端方面,以下参数从account.password对象中的passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow对象中提取。
-
g := algo.g
-
p := algo.p客户端需要检查p是否是安全的2048位素数(即p和(p-1)/2都是素数,且g生成一个素数阶为(p-1)/2的循环子群,即是模p的二次剩余。由于g总是等于 2、3、4、5、6 或 7,这可以通过二次互反律轻松实现,给出一个简单的条件——即p 模 8 = 7,当g = 2;p 模 3 = 2,表示g = 3;g= 4没有额外条件;p 模 5 = 1 或 4,g= 5;p 模 24 = 19 或 23,表示g = 6;p模 7 = 3、5 或 6,g= 7。客户端检查完g和p后,缓存结果是合理的,以避免未来重复冗长的计算。该缓存可能与用于授权密钥生成的缓存共享。2^2047 < p < 2^2048
如果客户端的随机数生成器不够完善,使用account.password的secure_random作为额外种子是合理的。
-
password := (user-provided password)
-
salt1 := algo.salt1
-
salt2 := algo.salt2
-
g_b := srp_Bsrp_B和 都从account.password对象中提取出来。srp_id
参数在客户端和服务器端均可生成:k
- k := H(p | g)
共享参数被生成:客户端执行此操作,服务器对“我们稍后发送他”同样操作(见下文)ug_a
- u := H(g_a | g_b)
最终参数仅在客户端生成:
- x := PH2(password, salt1, salt2)
- v := pow(g, x) mod p
服务器已经有 ,从我们设置密码开始。v
对于商品,最终生成一个共享参数:
- k_v := (k * v) mod p
最后,密钥交换过程开始于双方。
客户端计算一个2048位的数字a(使用足够的熵或服务器的随机数;见上文),并生成:
- g_a := pow(g, a) mod p.
服务器利用足够的熵计算出一个2048位的数字b,并生成发送给我们的参数(见上文)。g_b
- g_b := (k_v + (pow(g, b) mod p)) mod p
最后,生成SRP会话密钥:
客户端:
- t := (g_b - k_v) mod p(模正值,若结果为负增量,则为p)
- s_a := pow(t, a + u * x) mod p
- k_a := H(s_a)
服务器端:
- s_b := pow(g_a * (pow(v, u) mod p), b) mod p
- k_b := H(s_b)
自从:
- g_b := (k_v + (pow(g, b) mod p)) mod p
- t := (g_b - k_v) mod p
- t := ((k_v + (pow(g, b) mod p)) - k_v) mod p
- t := pow(g, b) mod p
- s_a := pow(t, a + u * x) mod p
- s_a := pow(pow(g, b) mod p, a + u * x) mod p
并且:
-
g_a := pow(g, a) mod p
-
v := pow(g, x) mod p
-
s_b := pow(g_a * (pow(v, u) mod p), b) mod p
-
s_b := pow((pow(g, a) mod p) * (pow(pow(g, x) mod p, u) mod p), b) mod p
-
s_b := pow(pow(g, a + x * u) mod p, b) mod p
-
s_b := pow(pow(g, b) mod p, a + u * x) mod p
-
s_a := pow(pow(g, b) mod p, a + u * x) mod p
这意味着:
- s_b === s_a
- k_b === k_a
最后,根据SRP规定:
- M1 := H(H(p) xor H(g) | H(salt1) | H(salt2) | g_a | g_b | k_a)
M1与(作为参数)以及从account.password对象提取的 ,一起传递给inputCheckPasswordSRP。g_aAsrp_id
服务器随后计算:
- M2 := H(H(p) xor H(g) | H(salt1) | H(salt2) | g_a | g_b | k_b)
既然我们说了这个:
- s_b === s_a
- k_b === k_a
这意味着,如果一切都做得正确,
- M1 === M2
如果密码不正确,将返回400 PASSWORD_HASH_INVALID。
设置新的2FA密码
要设置新的双重身份验证密码,请使用account.updatePasswordSettings方法。
如果密码已经设置好,按照使用 SRP 检查密码的方式生成 InputCheckPasswordSRP 对象,并将其插入 account.updatePasswordSettings 方法的字段。
要移除当前密码,请在账户中传递空密码。PasswordInputSettings 对象。passwordnew_password_hash
要设置新密码,生成字段时使用SRP参数和account.getPassword获得的KDF算法。 然后用 中指定的 KDF 算法生成新字节,只需先在 中附加 32 个足够随机的字节。 按照使用SRP检查密码的方式,只需在参数生成时停止,并用作:passwordnew_password_hashnew_settingssalt1vnew_password_hash
- v := pow(g, x) mod p
像往常一样,大端序形式,填充到2048位。
电子邮件验证
在设置双重授权时,建议设置恢复邮箱,以便用户忘记密码时通过邮箱地址恢复密码。
要设置恢复邮箱,必须先验证。 这可以在使用 account.updatePasswordSettings 设置新密码时,直接通过在 account.passwordInputSettings 构造函数中设置 email 参数和标志来实现。 如果邮件未被验证,将返回EMAIL_UNCONFIRMED_X 400错误,其中X是刚刚发送到邮件的验证码长度。 使用 account.confirmPasswordEmail 输入收到的验证码并启用恢复邮箱。 请使用account.resendPasswordEmail重新发送验证码。 请使用account.cancelPasswordEmail取消验证码。
要获取当前的恢复邮箱,请使用account.getPasswordSettings。
密码恢复
如果用户忘记了自己的2FA密码,以下恢复选项可供选择:
- 仅登录会话:密码重置 »
- 已登录与未登录会话:电子邮件恢复 »
- 未登录会话:账户删除 »
密码重置
密码重置只能从已登录会话请求。
以下步骤可用于重置密码而不删除账户:
account.resetPasswordFailedWait#e3779861 retry_date:int = account.ResetPasswordResult; account.resetPasswordRequestedWait#e9effc7d until_date:int = account.ResetPasswordResult; account.resetPasswordOk#e926d63e = account.ResetPasswordResult; ---functions--- account.resetPassword#9308ce1b = account.ResetPasswordResult; account.declinePasswordReset#4c9409f6 = Bool;
如果用户已经登录且忘记了双重身份验证密码,可以使用 account.resetPassword 发起密码重置。
成功后,调用最初会返回 account.resetPasswordRequestedWait 构造子,并启动一个为期 7 天的服务器端计时器,在此期间用户可以通过 Telegram 服务账户发送的按钮或直接在界面内使用 account.declinePasswordReset 终止重置过程。
当时间到来时,再次调用account.resetPassword,返回account.resetPasswordOk,表示密码已成功重置。
如果用户最近请求密码重置但被取消,初始 account.resetPassword 调用会返回 account.resetPasswordFailedWait,且必须等待指定日期后再请求一次重置。
请注意,如果用户已经知道自己的2FA密码,并且仅仅想禁用2FA,则禁用密码时也必须使用同样的流程。”
电子邮件恢复
电子邮件恢复可以从已登录的会话请求,如果用户成功提供登录码,也可以从未登录的会话请求。
无论哪种情况,账户都必须有一个关联的恢复邮箱。”
要恢复遗忘的双重认证密码,必须通过 auth.requestPasswordRecovery 方法发送邮件到之前指定的地址。
使用auth.checkRecoveryPassword确认用户提供了有效的验证码。
然后用auth.recoverPassword和收到的验证码删除当前的双重验证密码,设置新的密码请按照以下步骤操作。
账户删除
如果用户成功提供了登录码,但忘记了双重验证密码,且无法访问其他登录会话,可以按照以下步骤删除该账户。
使用2FA密码
API中的多种方法,如支付、通道所有权转移等,要求用户使用作为InputCheckPasswordSRP构造函数传递的2FA密码进行认证,该构造函数按上述规定生成。
所有需要登录后验证密码的方法都可能出现以下RPC错误:
- PASSWORD_MISSING- 不配置双重认证密码,但调用该方法需要设置一个。设置一个双重认证密码,然后重复方法调用。
- PASSWORD_TOO_FRESH_%d- 2FA密码不到24小时前修改,几秒钟后再试。%d
- SESSION_TOO_FRESH_%d- 本次会话创建不到24小时,请在几秒钟内重试。%d
- PASSWORD_HASH_INVALID- 指定的密码无效(或提供了 inputCheckPasswordEmpty,但需要两步验证密码)。
调用此类方法的通常流程是先调用通过输入CheckPasswordEmpty的方法(假如未配置密码,即使实际上已配置);然后,根据返回的RPC错误,继续执行:
- PASSWORD_HASH_INVALID- 2FA密码插入流程,使用用户提供的密码重新调用方法
- PASSWORD_MISSING- 进行2FA密码设置流程,然后用新提供的密码重新调用该方法
- 所有其他RPC错误——显示错误信息及错误描述
该流程有助于避免与当前登录的其他会话发生可能更改密码的竞态条件。