diff --git a/cryptojs/cryptojs.go b/cryptojs/cryptojs.go new file mode 100644 index 0000000..b1af6bc --- /dev/null +++ b/cryptojs/cryptojs.go @@ -0,0 +1,126 @@ +package cryptojs + +import ( + "encoding/base64" + "crypto/md5" + "crypto/aes" + "crypto/cipher" + "math/rand" + "errors" + + "git.meow.tf/tyler/go-pastee/evpkdf" + "bytes" +) + +const ( + keySize = 32 +) + +func Encrypt(data, passphrase string) (string, error) { + salt := make([]byte, 8) + + if _, err := rand.Read(salt); err != nil { + return "", err + } + + key := make([]byte, keySize) + iv := make([]byte, aes.BlockSize) + + keymat := evpkdf.New(md5.New, []byte(passphrase), salt, keySize + aes.BlockSize, 1) + keymatbuf := bytes.NewReader(keymat) + + n, err := keymatbuf.Read(key) + + if n != keySize || err != nil { + return "", err + } + + n, err = keymatbuf.Read(iv) + + if n != aes.BlockSize || err != nil { + return "", err + } + + block, err := aes.NewCipher(key) + + if err != nil { + return "", err + } + + padded, err := pkcs7Pad([]byte(data), block.BlockSize()) + + if err != nil { + return "", err + } + + ciphertext := make([]byte, len(padded)) + + cbc := cipher.NewCBCEncrypter(block, iv) + cbc.CryptBlocks(ciphertext, padded) + + return encode(ciphertext, salt), nil +} + +func Decrypt(b64, passphrase string) ([]byte, error) { + salt, ct, err := decode(b64) + + if err != nil { + return nil, err + } + + key := make([]byte, keySize) + iv := make([]byte, aes.BlockSize) + + keymat := evpkdf.New(md5.New, []byte(passphrase), salt, keySize + aes.BlockSize, 1) + keymatbuf := bytes.NewReader(keymat) + + n, err := keymatbuf.Read(key) + + if n != keySize || err != nil { + return nil, err + } + + n, err = keymatbuf.Read(iv) + + if n != aes.BlockSize || err != nil { + return nil, err + } + + block, err := aes.NewCipher(key) + + if err != nil { + return nil, err + } + + cbc := cipher.NewCBCDecrypter(block, iv) + cbc.CryptBlocks(ct, ct) + + plain, err := pkcs7Unpad(ct, block.BlockSize()) + + if err != nil { + return nil, err + } + + return plain, nil +} + +func encode(ct []byte, salt []byte) string { + b := []byte("Salted__") + b = append(b, salt...) + b = append(b, ct...) + return base64.StdEncoding.EncodeToString(b) +} + +func decode(b64 string) ([]byte, []byte, error) { + decoded, err := base64.StdEncoding.DecodeString(b64) + + if err != nil { + return nil, nil, err + } + + if string(decoded[0:8]) != "Salted__" { + return nil, nil, errors.New("invalid data") + } + + return decoded[8:16], decoded[16:], nil +} \ No newline at end of file diff --git a/cryptojs/cryptojs_test.go b/cryptojs/cryptojs_test.go new file mode 100644 index 0000000..a5301cb --- /dev/null +++ b/cryptojs/cryptojs_test.go @@ -0,0 +1,47 @@ +package cryptojs + +import "testing" + +func Test_EncryptDecrypt(t *testing.T) { + enc, err := Encrypt("testing123", "testing") + + if err != nil { + t.Fatal("Unable to encrypt:", err) + } + + b, err := Decrypt(enc, "testing") + + if err != nil { + t.Fatal("Unable to decrypt:", err) + } + + final := string(b) + + if final != "testing123" { + t.Fatal("Final text does not match:", final) + } +} + +func Test_WebsiteEncrypt(t *testing.T) { + enc, err := Encrypt("Testing Paste.ee", "wXBbztgFOsZtTvhK1vcZlR7izK84bmUW") + + if err != nil { + t.Fatal("Error:", err) + } + + if enc != "U2FsdGVkX183sL2jzqJiJdhIl0O7R9v46TIkKnORreGpUufYUsqjqYO9b++CSyvV" { + t.Fatal("Unexpected output:", enc) + } +} + +func Test_WebsiteDecrypt(t *testing.T) { + b, err := Decrypt("U2FsdGVkX183sL2jzqJiJdhIl0O7R9v46TIkKnORreGpUufYUsqjqYO9b++CSyvV", "wXBbztgFOsZtTvhK1vcZlR7izK84bmUW") + + if err != nil { + t.Fatal("Unable to decrypt:", err) + } + + if string(b) != "Testing Paste.ee" { + t.Fatal("Unexpected output:", string(b)) + } +} \ No newline at end of file diff --git a/cryptojs/pkcs7.go b/cryptojs/pkcs7.go new file mode 100644 index 0000000..ad978ac --- /dev/null +++ b/cryptojs/pkcs7.go @@ -0,0 +1,48 @@ +package cryptojs + +import ( + "fmt" + "bytes" +) + +// Appends padding. +func pkcs7Pad(data []byte, blocklen int) ([]byte, error) { + if blocklen <= 0 { + return nil, fmt.Errorf("invalid blocklen %d", blocklen) + } + padlen := uint8(1) + for ((len(data) + int(padlen)) % blocklen) != 0 { + padlen++ + } + + if int(padlen) > blocklen { + panic(fmt.Sprintf("generated invalid padding length %v for block length %v", padlen, blocklen)) + } + pad := bytes.Repeat([]byte{byte(padlen)}, int(padlen)) + return append(data, pad...), nil +} + +// Returns slice of the original data without padding. +func pkcs7Unpad(data []byte, blocklen int) ([]byte, error) { + if blocklen <= 0 { + return nil, fmt.Errorf("invalid blocklen %d", blocklen) + } + if len(data)%blocklen != 0 || len(data) == 0 { + return nil, fmt.Errorf("invalid data len %d", len(data)) + } + + padlen := int(data[len(data)-1]) + if padlen > blocklen || padlen == 0 { + // Not padded + return data, nil + } + // check padding + pad := data[len(data)-padlen:] + for i := 0; i < padlen; i++ { + if pad[i] != byte(padlen) { + return data, nil + } + } + + return data[:len(data)-padlen], nil +} diff --git a/evpkdf/evpkdf.go b/evpkdf/evpkdf.go new file mode 100644 index 0000000..0a266ee --- /dev/null +++ b/evpkdf/evpkdf.go @@ -0,0 +1,55 @@ +// Copyright (c) 2016 Tristan Colgate-McFarlane +// +// This file is part of evpkdf. +// +// radia is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// radia is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with radia. If not, see . + +// Package evpkdf implements OpenSSL EVP Key derivation function, aiming to +// be compatible with crypto-js default behaviour. +package evpkdf + +import ( + "bytes" + "hash" + "io" +) + +//New creates a key derivation function that should match the EVP kdf in crypto-js, which, in turn +// should be compatible with openssl's EVP kdf +func New(hash func() hash.Hash, password []byte, salt []byte, keysize int, iterations int) []byte { + hasher := hash() + derivedKey := []byte{} + block := []byte{} + + // Generate key + for len(derivedKey) < keysize { + if len(block) != 0 { + io.Copy(hasher, bytes.NewBuffer(block)) + } + io.Copy(hasher, bytes.NewBuffer(password)) + io.Copy(hasher, bytes.NewBuffer(salt)) + block = hasher.Sum(nil) + hasher.Reset() + + // Iterations + for i := 1; i < iterations; i++ { + io.Copy(hasher, bytes.NewBuffer(block)) + block = hasher.Sum(nil) + hasher.Reset() + } + + derivedKey = append(derivedKey, block...) + } + return derivedKey[0:keysize] +} \ No newline at end of file diff --git a/pastee.go b/pastee.go index 54a487d..a109d60 100644 --- a/pastee.go +++ b/pastee.go @@ -1,17 +1,118 @@ -package pastee +package GoPastee import ( "net/http" "io" "encoding/json" "bytes" + "strconv" + "time" + "net/url" + "math/rand" + + "git.meow.tf/tyler/go-pastee/cryptojs" ) func New(key string) *Pastee { - return &Pastee{ApiKey: key, Base: "https://api.paste.ee/v1", Client: &http.Client{}} + return &Pastee{ApiKey: key, Base: "https://api.paste.ee/v1", Client: &http.Client{Timeout: time.Minute}} +} + +func (p *Pastee) Authenticate(username, password string) (*AuthResponse, error) { + body, err := json.Marshal(&authRequest{username, password}) + + if err != nil { + return nil, err + } + + req, err := p.newRequest("POST", "users/authenticate", bytes.NewReader(body)) + + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Content-Length", strconv.Itoa(len(body))) + + res, err := p.Client.Do(req) + + if err != nil { + return nil, err + } + + defer res.Body.Close() + + var response AuthResponse + + if err := json.NewDecoder(res.Body).Decode(&response); err != nil { + return nil, err + } + + return &response, nil +} + +func (p *Pastee) List() (*PasteListResponse, error) { + q := &url.Values{} + q.Set("perpage", "25") + q.Set("page", "1") + + req, err := p.newRequest("GET", "pastes?" + q.Encode(), nil) + + if err != nil { + return nil, err + } + + res, err := p.Client.Do(req) + + if err != nil { + return nil, err + } + + defer res.Body.Close() + + var response PasteListResponse + + if err := json.NewDecoder(res.Body).Decode(&response); err != nil { + return nil, err + } + + return &response, nil +} + +func (p *Pastee) Get(id string) (*Paste, error) { + req, err := p.newRequest("GET", "pastes/" + id, nil) + + if err != nil { + return nil, err + } + + res, err := p.Client.Do(req) + + if err != nil { + return nil, err + } + + defer res.Body.Close() + + var response Paste + + if err := json.NewDecoder(res.Body).Decode(&response); err != nil { + return nil, err + } + + return &response, nil } func (p *Pastee) Submit(paste *Paste) (*PasteResponse, error) { + var key string + + if paste.Encrypted { + key = RandStringBytesMaskImprSrc(32) + + for _, section := range paste.Sections { + section.Contents = cryptojs.Encrypt(section.Contents, key) + } + } + body, err := json.Marshal(paste) if err != nil { @@ -25,7 +126,7 @@ func (p *Pastee) Submit(paste *Paste) (*PasteResponse, error) { } req.Header.Set("Content-Type", "application/json") - req.Header.Set("Content-Length", len(body)) + req.Header.Set("Content-Length", strconv.Itoa(len(body))) res, err := p.Client.Do(req) @@ -41,11 +142,15 @@ func (p *Pastee) Submit(paste *Paste) (*PasteResponse, error) { return nil, err } + if key != "" { + response.Key = key + } + return &response, nil } func (p *Pastee) newRequest(method, path string, body io.Reader) (*http.Request, error) { - req, err := http.NewRequest(method, p.Base + path, body) + req, err := http.NewRequest(method, p.Base + "/" + path, body) if err != nil { return nil, err @@ -54,4 +159,31 @@ func (p *Pastee) newRequest(method, path string, body io.Reader) (*http.Request, req.Header.Set("X-Auth-Token", p.ApiKey) return req, nil +} + +const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +const ( + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = src.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + + return string(b) } \ No newline at end of file diff --git a/structs.go b/structs.go index 0e66748..91e4cad 100644 --- a/structs.go +++ b/structs.go @@ -1,4 +1,4 @@ -package pastee +package GoPastee import "net/http" @@ -9,6 +9,8 @@ type Pastee struct { } type Paste struct { + ID string `json:"id,omitempty"` + Views int `json:"views,omitempty"` Encrypted bool `json:"encrypted,omitempty"` Description string `json:"description,omitempty"` Sections []*Section `json:"sections"` @@ -29,6 +31,34 @@ type Error struct { type PasteResponse struct { Success bool `json:"success"` Errors []*Error `json:"errors"` + Key string `json:"-"` ID string `json:"id"` Link string `json:"link"` +} + +type authRequest struct { + username string `json:"username"` + password string `json:"password"` +} + +type AuthResponse struct { + Success bool `json:"success"` + Key string `json:"key"` +} + +type PaginationResponse struct { + Total int `json:"total"` + PerPage int `json:"per_page"` + CurrentPage int `json:"current_page"` + LastPage int `json:"last_page"` + NextPageURL string `json:"next_page_url"` + PreviousPageURL string `json:"prev_page_url"` + From int `json:"from"` + To int `json:"to"` +} + +type PasteListResponse struct { + *PaginationResponse + + Data []*Paste `json:"data"` } \ No newline at end of file