feat: new helpers and masking/encrypting attributes
New helper functions were added to make call-site less likely to need to pull `go.opentelemetry.io/otel/attribute` as a dependency. Additionally `Encrypted` and `Masked` were added to add a possibility of logging sensitive data in a more secure manner.
This commit is contained in:
192
pkg/attr/attr.go
192
pkg/attr/attr.go
@ -1,18 +1,27 @@
|
||||
package attr
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"git.ma-al.com/maal-libraries/observer/pkg/level"
|
||||
"github.com/gofrs/uuid"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/semconv/v1.25.0"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.25.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
type KV = attribute.KeyValue
|
||||
type KeyValue = attribute.KeyValue
|
||||
type Key = attribute.Key
|
||||
type Value = attribute.Value
|
||||
@ -72,11 +81,192 @@ const (
|
||||
LayerUtil = "util"
|
||||
)
|
||||
|
||||
type secretKey struct {
|
||||
Key []byte
|
||||
Cipher cipher.Block
|
||||
Nonce []byte
|
||||
}
|
||||
|
||||
// Interprets a string as AES secret key (32 bytes) first decoding it with base64
|
||||
// It can be used to set the variable `EncryptSecretKey` which is responsible
|
||||
// for encrypting the `Encrypted` attributes.
|
||||
func NewSecretKey(key string) (secretKey, error) {
|
||||
keyBytes, err := base64.RawStdEncoding.DecodeString(key)
|
||||
if err != nil {
|
||||
return secretKey{}, err
|
||||
}
|
||||
if len(keyBytes) != 32 {
|
||||
return secretKey{}, errors.New("wrong length of encryption key, should be 32 bits")
|
||||
}
|
||||
cipher, err := aes.NewCipher(keyBytes)
|
||||
if err != nil {
|
||||
return secretKey{}, err
|
||||
}
|
||||
nonce := make([]byte, 12)
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return secretKey{}, err
|
||||
}
|
||||
return secretKey{
|
||||
Key: keyBytes,
|
||||
Cipher: cipher,
|
||||
Nonce: nonce,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// **Unless set, it will default to a random key that cannot be later retrievied!**
|
||||
//
|
||||
// The variable is used to encrypt values provided to the `Encrypted` attribute.
|
||||
var EncryptSecretKey secretKey = func() secretKey {
|
||||
key := make([]byte, 32)
|
||||
rand.Read(key)
|
||||
cipher, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
nonce := make([]byte, 12)
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return secretKey{
|
||||
Key: key,
|
||||
Cipher: cipher,
|
||||
Nonce: nonce,
|
||||
}
|
||||
}()
|
||||
|
||||
// Build an attribute with a value formatted as json
|
||||
func JsonAttr(key string, jsonEl map[string]interface{}) attribute.KeyValue {
|
||||
jsonStr, _ := json.Marshal(jsonEl)
|
||||
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(string(jsonStr))}
|
||||
}
|
||||
|
||||
// Create an arbitrary attribute with value marshalled to json.
|
||||
// In case of marshalling error, it is returned in place of value.
|
||||
func Json[M json.Marshaler](key string, val M) attribute.KeyValue {
|
||||
data, err := json.Marshal(val)
|
||||
if err != nil {
|
||||
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(err.Error())}
|
||||
} else {
|
||||
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(string(data))}
|
||||
}
|
||||
}
|
||||
|
||||
// Create an arbitrary attribute with a `string` value.
|
||||
func String(key string, val string) attribute.KeyValue {
|
||||
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(val)}
|
||||
}
|
||||
|
||||
// Create an arbitrary attribute with a `[]string` value.
|
||||
func StringSlice(key string, val []string) attribute.KeyValue {
|
||||
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringSliceValue(val)}
|
||||
}
|
||||
|
||||
// Create an arbitrary attribute with an `int` value.
|
||||
func Int(key string, val int) attribute.KeyValue {
|
||||
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.IntValue(val)}
|
||||
}
|
||||
|
||||
// Create an arbitrary attribute with an `int64` value.
|
||||
func Int64(key string, val int64) attribute.KeyValue {
|
||||
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.Int64Value(val)}
|
||||
}
|
||||
|
||||
// Cast value to an `int` to create a new attribute.
|
||||
func Uint(key string, val uint) attribute.KeyValue {
|
||||
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.IntValue(int(val))}
|
||||
}
|
||||
|
||||
// Cast value to an `int` to create a new attribute.
|
||||
func Uint8(key string, val uint8) attribute.KeyValue {
|
||||
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.IntValue(int(val))}
|
||||
}
|
||||
|
||||
// Create an arbitrary attribute using an `uuid.UUID` from `github.com/gofrs/uuid` as value.
|
||||
func Uuid(key string, val uuid.UUID) attribute.KeyValue {
|
||||
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(val.String())}
|
||||
}
|
||||
|
||||
// Create an arbitrary attribute using standard library's `time.Time` as value. It will be formatted using RFC3339.
|
||||
func Time(key string, val time.Time) attribute.KeyValue {
|
||||
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(val.Format(time.RFC3339))}
|
||||
}
|
||||
|
||||
// Create an arbitrary attribute with bytes encoded to base64 (RFC 4648) as value.
|
||||
func BytesB64(key string, val []byte) attribute.KeyValue {
|
||||
res := base64.StdEncoding.EncodeToString(val)
|
||||
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(res)}
|
||||
}
|
||||
|
||||
// Create an arbitrary attribute with bytes encoded to hexadecimal format as value.
|
||||
func BytesHex(key string, val []byte) attribute.KeyValue {
|
||||
res := hex.EncodeToString(val)
|
||||
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(res)}
|
||||
}
|
||||
|
||||
// Create an arbitrary attribute with value encrypted using `secretKey` which should be set on
|
||||
// global variable `EncryptSecretKey` using `NewSecretKey`. The result will be encoded using
|
||||
// base64.
|
||||
//
|
||||
// This approach is an alternative to logs tokenization. It is using AES symmetric encryption
|
||||
// that is suspectible to brute force attacks. It is a computionally expensive attribute to
|
||||
// generate.
|
||||
//
|
||||
// In most cases, for very sensitive data it would be a better approach to use masking instead.
|
||||
// Encrypting the fields of the logs/traces can provide an extra protection while they are being
|
||||
// transported to a log collector and when the collector does not encrypt logs at rest (but most
|
||||
// should implement this feature). This will mostly protect the logs from developers working
|
||||
// with them provided that they do not have access to the key. The key should be set from an
|
||||
// environment variable defined on application deployment. Alternatively it could be set from
|
||||
// a secure vault, a software for storing private keys.
|
||||
func Encrypted(key string, val string) attribute.KeyValue {
|
||||
aesGcm, err := cipher.NewGCM(EncryptSecretKey.Cipher)
|
||||
if err != nil {
|
||||
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(err.Error())}
|
||||
}
|
||||
resBytes := aesGcm.Seal(nil, EncryptSecretKey.Nonce, []byte(val), nil)
|
||||
res := base64.StdEncoding.EncodeToString(resBytes)
|
||||
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(res)}
|
||||
}
|
||||
|
||||
// Creates an arbitrary attribute with masked value. It will leave only last 4 (or less) characters
|
||||
// unmasked by 'X' characters.
|
||||
//
|
||||
// It is not a good idea to use it for logging passwords as it preserves the lenght of the input.
|
||||
//
|
||||
// Masking is a good idea for very sensitive data like official identity numbers, or addresses.
|
||||
// Storing such data in logs is usually too much of a risk even when it is encrypted.
|
||||
// However, for the purpose of debugging it might be convenient to be able to distinguish one record
|
||||
// from another.
|
||||
func Masked(key string, val string) attribute.KeyValue {
|
||||
lenght := len(val)
|
||||
var unmasked int
|
||||
|
||||
if lenght <= 4 {
|
||||
unmasked = 1
|
||||
} else {
|
||||
if lenght <= 8 {
|
||||
unmasked = 2
|
||||
} else {
|
||||
if lenght <= 12 {
|
||||
unmasked = 3
|
||||
} else {
|
||||
unmasked = 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
masked := lenght - unmasked
|
||||
resBytes := make([]byte, lenght)
|
||||
i := 0
|
||||
|
||||
for ; i < masked; i++ {
|
||||
resBytes[i] = byte('X')
|
||||
}
|
||||
for ; i < lenght; i++ {
|
||||
resBytes[i] = byte(val[i])
|
||||
}
|
||||
|
||||
return attribute.KeyValue{Key: attribute.Key(key), Value: attribute.StringValue(string(resBytes))}
|
||||
}
|
||||
|
||||
// An attribute informing about the severity or importance of an event using our own standard of log levels that
|
||||
|
Reference in New Issue
Block a user