You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
248 lines
6.0 KiB
248 lines
6.0 KiB
package main
|
|
|
|
import (
|
|
"golang.org/x/crypto/bcrypt"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"database/sql"
|
|
"encoding/gob"
|
|
"fmt"
|
|
"net"
|
|
"sync"
|
|
"time"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
const AllowRegistration = true // Disable after users are added!
|
|
var key = []byte("2f5680a7fb57ce83e8e83dbb1114ee31") // 32 bytes
|
|
|
|
var nonceStore = struct {
|
|
sync.Mutex
|
|
data map[string]int64
|
|
}{data: make(map[string]int64)}
|
|
|
|
const nonceExpiry = 30 * time.Second
|
|
|
|
type Request struct {
|
|
Username string
|
|
Password string
|
|
Operation string
|
|
Site string
|
|
VaultData string
|
|
Timestamp int64 // Unix time (seconds)
|
|
Nonce string // Random client-supplied nonce
|
|
}
|
|
|
|
type Response struct {
|
|
Message string
|
|
}
|
|
|
|
var ipAttempts = struct {
|
|
sync.Mutex
|
|
data map[string][]int64
|
|
}{data: make(map[string][]int64)}
|
|
|
|
const maxAttemptsPerMinute = 5
|
|
|
|
func isRateLimited(ip string) bool {
|
|
now := time.Now().Unix()
|
|
ipAttempts.Lock()
|
|
defer ipAttempts.Unlock()
|
|
|
|
attempts := ipAttempts.data[ip]
|
|
// Keep only the last 60 seconds of attempts
|
|
recent := make([]int64, 0)
|
|
for _, t := range attempts {
|
|
if now-t <= 60 {
|
|
recent = append(recent, t)
|
|
}
|
|
}
|
|
ipAttempts.data[ip] = append(recent, now)
|
|
return len(recent) >= maxAttemptsPerMinute
|
|
}
|
|
|
|
func isValidNonce(nonce string, timestamp int64) bool {
|
|
now := time.Now().Unix()
|
|
if now-timestamp > int64(nonceExpiry.Seconds()) {
|
|
return false // Too old
|
|
}
|
|
|
|
nonceStore.Lock()
|
|
defer nonceStore.Unlock()
|
|
|
|
if t, exists := nonceStore.data[nonce]; exists {
|
|
if now-t < int64(nonceExpiry.Seconds()) {
|
|
return false // Replay detected
|
|
}
|
|
}
|
|
|
|
nonceStore.data[nonce] = now
|
|
return true
|
|
}
|
|
|
|
func hashPassword(pw string) (string, error) {
|
|
bytes, err := bcrypt.GenerateFromPassword([]byte(pw), 12)
|
|
return string(bytes), err
|
|
}
|
|
|
|
func checkPassword(hash, pw string) error {
|
|
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw))
|
|
}
|
|
|
|
func encrypt(text string) ([]byte, error) {
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nonce := make([]byte, gcm.NonceSize())
|
|
return gcm.Seal(nonce, nonce, []byte(text), nil), nil
|
|
}
|
|
|
|
func decrypt(data []byte) (string, error) {
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
nonceSize := gcm.NonceSize()
|
|
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
|
|
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(plaintext), nil
|
|
}
|
|
|
|
func handleConnection(conn net.Conn, db *sql.DB) {
|
|
defer conn.Close()
|
|
dec := gob.NewDecoder(conn)
|
|
enc := gob.NewEncoder(conn)
|
|
|
|
remoteAddr := conn.RemoteAddr().String()
|
|
|
|
var req Request
|
|
if err := dec.Decode(&req); err != nil {
|
|
return
|
|
}
|
|
|
|
if isRateLimited(remoteAddr) {
|
|
enc.Encode(Response{"Too many requests. Try later."})
|
|
return
|
|
}
|
|
|
|
if !isValidNonce(req.Nonce, req.Timestamp) {
|
|
enc.Encode(Response{"Invalid or replayed request"})
|
|
return
|
|
}
|
|
|
|
if req.Operation == "register" {
|
|
if AllowRegistration == false {
|
|
enc.Encode(Response{"Registration Disabled!"})
|
|
return
|
|
}
|
|
hashed, err := hashPassword(req.Password)
|
|
if err != nil {
|
|
enc.Encode(Response{"Failed to hash password"})
|
|
return
|
|
}
|
|
_, err = db.Exec("INSERT INTO users (username, password) VALUES (?, ?)", req.Username, hashed)
|
|
if err != nil {
|
|
enc.Encode(Response{"Registration failed (user exists?)"})
|
|
} else {
|
|
enc.Encode(Response{"Registration successful"})
|
|
}
|
|
return
|
|
}
|
|
// Authenticate all other operations
|
|
var storedHash string
|
|
err := db.QueryRow("SELECT password FROM users WHERE username = ?", req.Username).Scan(&storedHash)
|
|
if err != nil || checkPassword(storedHash, req.Password) != nil {
|
|
enc.Encode(Response{"Authentication failed"})
|
|
return
|
|
}
|
|
|
|
switch req.Operation {
|
|
case "store":
|
|
encrypted, err := encrypt(req.VaultData)
|
|
if err != nil {
|
|
enc.Encode(Response{"Encryption failed"})
|
|
return
|
|
}
|
|
_, err = db.Exec("INSERT OR REPLACE INTO accounts (user, site, password) VALUES (?, ?, ?)",
|
|
req.Username, req.Site, encrypted)
|
|
if err != nil {
|
|
enc.Encode(Response{"Database error"})
|
|
return
|
|
}
|
|
enc.Encode(Response{"Password stored successfully"})
|
|
|
|
case "get":
|
|
var encrypted []byte
|
|
err := db.QueryRow("SELECT password FROM accounts WHERE user = ? AND site = ?", req.Username, req.Site).Scan(&encrypted)
|
|
if err == sql.ErrNoRows {
|
|
enc.Encode(Response{"Site not found"})
|
|
return
|
|
} else if err != nil {
|
|
enc.Encode(Response{"Database error"})
|
|
return
|
|
}
|
|
|
|
pwd, err := decrypt(encrypted)
|
|
if err != nil {
|
|
enc.Encode(Response{"Decryption failed"})
|
|
return
|
|
}
|
|
enc.Encode(Response{pwd})
|
|
|
|
default:
|
|
enc.Encode(Response{"Unknown operation"})
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
db, err := sql.Open("sqlite3", "vault.db")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer db.Close()
|
|
|
|
_, err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
username TEXT PRIMARY KEY,
|
|
password TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS accounts (
|
|
user TEXT,
|
|
site TEXT,
|
|
password BLOB,
|
|
PRIMARY KEY(user, site),
|
|
FOREIGN KEY(user) REFERENCES users(username)
|
|
)
|
|
`)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
ln, err := net.Listen("tcp", ":9898")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
fmt.Println("Server started on port 9898")
|
|
|
|
for {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
go handleConnection(conn, db)
|
|
}
|
|
}
|
|
|