Skip to content

Connect-Keeper: Automatically perform actions on DeviceAuth and TwoFactor #125

@bror-lauritz

Description

@bror-lauritz

When running Connect-Keeper, I want to be able to automatically answer the DeviceAuth and TwoFactor steps in the AuthFlow. I've tried to answer them by piping the answer into another process, which partially works but is quite fragile.

What I want to do

Consider the TwoFactor step:

    Keeper Username:  $username
Available Commands
channel=<authenticator> to change channel.
expire=<now | 30_days | never> to set 2fa expiration.
<code> to send a 2fa code.
<Enter> to resume
2FA channel(authenticator) expire[now]: 

I want to automatically answer the OTP here.
The same for DeviceAuth, but I want to first send "channel=2fa" and then the OTP.

What I have implemented

My current solution patches Connect-Keeper as defined in AuthCommands.ps1 with two new parameters and two if statements (see commented areas)

function Connect-Keeper {
    <#
    .Synopsis
    Login to Keeper

   .Parameter Username
    User email

    .Parameter NewLogin
    Do not use Last Login information

    .Parameter SsoPassword
    Use Master Password for SSO account

    .Parameter Server
    Change default keeper server

    # New Parameters
    .Parameter TwoFactorChannel
    Which channel to use when authenticating with 2FA

    .Parameter TwoFactorAction
    The action to take when authenticating with 2FA (i.e. push or <code>)

    .Parameter DeviceAuthChannel
    Which channel to use when authenticating a device

    .Parameter DeviceAuthAction
    The action to take when authenticating a device (i.e. push or <code>)
#>
    [CmdletBinding(DefaultParameterSetName = 'regular')]
    Param(
        [Parameter(Position = 0)][string] $Username,
        [Parameter()] [SecureString]$Password,
        [Parameter()][switch] $NewLogin,
        [Parameter(ParameterSetName = 'sso_password')][switch] $SsoPassword,
        [Parameter(ParameterSetName = 'sso_provider')][switch] $SsoProvider,
        [Parameter()][string] $Server,

        # New parameters
        [Parameter()][securestring[]] $TwoFactorActions,
        [Parameter()][SecureString[]] $DeviceAuthActions # Can be sensitive
    )

    Disconnect-Keeper -Resume | Out-Null

    $storage = New-Object KeeperSecurity.Configuration.JsonConfigurationStorage
    if (-not $Server) {
        $Server = $storage.LastServer
        if ($Server) {
            Write-Information -MessageData "`nUsing Keeper Server: $Server`n"
        }
        else {
            Write-Information -MessageData "`nUsing Default Keeper Server: $([KeeperSecurity.Authentication.KeeperEndpoint]::DefaultKeeperServer)`n"
        }
    }


    $endpoint = New-Object KeeperSecurity.Authentication.KeeperEndpoint($Server, $storage.Servers)
    $endpoint.DeviceName = 'PowerShell Commander'
    $endpoint.ClientVersion = 'c16.1.0'
    $authFlow = New-Object KeeperSecurity.Authentication.Sync.AuthSync($storage, $endpoint)

    $authFlow.ResumeSession = $true
    $authFlow.AlternatePassword = $SsoPassword.IsPresent

    if (-not $NewLogin.IsPresent -and -not $SsoProvider.IsPresent) {
        if (-not $Username) {
            $Username = $storage.LastLogin
        }
    }

    $namePrompt = 'Keeper Username'
    if ($SsoProvider.IsPresent) {
        $namePrompt = 'Enterprise Domain'
    }

    if ($Username) {
        Write-Output "$(($namePrompt + ': ').PadLeft(21, ' ')) $Username"
    }
    else {
        while (-not $Username) {
            $Username = Read-Host -Prompt $namePrompt.PadLeft(20, ' ')
        }
    }
    if ($SsoProvider.IsPresent) {
        $authFlow.LoginSso($Username).GetAwaiter().GetResult() | Out-Null
    }
    else {
        $passwords = @()
        if ($Password) {
            if ($Password -is [SecureString]) {
                $passwords += [Net.NetworkCredential]::new('', $Password).Password
            }
            elseif ($Password -is [String]) {
                $passwords += $Password
            }
        }
        $authFlow.Login($Username, $passwords).GetAwaiter().GetResult() | Out-Null
    }
    Write-Output ""
    while (-not $authFlow.IsCompleted) {
        if ($lastStep -ne $authFlow.Step.State) {
            printStepHelp $authFlow
            $lastStep = $authFlow.Step.State
        }

        $prompt = getStepPrompt $authFlow

        #### Start My Changes ####
        # If we're on the DeviceApproval step and the user has specified $DeviceAuthActions, do those actions in order
        if ($DeviceAuthActions -and $authFlow.Step -is [KeeperSecurity.Authentication.Sync.DeviceApprovalStep]) {
            executeStepAction $authFlow ($DeviceAuthActions[0] | ConvertFrom-SecureString -AsPlainText)
            $DeviceAuthActions = $DeviceAuthActions[1..$DeviceAuthActions.Length] # Just do one thing per while loop
        }
        # If we're on the TwoFactor step and the user has specified $TwoFactorActions, do those actions in order
        elseif ($TwoFactorActions -and $authFlow.Step -is [KeeperSecurity.Authentication.Sync.TwoFactorStep]) {
            executeStepAction $authFlow ($TwoFactorActions[0] | ConvertFrom-SecureString -AsPlainText)
            $TwoFactorActions = $TwoFactorActions[1..$TwoFactorActions.Length] # Just do one thing per while loop
        }

        # Otherwise, do everything like normal
        elseif ($action) {

            # move this here to avoid the "Read-Host" prompt
            if ($authFlow.Step -is [KeeperSecurity.Authentication.Sync.PasswordStep]) {
                $securedPassword = Read-Host -Prompt $prompt -AsSecureString
                if ($securedPassword.Length -gt 0) {
                    $action = [Net.NetworkCredential]::new('', $securedPassword).Password
                }
                else {
                    $action = ''
                }
            }
            else {
                $action = Read-Host -Prompt $prompt
            }

            if ($action -eq '?') {
                }
            else {
                executeStepAction $authFlow $action
            }
        }
        #### End My Changes ####
    }

    if ($authFlow.Step.State -ne [KeeperSecurity.Authentication.Sync.AuthState]::Connected) {
        if ($authFlow.Step -is [KeeperSecurity.Authentication.Sync.ErrorStep]) {
            Write-Warning $authFlow.Step.Message
        }
        return
    }

    $auth = $authFlow
    if ([KeeperSecurity.Authentication.AuthExtensions]::IsAuthenticated($auth)) {
        Write-Debug -Message "Connected to Keeper as $Username"

        $vault = New-Object KeeperSecurity.Vault.VaultOnline($auth)
        $task = $vault.SyncDown()
        Write-Information -MessageData 'Syncing ...'
        $task.GetAwaiter().GetResult() | Out-Null
        $vault.AutoSync = $true

        $Script:Context.Auth = $auth
        $Script:Context.Vault = $vault

        [KeeperSecurity.Vault.VaultData]$vaultData = $vault
        Write-Information -MessageData "Decrypted $($vaultData.RecordCount) record(s)"
        Set-KeeperLocation -Path '\' | Out-Null
    }
}

I can then run this to do all the steps I'd like:

$otp = Get-Otp $KeeperTotpSecret | ConvertTo-SecureString -AsPlainText
$params = @{
    Username = $KeeperUser
    Password = $KeeperPassword
    SsoPassword = $true
    TwoFactorActions = @(("channel=authenticator" | ConvertTo-SecureString -AsPlainText), $otp)
    DeviceAuthActions = @(("channel=2fa" | ConvertTo-SecureString -AsPlainText), $otp)
    ErrorAction = 'Stop'
}
Connect-Keeper @params 

(I don't know if the actions should be secure string or not, from a security perspective)

Limitations with my method

This works pretty well for my purposes, but I'd like for something like this in the source so I don't have to verify that it works after every new release.

One drawback is that it still unnecessarily prints out

    Keeper Username:  $username
Available Commands
channel=<authenticator> to change channel.
expire=<now | 30_days | never> to set 2fa expiration.
<code> to send a 2fa code.
<Enter> to resume

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions