1diff --git a/main.go b/main.go
2index 918b7948a56dd1f13f744cf0965af8ffdae76e44..ab4aee90d096f54cc9235ea4b5e48c27a206774b 100644
3--- a/main.go
4+++ b/main.go
5@@ -2,8 +2,6 @@ package main
6
7 import (
8 "context"
9- "crypto/rand"
10- "encoding/base64"
11 "flag"
12 "fmt"
13 "log/slog"
14@@ -12,7 +10,6 @@ "os/signal"
15 "time"
16
17 "github.com/alecthomas/chroma/v2/styles"
18- "golang.org/x/crypto/bcrypt"
19
20 "git.gabrielgio.me/cerrado/pkg/config"
21 "git.gabrielgio.me/cerrado/pkg/handler"
22@@ -21,9 +18,6 @@ "git.gabrielgio.me/cerrado/pkg/worker"
23 )
24
25 func main() {
26- ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
27- defer stop()
28-
29 if len(os.Args) == 4 && os.Args[1] == "hash" {
30 err := hash(os.Args[2], os.Args[3])
31 if err != nil {
32@@ -42,36 +36,34 @@ }
33 return
34 }
35
36- if err := run(ctx); err != nil {
37+ if err := run(); err != nil {
38 slog.Error("Server", "error", err)
39 os.Exit(1)
40 }
41 }
42
43 func hash(username string, password string) error {
44- passphrase := fmt.Sprintf("%s:%s", username, password)
45- bytes, err := bcrypt.GenerateFromPassword([]byte(passphrase), 14)
46+ hash, err := service.GenerateHash(username, password)
47 if err != nil {
48 return err
49 }
50- fmt.Println(string(bytes))
51+ fmt.Println(hash)
52 return nil
53 }
54
55 func key() error {
56- key := make([]byte, 64)
57-
58- _, err := rand.Read(key)
59+ en, err := service.GenerateAesKey()
60 if err != nil {
61 return err
62 }
63-
64- en := base64.StdEncoding.EncodeToString(key)
65 fmt.Println(en)
66 return nil
67 }
68
69-func run(ctx context.Context) error {
70+func run() error {
71+ ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
72+ defer stop()
73+
74 configPath := flag.String("config", "/etc/cerrado.scfg", "File path for the configuration file")
75
76 flag.Parse()
77diff --git a/pkg/service/auth.go b/pkg/service/auth.go
78new file mode 100644
79index 0000000000000000000000000000000000000000..1fbf4b61724c079a9557bc675846ba94d1ad370e
80--- /dev/null
81+++ b/pkg/service/auth.go
82@@ -0,0 +1,117 @@
83+package service
84+
85+import (
86+ "bytes"
87+ "crypto/aes"
88+ "crypto/cipher"
89+ "crypto/rand"
90+ "encoding/base64"
91+ "fmt"
92+ "io"
93+
94+ "golang.org/x/crypto/bcrypt"
95+)
96+
97+type (
98+ AuthService struct {
99+ authRepository authRepository
100+ }
101+
102+ authRepository interface {
103+ GetPassphrase() []byte
104+ GetBase64AesKey() []byte
105+ }
106+)
107+
108+var tokenSeed = []byte("cerrado")
109+
110+func (a *AuthService) CheckAuth(username, password string) bool {
111+ passphrase := a.authRepository.GetPassphrase()
112+ pass := []byte(fmt.Sprintf("%s:%s", username, password))
113+
114+ err := bcrypt.CompareHashAndPassword(passphrase, pass)
115+
116+ return err == nil
117+}
118+
119+func (a *AuthService) IssueToken() ([]byte, error) {
120+ // TODO: do this block only once
121+ base := a.authRepository.GetBase64AesKey()
122+
123+ dbuf, err := base64.StdEncoding.DecodeString(string(base))
124+ if err != nil {
125+ return nil, err
126+ }
127+
128+ block, err := aes.NewCipher(dbuf)
129+ if err != nil {
130+ return nil, err
131+ }
132+
133+ gcm, err := cipher.NewGCM(block)
134+ if err != nil {
135+ return nil, err
136+ }
137+
138+ nonce := make([]byte, gcm.NonceSize())
139+ if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
140+ return nil, err
141+ }
142+
143+ ciphertext := gcm.Seal(nonce, nonce, tokenSeed, nil)
144+
145+ return ciphertext, nil
146+}
147+
148+func (a *AuthService) ValidateToken(token []byte) (bool, error) {
149+ base := a.authRepository.GetBase64AesKey()
150+
151+ dbuf, err := base64.StdEncoding.DecodeString(string(base))
152+ if err != nil {
153+ return false, err
154+ }
155+
156+ block, err := aes.NewCipher(dbuf)
157+ if err != nil {
158+ return false, err
159+ }
160+
161+ gcm, err := cipher.NewGCM(block)
162+ if err != nil {
163+ return false, err
164+ }
165+
166+ nonceSize := gcm.NonceSize()
167+ if len(token) < nonceSize {
168+ return false, fmt.Errorf("ciphertext too short")
169+ }
170+
171+ nonce, ciphertext := token[:nonceSize], token[nonceSize:]
172+ plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
173+ if err != nil {
174+ return false, err
175+ }
176+
177+ return bytes.Equal(tokenSeed, plaintext), nil
178+}
179+
180+func GenerateHash(username, password string) (string, error) {
181+ passphrase := fmt.Sprintf("%s:%s", username, password)
182+ bytes, err := bcrypt.GenerateFromPassword([]byte(passphrase), 14)
183+ if err != nil {
184+ return "", err
185+ }
186+
187+ return string(bytes), nil
188+}
189+
190+func GenerateAesKey() (string, error) {
191+ key := make([]byte, 32)
192+
193+ _, err := rand.Read(key)
194+ if err != nil {
195+ return "", err
196+ }
197+
198+ return base64.StdEncoding.EncodeToString(key), nil
199+}
200diff --git a/pkg/service/auth_test.go b/pkg/service/auth_test.go
201new file mode 100644
202index 0000000000000000000000000000000000000000..06bf76fad52fbd4ac6ba8671142a573aa4bd6dfc
203--- /dev/null
204+++ b/pkg/service/auth_test.go
205@@ -0,0 +1,119 @@
206+// go:build unit
207+
208+package service
209+
210+import (
211+ "testing"
212+)
213+
214+func TestCheck(t *testing.T) {
215+ testCases := []struct {
216+ name string
217+ passphrase []byte
218+ username string
219+ password string
220+ wantError bool
221+ }{
222+ {
223+ name: "generated",
224+ passphrase: nil,
225+ username: "gabrielgio",
226+ password: "adminadmin",
227+ wantError: false,
228+ },
229+ {
230+ name: "static",
231+ passphrase: []byte("$2a$14$W2yT0E6Zm8nTecqipHUQGOLC6PvNjIQqpQTW/MZmD5oqDfaBJnBV6"),
232+ username: "gabrielgio",
233+ password: "adminadmin",
234+ wantError: false,
235+ },
236+ {
237+ name: "error",
238+ passphrase: []byte("This is not a valid hash"),
239+ username: "gabrielgio",
240+ password: "adminadmin",
241+ wantError: true,
242+ },
243+ }
244+
245+ for _, tc := range testCases {
246+ t.Run(tc.name, func(t *testing.T) {
247+ mock := &mockAuthRepository{
248+ username: tc.username,
249+ password: tc.password,
250+ passphrase: tc.passphrase,
251+ }
252+
253+ service := AuthService{authRepository: mock}
254+
255+ if service.CheckAuth(tc.username, tc.password) == tc.wantError {
256+ t.Errorf("Invalid result, wanted %t got %t", tc.wantError, !tc.wantError)
257+ }
258+ })
259+ }
260+}
261+
262+func TestValidate(t *testing.T) {
263+ testCases := []struct {
264+ name string
265+ aesKey []byte
266+ }{
267+ {
268+ name: "generated",
269+ aesKey: nil,
270+ },
271+ {
272+ name: "static",
273+ aesKey: []byte("RTGkmunKmi5agh7jaqENunG2zI/godnkqhHaHyX/AVg="),
274+ },
275+ }
276+
277+ for _, tc := range testCases {
278+ t.Run(tc.name, func(t *testing.T) {
279+ mock := &mockAuthRepository{
280+ aesKey: tc.aesKey,
281+ }
282+
283+ service := AuthService{authRepository: mock}
284+
285+ token, err := service.IssueToken()
286+ if err != nil {
287+ t.Fatalf("Error issuing token: %s", err.Error())
288+ }
289+
290+ v, err := service.ValidateToken(token)
291+ if err != nil {
292+ t.Fatalf("Error validating token: %s", err.Error())
293+ }
294+
295+ if !v {
296+ t.Error("Invalid token generated")
297+ }
298+ })
299+ }
300+}
301+
302+type mockAuthRepository struct {
303+ username string
304+ password string
305+ passphrase []byte
306+
307+ aesKey []byte
308+}
309+
310+func (m *mockAuthRepository) GetPassphrase() []byte {
311+ if m.passphrase == nil {
312+ hash, _ := GenerateHash(m.username, m.password)
313+ m.passphrase = []byte(hash)
314+ }
315+ return m.passphrase
316+}
317+
318+func (m *mockAuthRepository) GetBase64AesKey() []byte {
319+ if m.aesKey == nil {
320+ key, _ := GenerateAesKey()
321+ m.aesKey = []byte(key)
322+ }
323+ return m.aesKey
324+}