From 5d037e8297a192996b7281af0ca761c160aaed30 Mon Sep 17 00:00:00 2001 From: Ryan Mehri Date: Fri, 15 May 2020 17:58:09 -0600 Subject: Add encryption to content when password is specified --- backend/cache/cache.go | 18 +++++++++++-- backend/db/db.go | 21 ++++++++++++--- backend/db/schemas.go | 1 + backend/hashing/hash.go | 41 ---------------------------- backend/security/encrypt.go | 65 +++++++++++++++++++++++++++++++++++++++++++++ backend/security/hash.go | 41 ++++++++++++++++++++++++++++ 6 files changed, 141 insertions(+), 46 deletions(-) delete mode 100644 backend/hashing/hash.go create mode 100644 backend/security/encrypt.go create mode 100644 backend/security/hash.go (limited to 'backend') diff --git a/backend/cache/cache.go b/backend/cache/cache.go index 71007e5..6d5eb42 100644 --- a/backend/cache/cache.go +++ b/backend/cache/cache.go @@ -2,7 +2,7 @@ package cache import ( "errors" - "github.com/jackyzha0/ctrl-v/hashing" + "github.com/jackyzha0/ctrl-v/security" "sync" "github.com/jackyzha0/ctrl-v/db" @@ -17,6 +17,7 @@ var C *Cache var PasteNotFound = errors.New("could not find a paste with that hash") var UserUnauthorized = errors.New("paste is password protected") +var EncryptionError = errors.New("could not encrypt the given content") func init() { C = &Cache{ @@ -46,9 +47,22 @@ func (c *Cache) Get(hash, userPassword string) (db.Paste, error) { // if there is a password, check the provided one against it if p.Password != "" { // if passwords do not match, the user is unauthorized - if !hashing.PasswordsEqual(p.Password, userPassword) { + if !security.PasswordsEqual(p.Password, userPassword) { return db.Paste{}, UserUnauthorized } + + // if password matches, decrypt content + key, _, err := security.DeriveKey([]byte(userPassword), p.Salt) + if err != nil { + return db.Paste{}, EncryptionError + } + + decryptedBytes, err := security.Decrypt(key, []byte(p.Content)) + if err != nil { + return db.Paste{}, EncryptionError + } + + p.Content = string(decryptedBytes) } return p, nil diff --git a/backend/db/db.go b/backend/db/db.go index 4e58188..b18eddf 100644 --- a/backend/db/db.go +++ b/backend/db/db.go @@ -5,7 +5,7 @@ import ( "os" "time" - "github.com/jackyzha0/ctrl-v/hashing" + "github.com/jackyzha0/ctrl-v/security" "github.com/joho/godotenv" log "github.com/sirupsen/logrus" ) @@ -30,7 +30,7 @@ const ContentLimit = 100000 // creates a new paste with title, content and hash, returns the hash of the created paste func New(ip, content, expiry, title, password string) (string, error) { // generate hash from ip - hash := hashing.GenerateURI(ip) + hash := security.GenerateURI(ip) // check for size of title and content errs := checkLengths(title, content) @@ -45,9 +45,24 @@ func New(ip, content, expiry, title, password string) (string, error) { Title: title, } + // if there is a password, encrypt content and hash the password if password != "" { + // use pass to encrypt content + key, salt, err := security.DeriveKey([]byte(password), nil) + if err != nil { + return "", fmt.Errorf("could not generate key: %s", err.Error()) + } + new.Salt = salt + + encryptedBytes, err := security.Encrypt(key, []byte(new.Content)) + if err != nil { + return "", fmt.Errorf("could not encrypt content: %s", err.Error()) + } + + new.Content = string(encryptedBytes) + // hash given password - hashedPass, err := hashing.HashPassword(password) + hashedPass, err := security.HashPassword(password) if err != nil { return "", fmt.Errorf("could not hash password: %s", err.Error()) } diff --git a/backend/db/schemas.go b/backend/db/schemas.go index 4c73f82..d3551fc 100644 --- a/backend/db/schemas.go +++ b/backend/db/schemas.go @@ -14,4 +14,5 @@ type Paste struct { Expiry time.Time `bson:"expiry"` Title string Password string + Salt []byte } diff --git a/backend/hashing/hash.go b/backend/hashing/hash.go deleted file mode 100644 index e944fbe..0000000 --- a/backend/hashing/hash.go +++ /dev/null @@ -1,41 +0,0 @@ -package hashing - -import ( - "crypto/md5" - "encoding/hex" - "golang.org/x/crypto/bcrypt" - "math/big" - "time" -) - -const UrlLength = 7 - -// GenerateURI creates a unique identifier for a paste based on ip and timestamp -func GenerateURI(ip string) string { - timeStamp := time.Now().String() - return hashString(ip + timeStamp)[:UrlLength] -} - -// hashes using MD5 and then converts to base 62 -func hashString(text string) string { - hash := md5.Sum([]byte(text)) - hexStr := hex.EncodeToString(hash[:]) - - bi := big.NewInt(0) - bi.SetString(hexStr, 16) - return bi.Text(62) -} - -func HashPassword(password string) (string, error) { - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - return string(hashedPassword), err -} - -func PasswordsEqual(dbPassword, parsedPassword string) bool { - dbPassBytes := []byte(dbPassword) - parsedPassBytes := []byte(parsedPassword) - compErr := bcrypt.CompareHashAndPassword(dbPassBytes, parsedPassBytes) - - // if comparison error, the given password is not valid - return compErr == nil -} \ No newline at end of file diff --git a/backend/security/encrypt.go b/backend/security/encrypt.go new file mode 100644 index 0000000..fff027c --- /dev/null +++ b/backend/security/encrypt.go @@ -0,0 +1,65 @@ +package security + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "golang.org/x/crypto/scrypt" +) + +func Encrypt(key, data []byte) ([]byte, error) { + blockCipher, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(blockCipher) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err = rand.Read(nonce); err != nil { + return nil, err + } + + cipherText := gcm.Seal(nonce, nonce, data, nil) + + return cipherText, nil +} + +func Decrypt(key, data []byte) ([]byte, error) { + blockCipher, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(blockCipher) + if err != nil { + return nil, err + } + + nonce, cipherText := data[:gcm.NonceSize()], data[gcm.NonceSize():] + plaintext, err := gcm.Open(nil, nonce, cipherText, nil) + if err != nil { + return nil, err + } + + return plaintext, nil +} + +func DeriveKey(password, salt []byte) ([]byte, []byte, error) { + if salt == nil { + salt = make([]byte, 16) + if _, err := rand.Read(salt); err != nil { + return nil, nil, err + } + } + + key, err := scrypt.Key(password, salt, 16384, 8, 1, 16) + if err != nil { + return nil, nil, err + } + + return key, salt, nil +} diff --git a/backend/security/hash.go b/backend/security/hash.go new file mode 100644 index 0000000..b6ce167 --- /dev/null +++ b/backend/security/hash.go @@ -0,0 +1,41 @@ +package security + +import ( + "crypto/md5" + "encoding/hex" + "golang.org/x/crypto/bcrypt" + "math/big" + "time" +) + +const UrlLength = 7 + +// GenerateURI creates a unique identifier for a paste based on ip and timestamp +func GenerateURI(ip string) string { + timeStamp := time.Now().String() + return hashString(ip + timeStamp)[:UrlLength] +} + +// hashes using MD5 and then converts to base 62 +func hashString(text string) string { + hash := md5.Sum([]byte(text)) + hexStr := hex.EncodeToString(hash[:]) + + bi := big.NewInt(0) + bi.SetString(hexStr, 16) + return bi.Text(62) +} + +func HashPassword(password string) (string, error) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(hashedPassword), err +} + +func PasswordsEqual(dbPassword, parsedPassword string) bool { + dbPassBytes := []byte(dbPassword) + parsedPassBytes := []byte(parsedPassword) + compErr := bcrypt.CompareHashAndPassword(dbPassBytes, parsedPassBytes) + + // if comparison error, the given password is not valid + return compErr == nil +} \ No newline at end of file -- cgit v1.2.3