Files
Gokapi/internal/encryption/Encryption.go
2022-03-08 18:14:47 +01:00

279 lines
7.4 KiB
Go

package encryption
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"github.com/forceu/gokapi/internal/helper"
"github.com/forceu/gokapi/internal/models"
"github.com/secure-io/sio-go"
"golang.org/x/crypto/scrypt"
"io"
"log"
"time"
)
// NoEncryption means all files are stored in plaintext
const NoEncryption = 0
// LocalEncryptionStored means remote files are stored in plaintext, cipher for local files is in plaintext
const LocalEncryptionStored = 1
// LocalEncryptionInput means remote files are stored in plaintext, password needs to be entered on startup
const LocalEncryptionInput = 2
// FullEncryptionStored means all files are encrypted, cipher for local files is in plaintext
const FullEncryptionStored = 3
// FullEncryptionInput means all files are encrypted, password needs to be entered on startup
const FullEncryptionInput = 4
// EndToEndEncryption means all files are encrypted and decrypted client-side
const EndToEndEncryption = 5
var encryptedKey, ramCipher []byte
const blockSize = 32
const nonceSize = 12
// Init needs to be called to load the master key into memory or ask the user for the password
func Init(config models.Configuration) {
switch config.Encryption.Level {
case NoEncryption:
return
case LocalEncryptionStored:
fallthrough
case FullEncryptionStored:
initWithCipher(config.Encryption.Cipher)
case LocalEncryptionInput:
fallthrough
case FullEncryptionInput:
initWithPassword(config.Encryption.Salt, config.Encryption.Checksum, config.Encryption.ChecksumSalt)
case EndToEndEncryption:
// TODO
}
}
func initWithPassword(saltPw, expectedChecksum, saltChecksum string) {
if saltPw == "" || saltChecksum == "" {
log.Fatal("Empty salt provided. Please rerun setup with --reconfigure")
}
pw := readAndCheckPassword(expectedChecksum, saltChecksum)
cipherKey, err := scrypt.Key([]byte(pw), []byte(saltPw), 1048576, 8, 1, blockSize)
if err != nil {
cipherKey = []byte{}
log.Fatal(err)
}
storeMasterKey(cipherKey)
}
func readAndCheckPassword(expectedChecksum, saltChecksum string) string {
fmt.Println("Please enter encryption password:")
pw := helper.ReadPassword()
if pw == "" {
log.Fatal("Empty password provided")
}
fmt.Print("Checking password")
checksumFinished := false
go func() {
for !checksumFinished {
fmt.Print(".")
time.Sleep(time.Second)
}
}()
checkSum := PasswordChecksum(pw, saltChecksum)
checksumFinished = true
if checkSum != expectedChecksum {
pw = ""
fmt.Println("FAIL")
log.Fatal("Incorrect password provided")
}
fmt.Println("OK")
return pw
}
// PasswordChecksum creates a checksum which is used to check if the supplied password is correct
func PasswordChecksum(pw, salt string) string {
cipherKey, err := scrypt.Key([]byte(pw), []byte(salt), 1048576, 8, 1, blockSize)
if err != nil {
cipherKey = []byte{}
log.Fatal(err)
}
hasher := sha256.New()
hasher.Write(cipherKey)
return hex.EncodeToString(hasher.Sum(nil))
}
func initWithCipher(cipherKey []byte) {
if len(cipherKey) != 32 {
log.Fatal("Invalid cipher provided. Please rerun setup with --reconfigure")
}
storeMasterKey(cipherKey)
}
func storeMasterKey(cipherKey []byte) {
var err error
ramCipher, err = getRandomData(blockSize)
if err != nil {
log.Fatal(err)
}
encryptedKey, err = encryptDecryptText(cipherKey, ramCipher, make([]byte, nonceSize), true)
if err != nil {
log.Fatal(err)
}
}
func getMasterCipher() []byte {
key, err := encryptDecryptText(encryptedKey, ramCipher, make([]byte, nonceSize), false)
if err != nil {
key = []byte{}
log.Fatal(err)
}
return key
}
// Encrypt encrypts a file
func Encrypt(encInfo *models.EncryptionInfo, input io.Reader, output io.Writer) error {
key, err := generateNewFileKey(encInfo)
if err != nil {
return err
}
stream := getStream(key)
nonce := make([]byte, stream.NonceSize()) // Nonce is not used
reader := stream.EncryptReader(input, nonce, nil)
_, err = io.Copy(output, reader)
return err
}
// DecryptReader modifies a reader so it can decrypt encrypted files
func DecryptReader(encInfo models.EncryptionInfo, input io.Reader, output io.Writer) error {
key, err := GetCipherFromFile(encInfo)
if err != nil {
return err
}
stream := getStream(key)
nonce := make([]byte, stream.NonceSize()) // Nonce is not used
reader := stream.DecryptReader(input, nonce, nil)
_, err = io.Copy(output, reader)
return err
}
// GetDecryptWriter returns a writer that can decrypt encrypted files
func GetDecryptWriter(cipherKey []byte, input io.Writer) (io.Writer, error) {
stream := getStream(cipherKey)
nonce := make([]byte, stream.NonceSize()) // Nonce is not used
return stream.DecryptWriter(input, nonce, nil), nil
}
// GetDecryptReader returns a reader that can decrypt encrypted files
func GetDecryptReader(cipherKey []byte, input io.Reader) (io.Reader, error) {
stream := getStream(cipherKey)
nonce := make([]byte, stream.NonceSize()) // Nonce is not used
return stream.DecryptReader(input, nonce, nil), nil
}
// GetEncryptReader returns a reader that can encrypt plain files
func GetEncryptReader(cipherKey []byte, input io.Reader) (io.Reader, error) {
stream := getStream(cipherKey)
nonce := make([]byte, stream.NonceSize()) // Nonce is not used
return stream.EncryptReader(input, nonce, nil), nil
}
func generateNewFileKey(encInfo *models.EncryptionInfo) ([]byte, error) {
encryptionKey, err := getRandomData(blockSize)
if err != nil {
return []byte{}, err
}
nonce, err := getRandomData(nonceSize)
if err != nil {
return []byte{}, err
}
encInfo.Nonce = nonce
encInfo.IsEncrypted = true
encKey, err := fileCipherEncrypt(encryptionKey, nonce)
if err != nil {
return []byte{}, err
}
encInfo.DecryptionKey = encKey
return encryptionKey, nil
}
// GetCipherFromFile loads the cipher from a file model
func GetCipherFromFile(encInfo models.EncryptionInfo) ([]byte, error) {
cipherFile, err := fileCipherDecrypt(encInfo.DecryptionKey, encInfo.Nonce)
if err != nil {
return []byte{}, err
}
return cipherFile, nil
}
func getStream(cipherKey []byte) *sio.Stream {
block, err := aes.NewCipher(cipherKey)
if err != nil {
log.Fatal(err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
log.Fatal(err)
}
stream := sio.NewStream(gcm, sio.BufSize)
if err != nil {
log.Fatal(err)
}
return stream
}
func fileCipherEncrypt(input, nonce []byte) ([]byte, error) {
return encryptDecryptText(input, getMasterCipher(), nonce, true)
}
func fileCipherDecrypt(input, nonce []byte) ([]byte, error) {
return encryptDecryptText(input, getMasterCipher(), nonce, false)
}
func encryptDecryptText(input, cipherBlock, nonce []byte, doEncrypt bool) ([]byte, error) {
block, err := aes.NewCipher(cipherBlock)
if err != nil {
return []byte{}, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return []byte{}, err
}
if doEncrypt {
return aesgcm.Seal(nil, nonce, input, nil), nil
}
return aesgcm.Open(nil, nonce, input, nil)
}
func getRandomData(size int) ([]byte, error) {
data := make([]byte, size)
read, err := rand.Read(data)
if err != nil {
return []byte{}, err
}
if read != size {
return []byte{}, errors.New("incorrect size written")
}
return data, nil
}
// GetRandomCipher a 32 byte long array with random data
func GetRandomCipher() ([]byte, error) {
return getRandomData(blockSize)
}
// GetRandomNonce a 12 byte long array with random data
func GetRandomNonce() ([]byte, error) {
return getRandomData(nonceSize)
}