pax_global_header00006660000000000000000000000064147160775020014523gustar00rootroot0000000000000052 comment=1915299fe443d6fa53dd33c060a0a0e8f8eaa76e privatebin-cli-2.0.2/000077500000000000000000000000001471607750200144345ustar00rootroot00000000000000privatebin-cli-2.0.2/.github/000077500000000000000000000000001471607750200157745ustar00rootroot00000000000000privatebin-cli-2.0.2/.github/workflows/000077500000000000000000000000001471607750200200315ustar00rootroot00000000000000privatebin-cli-2.0.2/.github/workflows/release.yaml000066400000000000000000000011111471607750200223270ustar00rootroot00000000000000name: "release" on: pull_request: push: tags: - "*" permissions: contents: "write" jobs: goreleaser: runs-on: "ubuntu-latest" steps: - run: "sudo apt-get install pandoc" - uses: "actions/checkout@v4" with: fetch-depth: 0 - uses: "actions/setup-go@v5" with: go-version: "1.22" - uses: "goreleaser/goreleaser-action@v5" with: distribution: "goreleaser" version: "latest" args: "release --clean" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} privatebin-cli-2.0.2/.gitignore000066400000000000000000000000331471607750200164200ustar00rootroot00000000000000bin/* man/* dist/ vendor/ privatebin-cli-2.0.2/.goreleaser.yaml000066400000000000000000000020041471607750200175220ustar00rootroot00000000000000version: 1 before: hooks: - "make man" - "go mod vendor" report_sizes: true builds: - id: "privatebin" env: - "CGO_ENABLED=0" - "GO111MODULE=on" main: "./cmd/privatebin" binary: "privatebin" goos: - "windows" - "linux" - "darwin" - "freebsd" - "openbsd" goarch: - "amd64" - "arm" - "arm64" goarm: - "6" - "7" flags: - "-trimpath" - "-mod=readonly" ldflags: - "-s -w -X main.version=v{{.Version}} -X main.commit={{.ShortCommit}} -X main.date={{.Date}}" release: github: owner: "gearnode" name: "privatebin" draft: true prerelease: "auto" archives: - format: "tar.gz" format_overrides: - goos: "windows" format: "zip" files: - "LICENSE.txt" - "README.md" source: enabled: true name_template: "{{ .Tag }}-source" format: "tar.gz" files: - "vendor" - "man" checksum: algorithm: "sha512" changelog: disable: true privatebin-cli-2.0.2/CHANGELOG.md000066400000000000000000000033351471607750200162510ustar00rootroot00000000000000# Introduction All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [2.0.1] - 2024-04-15 ### Fixed - Top level flags are not handled. ## [2.0.0] - 2024-04-11 ### Added - Add `privatebin show` command. - Add `privatebin create` command. ### Changed - Minimal Golang version is now v1.22. - Minimal PrivateBin instance version is now 1.7. - Configuration use kebab-case instead of sake-case. ## [1.4.0] - 2023-01-08 ### Added - Add `-gzip` flag to compress data with gzip. ### Changed - According to OWAP recommendation, increase the number of PBKDF2 iterations. ## [1.3.0] - 2022-11-06 ### Added - Add `-filename` flag to read file instead of stdin. - Add `-attachment` flag to update data as an attchment. ### Changed - Upgrade to Go 1.19. - Use `gearno.de` import url. ### Fixed - Create request error not handled. ## [1.2.0] - 2022-09-04 ### Added - Add privatebin version through the `-version` flag. ### Fixed - Add `User-Agent` request header to mitigate WAF (Cloudflare, etc.) blocking request from the CLI. ## [1.1.1] - 2022-07-20 Nothing. ## [1.1.0] - 2022-06-23 ### Added - Add privatebin paste password support. Via the optional `-password` flag. ## [1.0.1] - 2022-01-20 ### Fixed - Missing URL path on the returned URL. ## [1.0.0] - 2021-09-06 ### Added - Add privatebin(1) man page. - Add privatebin.conf(5) man page. ### Changed - Makefile is now BSD and GNU compatible. - Configuration file is now stored in the `~/.config/privatebin/config.json`. ## [0.1.0] - 2021-05-19 - First release. privatebin-cli-2.0.2/GNUmakefile000066400000000000000000000026141471607750200165110ustar00rootroot00000000000000PREFIX = /usr/local BINDIR = $(PREFIX)/bin MANDIR = $(PREFIX)/share/man MKDIR = mkdir -p GO = go PANDOC = pandoc INSTALL = install RM = rm -f DATETIME = "Apr 15, 2024" VERSION = 2.0.1 LDFLAGS = -ldflags "-X 'main.cliVersion=$(VERSION)'" BIN = bin/privatebin .PHONY: all build man install uninstall clean all: build man build: $(GO) build $(LDFLAGS) -o $(BIN) cmd/privatebin/main.go cmd/privatebin/cfg.go man: @$(MKDIR) man $(PANDOC) --standalone --to man -M footer=$(VERSION) -M date=$(DATETIME) doc/privatebin.1.md -o man/privatebin.1 $(PANDOC) --standalone --to man -M footer=$(VERSION) -M date=$(DATETIME) doc/privatebin-create.1.md -o man/privatebin-create.1 $(PANDOC) --standalone --to man -M footer=$(VERSION) -M date=$(DATETIME) doc/privatebin-show.1.md -o man/privatebin-show.1 $(PANDOC) --standalone --to man -M footer=$(VERSION) -M date=$(DATETIME) doc/privatebin.conf.5.md -o man/privatebin.conf.5 install: build man $(INSTALL) -m 755 $(BIN) $(BINDIR)/privatebin $(INSTALL) -m 644 man/privatebin.1 $(MANDIR)/man1/privatebin.1 $(INSTALL) -m 644 man/privatebin-create.1 $(MANDIR)/man1/privatebin-create.1 $(INSTALL) -m 644 man/privatebin-show.1 $(MANDIR)/man1/privatebin-show.1 $(INSTALL) -m 644 man/privatebin.conf.5 $(MANDIR)/man5/privatebin.conf.5 uninstall: $(RM) $(BINDIR)/privatebin $(RM) $(MANDIR)/man1/privatebin.1 $(RM) $(MANDIR)/man5/privatebin.conf.5 clean: $(RM) -r bin man privatebin-cli-2.0.2/LICENSE.txt000066400000000000000000000013621471607750200162610ustar00rootroot00000000000000Copyright (c) 2020-2024 Bryan Frimin . Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. privatebin-cli-2.0.2/README.md000066400000000000000000000051171471607750200157170ustar00rootroot00000000000000

PrivateBin CLI Logo
A powerful CLI for creating and managing PrivateBin pastes with ease.

GitHub License GitHub Tag

## Overview PrivateBin's secure and anonymous paste service is indispensable for many developers and privacy enthusiasts. Recognizing the need for a more efficient way to interact with PrivateBin from the terminal, I developed this CLI tool. It's designed to seamlessly integrate with your workflow, enabling swift creation and management of pastes. ## Installation `privatebin` can be installed using a prebuilt binary, through package managers, or from source. Follow the instructions below for your preferred method. ### Arch Linux Available on the Arch User Repository (AUR). Install using your favorite AUR helper: - [privatebin-cli](https://aur.archlinux.org/packages/privatebin-cli/) - Release package #### Example Installation: ```console yay -Sy privatebin-cli ``` ### Prebuilt binary Prebuilt binaries are available for a variety of operating systems and architectures. Visit the latest release page, and scroll down to the Assets section. 1. Download the archive for the desired edition, operating system, and architecture 2. Extract the archive 3. Move the executable to the desired directory 4. Add this directory to the PATH environment variable 5. Verify that you have execute permission on the file ### Build from Source 1. Clone the repository: git clone https://github.com/gearnode/privatebin.git 2. Navigate to the project directory: cd privatebin 3. Build the project (binary and man pages): make 4. Install the binary and man pages on your system: make install ## Usage Create a paste from a file: cat resume.txt | privatebin create Display a paste: privatebin show https://privatebin.net/?420fc9597328c72f#EezApNVTTRUuEkt1jj7r9vSfewLBvUohDSXWuvPEs1bF ## Documentation For detailed information on all CLI commands and features, check out the [handbook](doc/handbook.md). ## Support Encountered a bug or have questions? Feel free to open a GitHub issue or contact me directly via [email](mailto:bryan@frimin.fr). ## License This project is released under the ISC license. See the [LICENSE.txt](LICENSE.txt) file for details. It's designed with both openness and freedom of use in mind, but with no warranty as per the ISC standard disclaimer. privatebin-cli-2.0.2/adata.go000066400000000000000000000071111471607750200160350ustar00rootroot00000000000000// Copyright (c) 2020-2024 Bryan Frimin . // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY // AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. package privatebin import ( "encoding/base64" "encoding/json" ) type ( AData struct { Spec Spec Formatter string OpenDiscussion bool BurnAfterReading bool } Spec struct { IV []byte Salt []byte Iterations int KeySize int TagSize int Algorithm EncryptionAlgorithm Mode EncryptionMode Compression CompressionAlgorithm } ) func (adata AData) MarshalJSON() ([]byte, error) { return json.Marshal( [4]any{ adata.Spec, adata.Formatter, btoi(adata.OpenDiscussion), btoi(adata.BurnAfterReading), }, ) } func (adata *AData) UnmarshalJSON(data []byte) error { var values [4]json.RawMessage err := json.Unmarshal(data, &values) if err != nil { return err } var spec Spec err = json.Unmarshal(values[0], &spec) if err != nil { return err } var ( formatter string openDiscussion, burnAterReading int ) err = json.Unmarshal(values[1], &formatter) if err != nil { return err } err = json.Unmarshal(values[2], &openDiscussion) if err != nil { return err } err = json.Unmarshal(values[3], &burnAterReading) if err != nil { return err } *adata = AData{spec, formatter, itob(openDiscussion), itob(burnAterReading)} return nil } func (spec Spec) MarshalJSON() ([]byte, error) { return json.Marshal( [8]any{ base64.StdEncoding.EncodeToString(spec.IV), base64.StdEncoding.EncodeToString(spec.Salt), spec.Iterations, spec.KeySize, spec.TagSize, spec.Algorithm, spec.Mode, spec.Compression, }, ) } func (spec *Spec) UnmarshalJSON(data []byte) error { var values [8]json.RawMessage err := json.Unmarshal(data, &values) if err != nil { return err } var ( encodedIv, encodedSalt string iv, salt []byte iterations, keySize, tagSize int algorithm EncryptionAlgorithm mode EncryptionMode compression CompressionAlgorithm ) err = json.Unmarshal(values[0], &encodedIv) if err != nil { return err } iv, err = decode64(encodedIv) if err != nil { return err } err = json.Unmarshal(values[1], &encodedSalt) if err != nil { return err } salt, err = decode64(encodedSalt) if err != nil { return err } err = json.Unmarshal(values[2], &iterations) if err != nil { return err } err = json.Unmarshal(values[3], &keySize) if err != nil { return err } err = json.Unmarshal(values[4], &tagSize) if err != nil { return err } err = json.Unmarshal(values[5], &algorithm) if err != nil { return err } err = json.Unmarshal(values[6], &mode) if err != nil { return err } err = json.Unmarshal(values[7], &compression) if err != nil { return err } *spec = Spec{iv, salt, iterations, keySize, tagSize, algorithm, mode, compression} return nil } privatebin-cli-2.0.2/cmd/000077500000000000000000000000001471607750200151775ustar00rootroot00000000000000privatebin-cli-2.0.2/cmd/privatebin/000077500000000000000000000000001471607750200173425ustar00rootroot00000000000000privatebin-cli-2.0.2/cmd/privatebin/cfg.go000066400000000000000000000062721471607750200204370ustar00rootroot00000000000000// Copyright (c) 2020-2024 Bryan Frimin . // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY // AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. package main import ( "encoding/json" "fmt" "io" "os" ) type ( AuthCfg struct { Username string `json:"username"` Password string `json:"password"` } BinCfg struct { Name string `json:"name"` Host string `json:"host"` Auth AuthCfg `json:"auth"` Expire string `json:"expire"` OpenDiscussion *bool `json:"open-discussion"` BurnAfterReading *bool `json:"burn-after-reading"` GZip *bool `json:"gzip"` Formatter string `json:"formatter"` ExtraHeaderFields map[string]string `json:"extra-header-fields"` } Cfg struct { Bin []BinCfg `json:"bin"` Expire string `json:"expire"` OpenDiscussion bool `json:"open-discussion"` BurnAfterReading bool `json:"burn-after-reading"` GZip bool `json:"gzip"` Formatter string `json:"formatter"` ExtraHeaderFields map[string]string `json:"extra-header-fields"` } ) func defaultConfig() *Cfg { return &Cfg{ Expire: "1day", Formatter: "plaintext", GZip: true, ExtraHeaderFields: make(map[string]string), } } func findBinCfg(cfg *Cfg, name string) (*BinCfg, error) { for _, bin := range cfg.Bin { if bin.Name == name { return &bin, nil } } return nil, fmt.Errorf("cannot find %q bin configuration", name) } func loadCfgFile(path string) (*Cfg, error) { file, err := os.Open(path) if err != nil { return nil, fmt.Errorf("cannot open file: %v", err) } value, err := io.ReadAll(file) if err != nil { return nil, fmt.Errorf("cannot read file: %v", err) } cfg := defaultConfig() if err := json.Unmarshal(value, cfg); err != nil { return nil, fmt.Errorf("cannot unmarshal file: %v", err) } for i, binCfg := range cfg.Bin { if binCfg.Expire == "" { binCfg.Expire = cfg.Expire } if binCfg.OpenDiscussion == nil { binCfg.OpenDiscussion = &cfg.OpenDiscussion } if binCfg.BurnAfterReading == nil { binCfg.BurnAfterReading = &cfg.BurnAfterReading } if binCfg.Formatter == "" { binCfg.Formatter = cfg.Formatter } if binCfg.GZip == nil { binCfg.GZip = &cfg.GZip } if binCfg.ExtraHeaderFields == nil { binCfg.ExtraHeaderFields = cfg.ExtraHeaderFields } cfg.Bin[i] = binCfg } return cfg, nil } privatebin-cli-2.0.2/cmd/privatebin/main.go000066400000000000000000000215711471607750200206230ustar00rootroot00000000000000// Copyright (c) 2020-2024 Bryan Frimin . // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY // AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. package main import ( "context" "encoding/base64" "encoding/json" "fmt" "io" "net/url" "os" "path" "path/filepath" "strings" "github.com/spf13/cobra" "go.gearno.de/privatebin/v2" ) var ( version = "dev" commit = "unknow" date = "unknow" userAgent = "privatebin-cli/" + version + " (source; https://go.gearno.de/privatebin)" cfgPath string binName string extraHeaderFields []string client *privatebin.Client binCfg *BinCfg output string ctx = context.Background() clientOptions = []privatebin.Option{ privatebin.WithUserAgent(userAgent), } expire string openDiscussion bool burnAfterReading bool gzip bool formatter string password string filename string attachment bool insecure bool confirmBurn bool rootCmd = &cobra.Command{ Use: "privatebin", Version: fmt.Sprintf("%s-%s (%s)", version, commit, date), Short: "A streamlined CLI for effortlessly creating and managing PrivateBin pastes", PersistentPreRunE: func(cmd *cobra.Command, args []string) error { switch output { case "": case "json": default: return fmt.Errorf("invalid output: %q, valid options are '', 'json'", output) } if cfgPath == "" { homeDir, err := os.UserHomeDir() if err != nil { return fmt.Errorf("cannot get user home directory: %w", err) } cfgPath = path.Join(homeDir, ".config", "privatebin", "config.json") } cfg, err := loadCfgFile(cfgPath) if err != nil { return fmt.Errorf("cannot load configuration: %w", err) } binCfg, err = findBinCfg(cfg, binName) if err != nil { return fmt.Errorf("cannot find %q bin configuration: %w", binName, err) } clientOptions = append( clientOptions, privatebin.WithBasicAuth( binCfg.Auth.Username, binCfg.Auth.Password, ), ) for k, v := range binCfg.ExtraHeaderFields { clientOptions = append( clientOptions, privatebin.WithCustomHeaderField(k, v), ) } for _, value := range extraHeaderFields { parts := strings.SplitN(value, ":", 2) if len(parts) != 2 { return fmt.Errorf("invalid header field format: '%s', expected 'key: value'", value) } clientOptions = append( clientOptions, privatebin.WithCustomHeaderField( strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]), ), ) } host, err := url.Parse(binCfg.Host) if err != nil { return fmt.Errorf("cannot parse %q bin %q host: %w", binCfg.Name, binCfg.Host, err) } client = privatebin.NewClient(*host, clientOptions...) return nil }, } showCmd = &cobra.Command{ Use: "show", Short: "Show a paste", SilenceUsage: true, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { link, err := url.Parse(args[0]) if err != nil { return fmt.Errorf("cannot parse paste url: %w", err) } if link.Scheme+"://"+link.Host != binCfg.Host { if !insecure { return fmt.Errorf("untrusted privatebin instance use --insecure flag or add it to the configuration") } } options := privatebin.ShowPasteOptions{ Password: []byte(password), ConfirmBurn: confirmBurn, } result, err := client.ShowPaste(ctx, *link, options) if err != nil { return fmt.Errorf("cannot show paste: %w", err) } switch output { case "": fmt.Fprintf(os.Stdout, "%s\n", result.Paste.Data) case "json": var comments []map[string]string for _, comment := range result.Comments { comments = append( comments, map[string]string{ "comment_id": comment.CommentID, "paste_id": comment.PasteID, "parent_id": comment.ParentID, "nickname": comment.Nickname, "text": comment.Text, }, ) } json.NewEncoder(os.Stdout).Encode( map[string]any{ "paste_id": result.PasteID, "paste": map[string]string{ "attachment_name": result.Paste.AttachmentName, "attachment": base64.StdEncoding.EncodeToString(result.Paste.Attachement), "data": base64.StdEncoding.EncodeToString(result.Paste.Data), }, "comment_count": result.CommentCount, "comments": comments, }, ) } return nil }, } createCmd = &cobra.Command{ Use: "create", Short: "Create a paste", SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { if cmd.Flags().Changed("expire") { binCfg.Expire = expire } if cmd.Flags().Changed("open-discussion") { binCfg.OpenDiscussion = &openDiscussion } if cmd.Flags().Changed("burn-after-reading") { binCfg.BurnAfterReading = &burnAfterReading } if cmd.Flags().Changed("gzip") { binCfg.GZip = &gzip } if cmd.Flags().Changed("formatter") { binCfg.Formatter = formatter } var ( attachementName string data []byte err error ) if cmd.Flags().Changed("filename") { file, err := os.Open(filename) if err != nil { return fmt.Errorf("cannot open %q file: %w", filename, err) } data, err = io.ReadAll(file) if err != nil { return fmt.Errorf("cannot read %q file: %w", filename, err) } if cmd.Flags().Changed("attachment") { attachementName = filepath.Base(filename) } } else { data, err = io.ReadAll(os.Stdin) if err != nil { return fmt.Errorf("cannot read stdin: %w", err) } if cmd.Flags().Changed("attachment") { attachementName = "stdin" } } options := privatebin.CreatePasteOptions{ AttachmentName: attachementName, Formatter: binCfg.Formatter, Expire: binCfg.Expire, OpenDiscussion: *binCfg.OpenDiscussion, BurnAfterReading: *binCfg.BurnAfterReading, Password: []byte(password), Compress: privatebin.CompressionAlgorithmNone, } if *binCfg.GZip { options.Compress = privatebin.CompressionAlgorithmGZip } result, err := client.CreatePaste(ctx, data, options) if err != nil { return fmt.Errorf("cannot create the paste: %w", err) } switch output { case "": fmt.Fprintf(os.Stdout, "%s\n", result.PasteURL.String()) case "json": json.NewEncoder(os.Stdout).Encode( map[string]any{ "paste_id": result.PasteID, "paste_url": result.PasteURL.String(), "delete_token": result.DeleteToken, }, ) } return nil }, } ) func init() { rootCmd.PersistentFlags().StringVarP(&output, "output", "o", "", "the command output format") rootCmd.PersistentFlags().StringVarP(&cfgPath, "config", "c", "", "the config file (default is $HOME/.config/privatebin/config.json)") rootCmd.PersistentFlags().StringVarP(&binName, "bin", "b", "", "the name of the privatebin instance to use (default \"\")") rootCmd.PersistentFlags().StringSliceVarP(&extraHeaderFields, "header", "H", []string{}, "extra HTTP header fields to include in the request sent") createCmd.Flags().StringVar(&expire, "expire", "", "the time to live of the paste") createCmd.Flags().BoolVar(&openDiscussion, "open-discussion", false, "enable discussion on the paste") createCmd.Flags().BoolVar(&burnAfterReading, "burn-after-reading", false, "delete the paste after reading") createCmd.Flags().BoolVar(&gzip, "gzip", true, "gzip the paste data") createCmd.Flags().StringVar(&formatter, "formatter", "", "the text formatter to use, can be plaintext, markdown or syntaxhighlighting") createCmd.Flags().StringVar(&password, "password", "", "the paste password") createCmd.Flags().StringVar(&filename, "filename", "", "read filepath instead of stdin") createCmd.Flags().BoolVar(&attachment, "attachment", false, "create the paste as an attachment") showCmd.Flags().BoolVar(&insecure, "insecure", false, "allow reading paste from untrusted instance") showCmd.Flags().BoolVar(&confirmBurn, "confirm-burn", false, "confirm paste opening, it will be deleted immediately afterwards") showCmd.Flags().StringVar(&password, "password", "", "the paste password") rootCmd.AddCommand(showCmd, createCmd) } func main() { rootCmd.Execute() } privatebin-cli-2.0.2/compression_algorithm.go000066400000000000000000000031471471607750200213770ustar00rootroot00000000000000// Copyright (c) 2020-2024 Bryan Frimin . // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY // AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. package privatebin import ( "encoding/json" ) const ( CompressionAlgorithmUnknow CompressionAlgorithm = iota CompressionAlgorithmNone CompressionAlgorithmGZip ) type CompressionAlgorithm uint8 func (ca CompressionAlgorithm) MarshalJSON() ([]byte, error) { return json.Marshal(ca.String()) } func (ca *CompressionAlgorithm) UnmarshalJSON(data []byte) error { var ( v CompressionAlgorithm s string ) err := json.Unmarshal(data, &s) if err != nil { return err } switch s { case "none": v = CompressionAlgorithmNone case "zlib": v = CompressionAlgorithmGZip default: v = CompressionAlgorithmUnknow } *ca = v return nil } func (ca CompressionAlgorithm) String() string { switch ca { case CompressionAlgorithmNone: return "none" case CompressionAlgorithmGZip: return "zlib" default: return "unknow" } } privatebin-cli-2.0.2/doc/000077500000000000000000000000001471607750200152015ustar00rootroot00000000000000privatebin-cli-2.0.2/doc/handbook.md000066400000000000000000000004641471607750200173140ustar00rootroot00000000000000# Introduction This document is the handbook for the privatebin command line interface. Documentation is now available as man pages: - [privatebin(1)](privatebin.1.md) - [privatebin-create(1)](privatebin-create.1.md) - [privatebin-show(1)](privatebin-show.1.md) - [privatebin.conf(5)](privatebin.conf.5.md) privatebin-cli-2.0.2/doc/logo.png000066400000000000000000003106511471607750200166550ustar00rootroot00000000000000PNG  IHDRߊsRGB IDATx^ eYY&313kbFv^nmSQ@D EJmAi]^+kѧ@EQUVEeYsΙ1 {{Ȉq#2#n"3}YAA@A@-  @]&  0 JA@!t  >(]A@e C tAA@B9   BC0A@A@]  0 JA@!t  >(]A@e C tAA@B9   BC0A@A@]  0 JA@!t  >(]A@e C tAA@B9   BC0A@A@]  0 JA@!t  >(]A@e C tAA@B9   BC0A@A@]  0 JA@!t  >(]A@e C tAA@B9   BC0A@A@]  0 JA@!t  >(]A@e C tAA@B9   BC0A@A@]  0 JA@!t  >(]A@e C tAA@B9   BC0A@A@]  0 JA@!t  >(]A@e C tAA@B9   BC0A@A@]  0 JA@!t  >(]A@e C tAA@B9  zX4~&>%m/$MoX Saއ޻ G@}O`+#<ӿ#4W5(UPFH7]TyHQE+vP[i BΣ/}߲_|;kBC砵B xJCP О{ߥ:1q>T8tumYp6E@}t{k"fq񪇣Y *VP?4Ghh4!$@+" ]{k"%Bo̥[?I֭ztGRvb>C3+9;s:!EQT !R/D+&^_w a&  !m5٭@߸xO H<ҝ:a5#ttZ|ҽ'rW1&@ 4/FԓHKw>  !4-~R;~3GD1XJD-p: !۪ԑ2oԐ=4E!׊ 9ߧ$(*VZAS*_?rÏ\ke !m4խ>|M٤49x:F&f%f,M:vOlmک Y#/ 3UF=D PoG;Oo D@@}{r O|ixU:!bkvqfe[mbq~L%ߋ{W924hE$ JW|x-4QoaNnvO5[gQ N! 8#M P EdN}t 99O%&OTN.kL 9E6! Wn-@vOƍ㪨P Plg6P2Tc76_!v"%kkH=B ModAic{ozGa_9r lBRX ol67.*$&S]MҶ Isǚ Ρp$SlS>I5b_!ˈtd ~cTŅ0A`}B__`I&BOGdzܰ:)'r@g wYIЂB_4^pƯ;֭r#A@^OA@X>|izoG( Qӧ{Hk+d [ \8 -[ޝT]{e;s>!:ڬPsCH[ R$"oO3&/OemU L Zh<7%D꣨ux ЊdYs[KPnEN4tuyܗLB粪=*<7U>zRx*ai+%Њv:zkKfP\ABV@Ԛ~^ #D^:k~שXyymT\Yߘ="j D7yqvL^3\v3,#ƙhGn4 Gl.vޔl5iIRA ##}2U{n?ѐ+A}$9om5]__Dk(#@o^d@c#h/%@kB16NsX0ۆ(wBݾ't>W5K>cH*6~BHs)!Z"B-\3c_ɛ'!U%noOwD'G^= wiXDi)&1L/@@]s8]RN ^WKs; 91g eY#c:;5#EJъH=G #׼x?+_+Bu"P{=Ө=<+HOMHT7-5z^ҵ7F@Өg1v4m bﺯ[~l<6гbA`BA. c'{&J Cde6n\mNoF>^gy+:BgC.ДN{@3/ ?U蓟\r 0JV@؇v+7[Q-؎mI-qCSXFc_{K; tUyNvm9%r.V>0. &.8]?sp]!78B[|#~~7VPs(sj֨[B(ܕ3d~^! tWr)Mj·VR* h;{'8;/AB:2fuhBS\ҋHS QZ*MB޳5yBOl6-&&", VRD-W⹉W3W!W [!9n5">W,7(QnR6Y $-aT>{VLNKV]$ƯLC欓^'r𭏳^D*"lS;aɳylOEq/6u;%r 00J'VB@]'Vwi uĀL/Ō"{^+boJ?Z+ꕿ_j?2Cbntu#wM:IQFKUVPS7ݡ&52r B'zo};C9jIڂJS4!Nly\qSvNĿL)ΩlЧ,=l*ߙB)K6g2bA_z{KoaB:oAEL T 10E4tdkֲjeeq:OVÔ˃q_e?}#]73hdj|8)I{M!҈*ݍ!Q W<3+ii t;rnH@@8Z$$CdICڂ($[BeNz =;M|m[Ӷ<%u=NMY#; I, WB~8IIh1.E8/vSj] !<{]O'?^fѮi ;%Qlg FeR}ވix(O4aQzRi?/ϘzӇ:(6! Vn{n>O)Ñp~ܦ$H԰E0@3..\ݯGa?u^~h?6d:~3DwNaB_QǽCn ]6b-c-M%[A!0sMڍlޠs>٦"'<ЙحF $Y Mpr!"A/EPŝ~}7Ek_B懶C>j,>cJIP.s Ȝ%e2a ?g<$LڔY۷xŎoyZFҼ2%m<ϻkBym.+p(Q)7S‰[PS5V,z )ɓGϼNEBSH;5<3".->:ZۄeZswSZ'ջ%|vcNgJR30צ%3a[ 8)؁$G6z+{%ԣ*>ܱ[{lCC:I8wߙ4Ϡ6$NSi*[۲ȞZ}s6ZBR8VA; {31CllVz8a eQ#_@w L9' Ug[y/Br.К#(1I5tXAKЊ Վ׿]{MH3F@]&¦B`w_=[). 14QK&t$&/-L+3~db1S;v~L_jŽ3[SbrlҰm)c%&˕s9iPAP؁$J'ߪ\3E@}c>|I(U㲦K!ԭyNS7*_ч;۳:;[=mٽΩN;RuǓݚ! ;бN7u/-x^u9ڛ4گ%Rv1LkD||r,/ Ƥ]z{ >!i6Ѱnצoב$'t}]Q'0̛d/D4 mhRRS'V If0 [n3tɘ]e\QfbC;GCކu=319TqnX?&|Bs{4NG1hchbPĭ?Q:$4c" }ty[^,-)̪'Vyԗ; Vrb_w kAR9d3Jl|SA Qufv44v* M}(1]t;}#yWˑ|-ONBR\րÔ"b'Qk< VU~I;Bg7MO>'Cqq 2\lvS;W*N(K9K+8v;3Q׎LH3x%S'N:`"K┐^H6NHWbګBE$)-sT?H# ]fP "69$nKlD?\mঙO>KShC"cymDTZC!191VyeC25T=#m:=R7xu IEcFT/@PPe׭ߧ&z 5Ê&W_=?-^^g-b*҆lTm8e7su;P\ҹH]M3O|Ov 6!yk & @D|SW/K@8fXBj^ q)7Q,΢U; /mR1umn4u{ f9w|(ߔl 5lCjr)<$_(B l-DaJxk^#w76KߥÉp~⎝ _ }͕pi4 &yjCLNp\4G5-^n]v$[#J7N\oOP"ZI-"B˯Mv] @\@A)R~BJnmIͦmm15e3 ҒP%7]Ƞ)f.*o6)˻ϞR.Jhj!՛~x޹V@jߋJx^ra@Fv HxpIy:L+7"JSȡ$'SϜgjT39٨YH3=5t 1vX [Q݀gS|ť dm};2$ N&K8Ǭsț!:e'ˈbO'/;_  R5{ uJS-Ε"2$wnxwÝ/&~{;|6ZQ06hLȠ-U<.>Ms!QaJFrh)Rho=W[o־n>HK3꓏EނwwZ`_r<ԽTm%a͔턓-?]AXW鐗frZ.-%u,o^4W*˲F~N^x> PŋQ]k-8b"∰ ߼J)#W5*@ZGFMH/4*Pkkj+z1@x^WE;%r΢MIƖkvmy by P`!GԚ`t %,ߜ(Dzۦ.< ^(/@ڎN7|,+=Рo BUZ!NjUW-VuG7ݐ{CDz,>}7WJICZP'n%#-Ix)Ƀ;7Н$1\W871j$|+ɆU1tX.L`t| ]@4Z'bF M_|gYiC@Ϡ=JǀK8}nxPf _l0]Кd'8:&iN~c8%ust F8AnS?.K /~#  w( к%:(6jPK: =p7<+{ުյ7ƑjJE=268b}m(l9*D"(^ \+=qDY)~sv{z˘;B͙JN K3[:`4I/&nJx 96GMET>&N*yO~ /=Uݒr]NFbsw-D$u?1q'n!0l>DGg 0xΰE_JxT;e9B n9ɒxMy!&) hcދbj}=0z#x;CNzHm8,V$tиw$)UKs&O)Fk>4vg}AeYw& x@[{hcHKwÛR_~]Ag0b"}:q9VЧhBۦ,ebd5E=-Φ奼Sp5d&BwI.뺕3k"tNBM>F;k\"cl+:`ʡI9MNi;& :9Ʊ;5P}<Ɍ>sHg{Ѫ}%FxGSp6Xu1c< ..qw7 3딢H*۲Z=vg{JT< ?mg$ Dg0e?$֡4Uw[.JC vSxW?B4 8fg:52| Nt҂dת!DJ A |Ў4 *z3y~ }蠄٦p Tw5J t89F.˅e!on`%VlhQϜ蝽ڍG@Jkr6 7WX#wm?T&{ 4^ Ŷe\( Avj#0fo+ѕ֩y' O$FфR*XS E /H*"0ށע4v3ɛ/j$̪#ڒ2Abyu .uZF.L܆Gu=8) 'P;z7j3 H/"ю( ~r \ %zOEPl48tj&`ʙLqc<Ұ9qtwő^{ БQ:Z-5݆B8xjTnxxkAsEN a+`4Tm'[ƯEG0PR*iJyRzRFM_Ɲ2yT9Ҭ) V5o(yr3wgMlHq4 |5/(4 e61P60?{dz9_gBckޑw{&7k CmNҧ8 L(MQ/LReL 2dN88z(yO"I5)Aũ(05mfy?w\Hz}%\Ҡ&͘V|p}J02EFЎ=Ԣ2e?|Jj_3MF0oFϼ뽳3ytE A$Y'v\ԙqر}dǼ.AldECGUVcg٥_rNjEE-5p6|16W+TD6;Ϲ8/Lzӻ_VG=qj"ͯGe['T3t"t8'$~M_jNCp-<~!Q5(cLhɹ;]R7^y»QpB-82NCB _bhc/Ȏ׾- _ZԐ$B[rV/YI(caʖX;' ҦJ>2H :L\O Jz|(GX? כϴsιŊXL/*_S7rYIii$\KDֆ;u'AKy%}-?g%gpn*N´xkIAS "an^$iYTEs_o6}[N^F xMlX&'bwbC,\ w:Up2B`efqSr6iڕjr,ejX'1sWǖM$eRV2>6;^/2Z)1Go)qZi}薬yo)p8Hu+4 %_'8OF4w%op Ea"sa:n _chLQ |!T&C9QfDFNe624є#֙HٷiHYlsBo9nCM0% usSNrtkPM} ut! d}=/<+0|onsH 3uR˳l " ѲoL?]ϜgSgR2"rY>"5DBLx9_ 5hĩQXd n'KKٽtmܔȏ!:C^0nK }SOZC9J>BSx].uqРb{|4> >_4 D:e1U(JϠv6$>"s:^%)|2 QogD_^BRwy|]짼!ʡ_Arr#ۉ؅o ??rV@@}+*X{؏bJI [_2z!NpV^xг4Kh whM v8r[BIA@5=$I[/ [0U}Bk;G^ͻbRȞ#lp&֜fs;,"}]x]N XI-QYfp} ':2ӗS{uswId$Ub{z"#t %sS]@;)"N( WޓL}Ǐc6C.$lL$]^ӿ7>7sEoŇQM=Z͑mcq^_}#pQzuhƄZn#tN|ֻ8EhGuM뜾kR^rx5!'Jx:F~Ly 0y+SQyם<#bjIpv~ 8_xFϡl'uh@I:t!8s^Kjqo̗s~ ˥/hIdd%BOlOH ŮTAkJWzg %$7Pz tߣr"P )FJP^&nH`"wh7 ~y*'Sbrra=4¼p.9I{Πne,gw\"%]ZLFTF"MtqpP&" .S/iIur"6RB'2:/wY2>a?γĘs^ 37K)W=][˃rc7}K/:'h,~}_G(&؄ֽ{SUW$l_aU$VA%ŻyΌz[K>?Ve9#0r|ۿwΟSe*'RHxR2iYd*9-#o$!-5x3/VVLdHE)kȫ)NX@3݁mع~ IDAT[^`'FX"[s*{vb1fߙNj@ #.)uچ Wg'}ˋB'wSD5p{߆ڑiWNjOy/G:Ϩ =JS2~3G*OZ0-v]nݸ10J!\D|D"-N"-\ݯ}rhy~s?|e;7 ?Earrʩ )=μF2]yIX7>9GS'E9:1EcW`jMi˚ǜ4&0IKNRP Nۨ< uQ߻-nS'?Mm tR|8IDFn7L0Q`n4#(GQM(d^ r#B)Oм\tz>SFΖn:Wdͭ*X-^DiD*IFn+~LMj-9" ~n^Ӵ|qřGQF^ZCVHa;f#g!;Y͈ND@pOޏ <rڠ8z߆UV}ši h꒯y tH*ӄrS6I6(H)RPBGJ7SG^.[6ArKuصi( GRiS6GW}*1*ˣnr |>ЙV& \ʑÒBSՂ_ȡ ^-&S{YlKv%Ls2*kt?BxwM"bș8@@St |JsHvq HbH 33Z`!vXI=%Ry3dw>d5!u:|N;gz{1s>x!8EWXy4m-o:ِxP VwR/39+ǀOd\x҈Z =B9'oxy,go4}F7C{歿~"g@.*fAM٢C#组Sh[{&88՝niHΡʽ;\KfS?M˽3S#?p*L~5W :9ZJȑf붲nyrKg=Mi\Kt%!DjDާG0!O#)wc|:/* @aҔƤ|]Mv u(SםЬPx:)IRG@D1藀G1}nxѳc9i^4K4ݻHzKH膇|W2YM Efu(hh&[ ۍVzď?;,kkǶS:/gƪ v B%(w5->\@BeyT Y5id-=ul\>m"o V ᕨdI5Vb5VzP t"EFn%`HI"M(( )DK`YܟnV77hA \SQ蹃|q 9x^#]AIN.u@xM8g qžxdE."ޗS{osțS禬qg B/Lr,SRvy{Q^BQRבY3˿|Y:f}̝H_ѳ+=[S`I弮䧚s9;֢QDU(OKr! q}o7֣?WËx*S>^:jmmGHb4Dç93AFhN\j\=[)HIUh/&v@_Kơ3db$Č7X>󲦳\L ;5N?##i<$FYx"f. !"Z R RHvL=sjwbuCsvΑi:zuďg&|lGCw>众#h21ROtn犱b&6~LvZ 4k)\2)T1GsT^7+;r >xC?ka/j}P()yB+}VaƓ<FA+H]C%oq#ĒGOh^_@fz34$fnG6 d"(^oFn@6)cpz^E]Ur> N4{JJ#I^5Tp:>ew_aN?}AODo?e3+68.AscpT$qz"l$@ ͙"8{fT.aNeu2~\䜍A@}cp뮍p l?TI*"i\xE9-ZhqW67͔K^M;o69˾ AޥgrmYȝjвg氓[hzVT)4uQJP >ey+^oںmLZ .k[Fzn{ny6жTg.J&w5bUE}/ת=g] ׌ڣu,me2u5*~ΫZ;-,G3ǹi5%iJ!5W̢*2(L5q5Fve\8i WVh3ጼJ!f]Teғm\ եvV a V 6ɶmP63Ns>,%Nu{ N^o-~S1 ^ﰤN>Н\p0[C.1P-^ < / :y7fO| 8FY;J5Hӥe|jB؏!m/6vDv:_Ν{wh=j ~A#9t8?Rl?z+f\C'kgjg Q3껧x&"ro)+oӊhQq5ʻnFktE3mscK6Br8 :a¥XݽF)1٭ΦٕNߜBjo>MFW3VbupH >u4PD.{%l8wiV|S9_9MTD4&HѢ5^[0u7锉[Gm9fL2YW3!M#w=j3I܄Oi~鉯Ƈ1mɝS re|}N+jd6]d\DX5wp/d ml1ES{zqugl.ugicM z֬6 gҬ x5(\`ExFua"L0XfIJHxmj_7r&[iI `pjsy ?Q(rhqZxX<dꎛ<2͎F(Gjq,GS c(\g6R~*UA+C8i3hX6c9V"cqSYm@1Ԏ 텇OcEamѳr&nSإ5N~^aXymef*h&cXΫU|spiL?jj<0mb'ƞ'R$/GykS2]Mk˹:%\mX ~ aIeR0$K{KE14w0kPgPQl|qC&3cԻƈ]m t ޕs}~("}R.Q+]A SFߜvm >]=~7GPj5Jֳ(ofB?#Û6T!CjFjTM;R S"9LBDt"4uPSTBhB(״͠F Zzp&%;Q ?^4+ٱ̈́qPt@}00(ZG<a,ŠGIM'y#!P)xn"JHT͔򮗠‹0@e6 g7D:6O2=fS2xq.jlq4FkK<=v.cz |xJq}T.477.[Щ09H!O ny AFI!S}oF/!dK[K + dYzs9W1&}T6" Aiv "j4Svn#ә#[эc܉LZtiC$\Bt ~0ȟ4Gqb/&Epf¤ J ]Q JPrx3 Ȳok+$)03prN8 $z x`'J7[!e(|әNE3*JI:^j|]w jl"iiEcwCX8}/鯢6[SKHS5{mNq]eИM; gyM 9G[vU:Vr}_,}cWFVioI.6Ѳ-Hv4f2$؃(zMhr!v^ܩi sPxawLK]N-:XJ׋Mpvm@t@LV"PPhqTx^Ev(PbYڗ:'zt$kyWZ$t1:;0Wywi8;Qbo7(߂`x*2bǭǔʸ9ȭ\z泦UmJܜǀ5.)c_b=՞EXGf% (3%MDsAQ1TqȺ.=-n ۅӕBs9;5ύSJu@!B^VB?؟:VLۉ2uʩ E1Z ToBuՀ䓜3A3fR`:.Uqs:۲IraldM!U).9FEA`aLVSJSL@>g)sm=T^Dv oV\z6.w8ဲر#v#(`R(/o{p Pĺ _Cdjd  +z7y7nb"zrP#:yN:B2k4D)B7lBKفA8\%Zjv98#Dڎépv|_Y]z#dž"pަ ?:-OiPcTq.[6]$ޛ8ugb{ /(.%ٲlglklYbx8q2T0ʌ=SW͌cY-%KDXW*. D/oI}9{n4@s[ T!q ʯБï ^ ZVu-f#[nN\OeWX@_Q85m9\6}\;AM ϣ1ԧ />j8(PbqGxdSk zG~޶v~yFWې"h zj¶s K-e RD6!k47"}?*~^g+J:I0,mC>9.i)5O~E=㝙!.eyo=b!JK~]-tJ!mz>C>Todᘙ~|k>1g7rJ9]лyVl4Gm& IFr-@e Iq`w/2mPޘջYqumIT,uqvw/2jP IDAT|i ~ } "Uo"Y*"(l(>y>箻i_ҋcS}"aٝ,'" |d>),I [ŐEBploO1ƾ^*F+Ɣ s.4d9ͲDJXxXeK7o!`9®h u8b=lVhք(G/ڕ5[AgL~=rVGǹ a;*@.]A44B,ZhPY|{P] xCHȕ#Op6iΕٰp^kt(YfnnlUW(Mx,7/G_SE9LrKI>nm Eu p8VTUՎ`o{%8 '<>Ҟm=9߀ \Iv%7KzqNgVN9@)@ua&o=r{izHA㝿9jjQXX^Kii[{f "+M~0Q9>z0I ^0 %pd줳a u/0|sK6D@@5ܬ=b$ ҋb"_!mU֖j/0quYVe`/9Wd.gKJpr[a*ۢCE.I r$$ @܀w#CWs O(PODPjP73DV EiZ@iwBx KEDY.iuOE_Be9woSδlR Z#p{@,[2:TTOW#Rz;b@Odă?- `A Geǀ~)_ Jcw9pGRr")4%ZT(00BwEaHòP539 {x ?= /M$--c.z 3. Y 8piWL  c]ȿjx s˯] ɛ%fvLYY:x@ YzemZ5]hlSGyiRN@ u!6[V7TbJl7lVsWm[gQfPF]/ts1/[+7 H}?BI 䛃'y;?=@1+}SKg .YBن+sVTYaǠ%JlH ҪgʗזnQ-!„яۀҍN7jM Ih> l8tl$ @݇P> ,'Zd<˘} gCG-B`f) ;n) 8qFK~yy=xax)zw[1}, hnY.70U6x  0"{0̱'.]c3O8\\ivU=c]3cBV(kLkX8_IA5` i? G4r0Ei̜gے I_/گm\1%yW|vGn-}o#t)콦< '=|--BdDјr^ͅ`,tPӒޕCq2R??r w\G0`F= oS7u6ar^m^~2@wsǻPkq' 4^E}%4g^F<"Fj-xi$CD&%Tҷ޾%p[ŚЕol]VEywM X}'. 2/1I3jSXo(3ytwѬ뵋/&W6 {Iq:G+Jm aT sSC;qogYmh/+oKfw<b'X8$F_5E55Ği w MW{7rP9@'QC++cr1BX\Qx/@Ǯf;[wE6-TyYd/5ula#Pϵf[MEM$f^ȒQ-]ע1$(eL[DZ1v+o]ȪѹcIإ?RqúrPZׇHvXmD<.$Ǥ-]֍,XgL+mLMvמtw7ٌg쥩A *j}H 7M3f|# ? -ق'`NlK95@<ǛpO+00&AkZhFv@ˀ7̿wG:?xmT aL(,{=ʼn-t1?>;<ݽ;|sO=yB;wU7$#}';FdER{KJVծZsŶW+/$K%VvSBBR%슍-)s*`zwn5'uD4g81+@C[j<R7=9oL#S^NJhk4!n@u$jcKs+P%U\+[ޞZ5oHh ॓CлB66 CSD!&"࿉_ { ({ D'-1m'eN 0rcG<н/nxJ7|ށy3=Oʟϡ Wט(a˅ڽjUj0zgA c!vMuޱkinQ/cۂ\gj8\=1!z*KL OHr~ 9""\F(9z,nUXnҦ,ޯeov0UI Vk[ɝ6?4:T(S-^G֑=VJCmU=?>Ui$&_iJ1"?F&-owb.W SxfwG`Kyc: `q""+e(P1Lo@߁O!Ytl5ҼC6HvI1Ow.kܶR:ڝ4h>4YD2nh/!8SONN"">Z.B&†J=qm{.qǯ @]^79TghgVյ!/OS]- IʧQCG~~6Qdg7c#=Zsc`&\ɐm_ g0$9eKS4e]QZI x,BY'+%X< ڇrf'[@y+Ji),Ip=^ݗ#(iM] ;DOuKllk_`:s{ šq/ POjhxG0qi'(Tlӫw>8t$ܣj!$mҏ!#CyEƺl%P)Բ~;w,r祻;7{jgZ3 ?K צ%+$_5+ x[Y\zyi}6{W.dZu9Z kKskt$"[,vbںf;}O끀  hEVR⊷$BQt3\%#8 SgiSUKꥯQ`>gGWr}t= 蒜dj Xx[eͥeS}P S\L a: ض>ucr xy^u TqcUċ(Va^ C7aeq7( w~[w]cϿһ{7CmFFhZ.dR&h R&bJonJ̯3;hRUE05]x!Va|j+/b6 ڶ&"*+Mة|J,].j~6}v:mN".v 0HJ72YT 0 PW)JrPם8Ϗ9'/ {oݑW0_ |Nu:ٸ,IeKR{Ћ}OݦK=#]PڬHE߷vKdde0.o0B }oYn~!ᵂd[s~-#mO2{`Ԓ\W# (]V!WIK6]r #eGVˑt(Lju#ҷO`~1xEDޜ*-9º –~[TW<P0lnYa/-kU HB GXtibd[Fb'tWI *J!sDqAtFݬ"I18$y>qП;)ISjԶhR;ewؐFK5+ d@1w0jE£*]t#^YDtӃ'5g7z!=)_vl 53rH b Q}s=U`LddC9+at~:=܊ 0K)63zќUʵ&Ѓl-w}@]9DoޛF"#fS4`^=5B)>:! rLm#Xks '}k/ADDvI?pT?ofVw(nĩd=Y{K!A+DRgn=ʠ!t_):vd - { ~aqD̞959t⑞ullޗЇC@Zл填Zb cλ7P4` 60]\ǥD-02RGƍ͡|cŋc"ޅjH3 F=]즫i+$_}p%/)v/3x]U.%c6x ; $:S:G%+K]CيVي83㣕0:G}Vhó董VEG1 z3NQݬ[+5bOx IDATVtDtk3"omƹwcC }zz\Ö 6!![h:1?V,:Oo}-ni[R59z2/\Ksn.&!w;E*ؤϱ$Eb_! #?ھsjsa0(h\ \wޡAmyg{~>qx} tۈ0$Ywr3wM |Wu}.]{E@f΂aΗjnWFl0:'~53׼'׭ގ c K]'˗uRz} +x LzA94H(Iа0>sEa組,HHM+zpR*l]M;yUjH>/olj!m̑h,.Yť(.o#H1e7RfRE= !܄ 4 /)!u4iB svtt [5etFiËH҆]lvг=}؎w6 ˝ u|6&uB fw(0X ߷5\pGjh`E{%41Rm:Q`>˜}0ruisR܍V.ʌy8HwiӀ֊;urZ d'/ S\M\[ww7v*u@;/rJ RJX*hiI(Cbht5w*3;ZLK*Is3\~ ӯ i /=L uRr#Δz@J4q$Wzͬߢmk9iEoY}|.nN0Yx4R"$f4 ݄я}w5@҇/YX[U-ЭĮrTl}:mr#e6xlЩ'`^z# .~?9,Vv\tzd?hM(ǟF-R0Ro]q:]ЋYw QGT'oSC( .ׅ{00Cg. 9_9uꚺKyC_6:s|N~HqVe>=}s{ =,EtL~D]`Gu0&~[{+h,|(g (V(cHAM":BEÙ':9,Cbd.j+ ){0iBGB3-LgX+Q ״ŔpX< ׷yKʊJH3}X,)3e/!T1׸C?U?D7i#-yK/ bY꼁R@-4U[er Nx΃#U,z8Wh|.]nzl*Z:֒}gnGAG麡Sߴzsبb7= $o ~KKTK#Ї\IY~3~A K(%M^s@u^l(b3 cD0+5ۮ[gdQBYw:SvZrH[qzfbpGQZ.g՜4a9[{ٰh գG&Y7Aݿu x386T`¦U="bzu1%cd% Tns&/]칖Db\+X}񕳡ew`4쿝*B_k'IȽ omN)n7u3X]TC+ aBVq͑|ca~6NP{`Ќ s.6d^ZdQu=7Il_8`~> kȅ2AwЯ yuЮ`3Tב;_~9j8?Qս;! 2mf,ٗBy!LR)DHzq(eP(,m+\cRϾ8w-Yϟ܉L"|x* E /%a;|j3D{rMzpۄf+oWN/p<{5`b,C\U%}ܺ_/leX :r#7me{tlRw7 ᾗ=tt%ϹQ4n\hʼ9E&Xr9vR(Kg;$;觀* Ά (Jc2H_=3%ҥ.<%:H[2 ͉}a.C8_`|3 %o :3 `|1Ͼxvzn|>R~o%SsBX^4 i:y⹃^}zC-.tWr.._ŋ@lW8imy Yd*l8-EZ^"jT=UF?xD$-mN%6rmw}tQҷ%vUWI]Ul4Vخ,5ƿ^6d pTv%Tyo rMx/>H*aKVwײHl8C]\ w gzK}E]G\e=g#C'+ Ϋ@oY#d1YhSou iZ|:_<0Z Q/ FNHZwbn] jx>qDfBa\pIkٚIyKt?i;Q^4U1R2*)]1A eH޺`z![=%ekͻ~/]75mb%5w!H bZdzheoAyCboEM f )A@<"] } eRs&Ίi.9+Wo@w>ׁn 8,B-E n׎2wj,,M4007|(eϣL1%RσOd"4E@X bQQ\Re4lҧ59-fE۠>'0N!kLRr<0"v*]<~BF6 ʸcĤ&*] &аb$.Ӹf#/-]>;<=@v}ү c)W}>܍CQJP}B&Ңx x.| ouQq&@}c]>_U8  ](v$נ8ܮ 9MJco*lR͹c :Ȣk715@}tTSlwq޽'倞"ԃP X]dT3~S[L~{$?e]"̧`瀃D آ^FQ!|Y`aLPNZIEN:7w&#Y\zf4mdA%IH.s~ɥ;bwҭ-Qa;ip,@ eݞ1{#[JhUd$$(sWbgoqx،݄I>u)3S(#93`;a mu 'Z*j^ d x+H澅Zrfz<m>#YMX'JD-(EMr0v/jd(Y2 }33 /Hzq,Jн0CC_БO{>;<[ vw'{Yg5 IDATtEK3(;`]ܙC?-5!>?:(6*"e`u)ܣEϡta!"f7*"LZfV+θ|A8e +ac>OȿOJl^mf)T$8۾?|$zp+.ΓhHdP },0̝T7ؒ'C)kPlRM:jThС < &ǒ\A;5i4UX\$TJT8cHIdHn݉']_COXM b1蜰ftW|Xl#^m&\-[[lX[5$|W6W9a'{{v=潘ވʑD(jc0*αՈgej)co;WylK׌{NkHqW} o~EƱ*1N<J-2_1:oN~! -J7I64i~#~m@Jklѓ,0aEl*0]84x:ڲBs@0Ph:~ E>ڧm$oM:)>Hq[M}`S  Y)A+c1Ci #"#+ΰ͔LIaP6*v+~n.Os= ]y-Bi5@L?{Z6o@w'O%!ݽݨ,=CB*SZ[H7d&oб~$VF5z_(,)G4gB EKb"^;VڢLɄa{6n<5 YJv=?M@)@hj[{)OK45ϣ?xC@WD?a+k䈇5C''"v&#%G10} BQ ,sAmz!`OqܗQ_G뀹;5ZH[!se<r7 tz3G{mYr9=@ߥЕÍ{Ocΐ4*\HM?0j>[Y&$BgMW ;PHʣ?IFC#1~ &*$?Ed[)̾𚏣)Qc/&}:ЕZ}f{≻HtJ9 ;qϦMՐ{N͐宠YIeZ~zW!FIanh&W0U];m 3{Sz Mw!@/ H-]m~\oW4R!Z[p]X,(IjD5 kZmZ3f? 4aiP w TQ|bI`۸`{,ݭ˶ x On=Ǯ߉w.>b$'6HqcfppX1։ P>~~?ꑟrT0U@pC-f.)T*IM,0K= ٥& |~)!Fz)B폪mVгkn}ҮKt4?m˅8qZG=Z"c+ՇҶoX=tT4Z~5  0AD4hsא ~ _B2L"K<͟;0|UGW#)now;" Oŷ1zߣ7V̕ >p>~\ɘf ?QF@O ='ЯyTѽh6*1Ɓ~9dylVzB':FĆ.0~_>~3 ֙Rn-oi~#O>KSV@WIRhWXS F W--78 ;CuT}5*Jw>V0Jits4,0U\xϰrRj=|, LvkAp2ο'8w2&~_RL,avajnQ=JcџchH]E'!S8s]cꡯg"wC1׾wo!=r;9ym ]R} YQbv2ZAԩHkh݇QJgwkvdNDnȉnN~Pi &^\_"ckn% H $q owԡ流@gμ]虧m"i-5Tk>$,zrFu#+ߍhP9U0^9]ӃQrgD4 2/aBZIr kٕݥ@od<*(ťPjuk,tXq+ CG=s|O!T"2BaG~/(= Zmx7[ݎ$v8KVB4_w2nd2f6O"zXET2B,g5ņwyh,)aw}fy}}saЋ< `hu'EJpEUP1=?T> Ó ((k2lVvLb,J}Gޤnm^xfv5c_ZHI@.h+d~#,]`p\ ԿfsA+z=/_ZK%*tv[[ڜ%_5z2R'Q᧤T{K6T`Nw| ( Ӗ ukmK>p(뀹tVC}q/cs5Zr ƁƓ?G"h^D)IqG@nCOQB换݊cwiԃ -Sʴر WnOU4-o%o8 %;- NyM@@Oߏ,~~FƘ QR5iJj޻p?G_ mr=yzzz7殞K/iwi xCs%@gnt彲ZA$C"R?uW]7`]w9WI&>RS|rf 4ޖS B&%pz S7~9$uY{{ [m pDCaȳ I*sdߏ~V4Ss/F2<GRoa?DBkNMw*ԩ*V{ѹQsG/F1b @ +h}o,Я\= @,`w zK=/ Eax}%j1~t9MsY87$3`Ko"sB shXa*Pb'+ۿPҸEpoqIbC$Uʇ2=rE͞]Đ"i$#cD7cŸp;PZ! %=Dԕ7~>.1qF"%_C΋#s?\L< 3]G9t2@N 9}%MN 3Js]jN餳`9k0E]@K<:~ p1j:q&-piR]~A"?B+ Q+Z $\iJ7%qfqQ;wX?'tE!e{pnjull_s-Z}TV ? Gky~\u(67k{4nͲMJ-=if́j<_Do"jv4!JgtEMҕ/#q=BN` JV1[2"Άr౵4Y_~ξ9,w tu k~3sɻt=hsx}{j]@oZ:rU"*0i͙XlmO:6JSw(#Jm.x+ `])RzlJ)s 0_x3FX%kH~kIع)bMԋ^69Le 7gYݡ=KtO#߼uc($'qp!N ?Oz19z1;ԇ52 O94$';Hb(\ W~jz @AMY#O+x<@8c艇SLVzIPJ  *~%PnZK /wyWa~3NKC_hol@7 @T;w3*Qj$. V؈8J9 J|7`?B_p9oX+:Ons&/@<;&"ZN>J=p 8ہOvt/1OZv^k3| I>kΟɯzF5`0r&`*U;NCخϡSZ߁^%,`s^cFكVpRFG#}!wz= =Ǫo2n=лzG9t-)j%ݗ3SQ 4HTS؈uonC 26ӎ^tXjSh& IDATv4 Q;;MK׮ {`1;}zn]m7At@ tz*tJ]B")x[f,9X5A<5~_ jÏ~ dL3磸avZ;3p590fbClNJ,SV2I t8rcٿz!a P723㭼䒎q஗zvm@NR]o2Ks`)褉r:D-6nc)7tq#LB!Aq͇<7IvK\aB@ :.q/=BȳtSv%= ˼ZCjPw#W܁nP r2YCM4m\y]nw0!Gc8i0Ư|E zTuR1m[;zYɮ=Ƴ-=/BoԣFkތڏ6)-Tãh>GGUcPሔzvmp jZi.gv9= rwЭ-oZw/> ̡_K@mt~ib"aF(xG-phe9h3{5?w" 5$,:DDS RŀIJu4Ҝ .i^SEN71j*!oFϚom _5|ek~'!D4s/~ʅsp,@̄h4. @oJ4?w)1bۉ~݋Z4_h~€NR\Ż@t7 ھޫ9t[h>^UTYhVϴk;*[m g`"MV4v 띤3Mwb$^e .MPEZݺ4%ij5$JO JŚ'[4._W鐰fVf&p%8܉^`A".:~ pXc?gb\R#pQPB鞶^Y@3'aU_!$r1n@[vD=v5PF6c;?d[:),ȱ: o̱줸:^SHek8/KQt/_ekП󝝝u./AtN*FM1ezD;9$@U ")By/=H+i;v)jx6`y~'gsNl1Z3YE6&Hq̡W;^kC%%W гekmJs՟|!Kl]uFš S(ETs;-(mo@]( :d.y) O:Biam5x |FJHJY:.s:@0΍Q-[gEmQII `S!97Oz#ȓte9\zs˦. zb5((=ZMٸ8bzx8bױʿ\w p*&55* (_0z=|[˴{h#&ӯ-5ϵ!]\eza:tn̡LeN1۫`ҽ[ `ʩȿ:gzy#_c.!_ G/$Bt*epEw21, D'`W=7G }mp1uk+x(Qy,:\bit14B뷇jFN~SBp.a|}рHI1bka]Zl͓A̡#}m-;kS)%!ŕw=%ŵ7}dЛξ8ûUf ]JYB_+!W0X;jƷcޏΛdC[͛b_ڧ]O%KY{R"67JBEOF@1m0BsQ?CԅVz1܋|( HKOnV: @sm pEexP Y]k{QM*f~ 5?%8.7Rmc=t݄:Vַ_osr4JQTuͭP)wArf#-YE "ab #j021N, O% ë# `RbW `n)k>uۧ:~YL@gm0`cMrx>p+"8&]$iarZt1~鯐Kȱ9,>ѠI#H"jm(?Π4XGw:& yMӌI ~c? wS<{&Q8g'-蜼ŪbOqa~~ߛxy&* ݌2 N#3pQȍ˅Ecv[)t e,4s<%Zd;eLxwhp.~xlOZǎ>g=]X(2COA\Y:$*a+rKT{ ,Pzq^X۔^e]'ҞgEK_D1yNO(2 m -5'@xt>PJz&tb)ȦYƍB14;ߒv?a'ћ;)P tܤ^ RuVwKL6"7P>89ҍkh1!uON|ų0$\6 dwݬt |`O;k{Lk#ƫo C7<@+t /"sf !irZ“=ٶ@Ŭj5()h}=+ލ q>bWs\R^#PcᓟA.܏SsR[,7.#JLn,Ax)8Z_~Q?^ [~rgAc?as I7:jlSO@I04(~;ÀCx2'¨6 hFW$jٯ$EtFv7 L:r,3/v:eo ˎc_4h3 N޼H;~| ˣƗL7w⡧"?;yO8ǐ ޅ@absJLȊ Cx??CFp>oDqNOӀP#%`{tY Wo=]'zT|R_OR47Z .,GD%Zz>CB #5Լ#?ut*^X~AtR_WЙE3p<Z"&i`a =W(_B=g1e~i=%>]1,Q,w8H1;zw>= t?K<%WǞ|2d_S| \ޑ=)@gW([ck^> =p# Jan䖥KIH7$++rAn&D.$ۮ! |oew G7럁_JVt4ׁ&OJӇFn686f͘`h&38fhЄ5P~ _F;aZ35(c9N - m r/IlER) A(lA=@se^@ḱ_=5q@4+7@1vЯDt 2/a=t5=TKKbekXM(EX$>9d8%G/C(xq'v' Tspzx)Qm`2skй\y3GQ W_Cۍ4+P얾T-n'g!-#i A>ѣ2 ]8_sa9O#f:Fk=V1 Ɲ;lPަ4n]51^< 5L=c'|E)ҧ`[&-RJ ( 2}²]v/lpW)街#$NEYdlyCzJqB'ڝݴdSg >ǛP\oe 44NA)GڢKxѩ?ExS T`h2kkB𴼸zM/nH}oPz7Blb3)7d &i̾:턊lao¯>xֿ7STGSCSV7geK\hb$G\H}WNe x9:i=Hۡ,KyhD};Օ~pk0ȼD6sx]@2s֡K]'r7%^i)a#$Qd&[j[g'RJ|Yd85ϹL<\9C@P>L51nwY0 ~"kjڦ #݋gX\۴TingRv3>\FmP{a@i /I:9sp :@%=PD3CCﺬSt=YĕГ(Tax4s!FVs!Iy.e?}ʽ]t6`Лy}rwTgzt1%m|: e;nB^CMPq_Bn;v$Nq"hT_WH 4ej0~ ǿrrAFRfT,Ma|oW*kQ־CʵQu)t;Q'ڻ1aRz i;q:`+8w~,K#NqC*y|6F%\8Nߊf ~ ms%u1Fe#!xSȩѓCU&<\ u%d:K9rW{6*@G+Wws=Y 5 &.K":Ts;1xǀ$X8;kHg8F }%<;xkB.:pؠO#""J ׷bk-Pz*i!Z@Gop}(}qb14S5OҁFi=\zN=ϊP1P 8Q! uc<7XK/}ZW.V6Ғkc?z aY3JD!;]O@R3},}ll5t4yw>ĘNU72 C2Jt܍X>LP>NeL"xZ`$m(D@rUF  yL-ZvDgF36P ]JĥQ PP#j )|М3U،eN`"9{?zS{g#KI}ԫ Nq & lK5ђ5S\ԗ IDATa'=BW8?8/W ^}A&)`*>uCRnNNrtGNG 蝎`ǓݒuYG0~ Ҝ%:̚OU@[TzƻEՅ嵧b%&ap܉kef[TG-yJ0)0L>VN28/y:Ҙm~l j־ڏAVӣܤgҐQ3 ./6}L.0Cţp!ėCc4C2x58RlY}{4qzBfX8K6wk"*2lr_F랄'SHJөn厊:S.f/jP5?"! }W9ήWtŀЍNXP 1.l.ԡ')sPO6)@Зx[CgQ4lm{7qzē:+vq1_1~o~Ye=Pw[4`˙ƫMmRNĻ#^O?&WaIzMЃXՑs8lZY}G=ʡ/"ʹ@RbB}!KQPZv@@ [4%c3Zclڻ9Lh\"bnnr4g|j&c.>{yP/=`-)#ǜP{ݲ4i4Ju@'&%y08Y8;P!wF#{,;Iy@\z1_:f%̤{qQ뼚to#% wC?L[/ ĭtJlם + @S;C_hXhmi 2Rwy%{O{ " 0͜"t5E^C/;{pnL-w5;pEK~鵇=6{,թ^L8+= M lg :/Tf N '0M;$N*ƭs8DXZ362d A.#,DMC""FW$uc0um֦</=|@:@ ) A@-G]U t@kHd=6Ez. uQ5zY4z$NHC2)3>:+=;qCEڦR_.18,3ΰYcSz3@wR:W]@mIӀ|yՀn<84ĽYvc/:1ކU 0Ұ =&𖸑K(_E܏!p?O}sW#%7Ӓl HHSVōp4:$s>oJq+-P h9tQ;\O|I Τl'!_.N\fmn[ Di?)!VM $PQ3~[I^>rP;'Ax:P=Mqͣ v1g2|>vH7~ `y ʭalC7,C<`(:i=t"!iR찡&=w-B;O!N؍D Kwd zE‹kUb&,-ê,PDT0ѿo#Xϯs=nܽ47bֆC 3Tjd1dsGwq@9~)ecW%zX5q ϰ.3y*pD"|AE k%><9ׄp8 ;|x6Ё r_|@gzHK>wȺvސ1@[=T"r_J@o o@7e`ɜ8 .P T.׿ [Ǡ{G٨p 8sPC@ᒸX/lЅDóm]DSfmx7Pئ˘μ-v*A]X>Hc[VC׶J80=]TU,UO*b(:t7iDqC5J{P^!-RFAR&*r] p11iCR(t1қ:C {:nU~9IHK}{Y=zCgtQup,)f=wSB oZH1J6Qכ5]'0o`EIԯ\FeN@X!xݬC϶m:"#jV @-A.#x w*ڸ_pMw3m8@tko %pS e4|\d]vj]i{PUg@;c&d{1Hc:oxHp=RoV ZQNjzW-2ц` 趶ObC))|7^=8pS|oQtϠTtޤwΰ8K@F&`7;#`XT95uՏI7 Xn وXPSW`׫!Pcg#ɰRJn3݂M%8n&A$PɽڍX6|N~k j?D90?+]D(EvoSFӗfPk_{<+F*]$!Y 9-f!*pQBB\zJ7{j3<%s[+nz&βг^kг981z(ȻPGMc-b[)_95pT_.0Hՙ\SSiƳr;62(B. d=߃3( tPe޽_}c7),..1]@_| [{Чm̆Wwy=N=a߄t뵠V<]s )i܂iTN}^{ȩ+PKp=A{7D_,SkC'=74CPb=LXeh E`=4>K.wCu%qg >'e#ȇlq AĂ"w8AMa1 `7rfi!M:B0/GoPE"ԳKHȟ!E5lÔ6'Z]@,C]ײ:E.j:ΐ&1ܲ_ŀ.0Q-"߻-藐kH°.Nϣd\} .JW9"ׅuhDfo @7wk:BҽYM&7v!o 23u%oN_ =QGz>7EZD4 4x~V7˪J.4سLH1ZRBޏ;w"J#R%*+\6P9bZaTO1nNB%],K _#5r:7MZFi {wK7Nѕ-OF9 l}֣>?a禽eߔ(|$}5ڴ7(iN-%9L?ksg3aFdm;9ʯ.eV WM3ЯгQv=ޖgna=aEP(^7uFfVXGOFT-Bfr%,w;<Pq^~p8m*2hr^l(b4[%}ݲk]@F=]jߏ(đn3iÊ Dis= Ї۱z'ϔdGchnk?g(' 7/d6M"lz5 dՎH2ǩ^ݼyZ*Z,ѕ_keiԭF#kF-tYipCNCS6?X[| d8`5v9ijm̠QɓQhz6Hۍk.$vIzXQ{1x IL6OԐLq9T5=}ܴ` t iU8iɛ,wBEJ^}=[` ʓ)>,!ȅi:LU1v SO;hkKO= pWgΝEҩ``x ]Dj˯ĤoHEM0: w_o<; 8/aW:0DPzbZdi .STAo|Zg:X| F:223-}>ڕ~me`nV"u$p߾J}'k/atI/ $d,"MHLă֓ȗ{"-i4@oq-w«_fDS uJ4}psET!ժ[0 E(E.εczE@yq񇿃_KauR_kȦA-e__x盃476GTJ;O/󱍯:~:~Fgyt#޴th'!w NZR.[MVb\ʝ BM*[;ßA47˝CKtrVes5b%.M()dg~af^pl'WtBԀ6wH{cj; ? 5k55鲓؋O;U$ď! &-̩RVvUp {d-;-YagݖiŗЧ-VAsjRzpЯ[k0ȼSDγ^г>"N@*<tdL2г:h!yq( NJo_1Y)k 2]';I" fp|*7݁f^l;~urH^#wˇ p2!&#l=t#$#S)sdZYǶ,wȶG 蝏aSgyꞤyL@?ß@`פ3C3]giЍ8s>iyjj< c"w}(mnrz 7KйxLHY^$PyO\p@:ȋ(`nPƐ: t逞U<>_GlԙC׀>=>ts'=zsFG:Ϣ 3=9#FyH :=ތ~:%uoRN &m<(ÔZZ-Mo*'hC蜫@A%qigPx4b/iM-Nt B!x ?wf(MhFo+ŵ :O]kOy+ϗ=IZﻀ~c&N]2Iq#@tD|:i6˦g,iг|w-BI C'a;Х<ݕ˿ =#g'PI F@sJiQpX~ɅQr"3IsQ.vz' /Kc/kI[jYuG?N251KtX5;Ŋ? 1鱧[_f0k),O +eMyΞh4r'OĻ_׶}.7=T}q{UISc>7BVn;'ps_V` {j/}HVEɦcw&^,@O'Uxsa(#S%djjXgE.PIXgSB?t$5TVFVmsku9E=)/݂7 @2r ;g+fnaY mPA1`g15 JSU]:II+k2i~V9NoaHrӋ.T_hrF7!!J8k.]z]j&tYߏʑG2i:7m2kU43 )ou}'v 2/1U#$<,+U@O{W#;L@ߍU?8oBX'-.5,"Ӻy@&EmiTGGR9vPI(Ǖ+ҳ!xQ\MBzz_-Is ^6lVp#a[@+|q Dr I[Nf+!sun(ejqx Uj.]ԍPSPڈʼLhqՀ=oIȀg杷B{$:8ȉB\O~LŽY~ y;U"Ve+o"M"Sd]VՀn x'2E5let7?YI]:9ZXz1R_C&Hk2coPP$\Hmгйyj%ưv} p@+ Yr_+5LpmÏcr1'@~^<wTɿ|ҰYպt&VtijQukq6i->W!L\D*@ 6i)laS#wdҚ sR:^FA4F(aƉ6X'vMlBuфhGZ֜\zsUsS/:_ldα5ReF7/Wa [#Wa&,p#=P*.1.ˤn<&-D8mQ9;Яu({}9w]/t1l c?WzAp7\"@|&&c1ɡG&.{̄ܛz7*q.K՘= ԞCmQ~t:zN2IL/CաtMEfAn#dؕE`sӶԳʋ8/)ɌϜ(e<_q MVJx|.^ fc3C:bc}b({^)a.ۀtSj 15% WgǴ2)>^&4e}týek].tE>ܫOIy!vƥ$ND+f+ieRZ=OR*B&hЕڴn"sL篂J`qL&G98<a7u)#Hʦ-!OLr%TfSe)cor).)UOމ7k()Z:ùXg=/! qp̣-ӓdςgplwxiFyFFf=jL8s좓Q-w`p[Z >-Di6 3&c2G &o: @KtU\:;z^uiñt`zCoad*f@,xr'Ve1$j%GD0.݆Iڸ춭'{pK2&OF Gx$ H$hMr/6?) H9CjLgT=6c:{\mSxgfWysPMg65oVV6lem0 I1D+j)|:V! oᮼK2@rW4PE5zr }t J6;V;@G ˽7| $گUk>u7VKV^dɗL@f!wOYwNƪ!è^~c羍sNj}UA"J?&+uC&Rbm1S ŮضbrA-C# \y@Wc @469A6ם}NA]l]hߘwN LH^[6⣮\DNB!qףg|PHAWYwchxhԐG޹ywKcQc>([AԏoNW;}vnHy=(Pes;\A1 l"1"$9o܍&h6~55d #hTkP=.E$zQ 2 il=n^Nzz9Jצ?%w_=,bY.E{DGõ$kڲF-4ܛa" ӮiuPPMه @ݗW̭6=vڌnɒkAʚK2E$i(./| E te,Nw3f)[>=Gx ͯ궿  Rΐ{wCS'Zj@')2L9wwjStv\S1KY@'!IC=mi;71RPinj?Ccj/_ր.rsPBX A J9} 0Kɗ>CLQ_ʹAc+$?״qC-U!ؓ}NBQ^Hd")h[-@TMF_߄w+ 7:cI%)6tx2OshZjn?t@ekԡ5e X2!y!>;<;z]ɡRF562C'!w'4Y*@4X!N0nEw>$[ g6kAևQ.%]y6gទ 2>L@e15}8 ((.PO׸A:ZԌ{uKd^*^Lg, i ]7~ij&,mRے̺iz쯾OFChq sԢW"5/ 7ڈ5? HT. (PYJzz@FgnŦz}yR;.)ߘb\uSn=t+=zM#~#^Gn=Я@7,$,C^|MH2QGKJ7E]A$n:EMoxƙ, ^0Ȳ<`A $Eɲ+*+cIVEa$Kd9rȎCemɥhJ$G*)E\@+2l:kO{랥C f]* SV^D^@!`ZZN /kM^Vzve뿍r>@GAz<e@/UY!06 [t0Adg)-/ݘl֎ !6ɹ,`:Hv5$af ӅМ x )}kG~c9y ;Nb7 >CK`[G@?o5l{I,r|qh/ұ{иӀtt>&1\7˝yJ׸G[$@ClyP Y/ $o,|' "snIxT`f^Kۦ.~E=8vO9~1Wi c}\,[ޕOKY blYx1AJq41 T}d&zviJc}{x24yW]`l}63:  Л}oM, [з,NشlB݌[yIqNp4-O8[3emj+BernZ$L _,-G7Yn:}K龅jDRo7 sѧmKG9kGZVr_>^n'gYk3Ke=9V t[MMPbh|&@NfVDta6 "W{,DURӄH}x %j.:U n27ȧ%Pl[)`%pt^x-:a|UNt ׀kTBhiۜ."!c[t`EV7܁ q?9"tj#>~cT@[Q/0 h[X}cAj/V\&z>C =Hk~ztrh;l){ImpwꉿĻDrbt*b3ocB-WJc2]x>,=O!Y}`7nKX5s-ܓ]Jne7H,W^Fhwu9ʻ9Q/%h)~}p ]SG;EP!c# /|jXxkTjj*qxC6e2Gܾ^X/}ZԾORWzEG곿 3 Se ږͬgUQT1t^̶ mS.p޻X~f"8k _|"eҏu5˲ Pi}|njԧn ƥp+%zoZݗ\?gw=j<֡y㽸Zt ?X3hο.Q-Z)ٴ))]? dC!sp y-/MZt>Tl[L Z8os<^(4ℐENě.s&,5sށS,sB V$CȦ*I`16ukm>|$#%S9I-> syޛQ")f9(v2?7`F; - "X}?`1cv=_˦*g tYB$Nw~d^ #@'?=!znzʍcKIx#} X;.˹\ZκG)W9]msٻ P2ݶiր ڋDFu%Maam뽰,iŨWK,Ǵkz|,TaL, Jz[=oςܦص߸z'4!M@'wJ|''ɳN`絍nx]ʒ܄#'{8:Yv<06}<L0s/L YmTTP_@<Xe٭D޹4NUN+̓+#Z'o +}e[{!vCO}޸_fErB=۽a!`E6UZ`@ET$q]~MWQL +AضF;^ZzU[f3in6,X9$:4*୫u(WJlcq,oсUdM4ޏ~c zl:z<\sx6.' r%n){B47ԋ5ɄGE"єX IDATɤ%h{݃q+3u@xP2[2-*y+O]ic.5a`4]wj\*I)b)B$U֏ ;E,Q̮KA1 ? o<$y *Z|X O|'0> c3 mI#@){+< nY{^\F醗}YiAw}5(A"!pyYN# $zb9)ԵB)u\:Lr"6mD:I@DLCWf]FC2Md{06ʇ^^!3v~ݖG0k-]z[fH~ k=K"-Y3&]C܁{u0؋ Hy=iYinݤ6\#;n%PwRǻ=0.⹮`.0޹ aN=_~Dixl{=([#̴g}qL%h=oaoXemWK`"н#', kr49lc/3EZ{8\OY߬tיqHW n)sgAK%WyrgTf9uEOg7E-[Fw1N. \3P);!# ܖ]`9jM-J`[ؠ/=dЙ՜%fg=f!4.Wu46 WH#$CQc0n0wa`uZRS#dgR* _9jC񱛢]ɓM%eTxǁ[oG1N.:N )Jnt $#.x۝8(x{EF=fqK𙪖(C:,֓C<n.igZ;I[]+,]W7| | 3_bA`VkxzL9!lewc@1A ېϧLjnւ~V+O>N6v3A#@ph IFmh45Z}I>eP/@- v6;T:-M'I?R#Fv(؇4F47H[*3ܭ /4q6NmEXcws(N%ue^pqN]G8]C$%mw} OjF->*=? n{rSC:KŰS+ԝЙ"5cX;6{FؖϸT^?a鏚yZ-t @ RĞܝnyqFSd9hz@qi󼐅zѿ7oSEr/[y*z-ͦ7k2Yظ.NK?4A=P>KMA.ɧPPhΛX$hdʵF,F]hC=iARbtrHeP|]Pj.q*(|* xob魯Hc5x)i}F.PXb9TZ0g@rp\^b\ Y ~m:}X3\b /{_}n<$C.\n%8?t7 .3\:!Tf:nՉ;ј}==`XsU{Cs.^[AZ 8ˑaK7BD% lr 09,:jf;# PP]`YC[~!"ylvC$F=jevZroZ؍PgDVV\g->缓 IxՐ˳pJa5a@E,kbfh3Nk{i ^{ +:iHz@zqǟ<6:af9of֞~dn7RsKw5|%.ȺyerۦɚSn2dpCSC}6`>MB주g=Rj+P^ yk&ʘ[,BLB]OUv=T:-9suJ=U*kE7_EsIϡA%hO[҃[cg^vm/P5}og?cz_[階LVJ|m9֣:Z4`9z3<޵@F0ohҜ\ߗrhǩ>sԱ%EW˥Y 3o]T&mE+)Rە){-翎7QN:_n&Z 0",udΉ1Jۻ}G' v~&a>/jO}NH "5akG/Jz;KEIyP˭JOV]8Kě@;!5S7 }?0`fNk0+]˪8Mp2%g`Q(%5k} !MUn+I|4XC{ O6|O^B2,J ?Fm'Ñh9,~>2 qN! 5CFt\YEA(Ӟ.bV[I(v?,ڋnLy=!epaGZ*eڻzWGF(}o'ál*xc\HYm#jbZ&FTۧwVj"Zϣh.?"DSs \XiC%yK9"KvNaA*1LVE'8`|r~}W>F>9eX?K߁ĚJECzK0AcѮMm6hOKPCL_I~=@:q?"+)lyq;-EY(-PEVԣ 5Ks[Gͣ5Q/MxQO/ $hJY%KН/P(v% 5o"UY"=&YJY:[J-sO&A;ۃx?^I`.` dutբ03($@ "Ǔca)k٧ːH9::Yk~D2^ ɯceoa,G%X۪#,k+ -~4݀y}t> 2-??zj{~caTYIQmwЀnEg5:݇Kb6٣jHi!N+F+EcVBYIr5% <ƬPק$ogjWx҅)(Z 6g IBHȳį+,t[ בM0$^Ebb rx;/3#i3Q/]He%(*3U]9<_)b ]R^O`n9˵ۗX݁z]:gwӺRw!lc[ ndMĺ6IJ>pgOEwMӨ*LDMbxIՎΣ$=(P}%%{x̄/ @1Z^g>R(zu6Nk=KڸAUehKMuNW6fi]DOgPUI!N&N ?@+jg3`|IinQWuKT1Eҗi;%_VM/?>pH|*!Ȝ TR$i0"UDh/"ṼȤ?"fRdԡ߿~魴Ü ~by}R1;熿=M4F _![z7-z¹3&«BuD[CDwxwDlo֋ѹB8TʦV>*e%KY ^?(} h0Aq/Abu)Ϋ L`[6}~S 6{wIVD+,>kFxF/.V,UTp}沦`=R8ll"@T͇uH=wM׷>H`Hos_ߜs)9IuJʀi.w-J`#bX2[vxijB=Cׅ8bo?0wvsnP ֥k|BYI<֯sl&#XrkrHbϣ$&wP1&hqJUĵk=I]/r?>:)SI?o"NeWb=n-OY0/yn(ÊRāCp-\?t >kko,!eRb tŧ=r"dBt#z%u@+Z=.wE ZܼD '6gGz>Hv#1_?4zdfLncJ'RsO*ϰqnzhj(\ޟKʁY/֙0Z&$?-J=X6S!=@@LejQkŇ|P ҙ`^$RՆ$E֔8$%ބ"<GvsWȺgP Qۨ-or{CSuZw^:PڍcM #R\zqf*Mid+;OE /3Y} M^˂5`VT hRhyFmtH*:ֲhw4DGg ++!7dg0ͺ,Ewhց!#ˀ8{hsbo}+~̴v\˭/` ĶQIf`geIg3z=jw2P $$9٘xv5ByUB`Qt\^G3ؙEoqjJQ&n^EQxc \)ECsq: M2,kszx,:,P*,Q0Daq\WDq`^vL=piI:^\#ny{m ކՇTN;R_X9f[x-S h1k Ncه{YU kR&-E?n68 ]ƭ^(֔miƒD0Q9$Gel]@1$6.ݷ.R_aC:drr~2]{U?d-YCdLR2s[ez2uRF\#@\vvyv/r љZ.*$as1IfM*qQ?Mı5n9z޶s"9P9D_Gݩnة0!ajc|C=js? !ĊBs~}.]zv`EU:K#33M1wKh~g@#<_tn:f %]Bi2{”[K`?9g%õSXeSrS 2u%RfFb@Zw1 ̚SѳK&92TVg~XytQoB=JeOdz4L i~xr1LpcƔ?Τw`|׃?8{?K7ѕ/V#@XI]4Y 4LcM8Gi`KGH*\ӚN9"FثTюXgrv}C@FfBEJ= CJղo;rszO fe8sYB$~N: m@6#5vSQ4Qo&3rU5JoAxQ;xWJX?t ߂ou&B'yNa* oqeX/Cw.ݠLK򞂹MLcR$į Bm=aqw9)XZ=wlYNF }j?^{*QendOk(<h`㤴d1+qK%rlp_%yřv"ω=ǀZ| f'Q[Pfn hmjylc=2]NbJH)Hb'&ҮA0V:mk,8{nt쥗/|䵏%g1(VZzq!e S} GWNޯlyW gBrU*knqЏ!o/k.E__AֱmTn 0`?dS'ZE@7vxY9Gd袶vlO k +WrE`luHb&Zg Z)qɍ1r6i6mħtE/ϲ3;iVRMOKI@Kcڲɼg+%c;JW.Z)t?kMcrѢ 1\FJOLM/iOiv v'4ZiC1}r!Ÿv-u9"lGz,l $ѰNU2m@y,bgBJ`bԥTho!8R[ba[n=Pzs4Fpxg Oaukh>]5 ;9~d&B0 2!cC|LB/W}Vu>̂eFL)h|s׾lspyT|ڲD.6SCUw 0S{%0/mc|oL{Rd+4xIXNZ%)iո&4[wn$HK, n%6*) pʠgy"w`qçf]j<dp.sg Wz-s wU/P-s&UfQhXsh0CRջG@13ңO}k,2:t#[U Eʼ C9 x[=e+`.jy|az)Q 3p,@+Ěًh}gV5:~gH`Wyg}< OW4y\ c*.]յ@i:]d!0@&zV[YlIe u4խ)G؟Z{c3&"}!3R^n֕sœ#"5ʮ\-bSeY:hfBY&+ F~2E,tЭ_+OIqתr^ѓ% \V`W>jƼȤ>HsP(yEY=ȴ=g]G"׵TgQ %~ltq5}˧k~eU*_-/y=˼7߁9u2VH VTHs Nu^C~Ar]YKhCHS#T> hXӶ ˭%k5 x~ϯ 5>1X\al?kiYؖwzgM;.?5* :H[+Tj@9-)PeT۴viõV(I!Xb܁@HC yw[Pޮ7f7ՕU+d}փSb@z?8@-}\RU8ze+eE {CeS@G i$v; /EΎhg_BjxU=H* Ma,<tڎK)q;GWf8<.pvzs$\L/7J|'5HرПD )d}>.Gp+] ,<MZG~u>$y K3TZ2]u}BrҒ5銦y[m)܎皽Mc"N x,jcncK,G<3Ku< 4[XJz-}e6'f0}7]r6sρCw$Zg9˨u-^TI;{|5c זqͻ[Qb=ֹ@7P؆16Q BM!I"g.<W&^k?Kkuxikwr[ds! PJf8t ?hR!T-pTSEhs*`l{njf';ZPrfJߡY.U1hBqt+V˅OX\\,sO?K1Y4˞ xݥHv:)S{X. 0(o]/@V dUXiWԧ[~؞=NwS1~WSW%k@ \Wuoæ倚ѽ4&}B5dwH@16VJn2$R Fhw*h&j7bfCeZv81m!;+Q%Y<+ɧ-\n*]13k~ۖY0>s6O"W`YKPҍD+BiV ,bSЇa9^'O8YW1w. '_^.ex. 35,Z=u^o;{wuJ`wf3kCz5oMxd$6Cr24p-L怱 .ݮ/rʬ8y}x1N#s&hg͡w`rfbkkRҲ:bXr+2e1^_n|ړa\{ ٞϞ3/ [oҳA%ӵ( /c㨶MF)0AfObufaHAR."/U+0# iX|C肻P!2\.yxrfAXPA^G6I~$ٵLx仼C~V: IDATFۙ|.lT|.yDqnIc` {]-;_AIwR_7H=?[plXgYvw7tڈuڇe%_zY2I$܏1fBf&ycL]BVGW6'7%Ap#P.M{K.[I[ﭼ(mwTߚp\4o.A.EԨ\_zBd?Mԗop JCdvJBU$M:)48}/K.#%p|Gz4%tǾ򿋖_oxMdh3^YRXB, 3d~*aw;ri};LȶZh~B41tYp*1}ȦdUba)l8XVoOlɢGps-tݭlD>zF d{1eTd[\ts鐜 DҾɣPX|ߴPлaԠ{K!Ji`͡1{߯O;l%κ%0 9W1|٥UQTmk{.E~⤝e.ʔ )*+],jM v#uص}0lRuMcH|]{EG #}&'|0ւdM~ F;gP?ǘˑQѓ .x2Ƣ𷤟%,KN/ccHG0^]@XItn!`Rr$1G5mU:SY$^sTy%ȶ^C* y߀gjl āf_ys3:(sZ{gm}jkY ^J!ZpsqESbfpCZ@eC\81Ri4\-[#4B bCA = $Cr൳UOD\wYPU\K!n {$FC$5̿Ex1.T>|P73 kTenW>s^7\VXp˪gU$J) HE!&jraS$эlN"_Û^&nsH`WCC\zG$^{NiD^SR.ͮg_7²Rd,7Uz-taKlOZ]t ݲnenDccvǁ=@lxH87nco:@U=K'np<^jyB2<1Q; 9 xYz9XP/$joU~;9{)}C).׺8/wHy $^tI,RU#S ^8:f?cwƝ ޻b͌8yc%ypl hMHVfЉ9i7k 8͊%۶nbp5NC%&9myU%*ֻR2ك}*5O7mއjŽě]Vh\:JcJLltk7vF$ʒ55F?(XB,! =d$^`5P`h&t}"NsD(Kr]]I)]p& Ҭfw 1}w|O__/.n/t]yKѿߨ&A0fߵUt QA?ʤ*CIliȺvR pV%M;_%RrФ=cJC@L` fQsf"O!~MP>63.%>;۟-_#.IyEM>XH ? y黏a7O"Ģ(/ "#FGq/=|BKL79ٸNoCM*=Dž<6VxPƉnT EH*3\eFW.n/*6vJ`iXqԃ”X [:Z6~ܴhe̋9Xl`/`gm1j;Xk6mBƃϺcm'ݩ!'zVa7JzMEsYzm ik,= EbΜQNΜlC3κkX~단83khEnjsG&>t/O(g{?g]8}o_eӤɂ6"`גGD'ٍݻ{?ubKߗ}IFk[ڷ/XrE@>4IhɱE#w,3w(Q嚄b6ǫB/Pm4ȳB/ޮP+hq"j܋ir7"Lb;)h7')s~@WJ)2UE2ӁϦ>(GgLuDނuCbλ6 S2^VL) U7tl朲DJRgN+>_QB D+"!K}47v5vۏ7n72::(snvy_B5nOܭmok'Vn]v:6&ܘYʗ=ܩH#%)˾HF SY qRC7A4v ;R7e"=cEWAeao΅_<"u z4;@Y @gU0{z\[ T8x-. Qv|{НSf3^%vM>ri ]3! k7d;C߇oה^B .|O{VӚr^ײkbqBWAlo;ur6I1Y@Ň]>̰D N눽00uǁzYb3:5Af2tܱ'q5:װvGP3ژ;R^HCʟ3MkT$w-BAPgn|к.訡&0 t)tl7}l9Ob%r{?iZqpUZh4򺤇w2E\ 8I|[S^*`sƲm)ag[twˁ= q$8Ro/0Mn&`0VmY "?k^*b>!@P@4O<'QѨuau(jnEtUXIC~dZ灇󕗏^ !Bv޾_Xzr %p}-/(kwgi:~O΀ԟ]S^L% 8ZZ&q9jW)yh,?S_F`QI~{ڤ<*e>/YkAd?9[u88LvFXy=l?b~O~7蔑)%@~34'[C`&_]fդFfvˢhPBO^Ui1<|1)l;IRT"l,KuLU.|ZRN`ְcixÛ1{ፀO+43l0G Ҟ(-]ɔ6e ǐ~g {-i >5ѫ ֖$r^IcEj 9Cn<ʍ󡗥•,IHqvrO[nhd]t5pI%pϼ?'|ɊF,y$%Ilb\tJET91 hǴC _6.lёsmm"g( "ck8? oǀW }7j _hݜ.v-mؿRO{$e>ʺ2Ql0BdL#{_ązP;gY61BLH B_r 2kYeaIat?sf<ޙuwwν-v$@lU+?(R$ A -$D6 DA4E$P" mBL)H.Jݲ1ް1{f޼ws9wgؙwI/w}y{R]}-?o֏jƬk^B> oaf ?k/6h>;[r2/̹szfzB6vc6J(J\}qO01?# շ25;|͙̅*/_S? ŚW!Гt;l/dD갗 6 Q(">0jڶoy;&޷,}e&0suwLSԔgLT!$1BcF=yԍ6գKO7_.9繲z 4vRx7DxlY20nnݞΞZ}009ڕ%;cቦkPq1`^Y~d?,}A"0uG>j?E"Ф;vuh:mfVU\b--;-2?vO{t9`rup}١l U˧!V,Ɓm4 ܾϊX]G%u!<>V8ݱ覤^&u@n?\@uRF)/봇}뚭ȻCX: H@S[|1IJ[ä̧&;#ovq|se1F$a~2ڳ\&\͖49JMtL׹п|[N͉;aϘbE=ŗ&覢 [u̾9/a"5NZ\L;y@| $9x˫'~P S"ۂ/;)9sc7R{XA\й4kGncDw8X9Mͭ=gBt{;|.$֝;=U1ygk{ULZE&e1E NaҤV|QEo}w @1* M^'ITsL=fNj#a]rj-~Xe^rݙM\oż\)W#?G5K4tLnz̏y5 FyTO {M  p:E Lӧ+C/ $'$3\92aF̫үr>GEu-ӳu{9w;gIJ‚4U16چ 蛜h1a0M?(h` @+?7!hi @Ы>=o=y#5$YΤpNR%[KW41J oSI9t,\ȽB?$h뜳qo'Eg&i#'~hsu @Ы}mSnKNȎIixt8mO mǜХ~#rбTm巳>ٹ:(^ݲpyo4ssCW;u+tpFoƶ7l~ h/3`~&vz[ÛI?E>{3QW 4%E]qN;z,vF]ۭq>W牊iJ|vcZ$Kڽ@sV1emUP[ R"/!C;w]6 R +E]Uӏθ=5DV{[ǚW6IV&3K ^&|)$~g nۜ7Xxsmj9w!n J ;9+OV|gݝt'$ygA+&Aر[:b%k;>X T\77L fx?I7k]\;?e':弜u9".{IJs6E}d[CN@2Eq޷DKؤUxs'<8+mdSpS*}k(PG^}EG JPbx} r#_ǫOJ:&0Œ$8ht1JJ Os z>؂)Ω1=#薯g˚ 95$Κ4z6HQ<@1J;(wU%A_Uܸj8= NRݛ!JfĆ)sdc9[aΜ1$479^M#J}/.BGF07Ԟ)cwwݕ<8e >h¸Waw)j|dۂ%\åUZPy\XQgy@bVjV:Yfm8m v۔Ol7^֑-(YM]#t82l{]b PtNyoDGﯩC$":6y-$% u)ݞuwum{po3<ǚSE?ar˜?-eDx,nk,bb@o5|pA8 d@rR&]xSJԆ#6+ڗ 65y`O" tD~9ͨz,(5ݠII[b/=mtƭN,\ |&T)G{-q^8C'ÉQ_MPCgnYhg|QwH`CxbӢVIj"r@/ @{I. =34LCh֪;Z8MF7Ƹ2}fE.8Sl՞s,fkFW^)T/^'6Do PZc~f{T{k OHug&01BVdObԿnV۩Ǜd;y`\) rIj\g C^@CB̞%ڻ}q2q,0m٫oA79x#40"!@P$PK_NKB^@; G`ޭ?{[ΐTi߂B GS%C4 ѬZpd͋o,x4 G_I#0{W_[R!fabe=}0&? )=#걉}u" n~])j N^BA`;^v'ƒy/b8L :. :WAF<(bQ[KA}};~S1FZ&A/vu#0/.'H&ԶV.9lSp݊޼&nOD"Xn opynlcoZ@_ pfk6?)P) W }]hl Ljʉ8I":Il qN4Us_5 ANBxӏmWgkfH3$$F։&y5Nv& 4ʻOz6nmm[xb:0^gO3wt \>sB`c'2NK#1hl|o,Z 'A/~}H@ɉOcT7[^QF;}%XLݝgwKw;+O' ]A_=BYN]]둻\'#$AJR\^3t\l>scd Ay%>z6 <]gNp]gٕs lЉSv8Q[_)&)MrtH'MJE$+O ^*/x'@1>@`L=vӗ#7ajgHꘒP'AiŔg7+~ɳ5xIXjw9;֏]'C{vw>_OȀ|AgHIf*M^I~/7ktDԼi)Y(o޲D]򶵒ݨtEOqB{oVNХi \kϊ>tBXwkq*_́{U *K  L>HZ(ihS \*[Nr:)vJsu,¬.[Uk܅>F{]&S{9^0 h %FcQ  p.fGrΫ'*+||pnJl"w |_zI*jAjDm=L᫾:ox:#A_>,‹=F'&'(Yx.04Xx}udne.=O.Y(iwԔhPQ񆷿mwdKA}aR X3o<9/9z]NLgf^5:ϋg/ ȋCzY}wA7cݑ=Gy¤j eBP ~08t?XራЇ?vď߮cToqF$n ܖ|>Mֈs7gsg=9Sd'v/ٸN-5H\C~}鵗< B,2/D\@t|֐Z/VT4yM6;ug44(]q˟NLyQ).#|nҽx k L`og~Iyj85Ev?{I'A$bvdJ.HK%ZmRDx&\aq_? ؏(.zq-9ZG'쿴!?CR>Su- (OФzسCFP}㋩]}ƶ~!t@ @ЋGh!!SMTBs.tL"?9ll!4A~}ĺsޘDRK54~-C2ЃzZ s_|g}K8v |/Z9C"!+M6ˡg;?M:Ŵ1jWw9PzZ s-׶&jQд6y4Kx4g$`>@9@я@@ 0@@ GX  Pq  Pr#8z(z9VT+@@*N^A@A^~  'A   A/G? W||ra@ @+>`>@9@я@@ 0@@ GX  Pq  Pr#8z(z9VT+@@*N^A@A^~  'A   A/G? W||ra@ @+>`>@9@я@@ 0@@ GX  Pq  Pr#8z(z9VT+@@*N^A@A^~  'A   A/G? W||ra@ @+>`>@9@я@@ 0@@ GX  Pq  Pr#8z(z9VT+@@*N®IENDB`privatebin-cli-2.0.2/doc/privatebin-create.1.md000066400000000000000000000023041471607750200212650ustar00rootroot00000000000000--- title: PRIVATEBIN-CREATE header: Privatebin Manual footer: 1.0.0 date: Jan 20, 2022 section: 1 --- # NAME **privatebin-create** – create a paste # SYNOPSIS **privatebin create** [-h | -help] [-\-burn-after-reading] [-\-expire=\]\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ [-\-formatter=\] [-\-open-discussion]\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ [-\-password=\] [-\-gzip] [-\-attachment] \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ [-\-filename=\] *STDIN* # DESCRIPTION Create paste. # OPTIONS **-h, -\-help** : Show help message. **-\-burn-after-reading** : Delete the paste after reading. **-\-expire** \ : The time to live of the paste. **-\-formatter** \ : The text formatter to use, can be plaintext, markdown or syntaxhighlighting. **-\-open-discussion** : Enable discussion on the paste. **-\-password** : Add password on the paste. **-\-attachment** : Create the paste as an attachment. **-\-filename** : Open and read filename instead of `stdin`. **-\-gzip** : GZip the paste data. # EXAMPLES Create a paste on the default privatebin instance: $ cat example.txt | privatebin create # SEE ALSO **privatebin.conf**(5) # AUTHORS Bryan Frimin. privatebin-cli-2.0.2/doc/privatebin-show.1.md000066400000000000000000000013541471607750200210060ustar00rootroot00000000000000--- title: PRIVATEBIN-SHOW header: Privatebin Manual footer: 1.0.0 date: Jan 20, 2022 section: 1 --- # NAME **privatebin-show** – show a paste # SYNOPSIS **privatebin show** [-h | -\-help] [-\-confirm-burn] [-\-insecure]\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ [-\-password] \ # DESCRIPTION Show paste. # OPTIONS **-h, -\-help** : Show help message. **-\-confirm-burn** : Confirm paste opening. It will be deleted immediately afterwards. **-\-insecure** : Allow reading paste from untrusted instance. **-\-password** : The paste password when paste has a password. # EXAMPLES Create a paste on the default privatebin instance: $ privatebin show https://example.com/foobar#mk # SEE ALSO **privatebin.conf**(5) # AUTHORS Bryan Frimin. privatebin-cli-2.0.2/doc/privatebin.1.md000066400000000000000000000024211471607750200200240ustar00rootroot00000000000000--- title: PRIVATEBIN header: Privatebin Manual footer: 1.0.0 date: Jan 20, 2022 section: 1 --- # NAME **privatebin** – manage privatebin pastes with simple shell command # SYNOPSIS **privatebin** [-h | -\-help] [-v | -\-version] [-\-bin=\]\ \ \ \ \ \ \ \ \ \ \ \ [-\-config=\] [-\-header=\]\ \ \ \ \ \ \ \ \ \ \ \ [-\-output=\] \ [\] # DESCRIPTION A minimalist, open source command line interface for **PrivateBin** instances. # OPTIONS **-h, -\-help** : Show help message. **-v, --version** : Prints the privatebin cli version. **-b, -\-bin** \ : The privatebin instance name. **-c, -\-config** \ : The path of the configuration file (default "~/.config/privatebin/config.json"). **-H, -\-header** \ : The extra HTTP header fields to include in the request sent. **-o, -\-output** \ : The output format can be \"\" or \"json\" (default \"\"). # COMMANDS **privatebin-create(1)** : Create a paste **privatebin-show(1)** : Show a paste # EXIT STATUS The **privatebin** utility exits 0 on success, and >0 if an error occurs. # EXAMPLES Create a paste on the default privatebin instance: $ cat example.txt | privatebin create # SEE ALSO **privatebin.conf**(5) # AUTHORS Bryan Frimin. privatebin-cli-2.0.2/doc/privatebin.conf.5.md000066400000000000000000000052161471607750200207610ustar00rootroot00000000000000--- title: PRIVATEBIN.CONF header: Privatebin Manual footer: 1.0.0 date: Jan 20, 2022 section: 1 --- # NAME **privatebin.conf** – privatebin CLI configuration file. # DESCRIPTION The privatebin(1) command line interface create paste to an PrivateBin instance configured in the **config.json**. # FORMAT ## Top level object keys: **open-discussion** *bool* (default: false) : The default value of open discussion for a paste. **burn-after-reading** *bool* (default: false) : The default value of burn after reading for a paste. **formatter** *string* (default: "plaintext") : The default formatter for a paste. **expire** *string* (default: "1day") : The default time to live for a paste. **gzip** *bool* (default: false) : Enable GZip the paste data. **extra-header-fields** *object* : The extra HTTP header fields to include in the request sent. **bin** *array\* : The list of bin instances. ## The bin object format: **name** *string* : The name of the bin instance. **host** *string* : The url of the bin instance. **auth** *auth* : The basic auth configuration of the bin instance. **expire** *string* : The default time to live for a paste. **open-discussion** *bool* : The default value of open discussion for a paste. **burn-after-reading** *bool* : The default value of burn after reading for a paste. **formatter** *string* : The formatter for the paste. **gzip** *bool* : GZip the paste data. **extra-header-fields** *object* : The extra HTTP header fields to include in the request sent. ## The auth object format: **username** *string* : The basic auth username. **password** *string* : The basic auth password. # EXAMPLES Minimal privatebin configuration file: { "bin": [ { "name": "", // default "host": "https://privatebin.net" } ] } A bit more complete configuration file: { "bin": [ { "name": "example", "host": "bin.example.com", "auth": { "username": "john.doe", "password": "s$cr$t" }, "formatter": "markdown", "burn-after-reading": false }, { "name": "", "host": "https://privatebin.net" "extra-header-fields": { "Foo": "Bar", }, }, ], "burn-after-reading": true } # FILES *~/.config/privatebin/config.json* : Default location of the privatebin configuration. The file has to be created manually as it is not installed with a standard installation. # AUTHORS Bryan Frimin. privatebin-cli-2.0.2/encryption_algorithm.go000066400000000000000000000027401471607750200212260ustar00rootroot00000000000000// Copyright (c) 2020-2024 Bryan Frimin . // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY // AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. package privatebin import ( "encoding/json" ) const ( EncryptionAlgorithmUnknow EncryptionAlgorithm = iota EncryptionAlgorithmAES ) type EncryptionAlgorithm uint8 func (ea EncryptionAlgorithm) MarshalJSON() ([]byte, error) { return json.Marshal(ea.String()) } func (ea *EncryptionAlgorithm) UnmarshalJSON(data []byte) error { var ( v EncryptionAlgorithm s string ) err := json.Unmarshal(data, &s) if err != nil { return err } switch s { case "aes": v = EncryptionAlgorithmAES default: v = EncryptionAlgorithmUnknow } *ea = v return nil } func (ea EncryptionAlgorithm) String() string { switch ea { case EncryptionAlgorithmAES: return "aes" default: return "unknow" } } privatebin-cli-2.0.2/encryption_mode.go000066400000000000000000000026511471607750200201650ustar00rootroot00000000000000// Copyright (c) 2020-2024 Bryan Frimin . // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY // AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. package privatebin import ( "encoding/json" ) const ( EncryptionModeUnknow EncryptionMode = iota EncryptionModeGCM ) type EncryptionMode uint8 func (em EncryptionMode) MarshalJSON() ([]byte, error) { return json.Marshal(em.String()) } func (em *EncryptionMode) UnmarshalJSON(data []byte) error { var ( v EncryptionMode s string ) err := json.Unmarshal(data, &s) if err != nil { return err } switch s { case "gcm": v = EncryptionModeGCM default: v = EncryptionModeUnknow } *em = v return nil } func (em EncryptionMode) String() string { switch em { case EncryptionModeGCM: return "gcm" default: return "unknow" } } privatebin-cli-2.0.2/go.mod000066400000000000000000000004121471607750200155370ustar00rootroot00000000000000module go.gearno.de/privatebin/v2 go 1.22 require ( github.com/spf13/cobra v1.8.0 go.gearno.de/encoding/base58 v0.1.0 golang.org/x/crypto v0.21.0 ) require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect ) privatebin-cli-2.0.2/go.sum000066400000000000000000000023161471607750200155710ustar00rootroot00000000000000github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= go.gearno.de/encoding/base58 v0.1.0 h1:3JBnzpClKHYgNs6ZfWPdTgELZOu92tO/XAUUNSLCGXs= go.gearno.de/encoding/base58 v0.1.0/go.mod h1:MoF1a0m5ooBndFSaMGOUtyYCTRQmsLm0+vmdrcrrok4= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= privatebin-cli-2.0.2/paste.go000066400000000000000000000052351471607750200161040ustar00rootroot00000000000000// Copyright (c) 2020-2024 Bryan Frimin . // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY // AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. package privatebin import ( "encoding/base64" "encoding/json" "errors" "fmt" "mime" "net/url" "path/filepath" "strings" ) type ( Paste struct { Data []byte Attachement []byte AttachmentName string MimeType string } ) func (p Paste) MarshalJSON() ([]byte, error) { output := map[string]string{} if len(p.Attachement) > 0 { mimeType := p.MimeType if mimeType == "" { ext := filepath.Ext(p.AttachmentName) mimeType = mime.TypeByExtension(ext) if p.MimeType == "" { mimeType = "application/octet-stream" } } if p.AttachmentName != "" { output["attachment_name"] = p.AttachmentName } output["attachment"] = fmt.Sprintf( "data:%s;base64,%s", mimeType, base64.StdEncoding.EncodeToString(p.Attachement), ) } if len(p.Data) > 0 { output["paste"] = string(p.Data) } return json.Marshal(output) } func (p *Paste) UnmarshalJSON(data []byte) error { output := map[string]string{} err := json.Unmarshal(data, &output) if err != nil { return err } var attachment []byte var mimeType string attachmentURL, ok := output["attachment"] if ok { parsedURL, err := url.Parse(attachmentURL) if err != nil { return fmt.Errorf("invalid attachment: error parsing url: %w", err) } parts := strings.Split(parsedURL.Opaque, ",") if len(parts) != 2 { return errors.New("invalid attachment: invalid data URL format") } if !strings.HasSuffix(parts[0], ";base64") { return errors.New("invalid attachment: missing or invalid base64 encoding") } mimeType = strings.TrimPrefix(parts[0], ";base64") if mimeType == "" { mimeType = "application/octet-stream" } attachment, err = decode64(parts[1]) if err != nil { return fmt.Errorf("invalid attachment: cannot base64 decode data: %w", err) } } *p = Paste{ []byte(output["paste"]), attachment, output["attachment_name"], mimeType, } return nil } privatebin-cli-2.0.2/privatebin.go000066400000000000000000000314441471607750200171340ustar00rootroot00000000000000// Copyright (c) 2020-2024 Bryan Frimin . // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY // AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. package privatebin import ( "bytes" "compress/flate" "context" "crypto/aes" "crypto/cipher" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "strings" "go.gearno.de/encoding/base58" "golang.org/x/crypto/pbkdf2" ) const ( apiVersion = 2 iterationCount = 600_000 keySize = 256 tagSize = 128 ) type ( Client struct { endpoint url.URL httpClient *http.Client username string password string customHTTPHeaderFields map[string]string userAgent string } Option func(c *Client) CreatePasteOptions struct { AttachmentName string Formatter string Expire string OpenDiscussion bool BurnAfterReading bool Compress CompressionAlgorithm Password []byte } ShowPasteOptions struct { Password []byte ConfirmBurn bool } CreatePasteResult struct { PasteID string PasteURL url.URL DeleteToken string } ShowPasteResult struct { PasteID string CommentCount int Paste Paste Comments []Comment } Comment struct { CommentID string PasteID string ParentID string Nickname string Text string } createPasteRequest struct { V int `json:"v"` AData AData `json:"adata"` Meta createPasteRequestMeta `json:"meta"` CT string `json:"ct"` } createPasteRequestMeta struct { Expire string `json:"expire"` } createPasteResponse struct { ID string `json:"id"` Status int `json:"status"` Message string `json:"message"` URL string `json:"url"` DeleteToken string `json:"deletetoken"` } showPasteRequestMeta struct { Created int `json:"created"` TimeToLive int `json:"time_to_live"` } showPasteResponse struct { Status int `json:"status"` Message string `json:"message"` ID string `json:"id"` URL string `json:"url"` V int `json:"v"` AData AData `json:"adata"` Meta showPasteRequestMeta `json:"meta"` CT string `json:"ct"` Comments []showPasteResponseComment `json:"comments"` CommentCount int `json:"comment_count"` CommentOffset int `json:"comment_offset"` Context string `json:"@context"` } showPasteResponseCommentMeta struct { Icon string `json:"icon"` Created int `json:"created"` } showPasteResponseComment struct { ID string `json:"id"` PasteID string `json:"pasteid"` ParentID string `json:"parentid"` URL string `json:"url"` V int `json:"v"` CT string `json:"ct"` AData Spec `json:"adata"` Meta showPasteResponseCommentMeta `json:"meta"` } ) func WithBasicAuth(username, password string) Option { return func(c *Client) { c.username = username c.password = password } } func WithCustomHeaderField(k, v string) Option { return func(c *Client) { c.customHTTPHeaderFields[k] = v } } func WithUserAgent(userAgent string) Option { return func(c *Client) { c.userAgent = userAgent } } func NewClient(endpoint url.URL, options ...Option) *Client { client := &Client{ endpoint: endpoint, httpClient: defaultPooledClient(), customHTTPHeaderFields: make(map[string]string), } for _, option := range options { option(client) } return client } func (c *Client) ShowPaste( ctx context.Context, urlWithMasterKey url.URL, opts ShowPasteOptions, ) (*ShowPasteResult, error) { fragment := urlWithMasterKey.Fragment if strings.HasPrefix(urlWithMasterKey.Fragment, "-") { fragment = urlWithMasterKey.Fragment[1:] if !opts.ConfirmBurn { return nil, fmt.Errorf("cannot read a paste that is set to be burned after reading") } } masterKey, err := base58.Decode(fragment) if err != nil { return nil, fmt.Errorf("cannot decode master key: %w", err) } urlWithoutMasterKey := urlWithMasterKey urlWithoutMasterKey.Fragment = "" req, err := http.NewRequestWithContext( ctx, http.MethodGet, urlWithoutMasterKey.String(), nil, ) if err != nil { return nil, fmt.Errorf("cannot create request: %w", err) } req.Header.Set("User-Agent", c.userAgent) req.Header.Set("X-Requested-With", "JSONHttpRequest") if c.username != "" || c.password != "" { req.SetBasicAuth(c.username, c.password) } res, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("cannot execute http request: %w", err) } defer res.Body.Close() var pasteResponse showPasteResponse err = json.NewDecoder(res.Body).Decode(&pasteResponse) if err != nil { return nil, fmt.Errorf("cannot decode response body: %w", err) } if pasteResponse.Status != 0 { return nil, fmt.Errorf("cannot load paste: server respond with %d status: %s", pasteResponse.Status, pasteResponse.Message) } authData, err := json.Marshal(pasteResponse.AData) if err != nil { return nil, fmt.Errorf("cannot encode adata: %w", err) } masterKeyWithPassword := append(masterKey, opts.Password...) cipherText, err := decrypt(masterKeyWithPassword, pasteResponse.CT, authData, pasteResponse.AData.Spec) if err != nil { return nil, fmt.Errorf("cannot decrypt data: %w", err) } var paste Paste err = json.Unmarshal(cipherText, &paste) if err != nil { return nil, fmt.Errorf("cannot unmarshal paste content: %w", err) } var comments []Comment for i, comment := range pasteResponse.Comments { authData, err := json.Marshal(comment.AData) if err != nil { return nil, fmt.Errorf("cannot encode comment (#%d) adata: %w", i, err) } data, err := decrypt(masterKeyWithPassword, comment.CT, authData, comment.AData) if err != nil { return nil, fmt.Errorf("cannot decrypt comment (#%d): %w", i, err) } var message map[string]string err = json.Unmarshal(data, &message) if err != nil { return nil, fmt.Errorf("cannot decode comment (#%d): %w", i, err) } comments = append( comments, Comment{ CommentID: comment.ID, PasteID: comment.PasteID, ParentID: comment.ParentID, Nickname: message["nickname"], Text: message["comment"], }, ) } return &ShowPasteResult{ PasteID: pasteResponse.ID, CommentCount: pasteResponse.CommentCount, Paste: paste, Comments: comments, }, nil } func (c *Client) CreatePaste( ctx context.Context, data []byte, opts CreatePasteOptions, ) (*CreatePasteResult, error) { var paste Paste if opts.AttachmentName != "" { paste = Paste{nil, data, opts.AttachmentName, ""} } else { paste = Paste{data, nil, "", ""} } pasteData, err := json.Marshal(&paste) if err != nil { return nil, fmt.Errorf("cannot json marshal paste content: %w", err) } masterKey, err := generateRandomBytes(32) if err != nil { return nil, fmt.Errorf("cannot generate random bytes: %w", err) } iv, err := generateRandomBytes(12) if err != nil { return nil, fmt.Errorf("cannot generate iv: %w", err) } salt, err := generateRandomBytes(8) if err != nil { return nil, fmt.Errorf("cannot generate salt: %w", err) } masterKeyWithPassword := append(masterKey, opts.Password...) key := pbkdf2.Key(masterKeyWithPassword, salt, iterationCount, keySize/8, sha256.New) if opts.Compress == CompressionAlgorithmGZip { var buf bytes.Buffer fw, err := flate.NewWriter(&buf, flate.BestCompression) if err != nil { return nil, fmt.Errorf("cannot create new flate writer: %w", err) } if _, err := fw.Write(pasteData); err != nil { return nil, fmt.Errorf("cannot write in flate buf: %w", err) } if err := fw.Close(); err != nil { return nil, fmt.Errorf("cannot close flate writer: %w", err) } pasteData = buf.Bytes() } adata := AData{ Spec{ iv, salt, iterationCount, keySize, tagSize, EncryptionAlgorithmAES, EncryptionModeGCM, opts.Compress, }, opts.Formatter, opts.OpenDiscussion, opts.BurnAfterReading, } authData, err := json.Marshal(adata) if err != nil { return nil, fmt.Errorf("cannot encode adata: %w", err) } cipherBlock, err := aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("cannot create new cipher: %w", err) } gcm, err := cipher.NewGCM(cipherBlock) if err != nil { return nil, fmt.Errorf("cannot create new galois counter mode: %w", err) } cipherText := gcm.Seal(nil, iv, pasteData, authData) createPasteReq := &createPasteRequest{ V: apiVersion, AData: adata, Meta: createPasteRequestMeta{Expire: opts.Expire}, CT: base64.StdEncoding.EncodeToString(cipherText), } var reqBody bytes.Buffer err = json.NewEncoder(&reqBody).Encode(createPasteReq) if err != nil { return nil, fmt.Errorf("cannot marshal paste request: %w", err) } req, err := http.NewRequestWithContext( ctx, http.MethodPost, c.endpoint.String(), &reqBody, ) if err != nil { return nil, fmt.Errorf("cannot create request: %w", err) } for k, v := range c.customHTTPHeaderFields { req.Header.Set(k, v) } if c.userAgent != "" { req.Header.Set("User-Agent", c.userAgent) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Length", strconv.Itoa(reqBody.Len())) req.Header.Set("X-Requested-With", "JSONHttpRequest") if c.username != "" || c.password != "" { req.SetBasicAuth(c.username, c.password) } res, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("cannot execute http request: %w", err) } defer res.Body.Close() pasteResponse := createPasteResponse{} err = json.NewDecoder(res.Body).Decode(&pasteResponse) if err != nil { return nil, fmt.Errorf("cannot decode response body: %w", err) } if pasteResponse.Status != 0 { return nil, fmt.Errorf("cannot create paste: server respond with %d status: %s", pasteResponse.Status, pasteResponse.Message) } pasteID, err := url.Parse(pasteResponse.URL) if err != nil { return nil, fmt.Errorf("cannot parse paste url: %w", err) } fragment := base58.Encode(masterKey) if opts.BurnAfterReading { fragment = "-" + fragment } pasteLink := url.URL{ Scheme: c.endpoint.Scheme, Host: c.endpoint.Host, Path: c.endpoint.Path, RawQuery: pasteID.RawQuery, Fragment: fragment, } return &CreatePasteResult{ PasteID: pasteResponse.ID, PasteURL: pasteLink, DeleteToken: pasteResponse.DeleteToken, }, nil } func decrypt(masterKey []byte, ct string, adata []byte, spec Spec) ([]byte, error) { encryptedCipherText, err := decode64(ct) if err != nil { return nil, fmt.Errorf("cannot base64 decode cipher text: %w", err) } key := pbkdf2.Key( masterKey, spec.Salt, spec.Iterations, spec.KeySize/8, sha256.New, ) var ( cipherBlock cipher.Block gcm cipher.AEAD ) switch spec.Algorithm { case EncryptionAlgorithmAES: cipherBlock, err = aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("cannot create new cipher: %w", err) } default: return nil, fmt.Errorf("unsupported encryption algorithm: %q", spec.Algorithm) } switch spec.Mode { case EncryptionModeGCM: gcm, err = newGCMWithNonceAndTagSize( cipherBlock, len(spec.IV), spec.TagSize/8, ) if err != nil { return nil, fmt.Errorf("cannot create new galois counter mode: %w", err) } default: return nil, fmt.Errorf("unsupported encryption mode: %q", spec.Mode) } cipherText, err := gcm.Open(nil, spec.IV, encryptedCipherText, adata) if err != nil { return nil, err } switch spec.Compression { case CompressionAlgorithmNone: case CompressionAlgorithmGZip: buf := bytes.NewBuffer(cipherText) fr := flate.NewReader(buf) defer fr.Close() cipherText, err = io.ReadAll(fr) if err != nil { return nil, fmt.Errorf("cannot read gzip: %w", err) } default: return nil, fmt.Errorf("unsupported compression mode: %q", spec.Compression) } return cipherText, nil } privatebin-cli-2.0.2/utils.go000066400000000000000000000056571471607750200161400ustar00rootroot00000000000000// Copyright (c) 2020-2024 Bryan Frimin . // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY // AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. package privatebin import ( "crypto/cipher" "crypto/rand" "encoding/base64" "errors" "net" "net/http" "runtime" "time" ) func btoi(v bool) int { if v { return 1 } return 0 } func itob(v int) bool { return v != 0 } func generateRandomBytes(n uint32) ([]byte, error) { b := make([]byte, n) if _, err := rand.Read(b); err != nil { return nil, err } return b, nil } func decode64(s string) ([]byte, error) { if len(s)%4 == 0 { return base64.StdEncoding.DecodeString(s) } return base64.RawStdEncoding.DecodeString(s) } func defaultPooledClient() *http.Client { dial := &net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, DualStack: true, } transport := &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: dial.DialContext, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, ForceAttemptHTTP2: true, MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1, } return &http.Client{Transport: transport} } // Golang standard library does not expose GCM with custom nonce and // tag size, even if it supported. Following code is a backport from // the Golang crypto module to allowing it. // // References: // - https://go-review.googlesource.com/c/go/+/116435 // - https://github.com/golang/go/issues?q=NewGCMWithNonceAndTagSize // - https://github.com/golang/go/issues/42470 const ( gcmBlockSize = 16 gcmMinimumTagSize = 12 // NIST SP 800-38D recommends tags with 12 or more bytes. ) type gcmAble interface { NewGCM(nonceSize, tagSize int) (cipher.AEAD, error) } func newGCMWithNonceAndTagSize(cipher cipher.Block, nonceSize, tagSize int) (cipher.AEAD, error) { if tagSize < gcmMinimumTagSize || tagSize > gcmBlockSize { return nil, errors.New("cipher: incorrect tag size given to GCM") } if nonceSize <= 0 { return nil, errors.New("cipher: the nonce can't have zero length, or the security of the key will be immediately compromised") } if cipher, ok := cipher.(gcmAble); ok { return cipher.NewGCM(nonceSize, tagSize) } panic("non GCM crypto is not supported") }