user_store.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. package state
  2. import (
  3. "bytes"
  4. "crypto/md5"
  5. "database/sql"
  6. "embed"
  7. "errors"
  8. "fmt"
  9. "io"
  10. "io/fs"
  11. "net/http"
  12. "time"
  13. "github.com/mk6i/retro-aim-server/wire"
  14. "github.com/golang-migrate/migrate/v4"
  15. "github.com/golang-migrate/migrate/v4/database/sqlite3"
  16. "github.com/golang-migrate/migrate/v4/source/httpfs"
  17. "github.com/google/uuid"
  18. _ "github.com/mattn/go-sqlite3"
  19. )
  20. // BlockedState represents the blocked status between two users
  21. type BlockedState int
  22. //go:embed migrations/*
  23. var migrations embed.FS
  24. var (
  25. // ErrDupUser indicates that a user already exists.
  26. ErrDupUser = errors.New("user already exists")
  27. // ErrNoUser indicates that a user does not exist.
  28. ErrNoUser = errors.New("user does not exist")
  29. )
  30. const (
  31. // BlockedNo indicates that neither user blocks the other.
  32. BlockedNo BlockedState = iota
  33. // BlockedA indicates that user A blocks user B.
  34. BlockedA
  35. // BlockedB indicates that user B blocks user A.
  36. BlockedB
  37. )
  38. // User represents a user account.
  39. type User struct {
  40. // ScreenName is the AIM screen name.
  41. ScreenName string `json:"screen_name"`
  42. // AuthKey is the salt for the MD5 password hash.
  43. AuthKey string `json:"-"`
  44. // StrongMD5Pass is the MD5 password hash format used by AIM v4.8-v5.9.
  45. StrongMD5Pass []byte `json:"-"`
  46. // WeakMD5Pass is the MD5 password hash format used by AIM v3.5-v4.7. This
  47. // hash is used to authenticate roasted passwords for AIM v1.0-v3.0.
  48. WeakMD5Pass []byte `json:"-"`
  49. }
  50. // ValidateHash checks if md5Hash is identical to one of the password hashes.
  51. func (u *User) ValidateHash(md5Hash []byte) bool {
  52. return bytes.Equal(u.StrongMD5Pass, md5Hash) || bytes.Equal(u.WeakMD5Pass, md5Hash)
  53. }
  54. // ValidateRoastedPass checks if the provided roasted password matches the MD5
  55. // hash of the user's actual password. A roasted password is a XOR-obfuscated
  56. // form of the real password, intended to add a simple layer of security.
  57. func (u *User) ValidateRoastedPass(roastedPass []byte) bool {
  58. var roastTable = [16]byte{
  59. 0xF3, 0x26, 0x81, 0xC4, 0x39, 0x86, 0xDB, 0x92,
  60. 0x71, 0xA3, 0xB9, 0xE6, 0x53, 0x7A, 0x95, 0x7C,
  61. }
  62. clearPass := make([]byte, len(roastedPass))
  63. for i := range roastedPass {
  64. clearPass[i] = roastedPass[i] ^ roastTable[i%len(roastTable)]
  65. }
  66. md5Hash := weakMD5PasswordHash(string(clearPass), u.AuthKey) // todo remove string conversion
  67. return bytes.Equal(u.WeakMD5Pass, md5Hash)
  68. }
  69. // HashPassword computes MD5 hashes of the user's password. It computes both
  70. // weak and strong variants and stores them in the struct.
  71. func (u *User) HashPassword(passwd string) error {
  72. u.WeakMD5Pass = weakMD5PasswordHash(passwd, u.AuthKey)
  73. u.StrongMD5Pass = strongMD5PasswordHash(passwd, u.AuthKey)
  74. return nil
  75. }
  76. //goland:noinspection GoUnhandledErrorResult
  77. func weakMD5PasswordHash(pass, authKey string) []byte {
  78. hash := md5.New()
  79. io.WriteString(hash, authKey)
  80. io.WriteString(hash, pass)
  81. io.WriteString(hash, "AOL Instant Messenger (SM)")
  82. return hash.Sum(nil)
  83. }
  84. //goland:noinspection GoUnhandledErrorResult
  85. func strongMD5PasswordHash(pass, authKey string) []byte {
  86. top := md5.New()
  87. io.WriteString(top, pass)
  88. bottom := md5.New()
  89. io.WriteString(bottom, authKey)
  90. bottom.Write(top.Sum(nil))
  91. io.WriteString(bottom, "AOL Instant Messenger (SM)")
  92. return bottom.Sum(nil)
  93. }
  94. // SQLiteUserStore stores user feedbag (buddy list), profile, and
  95. // authentication credentials information in a SQLite database.
  96. type SQLiteUserStore struct {
  97. db *sql.DB
  98. }
  99. // NewSQLiteUserStore creates a new instance of SQLiteUserStore. If the
  100. // database does not already exist, a new one is created with the required
  101. // schema.
  102. func NewSQLiteUserStore(dbFilePath string) (*SQLiteUserStore, error) {
  103. db, err := sql.Open("sqlite3", dbFilePath)
  104. if err != nil {
  105. return nil, err
  106. }
  107. store := &SQLiteUserStore{db: db}
  108. return store, store.runMigrations()
  109. }
  110. func (f SQLiteUserStore) runMigrations() error {
  111. migrationFS, err := fs.Sub(migrations, "migrations")
  112. if err != nil {
  113. return fmt.Errorf("failed to prepare migration subdirectory: %v", err)
  114. }
  115. sourceInstance, err := httpfs.New(http.FS(migrationFS), ".")
  116. if err != nil {
  117. return fmt.Errorf("failed to create source instance from embedded filesystem: %v", err)
  118. }
  119. driver, err := sqlite3.WithInstance(f.db, &sqlite3.Config{})
  120. if err != nil {
  121. return fmt.Errorf("cannot create database driver: %v", err)
  122. }
  123. m, err := migrate.NewWithInstance("httpfs", sourceInstance, "sqlite3", driver)
  124. if err != nil {
  125. return fmt.Errorf("failed to create migrate instance: %v", err)
  126. }
  127. if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
  128. return fmt.Errorf("failed to run migrations: %v", err)
  129. }
  130. return nil
  131. }
  132. // AllUsers returns all stored users. It only populates the User.ScreenName field
  133. // populated in the returned slice.
  134. func (f SQLiteUserStore) AllUsers() ([]User, error) {
  135. q := `SELECT screenName FROM user`
  136. rows, err := f.db.Query(q)
  137. if err != nil {
  138. return nil, err
  139. }
  140. defer rows.Close()
  141. var users []User
  142. for rows.Next() {
  143. u := User{}
  144. if err := rows.Scan(&u.ScreenName); err != nil {
  145. return nil, err
  146. }
  147. users = append(users, u)
  148. }
  149. if err := rows.Err(); err != nil {
  150. return nil, err
  151. }
  152. return users, nil
  153. }
  154. // User looks up a user by screen name. It populates the User record with
  155. // credentials that can be used to validate the user's password.
  156. func (f SQLiteUserStore) User(screenName string) (*User, error) {
  157. q := `
  158. SELECT
  159. screenName,
  160. authKey,
  161. weakMD5Pass,
  162. strongMD5Pass
  163. FROM user
  164. WHERE screenName = ?
  165. `
  166. u := &User{}
  167. err := f.db.QueryRow(q, screenName).Scan(&u.ScreenName, &u.AuthKey, &u.WeakMD5Pass, &u.StrongMD5Pass)
  168. if errors.Is(err, sql.ErrNoRows) {
  169. return nil, nil
  170. }
  171. return u, err
  172. }
  173. // InsertUser inserts a user to the store. Return ErrDupUser if a user with the
  174. // same screen name already exists.
  175. func (f SQLiteUserStore) InsertUser(u User) error {
  176. q := `
  177. INSERT INTO user (screenName, authKey, weakMD5Pass, strongMD5Pass)
  178. VALUES (?, ?, ?, ?)
  179. ON CONFLICT (screenName) DO NOTHING
  180. `
  181. result, err := f.db.Exec(q, u.ScreenName, u.AuthKey, u.WeakMD5Pass, u.StrongMD5Pass)
  182. if err != nil {
  183. return err
  184. }
  185. rowsAffected, err := result.RowsAffected()
  186. if err != nil {
  187. return err
  188. }
  189. if rowsAffected == 0 {
  190. return ErrDupUser
  191. }
  192. return nil
  193. }
  194. // SetUserPassword sets the user's password hashes and auth key.
  195. func (f SQLiteUserStore) SetUserPassword(u User) error {
  196. tx, err := f.db.Begin()
  197. if err != nil {
  198. return err
  199. }
  200. defer func() {
  201. if err != nil {
  202. tx.Rollback()
  203. }
  204. }()
  205. q := `
  206. UPDATE user
  207. SET authKey = ?, weakMD5Pass = ?, strongMD5Pass = ?
  208. WHERE screenName = ?
  209. `
  210. result, err := tx.Exec(q, u.AuthKey, u.WeakMD5Pass, u.StrongMD5Pass, u.ScreenName)
  211. if err != nil {
  212. return err
  213. }
  214. rowsAffected, err := result.RowsAffected()
  215. if err != nil {
  216. return err
  217. }
  218. if rowsAffected == 0 {
  219. // it's possible the user didn't change OR the user doesn't exist.
  220. // check if the user exists.
  221. var exists int
  222. err = tx.QueryRow("SELECT COUNT(*) FROM user WHERE screenName = ?", u.ScreenName).Scan(&exists)
  223. if err != nil {
  224. return err // Handle possible SQL errors during the select
  225. }
  226. if exists == 0 {
  227. return ErrNoUser // User does not exist
  228. }
  229. }
  230. return tx.Commit()
  231. }
  232. // Feedbag fetches the contents of a user's feedbag (buddy list).
  233. func (f SQLiteUserStore) Feedbag(screenName string) ([]wire.FeedbagItem, error) {
  234. q := `
  235. SELECT
  236. groupID,
  237. itemID,
  238. classID,
  239. name,
  240. attributes
  241. FROM feedbag
  242. WHERE screenName = ?
  243. `
  244. rows, err := f.db.Query(q, screenName)
  245. if err != nil {
  246. return nil, err
  247. }
  248. defer rows.Close()
  249. var items []wire.FeedbagItem
  250. for rows.Next() {
  251. var item wire.FeedbagItem
  252. var attrs []byte
  253. if err := rows.Scan(&item.GroupID, &item.ItemID, &item.ClassID, &item.Name, &attrs); err != nil {
  254. return nil, err
  255. }
  256. if err := wire.Unmarshal(&item.TLVLBlock, bytes.NewBuffer(attrs)); err != nil {
  257. return items, err
  258. }
  259. items = append(items, item)
  260. }
  261. return items, nil
  262. }
  263. // FeedbagLastModified returns the last time a user's feedbag (buddy list) was
  264. // updated.
  265. func (f SQLiteUserStore) FeedbagLastModified(screenName string) (time.Time, error) {
  266. var lastModified sql.NullInt64
  267. q := `SELECT MAX(lastModified) FROM feedbag WHERE screenName = ?`
  268. err := f.db.QueryRow(q, screenName).Scan(&lastModified)
  269. return time.Unix(lastModified.Int64, 0), err
  270. }
  271. // FeedbagDelete deletes an entry from a user's feedbag (buddy list).
  272. func (f SQLiteUserStore) FeedbagDelete(screenName string, items []wire.FeedbagItem) error {
  273. // todo add transaction
  274. q := `DELETE FROM feedbag WHERE screenName = ? AND itemID = ?`
  275. for _, item := range items {
  276. if _, err := f.db.Exec(q, screenName, item.ItemID); err != nil {
  277. return err
  278. }
  279. }
  280. return nil
  281. }
  282. // FeedbagUpsert upserts an entry to a user's feedbag (buddy list). An entry is
  283. // created if it doesn't already exist, or modified if it already exists.
  284. func (f SQLiteUserStore) FeedbagUpsert(screenName string, items []wire.FeedbagItem) error {
  285. q := `
  286. INSERT INTO feedbag (screenName, groupID, itemID, classID, name, attributes, lastModified)
  287. VALUES (?, ?, ?, ?, ?, ?, UNIXEPOCH())
  288. ON CONFLICT (screenName, groupID, itemID)
  289. DO UPDATE SET classID = excluded.classID,
  290. name = excluded.name,
  291. attributes = excluded.attributes,
  292. lastModified = UNIXEPOCH()
  293. `
  294. for _, item := range items {
  295. buf := &bytes.Buffer{}
  296. if err := wire.Marshal(item.TLVLBlock, buf); err != nil {
  297. return err
  298. }
  299. _, err := f.db.Exec(q,
  300. screenName,
  301. item.GroupID,
  302. item.ItemID,
  303. item.ClassID,
  304. item.Name,
  305. buf.Bytes())
  306. if err != nil {
  307. return err
  308. }
  309. }
  310. return nil
  311. }
  312. // AdjacentUsers returns all users who have screenName in their buddy list.
  313. // Exclude users who are on screenName's block list.
  314. func (f SQLiteUserStore) AdjacentUsers(screenName string) ([]string, error) {
  315. q := `
  316. SELECT f.screenName
  317. FROM feedbag f
  318. WHERE f.name = ?
  319. AND f.classID = 0
  320. -- Don't show screenName that its blocked buddy is online
  321. AND NOT EXISTS(SELECT 1 FROM feedbag WHERE screenName = ? AND name = f.screenName AND classID = 3)
  322. -- Don't show blocked buddy that screenName is online
  323. AND NOT EXISTS(SELECT 1 FROM feedbag WHERE screenName = f.screenName AND name = f.name AND classID = 3)
  324. `
  325. rows, err := f.db.Query(q, screenName, screenName, screenName)
  326. if err != nil {
  327. return nil, err
  328. }
  329. defer rows.Close()
  330. var items []string
  331. for rows.Next() {
  332. var sn string
  333. if err := rows.Scan(&sn); err != nil {
  334. return nil, err
  335. }
  336. items = append(items, sn)
  337. }
  338. return items, nil
  339. }
  340. // Buddies returns all user's buddies. Don't return a buddy if the user has
  341. // them on their block list.
  342. func (f SQLiteUserStore) Buddies(screenName string) ([]string, error) {
  343. q := `
  344. SELECT f.name
  345. FROM feedbag f
  346. WHERE f.screenName = ? AND f.classID = 0
  347. -- Don't include buddy if they blocked screenName
  348. AND NOT EXISTS(SELECT 1 FROM feedbag WHERE screenName = f.name AND name = ? AND classID = 3)
  349. -- Don't include buddy if screen name blocked them
  350. AND NOT EXISTS(SELECT 1 FROM feedbag WHERE screenName = ? AND name = f.name AND classID = 3)
  351. `
  352. rows, err := f.db.Query(q, screenName, screenName, screenName)
  353. if err != nil {
  354. return nil, err
  355. }
  356. defer rows.Close()
  357. var items []string
  358. for rows.Next() {
  359. var sn string
  360. if err := rows.Scan(&sn); err != nil {
  361. return nil, err
  362. }
  363. items = append(items, sn)
  364. }
  365. return items, nil
  366. }
  367. // BlockedState returns the BlockedState between two users.
  368. func (f SQLiteUserStore) BlockedState(screenNameA, screenNameB string) (BlockedState, error) {
  369. q := `
  370. SELECT EXISTS(SELECT 1
  371. FROM feedbag f
  372. WHERE f.classID = 3
  373. AND f.screenName = ?
  374. AND f.name = ?)
  375. UNION ALL
  376. SELECT EXISTS(SELECT 1
  377. FROM feedbag f
  378. WHERE f.classID = 3
  379. AND f.screenName = ?
  380. AND f.name = ?)
  381. `
  382. row, err := f.db.Query(q, screenNameA, screenNameB, screenNameB, screenNameA)
  383. if err != nil {
  384. return BlockedNo, err
  385. }
  386. defer row.Close()
  387. var blockedA bool
  388. if row.Next() {
  389. if err := row.Scan(&blockedA); err != nil {
  390. return BlockedNo, err
  391. }
  392. }
  393. var blockedB bool
  394. if row.Next() {
  395. if err := row.Scan(&blockedB); err != nil {
  396. return BlockedNo, err
  397. }
  398. }
  399. switch {
  400. case blockedA:
  401. return BlockedA, nil
  402. case blockedB:
  403. return BlockedB, nil
  404. default:
  405. return BlockedNo, nil
  406. }
  407. }
  408. // Profile fetches a user profile. Return empty string if the user
  409. // does not exist or has no profile.
  410. func (f SQLiteUserStore) Profile(screenName string) (string, error) {
  411. q := `
  412. SELECT IFNULL(body, '')
  413. FROM profile
  414. WHERE screenName = ?
  415. `
  416. var profile string
  417. err := f.db.QueryRow(q, screenName).Scan(&profile)
  418. if err != nil && !errors.Is(err, sql.ErrNoRows) {
  419. return "", err
  420. }
  421. return profile, nil
  422. }
  423. // SetProfile sets the text contents of a user's profile.
  424. func (f SQLiteUserStore) SetProfile(screenName string, body string) error {
  425. q := `
  426. INSERT INTO profile (screenName, body)
  427. VALUES (?, ?)
  428. ON CONFLICT (screenName)
  429. DO UPDATE SET body = excluded.body
  430. `
  431. _, err := f.db.Exec(q, screenName, body)
  432. return err
  433. }
  434. func (f SQLiteUserStore) BARTUpsert(itemHash []byte, body []byte) error {
  435. q := `
  436. INSERT INTO bartItem (hash, body)
  437. VALUES (?, ?)
  438. ON CONFLICT DO NOTHING
  439. `
  440. _, err := f.db.Exec(q, itemHash, body)
  441. return err
  442. }
  443. func (f SQLiteUserStore) BARTRetrieve(hash []byte) ([]byte, error) {
  444. q := `
  445. SELECT body
  446. FROM bartItem
  447. WHERE hash = ?
  448. `
  449. var body []byte
  450. err := f.db.QueryRow(q, hash).Scan(&body)
  451. if errors.Is(err, sql.ErrNoRows) {
  452. err = nil
  453. }
  454. return body, nil
  455. }
  456. // NewStubUser creates a new user with canned credentials. The default password
  457. // is "welcome1". This is typically used for development purposes.
  458. func NewStubUser(screenName string) (User, error) {
  459. uid, err := uuid.NewRandom()
  460. if err != nil {
  461. return User{}, err
  462. }
  463. u := User{
  464. ScreenName: screenName,
  465. AuthKey: uid.String(),
  466. }
  467. err = u.HashPassword("welcome1")
  468. return u, err
  469. }