blob: ac8e2090b58721df8ac21db13ce704549689233e [file]
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package plugin
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/core/utils"
)
const EncodeKeyEnvStr = "ENCRYPTION_SECRET"
// gcmNonceSize is the standard nonce size for AES-GCM.
const gcmNonceSize = 12
// Encrypt AES-GCM encrypts plaintext using ENCRYPTION_SECRET, then base64-encodes the result.
// The output format is: base64(nonce || ciphertext || tag).
func Encrypt(encryptionSecret, plainText string) (string, errors.Error) {
// add suffix to the data part
inputBytes := append([]byte(plainText), 123, 110, 100, 100, 116, 102, 125)
// perform encryption
output, err := AesEncrypt(inputBytes, []byte(encryptionSecret))
if err != nil {
return plainText, err
}
// Return the result after Base64 processing
return base64.StdEncoding.EncodeToString(output), nil
}
// Decrypt base64-decodes then AES-GCM decrypts ciphertext using ENCRYPTION_SECRET.
// For backward compatibility, it also attempts AES-CBC decryption if the data looks like legacy format.
func Decrypt(encryptionSecret, encryptedText string) (string, errors.Error) {
// when encryption key is not set
if encryptionSecret == "" {
// return error message
return encryptedText, errors.Default.New("encryptionSecret is required")
}
// Decode Base64
decodingFromBase64, err1 := base64.StdEncoding.DecodeString(encryptedText)
if err1 != nil {
return encryptedText, errors.Convert(err1)
}
// perform AES decryption
output, err2 := AesDecrypt(decodingFromBase64, []byte(encryptionSecret))
if err2 != nil {
return encryptedText, err2
}
// Verify and remove suffix
oSize := len(output)
if oSize >= 7 {
check := output[oSize-7 : oSize]
backEnd := []byte{123, 110, 100, 100, 116, 102, 125}
if string(check) == string(backEnd) {
output = output[0 : oSize-7]
// return result
return string(output), nil
}
}
return "", errors.Default.New("invalid encryptionSecret")
}
// PKCS7Padding PKCS7 padding
func PKCS7Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
// PKCS7UnPadding PKCS7 unPadding
func PKCS7UnPadding(origData []byte) []byte {
length := len(origData)
if length == 0 {
return nil
}
unpadding := int(origData[length-1])
if unpadding >= length {
return nil
}
return origData[:(length - unpadding)]
}
// AesEncrypt AES-256-GCM encrypts origData using key.
// The returned bytes are: nonce (12 bytes) || ciphertext || tag.
func AesEncrypt(origData, key []byte) ([]byte, errors.Error) {
sha256Key := sha256.Sum256(key)
block, err := aes.NewCipher(sha256Key[:])
if err != nil {
return nil, errors.Convert(err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, errors.Convert(err)
}
nonce := make([]byte, gcmNonceSize)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, errors.Convert(err)
}
ciphertext := gcm.Seal(nonce, nonce, origData, nil)
return ciphertext, nil
}
// AesDecrypt decrypts crypted data using key.
// It first tries AES-256-GCM (expects a 12-byte nonce prefix).
// If that fails and the data length is a multiple of the AES block size (legacy CBC format),
// it falls back to AES-256-CBC for backward compatibility.
func AesDecrypt(crypted, key []byte) ([]byte, errors.Error) {
sha256Key := sha256.Sum256(key)
block, err := aes.NewCipher(sha256Key[:])
if err != nil {
return nil, errors.Convert(err)
}
blockSize := block.BlockSize()
// Try GCM first if the data is long enough to contain a nonce.
if len(crypted) >= gcmNonceSize+blockSize {
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, errors.Convert(err)
}
nonce := crypted[:gcmNonceSize]
ciphertext := crypted[gcmNonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err == nil {
return plaintext, nil
}
// GCM decryption failed; fall through to try legacy CBC.
}
// Legacy CBC fallback.
if len(crypted)%blockSize != 0 {
return nil, errors.Default.New(fmt.Sprintf("The length of the data to be decrypted is [%d], so cannot match the required block size [%d]", len(crypted), blockSize))
}
blockMode := cipher.NewCBCDecrypter(block, sha256Key[:blockSize])
origData := make([]byte, len(crypted))
blockMode.CryptBlocks(origData, crypted)
origData = PKCS7UnPadding(origData)
return origData, nil
}
// RandomEncryptionSecret will return a random string of length 128
func RandomEncryptionSecret() (string, errors.Error) {
return utils.RandLetterBytes(128)
}