commit b9e06a23aa62889d6dad5592e45437bdd843bbcd Author: Robert Date: Thu Jul 3 18:22:46 2025 -0400 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..98e6ef6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.db diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..717321d --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License + +Copyright (c) 2025 Bob Strutts + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/client.go b/client.go new file mode 100644 index 0000000..f956f23 --- /dev/null +++ b/client.go @@ -0,0 +1,80 @@ +package main + +import ( + "crypto/rand" + "encoding/gob" + "encoding/hex" + "fmt" + "net" + "time" +) + +type Request struct { + Operation string + Site string + Password string + Timestamp int64 + Nonce string +} + +type Response struct { + Message string +} + +func generateNonce() (string, error) { + b := make([]byte, 12) // 96-bit nonce + _, err := rand.Read(b) + if err != nil { + return "", err + } + return hex.EncodeToString(b), nil +} + +func main() { + conn, err := net.Dial("tcp", "localhost:9898") + if err != nil { + panic(err) + } + defer conn.Close() + + var op, site, pwd string + + fmt.Print("Operation (store/get): ") + fmt.Scanln(&op) + fmt.Print("Site: ") + fmt.Scanln(&site) + + if op == "store" { + fmt.Print("Password: ") + fmt.Scanln(&pwd) + } + + nonce, err := generateNonce() + if err != nil { + panic(err) + } + + req := Request{ + Operation: op, + Site: site, + Password: pwd, + Timestamp: time.Now().Unix(), + Nonce: nonce, + } + + enc := gob.NewEncoder(conn) + dec := gob.NewDecoder(conn) + + if err := enc.Encode(req); err != nil { + fmt.Println("Failed to send request:", err) + return + } + + var res Response + if err := dec.Decode(&res); err != nil { + fmt.Println("Failed to receive response:", err) + return + } + + fmt.Println("Server response:", res.Message) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1efa97c --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module cliVault + +go 1.24.3 + +require github.com/mattn/go-sqlite3 v1.14.28 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..42e5bac --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= diff --git a/keygen.go b/keygen.go new file mode 100644 index 0000000..77ace22 --- /dev/null +++ b/keygen.go @@ -0,0 +1,25 @@ +package main + +import ( + "crypto/rand" + "encoding/hex" + "fmt" +) + +func generateKey() ([]byte, error) { + key := make([]byte, 16) + _, err := rand.Read(key) + if err != nil { + return nil, err + } + return key, nil +} + +func main() { + key, err := generateKey() + if err != nil { + panic(err) + } + + fmt.Println("Generated 32-byte key (hex):", hex.EncodeToString(key)) +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..4b28bd2 --- /dev/null +++ b/server.go @@ -0,0 +1,198 @@ +package main + +import ( + "crypto/aes" + "crypto/cipher" + "database/sql" + "encoding/gob" + "fmt" + "net" + "sync" + "time" + + _ "github.com/mattn/go-sqlite3" +) + +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 { + Operation string + Site string + Password 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 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 + } + + switch req.Operation { + case "store": + encrypted, err := encrypt(req.Password) + if err != nil { + enc.Encode(Response{"Encryption failed"}) + return + } + _, err = db.Exec("INSERT OR REPLACE INTO accounts (site, password) VALUES (?, ?)", 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 site = ?", 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 accounts ( + site TEXT PRIMARY KEY, + password BLOB + )`) + 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) + } +}