229 lines
5.6 KiB
Go
229 lines
5.6 KiB
Go
package main
|
|
|
|
import (
|
|
// "database/sql"
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/pbkdf2"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"golang.org/x/term"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
var (
|
|
serverAddress string
|
|
username string
|
|
password string
|
|
dataDir string
|
|
dataPath string
|
|
cachePath string
|
|
scanner bufio.Scanner
|
|
)
|
|
|
|
type Note struct {
|
|
Body string `json:"body"`
|
|
Color int64 `json:"color"`
|
|
Date string `json:"date"`
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
}
|
|
|
|
func check(err error, format string, a ...any) bool {
|
|
if err != nil {
|
|
log.Fatalf(format, a, err)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func readPassword() (string, error) {
|
|
stdin := int(syscall.Stdin)
|
|
oldState, err := term.GetState(stdin)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer term.Restore(stdin, oldState)
|
|
|
|
sigch := make(chan os.Signal, 1)
|
|
signal.Notify(sigch, os.Interrupt)
|
|
|
|
go func() {
|
|
<-sigch
|
|
term.Restore(stdin, oldState)
|
|
os.Exit(1)
|
|
}()
|
|
|
|
password, err := term.ReadPassword(stdin)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return string(password), nil
|
|
}
|
|
|
|
func pkcs7strip(data []byte, blockSize int) ([]byte, error) {
|
|
length := len(data)
|
|
if length == 0 {
|
|
return nil, errors.New("pkcs7: Data is empty")
|
|
}
|
|
if length%blockSize != 0 {
|
|
return nil, errors.New("pkcs7: Data is not block-aligned")
|
|
}
|
|
padLen := int(data[length-1])
|
|
ref := bytes.Repeat([]byte{byte(padLen)}, padLen)
|
|
if padLen > blockSize || padLen == 0 || !bytes.HasSuffix(data, ref) {
|
|
return nil, errors.New("pkcs7: Invalid padding")
|
|
}
|
|
return data[:length-padLen], nil
|
|
}
|
|
|
|
func main() {
|
|
scanner = *bufio.NewScanner(os.Stdin)
|
|
home := os.Getenv("HOME")
|
|
dataPath = os.Getenv("XDG_DATA_HOME")
|
|
cachePath = os.Getenv("XDG_CACHE_HOME")
|
|
if len(dataPath) < 1 {
|
|
dataPath = home + "/.local/share"
|
|
}
|
|
if len(cachePath) < 1 {
|
|
cachePath = home + "/.cache"
|
|
}
|
|
cachePath += "/notes/notes.db"
|
|
dataDir = dataPath + "/notes"
|
|
dataPath += "/notes/userinfo.json"
|
|
|
|
// db, err := sql.Open("sqlite", cachePath)
|
|
// if err != nil {
|
|
// log.Fatal("Error opening sqlite database for notes cache at ", cachePath, ": ", err)
|
|
// }
|
|
args := os.Args[1:]
|
|
var command string
|
|
if len(args) < 1 {
|
|
command = ""
|
|
} else {
|
|
command = args[0]
|
|
}
|
|
switch command {
|
|
case "l", "ls", "list", "search", "find":
|
|
userDataBytes, err := os.ReadFile(dataPath)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
login()
|
|
}
|
|
check(err, "Error reading user data at '%s': %s", dataPath)
|
|
var userData []string
|
|
err = json.Unmarshal(userDataBytes, &userData)
|
|
check(err, "Error unmarshalling user data: %s")
|
|
|
|
serverAddress, username, password = userData[0], userData[1], userData[2]
|
|
|
|
response, err := http.Get(fmt.Sprintf("https://%s/get?user=%s", serverAddress, url.QueryEscape(username)))
|
|
check(err, "Error getting notes from '%s' as '%s': %s", serverAddress, username)
|
|
defer response.Body.Close()
|
|
body, err := io.ReadAll(response.Body)
|
|
check(err, "Error reading response body while getting notes: %s")
|
|
var jsonBody [][2]string
|
|
err = json.Unmarshal(body, &jsonBody)
|
|
check(err, "Error unmarshalling response body: %s")
|
|
|
|
fzf := exec.Command("fzf")
|
|
stdin, err := fzf.StdinPipe()
|
|
check(err, "Error executing fzf: %s")
|
|
check(fzf.Start(), "Error starting fzf: %s")
|
|
|
|
decryptedBody := make([]Note, len(jsonBody))
|
|
for i := range len(jsonBody) {
|
|
err := json.Unmarshal([]byte(decrypt(jsonBody[i][1], password)), &decryptedBody[i])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if len(decryptedBody[i].Title) > 0 && len(decryptedBody[i].Body) > 0 {
|
|
fmt.Fprintf(stdin, "%s | %s\n", decryptedBody[i].Title, strings.ReplaceAll(decryptedBody[i].Body, "\n", " "))
|
|
}
|
|
}
|
|
fzf.Wait()
|
|
|
|
default:
|
|
login()
|
|
}
|
|
}
|
|
|
|
func login() {
|
|
fmt.Println("Welcome to Notes! To get started, choose your server and user.")
|
|
fmt.Print("Server Address [notes.miningtcup.me]: ")
|
|
_, err := fmt.Scanln(&serverAddress)
|
|
if err != nil {
|
|
serverAddress = "notes.miningtcup.me"
|
|
}
|
|
|
|
fmt.Print("Username: ")
|
|
scanner.Scan()
|
|
username = url.QueryEscape(scanner.Text())
|
|
|
|
fmt.Print("Password: ")
|
|
passwordBytes, err := readPassword()
|
|
check(err, "Error reading user input (password): %s")
|
|
password = url.QueryEscape(string(passwordBytes))
|
|
|
|
jsonData, err := json.Marshal([]string{serverAddress, username, password})
|
|
check(err, "Error marshalling server address and credentials: %s")
|
|
err = os.Mkdir(dataDir, 0700)
|
|
if !errors.Is(err, fs.ErrExist) {
|
|
check(err, "Error creating data directory at '%s': %s", dataDir)
|
|
}
|
|
err = os.WriteFile(dataPath, jsonData, 0600)
|
|
check(err, "Error writing to '%s': ", dataPath)
|
|
|
|
fmt.Println("\nAwesome! To get started, run 'notes add' or 'notes list'")
|
|
}
|
|
|
|
func decrypt(data string, password string) string {
|
|
dataBytes, err := base64.StdEncoding.DecodeString(data)
|
|
check(err, "Error decoding base64 data: %s")
|
|
|
|
if len(dataBytes) < 32 {
|
|
return ""
|
|
}
|
|
|
|
iv := dataBytes[0:16]
|
|
salt := dataBytes[16:32]
|
|
ciphertext := dataBytes[32:]
|
|
|
|
if len(ciphertext)%aes.BlockSize != 0 {
|
|
fmt.Printf("Ciphertext length %d is not a multiple of block size %d\n", len(ciphertext), aes.BlockSize)
|
|
return ""
|
|
}
|
|
|
|
key, err := pbkdf2.Key(sha256.New, password, salt, 65536, 32)
|
|
check(err, "Error deriving pbkdf2-sha256 key from password: %s")
|
|
block, err := aes.NewCipher(key)
|
|
check(err, "Error creating AES key: %s")
|
|
mode := cipher.NewCBCDecrypter(block, iv)
|
|
|
|
plaintext := make([]byte, len(ciphertext))
|
|
mode.CryptBlocks(plaintext, ciphertext)
|
|
|
|
strippedPlaintext, err := pkcs7strip(plaintext, 16)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return string(strippedPlaintext)
|
|
}
|