// 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 parquet

import (
	"crypto/rand"
	"unicode/utf8"

	format "github.com/apache/arrow/go/v12/parquet/internal/gen-go/parquet"
)

// Constants that will be used as the default values with encryption/decryption
const (
	// By default we'll use AesGCM as our encryption algorithm
	DefaultEncryptionAlgorithm       = AesGcm
	MaximalAadMetadataLength   int32 = 256
	// if encryption is turned on, we will default to also encrypting the footer
	DefaultEncryptedFooter = true
	DefaultCheckSignature  = true
	// by default if you set the file decryption properties, we will error
	// on any plaintext files unless otherwise specified.
	DefaultAllowPlaintextFiles       = false
	AadFileUniqueLength        int32 = 8
)

// ColumnPathToDecryptionPropsMap maps column paths to decryption properties
type ColumnPathToDecryptionPropsMap map[string]*ColumnDecryptionProperties

// ColumnPathToEncryptionPropsMap maps column paths to encryption properties
type ColumnPathToEncryptionPropsMap map[string]*ColumnEncryptionProperties

// ColumnEncryptionProperties specifies how to encrypt a given column
type ColumnEncryptionProperties struct {
	columnPath             string
	encrypted              bool
	encryptedWithFooterKey bool
	key                    string
	keyMetadata            string
	utilized               bool
}

// ColumnPath returns which column these properties are for
func (ce *ColumnEncryptionProperties) ColumnPath() string {
	return ce.columnPath
}

// IsEncrypted returns true if this column is encrypted.
func (ce *ColumnEncryptionProperties) IsEncrypted() bool { return ce.encrypted }

// IsEncryptedWithFooterKey returns if this column was encrypted with the footer key itself, or false if a separate
// key was used for encrypting this column.
func (ce *ColumnEncryptionProperties) IsEncryptedWithFooterKey() bool {
	return ce.encryptedWithFooterKey
}

// Key returns the key used for encrypting this column if it isn't encrypted by the footer key
func (ce *ColumnEncryptionProperties) Key() string { return ce.key }

// KeyMetadata returns the key identifier which is used with a KeyRetriever to get the key for this column if it is not
// encrypted using the footer key
func (ce *ColumnEncryptionProperties) KeyMetadata() string { return ce.keyMetadata }

// WipeOutEncryptionKey Clears the encryption key, used after completion of file writing
func (ce *ColumnEncryptionProperties) WipeOutEncryptionKey() { ce.key = "" }

// IsUtilized returns whether or not these properties have already been used, if the key is empty
// then this is always false
func (ce *ColumnEncryptionProperties) IsUtilized() bool {
	if ce.key == "" {
		return false
	}
	return ce.utilized
}

// SetUtilized is used for marking it as utilized once it is used in FileEncryptionProperties
// as the encryption key will be wiped out on completion of writing
func (ce *ColumnEncryptionProperties) SetUtilized() {
	ce.utilized = true
}

// Clone returns a instance of ColumnEncryptionProperties with the same key and metadata
func (ce *ColumnEncryptionProperties) Clone() *ColumnEncryptionProperties {
	copy := ce.key
	return NewColumnEncryptionProperties(ce.columnPath, WithKey(copy), WithKeyMetadata(ce.keyMetadata))
}

type colEncryptConfig struct {
	key         string
	keyMetadata string
	encrypted   bool
}

// ColumnEncryptOption how to specify options to the the NewColumnEncryptionProperties function.
type ColumnEncryptOption func(*colEncryptConfig)

// WithKey sets a column specific key.
// If key is not set on an encrypted column, the column will be encrypted with the footer key.
// key length must be either 16, 24, or 32 bytes
// the key is cloned and will be wiped out (array values set to 0) upon completion of file writing.
// Caller is responsible for wiping out input key array
func WithKey(key string) ColumnEncryptOption {
	return func(c *colEncryptConfig) {
		if key != "" {
			c.key = key
		}
	}
}

// WithKeyMetadata sets the key retrieval metadata, use either KeyMetadata or KeyID but not both
func WithKeyMetadata(keyMeta string) ColumnEncryptOption {
	return func(c *colEncryptConfig) {
		c.keyMetadata = keyMeta
	}
}

// WithKeyID is a convenience function to set the key metadata using a string id.
// Set a key retrieval metadata (converted from String). and use either KeyMetadata or KeyID, not both.
// KeyID will be converted to metadata (UTF-8 Array)
func WithKeyID(keyID string) ColumnEncryptOption {
	if !utf8.ValidString(keyID) {
		panic("parquet: key id should be UTF8 encoded")
	}
	return WithKeyMetadata(keyID)
}

// NewColumnEncryptionProperties constructs properties for the provided column path, modified by the options provided
func NewColumnEncryptionProperties(name string, opts ...ColumnEncryptOption) *ColumnEncryptionProperties {
	var cfg colEncryptConfig
	cfg.encrypted = true
	for _, o := range opts {
		o(&cfg)
	}
	return &ColumnEncryptionProperties{
		utilized:               false,
		encrypted:              cfg.encrypted,
		encryptedWithFooterKey: cfg.encrypted && cfg.key == "",
		keyMetadata:            cfg.keyMetadata,
		key:                    cfg.key,
		columnPath:             name,
	}
}

// ColumnDecryptionProperties are the specifications for how to decrypt a given column.
type ColumnDecryptionProperties struct {
	columnPath string
	key        string
	utilized   bool
}

// NewColumnDecryptionProperties constructs a new ColumnDecryptionProperties for the given column path, modified by
// the provided options
func NewColumnDecryptionProperties(column string, opts ...ColumnDecryptOption) *ColumnDecryptionProperties {
	var cfg columnDecryptConfig
	for _, o := range opts {
		o(&cfg)
	}

	return &ColumnDecryptionProperties{
		columnPath: column,
		utilized:   false,
		key:        cfg.key,
	}
}

// ColumnPath returns which column these properties describe how to decrypt
func (cd *ColumnDecryptionProperties) ColumnPath() string { return cd.columnPath }

// Key returns the key specified to decrypt this column, or is empty if the Footer Key should be used.
func (cd *ColumnDecryptionProperties) Key() string { return cd.key }

// IsUtilized returns whether or not these properties have been used for decryption already
func (cd *ColumnDecryptionProperties) IsUtilized() bool { return cd.utilized }

// SetUtilized is used by the reader to specify when we've decrypted the column and have used the key so we know
// to wipe out the keys.
func (cd *ColumnDecryptionProperties) SetUtilized() { cd.utilized = true }

// WipeOutDecryptionKey is called after decryption to ensure the key doesn't stick around and get re-used.
func (cd *ColumnDecryptionProperties) WipeOutDecryptionKey() { cd.key = "" }

// Clone returns a new instance of ColumnDecryptionProperties with the same key and column
func (cd *ColumnDecryptionProperties) Clone() *ColumnDecryptionProperties {
	return NewColumnDecryptionProperties(cd.columnPath, WithDecryptKey(cd.key))
}

type columnDecryptConfig struct {
	key string
}

// ColumnDecryptOption is the type of the options passed for constructing Decryption Properties
type ColumnDecryptOption func(*columnDecryptConfig)

// WithDecryptKey specifies the key to utilize for decryption
func WithDecryptKey(key string) ColumnDecryptOption {
	return func(cfg *columnDecryptConfig) {
		if key != "" {
			cfg.key = key
		}
	}
}

// AADPrefixVerifier is an interface for any object that can be used to verify the identity of the file being decrypted.
// It should panic if the provided AAD identity is bad.
//
// In a data set, AAD Prefixes should be collected, and then checked for missing files.
type AADPrefixVerifier interface {
	// Verify identity of file. panic if bad
	Verify(string)
}

// DecryptionKeyRetriever is an interface for getting the desired key for decryption from metadata. It should take in
// some metadata identifier and return the actual Key to use for decryption.
type DecryptionKeyRetriever interface {
	GetKey(keyMetadata []byte) string
}

// FileDecryptionProperties define the File Level configuration for decrypting a parquet file. Once constructed they are
// read only.
type FileDecryptionProperties struct {
	footerKey                     string
	aadPrefix                     string
	checkPlaintextFooterIntegrity bool
	plaintextAllowed              bool
	utilized                      bool
	columnDecryptProps            ColumnPathToDecryptionPropsMap
	Verifier                      AADPrefixVerifier
	KeyRetriever                  DecryptionKeyRetriever
}

// NewFileDecryptionProperties takes in the options for constructing a new FileDecryptionProperties object, otherwise
// it will use the default configuration which will check footer integrity of a plaintext footer for an encrypted file
// for unencrypted parquet files, the decryption properties should not be set.
func NewFileDecryptionProperties(opts ...FileDecryptionOption) *FileDecryptionProperties {
	var cfg fileDecryptConfig
	cfg.checkFooterIntegrity = DefaultCheckSignature
	cfg.plaintextAllowed = DefaultAllowPlaintextFiles
	for _, o := range opts {
		o(&cfg)
	}
	return &FileDecryptionProperties{
		Verifier:                      cfg.verifier,
		footerKey:                     cfg.footerKey,
		checkPlaintextFooterIntegrity: cfg.checkFooterIntegrity,
		KeyRetriever:                  cfg.retriever,
		aadPrefix:                     cfg.aadPrefix,
		columnDecryptProps:            cfg.colDecrypt,
		plaintextAllowed:              cfg.plaintextAllowed,
		utilized:                      false,
	}
}

// ColumnKey returns the key to be used for decrypting the provided column.
func (fd *FileDecryptionProperties) ColumnKey(path string) string {
	if d, ok := fd.columnDecryptProps[path]; ok {
		if d != nil {
			return d.Key()
		}
	}
	return ""
}

// FooterKey returns the key utilized for decrypting the Footer if encrypted and any columns that are encrypted with
// the footer key.
func (fd *FileDecryptionProperties) FooterKey() string { return fd.footerKey }

// AadPrefix returns the prefix to be supplied for constructing the identification strings when decrypting
func (fd *FileDecryptionProperties) AadPrefix() string { return fd.aadPrefix }

// PlaintextFooterIntegrity returns whether or not an integrity check will be performed on a plaintext footer for an
// encrypted file.
func (fd *FileDecryptionProperties) PlaintextFooterIntegrity() bool {
	return fd.checkPlaintextFooterIntegrity
}

// PlaintextFilesAllowed returns whether or not this instance of decryption properties are allowed on a plaintext file.
func (fd *FileDecryptionProperties) PlaintextFilesAllowed() bool { return fd.plaintextAllowed }

// SetUtilized is called to mark this instance as utilized once it is used to read a file. A single instance
// can be used for reading one file only. Setting this ensures the keys will be wiped out upon completion of file reading.
func (fd *FileDecryptionProperties) SetUtilized() { fd.utilized = true }

// IsUtilized returns whether or not this instance has been used to decrypt a file. If the footer key and prefix are
// empty and there are no column decryption properties, then this is always false.
func (fd *FileDecryptionProperties) IsUtilized() bool {
	if fd.footerKey == "" && len(fd.columnDecryptProps) == 0 && fd.aadPrefix == "" {
		return false
	}
	return fd.utilized
}

// WipeOutDecryptionKeys will clear all the keys for this instance including the column level ones, this will be called
// after this instance has been utilized.
func (fd *FileDecryptionProperties) WipeOutDecryptionKeys() {
	fd.footerKey = ""
	for _, cd := range fd.columnDecryptProps {
		cd.WipeOutDecryptionKey()
	}
}

// Clone returns a new instance of these properties, changing the prefix if set (keeping the same prefix if left empty)
func (fd *FileDecryptionProperties) Clone(newAadPrefix string) *FileDecryptionProperties {
	keyCopy := fd.footerKey
	colDecryptMapCopy := make(ColumnPathToDecryptionPropsMap)
	for k, v := range fd.columnDecryptProps {
		colDecryptMapCopy[k] = v.Clone()
	}
	if newAadPrefix == "" {
		newAadPrefix = fd.aadPrefix
	}
	return &FileDecryptionProperties{
		footerKey:                     keyCopy,
		KeyRetriever:                  fd.KeyRetriever,
		checkPlaintextFooterIntegrity: fd.checkPlaintextFooterIntegrity,
		Verifier:                      fd.Verifier,
		columnDecryptProps:            colDecryptMapCopy,
		aadPrefix:                     newAadPrefix,
		plaintextAllowed:              fd.plaintextAllowed,
		utilized:                      false,
	}
}

type fileDecryptConfig struct {
	footerKey            string
	aadPrefix            string
	verifier             AADPrefixVerifier
	colDecrypt           ColumnPathToDecryptionPropsMap
	retriever            DecryptionKeyRetriever
	checkFooterIntegrity bool
	plaintextAllowed     bool
}

// FileDecryptionOption is how to supply options to constructing a new FileDecryptionProperties instance.
type FileDecryptionOption func(*fileDecryptConfig)

// WithFooterKey sets an explicit footer key. If Applied on a file that contains footer key
// metadata the metadata will be ignored, the footer will be decrypted/verified with this key.
//
// If the explicit key is not set, footer key will be fetched from the key retriever.
// With explcit keys or AAD prefix, new encryption properties object must be created for each
// encrypted file.
//
// Explicit encryption keys (footer and column) are cloned.
// Upon completion of file reading, the cloned encryption keys in the properties will be wiped out
// Caller is responsible for wiping out the input key array
// footer key length must be either 16, 24, or 32 bytes
func WithFooterKey(key string) FileDecryptionOption {
	return func(cfg *fileDecryptConfig) {
		if key != "" {
			cfg.footerKey = key
		}
	}
}

// WithPrefixVerifier supplies a verifier object to use for verifying the AAD Prefixes stored in the file.
func WithPrefixVerifier(verifier AADPrefixVerifier) FileDecryptionOption {
	return func(cfg *fileDecryptConfig) {
		if verifier != nil {
			cfg.verifier = verifier
		}
	}
}

// WithColumnKeys sets explicit column keys.
//
// It's also possible to set a key retriever on this property object.
//
// Upon file decryption, availability of explicit keys is checked before invocation
// of the retreiver callback.
//
// If an explicit key is available for a footer or a column, its key metadata will be ignored.
func WithColumnKeys(decrypt ColumnPathToDecryptionPropsMap) FileDecryptionOption {
	return func(cfg *fileDecryptConfig) {
		if len(decrypt) == 0 {
			return
		}
		if len(cfg.colDecrypt) != 0 {
			panic("column properties already set")
		}
		for _, v := range decrypt {
			if v.IsUtilized() {
				panic("parquet: column properties utilized in another file")
			}
			v.SetUtilized()
		}
		cfg.colDecrypt = decrypt
	}
}

// WithKeyRetriever sets a key retriever callback. It's also possible to set explicit footer or column keys.
func WithKeyRetriever(retriever DecryptionKeyRetriever) FileDecryptionOption {
	return func(cfg *fileDecryptConfig) {
		if retriever != nil {
			cfg.retriever = retriever
		}
	}
}

// DisableFooterSignatureVerification skips integrity verification of plaintext footers.
//
// If not called, integrity of plaintext footers will be checked in runtime, and will panic
// if the footer signing key is not available
// or if the footer content and signature don't match
func DisableFooterSignatureVerification() FileDecryptionOption {
	return func(cfg *fileDecryptConfig) {
		cfg.checkFooterIntegrity = false
	}
}

// WithPlaintextAllowed sets allowing plaintext files.
//
// By default, reading plaintext (unencrypted) files is not allowed when using
// a decryptor.
//
// In order to detect files that were not encrypted by mistake.
// However the default behavior can be overridden by using this method.
func WithPlaintextAllowed() FileDecryptionOption {
	return func(cfg *fileDecryptConfig) {
		cfg.plaintextAllowed = true
	}
}

// WithDecryptAadPrefix explicitly supplies the file aad prefix.
//
// A must when a prefix is used for file encryption, but not stored in the file.
func WithDecryptAadPrefix(prefix string) FileDecryptionOption {
	return func(cfg *fileDecryptConfig) {
		if prefix != "" {
			cfg.aadPrefix = prefix
		}
	}
}

// Algorithm describes how something was encrypted, representing the EncryptionAlgorithm object from the
// parquet.thrift file.
type Algorithm struct {
	Algo Cipher
	Aad  struct {
		AadPrefix       []byte
		AadFileUnique   []byte
		SupplyAadPrefix bool
	}
}

// ToThrift returns an instance to be used for serializing when writing a file.
func (e Algorithm) ToThrift() *format.EncryptionAlgorithm {
	if e.Algo == AesGcm {
		return &format.EncryptionAlgorithm{
			AES_GCM_V1: &format.AesGcmV1{
				AadPrefix:       e.Aad.AadPrefix,
				AadFileUnique:   e.Aad.AadFileUnique,
				SupplyAadPrefix: &e.Aad.SupplyAadPrefix,
			},
		}
	}
	return &format.EncryptionAlgorithm{
		AES_GCM_CTR_V1: &format.AesGcmCtrV1{
			AadPrefix:       e.Aad.AadPrefix,
			AadFileUnique:   e.Aad.AadFileUnique,
			SupplyAadPrefix: &e.Aad.SupplyAadPrefix,
		},
	}
}

// AlgorithmFromThrift converts the thrift object to the Algorithm struct for easier usage.
func AlgorithmFromThrift(enc *format.EncryptionAlgorithm) (ret Algorithm) {
	if enc.IsSetAES_GCM_V1() {
		ret.Algo = AesGcm
		ret.Aad.AadFileUnique = enc.AES_GCM_V1.AadFileUnique
		ret.Aad.AadPrefix = enc.AES_GCM_V1.AadPrefix
		ret.Aad.SupplyAadPrefix = *enc.AES_GCM_V1.SupplyAadPrefix
		return
	}
	ret.Algo = AesCtr
	ret.Aad.AadFileUnique = enc.AES_GCM_CTR_V1.AadFileUnique
	ret.Aad.AadPrefix = enc.AES_GCM_CTR_V1.AadPrefix
	ret.Aad.SupplyAadPrefix = *enc.AES_GCM_CTR_V1.SupplyAadPrefix
	return
}

// FileEncryptionProperties describe how to encrypt a parquet file when writing data.
type FileEncryptionProperties struct {
	alg                  Algorithm
	footerKey            string
	footerKeyMetadata    string
	encryptedFooter      bool
	fileAad              string
	utilized             bool
	storeAadPrefixInFile bool
	aadPrefix            string
	encryptedCols        ColumnPathToEncryptionPropsMap
}

// EncryptedFooter returns if the footer for this file should be encrypted or left in plaintext.
func (fe *FileEncryptionProperties) EncryptedFooter() bool { return fe.encryptedFooter }

// Algorithm returns the description of how we will perform the encryption, the algorithm, prefixes, and so on.
func (fe *FileEncryptionProperties) Algorithm() Algorithm { return fe.alg }

// FooterKey returns the actual key used to encrypt the footer if it is encrypted, or to encrypt any columns which
// will be encrypted with it rather than their own keys.
func (fe *FileEncryptionProperties) FooterKey() string { return fe.footerKey }

// FooterKeyMetadata is used for retrieving a key from the key retriever in order to set the footer key
func (fe *FileEncryptionProperties) FooterKeyMetadata() string { return fe.footerKeyMetadata }

// FileAad returns the aad identification to be used at the file level which gets concatenated with the row and column
// information for encrypting data.
func (fe *FileEncryptionProperties) FileAad() string { return fe.fileAad }

// IsUtilized returns whether or not this instance has been used to encrypt a file
func (fe *FileEncryptionProperties) IsUtilized() bool { return fe.utilized }

// SetUtilized is called after writing a file. A FileEncryptionProperties object can be used for writing one file only,
// the encryption keys will be wiped out upon completion of writing the file.
func (fe *FileEncryptionProperties) SetUtilized() { fe.utilized = true }

// EncryptedColumns returns the mapping of column paths to column encryption properties
func (fe *FileEncryptionProperties) EncryptedColumns() ColumnPathToEncryptionPropsMap {
	return fe.encryptedCols
}

// ColumnEncryptionProperties returns the properties for encrypting a given column.
//
// This may be nil for columns that aren't encrypted or may be default properties.
func (fe *FileEncryptionProperties) ColumnEncryptionProperties(path string) *ColumnEncryptionProperties {
	if len(fe.encryptedCols) == 0 {
		return NewColumnEncryptionProperties(path)
	}
	if c, ok := fe.encryptedCols[path]; ok {
		return c
	}
	return nil
}

// Clone allows returning an identical property setup for another file with the option to update the aadPrefix,
// (if given the empty string, the current aad prefix will be used) since a single instance can only be used
// to encrypt one file before wiping out the keys.
func (fe *FileEncryptionProperties) Clone(newAadPrefix string) *FileEncryptionProperties {
	footerKeyCopy := fe.footerKey
	encryptedColsCopy := make(ColumnPathToEncryptionPropsMap)
	for k, v := range fe.encryptedCols {
		encryptedColsCopy[k] = v.Clone()
	}
	if newAadPrefix == "" {
		newAadPrefix = fe.aadPrefix
	}

	opts := []EncryptOption{
		WithAlg(fe.alg.Algo), WithFooterKeyMetadata(fe.footerKeyMetadata),
		WithAadPrefix(newAadPrefix), WithEncryptedColumns(encryptedColsCopy),
	}
	if !fe.encryptedFooter {
		opts = append(opts, WithPlaintextFooter())
	}
	if !fe.storeAadPrefixInFile {
		opts = append(opts, DisableAadPrefixStorage())
	}
	return NewFileEncryptionProperties(footerKeyCopy, opts...)
}

// WipeOutEncryptionKeys clears all of the encryption keys for this and the columns
func (fe *FileEncryptionProperties) WipeOutEncryptionKeys() {
	fe.footerKey = ""
	for _, elem := range fe.encryptedCols {
		elem.WipeOutEncryptionKey()
	}
}

type configEncrypt struct {
	cipher               Cipher
	encryptFooter        bool
	keyMetadata          string
	aadprefix            string
	storeAadPrefixInFile bool
	encryptedCols        ColumnPathToEncryptionPropsMap
}

// EncryptOption is used for specifying values when building FileEncryptionProperties
type EncryptOption func(*configEncrypt)

// WithPlaintextFooter sets the writer to write the footer in plain text, otherwise the footer will be encrypted
// too (which is the default behavior).
func WithPlaintextFooter() EncryptOption {
	return func(cfg *configEncrypt) {
		cfg.encryptFooter = false
	}
}

// WithAlg sets the encryption algorithm to utilize. (default is AesGcm)
func WithAlg(cipher Cipher) EncryptOption {
	return func(cfg *configEncrypt) {
		cfg.cipher = cipher
	}
}

// WithFooterKeyID sets a key retrieval metadata to use (converted from string), this must be a utf8 string.
//
// use either WithFooterKeyID or WithFooterKeyMetadata, not both.
func WithFooterKeyID(key string) EncryptOption {
	if !utf8.ValidString(key) {
		panic("parquet: footer key id should be UTF8 encoded")
	}
	return WithFooterKeyMetadata(key)
}

// WithFooterKeyMetadata sets a key retrieval metadata to use for getting the key.
//
// Use either WithFooterKeyID or WithFooterKeyMetadata, not both.
func WithFooterKeyMetadata(keyMeta string) EncryptOption {
	return func(cfg *configEncrypt) {
		if keyMeta != "" {
			cfg.keyMetadata = keyMeta
		}
	}
}

// WithAadPrefix sets the AAD prefix to use for encryption and by default will store it in the file
func WithAadPrefix(aadPrefix string) EncryptOption {
	return func(cfg *configEncrypt) {
		if aadPrefix != "" {
			cfg.aadprefix = aadPrefix
			cfg.storeAadPrefixInFile = true
		}
	}
}

// DisableAadPrefixStorage will set the properties to not store the AadPrefix in the file. If this isn't called
// and the AadPrefix is set, then it will be stored. This needs to in the options *after* WithAadPrefix to have an effect.
func DisableAadPrefixStorage() EncryptOption {
	return func(cfg *configEncrypt) {
		cfg.storeAadPrefixInFile = false
	}
}

// WithEncryptedColumns sets the map of columns and their properties (keys etc.) If not called, then all columns will
// be encrypted with the footer key. If called, then columns not in the map will be left unencrypted.
func WithEncryptedColumns(encrypted ColumnPathToEncryptionPropsMap) EncryptOption {
	none := func(*configEncrypt) {}
	if len(encrypted) == 0 {
		return none
	}
	return func(cfg *configEncrypt) {
		if len(cfg.encryptedCols) != 0 {
			panic("column properties already set")
		}
		for _, v := range encrypted {
			if v.IsUtilized() {
				panic("column properties utilized in another file")
			}
			v.SetUtilized()
		}
		cfg.encryptedCols = encrypted
	}
}

// NewFileEncryptionProperties returns a new File Encryption description object using the options provided.
func NewFileEncryptionProperties(footerKey string, opts ...EncryptOption) *FileEncryptionProperties {
	var cfg configEncrypt
	cfg.cipher = DefaultEncryptionAlgorithm
	cfg.encryptFooter = DefaultEncryptedFooter
	for _, o := range opts {
		o(&cfg)
	}

	props := &FileEncryptionProperties{
		footerKey:            footerKey,
		footerKeyMetadata:    cfg.keyMetadata,
		encryptedFooter:      cfg.encryptFooter,
		aadPrefix:            cfg.aadprefix,
		storeAadPrefixInFile: cfg.storeAadPrefixInFile,
		encryptedCols:        cfg.encryptedCols,
		utilized:             false,
	}

	aadFileUnique := [AadFileUniqueLength]uint8{}
	_, err := rand.Read(aadFileUnique[:])
	if err != nil {
		panic(err)
	}

	supplyAadPrefix := false
	if props.aadPrefix == "" {
		props.fileAad = string(aadFileUnique[:])
	} else {
		props.fileAad = props.aadPrefix + string(aadFileUnique[:])
		if !props.storeAadPrefixInFile {
			supplyAadPrefix = true
		}
	}
	props.alg.Algo = cfg.cipher
	props.alg.Aad.AadFileUnique = aadFileUnique[:]
	props.alg.Aad.SupplyAadPrefix = supplyAadPrefix
	if cfg.aadprefix != "" && cfg.storeAadPrefixInFile {
		props.alg.Aad.AadPrefix = []byte(props.aadPrefix)
	}
	return props
}
