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