|
|
|
|
@ -2,20 +2,42 @@ package main |
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
|
"golang.org/x/crypto/bcrypt" |
|
|
|
|
"golang.org/x/crypto/chacha20poly1305" |
|
|
|
|
"golang.org/x/crypto/scrypt" |
|
|
|
|
"crypto/sha256" |
|
|
|
|
"crypto/rand" |
|
|
|
|
"crypto/aes" |
|
|
|
|
"crypto/cipher" |
|
|
|
|
"database/sql" |
|
|
|
|
"encoding/base64" |
|
|
|
|
"encoding/pem" |
|
|
|
|
"encoding/gob" |
|
|
|
|
"fmt" |
|
|
|
|
"net" |
|
|
|
|
"sync" |
|
|
|
|
"time" |
|
|
|
|
"bytes" |
|
|
|
|
"errors" |
|
|
|
|
"flag" |
|
|
|
|
"os" |
|
|
|
|
"log" |
|
|
|
|
"io/ioutil" |
|
|
|
|
"gopkg.in/yaml.v2" |
|
|
|
|
|
|
|
|
|
_ "github.com/mattn/go-sqlite3" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
const AllowRegistration = true // Disable after users are added!
|
|
|
|
|
var key = []byte("2f5680a7fb57ce83e8e83dbb1114ee31") // 32 bytes
|
|
|
|
|
var ChaKey = []byte("") |
|
|
|
|
|
|
|
|
|
type Config struct { |
|
|
|
|
Auth struct { |
|
|
|
|
ChaKey string `yaml:"ChaKey"` |
|
|
|
|
PEM string `yaml:"PEM"` |
|
|
|
|
} `yaml:"auth"` |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
var config Config |
|
|
|
|
|
|
|
|
|
var nonceStore = struct { |
|
|
|
|
sync.Mutex |
|
|
|
|
@ -24,7 +46,9 @@ var nonceStore = struct { |
|
|
|
|
|
|
|
|
|
const nonceExpiry = 30 * time.Second |
|
|
|
|
|
|
|
|
|
var key []byte |
|
|
|
|
type Request struct { |
|
|
|
|
KeyPwd string |
|
|
|
|
Username string |
|
|
|
|
Password string |
|
|
|
|
Operation string |
|
|
|
|
@ -36,6 +60,7 @@ type Request struct { |
|
|
|
|
|
|
|
|
|
type Response struct { |
|
|
|
|
Message string |
|
|
|
|
Enc string |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
var ipAttempts = struct { |
|
|
|
|
@ -81,6 +106,106 @@ func isValidNonce(nonce string, timestamp int64) bool { |
|
|
|
|
return true |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// decryptPEMToKey decrypts a PEM block with the given password
|
|
|
|
|
func decryptPEMToKey(filename string, password string) ([]byte, error) { |
|
|
|
|
// Read the entire file
|
|
|
|
|
pemData, err := os.ReadFile(filename) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, fmt.Errorf("failed to read PEM file: %v", err) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Decode the PEM block
|
|
|
|
|
pemBlock, _ := pem.Decode(pemData) |
|
|
|
|
if pemBlock == nil { |
|
|
|
|
return nil, errors.New("failed to decode PEM block") |
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if the PEM block is encrypted
|
|
|
|
|
if pemBlock.Headers["Key-Derivation"] == "" { |
|
|
|
|
return nil, errors.New("PEM block is not password-protected") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Get salt and IV from headers
|
|
|
|
|
salt, err := hexStringToBytes(pemBlock.Headers["SALT"]) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, fmt.Errorf("invalid salt: %v", err) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
iv, err := hexStringToBytes(pemBlock.Headers["IV"]) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, fmt.Errorf("invalid IV: %v", err) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Derive encryption key
|
|
|
|
|
var encryptionKey []byte |
|
|
|
|
switch pemBlock.Headers["Key-Derivation"] { |
|
|
|
|
case "scrypt": |
|
|
|
|
encryptionKey, err = scrypt.Key([]byte(password), salt, 32768, 8, 1, 32) |
|
|
|
|
//case "pbkdf2":
|
|
|
|
|
//encryptionKey = pbkdf2.Key([]byte(password), salt, 10000, 32, sha256.New)
|
|
|
|
|
default: |
|
|
|
|
return nil, errors.New("unsupported key derivation function") |
|
|
|
|
} |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Decrypt the key
|
|
|
|
|
block, err := aes.NewCipher(encryptionKey) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
decrypted := make([]byte, len(pemBlock.Bytes)) |
|
|
|
|
stream := cipher.NewCFBDecrypter(block, iv) |
|
|
|
|
stream.XORKeyStream(decrypted, pemBlock.Bytes) |
|
|
|
|
|
|
|
|
|
return decrypted, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// hexStringToBytes converts a hex string to bytes
|
|
|
|
|
func hexStringToBytes(hexStr string) ([]byte, error) { |
|
|
|
|
if len(hexStr)%2 != 0 { |
|
|
|
|
return nil, errors.New("hex string length must be even") |
|
|
|
|
} |
|
|
|
|
bytes := make([]byte, len(hexStr)/2) |
|
|
|
|
for i := 0; i < len(hexStr); i += 2 { |
|
|
|
|
b, err := hexByte(hexStr[i], hexStr[i+1]) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
bytes[i/2] = b |
|
|
|
|
} |
|
|
|
|
return bytes, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// hexByte converts two hex characters to a byte
|
|
|
|
|
func hexByte(a, b byte) (byte, error) { |
|
|
|
|
high, err := hexDigitToByte(a) |
|
|
|
|
if err != nil { |
|
|
|
|
return 0, err |
|
|
|
|
} |
|
|
|
|
low, err := hexDigitToByte(b) |
|
|
|
|
if err != nil { |
|
|
|
|
return 0, err |
|
|
|
|
} |
|
|
|
|
return (high << 4) | low, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// hexDigitToByte converts a single hex character to its byte value
|
|
|
|
|
func hexDigitToByte(c byte) (byte, error) { |
|
|
|
|
switch { |
|
|
|
|
case '0' <= c && c <= '9': |
|
|
|
|
return c - '0', nil |
|
|
|
|
case 'a' <= c && c <= 'f': |
|
|
|
|
return c - 'a' + 10, nil |
|
|
|
|
case 'A' <= c && c <= 'F': |
|
|
|
|
return c - 'A' + 10, nil |
|
|
|
|
default: |
|
|
|
|
return 0, fmt.Errorf("invalid hex digit %c", c) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func hashPassword(pw string) (string, error) { |
|
|
|
|
bytes, err := bcrypt.GenerateFromPassword([]byte(pw), 12) |
|
|
|
|
return string(bytes), err |
|
|
|
|
@ -121,11 +246,75 @@ func decrypt(data []byte) (string, error) { |
|
|
|
|
return string(plaintext), nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func getKey()([]byte) { |
|
|
|
|
// Hash it to 32 bytes using SHA-256
|
|
|
|
|
hashedKey := sha256.Sum256(ChaKey) |
|
|
|
|
theKey := hashedKey[:] // Convert [32]byte to []byte
|
|
|
|
|
return theKey |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func chEnc(pwd string)(string, error) { |
|
|
|
|
// Create cipher
|
|
|
|
|
aead, err := chacha20poly1305.NewX(getKey()) |
|
|
|
|
if err != nil { |
|
|
|
|
return "", err |
|
|
|
|
}
|
|
|
|
|
// Create nonce
|
|
|
|
|
nonce := make([]byte, aead.NonceSize()) |
|
|
|
|
if _, err := rand.Read(nonce); err != nil { |
|
|
|
|
return "", err |
|
|
|
|
}
|
|
|
|
|
// Encode with gob first
|
|
|
|
|
var buf bytes.Buffer |
|
|
|
|
tempEnc := gob.NewEncoder(&buf) |
|
|
|
|
if err := tempEnc.Encode(pwd); err != nil { |
|
|
|
|
return "", err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Encrypt the encoded data
|
|
|
|
|
encrypted := aead.Seal(nil, nonce, buf.Bytes(), nil) |
|
|
|
|
|
|
|
|
|
// Send nonce + encrypted data
|
|
|
|
|
fullMessage := append(nonce, encrypted...) |
|
|
|
|
encoded := base64.StdEncoding.EncodeToString(fullMessage) |
|
|
|
|
|
|
|
|
|
return encoded, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func chDec(eText string)(string) { |
|
|
|
|
if eText == "" { |
|
|
|
|
log.Fatalf("Error: Blank") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Decoding from base64
|
|
|
|
|
decoded, err := base64.StdEncoding.DecodeString(eText) |
|
|
|
|
if err != nil { |
|
|
|
|
log.Fatalf("Error: Base64 decode") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Create cipher instance (XChaCha20-Poly1305 for longer nonces)
|
|
|
|
|
aead, err := chacha20poly1305.NewX(getKey()) |
|
|
|
|
if err != nil { |
|
|
|
|
log.Fatalf("Error: Cha20 key") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
encryptedMsg := decoded |
|
|
|
|
|
|
|
|
|
// Decrypt: Split nonce and ciphertext
|
|
|
|
|
decryptedNonce := encryptedMsg[:aead.NonceSize()] |
|
|
|
|
decryptedCiphertext := encryptedMsg[aead.NonceSize():] |
|
|
|
|
|
|
|
|
|
decrypted, err := aead.Open(nil, decryptedNonce, decryptedCiphertext, nil) |
|
|
|
|
if err != nil { |
|
|
|
|
log.Fatalf("Error: aead open") |
|
|
|
|
} |
|
|
|
|
return string(decrypted) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
@ -133,82 +322,156 @@ func handleConnection(conn net.Conn, db *sql.DB) { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
username := chDec(req.Username) |
|
|
|
|
keypwd := chDec(req.KeyPwd) |
|
|
|
|
password := chDec(req.Password) |
|
|
|
|
|
|
|
|
|
if username == "" { |
|
|
|
|
enc.Encode(Response{Message: "Required username, blank found!", Enc: ""}) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
if keypwd == "" { |
|
|
|
|
enc.Encode(Response{Message: "Required key passphrase, blank found!", Enc: ""}) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
if password == "" { |
|
|
|
|
enc.Encode(Response{Message: "Required user password, blank found!", Enc: ""}) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
k, err := decryptPEMToKey(config.Auth.PEM, keypwd) |
|
|
|
|
if err != nil { |
|
|
|
|
enc.Encode(Response{Message: "Unable to decode data!", Enc: ""}) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
key = k |
|
|
|
|
|
|
|
|
|
if isRateLimited(remoteAddr) { |
|
|
|
|
enc.Encode(Response{"Too many requests. Try later."}) |
|
|
|
|
enc.Encode(Response{Message: "Too many requests. Try later.", Enc: ""}) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if !isValidNonce(req.Nonce, req.Timestamp) { |
|
|
|
|
enc.Encode(Response{"Invalid or replayed request"}) |
|
|
|
|
enc.Encode(Response{Message: "Invalid or replayed request", Enc: ""}) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if req.Operation == "register" { |
|
|
|
|
if AllowRegistration == false { |
|
|
|
|
enc.Encode(Response{"Registration Disabled!"}) |
|
|
|
|
enc.Encode(Response{Message: "Registration Disabled!", Enc: ""}) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
hashed, err := hashPassword(req.Password) |
|
|
|
|
hashed, err := hashPassword(password) |
|
|
|
|
if err != nil { |
|
|
|
|
enc.Encode(Response{"Failed to hash password"}) |
|
|
|
|
enc.Encode(Response{Message: "Failed to hash password", Enc: ""}) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
_, err = db.Exec("INSERT INTO users (username, password) VALUES (?, ?)", req.Username, hashed) |
|
|
|
|
_, err = db.Exec("INSERT INTO users (username, password) VALUES (?, ?)", username, hashed) |
|
|
|
|
if err != nil { |
|
|
|
|
enc.Encode(Response{"Registration failed (user exists?)"}) |
|
|
|
|
enc.Encode(Response{Message: "Registration failed (user exists?)", Enc: ""}) |
|
|
|
|
} else { |
|
|
|
|
enc.Encode(Response{"Registration successful"}) |
|
|
|
|
enc.Encode(Response{Message: "Registration successful", Enc: ""}) |
|
|
|
|
} |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
site := chDec(req.Site) |
|
|
|
|
if site == "" { |
|
|
|
|
enc.Encode(Response{Message: "Required site name, blank found!", Enc: ""}) |
|
|
|
|
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"}) |
|
|
|
|
err = db.QueryRow("SELECT password FROM users WHERE username = ?", username).Scan(&storedHash) |
|
|
|
|
if err != nil || checkPassword(storedHash, password) != nil { |
|
|
|
|
enc.Encode(Response{Message: "Authentication failed", Enc: ""}) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
switch req.Operation { |
|
|
|
|
case "store": |
|
|
|
|
encrypted, err := encrypt(req.VaultData) |
|
|
|
|
vaultdata := chDec(req.VaultData) |
|
|
|
|
encrypted, err := encrypt(vaultdata) |
|
|
|
|
if err != nil { |
|
|
|
|
enc.Encode(Response{"Encryption failed"}) |
|
|
|
|
enc.Encode(Response{Message: "Encryption failed", Enc: ""}) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
_, err = db.Exec("INSERT OR REPLACE INTO accounts (user, site, password) VALUES (?, ?, ?)", |
|
|
|
|
req.Username, req.Site, encrypted) |
|
|
|
|
username, site, encrypted) |
|
|
|
|
if err != nil { |
|
|
|
|
enc.Encode(Response{"Database error"}) |
|
|
|
|
enc.Encode(Response{Message: "Database error", Enc: ""}) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
enc.Encode(Response{"Password stored successfully"}) |
|
|
|
|
enc.Encode(Response{Message: "Password stored successfully", Enc: ""}) |
|
|
|
|
|
|
|
|
|
case "get": |
|
|
|
|
var encrypted []byte |
|
|
|
|
err := db.QueryRow("SELECT password FROM accounts WHERE user = ? AND site = ?", req.Username, req.Site).Scan(&encrypted) |
|
|
|
|
err := db.QueryRow("SELECT password FROM accounts WHERE user = ? AND site = ?", username, site).Scan(&encrypted) |
|
|
|
|
if err == sql.ErrNoRows { |
|
|
|
|
enc.Encode(Response{"Site not found"}) |
|
|
|
|
enc.Encode(Response{Message: "Site not found", Enc: ""}) |
|
|
|
|
return |
|
|
|
|
} else if err != nil { |
|
|
|
|
enc.Encode(Response{"Database error"}) |
|
|
|
|
enc.Encode(Response{Message: "Database error", Enc: ""}) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
pwd, err := decrypt(encrypted) |
|
|
|
|
if err != nil { |
|
|
|
|
enc.Encode(Response{"Decryption failed"}) |
|
|
|
|
enc.Encode(Response{Message: "Decryption failed", Enc: ""}) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
ePwd, err := chEnc(pwd) |
|
|
|
|
if err != nil { |
|
|
|
|
enc.Encode(Response{Message: "Encryption failed", Enc: ""}) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
enc.Encode(Response{pwd}) |
|
|
|
|
enc.Encode(Response{Message: "", Enc: ePwd}) |
|
|
|
|
|
|
|
|
|
default: |
|
|
|
|
enc.Encode(Response{"Unknown operation"}) |
|
|
|
|
enc.Encode(Response{Message: "Unknown operation", Enc: ""}) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func main() { |
|
|
|
|
db, err := sql.Open("sqlite3", "vault.db") |
|
|
|
|
vaultPtr := flag.String("dbfile", "vault.db", "Enter dbfile") |
|
|
|
|
configFilePtr := flag.String("config", "config.yaml", "Path to the YAML configuration file") |
|
|
|
|
flag.Parse() |
|
|
|
|
|
|
|
|
|
if *vaultPtr == "" { |
|
|
|
|
fmt.Println("Please specify a db file using -dbfile") |
|
|
|
|
flag.Usage() |
|
|
|
|
os.Exit(1) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if *configFilePtr == "" { |
|
|
|
|
fmt.Println("Please specify a configuration file using -config") |
|
|
|
|
flag.Usage() |
|
|
|
|
os.Exit(1) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Read the config file
|
|
|
|
|
yamlFile, err := ioutil.ReadFile(*configFilePtr) |
|
|
|
|
if err != nil { |
|
|
|
|
log.Fatalf("Error reading YAML file: %v\n", err) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Parse the YAML
|
|
|
|
|
err = yaml.Unmarshal(yamlFile, &config) |
|
|
|
|
if err != nil { |
|
|
|
|
log.Fatalf("Error parsing YAML file: %v\n", err) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Print the ChaKey
|
|
|
|
|
if config.Auth.ChaKey == "" { |
|
|
|
|
fmt.Println("Warning: ChaKey not found in configuration file") |
|
|
|
|
} |
|
|
|
|
ChaKey = []byte(config.Auth.ChaKey) |
|
|
|
|
if config.Auth.PEM == "" { |
|
|
|
|
fmt.Println("Warning: PEM file not found in configuration file") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
db, err := sql.Open("sqlite3", *vaultPtr) |
|
|
|
|
if err != nil { |
|
|
|
|
panic(err) |
|
|
|
|
} |
|
|
|
|
|