tAdd tokens verification to submit flags - scoreboard - Interactive scoreboard for CTF-like games
 (HTM) git clone git://git.z3bra.org/scoreboard.git
 (DIR) Log
 (DIR) Files
 (DIR) Refs
       ---
 (DIR) commit 02405c1d3835605e574f366f6b4fea04dcfae822
 (DIR) parent d4209a84c73bef28336b2f1f63b39181f7dfc8da
 (HTM) Author: Willy Goiffon <contact@z3bra.org>
       Date:   Wed,  7 Dec 2022 09:01:10 +0100
       
       Add tokens verification to submit flags
       
       Diffstat:
         M db.go                               |      51 ++++++++-----------------------
         M go.mod                              |       1 +
         M go.sum                              |       1 +
         M main.go                             |      37 +++++++++++++++++++++++++------
         M player.go                           |     105 +++++++++++++++++++++++--------
         M playerbox.go                        |       9 ++++++---
         M ui.go                               |      23 ++++++++++++++++-------
         D util.go                             |      49 -------------------------------
       
       8 files changed, 147 insertions(+), 129 deletions(-)
       ---
 (DIR) diff --git a/db.go b/db.go
       t@@ -22,33 +22,14 @@ import (
        const (
                // DB queries
                DB_CREATE string = `
       -        CREATE TABLE IF NOT EXISTS score(id INTEGER PRIMARY KEY, name TEXT, token TEXT, flag INT, score INT, ts INT);
       -        `
       -        DB_FILL string = `
       -        DELETE FROM score;
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('WGS', 'token0', 3,  302,   2);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('DQK', 'token1', 5, 1337,  20);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('VNM', 'token2', 5, 1000,   3);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('PLR', 'token3', 5, 1000,   2);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('UKN', 'token4', 2,  200, 200);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('JFK', 'token5', 3,  300, 200);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('AAA', 'token6', 1,  130, 1670260535);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('BBB', 'token7', 4,  400, 1670260535);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('CCC', 'token8', 4,  400, 1670260535);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('DDD', 'token9', 4,  407, 1670260535);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('EEE', 'tokenA', 4,  405, 1670260535);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('FFF', 'tokenB', 4,  401, 1670260535);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('GGG', 'tokenC', 4,  406, 1670260535);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('HHH', 'tokenD', 4,  408, 1670260445);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('III', 'tokenE', 4,  409, 1670260435);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('JJJ', 'tokenF', 4,  402, 1670260535);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('KKK', 'tokenG', 4,  400, 1670260535);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('LLL', 'tokenH', 4,  404, 1670260535);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('MMM', 'tokenI', 4,  403, 1670260535);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('NNN', 'tokenJ', 4,  400, 1670260535);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('OOO', 'tokenK', 4,  400, 1670260535);
       -        INSERT INTO score(name,token,flag,score,ts) VALUES ('PPP', 'tokenL', 4,  400, 1670260535);
       -        `
       +        CREATE TABLE IF NOT EXISTS
       +          score(
       +            hash TEXT,
       +            name TEXT,
       +            flag INT,
       +            score INT,
       +            ts INT
       +        );`
        )
        
        
       t@@ -67,18 +48,12 @@ func db_init(file string) (*sql.DB, error) {
                if err != nil {
                        return nil, err
                }
       -        ///*
       -        _, err = db.Exec(DB_FILL)
       -        if err != nil {
       -                panic(err)
       -        }
       -        //*/
                return db, nil
        }
        
        func db_count(db *sql.DB) int {
                var count int
       -        query := `SELECT count(id) FROM score;`
       +        query := `SELECT count(*) FROM score;`
                row := db.QueryRow(query)
                row.Scan(&count)
                return count
       t@@ -102,7 +77,7 @@ func db_score_count(db *sql.DB, score, ts int) int {
        func db_flag_count(db *sql.DB, flag int) int {
                var count int
                query := `SELECT
       -          count(id)
       +          count(*)
                  FROM score
                  WHERE
                    flag >= ?
       t@@ -116,7 +91,7 @@ func db_flag_count(db *sql.DB, flag int) int {
        func db_id(db *sql.DB, nick string) bool {
                var count int
                query := `SELECT
       -          count(id)
       +          count(*)
                  FROM score
                  WHERE
                    name = ?
       t@@ -129,7 +104,7 @@ func db_id(db *sql.DB, nick string) bool {
        
        func db_ranked_players(db *sql.DB, offset, limit int) ([]Player, error) {
                query := `SELECT
       -          id,name,token,flag,score
       +          name,flag,score
                  FROM score
                  ORDER BY
                    score DESC,
       t@@ -147,7 +122,7 @@ func db_ranked_players(db *sql.DB, offset, limit int) ([]Player, error) {
                players := make([]Player, 0)
                for rows.Next() {
                        var p Player
       -                err := rows.Scan(&p.id, &p.name, &p.token, &p.flag, &p.score)
       +                err := rows.Scan(&p.name, &p.flag, &p.score)
                        if err != nil {
                                return nil, err
                        }
 (DIR) diff --git a/go.mod b/go.mod
       t@@ -9,6 +9,7 @@ require (
        
        require (
                github.com/dustin/go-humanize v1.0.0
       +        golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
                modernc.org/sqlite v1.20.0
        )
        
 (DIR) diff --git a/go.sum b/go.sum
       t@@ -28,6 +28,7 @@ github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
        github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
        golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
        golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
       +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
        golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
        golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
        golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 (DIR) diff --git a/main.go b/main.go
       t@@ -32,6 +32,13 @@ const (
                BOARD_WIDTH int = 26
                BOARD_HEIGHT int = 15
                DB string = "leaderboard.db"
       +        TOKEN_REMINDER string = `TOKEN FOR %s: %s
       +
       +This token will be requested when you submit flag #%d.
       +Save it carefully, and do not share it with anyone.
       +
       +Good bye hunter. Good luck.
       +`
        )
        
        type Application struct {
       t@@ -41,7 +48,7 @@ type Application struct {
                pages *tview.Pages
                frame *tview.Grid
                board *tview.Flex
       -        player Player
       +        player *Player
        }
        
        var flag_sha256 = [...]string {
       t@@ -60,7 +67,7 @@ var flag_sha256 = [...]string {
        var cyboard Application
        
        func usage() {
       -        fmt.Println("ssh board@cyb.farm [flag SHA256]")
       +        fmt.Println("ssh board@cyb.farm [FLAG]")
                os.Exit(0)
        }
        
       t@@ -79,16 +86,29 @@ func pageToken() tview.Primitive {
                        SetLabel("TOKEN ").
                        SetPlaceholder("").
                        SetFieldStyle(tcell.StyleDefault.Reverse(true)).
       -                SetFieldWidth(28)
       +                SetFieldWidth(30)
        
                input.SetAcceptanceFunc(func(text string, ch rune) bool {
       -                matched, err := regexp.Match(`^[a-zA-Z0-9]+$`, []byte(text))
       +                if len(text) > 24 {
       +                        return false
       +                }
       +
       +                // tokens are base32 strings
       +                matched, err := regexp.Match(`^[A-Z2-7]+$`, []byte(text))
                        if err != nil {
                                panic(err)
                        }
                        return matched
                })
                input.SetDoneFunc(func(key tcell.Key) {
       +                        if key != tcell.KeyEnter {
       +                                return
       +                        }
       +
       +                        if len(input.GetText()) != 24 {
       +                                cyboard.Popup("ERROR", "Invalid token format")
       +                                return
       +                        }
                                err := cyboard.player.FromToken(input.GetText())
                                if err != nil {
                                        cyboard.Fatal(err)
       t@@ -136,7 +156,6 @@ func main() {
                if err != nil {
                        panic(err)
                }
       -        cyboard.player.db = cyboard.db
                defer cyboard.db.Close()
        
                cyboard.flag = 0
       t@@ -144,7 +163,7 @@ func main() {
                cyboard.pages = tview.NewPages()
                cyboard.frame = tview.NewGrid()
                cyboard.board = tview.NewFlex()
       -        cyboard.player.id = -1
       +        cyboard.player = &Player{ db: cyboard.db }
        
                cyboard.pages.SetBackgroundColor(tcell.ColorDefault)
        
       t@@ -173,7 +192,7 @@ func main() {
                                cyboard.NewPlayer(rank)
                                cyboard.pages.SwitchToPage("board")
                        case 2,3,4,5:
       -                        cyboard.pages.ShowPage("token")
       +                        cyboard.pages.SwitchToPage("token")
                        default:
                                fmt.Println("Incorrect flag")
                                return
       t@@ -186,4 +205,8 @@ func main() {
                if err := cyboard.app.SetRoot(cyboard.pages, true).EnableMouse(true).Run(); err != nil {
                        panic(err)
                }
       +
       +        if cyboard.player.token != "" && cyboard.flag < (len(flag_sha256)) {
       +                fmt.Printf(TOKEN_REMINDER, cyboard.player.name, cyboard.player.token, cyboard.flag + 1)
       +        }
        }
 (DIR) diff --git a/player.go b/player.go
       t@@ -15,11 +15,13 @@
        package main
        
        import (
       +        "crypto/rand"
       +        "database/sql"
       +        "encoding/base32"
                "errors"
                "fmt"
                "time"
       -        "database/sql"
       -        //"golang.org/x/crypto/argon2"
       +        "golang.org/x/crypto/scrypt"
                "github.com/dustin/go-humanize"
        
                _ "modernc.org/sqlite"
       t@@ -27,49 +29,85 @@ import (
        
        type Player struct {
                db *sql.DB
       -        id int
       -        name string
                token string
       +        name string
                flag int
                score int
                ts int64
        }
        
       -func (p *Player) Register() int64 {
       -        query := `INSERT INTO score(name,token,flag,score,ts) VALUES(?,?,?,?,?);`
       -        r, err := p.db.Exec(query, p.name, p.token, p.flag, p.score, p.ts)
       +func randbuf(length int64) []byte {
       +        b := make([]byte, length)
       +        _, err := rand.Read(b)
                if err != nil {
                        panic(err)
                }
       +        return b
       +}
       +
       +func tokenize(name string) (string, string, error) {
       +        key := randbuf(12)
       +        salt := base32.StdEncoding.EncodeToString([]byte(name))
        
       -        id, _ := r.LastInsertId()
       +        //token := []byte(name)
       +        //token = append(token, key...)
       +        token := key
       +        token = append(token, []byte(name)...)
        
       -        return id
       +        // use name as salt
       +        dk, err := scrypt.Key(key, []byte(salt), 1<<15, 8, 1, 32)
       +        if err != nil {
       +                return "", "", err
       +        }
       +        hash32 := base32.StdEncoding.EncodeToString(dk)
       +        token32 := base32.StdEncoding.EncodeToString(token)
       +
       +        return token32, hash32, nil
        }
        
       -func (p *Player) Update() int64 {
       -        query := `UPDATE score SET flag = ?, score = ?, ts = ? WHERE id = ?;`
       -        r, err := p.db.Exec(query, p.flag, p.score, p.ts, p.id)
       +func (p *Player) Register() error {
       +        var hash string
       +        var err error
       +        p.token, hash, err = tokenize(p.name)
                if err != nil {
       -                panic(err)
       +                return err
                }
        
       -        id, _ := r.LastInsertId()
       +        query := `INSERT INTO score(name,hash,flag,score,ts) VALUES(?,?,?,?,?);`
       +        _, err = p.db.Exec(query, p.name, hash, p.flag, p.score, p.ts)
       +        if err != nil {
       +                return err
       +        }
       +
       +        return nil
       +}
       +
       +func (p *Player) Update() error {
       +        var hash string
       +        var err error
       +        p.token, hash, err = tokenize(p.name)
       +        if err != nil {
       +                return err
       +        }
       +        query := `UPDATE score SET hash = ?, flag = ?, score = ?, ts = ? WHERE name = ?;`
       +        _, err = p.db.Exec(query, hash, p.flag, p.score, p.ts, p.name)
       +        if err != nil {
       +                return err
       +        }
        
       -        return id
       +        return nil
        }
        
        func (p *Player) ScoreRank() int {
                var count int
                query := `SELECT
       -          count(id)
       +          count(*)
                  FROM score
                  WHERE
       -            score >= ? AND
       -            ts <= ?
       +            score >= ? OR (score == ? AND ts <= ?)
                ;`
        
       -        row := p.db.QueryRow(query, p.score, p.ts)
       +        row := p.db.QueryRow(query, p.score, p.score, p.ts)
                row.Scan(&count)
                return count
        }
       t@@ -77,7 +115,7 @@ func (p *Player) ScoreRank() int {
        func (p *Player) FlagRank() int {
                var count int
                query := `SELECT
       -          count(id)
       +          count(*)
                  FROM score
                  WHERE
                    flag >= ?
       t@@ -128,7 +166,7 @@ func (p *Player) Submit(flag int) error {
                }
        
                if flag != p.flag + 1 {
       -                return errors.New(fmt.Sprintf("Missing %s flag", humanize.Ordinal(p.flag + 1)))
       +                return errors.New(fmt.Sprintf("Missing %s flag for %s", humanize.Ordinal(p.flag + 1), p.name))
                }
        
                p.ts = time.Now().Unix()
       t@@ -145,12 +183,29 @@ func (p *Player) Submit(flag int) error {
        }
        
        func (p *Player) FromToken(token string) error {
       -        query := `SELECT id,name,token,flag,score,ts FROM score WHERE token = ?`
       +        var err error
       +        blob, err := base32.StdEncoding.DecodeString(token)
       +        if err != nil {
       +                return err
       +        }
       +
       +        key := blob[:12]
       +        p.name = string(blob[12:])
       +
       +        salt := base32.StdEncoding.EncodeToString([]byte(p.name))
       +
       +        // use player id as salt
       +        dk, err := scrypt.Key(key, []byte(salt), 1<<15, 8, 1, 32)
       +        if err != nil {
       +                return err
       +        }
       +        hash := base32.StdEncoding.EncodeToString(dk)
       +        query := `SELECT name,flag,score,ts FROM score WHERE name = ? AND hash = ?`
        
       -        row := p.db.QueryRow(query, token)
       -        err := row.Scan(&p.id, &p.name, &p.token, &p.flag, &p.score, &p.ts)
       +        row := p.db.QueryRow(query, p.name, hash)
       +        err = row.Scan(&p.name, &p.flag, &p.score, &p.ts)
                if err == sql.ErrNoRows {
       -                return errors.New("Unmatched token")
       +                return errors.New("Invalid token")
                }
        
                return nil
 (DIR) diff --git a/playerbox.go b/playerbox.go
       t@@ -59,7 +59,7 @@ func manipulatebox(event *tcell.EventKey) *tcell.EventKey {
                        return event
        }
        
       -func PlayerBoxName(p Player) *tview.TextView {
       +func PlayerBoxName(p *Player) *tview.TextView {
                v := tview.NewTextView().
                        SetDynamicColors(true).
                        SetTextAlign(tview.AlignRight).
       t@@ -71,7 +71,10 @@ func PlayerBoxName(p Player) *tview.TextView {
                                if key == tcell.KeyEnter {
                                        p.name = fmt.Sprintf("%3s", playerbox.name)
                                        if ! p.Exists() {
       -                                        p.Register()
       +                                        err := p.Register()
       +                                        if err != nil {
       +                                                cyboard.Fatal(err)
       +                                        }
                                                cyboard.HighlightBoard(p.ScoreRank())
                                        } else {
                                                cyboard.Popup("NOPE", "Player name unavailable\nPlease pick another one")
       t@@ -88,7 +91,7 @@ func PlayerBoxName(p Player) *tview.TextView {
                return v
        }
        
       -func PlayerBoxGrid(p Player, rank int) *tview.Grid {
       +func PlayerBoxGrid(p *Player, rank int) *tview.Grid {
                gridcell := func (text string) *tview.TextView {
                        return tview.NewTextView().
                                SetDynamicColors(true).
 (DIR) diff --git a/ui.go b/ui.go
       t@@ -102,10 +102,10 @@ func (a *Application) NewPlayer(rank int) {
                } else {
                        offset = 0
                        t1_sz = rank - 1
       -                t2_sz = BOARD_HEIGHT - rank - 1
       +                t2_sz = BOARD_HEIGHT - rank
                }
                t1 := RankTable(offset, t1_sz, rank - t1_sz - 1, false).SetSelectable(false, false)
       -        t2 := RankTable(rank - 1, BOARD_HEIGHT, t2_sz, true).SetSelectable(false, false)
       +        t2 := RankTable(rank - 1, BOARD_HEIGHT, rank, true).SetSelectable(false, false)
                box := PlayerBoxGrid(a.player, rank)
        
                a.board.Clear().
       t@@ -115,7 +115,7 @@ func (a *Application) NewPlayer(rank int) {
                        AddItem(t2, t2_sz, 0, false)
        }
        
       -func popup(title, text string, callback func(key tcell.Key)) tview.Primitive {
       +func popup(title, text string, w, h int, callback func(key tcell.Key)) tview.Primitive {
                p := tview.NewTextView().
                        SetDynamicColors(true).
                        SetTextAlign(tview.AlignCenter).
       t@@ -123,16 +123,25 @@ func popup(title, text string, callback func(key tcell.Key)) tview.Primitive {
                        SetDoneFunc(callback)
        
                popup := tview.NewFrame(p).
       -                SetBorders(7, 0, 0, 0, 1, 1).
       +                SetBorders(1, 0, 0, 0, 1, 1).
                        AddText("PRESS ENTER", false, tview.AlignRight, tcell.ColorGray)
        
                popup.SetBorder(true).SetTitle(fmt.Sprintf(" %s ", title))
        
       -        return center(28, 19, popup)
       +        return center(w, h, popup)
       +}
       +
       +func (a *Application) Message(title, text string) {
       +        //p := popup(title, text, 52, 19, func(key tcell.Key) {
       +        p := popup(title, text, 64, 19, func(key tcell.Key) {
       +                a.pages.RemovePage("popup")
       +        })
       +
       +        a.pages.AddAndSwitchToPage("popup", p, true)
        }
        
        func (a *Application) Popup(title, text string) {
       -        p := popup(title, text, func(key tcell.Key) {
       +        p := popup(title, text, 28, 19, func(key tcell.Key) {
                        a.pages.RemovePage("popup")
                })
        
       t@@ -140,7 +149,7 @@ func (a *Application) Popup(title, text string) {
        }
        
        func (a *Application) Fatal(err error) {
       -        p := popup("ERROR", fmt.Sprintf("%s", err), func(key tcell.Key) {
       +        p := popup("ERROR", fmt.Sprintf("%s", err), 28, 19, func(key tcell.Key) {
                        a.app.Stop()
                })
        
 (DIR) diff --git a/util.go b/util.go
       t@@ -1,49 +0,0 @@
       -package main
       -
       -import (
       -        "fmt"
       -        "strings"
       -        "github.com/rivo/tview"
       -)
       -
       -func flagid(hash string) int {
       -        for i := 0; i<len(flag_sha256); i++ {
       -                if strings.ToUpper(hash) == flag_sha256[i] {
       -                        return i
       -                }
       -        }
       -        return -1
       -}
       -
       -func center(width, height int, p tview.Primitive) tview.Primitive {
       -        return tview.NewFlex().
       -                AddItem(nil, 0, 1, false).
       -                AddItem(tview.NewFlex().
       -                        SetDirection(tview.FlexRow).
       -                        AddItem(nil, 0, 1, false).
       -                        AddItem(p, height, 1, true).
       -                        AddItem(nil, 0, 1, false), width, 1, true).
       -                AddItem(nil, 0, 1, false)
       -}
       -
       -// Convert flag count to a visual string: 3 -> "XXX.."
       -func flag2str(n int) string {
       -        var flags [5]byte
       -        for i:=0; i <len(flags); i++ {
       -                if i < n {
       -                        flags[i] = 'X'
       -                } else {
       -                        flags[i] = '.'
       -                }
       -        }
       -
       -        return fmt.Sprintf("%s", flags)
       -}
       -
       -func newcell(text string) *tview.TableCell {
       -        return tview.NewTableCell(text).
       -                SetTransparency(true).
       -                SetSelectable(true).
       -                SetAlign(tview.AlignRight).
       -                SetExpansion(1)
       -}