fleet/orbit/pkg/update/notifications.go
Dante Catalfamo 643fc8314b
Orbit config receiver (#18518)
New interface for adding periodic jobs that rely on notifications/config
changes in Orbit.

Previously if we wanted to have recurring checks in Orbit, we would add
them into a chain of `GetConfig` calls. This call chain would be run
periodically by one of the runners registered with the cli application
framework.

The new method to register `OrbitConfigReceivers` with the
`OrbitClient`, and then register the orbit client itself with the
application framework.

Instead of having giving each fetcher an internal reference to the
previous fetcher that it must call, the receiver is registered with the
client and the new config is passed to the receiver.

This is the old `GetConfig()` interface:

```go
type OrbitConfigFetcher interface {
	GetConfig() (*fleet.OrbitConfig, error)
}
```

This is the new `OrbitConfigReceiver` interface:

```go
type OrbitConfigReceiver interface {
	Run(*OrbitConfig) error
}
```

To register a new receiver, you call the `RegisterConfigReceiver` method
on the client.

```go
orbitClient.RegisterConfigReceiver(extRunner)
```

Downsides of the old method:
- Spaghetti call chain setup
- Cascading failure, of one fails, all after it fail
- Run in series,  one long function call holds up the rest
- Anything that wants to restart orbit is added as a Runner to the
application, meaning there could be several timers calling `GetConfig`
and running the chain

Benefits of the new method:
- Clean `RegisterConfigReceiver` api, no call chaining required
- Config receivers can be added at runtime
- Isolated receivers, one failing call don't effect others
- All calls are run in parallel in goroutines, no calls can hold up the
rest
- No more need for multiple runners, using a context cancel, any
receiver can queue a call to restart orbit
- Single point to handle errors and logging for all receivers
- Panic recovery to stop orbit from crashing
- Easier to test, configs are passed in and do not require a call chain

This branch contains a little bit of code from the installer method I
was working on because I branched it off of that. (oops)

Not all code comments surrounding old `GetConfig()` methods have been
fully updated yet

Possible changes:
- Update the interface to take a context, so we can let receivers know
to exit early. I can imagine two cases for this:
  - The application is about to restart
  - We can set a timeout for how long receivers are allowed to take

Closes #12662

---------

Co-authored-by: Martin Angers <martin.n.angers@gmail.com>
Co-authored-by: Roberto Dip <dip.jesusr@gmail.com>
2024-05-09 15:22:56 -04:00

625 lines
22 KiB
Go

package update
import (
"errors"
"runtime"
"sync"
"sync/atomic"
"time"
"github.com/fleetdm/fleet/v4/orbit/pkg/bitlocker"
"github.com/fleetdm/fleet/v4/orbit/pkg/profiles"
"github.com/fleetdm/fleet/v4/orbit/pkg/scripts"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/rs/zerolog/log"
)
type runCmdFunc func() error
type checkEnrollmentFunc func() (bool, string, error)
type checkAssignedEnrollmentProfileFunc func(url string) error
// renewEnrollmentProfileConfigReceiver is a kind of middleware that wraps an
// OrbitConfigFetcher and detects if the fleet server sent a notification to
// renew the enrollment profile. If so, it runs the command (as root) to
// bootstrap the renewal of the profile on the device (the user still needs to
// execute some manual steps to accept the new profile).
//
// It ensures only one renewal command is executed at any given time, and that
// it doesn't re-execute the command until a certain amount of time has passed.
type renewEnrollmentProfileConfigReceiver struct {
// Frequency is the minimum amount of time that must pass between two
// executions of the profile renewal command.
Frequency time.Duration
// for tests, to be able to mock command execution. If nil, will use
// runRenewEnrollmentProfile.
runCmdFn runCmdFunc
// for tests, to be able to mock the function that checks for Fleet
// enrollment
checkEnrollmentFn checkEnrollmentFunc
// for tests, to be able to mock the function that checks for the assigned enrollment profile
checkAssignedEnrollmentProfileFn checkAssignedEnrollmentProfileFunc
// ensures only one command runs at a time, protects access to lastRun
cmdMu sync.Mutex
lastRun time.Time
fleetURL string
}
func ApplyRenewEnrollmentProfileConfigFetcherMiddleware(fetcher OrbitConfigFetcher, frequency time.Duration, fleetURL string) fleet.OrbitConfigReceiver {
return &renewEnrollmentProfileConfigReceiver{Frequency: frequency, fleetURL: fleetURL}
}
func (h *renewEnrollmentProfileConfigReceiver) Run(config *fleet.OrbitConfig) error {
if config.Notifications.RenewEnrollmentProfile {
if h.cmdMu.TryLock() {
defer h.cmdMu.Unlock()
// Note that the macOS notification popup will be shown periodically
// until the Fleet server gets notified that the device is now properly
// enrolled (after the user's manual steps, and osquery reporting the
// updated mdm enrollment).
// See https://github.com/fleetdm/fleet/pull/9409#discussion_r1084382455
if time.Since(h.lastRun) > h.Frequency {
// we perform this check locally on the client too to avoid showing the
// dialog if the client is enrolled to an MDM server.
enrollFn := h.checkEnrollmentFn
if enrollFn == nil {
enrollFn = profiles.IsEnrolledInMDM
}
enrolled, mdmServerURL, err := enrollFn()
if err != nil {
log.Error().Err(err).Msg("fetching enrollment status")
return nil
}
if enrolled {
log.Info().Msgf("a request to renew the enrollment profile was processed but not executed because the host is enrolled into an MDM server with URL: %s", mdmServerURL)
h.lastRun = time.Now().Add(-h.Frequency).Add(2 * time.Minute)
return nil
}
// we perform this check locally on the client too to avoid showing the
// dialog if the Fleet enrollment profile has not been assigned to the device in
// Apple Business Manager.
assignedFn := h.checkAssignedEnrollmentProfileFn
if assignedFn == nil {
assignedFn = profiles.CheckAssignedEnrollmentProfile
}
if err := assignedFn(h.fleetURL); err != nil {
log.Error().Err(err).Msg("checking assigned enrollment profile")
log.Info().Msg("a request to renew the enrollment profile was processed but not executed because there was an error checking the assigned enrollment profile.")
// TODO: Design a better way to backoff `profiles show` so that the device doesn't get rate
// limited by Apple. For now, wait at least 2 minutes before retrying.
h.lastRun = time.Now().Add(-h.Frequency).Add(2 * time.Minute)
return nil
}
fn := h.runCmdFn
if fn == nil {
fn = runRenewEnrollmentProfile
}
if err := fn(); err != nil {
// TODO: Look into whether we should increment lastRun here or implement a
// backoff to avoid unnecessary user notification popups and mitigate rate
// limiting by Apple.
log.Info().Err(err).Msg("calling /usr/bin/profiles to renew enrollment profile failed")
} else {
h.lastRun = time.Now()
log.Info().Msg("successfully called /usr/bin/profiles to renew enrollment profile")
}
} else {
log.Debug().Msg("skipped calling /usr/bin/profiles to renew enrollment profile, last run was too recent")
}
}
}
return nil
}
type execWinAPIFunc func(WindowsMDMEnrollmentArgs) error
type windowsMDMEnrollmentConfigReceiver struct {
// Frequency is the minimum amount of time that must pass between two
// executions of the windows MDM enrollment attempt.
Frequency time.Duration
// HostUUID is the current host's UUID.
HostUUID string
// OrbitNodeKey is the current host's orbit node key.
nodeKeyGetter OrbitNodeKeyGetter
// for tests, to be able to mock API commands. If nil, will use
// RunWindowsMDMEnrollment and RunWindowsMDMUnenrollment respectively.
execEnrollFn execWinAPIFunc
execUnenrollFn execWinAPIFunc
// ensures only one command runs at a time, protects access to lastXxxRun and
// isWindowsServer.
mu sync.Mutex
lastEnrollRun time.Time
lastUnenrollRun time.Time
isWindowsServer bool
}
type OrbitNodeKeyGetter interface {
GetNodeKey() (string, error)
}
func ApplyWindowsMDMEnrollmentFetcherMiddleware(
frequency time.Duration,
hostUUID string,
nodeKeyGetter OrbitNodeKeyGetter,
) fleet.OrbitConfigReceiver {
return &windowsMDMEnrollmentConfigReceiver{
Frequency: frequency,
HostUUID: hostUUID,
nodeKeyGetter: nodeKeyGetter,
}
}
var errIsWindowsServer = errors.New("device is a Windows Server")
// GetConfig calls the wrapped Fetcher's GetConfig method, and if the fleet
// server set the "needs windows enrollment" flag to true, executes the command
// to enroll into Windows MDM (or not, if the device is a Windows Server).
func (w *windowsMDMEnrollmentConfigReceiver) Run(cfg *fleet.OrbitConfig) error {
if cfg.Notifications.NeedsProgrammaticWindowsMDMEnrollment {
w.attemptEnrollment(cfg.Notifications)
} else if cfg.Notifications.NeedsProgrammaticWindowsMDMUnenrollment {
w.attemptUnenrollment()
}
return nil
}
func (w *windowsMDMEnrollmentConfigReceiver) attemptEnrollment(notifs fleet.OrbitConfigNotifications) {
if notifs.WindowsMDMDiscoveryEndpoint == "" {
log.Info().Err(errors.New("discovery endpoint is missing")).Msg("skipping enrollment, discovery endpoint is empty")
return
}
if w.mu.TryLock() {
defer w.mu.Unlock()
// do not enroll Windows Servers, and do not attempt enrollment if the last
// run is not at least Frequency ago.
if w.isWindowsServer {
log.Debug().Msg("skipped calling RegisterDeviceWithManagement to enroll Windows device, device is a server")
return
}
if time.Since(w.lastEnrollRun) <= w.Frequency {
log.Debug().Msg("skipped calling RegisterDeviceWithManagement to enroll Windows device, last run was too recent")
return
}
nodeKey, err := w.nodeKeyGetter.GetNodeKey()
if err != nil {
log.Info().Err(err).Msg("failed to get orbit node key to enroll Windows device")
return
}
fn := w.execEnrollFn
if fn == nil {
fn = RunWindowsMDMEnrollment
}
args := WindowsMDMEnrollmentArgs{
DiscoveryURL: notifs.WindowsMDMDiscoveryEndpoint,
HostUUID: w.HostUUID,
OrbitNodeKey: nodeKey,
}
if err := fn(args); err != nil {
if errors.Is(err, errIsWindowsServer) {
w.isWindowsServer = true
log.Info().Msg("device is a Windows Server, skipping enrollment")
} else {
log.Info().Err(err).Msg("calling RegisterDeviceWithManagement to enroll Windows device failed")
}
return
}
w.lastEnrollRun = time.Now()
log.Info().Msg("successfully called RegisterDeviceWithManagement to enroll Windows device")
}
}
func (w *windowsMDMEnrollmentConfigReceiver) attemptUnenrollment() {
if w.mu.TryLock() {
defer w.mu.Unlock()
// do not unenroll Windows Servers, and do not attempt unenrollment if the
// last run is not at least Frequency ago.
if w.isWindowsServer {
log.Debug().Msg("skipped calling UnregisterDeviceWithManagement to unenroll Windows device, device is a server")
return
}
if time.Since(w.lastUnenrollRun) <= w.Frequency {
log.Debug().Msg("skipped calling UnregisterDeviceWithManagement to unenroll Windows device, last run was too recent")
return
}
fn := w.execUnenrollFn
if fn == nil {
fn = RunWindowsMDMUnenrollment
}
// NOTE: args is actually unused by unenrollment, it is just for the
// function signature consistency.
args := WindowsMDMEnrollmentArgs{}
if err := fn(args); err != nil {
if errors.Is(err, errIsWindowsServer) {
w.isWindowsServer = true
log.Info().Msg("device is a Windows Server, skipping unenrollment")
} else {
log.Info().Err(err).Msg("calling UnregisterDeviceWithManagement to unenroll Windows device failed")
}
return
}
w.lastUnenrollRun = time.Now()
log.Info().Msg("successfully called UnregisterDeviceWithManagement to unenroll Windows device")
}
}
type runScriptsConfigReceiver struct {
// ScriptsExecutionEnabled indicates if this agent allows scripts execution.
// If it doesn't, scripts are not executed, but a response is returned to the
// Fleet server so it knows the agent processed the request. Note that this
// should be set to the value of the --scripts-enabled command-line flag. An
// additional, dynamic check is done automatically by the
// runScriptsConfigReceiver if this field is false to get the value from the
// MDM configuration profile.
ScriptsExecutionEnabled bool
// ScriptsClient is the client to use to fetch the script to execute and save
// back its results.
ScriptsClient scripts.Client
// the dynamic scripts enabled check is done to check via mdm configuration
// profile if the host is allowed to run dynamic scripts. It is only done
// on macos and only if ScriptsExecutionEnabled is false.
dynamicScriptsEnabled atomic.Bool
dynamicScriptsEnabledCheckInterval time.Duration
// for tests, if set will use this instead of profiles.GetFleetdConfig.
testGetFleetdConfig func() (*fleet.MDMAppleFleetdConfig, error)
// for tests, to be able to mock command execution. If nil, will use
// (scripts.Runner{...}).Run. To help with testing, the function receives as
// argument the scripts.Runner value that would've executed the call.
runScriptsFn func(*scripts.Runner, []string) error
// ensures only one script execution runs at a time
mu sync.Mutex
}
func ApplyRunScriptsConfigFetcherMiddleware(
scriptsEnabled bool, scriptsClient scripts.Client,
) (fleet.OrbitConfigReceiver, func() bool) {
scriptsFetcher := &runScriptsConfigReceiver{
ScriptsExecutionEnabled: scriptsEnabled,
ScriptsClient: scriptsClient,
dynamicScriptsEnabledCheckInterval: 5 * time.Minute,
}
// start the dynamic check for scripts enabled if required
scriptsFetcher.runDynamicScriptsEnabledCheck()
return scriptsFetcher, scriptsFetcher.scriptsEnabled
}
func (h *runScriptsConfigReceiver) runDynamicScriptsEnabledCheck() {
getFleetdConfig := h.testGetFleetdConfig
if getFleetdConfig == nil {
getFleetdConfig = profiles.GetFleetdConfig
}
// only run on macos and only if scripts are disabled by default for the
// agent (but always run if a test get fleetd config function is set).
if (runtime.GOOS == "darwin" && !h.ScriptsExecutionEnabled) || (h.testGetFleetdConfig != nil) {
go func() {
runCheck := func() {
cfg, err := getFleetdConfig()
if err != nil {
if err != profiles.ErrNotImplemented {
// note that an unenrolled host will not return an error, it will
// return the zero-value struct, so this logging should not be too
// noisy unless something goes wrong.
log.Info().Err(err).Msg("get fleetd configuration failed")
}
return
}
h.dynamicScriptsEnabled.Store(cfg.EnableScripts)
}
// check immediately at startup, before checking at the interval
runCheck()
// check every minute
for range time.Tick(h.dynamicScriptsEnabledCheckInterval) {
runCheck()
}
}()
}
}
// GetConfig calls the wrapped Fetcher's GetConfig method, and if the fleet
// server sent a list of scripts to execute, starts a goroutine to execute
// them.
func (h *runScriptsConfigReceiver) Run(cfg *fleet.OrbitConfig) error {
if len(cfg.Notifications.PendingScriptExecutionIDs) > 0 {
if h.mu.TryLock() {
log.Debug().Msgf("received request to run scripts %v", cfg.Notifications.PendingScriptExecutionIDs)
runner := &scripts.Runner{
ScriptExecutionEnabled: h.scriptsEnabled(),
Client: h.ScriptsClient,
}
fn := runner.Run
if h.runScriptsFn != nil {
fn = func(execIDs []string) error {
return h.runScriptsFn(runner, execIDs)
}
}
go func() {
defer h.mu.Unlock()
if err := fn(cfg.Notifications.PendingScriptExecutionIDs); err != nil {
log.Info().Err(err).Msg("running scripts failed")
return
}
log.Debug().Msgf("running scripts %v succeeded", cfg.Notifications.PendingScriptExecutionIDs)
}()
}
}
return nil
}
func (h *runScriptsConfigReceiver) scriptsEnabled() bool {
// scripts are always enabled if the agent is started with the
// --scripts-enabled flag. If it is not started with this flag, then
// scripts are enabled only if the mdm profile says so.
return h.ScriptsExecutionEnabled || h.dynamicScriptsEnabled.Load()
}
type DiskEncryptionKeySetter interface {
SetOrUpdateDiskEncryptionKey(diskEncryptionStatus fleet.OrbitHostDiskEncryptionKeyPayload) error
}
// execEncryptVolumeFunc handles the encryption of a volume identified by its
// string identifier (e.g., "C:").
//
// It returns a string representing the recovery key and an error if any occurs during the process.
type execEncryptVolumeFunc func(volumeID string) (recoveryKey string, err error)
// execGetEncryptionStatusFunc retrieves the encryption status of all volumes
// managed by Bitlocker.
//
// It returns a slice of bitlocker.VolumeStatus, each representing the
// encryption status of a volume, and an error if the operation fails.
type execGetEncryptionStatusFunc func() (status []bitlocker.VolumeStatus, err error)
// execDecryptVolumeFunc handles the decryption of a volume identified by its
// string identifier (e.g., "C:")
//
// It returns an error if the process fails.
type execDecryptVolumeFunc func(volumeID string) error
type windowsMDMBitlockerConfigReceiver struct {
// Frequency is the minimum amount of time that must pass between two
// executions of the windows MDM enrollment attempt.
Frequency time.Duration
// Bitlocker Operation Results
EncryptionResult DiskEncryptionKeySetter
// tracks last time a disk encryption has successfully run
lastRun time.Time
// ensures only one script execution runs at a time
mu sync.Mutex
// for tests, to be able to mock API commands. If nil, will use
// bitlocker.EncryptVolume
execEncryptVolumeFn execEncryptVolumeFunc
// for tests, to be able to mock API commands. If nil, will use
// bitlocker.GetEncryptionStatus
execGetEncryptionStatusFn execGetEncryptionStatusFunc
// for tests, to be able to mock the decryption process. If nil, will use
// bitlocker.DecryptVolume
execDecryptVolumeFn execDecryptVolumeFunc
}
func ApplyWindowsMDMBitlockerFetcherMiddleware(
frequency time.Duration,
encryptionResult DiskEncryptionKeySetter,
) fleet.OrbitConfigReceiver {
return &windowsMDMBitlockerConfigReceiver{
Frequency: frequency,
EncryptionResult: encryptionResult,
}
}
// GetConfig calls the wrapped Fetcher's GetConfig method, and if the fleet
// server set the "EnforceBitLockerEncryption" flag to true, executes the command
// to attempt BitlockerEncryption (or not, if the device is a Windows Server).
func (w *windowsMDMBitlockerConfigReceiver) Run(cfg *fleet.OrbitConfig) error {
if cfg.Notifications.EnforceBitLockerEncryption {
if w.mu.TryLock() {
defer w.mu.Unlock()
w.attemptBitlockerEncryption(cfg.Notifications)
}
}
return nil
}
func (w *windowsMDMBitlockerConfigReceiver) attemptBitlockerEncryption(notifs fleet.OrbitConfigNotifications) {
if time.Since(w.lastRun) <= w.Frequency {
log.Debug().Msg("skipped encryption process, last run was too recent")
return
}
// Windows servers are not supported. Check and skip if that's the case.
if isServer, err := IsRunningOnWindowsServer(); isServer || err != nil {
if err != nil {
log.Error().Err(err).Msg("checking if the host is a Windows server")
} else {
log.Debug().Msg("device is a Windows Server, encryption is not going to be performed")
}
return
}
const targetVolume = "C:"
encryptionStatus, err := w.getEncryptionStatusForVolume(targetVolume)
if err != nil {
log.Debug().Err(err).Msgf("unable to get encryption status for target volume %s, continuing anyway", targetVolume)
}
// don't do anything if the disk is being encrypted/decrypted
if w.bitLockerActionInProgress(encryptionStatus) {
log.Debug().Msgf("skipping encryption as the disk is not available. Disk conversion status: %d", encryptionStatus.ConversionStatus)
return
}
// if the disk is encrypted, try to decrypt it first.
if encryptionStatus != nil &&
encryptionStatus.ConversionStatus == bitlocker.ConversionStatusFullyEncrypted {
log.Debug().Msg("disk was previously encrypted. Attempting to decrypt it")
if err := w.decryptVolume(targetVolume); err != nil {
log.Error().Err(err).Msg("decryption failed")
if serverErr := w.updateFleetServer("", err); serverErr != nil {
log.Error().Err(serverErr).Msg("failed to send decryption failure to Fleet Server")
return
}
}
// return regardless of the operation output.
//
// the decryption process takes an unknown amount of time (depending on
// factors outside of our control) and the next tick will be a noop if the
// disk is not ready to be encrypted yet (due to the
// w.bitLockerActionInProgress check above)
return
}
recoveryKey, encryptionErr := w.performEncryption(targetVolume)
// before reporting the error to the server, check if the error we've got is valid.
// see the description of w.isMisreportedDecryptionError and issue #15916.
var pErr *bitlocker.EncryptionError
if errors.As(encryptionErr, &pErr) && w.isMisreportedDecryptionError(pErr, encryptionStatus) {
log.Error().Msg("disk encryption failed due to previous unsuccessful attempt, user action required")
return
}
if serverErr := w.updateFleetServer(recoveryKey, encryptionErr); serverErr != nil {
log.Error().Err(serverErr).Msg("failed to send encryption result to Fleet Server")
return
}
if encryptionErr != nil {
log.Error().Err(err).Msg("failed to encrypt the volume")
return
}
w.lastRun = time.Now()
}
// getEncryptionStatusForVolume retrieves the encryption status for a specific volume.
func (w *windowsMDMBitlockerConfigReceiver) getEncryptionStatusForVolume(volume string) (*bitlocker.EncryptionStatus, error) {
fn := w.execGetEncryptionStatusFn
if fn == nil {
fn = bitlocker.GetEncryptionStatus
}
status, err := fn()
if err != nil {
return nil, err
}
for _, s := range status {
if s.DriveVolume == volume {
return s.Status, nil
}
}
return nil, nil
}
// bitLockerActionInProgress determines an encryption/decription action is in
// progress based on the reported status.
func (w *windowsMDMBitlockerConfigReceiver) bitLockerActionInProgress(status *bitlocker.EncryptionStatus) bool {
if status == nil {
return false
}
// Check if the status matches any of the specified conditions
return status.ConversionStatus == bitlocker.ConversionStatusDecryptionInProgress ||
status.ConversionStatus == bitlocker.ConversionStatusDecryptionPaused ||
status.ConversionStatus == bitlocker.ConversionStatusEncryptionInProgress ||
status.ConversionStatus == bitlocker.ConversionStatusEncryptionPaused
}
// performEncryption executes the encryption process.
func (w *windowsMDMBitlockerConfigReceiver) performEncryption(volume string) (string, error) {
fn := w.execEncryptVolumeFn
if fn == nil {
fn = bitlocker.EncryptVolume
}
recoveryKey, err := fn(volume)
if err != nil {
return "", err
}
return recoveryKey, nil
}
func (w *windowsMDMBitlockerConfigReceiver) decryptVolume(targetVolume string) error {
fn := w.execDecryptVolumeFn
if fn == nil {
fn = bitlocker.DecryptVolume
}
return fn(targetVolume)
}
// isMisreportedDecryptionError checks whether the given error is a potentially
// misreported decryption error.
//
// It addresses cases where a previous encryption attempt failed due to other
// errors but subsequent attempts to encrypt the disk could erroneously return
// a bitlocker.FVE_E_NOT_DECRYPTED error.
//
// This function checks if the disk is actually fully decrypted
// (status.ConversionStatus == bitlocker.CONVERSION_STATUS_FULLY_DECRYPTED) and
// whether the reported error is bitlocker.FVE_E_NOT_DECRYPTED. If these
// conditions are met, the error is not accurately reflecting the disk's actual
// encryption state.
//
// For more context, see issue #15916
func (w *windowsMDMBitlockerConfigReceiver) isMisreportedDecryptionError(err *bitlocker.EncryptionError, status *bitlocker.EncryptionStatus) bool {
return err.Code() == bitlocker.ErrorCodeNotDecrypted &&
status != nil &&
status.ConversionStatus == bitlocker.ConversionStatusFullyDecrypted
}
func (w *windowsMDMBitlockerConfigReceiver) updateFleetServer(key string, err error) error {
// Getting Bitlocker encryption operation error message if any
// This is going to be sent to Fleet Server
bitlockerError := ""
if err != nil {
bitlockerError = err.Error()
}
// Update Fleet Server with encryption result
payload := fleet.OrbitHostDiskEncryptionKeyPayload{
EncryptionKey: []byte(key),
ClientError: bitlockerError,
}
return w.EncryptionResult.SetOrUpdateDiskEncryptionKey(payload)
}