2017-09-12 20:01:12 -07:00
/ *
Package aes defines a Cipher that uses 256 - bit AES - GCM authenticated encryption to encrypt values the SOPS tree .
* /
2023-07-11 21:09:23 +02:00
package aes //import "github.com/getsops/sops/v3/aes"
2016-08-02 14:02:01 -07:00
import (
2016-08-22 12:38:30 -07:00
cryptoaes "crypto/aes"
2016-08-02 14:02:01 -07:00
"crypto/cipher"
2016-08-11 18:14:27 -07:00
"crypto/rand"
2016-08-02 14:02:01 -07:00
"encoding/base64"
2016-08-11 15:47:42 -07:00
"fmt"
2016-08-02 14:02:01 -07:00
"regexp"
2016-08-11 16:12:40 -07:00
"strconv"
2025-02-15 23:11:13 +01:00
"time"
2017-09-06 17:36:39 -07:00
2023-07-11 21:09:23 +02:00
"github.com/getsops/sops/v3"
"github.com/getsops/sops/v3/logging"
2023-08-17 01:21:25 +02:00
"github.com/sirupsen/logrus"
2016-08-02 14:02:01 -07:00
)
2017-09-06 17:36:39 -07:00
var log * logrus . Logger
func init ( ) {
2017-09-07 10:49:27 -07:00
log = logging . NewLogger ( "AES" )
2017-09-06 17:36:39 -07:00
}
2016-08-24 10:29:28 -07:00
type encryptedValue struct {
2016-08-02 14:02:01 -07:00
data [ ] byte
iv [ ] byte
tag [ ] byte
datatype string
}
2017-08-08 18:18:26 -07:00
const nonceSize int = 32
2016-08-24 12:50:35 -07:00
2017-09-12 20:41:02 -07:00
type stashKey struct {
additionalData string
plaintext interface { }
2016-08-29 14:00:24 -07:00
}
2016-08-24 15:46:49 -07:00
// Cipher encrypts and decrypts data keys with AES GCM 256
2017-09-12 20:41:02 -07:00
type Cipher struct {
// stash is a map that stores IVs for reuse, so that the ciphertext doesn't change when decrypting and reencrypting
// the same values.
stash map [ stashKey ] [ ] byte
}
2019-07-08 15:32:33 -07:00
// NewCipher is the constructor for a new Cipher object
2017-09-12 20:41:02 -07:00
func NewCipher ( ) Cipher {
return Cipher {
stash : make ( map [ stashKey ] [ ] byte ) ,
}
}
2016-08-24 14:38:05 -07:00
2016-08-22 18:15:00 -07:00
var encre = regexp . MustCompile ( ` ^ENC\[AES256_GCM,data:(.+),iv:(.+),tag:(.+),type:(.+)\] ` )
2016-08-24 10:29:28 -07:00
func parse ( value string ) ( * encryptedValue , error ) {
2016-08-23 13:19:33 -07:00
matches := encre . FindStringSubmatch ( value )
2016-08-02 14:02:01 -07:00
if matches == nil {
2016-08-22 12:38:30 -07:00
return nil , fmt . Errorf ( "Input string %s does not match sops' data format" , value )
2016-08-02 14:02:01 -07:00
}
2016-08-23 13:19:33 -07:00
data , err := base64 . StdEncoding . DecodeString ( matches [ 1 ] )
2016-08-02 14:02:01 -07:00
if err != nil {
2016-08-22 18:16:51 -07:00
return nil , fmt . Errorf ( "Error base64-decoding data: %s" , err )
2016-08-02 14:02:01 -07:00
}
2016-08-23 13:19:33 -07:00
iv , err := base64 . StdEncoding . DecodeString ( matches [ 2 ] )
2016-08-02 14:02:01 -07:00
if err != nil {
2016-08-22 18:16:51 -07:00
return nil , fmt . Errorf ( "Error base64-decoding iv: %s" , err )
2016-08-02 14:02:01 -07:00
}
2016-08-23 13:19:33 -07:00
tag , err := base64 . StdEncoding . DecodeString ( matches [ 3 ] )
2016-08-02 14:02:01 -07:00
if err != nil {
2016-08-22 18:16:51 -07:00
return nil , fmt . Errorf ( "Error base64-decoding tag: %s" , err )
2016-08-02 14:02:01 -07:00
}
2016-08-23 12:49:18 -07:00
datatype := string ( matches [ 4 ] )
2016-08-02 14:02:01 -07:00
2016-08-24 10:29:28 -07:00
return & encryptedValue { data , iv , tag , datatype } , nil
2016-08-02 14:02:01 -07:00
}
2016-08-29 14:00:24 -07:00
// Decrypt takes a sops-format value string and a key and returns the decrypted value and a stash value
2017-09-12 20:41:02 -07:00
func ( c Cipher ) Decrypt ( ciphertext string , key [ ] byte , additionalData string ) ( plaintext interface { } , err error ) {
2018-05-08 14:09:11 -04:00
if isEmpty ( ciphertext ) {
2017-09-12 20:41:02 -07:00
return "" , nil
2016-10-28 15:30:55 +02:00
}
2017-09-12 20:41:02 -07:00
encryptedValue , err := parse ( ciphertext )
2016-08-02 14:02:01 -07:00
if err != nil {
2017-09-12 20:41:02 -07:00
return nil , err
2016-08-02 14:02:01 -07:00
}
2016-08-23 13:19:33 -07:00
aescipher , err := cryptoaes . NewCipher ( key )
2016-08-02 14:02:01 -07:00
if err != nil {
2017-09-12 20:41:02 -07:00
return nil , err
2016-08-02 14:02:01 -07:00
}
2016-08-23 13:19:33 -07:00
gcm , err := cipher . NewGCMWithNonceSize ( aescipher , len ( encryptedValue . iv ) )
2016-08-02 14:02:01 -07:00
if err != nil {
2017-09-12 20:41:02 -07:00
return nil , err
2016-08-02 14:02:01 -07:00
}
data := append ( encryptedValue . data , encryptedValue . tag ... )
2017-09-11 16:59:36 -07:00
decryptedBytes , err := gcm . Open ( nil , encryptedValue . iv , data , [ ] byte ( additionalData ) )
2016-08-02 14:02:01 -07:00
if err != nil {
2017-09-12 20:41:02 -07:00
return nil , fmt . Errorf ( "Could not decrypt with AES_GCM: %s" , err )
2016-08-02 14:02:01 -07:00
}
2016-08-22 18:16:51 -07:00
decryptedValue := string ( decryptedBytes )
2016-08-11 16:12:40 -07:00
switch encryptedValue . datatype {
case "str" :
2017-09-12 20:41:02 -07:00
plaintext = decryptedValue
2016-08-11 16:12:40 -07:00
case "int" :
2016-08-29 14:00:24 -07:00
plaintext , err = strconv . Atoi ( decryptedValue )
2016-08-11 16:12:40 -07:00
case "float" :
2016-08-29 14:00:24 -07:00
plaintext , err = strconv . ParseFloat ( decryptedValue , 64 )
2016-08-11 16:12:40 -07:00
case "bytes" :
2017-09-12 20:41:02 -07:00
plaintext = decryptedBytes
2016-08-11 16:12:40 -07:00
case "bool" :
2016-08-29 14:00:24 -07:00
plaintext , err = strconv . ParseBool ( decryptedValue )
2025-02-15 23:11:13 +01:00
case "time" :
var value time . Time
err = value . UnmarshalText ( decryptedBytes )
plaintext = value
2017-08-15 13:48:55 -07:00
case "comment" :
plaintext = sops . Comment { Value : decryptedValue }
2016-08-11 16:12:40 -07:00
default :
2017-09-12 20:41:02 -07:00
return nil , fmt . Errorf ( "Unknown datatype: %s" , encryptedValue . datatype )
2016-08-11 16:12:40 -07:00
}
2017-09-12 20:41:02 -07:00
c . stash [ stashKey { plaintext : plaintext , additionalData : additionalData } ] = encryptedValue . iv
return plaintext , err
2016-08-02 14:02:01 -07:00
}
2016-08-11 18:14:27 -07:00
2018-05-08 14:09:11 -04:00
func isEmpty ( value interface { } ) bool {
switch value := value . ( type ) {
case string :
return value == ""
case [ ] byte :
return len ( value ) == 0
case sops . Comment :
return isEmpty ( value . Value )
default :
return false
}
}
2016-08-24 10:29:28 -07:00
// Encrypt takes one of (string, int, float, bool) and encrypts it with the provided key and additional auth data, returning a sops-format encrypted string.
2017-09-12 20:41:02 -07:00
func ( c Cipher ) Encrypt ( plaintext interface { } , key [ ] byte , additionalData string ) ( ciphertext string , err error ) {
2018-05-08 14:09:11 -04:00
if isEmpty ( plaintext ) {
2016-10-28 15:30:55 +02:00
return "" , nil
}
2016-08-23 13:19:33 -07:00
aescipher , err := cryptoaes . NewCipher ( key )
2016-08-11 18:14:27 -07:00
if err != nil {
2016-08-22 18:16:51 -07:00
return "" , fmt . Errorf ( "Could not initialize AES GCM encryption cipher: %s" , err )
2016-08-11 18:14:27 -07:00
}
2016-08-29 14:00:24 -07:00
var iv [ ] byte
2017-09-12 20:41:02 -07:00
if stash , ok := c . stash [ stashKey { plaintext : plaintext , additionalData : additionalData } ] ; ! ok {
2016-08-29 14:00:24 -07:00
iv = make ( [ ] byte , nonceSize )
_ , err = rand . Read ( iv )
if err != nil {
return "" , fmt . Errorf ( "Could not generate random bytes for IV: %s" , err )
}
} else {
2017-09-12 20:41:02 -07:00
iv = stash
2016-08-11 18:14:27 -07:00
}
2016-08-24 12:50:35 -07:00
gcm , err := cipher . NewGCMWithNonceSize ( aescipher , nonceSize )
2016-08-11 18:14:27 -07:00
if err != nil {
return "" , fmt . Errorf ( "Could not create GCM: %s" , err )
}
2017-09-12 20:41:02 -07:00
var plainBytes [ ] byte
2016-08-24 12:52:10 -07:00
var encryptedType string
2017-09-12 20:41:02 -07:00
switch value := plaintext . ( type ) {
2016-08-11 18:14:27 -07:00
case string :
2016-08-24 12:52:10 -07:00
encryptedType = "str"
2017-09-12 20:41:02 -07:00
plainBytes = [ ] byte ( value )
2016-08-11 18:14:27 -07:00
case int :
2016-08-24 12:52:10 -07:00
encryptedType = "int"
2017-09-12 20:41:02 -07:00
plainBytes = [ ] byte ( strconv . Itoa ( value ) )
2016-08-11 18:14:27 -07:00
case float64 :
2016-08-24 12:52:10 -07:00
encryptedType = "float"
2016-09-07 09:41:29 -07:00
// The Python version encodes floats without padding 0s after the decimal point.
2017-09-12 20:41:02 -07:00
plainBytes = [ ] byte ( strconv . FormatFloat ( value , 'f' , - 1 , 64 ) )
2016-08-11 18:14:27 -07:00
case bool :
2016-08-24 12:52:10 -07:00
encryptedType = "bool"
2016-09-07 09:41:29 -07:00
// The Python version encodes booleans with Titlecase
2023-08-17 01:21:25 +02:00
if value {
plainBytes = [ ] byte ( "True" )
} else {
plainBytes = [ ] byte ( "False" )
}
2025-02-15 23:11:13 +01:00
case time . Time :
encryptedType = "time"
plainBytes , err = value . MarshalText ( )
if err != nil {
return "" , fmt . Errorf ( "Error marshaling timestamp %q: %w" , value , err )
}
2017-08-15 13:48:55 -07:00
case sops . Comment :
encryptedType = "comment"
2017-09-15 10:28:26 -07:00
plainBytes = [ ] byte ( value . Value )
2016-08-11 18:14:27 -07:00
default :
return "" , fmt . Errorf ( "Value to encrypt has unsupported type %T" , value )
}
2017-09-12 20:41:02 -07:00
out := gcm . Seal ( nil , iv , plainBytes , [ ] byte ( additionalData ) )
2016-08-22 18:16:51 -07:00
return fmt . Sprintf ( "ENC[AES256_GCM,data:%s,iv:%s,tag:%s,type:%s]" ,
base64 . StdEncoding . EncodeToString ( out [ : len ( out ) - cryptoaes . BlockSize ] ) ,
base64 . StdEncoding . EncodeToString ( iv ) ,
base64 . StdEncoding . EncodeToString ( out [ len ( out ) - cryptoaes . BlockSize : ] ) ,
2016-08-24 12:52:10 -07:00
encryptedType ) , nil
2016-08-11 18:14:27 -07:00
}