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)
-}