diff options
| -rw-r--r-- | backend/api/routes.go | 8 | ||||
| -rw-r--r-- | backend/cache/cache.go | 17 | ||||
| -rw-r--r-- | backend/db/db.go | 21 | ||||
| -rw-r--r-- | backend/db/schemas.go | 1 | ||||
| -rw-r--r-- | backend/security/encrypt.go | 81 | ||||
| -rw-r--r-- | backend/security/hash.go (renamed from backend/hashing/hash.go) | 2 |
6 files changed, 124 insertions, 6 deletions
diff --git a/backend/api/routes.go b/backend/api/routes.go index ff43714..474fdda 100644 --- a/backend/api/routes.go +++ b/backend/api/routes.go @@ -3,6 +3,7 @@ package api import ( "encoding/json" "fmt" + "github.com/jackyzha0/ctrl-v/security" "net/http" "time" @@ -87,6 +88,13 @@ func handleGetPaste(w http.ResponseWriter, r *http.Request, parsedPassword strin return } + // if internal error with encryption + if err == security.EncryptionError { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "%s", err) + return + } + // otherwise, return paste content, title, and current time w.Header().Set("Content-Type", "application/json") pasteMap := map[string]interface{}{ diff --git a/backend/cache/cache.go b/backend/cache/cache.go index 71007e5..43e615a 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" @@ -46,9 +46,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(userPassword, p.Salt) + if err != nil { + return db.Paste{}, security.EncryptionError + } + + decryptedContent, err := security.Decrypt(key, p.Content) + if err != nil { + return db.Paste{}, security.EncryptionError + } + + p.Content = decryptedContent } return p, nil diff --git a/backend/db/db.go b/backend/db/db.go index 4e58188..df112d0 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(password, nil) + if err != nil { + return "", fmt.Errorf("could not generate key: %s", err.Error()) + } + new.Salt = salt + + encryptedContent, err := security.Encrypt(key, new.Content) + if err != nil { + return "", fmt.Errorf("could not encrypt content: %s", err.Error()) + } + + new.Content = encryptedContent + // 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/security/encrypt.go b/backend/security/encrypt.go new file mode 100644 index 0000000..4af5e3c --- /dev/null +++ b/backend/security/encrypt.go @@ -0,0 +1,81 @@ +package security + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "errors" + "golang.org/x/crypto/scrypt" +) + +var EncryptionError = errors.New("could not encrypt the given content") + +func Encrypt(key, data string) (string, error) { + // initialize aes block cipher with given key + blockCipher, err := aes.NewCipher([]byte(key)) + if err != nil { + return "", err + } + + // wrap block cipher with Galois Counter Mode and standard nonce length + gcm, err := cipher.NewGCM(blockCipher) + if err != nil { + return "", err + } + + // generate nonce (number once used) unique to the given key + nonce := make([]byte, gcm.NonceSize()) + if _, err = rand.Read(nonce); err != nil { + return "", err + } + + // seal nonce with data to use during decryption + cipherText := gcm.Seal(nonce, nonce, []byte(data), nil) + + return string(cipherText), nil +} + +func Decrypt(key, data string) (string, error) { + // similar to encrypt, create cipher and wrap with GCM + blockCipher, err := aes.NewCipher([]byte(key)) + if err != nil { + return "", err + } + + gcm, err := cipher.NewGCM(blockCipher) + if err != nil { + return "", err + } + + // extract the nonce from the data + nonce, cipherText := data[:gcm.NonceSize()], data[gcm.NonceSize():] + + // use nonce to decrypt the data + plaintext, err := gcm.Open(nil, []byte(nonce), []byte(cipherText), nil) + if err != nil { + return "", err + } + + return string(plaintext), nil +} + +const keyBytes = 16 +const iterations = 16384 +const relativeMemoryCost = 8 +const relativeCPUCost = 1 + +func DeriveKey(password string, salt []byte) (string, []byte, error) { + if salt == nil { + salt = make([]byte, keyBytes) + if _, err := rand.Read(salt); err != nil { + return "", nil, err + } + } + + key, err := scrypt.Key([]byte(password), salt, iterations, relativeMemoryCost, relativeCPUCost, keyBytes) + if err != nil { + return "", nil, err + } + + return string(key), salt, nil +} diff --git a/backend/hashing/hash.go b/backend/security/hash.go index e944fbe..b6ce167 100644 --- a/backend/hashing/hash.go +++ b/backend/security/hash.go @@ -1,4 +1,4 @@ -package hashing +package security import ( "crypto/md5" |