ggdgsdbsdbbb / two_factor.v
124 lines · 111 sloc · 2.97 KB · f69106d41622b949751c70d69da785cd02447f13
Raw
1// Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by a GPL license that can be found in the LICENSE file.
3module main
4
5import time
6import encoding.base32
7import encoding.binary
8import crypto.hmac
9import crypto.sha1
10import crypto.rand as crypto_rand
11
12struct TwoFactor {
13 id int @[primary; sql: serial]
14mut:
15 user_id int
16 secret string
17 is_enabled bool
18 created_at int
19}
20
21const totp_period = 30
22const totp_digits = 6
23const totp_issuer = 'Gitly'
24
25fn (mut app App) find_two_factor(user_id int) ?TwoFactor {
26 rows := sql app.db {
27 select from TwoFactor where user_id == user_id limit 1
28 } or { []TwoFactor{} }
29 if rows.len == 0 {
30 return none
31 }
32 return rows.first()
33}
34
35fn (mut app App) upsert_two_factor(user_id int, secret string, is_enabled bool) ! {
36 if existing := app.find_two_factor(user_id) {
37 id := existing.id
38 sql app.db {
39 update TwoFactor set secret = secret, is_enabled = is_enabled where id == id
40 }!
41 return
42 }
43 tf := TwoFactor{
44 user_id: user_id
45 secret: secret
46 is_enabled: is_enabled
47 created_at: int(time.now().unix())
48 }
49 sql app.db {
50 insert tf into TwoFactor
51 }!
52}
53
54fn (mut app App) delete_two_factor(user_id int) ! {
55 sql app.db {
56 delete from TwoFactor where user_id == user_id
57 }!
58}
59
60fn (mut app App) user_has_two_factor(user_id int) bool {
61 tf := app.find_two_factor(user_id) or { return false }
62 return tf.is_enabled
63}
64
65fn generate_totp_secret() string {
66 mut buf := []u8{len: 20}
67 for i in 0 .. buf.len {
68 buf[i] = u8(crypto_rand.int_u64(256) or { 0 })
69 }
70 enc := base32.encode_to_string(buf)
71 return enc.trim_right('=')
72}
73
74fn decode_base32_secret(secret string) ![]u8 {
75 mut padded := secret.to_upper().replace(' ', '')
76 for padded.len % 8 != 0 {
77 padded += '='
78 }
79 return base32.decode(padded.bytes())!
80}
81
82fn hotp(key []u8, counter u64) int {
83 mut buf := []u8{len: 8}
84 binary.big_endian_put_u64(mut buf, counter)
85 mac := hmac.new(key, buf, sha1.sum, sha1.block_size)
86 offset := int(mac[mac.len - 1] & 0x0f)
87 bin := ((u32(mac[offset]) & 0x7f) << 24) | ((u32(mac[offset + 1]) & 0xff) << 16) | ((u32(mac[
88 offset + 2]) & 0xff) << 8) | (u32(mac[offset + 3]) & 0xff)
89 mut modulus := u32(1)
90 for _ in 0 .. totp_digits {
91 modulus *= 10
92 }
93 return int(bin % modulus)
94}
95
96fn totp_code_for(secret string, t i64) !string {
97 key := decode_base32_secret(secret)!
98 counter := u64(t / totp_period)
99 code := hotp(key, counter)
100 mut s := code.str()
101 for s.len < totp_digits {
102 s = '0' + s
103 }
104 return s
105}
106
107fn verify_totp(secret string, code string) bool {
108 if code.len != totp_digits {
109 return false
110 }
111 now := time.now().unix()
112 for offset in [i64(-1), 0, 1] {
113 expected := totp_code_for(secret, now + offset * totp_period) or { continue }
114 if expected == code {
115 return true
116 }
117 }
118 return false
119}
120
121fn totp_provisioning_uri(username string, secret string) string {
122 label := '${totp_issuer}:${username}'
123 return 'otpauth://totp/${label}?secret=${secret}&issuer=${totp_issuer}&algorithm=SHA1&digits=${totp_digits}&period=${totp_period}'
124}
125