From a8693c0c11ce47d3e2d0394b255e333aa0391802 Mon Sep 17 00:00:00 2001 From: Ted Pier Date: Mon, 25 Aug 2025 13:43:16 -0700 Subject: [PATCH] Initial commit --- go.mod | 18 ++++ go.sum | 49 ++++++++++ lib.go | 303 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 370 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 lib.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..756384c --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module miningtcup.me/notes-lib + +go 1.24.6 + +require modernc.org/sqlite v1.38.2 + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect + golang.org/x/sys v0.34.0 // indirect + modernc.org/libc v1.66.3 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..aac187a --- /dev/null +++ b/go.sum @@ -0,0 +1,49 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= +modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= +modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= +modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM= +modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= +modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= +modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/lib.go b/lib.go new file mode 100644 index 0000000..d2c590b --- /dev/null +++ b/lib.go @@ -0,0 +1,303 @@ +package noteslib + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/pbkdf2" + "crypto/rand" + "crypto/sha256" + "database/sql" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + "time" + + _ "modernc.org/sqlite" +) + +type Note struct { + Body string `json:"body"` + Color int64 `json:"color"` + Date string `json:"date"` + ID string `json:"id"` + Title string `json:"title"` +} + +func decodeColorInt(color int64) (r int64, g int64, b int64, a int64) { + // https://developer.android.com/reference/android/graphics/Color#color-ints + if color == 0 { + return 255, 255, 255, 255 + } + a = (color >> 24) & 0xff // or color >>> 24 + r = (color >> 16) & 0xff + g = (color >> 8) & 0xff + b = (color) & 0xff + return +} + +func encodeToColorInt(r int64, g int64, b int64, a int64) int64 { + // https://developer.android.com/reference/android/graphics/Color#color-ints + return (a&0xff)<<24 | (r&0xff)<<16 | (g&0xff)<<8 | (b & 0xff) +} + +func fetch(serverAddress string, username string) (jsonBody [][2]string, err error) { + response, err := http.Get( + fmt.Sprintf("https://%s/get?user=%s", + serverAddress, + url.QueryEscape(username)), + ) + if err != nil { + return + } + defer response.Body.Close() + + responseBody, err := io.ReadAll(response.Body) + if err != nil { + return + } + + err = json.Unmarshal(responseBody, &jsonBody) + if err != nil { + return + } + + return +} + +func load(db *sql.DB) (notes [][2]string, err error) { + var rowCount int + err = db.QueryRow("SELECT COUNT(*) FROM notes").Scan(&rowCount) + if err != nil { + return + } + + const limit = 100 + if rowCount > limit { // change this if there's ever an option to increase limit + rowCount = limit // change this if there's ever an option to increase limit + } + + rows, err := db.Query("SELECT * FROM notes LIMIT ?", rowCount) + if err != nil { + return + } + defer rows.Close() + + notes = make([][2]string, rowCount) + + for i := 0; rows.Next(); i++ { + var currentRow [2]string + if err != nil { + return + } + notes[i] = currentRow + } + + return +} + +func list(password string, jsonBody [2]string, notes *[]Note, wg *sync.WaitGroup) (err error) { + defer wg.Done() + + decryptedData, err := decrypt(jsonBody[1], password) + if err != nil { + return + } + + var decryptedBody Note + err = json.Unmarshal(decryptedData, &decryptedBody) + if err != nil { + return + } + + decryptedBody.ID = jsonBody[0] + if len(decryptedBody.Title) > 0 && len(decryptedBody.Body) > 0 { + *notes = append(*notes, decryptedBody) // ew + return + } + + return +} + +func del(serverAddress string, username string, uuid string, db *sql.DB) (err error) { + response, err := http.Get(fmt.Sprintf("https://%s/remove?user=%s&id=%s", serverAddress, url.QueryEscape(username), uuid)) + if err != nil { + return + } + defer response.Body.Close() + + _, err = io.ReadAll(response.Body) + if err != nil { + return + } + + stmt, err := db.Prepare("DELETE FROM notes WHERE id=?") + if err != nil { + return + } + defer stmt.Close() + + _, err = stmt.Exec(uuid) + return +} + +func create(title string, body string, newColor string, serverAddress string, username string, password string, templateNote Note) (err error) { + aInt, err := strconv.ParseInt(newColor[0:2], 16, 64) + if err != nil { + return + } + + rInt, err := strconv.ParseInt(newColor[2:4], 16, 64) + if err != nil { + return + } + + gInt, err := strconv.ParseInt(newColor[4:6], 16, 64) + if err != nil { + return + } + + bInt, err := strconv.ParseInt(newColor[6:8], 16, 64) + if err != nil { + return + } + + note := Note{ + Title: title, + Body: strings.ReplaceAll(body, "/", "\n"), + Color: encodeToColorInt(rInt, gInt, bInt, aInt), + Date: strconv.FormatInt(time.Now().UnixMilli(), 10), + } + jsonNote, err := json.Marshal(note) + if err != nil { + return + } + + data, err := encrypt(jsonNote, password) + if err != nil { + return + } + + response, err := http.Get( + fmt.Sprintf( + "https://%s/new?user=%s&data=%s", + serverAddress, url.QueryEscape(username), + url.QueryEscape(data), + ), + ) + if err != nil { + return + } + + defer response.Body.Close() + _, err = io.ReadAll(response.Body) + return +} + +func deriveKey(password string, salt []byte) (block cipher.Block, err error) { + key, err := pbkdf2.Key(sha256.New, password, salt, 65536, 32) + if err != nil { + return + } + + block, err = aes.NewCipher(key) + return +} + +func decrypt(data string, password string) (strippedPlaintext []byte, err error) { + dataBytes, err := base64.StdEncoding.DecodeString(data) + if err != nil { + return + } + + dataBytesLength := len(dataBytes) + if dataBytesLength < 32 { + err = fmt.Errorf("Data is of length %d, 32 required.", dataBytesLength) + return + } + + iv := dataBytes[0:16] + salt := dataBytes[16:32] + ciphertext := dataBytes[32:] + + ciphertextLength := len(ciphertext) + if ciphertextLength%aes.BlockSize != 0 { + err = fmt.Errorf("Ciphertext length %d is not a multiple of block size %d\n", ciphertextLength, aes.BlockSize) + return + } + + plaintext := make([]byte, ciphertextLength) + block, err := deriveKey(password, salt) + if err != nil { + return + } + + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(plaintext, ciphertext) + + strippedPlaintext, err = unpad(plaintext, 16) + return +} + +func encrypt(data []byte, password string) (encrypted string, err error) { + salt := make([]byte, 16) + iv := make([]byte, 16) + rand.Read(salt) + rand.Read(iv) + + plaintext, err := pad(data, 16) + if err != nil { + return + } + + block, err := deriveKey(password, salt) + if err != nil { + return + } + + mode := cipher.NewCBCEncrypter(block, iv) + ciphertext := make([]byte, len(plaintext)) + mode.CryptBlocks(ciphertext, plaintext) + + encrypted = base64.StdEncoding.EncodeToString( + bytes.Join( + [][]byte{ + iv, + salt, + ciphertext, + }, + []byte(""), + ), + ) + return +} + +func pad(buf []byte, size int) ([]byte, error) { + // https://github.com/mergermarket/go-pkcs7/blob/master/pkcs7.go + bufLen := len(buf) + padLen := size - bufLen%size + padded := make([]byte, bufLen+padLen) + copy(padded, buf) + for i := range padLen { + padded[bufLen+i] = byte(padLen) + } + return padded, nil +} + +func unpad(padded []byte, size int) (buf []byte, err error) { + if len(padded)%size != 0 { + err = fmt.Errorf("Padded value wasn't in correct size for pkcs7") + return + } + + bufLen := len(padded) - int(padded[len(padded)-1]) + buf = make([]byte, bufLen) + copy(buf, padded[:bufLen]) + return +}