commit
b9e06a23aa
@ -0,0 +1 @@ |
||||
*.db |
||||
@ -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. |
||||
@ -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) |
||||
} |
||||
@ -0,0 +1,5 @@ |
||||
module cliVault |
||||
|
||||
go 1.24.3 |
||||
|
||||
require github.com/mattn/go-sqlite3 v1.14.28 // indirect |
||||
@ -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= |
||||
@ -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)) |
||||
} |
||||
@ -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) |
||||
} |
||||
} |
||||
Loading…
Reference in new issue