Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6288136
Merge branch 'master' of github.com:rfc2119/bitbot into dev
rfc2119 Mar 30, 2020
b83cabc
Merge branch 'dev' of https://github.com/bbriggs/bitbot into dev
rfc2119 Mar 30, 2020
77e7d58
Added sequence_pastebin.png
Mar 31, 2020
69d9bdd
Added sequence_pastebin.png
Mar 31, 2020
a52a57e
squash me
rfc2119 Mar 31, 2020
fcb6b34
Merge branch 'feature/pastebin' of github.com:rfc2119/bitbot into fea…
rfc2119 Mar 31, 2020
4cc3dea
[WIP] privatebin plugin for bitbot
rfc2119 Apr 1, 2020
95ef333
types and routines for encrypting locally
rfc2119 Apr 5, 2020
9d48ec9
added routine for sending/receiving paste
rfc2119 Apr 6, 2020
b6d6050
adapting Go structrures to suit the JSON
rfc2119 Apr 7, 2020
1b8f859
manually created the paste request
rfc2119 Apr 8, 2020
856c78b
reworked the struct for pasteReq
rfc2119 Apr 12, 2020
5aee7a8
removed the misleading req.WriteReq()
rfc2119 Apr 16, 2020
809b30f
use a KDF to derive aes key; working version!
rfc2119 Apr 19, 2020
3dcc8a5
Uppercase arguments to covid19 stats function
bbriggs Mar 31, 2020
fd733e7
Get total for country by summing all provinces (#152)
bbriggs Apr 1, 2020
d898ade
Basic Province trigger patch
m-242 Apr 1, 2020
3b4a7c5
Fixed total trigger addition loops
m-242 Apr 1, 2020
9057ade
removing code anti-patterns (#158)
staticnotdynamic Apr 6, 2020
71dcbd0
test rebasing
rfc2119 Apr 25, 2020
d9a8936
Added golang-ci "//nolint" to avoid global variables false positives …
m-242 Apr 8, 2020
29d5660
Do not remind us that bitbot global vars are global.
m-242 Apr 8, 2020
dd543e5
Ignored False Positives on the Body.Close() method, golangci-lint wan…
m-242 Apr 8, 2020
a4f413c
Corrected mistakes
m-242 Apr 8, 2020
340d614
Added a linter to already nolinted false positives
m-242 Apr 8, 2020
16f853f
Fixed improper definition sanitizing
m-242 Apr 8, 2020
466b0d5
Merge branch 'dev' into feature/pastebin
m-242 Apr 25, 2020
eb6261c
!paste now activates on a private message only
rfc2119 Apr 25, 2020
a468ba7
Merge branch 'feature/pastebin' of github.com:rfc2119/bitbot into fea…
rfc2119 Apr 25, 2020
09a52c8
Formatted and other small changes that help with the linting but don'…
m-242 May 9, 2020
306da39
Merge pull request #1 from m-242/feature/pastebin
May 11, 2020
95f112d
Merge branch 'dev' into feature/pastebin
bbriggs Sep 25, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,4 @@ See our [contributing guide](CONTRIBUTING.md)
### License

Bitbot is available under the [MIT License](LICENSE)

301 changes: 301 additions & 0 deletions bitbot/paste.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
package bitbot

// welp; it turned out the key (the base58 encoded one) is used in a key-derivation function; that is, the key used in AES-GCM is the derived one; consequently, the salt we send is equally important; here're the specs
// key derivation function: pbkdf2
// key: password of paste + our random key
// salt: the one we send
// hash algorithm: sha-256 (used at the end of derivation)
// iterations: 10,000 (can we instruct the server to lower it ?)
// ====================================================
// will be using this: https://pkg.go.dev/golang.org/x/crypto/pbkdf2?tab=doc
import (
"bytes"
"compress/zlib"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"golang.org/x/crypto/pbkdf2"
// "encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
// "net"
// "time"
"net/http"
// "os"
"strings"

"github.com/btcsuite/btcutil/base58"
"github.com/whyrusleeping/hellabot"
)

const (
nonceSize = 16 // privatebin uses a nonce of 16 bytes by default
aesKeySize = 32 // using aes-256-gcm; for reference only
gcmTagSize = 16 // for reference
kdfSaltSize = 8 // for reference
)

// Array1 : not used directly in the paste request
type Array1 struct { // TODO: more descriptive name
// TODO: the following fields (till EOF) should be a (json ?) array
Nonce []byte // base64(cipher_iv); getRandomBytes(16) default
Kdfsalt []byte // base64(kdf_salt); getRandomBytes(8) default
KdfIterations int // pbkdf_iterations; default
KdfKeySize int // pbkdf_keysize; default
CipherTagSize int // cipher_tag_size (wtf ?); default
CipherAlgo string // cipher_algo; default
CipherMode string // cipher_mode; default
CompressionType string // compression_type; default
// EOF
}

// AuthData : Format is the paste's format.
type AuthData struct { //TODO: should be type "json" ?
//
EncryptionDetails []interface{} // TODO: more descriptive name (type was: Array1)
Format string // format of the paste
OpenDiscussion int // open-discussion flag (TODO: not sure if bool works)
BurnAfterReading int // burn-after-reading flag (TODO: not sure if bool works)
//
}

// PasteData : !shrug (see https://github.com/PrivateBin/PrivateBin/wiki/Encryption-format#data-passed-in)
type PasteData struct {
Paste string `json:"paste"` // ciphertext (encrypted zlib'd plaintext)
Attachment string `json:"attachment"`
AttachementName string `json:"attachment_name"`
Children []interface{} `json:"children"`
}

// ============================================================================================================================================================================================================================

// PasteMeta : https://raw.githubusercontent.com/PrivateBin/PrivateBin/master/js/types.jsonld
type PasteMeta struct {
Expire string `json:"expire"` // ["5min", "10min", "1hour", "1day", "1week", "1month", "1year", "never"]
}

// PasteResponse : A request's response, parsed
type PasteResponse struct {
Status int `json:"status"`
Id string `json:"id"`
Url string `json:"url"`
Deletetoken string `json:"deletetoken"`
}

// PasteRequest : A paste request
type PasteRequest struct {
AuthData []interface{} `json:"adata"`
Meta PasteMeta `json:"meta"`
Version int `json:"v"`
CipherText []byte `json:"ct"`
}

// NewRequest : Forges a new request to be posted.
func NewRequest(aData []interface{}, cipherText []byte, expiryDate string) *PasteRequest {

var (
req PasteRequest
)

meta := PasteMeta{expiryDate}

req.AuthData = aData
req.Meta = meta
req.Version = 2
req.CipherText = cipherText
return &req

}

// PasteTrigger : The trigger that takes care of the pasting.
var PasteTrigger = NamedTrigger{ //nolint
ID: "paste",
Help: "returns a pastebin link for a PRIVMSG to bitbot" +
"\nUsage: !paste <content>",
Condition: func(irc *hbot.Bot, m *hbot.Message) bool {
return m.Command == "PRIVMSG" && // This is a message
m.Params[0] == irc.Nick && // Private to the bot
strings.HasPrefix(m.Content, "!paste ") // beginning with !paste
},
Action: func(irc *hbot.Bot, m *hbot.Message) bool {

var (
pasteResp PasteResponse
pasteReq *PasteRequest
err error
)
plaintext := []byte(m.Content) // TODO: only fetch paste content and options
key, nonce, kdfsalt := generateEncryptionParameters()
adata := generateAuthenticationData(nonce, kdfsalt, "plaintext", 0, 0)
aesKey := pbkdf2.Key(key, kdfsalt, 100000, aesKeySize, sha256.New)
ciphertext := encrypt(plaintext, aesKey, nonce, adata) // auth tag is appended to ciphertext
pasteReq = NewRequest(adata, ciphertext, "1week")
if pasteResp, err = recvPaste(pasteReq); err != nil {
fmt.Println("could not receive paste")
return false
}

if pasteResp.Status != 0 {
irc.Reply(m, "This is impossible to see unless the same key AND message were used")
return false
}
url := fmt.Sprintf("https://bin.fraq.io%s#%s", pasteResp.Url, base58.Encode(key))
deleteURL := fmt.Sprintf("https://bin.fraq.io/?pasteid=%s&deletetoken=%s", pasteResp.Id, pasteResp.Deletetoken)
fmt.Printf("response: %v\n", pasteResp) // TODO: use logging
fmt.Printf("url: %s\n delete url: %s\n", url, deleteURL) // TODO: use logging
irc.Reply(m, fmt.Sprintf("Link: %s | Delete paste: %s", url, deleteURL))

return true
},
}

func generateAuthenticationData(iv []byte, dummyKDFsalt []byte, format string, openDiscussion int, burnAfterReading int) []interface{} {
// encryptionInfo := Array1{iv, dummyKDFsalt, 10000, 265, 128, "aes", "gcm", "zlib"}
// encryptionInfo := make([]interface{}, 0)
var (
encryptionInfo []interface{}
aData []interface{} // or aData := make([]interface{}, 0); then append
)
encryptionInfo = append(encryptionInfo, iv, dummyKDFsalt, 100000, 256, 128, "aes", "gcm", "none") // TODO: rget back zlib support
aData = append(aData, encryptionInfo, format, openDiscussion, burnAfterReading)
return aData
}
func generateEncryptionParameters() (key, iv, kdfSalt []byte) {

// since we'll be using a different random key for each paste,
// a fixed nonce should be OK (but we won't do it anyway)
totalSize := aesKeySize + nonceSize + kdfSaltSize
keyWithNonceAndKdfSalt := make([]byte, totalSize)
if _, err := io.ReadFull(rand.Reader, keyWithNonceAndKdfSalt); err != nil {
panic(err.Error())
}

key = keyWithNonceAndKdfSalt[:aesKeySize]
iv = keyWithNonceAndKdfSalt[aesKeySize : aesKeySize+nonceSize]
kdfSalt = keyWithNonceAndKdfSalt[totalSize-kdfSaltSize:] // dummy value for PBKDF as we don't use it

return key, iv, kdfSalt

}

func recvPaste(pasteReq *PasteRequest) (resp PasteResponse, err error) {
var (
jsonForm []byte
req *http.Request
r *http.Response
)

if jsonForm, err = json.Marshal(pasteReq); err != nil { // Marshal, not NewEncoder
fmt.Println(err)
}
fmt.Printf("marhsalled json: %s\n", jsonForm)
// ==== cert ==== //
// TODO: self-signed certificates workaround (https://groups.google.com/d/msg/golang-nuts/v5ShM8R7Tdc/I2wyTy1o118J)
cert, err := ioutil.ReadFile("/tmp/burp.pem") // TODO: to debug w/ burp, install its cert here
if err != nil {
fmt.Println("error in importing cert: ", err)
}
caCertPool := x509.NewCertPool()
if ok := caCertPool.AppendCertsFromPEM(cert); ok != true {
fmt.Println("error in appending cert")
}
transportOptions := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
RootCAs: caCertPool,
},
Proxy: http.ProxyFromEnvironment,
}

httpClient := &http.Client{Transport: transportOptions}
if req, err = http.NewRequest("POST", "https://bin.fraq.io", bytes.NewReader(jsonForm)); err != nil { // TODO: don't hardcode url
fmt.Println(err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
req.Header.Add("X-Requested-With", "JSONHttpRequest") // reason we used http.NewRequest w/ Client.Do()
// req.Header.Add("Origin", "https://bin.fraq.io")

// fmt.Println("debugging body: ")
// if b, err := req.GetBody(); err == nil{
// fmt.Println(ioutil.ReadAll(b))
// fmt.Println(ioutil.ReadAll(req.Body))
// }
// resp, err := http.Post(, "application/json", &jsonForm)
url, err := http.ProxyFromEnvironment(req)
fmt.Printf("proxy: %s %s\n", url, err)
// req.WriteProxy(os.Stdout)

if r, err = httpClient.Do(req); err != nil {
fmt.Printf("post to paste site error: %s\n", err) // TODO: use log
return resp, err
}
// fmt.Println(r.Body)
defer r.Body.Close()
// body, err := ioutil.ReadAll(r.Body)
// fmt.Printf("received body: %s, err %s\n", body, err)
// err = json.Unmarshal(body, &resp)
if err = json.NewDecoder(r.Body).Decode(&resp); err != nil {
fmt.Printf("json decoding error: %s\n", err) // TODO: use logging
}

return resp, err
}

func encrypt(plaintext, key, iv []byte, authenticationData []interface{}) (ciphertext []byte) {
// compresses the message with zlib and encrypts it with a random key

block, err := aes.NewCipher(key) // will auto-pick aes-256 because key size
if err != nil {
panic(err.Error())
}

aesgcm, err := cipher.NewGCMWithNonceSize(block, nonceSize) //TODO: should instruct privatebin to use standard nonce size instead
if err != nil {
panic(err.Error())
}

// compress and encrypt message, then encode key and return
var (
compressedCiphertext bytes.Buffer
// pasteData PasteData
// encodedCompressedPlaintext bytes.Buffer
cipherJson, authenticatedDataJson []byte
)
// pasteData = PasteData{Paste: string(plaintext)} // TODO: support file attachement and paste linking
pasteData := struct {
Paste string `json:"paste"`
}{
string(plaintext),
}
if cipherJson, err = json.Marshal(pasteData); err != nil { // Marshal, not NewEncoder
panic(err.Error())
}
if authenticatedDataJson, err = json.Marshal(authenticationData); err != nil { // Marshal, not NewEncoder
panic(err.Error())
}
fmt.Printf("marshalled cipher: %s\n", cipherJson)
fmt.Printf("marshalled adata: %s\n", authenticatedDataJson)

// TODO: add check for compression support (for now, assuming defaults); get back zlib
compressedCiphertextWriter := zlib.NewWriter(&compressedCiphertext)
compressedCiphertextWriter.Write(cipherJson)
compressedCiphertextWriter.Close()
// encoder := base64.NewEncoder(base64.StdEncoding, &encodedCompressedPlaintext)
// encoder.Write(compressedCiphertext.Bytes())
// encoder.Close()

// authData is authenticated as well(https://github.com/r4sas/PBinCLI/blob/682b47fbd3e24a8a53c3b484ba896a5dbc85cda2/pbincli/format.py#L122)
// kudos to filo for hinting about the tag location (https://github.com/golang/go/issues/32742)
// look for function " decryptOrPromptPassword" in privatebin.js; start debugging there
// TODO: fully support the API (https://github.com/PrivateBin/PrivateBin/wiki/API)
ciphertext = aesgcm.Seal(nil, iv, cipherJson, authenticatedDataJson) // TODO: zzzzlib
// encodedNonce := base64.StdEncoding.EncodeToString(nonce)
// encodedCipherText := base64.StdEncoding.EncodeToString(ciphertext)
fmt.Printf("pt: %s\n key: %s\n", plaintext, base58.Encode(key))
return ciphertext
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ var pluginMap = map[string]bitbot.NamedTrigger{
"markovTrainer": bitbot.MarkovTrainerTrigger,
"epeen": bitbot.EpeenTrigger,
"ipinfo": bitbot.IPinfoTrigger,
"paste": bitbot.PasteTrigger,
"urbd": bitbot.UrbanDictionaryTrigger,
"reminder": bitbot.ReminderTrigger,
"lennyface": bitbot.LennyTrigger,
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ services:
environment:
SERVER: "irc.example.com:6697"
CHANNELS: "#bots"
NICK: "bitbot-fraq"
NICK: "bitbot-test"
SSL: "true"
PROM: "true"
PROMADDR: "0.0.0.0:8080"
Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ go 1.12

require (
github.com/bbriggs/quotes v0.0.0-20190225011706-ef48dc0c0ec7
github.com/inconshreveable/log15 v0.0.0-20200109203555-b30bc20e4fd1
github.com/btcsuite/btcutil v1.0.1
github.com/inconshreveable/log15 v0.0.0-20180818164646-67afb5ed74ec // indirect
github.com/jinzhu/gorm v1.9.12
github.com/justinian/dice v0.0.0-20170728002755-6a18b51d929c
github.com/leanovate/gopter v0.2.4
github.com/mb-14/gomarkov v0.0.0-20190125094512-044dd0dcb5e7
github.com/mudler/sendfd v0.0.0-20150620134918-f0fc74c13877 // indirect
github.com/prometheus/client_golang v1.2.1
github.com/spf13/cobra v0.0.5
github.com/spf13/viper v1.4.0
github.com/whyrusleeping/hellabot v0.0.0-20200524171630-1ea75c7dc208
go.etcd.io/bbolt v1.3.3
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582
gopkg.in/inconshreveable/log15.v2 v2.0.0-20200109203555-b30bc20e4fd1
gopkg.in/sorcix/irc.v1 v1.1.4
Expand Down
Loading