first commit

main
Robert 5 months ago
commit b9e06a23aa
  1. 1
      .gitignore
  2. 22
      LICENSE
  3. 80
      client.go
  4. 5
      go.mod
  5. 2
      go.sum
  6. 25
      keygen.go
  7. 198
      server.go

1
.gitignore vendored

@ -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…
Cancel
Save