From 6f31f35c7b38fbf63d7a0c9322458e0b75828495 Mon Sep 17 00:00:00 2001 From: Alexander Kiryukhin Date: Thu, 18 Mar 2021 02:06:42 +0300 Subject: Initial --- internal/encryption/encryption.go | 93 ++++++++++++++++++++++++++++ internal/renderer/renderer.go | 45 ++++++++++++++ internal/storer/storer.go | 124 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 262 insertions(+) create mode 100644 internal/encryption/encryption.go create mode 100644 internal/renderer/renderer.go create mode 100644 internal/storer/storer.go (limited to 'internal') diff --git a/internal/encryption/encryption.go b/internal/encryption/encryption.go new file mode 100644 index 0000000..0f9c748 --- /dev/null +++ b/internal/encryption/encryption.go @@ -0,0 +1,93 @@ +package encryption + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "errors" + "io" + "strings" +) + +func addBase64Padding(value string) string { + m := len(value) % 4 + if m != 0 { + value += strings.Repeat("=", 4-m) + } + + return value +} + +func removeBase64Padding(value string) string { + return strings.Replace(value, "=", "", -1) +} + +func pad(src []byte) []byte { + padding := aes.BlockSize - len(src)%aes.BlockSize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + + return append(src, padtext...) +} + +func Unpad(src []byte) ([]byte, error) { + length := len(src) + unpadding := int(src[length-1]) + + if unpadding > length { + return nil, errors.New("unpad error. This could happen when incorrect encryption key is used") + } + + return src[:(length - unpadding)], nil +} + +func Encrypt(key []byte, text string) (string, error) { + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + msg := pad([]byte(text)) + ciphertext := make([]byte, aes.BlockSize+len(msg)) + iv := ciphertext[:aes.BlockSize] + + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return "", err + } + + cfb := cipher.NewCFBEncrypter(block, iv) + cfb.XORKeyStream(ciphertext[aes.BlockSize:], []byte(msg)) + finalMsg := removeBase64Padding(base64.URLEncoding.EncodeToString(ciphertext)) + + return finalMsg, nil +} + +func Decrypt(key []byte, text string) (string, error) { + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + decodedMsg, err := base64.URLEncoding.DecodeString(addBase64Padding(text)) + if err != nil { + return "", err + } + + if (len(decodedMsg) % aes.BlockSize) != 0 { + return "", errors.New("blocksize must be multipe of decoded message length") + } + + iv := decodedMsg[:aes.BlockSize] + msg := decodedMsg[aes.BlockSize:] + + cfb := cipher.NewCFBDecrypter(block, iv) + cfb.XORKeyStream(msg, msg) + + unpadMsg, err := Unpad(msg) + if err != nil { + return "", err + } + + return string(unpadMsg), nil +} diff --git a/internal/renderer/renderer.go b/internal/renderer/renderer.go new file mode 100644 index 0000000..bed603c --- /dev/null +++ b/internal/renderer/renderer.go @@ -0,0 +1,45 @@ +package renderer + +import ( + "html/template" + "net/http" + "path" + "path/filepath" +) + +// Renderer represents html template renderer +type Renderer struct { + templates map[string]*template.Template +} + +// New Renderer +func New(gpath string) (*Renderer, error) { + t := &Renderer{ + templates: make(map[string]*template.Template), + } + + includes, err := filepath.Glob(path.Join(gpath, "includes/*.gohtml")) + if err != nil { + return nil, err + } + + pages, err := filepath.Glob(path.Join(gpath, "pages/*.gohtml")) + if err != nil { + return nil, err + } + + for _, p := range pages { + tpls := append(includes, p) + tname := path.Base(p) + t.templates[tname] = template.Must(template.New(tname).ParseFiles(tpls...)) + } + + return t, nil +} + +func (r *Renderer) Render(tpl string, data Map, rw http.ResponseWriter) error { + rw.Header().Set("Content-Type", "text/html") + return r.templates[tpl].Execute(rw, data) +} + +type Map map[string]interface{} diff --git a/internal/storer/storer.go b/internal/storer/storer.go new file mode 100644 index 0000000..b8b4ffd --- /dev/null +++ b/internal/storer/storer.go @@ -0,0 +1,124 @@ +package storer + +import ( + "bytes" + "encoding/gob" + "time" + + "github.com/dgraph-io/badger" + "github.com/neonxp/sendsafe/internal/encryption" + "github.com/rs/xid" +) + +type Store struct { + db *badger.DB +} + +func New(dbFile string) (*Store, error) { + db, err := badger.Open(badger.DefaultOptions(dbFile)) + if err != nil { + return nil, err + } + + return &Store{ + db: db, + }, nil +} + +func (s *Store) Save(text string, pin string, ttl int) (string, error) { + var err error + + encrypted := false + + if pin != "" { + text, err = encryption.Encrypt([]byte(pin), text) + if err != nil { + return "", err + } + + encrypted = true + } + + record := memo{ + Text: text, + Encrypted: encrypted, + } + + buf := bytes.NewBuffer([]byte{}) + if err := gob.NewEncoder(buf).Encode(record); err != nil { + return "", err + } + + id := xid.New() + err = s.db.Update(func(txn *badger.Txn) error { + return txn.SetEntry(&badger.Entry{ + Key: id.Bytes(), + Value: buf.Bytes(), + ExpiresAt: uint64(time.Now().Add(time.Duration(ttl) * time.Minute).Unix()), + }) + }) + + return id.String(), err +} + +func (s *Store) IsEncrypted(id string) (bool, error) { + var encrypted bool + + return encrypted, s.db.View(func(txn *badger.Txn) error { + value, err := txn.Get([]byte(id)) + if err != nil { + return err + } + record := new(memo) + return value.Value(func(val []byte) error { + if err := gob.NewDecoder(bytes.NewBuffer(val)).Decode(record); err != nil { + return err + } + encrypted = record.Encrypted + return nil + }) + }) +} + +func (s *Store) Get(id string, pin string) (string, error) { + var text string + + return text, s.db.Update(func(txn *badger.Txn) error { + uid, err := xid.FromString(id) + if err != nil { + return err + } + value, err := txn.Get(uid.Bytes()) + if err != nil { + return err + } + record := new(memo) + err = value.Value(func(val []byte) error { + if err := gob.NewDecoder(bytes.NewBuffer(val)).Decode(record); err != nil { + return err + } + return nil + }) + if err != nil { + return err + } + + text = record.Text + if record.Encrypted { + text, err = encryption.Decrypt([]byte(pin), text) + if err != nil { + return err + } + } + return txn.Delete(uid.Bytes()) + }) +} + +func init() { + gob.Register(memo{}) +} + +type memo struct { + Text string + Encrypted bool +} -- cgit v1.2.3