]> git.lizzy.rs Git - go-anidb.git/blob - auth.go
Modernize
[go-anidb.git] / auth.go
1 package anidb
2
3 import (
4         "crypto/aes"
5         "crypto/cipher"
6         "crypto/rand"
7         "github.com/EliasFleckenstein03/go-anidb/udp"
8         "io"
9         "runtime"
10 )
11
12 // We still have the key and IV somewhere in memory...
13 // but it's better than plaintext.
14 type credentials struct {
15         username []byte
16         password []byte
17         udpKey   []byte
18 }
19
20 func (c *credentials) shred() {
21         if c != nil {
22                 io.ReadFull(rand.Reader, c.username)
23                 io.ReadFull(rand.Reader, c.password)
24                 io.ReadFull(rand.Reader, c.udpKey)
25                 c.username = nil
26                 c.password = nil
27                 c.udpKey = nil
28         }
29 }
30
31 // Randomly generated on every execution
32 var aesKey []byte
33
34 func init() {
35         aesKey = make([]byte, aes.BlockSize)
36         if _, err := io.ReadFull(rand.Reader, aesKey); err != nil {
37                 panic(err)
38         }
39 }
40
41 func crypt(plaintext string) []byte {
42         p := []byte(plaintext)
43
44         block, err := aes.NewCipher(aesKey)
45         if err != nil {
46                 panic(err)
47         }
48
49         ciphertext := make([]byte, len(p)+aes.BlockSize)
50         iv := ciphertext[:aes.BlockSize]
51         if _, err := io.ReadFull(rand.Reader, iv); err != nil {
52                 panic(err)
53         }
54
55         stream := cipher.NewCTR(block, iv)
56         stream.XORKeyStream(ciphertext[aes.BlockSize:], p)
57
58         return ciphertext
59 }
60
61 func decrypt(ciphertext []byte) string {
62         if len(ciphertext) <= aes.BlockSize {
63                 return ""
64         }
65         p := make([]byte, len(ciphertext)-aes.BlockSize)
66
67         block, err := aes.NewCipher(aesKey)
68         if err != nil {
69                 panic(err)
70         }
71
72         stream := cipher.NewCTR(block, ciphertext[:aes.BlockSize])
73         stream.XORKeyStream(p, ciphertext[aes.BlockSize:])
74
75         return string(p)
76 }
77
78 func newCredentials(username, password, udpKey string) *credentials {
79         return &credentials{
80                 username: crypt(username),
81                 password: crypt(password),
82                 udpKey:   crypt(udpKey),
83         }
84 }
85
86 func (udp *udpWrap) ReAuth() udpapi.APIReply {
87         if Banned() {
88                 return bannedReply
89         }
90
91         udp.credLock.Lock()
92         defer udp.credLock.Unlock()
93
94         if c := udp.credentials; c != nil {
95                 udp.logRequest(paramSet{cmd: "AUTH", params: paramMap{"user": decrypt(c.username)}})
96                 r := udp.AniDBUDP.Auth(
97                         decrypt(c.username),
98                         decrypt(c.password),
99                         decrypt(c.udpKey))
100                 udp.logReply(r)
101
102                 err := r.Error()
103
104                 if err != nil {
105                         switch r.Code() {
106                         // 555 -- banned
107                         // 601 -- server down, treat the same as a ban
108                         case 555, 601:
109                                 setBanned()
110                         case 500: // bad credentials
111                                 udp.credentials.shred()
112                                 udp.credentials = nil
113                         case 503, 504: // client rejected
114                                 panic(err)
115                         }
116                 }
117                 udp.connected = err == nil
118
119                 if udp.connected {
120                         if user := UserByName(decrypt(c.username)); user != nil {
121                                 udp.user = user
122                         } else {
123                                 // We can't use SendRecv here as it would deadlock
124                                 ch := make(chan udpapi.APIReply, 1)
125                                 udp.sendQueueCh <- paramSet{
126                                         cmd:    "USER",
127                                         params: paramMap{"user": decrypt(c.username)},
128                                         ch:     ch,
129                                 }
130                                 reply := <-ch
131
132                                 if reply != nil {
133                                         uid, _ := parseUserReply(reply)
134                                         udp.user = uid.User()
135                                 }
136                         }
137                 }
138
139                 runtime.GC()
140                 return r
141         }
142         return &noauthAPIReply{}
143 }
144
145 // Saves the used credentials in the AniDB struct, to allow automatic
146 // re-authentication when needed; they are (properly) encrypted with a key that's
147 // uniquely generated every time the module is initialized.
148 func (adb *AniDB) SetCredentials(username, password, udpKey string) {
149         adb.udp.credLock.Lock()
150         defer adb.udp.credLock.Unlock()
151
152         adb.udp.credentials.shred()
153         adb.udp.credentials = newCredentials(username, password, udpKey)
154 }
155
156 // Authenticates to anidb's UDP API and, on success, stores the credentials using
157 // SetCredentials. If udpKey is not "", the communication with the server
158 // will be encrypted, but in the VERY weak ECB mode.
159 func (adb *AniDB) Auth(username, password, udpKey string) (err error) {
160         defer runtime.GC() // any better way to clean the plaintexts?
161
162         adb.udp.sendLock.Lock()
163         defer adb.udp.sendLock.Unlock()
164
165         if !Banned() {
166                 adb.SetCredentials(username, password, udpKey)
167         }
168
169         // ReAuth clears the credentials if they're bad
170         return adb.udp.ReAuth().Error()
171 }
172
173 // Logs the user out and removes the credentials from the AniDB struct.
174 func (adb *AniDB) Logout() error {
175         adb.udp.credLock.Lock()
176         defer adb.udp.credLock.Unlock()
177
178         adb.udp.credentials.shred()
179         adb.udp.credentials = nil
180
181         adb.udp.sendLock.Lock()
182         defer adb.udp.sendLock.Unlock()
183
184         adb.udp.user = nil
185
186         if adb.udp.connected {
187                 adb.udp.connected = false
188                 adb.udp.logRequest(paramSet{cmd: "LOGOUT"})
189                 return adb.udp.Logout()
190         }
191         return nil
192 }