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) }