From fe4bdc9fbaece01fbaa28cf1fb5e83b1458c906f Mon Sep 17 00:00:00 2001 From: Cobin Bluth Date: Fri, 30 Apr 2021 21:41:29 +0200 Subject: [PATCH] init --- Readme.md | 41 +++++++ cmd/pbin/pbin.go | 126 ++++++++++++++++++++ go.mod | 8 ++ go.sum | 9 ++ hosts.go | 139 ++++++++++++++++++++++ pbin.go | 297 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 620 insertions(+) create mode 100644 Readme.md create mode 100644 cmd/pbin/pbin.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 hosts.go create mode 100644 pbin.go diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..8cd291c --- /dev/null +++ b/Readme.md @@ -0,0 +1,41 @@ +pbin +--- + + +this is an experimental project for putting pastes on privatebin, see here for a public directory: https://privatebin.info/directory/ + + +Install: +--- +install the normal "go" way: +``` +go get github.com/cbluth/pbin/cmd/pbin +``` +or download a binary from the releases page: +- "..." + +Usage: +--- + +Upload Paste: +``` +$ echo "anything" | pbin +https://privatebin.net/?5f9fc3956e8bc7bd#8NBafBFyqKWZrqPHiw4hC1JkL9Vx9mxEUGtXBT5wLNJF +``` + +Upload Base64 Paste: +``` +$ cat cat-meme.gif | pbin -base64 +https://privatebin.net/?c3dad23d043b0675#EEwJs9g3jSMC9gMHk5Gt5ptVDYpLXzCJMhP4Ufu3C3bf +``` + +Download Paste: +``` +$ pbin https://privatebin.net/?908a9812a167d638#AKQaAp7bwC9t7gLBJkLXxJt1ZQQyW4bfjnBCzbn73c95 +## prints to stdout +``` + +Download Base64 Paste: +``` +$ pbin -base64 https://privatebin.net/?c3dad23d043b0675#EEwJs9g3jSMC9gMHk5Gt5ptVDYpLXzCJMhP4Ufu3C3bf > cat-meme.gif +``` diff --git a/cmd/pbin/pbin.go b/cmd/pbin/pbin.go new file mode 100644 index 0000000..990d335 --- /dev/null +++ b/cmd/pbin/pbin.go @@ -0,0 +1,126 @@ +package main + +import ( + "bytes" + "encoding/base64" + "fmt" + "io" + "io/ioutil" + "log" + "net/url" + "os" + "strings" + + "github.com/cbluth/pbin" +) + +var ( + getURL *url.URL + outFile string + base64Mode bool + burnAfterRead bool +) + +func init() { + args := os.Args[1:] + for i, arg := range args { + switch arg { + case "-base64", "-b64": + { + base64Mode = true + } + case "-burn": + { + burnAfterRead = true + } + case "-o": + { + if !(len(args) > i+1) { + panic("missing output arg") + } + outFile = args[i+1] + } + } + if strings.HasPrefix(arg, "https://") { + u, err := url.Parse(arg) + if err != nil { + panic(err) + } + getURL = u + } + } +} + +func main() { + err := cli() + if err != nil { + panic(err) + } +} + +func cli() error { + switch { + case getURL != nil: + { + return get() + } + case getURL == nil: + { + return put() + } + } + return nil +} + +func put() error { + info, err := os.Stdin.Stat() + if err != nil { + return err + } + if info.Mode()&os.ModeNamedPipe == 0 { + log.Fatalln("no pipe input, TODO print help") + } + b, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return err + } + if base64Mode { + b = []byte(base64.StdEncoding.EncodeToString(b)) + } + pbin.BurnAfterReading = burnAfterRead + p, err := pbin.CraftPaste(b) + if err != nil { + return err + } + ur, _, err := p.Send() + if err != nil { + return err + } + fmt.Println(ur) + return nil +} + +func get() error { + b, err := pbin.GetPaste(getURL) + if err != nil { + return err + } + if base64Mode { + b, err = base64.StdEncoding.DecodeString(string(b)) + if err != nil { + return err + } + } + if outFile != "" { + err = ioutil.WriteFile(outFile, b, 0644) + if err != nil { + return err + } + } else { + _, err = io.Copy(os.Stdout, bytes.NewReader(b)) + if err != nil { + return err + } + } + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..77576f1 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/cbluth/pbin + +go 1.16 + +require ( + github.com/gearnode/base58 v0.0.0-20200201175139-69e2d70f0e30 + golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a8faa26 --- /dev/null +++ b/go.sum @@ -0,0 +1,9 @@ +github.com/gearnode/base58 v0.0.0-20200201175139-69e2d70f0e30 h1:RPq056iW9QyucBAxibIUIEUaRk8FvT/QwB3YbJPbxpg= +github.com/gearnode/base58 v0.0.0-20200201175139-69e2d70f0e30/go.mod h1:DVEyvP0OdbwmKHqpF7etLRKaGpAiNh+w66wVI3VzEzo= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/hosts.go b/hosts.go new file mode 100644 index 0000000..74481e7 --- /dev/null +++ b/hosts.go @@ -0,0 +1,139 @@ +package pbin + +import ( + mrand "math/rand" + "net" + "net/url" + "time" +) + +var ( + hosts = processHosts() +) + +type ( + host struct { + URL *url.URL + } +) + +func processHosts() []host { + hsts := []host{} + for _, h := range []string{ + // see: https://privatebin.info/directory/ + "https://bin.snopyta.org/", + "https://encryp.ch/note/", + "https://paste.0xfc.de/", + "https://paste.rosset.net/", + "https://pastebin.grey.pw/", + "https://privatebin.at/", + "https://privatebin.silkky.cloud/", + "https://zerobin.thican.net/", + "https://ceppo.xyz/PrivateBin/", + "https://paste.itefix.net/", + "https://paste.nemeto.fr/", + "https://paste.systemli.org/", + "https://privatebin.net/", + "https://bin.idrix.fr/", + "https://bin.veracry.pt/", + "https://snip.dssr.ch/", + "https://paste.oneway.pro/", + "https://paste.eccologic.net/", + "https://paste.rollenspiel.monster/", + "https://chobble.com/", + "https://bin.acquia.com/", + "https://p.kll.li/", + "https://paste.3q3.de/", + "https://pb.envs.net/", + "https://paste.fizi.ca/", + "https://bin.infini.fr/", + "https://criminal.sh/pastes/", + "https://pwnage.xyz/pastes/", + "https://secure.quantumwijeeworks.ru/", + "https://paste.whispers.us/", + "https://тайны.миры-аномалии.рф/", + "https://paste.d4v.is/", + "https://bin.mezzo.moe/", + "https://pastebin.aquilenet.fr/", + "https://pastebin.hot-chilli.net/", + "https://bin.xsden.info/", + "https://pad.stoneocean.net/", + "https://bin.moritz-fromm.de/", + "https://extrait.facil.services/", + "https://paste.i2pd.xyz/", + "https://paste.momobako.com/", + "https://bin.privacytools.io/", + "https://paste.taiga-san.net/", + "https://sw-servers.net/pb/", + "https://wtf.roflcopter.fr/paste/", + "https://paste.plugily.xyz/", + "https://awalcon.org/private/", + "https://t25b.com/", + "https://paste.acab.io/", + "https://zb.zerosgaming.de/", + "https://p.dousse.eu/", + "https://code.wt.pt/", + "https://ookris.usermd.net/", + "https://bin.lznet.dev/", + "https://gilles.wittezaele.fr/paste/", + "https://tromland.org/privatebin/", + "https://www.c787898.com/paste/", + "https://paste.dismail.de/", + "https://paste.tuxcloud.net/", + "https://files.iya.at/", + "https://bin.iya.at/", + "https://pb.nwsec.de/", + "https://privatebin.freinetz.ch/", + "https://paste.tech-port.de/", + "https://bin.nixnet.services/", + "https://zerobin.farcy.me/", + "https://paste.tildeverse.org/", + "https://paste.biocrafting.net/", + "https://vim.cx/", + "https://0.jaegers.net/", + "https://paste.jaegers.net/", + "https://bin.bissisoft.com/", + "https://bin.hopon.cam/", + } { + u, err := url.Parse(h) + if err != nil { + panic(err) + } + hsts = append(hsts, host{u}) + } + return hsts +} + +func pickRandom(n int) []host { + mrand.Seed(time.Now().UnixNano()) + mix := mrand.Perm(len(hosts)) + hsts := []host{} + for _, v := range mix[:n] { + hsts = append(hsts, hosts[v]) + } + return hsts +} + +func (h *host) ping() bool { + c, err := net.DialTimeout("tcp", net.JoinHostPort(h.URL.Hostname(), "443"), 5*time.Second) + if err != nil { + return false + } + defer c.Close() + return c != nil +} + +func findFastest() host { + r := pickRandom(5) + ping := make(chan host) + go func(ch chan host) { + for _, hs := range r { + go func(c chan host, h host) { + if h.ping() { + c <- h + } + }(ch, hs) + } + }(ping) + return <-ping +} diff --git a/pbin.go b/pbin.go new file mode 100644 index 0000000..145ed9c --- /dev/null +++ b/pbin.go @@ -0,0 +1,297 @@ +package pbin + +import ( + "bytes" + "compress/flate" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/gearnode/base58" + "golang.org/x/crypto/pbkdf2" +) + +var ( + OpenDiscussion bool + BurnAfterReading bool +) + +const ( + APIVersion int = 2 + Iterations int = 100000 // kdf iterations + KDFSecretSize int = 32 // bytes + AESKeySize int = 32 // bytes + NonceSize int = 12 // bytes + SaltSize int = 8 // bytes + TagSize int = 128 // bits + EncryptionAlgorithm string = "aes" + EncryptionMode string = "gcm" + DataCompression string = "zlib" + Format string = "syntaxhighlighting" + Expiry string = "1week" + // OpenDiscussion bool = false + // BurnAfterReading bool = false +) + +type ( + Paste struct { + Version int // 2 + ClearTextData []byte + ClearJSONData []byte + CipherJSONData []byte + RequestBodyJSONData []byte + KDFSecret [KDFSecretSize]byte + AESKey [AESKeySize]byte + Salt [SaltSize]byte + Nonce [NonceSize]byte // IV + Expire string + OpenDiscussion bool + BurnAfterReading bool + DisplayFormat string + } +) + +func CraftPaste(bytes []byte) (*Paste, error) { + p := &Paste{ + Version: APIVersion, + DisplayFormat: Format, + ClearTextData: bytes, + } + copy(p.Salt[:], randomBytes(SaltSize)) + copy(p.Nonce[:], randomBytes(NonceSize)) // IV + copy(p.KDFSecret[:], randomBytes(KDFSecretSize)) + p.Expire = Expiry + p.DisplayFormat = Format + p.OpenDiscussion = OpenDiscussion + p.BurnAfterReading = BurnAfterReading + err := p.encrypt() + if err != nil { + return nil, err + } + req := map[string]interface{}{} + req["v"] = APIVersion + req["adata"] = p.makeAData() + req["meta"] = map[string]interface{}{} + req["meta"].(map[string]interface{})["expire"] = Expiry + req["ct"] = base64.RawStdEncoding.EncodeToString(p.CipherJSONData) + p.RequestBodyJSONData, err = json.Marshal(&req) + if err != nil { + return nil, err + } + + return p, nil +} + +func (p *Paste) Send() (*url.URL, map[string]interface{}, error) { + host := findFastest() + req, err := http.NewRequest(http.MethodPost, host.URL.String(), bytes.NewBuffer(p.RequestBodyJSONData)) + if err != nil { + return nil, nil, err + } + req.Header.Set("X-Requested-With", "JSONHttpRequest") + client := &http.Client{} + res, err := client.Do(req) + if err != nil { + return nil, nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, nil, errors.New("error from server: " + host.URL.String()) + } + resBody, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, nil, err + } + resm := map[string]interface{}{} + err = json.Unmarshal(resBody, &resm) + if err != nil { + return nil, nil, err + } + if resm["status"].(float64) != 0 { + return nil, nil, errors.New("error from server: " + resm["message"].(string)) + } + purl, err := url.Parse(host.URL.String() + "?" + resm["id"].(string) + "#" + base58.Encode(p.KDFSecret[:])) + if err != nil { + return nil, nil, err + } + return purl, resm, nil +} + +func randomBytes(n int) []byte { + k := make([]byte, n) + _, err := rand.Read(k[:n]) + if err != nil { + panic(err) + } + return k +} + +func (p *Paste) encrypt() error { + err := (error)(nil) + p.ClearJSONData, err = json.Marshal( + &map[string]interface{}{ + "paste": string(p.ClearTextData), + }, + ) + if err != nil { + return err + } + copy(p.AESKey[:], makeAESKey(p.KDFSecret[:], p.Salt[:])) + c, err := aes.NewCipher(p.AESKey[:]) + if err != nil { + return err + } + gcm, err := cipher.NewGCM(c) + if err != nil { + return err + } + adata, err := json.Marshal(p.makeAData()) + if err != nil { + return err + } + b := bytes.Buffer{} + w, err := flate.NewWriter(&b, flate.BestCompression) + if err != nil { + return err + } + _, err = w.Write(p.ClearJSONData) + if err != nil { + return err + } + err = w.Close() + if err != nil { + return err + } + p.CipherJSONData = gcm.Seal(nil, p.Nonce[:], b.Bytes(), adata) + return nil +} + +func (p *Paste) makeAData() []interface{} { + openDiscussion := int(0) + burnAfterRead := int(0) + if p.OpenDiscussion { + openDiscussion = 1 + } + if p.BurnAfterReading { + burnAfterRead = 1 + } + return []interface{}{ + []interface{}{ + base64.RawStdEncoding.EncodeToString(p.Nonce[:]), // IV + base64.RawStdEncoding.EncodeToString(p.Salt[:]), // salt + Iterations, + 256, + TagSize, + EncryptionAlgorithm, + EncryptionMode, + DataCompression, + }, + Format, + openDiscussion, + burnAfterRead, + } +} + +func makeAESKey(secret []byte, salt []byte) []byte { + return pbkdf2.Key( + secret, + salt, + Iterations, + AESKeySize, + sha256.New, + ) +} + +func GetPaste(ur *url.URL) ([]byte, error) { + pID := ur.RawQuery + b58Pass := ur.Fragment + hostURL := strings.Split(ur.String(), "?")[0] + pasteDataURL := hostURL + "?pasteid=" + pID + req, err := http.NewRequest(http.MethodGet, pasteDataURL, nil) + if err != nil { + return nil, err + } + req.Header.Set("X-Requested-With", "JSONHttpRequest") + client := &http.Client{} + res, err := client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + b, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + m := map[string]interface{}{} + err = json.Unmarshal(b, &m) + if err != nil { + return nil, err + } + p := &Paste{} + if v, ok := m["ct"]; !ok { + return nil, errors.New("missing ct") + } else { + p.CipherJSONData, err = base64.RawStdEncoding.DecodeString(v.(string)) + if err != nil { + return nil, err + } + } + if v, ok := m["adata"]; !ok { + return nil, errors.New("missing adata") + } else { + nonceData, err := base64.RawStdEncoding.DecodeString(((v.([]interface{})[0]).([]interface{})[0]).(string)) // wtf + if err != nil { + return nil, err + } + copy(p.Nonce[:], nonceData) + saltData, err := base64.RawStdEncoding.DecodeString(((v.([]interface{})[0]).([]interface{})[1]).(string)) // wtf + if err != nil { + return nil, err + } + copy(p.Salt[:], saltData) + } + secret, err := base58.Decode(b58Pass) + if err != nil { + return nil, err + } + aesKey := makeAESKey(secret, p.Salt[:]) + c, err := aes.NewCipher(aesKey) + if err != nil { + return nil, err + } + gcm, err := cipher.NewGCM(c) + if err != nil { + return nil, err + } + adata, err := json.Marshal(p.makeAData()) + if err != nil { + return nil, err + } + flated, err := gcm.Open(nil, p.Nonce[:], p.CipherJSONData, adata) + if err != nil { + return nil, err + } + fr := flate.NewReader(bytes.NewBuffer(flated)) + defer fr.Close() + unflated, err := ioutil.ReadAll(fr) + if err != nil { + return nil, err + } + pd := map[string]interface{}{} + err = json.Unmarshal(unflated, &pd) + if err != nil { + return nil, err + } + if v, ok := pd["paste"]; ok { + return []byte(v.(string)), nil + } + return nil, errors.New("missing paste data") +}