diff --git a/README.md b/README.md index 75751c8..4dca12c 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,11 @@ Store and Retrieve data from `pastila` in Go for fun a profit-loss ```go import ("github.com/metrico/pasticca/paste") ``` + +## Example +See `main.go` + +#### Plaintext/JSON ```go // Example: Save some content content := "This is a test paste." @@ -22,12 +27,44 @@ if err != nil { } fmt.Printf("Loaded content: %s\nIs encrypted: %v\n", loadedContent, isEncrypted) ``` - -## Example -See `main.go` - ``` Saved paste with fingerprint/hash: 913ae2b1/748ab86a806c2de1fd5753fb3ffff516 Loaded content: This is a test paste. Is encrypted: false ``` + +#### Encrypted +```go +// Example: Save encrypted content +encryptedContent := "This is a secret message." +encryptedFingerprint, encryptedHashWithAnchor, err := paste.Save(encryptedContent, "", "", true) +if err != nil { + log.Fatalf("Error saving encrypted paste: %v", err) +} +fmt.Printf("Saved encrypted paste with fingerprint/hash: %s/%s\n", encryptedFingerprint, encryptedHashWithAnchor) + +// Artificial Delay +time.Sleep(1 * time.Second) + +// Example: Load and decrypt the encrypted content +decryptedContent, isStillEncrypted, err := paste.Load(encryptedFingerprint, encryptedHashWithAnchor) +if err != nil { +log.Fatalf("Error loading encrypted paste: %v", err) +} +fmt.Printf("Loaded and decrypted content: %s\nIs still encrypted: %v\n", decryptedContent, isStillEncrypted) + +// Example: Try to load encrypted content without the key +encryptedHashWithoutAnchor := strings.Split(encryptedHashWithAnchor, "#")[0] +encryptedContentWithoutKey, isEncryptedWithoutKey, err := paste.Load(encryptedFingerprint, encryptedHashWithoutAnchor) +if err != nil { + log.Fatalf("Error loading encrypted paste without key: %v", err) +} +fmt.Printf("Loaded encrypted content without key: %s\nIs encrypted: %v\n", encryptedContentWithoutKey, isEncryptedWithoutKey) +``` +``` +Saved encrypted paste with fingerprint/hash: b4765c53/94cf5b7bee267b1d41c9ada746ebe6e1#FFUgNmg29LqBLdN3LQdfzw== +Loaded and decrypted content: This is a secret message. +Is still encrypted: true +Loaded encrypted content without key: T2ZudNTJcSKSCkod+eUeHp9LnupkOSBl6OlL6ts7Uss0LvrtPMoFrDI= +Is encrypted: true +``` diff --git a/paste/paste.go b/paste/paste.go index 89f972d..b325acb 100644 --- a/paste/paste.go +++ b/paste/paste.go @@ -14,7 +14,6 @@ import ( "regexp" "strings" "fmt" - "io/ioutil" ) const clickhouseURL = "https://play.clickhouse.com/?user=paste" @@ -29,7 +28,14 @@ type DataResponse struct { } // Load retrieves data from Clickhouse -func Load(fingerprint, hash string) (string, bool, error) { +func Load(fingerprint, hashWithAnchor string) (string, bool, error) { + parts := strings.SplitN(hashWithAnchor, "#", 2) + hash := parts[0] + var key string + if len(parts) > 1 { + key = parts[1] + } + query := fmt.Sprintf(` SELECT content, is_encrypted FROM data @@ -40,8 +46,6 @@ func Load(fingerprint, hash string) (string, bool, error) { FORMAT JSON `, fingerprint, hash) - // fmt.Printf("Debug: Executing query: %s\n", query) // Debug print - resp, err := http.Post(clickhouseURL, "application/x-www-form-urlencoded", strings.NewReader(query)) if err != nil { return "", false, fmt.Errorf("HTTP request failed: %v", err) @@ -52,13 +56,11 @@ func Load(fingerprint, hash string) (string, bool, error) { return "", false, fmt.Errorf("HTTP status %s", resp.Status) } - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return "", false, fmt.Errorf("failed to read response body: %v", err) } - // fmt.Printf("Debug: Response body: %s\n", string(body)) // Debug print - var response DataResponse err = json.Unmarshal(body, &response) if err != nil { @@ -69,11 +71,52 @@ func Load(fingerprint, hash string) (string, bool, error) { return "", false, fmt.Errorf("paste not found or multiple rows returned (rows: %d)", response.Rows) } - // Convert uint8 to bool + content := response.Data[0].Content isEncrypted := response.Data[0].IsEncrypted != 0 - return response.Data[0].Content, isEncrypted, nil + if isEncrypted && key != "" { + decryptedContent, err := DecryptContent(content, key) + if err != nil { + return "", true, fmt.Errorf("failed to decrypt content: %v", err) + } + content = decryptedContent + } + + return content, isEncrypted, nil } + +// DecryptContent decrypts the content using the provided key +func DecryptContent(encryptedContent, keyBase64 string) (string, error) { + key, err := base64.StdEncoding.DecodeString(keyBase64) + if err != nil { + return "", fmt.Errorf("failed to decode key: %v", err) + } + + ciphertext, err := base64.StdEncoding.DecodeString(encryptedContent) + if err != nil { + return "", fmt.Errorf("failed to decode ciphertext: %v", err) + } + + block, err := aes.NewCipher(key) + if err != nil { + return "", fmt.Errorf("failed to create cipher: %v", err) + } + + if len(ciphertext) < aes.BlockSize { + return "", errors.New("ciphertext too short") + } + + iv := ciphertext[:aes.BlockSize] + ciphertext = ciphertext[aes.BlockSize:] + + stream := cipher.NewCTR(block, iv) + plaintext := make([]byte, len(ciphertext)) + stream.XORKeyStream(plaintext, ciphertext) + + return string(plaintext), nil +} + + // Save stores data in Clickhouse func Save(content, prevFingerprint, prevHash string, isEncrypted bool) (string, string, error) { text := content