diff --git a/Docs/Commands/Set-YubiKeyOTPSlotAccessCode.md b/Docs/Commands/Set-YubiKeyOTPSlotAccessCode.md new file mode 100644 index 0000000..50fb6cc --- /dev/null +++ b/Docs/Commands/Set-YubiKeyOTPSlotAccessCode.md @@ -0,0 +1,179 @@ +--- +external help file: powershellYK.dll-Help.xml +Module Name: powershellYK +online version: +schema: 2.0.0 +--- + +# Set-YubiKeyOTPSlotAccessCode + +## SYNOPSIS +Sets, changes or removes the OTP slot access code for a YubiKey. +he access code protects OTP slot configurations from unauthorized modifications. + +## SYNTAX + +### SetNewAccessCode +``` +Set-YubiKeyOTPSlotAccessCode -Slot [-AccessCode ] [-WhatIf] [-Confirm] [] +``` + +### ChangeAccessCode +``` +Set-YubiKeyOTPSlotAccessCode -Slot -AccessCode -CurrentAccessCode [-WhatIf] [-Confirm] + [] +``` + +### RemoveAccessCode +``` +Set-YubiKeyOTPSlotAccessCode -Slot -CurrentAccessCode [-RemoveAccessCode] [-WhatIf] [-Confirm] + [] +``` + +## DESCRIPTION +Sets, changes or removes the OTP slot access code for a YubiKey. +The access code protects OTP slot configurations from unauthorized modifications. +Access codes are 6 bytes in length, provided as 12-character hex strings. + +## EXAMPLES + +### Example 1 +```powershell +PS C:\> Set-YubiKeySlotAccessCode -Slot LongPress -AccessCode "010203040506" +``` + +Set a new access code for a slot (when no access code exists) + +### Example 2 +```powershell +PS C:\> Set-YubiKeyOTPSlotAccessCode -Slot ShortPress -CurrentAccessCode "010203040506" -AccessCode "060504030201" +``` + +Change an existing slot access code + +### Example 3 +```powershell +PS C:\> Set-YubiKeyOTPSlotAccessCode -Slot LongPress -CurrentAccessCode "010203040506" -RemoveAccessCode +``` + +Remove slot access code protection (set to all zeros) + +## PARAMETERS + +### -AccessCode +New access code (12-character hex string) + +```yaml +Type: String +Parameter Sets: SetNewAccessCode +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +```yaml +Type: String +Parameter Sets: ChangeAccessCode +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -CurrentAccessCode +Current access code (12-character hex string) + +```yaml +Type: String +Parameter Sets: ChangeAccessCode, RemoveAccessCode +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -RemoveAccessCode +Remove access code protection + +```yaml +Type: SwitchParameter +Parameter Sets: RemoveAccessCode +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Slot +Yubikey OTP Slot + +```yaml +Type: Slot +Parameter Sets: (All) +Aliases: +Accepted values: None, ShortPress, LongPress + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Confirm +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: cf + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -WhatIf +Shows what would happen if the cmdlet runs. +The cmdlet is not run. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: wi + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None + +## OUTPUTS + +### System.Object +## NOTES + +## RELATED LINKS diff --git a/Docs/Commands/powershellYK.md b/Docs/Commands/powershellYK.md index 87bc886..347540e 100644 --- a/Docs/Commands/powershellYK.md +++ b/Docs/Commands/powershellYK.md @@ -120,7 +120,7 @@ Removes a FIDO2 credential from the YubiKey. ### [Remove-YubikeyOATHAccount](Remove-YubikeyOATHAccount.md) Removes an account from the YubiKey OATH application. -### [Remove-YubikeyOTP](Remove-YubikeyOTP.md) +### [Remove-YubiKeyOTP](Remove-YubiKeyOTP.md) Remove YubiKey OTP slot. ### [Remove-YubikeyPIVKey](Remove-YubikeyPIVKey.md) @@ -162,9 +162,13 @@ Set the PIN for the FIDO2 application on the YubiKey. ### [Set-YubiKeyOATHPassword](Set-YubiKeyOATHPassword.md) Set the password for the YubiKey OATH application. -### [Set-YubikeyOTP](Set-YubikeyOTP.md) +### [Set-YubiKeyOTP](Set-YubiKeyOTP.md) Configure OTP slots +### [Set-YubiKeyOTPSlotAccessCode](Set-YubiKeyOTPSlotAccessCode.md) +Sets, changes or removes the OTP slot access code for a YubiKey. +he access code protects OTP slot configurations from unauthorized modifications. + ### [Set-YubikeyPIV](Set-YubikeyPIV.md) Allows the updating of PIV settings diff --git a/Module/Cmdlets/OTP/RemoveYubikeyOTP.cs b/Module/Cmdlets/OTP/RemoveYubikeyOTP.cs index 1e4e273..204f6ab 100644 --- a/Module/Cmdlets/OTP/RemoveYubikeyOTP.cs +++ b/Module/Cmdlets/OTP/RemoveYubikeyOTP.cs @@ -8,8 +8,9 @@ /// Removes the OTP configuration from the short-press slot /// /// .EXAMPLE -/// Remove-YubiKeyOTP -Slot LongPress -/// Removes the OTP configuration from the long-press slot +/// Remove-YubiKeyOTP -Slot LongPress -CurrentAccessCode "010203040506" +/// Removes the OTP configuration from the long-press slot when a slot access code is set +/// /// // Imports @@ -29,6 +30,11 @@ public class RemoveYubikeyOTPCommand : Cmdlet [Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "YubiOTP Slot", ParameterSetName = "Remove")] public Slot Slot { get; set; } + // The current access code (12-character hex string) if the slot is protected + [Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "Current access code (12-character hex string)", ParameterSetName = "Remove")] + [ValidateCount(12, 12)] + public string? CurrentAccessCode { get; set; } + // Connect to YubiKey when cmdlet starts protected override void BeginProcessing() { @@ -69,8 +75,28 @@ protected override void ProcessRecord() // Delete the slot configuration var deleteSlot = otpSession.DeleteSlotConfiguration(Slot); - deleteSlot.Execute(); // Note: Deletion may return an error even when successful - WriteInformation($"Removed OTP configuration from slot {Slot.ToString("d")}", new string[] { "OTP", "Info" }); + // If CurrentAccessCode is provided, use it + if (CurrentAccessCode != null) + { + // Convert hex string to byte array using Hex helper class + var currentAccessCodeBytes = powershellYK.support.Hex.Decode(CurrentAccessCode); + var slotAccessCode = new SlotAccessCode(currentAccessCodeBytes); + deleteSlot = deleteSlot.UseCurrentAccessCode(slotAccessCode); + } + try + { + deleteSlot.Execute(); // Note: Deletion may return an error even when successful + WriteInformation($"Removed OTP configuration from slot {Slot.ToString("d")}", new string[] { "OTP", "Info" }); + } + catch (Exception ex) + { + // Show a message to guide the user into providing or correcting a slot access code + // if (ex.Message.Contains("YubiKey Operation Failed") && ex.Message.Contains("state of non-volatile memory is unchanged")) + // { + // WriteWarning("The requested slot is protected with a slot access code. Either no access code was provided, or the provided code was incorrect. Please call the cmdlet again using -CurrentAccessCode with the correct code."); + // } + WriteError(new ErrorRecord(ex, "RemoveYubiKeyOTPError", ErrorCategory.InvalidOperation, null)); + } } } } diff --git a/Module/Cmdlets/OTP/SetYubikeyOTP.cs b/Module/Cmdlets/OTP/SetYubikeyOTP.cs index 188e4b0..8ca61a7 100644 --- a/Module/Cmdlets/OTP/SetYubikeyOTP.cs +++ b/Module/Cmdlets/OTP/SetYubikeyOTP.cs @@ -60,9 +60,21 @@ /// .EXAMPLE /// # Configure HOTP with 8 digits, TAB, and carriage return /// Set-YubiKeyOTP -Slot ShortPress -HOTP -Use8Digits -SendTabFirst -AppendCarriageReturn +/// +/// .EXAMPLE +/// # Set a new access code for a slot (when no access code exists) +/// Set-YubiKeyOTP -Slot LongPress -HOTP -AccessCode "010203040506" +/// +/// .EXAMPLE +/// # Change an existing slot access code +/// Set-YubiKeyOTP -Slot ShortPress -HOTP -CurrentAccessCode "010203040506" -AccessCode "060504030201" +/// +/// .EXAMPLE +/// # Authenticate with an existing access code to update slot configuration +/// Set-YubiKeyOTP -Slot LongPress -HOTP -CurrentAccessCode "010203040506" -Base32Secret "QRFJ7DTIVASL3PNYXWFIQAQN5RKUJD4U" /// - +// Imports using System.Management.Automation; using System.Runtime.InteropServices; using System.Security; @@ -170,6 +182,16 @@ public class SetYubikeyOTPCommand : PSCmdlet [Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "Use 8 digits instead of 6 for HOTP", ParameterSetName = "HOTP")] public SwitchParameter Use8Digits { get; set; } + // The new access code to set (will be converted to bytes, max 6 bytes) + [Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "New access code (12-character hex string)")] + [ValidateCount(12, 12)] + public string? AccessCode { get; set; } + + // The current access code (required when changing or authenticating) + [Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "Current access code (12-character hex string)")] + [ValidateCount(12, 12)] + public string? CurrentAccessCode { get; set; } + // Initializes the cmdlet by ensuring a YubiKey is connected protected override void BeginProcessing() { @@ -195,194 +217,239 @@ protected override void ProcessRecord() { using (var otpSession = new OtpSession((YubiKeyDevice)YubiKeyModule._yubikey!)) { - WriteDebug($"Working with {ParameterSetName}"); - if ((Slot == Yubico.YubiKey.Otp.Slot.ShortPress && !otpSession.IsShortPressConfigured) || - (Slot == Yubico.YubiKey.Otp.Slot.LongPress && !otpSession.IsLongPressConfigured) || - ShouldProcess($"Yubikey OTP {Slot}", "Set")) + try + { + WriteDebug($"Working with {ParameterSetName}"); + if ((Slot == Yubico.YubiKey.Otp.Slot.ShortPress && !otpSession.IsShortPressConfigured) || + (Slot == Yubico.YubiKey.Otp.Slot.LongPress && !otpSession.IsLongPressConfigured) || + ShouldProcess($"Yubikey OTP {Slot}", "Set")) + { + switch (ParameterSetName) + { + case "Yubico OTP": + // Configure Yubico OTP mode + Memory _publicID = new Memory(new byte[6]); + Memory _privateID = new Memory(new byte[6]); + Memory _secretKey = new Memory(new byte[16]); + ConfigureYubicoOtp configureyubicoOtp = otpSession.ConfigureYubicoOtp(Slot); + int? serial = YubiKeyModule._yubikey!.SerialNumber; + + // Handle Public ID configuration + if (PublicID is null) + { + configureyubicoOtp = configureyubicoOtp.UseSerialNumberAsPublicId(_publicID); + } + else + { + _publicID = PublicID; + configureyubicoOtp = configureyubicoOtp.UsePublicId(PublicID); + } + + // Handle Private ID configuration + if (PrivateID is null) + { + configureyubicoOtp = configureyubicoOtp.GeneratePrivateId(_privateID); + } + else + { + _privateID = PrivateID; + configureyubicoOtp = configureyubicoOtp.UsePublicId(PrivateID); + } + + // Handle Secret Key configuration + if (SecretKey is null) + { + configureyubicoOtp = configureyubicoOtp.GenerateKey(_secretKey); + } + else + { + _secretKey = SecretKey; + configureyubicoOtp = configureyubicoOtp.UseKey(SecretKey); + } + + configureyubicoOtp.Execute(); + + // Return configuration if any defaults were used + if (PublicID is null || PrivateID is null || SecretKey is null) + { + YubicoOTP retur = new YubicoOTP(serial, _publicID.ToArray(), _privateID.ToArray(), _secretKey.ToArray(), ""); + WriteObject(retur); + } + + // Handle YubiCloud upload + if (Upload.IsPresent) + { + // https://github.com/Yubico/yubikey-manager/blob/fbdae2bc12ba0451bcfc62372bc9191c10ecad0c/ykman/otp.py#L95 + // TODO: Implement Upload to YubiCloud + // @virot: upload is no longer supported. Need to output a CSV file for manual upload. + WriteWarning("Upload to YubiCloud functionality has been deprecated by Yubico."); + } + break; + + case "Static Password": + // Configure static password mode + ConfigureStaticPassword staticpassword = otpSession.ConfigureStaticPassword(Slot); + staticpassword = staticpassword.WithKeyboard(KeyboardLayout); + staticpassword = staticpassword.SetPassword((Marshal.PtrToStringUni(Marshal.SecureStringToGlobalAllocUnicode(Password!))!).AsMemory()); + if (AppendCarriageReturn.IsPresent) + { + staticpassword = staticpassword.AppendCarriageReturn(); + } + staticpassword.Execute(); + break; + + case "Static Generated Password": + // Configure static generated password mode + ConfigureStaticPassword staticgenpassword = otpSession.ConfigureStaticPassword(Slot); + Memory generatedPassword = new Memory(new char[PasswordLength]); + staticgenpassword = staticgenpassword.WithKeyboard(KeyboardLayout); + staticgenpassword = staticgenpassword.GeneratePassword(generatedPassword); + if (AppendCarriageReturn.IsPresent) + { + staticgenpassword = staticgenpassword.AppendCarriageReturn(); + } + staticgenpassword.Execute(); + break; + + case "ChallengeResponse": + // Configure challenge-response mode + Memory _CRsecretKey = new Memory(new byte[20]); + ConfigureChallengeResponse configureCR = otpSession.ConfigureChallengeResponse(Slot); + + // Handle Secret Key configuration + if (SecretKey is null) + { + configureCR = configureCR.GenerateKey(_CRsecretKey); + } + else + { + _CRsecretKey = SecretKey; + configureCR = configureCR.UseKey(SecretKey); + } + + // Configure touch requirement + if (RequireTouch.IsPresent) + { + configureCR = configureCR.UseButton(); + } + + // Configure algorithm + if (Algorithm == ChallengeResponseAlgorithm.HmacSha1) + { + configureCR = configureCR.UseHmacSha1(); + } + else if (Algorithm == ChallengeResponseAlgorithm.YubicoOtp) + { + configureCR = configureCR.UseYubiOtp(); + } + + configureCR.Execute(); + + // Return configuration if default key was used + if (SecretKey is null) + { + ChallangeResponse retur = new ChallangeResponse(_CRsecretKey.ToArray()); + WriteObject(retur); + } + break; + + case "HOTP": + // Configure HOTP mode + Memory _HOTPsecretKey = new Memory(new byte[20]); + ConfigureHotp configureHOTP = otpSession.ConfigureHotp(Slot); + + // Handle Secret Key configuration using Base32 + if (Base32Secret != null) + { + _HOTPsecretKey = powershellYK.support.Base32.Decode(Base32Secret); + configureHOTP = configureHOTP.UseKey(_HOTPsecretKey); + } + // Handle Secret Key configuration using Hex + else if (HexSecret != null) + { + _HOTPsecretKey = powershellYK.support.Hex.Decode(HexSecret); + configureHOTP = configureHOTP.UseKey(_HOTPsecretKey); + } + else if (SecretKey is null) + { + configureHOTP = configureHOTP.GenerateKey(_HOTPsecretKey); + } + else + { + _HOTPsecretKey = SecretKey; + configureHOTP = configureHOTP.UseKey(SecretKey); + } + + // Handle access code logic + byte[]? newAccessCodeBytes = null; + byte[]? currentAccessCodeBytes = null; + if (AccessCode != null) + { + newAccessCodeBytes = powershellYK.support.Hex.Decode(AccessCode); + } + if (CurrentAccessCode != null) + { + currentAccessCodeBytes = powershellYK.support.Hex.Decode(CurrentAccessCode); + } + SlotAccessCode? newAccessCode = null; + SlotAccessCode? currentAccessCode = null; + if (currentAccessCodeBytes != null) + { + currentAccessCode = new SlotAccessCode(currentAccessCodeBytes); + configureHOTP = configureHOTP.UseCurrentAccessCode(currentAccessCode); + // If AccessCode is not provided, preserve the current code + if (newAccessCodeBytes == null) + { + newAccessCode = currentAccessCode; + configureHOTP = configureHOTP.SetNewAccessCode(newAccessCode); + } + } + if (newAccessCodeBytes != null) + { + newAccessCode = new SlotAccessCode(newAccessCodeBytes); + configureHOTP = configureHOTP.SetNewAccessCode(newAccessCode); + } + + // Configure TAB before OTP if requested + if (SendTabFirst.IsPresent) + { + configureHOTP = configureHOTP.SendTabFirst(); + } + + // Configure carriage return if requested + if (AppendCarriageReturn.IsPresent) + { + configureHOTP = configureHOTP.AppendCarriageReturn(); + } + + // Configure 8 digits if requested + if (Use8Digits.IsPresent) + { + configureHOTP = configureHOTP.Use8Digits(); + } + + configureHOTP.Execute(); + + // Return both Hex and Base32 representations of the key + WriteObject(new + { + HexSecret = powershellYK.support.Hex.Encode(_HOTPsecretKey.ToArray()), + Base32Secret = powershellYK.support.Base32.Encode(_HOTPsecretKey.ToArray()) + }); + break; + } + } + } + catch (Exception ex) { - switch (ParameterSetName) + // Show a message to guide the user into providing or correcting a slot access code + if (ex.Message.Contains("YubiKey Operation Failed") && ex.Message.Contains("state of non-volatile memory is unchanged")) + { + WriteWarning("The requested slot is protected with a slot access code. Either no access code was provided, or the provided code was incorrect. Please call the cmdlet again using -CurrentAccessCode with the correct code."); + } + else { - case "Yubico OTP": - // Configure Yubico OTP mode - Memory _publicID = new Memory(new byte[6]); - Memory _privateID = new Memory(new byte[6]); - Memory _secretKey = new Memory(new byte[16]); - ConfigureYubicoOtp configureyubicoOtp = otpSession.ConfigureYubicoOtp(Slot); - int? serial = YubiKeyModule._yubikey!.SerialNumber; - - // Handle Public ID configuration - if (PublicID is null) - { - configureyubicoOtp = configureyubicoOtp.UseSerialNumberAsPublicId(_publicID); - } - else - { - _publicID = PublicID; - configureyubicoOtp = configureyubicoOtp.UsePublicId(PublicID); - } - - // Handle Private ID configuration - if (PrivateID is null) - { - configureyubicoOtp = configureyubicoOtp.GeneratePrivateId(_privateID); - } - else - { - _privateID = PrivateID; - configureyubicoOtp = configureyubicoOtp.UsePublicId(PrivateID); - } - - // Handle Secret Key configuration - if (SecretKey is null) - { - configureyubicoOtp = configureyubicoOtp.GenerateKey(_secretKey); - } - else - { - _secretKey = SecretKey; - configureyubicoOtp = configureyubicoOtp.UseKey(SecretKey); - } - - configureyubicoOtp.Execute(); - - // Return configuration if any defaults were used - if (PublicID is null || PrivateID is null || SecretKey is null) - { - YubicoOTP retur = new YubicoOTP(serial, _publicID.ToArray(), _privateID.ToArray(), _secretKey.ToArray(), ""); - WriteObject(retur); - } - - // Handle YubiCloud upload - if (Upload.IsPresent) - { - // https://github.com/Yubico/yubikey-manager/blob/fbdae2bc12ba0451bcfc62372bc9191c10ecad0c/ykman/otp.py#L95 - // TODO: Implement Upload to YubiCloud - // @virot: upload is no longer supported. Need to output a CSV file for manual upload. - WriteWarning("Upload to YubiCloud is not implemented yet!"); - } - break; - - case "Static Password": - // Configure static password mode - ConfigureStaticPassword staticpassword = otpSession.ConfigureStaticPassword(Slot); - staticpassword = staticpassword.WithKeyboard(KeyboardLayout); - staticpassword = staticpassword.SetPassword((Marshal.PtrToStringUni(Marshal.SecureStringToGlobalAllocUnicode(Password!))!).AsMemory()); - if (AppendCarriageReturn.IsPresent) - { - staticpassword = staticpassword.AppendCarriageReturn(); - } - staticpassword.Execute(); - break; - - case "Static Generated Password": - // Configure static generated password mode - ConfigureStaticPassword staticgenpassword = otpSession.ConfigureStaticPassword(Slot); - Memory generatedPassword = new Memory(new char[PasswordLength]); - staticgenpassword = staticgenpassword.WithKeyboard(KeyboardLayout); - staticgenpassword = staticgenpassword.GeneratePassword(generatedPassword); - if (AppendCarriageReturn.IsPresent) - { - staticgenpassword = staticgenpassword.AppendCarriageReturn(); - } - staticgenpassword.Execute(); - break; - - case "ChallengeResponse": - // Configure challenge-response mode - Memory _CRsecretKey = new Memory(new byte[20]); - ConfigureChallengeResponse configureCR = otpSession.ConfigureChallengeResponse(Slot); - - // Handle Secret Key configuration - if (SecretKey is null) - { - configureCR = configureCR.GenerateKey(_CRsecretKey); - } - else - { - _CRsecretKey = SecretKey; - configureCR = configureCR.UseKey(SecretKey); - } - - // Configure touch requirement - if (RequireTouch.IsPresent) - { - configureCR = configureCR.UseButton(); - } - - // Configure algorithm - if (Algorithm == ChallengeResponseAlgorithm.HmacSha1) - { - configureCR = configureCR.UseHmacSha1(); - } - else if (Algorithm == ChallengeResponseAlgorithm.YubicoOtp) - { - configureCR = configureCR.UseYubiOtp(); - } - - configureCR.Execute(); - - // Return configuration if default key was used - if (SecretKey is null) - { - ChallangeResponse retur = new ChallangeResponse(_CRsecretKey.ToArray()); - WriteObject(retur); - } - break; - - case "HOTP": - // Configure HOTP mode - Memory _HOTPsecretKey = new Memory(new byte[20]); - ConfigureHotp configureHOTP = otpSession.ConfigureHotp(Slot); - - // Handle Secret Key configuration using Base32 - if (Base32Secret != null) - { - _HOTPsecretKey = powershellYK.support.Base32.Decode(Base32Secret); - configureHOTP = configureHOTP.UseKey(_HOTPsecretKey); - } - // Handle Secret Key configuration using Hex - else if (HexSecret != null) - { - _HOTPsecretKey = powershellYK.support.Hex.Decode(HexSecret); - configureHOTP = configureHOTP.UseKey(_HOTPsecretKey); - } - else if (SecretKey is null) - { - configureHOTP = configureHOTP.GenerateKey(_HOTPsecretKey); - } - else - { - _HOTPsecretKey = SecretKey; - configureHOTP = configureHOTP.UseKey(SecretKey); - } - - // Configure TAB before OTP if requested - if (SendTabFirst.IsPresent) - { - configureHOTP = configureHOTP.SendTabFirst(); - } - - // Configure carriage return if requested - if (AppendCarriageReturn.IsPresent) - { - configureHOTP = configureHOTP.AppendCarriageReturn(); - } - - // Configure 8 digits if requested - if (Use8Digits.IsPresent) - { - configureHOTP = configureHOTP.Use8Digits(); - } - - configureHOTP.Execute(); - - // Return both Hex and Base32 representations of the key - WriteObject(new - { - HexSecret = powershellYK.support.Hex.Encode(_HOTPsecretKey.ToArray()), - Base32Secret = powershellYK.support.Base32.Encode(_HOTPsecretKey.ToArray()) - }); - break; + WriteError(new ErrorRecord(ex, "SetYubiKeyOTPError", ErrorCategory.InvalidOperation, null)); } } } diff --git a/Module/Cmdlets/OTP/SetYubikeyOTPSlotAccessCode.cs b/Module/Cmdlets/OTP/SetYubikeyOTPSlotAccessCode.cs new file mode 100644 index 0000000..95d01e1 --- /dev/null +++ b/Module/Cmdlets/OTP/SetYubikeyOTPSlotAccessCode.cs @@ -0,0 +1,176 @@ +/// +/// Sets, changes or removes the OTP slot access code for a YubiKey. +/// The access code protects OTP slot configurations from unauthorized modifications. +/// Access codes are 6 bytes in length, provided as 12-character hex strings. +/// +/// .EXAMPLE +/// # Set a new access code for a slot (when no access code exists) +/// Set-YubiKeySlotAccessCode -Slot LongPress -AccessCode "010203040506" +/// +/// .EXAMPLE +/// # Change an existing slot access code +/// Set-YubiKeyOTPSlotAccessCode -Slot ShortPress -CurrentAccessCode "010203040506" -AccessCode "060504030201" +/// +/// .EXAMPLE +/// # Remove slot access code protection (set to all zeros) +/// Set-YubiKeyOTPSlotAccessCode -Slot LongPress -CurrentAccessCode "010203040506" -RemoveAccessCode +/// +/// .NOTES +/// Access codes must be provided as 12-character hex strings representing 6 bytes. +/// +/// + +// Imports +using System.Management.Automation; +using Yubico.YubiKey; +using Yubico.YubiKey.Otp; + +namespace powershellYK.Cmdlets.OTP +{ + [Cmdlet(VerbsCommon.Set, "YubiKeyOTPSlotAccessCode", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] + public class SetYubiKeySlotAccessCodeCmdlet : PSCmdlet + { + // Specifies which YubiKey OTP slot to configure (ShortPress or LongPress) + [Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Yubikey OTP Slot")] + public Slot Slot { get; set; } + + // The new access code to set (will be converted to bytes, max 6 bytes) + [Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "New access code (12-character hex string)", ParameterSetName = "SetNewAccessCode")] + [Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "New access code (12-character hex string)", ParameterSetName = "ChangeAccessCode")] + [ValidateCount(12, 12)] + public string? AccessCode { get; set; } + + // The current access code (required when changing or removing) + [Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Current access code (12-character hex string)", ParameterSetName = "ChangeAccessCode")] + [Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Current access code (12-character hex string)", ParameterSetName = "RemoveAccessCode")] + [ValidateCount(12, 12)] + public string? CurrentAccessCode { get; set; } + + // Flag to remove access code protection (set to all zeros) + [Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "Remove access code protection", ParameterSetName = "RemoveAccessCode")] + public SwitchParameter RemoveAccessCode { get; set; } + + // Initialize processing and verify requirements + protected override void BeginProcessing() + { + // Connect to YubiKey if not already connected + if (YubiKeyModule._yubikey is null) + { + WriteDebug("No YubiKey selected, calling Connect-Yubikey..."); + try + { + var myPowersShellInstance = PowerShell.Create(RunspaceMode.CurrentRunspace).AddCommand("Connect-Yubikey"); + if (this.MyInvocation.BoundParameters.ContainsKey("InformationAction")) + { + myPowersShellInstance = myPowersShellInstance.AddParameter("InformationAction", this.MyInvocation.BoundParameters["InformationAction"]); + } + myPowersShellInstance.Invoke(); + WriteDebug($"Successfully connected."); + } + catch (Exception e) + { + throw new Exception(e.Message, e); + } + } + + // Verify that the YubiKey supports OTP + var yk = (YubiKeyDevice)YubiKeyModule._yubikey!; + bool hasOtp = false; + if (yk.AvailableUsbCapabilities.HasFlag(YubiKeyCapabilities.Otp)) + { + hasOtp = true; + } + WriteDebug($"YubiKey OTP support: {hasOtp}"); + if (!hasOtp) + { + throw new Exception("The connected YubiKey does not support OTP functionality."); + } + } + + // Process the main cmdlet logic + protected override void ProcessRecord() + { + using (var otpSession = new OtpSession((YubiKeyDevice)YubiKeyModule._yubikey!)) + { + WriteDebug($"Working with parameter set: {ParameterSetName}"); + + // Convert string access codes to byte arrays if provided + byte[]? newAccessCodeBytes = null; + byte[]? currentAccessCodeBytes = null; + + // Helper for string to byte[] conversion + byte[] ConvertAccessCodeString(string code, string paramName) + { + // Convert hex string to byte array using Hex helper class + return powershellYK.support.Hex.Decode(code); + } + + if (AccessCode != null) + { + newAccessCodeBytes = ConvertAccessCodeString(AccessCode, nameof(AccessCode)); + } + else if (RemoveAccessCode.IsPresent) + { + // Set to all zeros to remove access code protection + newAccessCodeBytes = new byte[SlotAccessCode.MaxAccessCodeLength]; + } + + if (CurrentAccessCode != null) + { + currentAccessCodeBytes = ConvertAccessCodeString(CurrentAccessCode, nameof(CurrentAccessCode)); + } + + // Create SlotAccessCode objects + SlotAccessCode? newAccessCode = null; + SlotAccessCode? currentAccessCode = null; + + if (newAccessCodeBytes != null) + { + newAccessCode = new SlotAccessCode(newAccessCodeBytes); + } + + if (currentAccessCodeBytes != null) + { + currentAccessCode = new SlotAccessCode(currentAccessCodeBytes); + } + + // Confirm the operation if ShouldProcess is enabled + if (!ShouldProcess("Update the slot access code?", "Continue?", "Confirm")) + { + return; + } + + try + { + // Use UpdateSlot to change/remove access code without overwriting the key + var updateSlot = otpSession.UpdateSlot(Slot); + + if (currentAccessCode != null) + { + updateSlot = updateSlot.UseCurrentAccessCode(currentAccessCode); + } + + if (newAccessCode != null) + { + updateSlot = updateSlot.SetNewAccessCode(newAccessCode); + } + + updateSlot.Execute(); + WriteInformation("YubiKey slot access code operation completed.", new[] { "OTP", "Info" }); + } + catch (Exception ex) + { + // Show a meaningful message if the slot is already protected with a slot access code + if (ex.Message.Contains("YubiKey Operation Failed") && ex.Message.Contains("state of non-volatile memory is unchanged")) + { + WriteWarning("A slot access code is already set, call cmdlet again using -CurrentAccessCode."); + } + else + { + WriteError(new ErrorRecord(ex, "SetYubiKeySlotAccessCodeError", ErrorCategory.InvalidOperation, null)); + } + } + } + } + } +} \ No newline at end of file diff --git a/Module/Cmdlets/OTP/SwitchYubikeyOTP.cs b/Module/Cmdlets/OTP/SwitchYubikeyOTP.cs index 73da0ea..6ecb589 100644 --- a/Module/Cmdlets/OTP/SwitchYubikeyOTP.cs +++ b/Module/Cmdlets/OTP/SwitchYubikeyOTP.cs @@ -58,7 +58,15 @@ protected override void ProcessRecord() } catch (Exception ex) { - WriteError(new ErrorRecord(ex, "OTPSwapError", ErrorCategory.OperationStopped, null)); + // If either slot is protected with an access code show a meaningful error + if (ex.Message.Contains("Warning, state of non-volatile memory is unchanged.")) + { + WriteError(new ErrorRecord(new Exception("Either one or both slots are protected with a slot access code."), "OTPSwapAccessCodeError", ErrorCategory.SecurityError, null)); + } + else + { + WriteError(new ErrorRecord(ex, "OTPSwapError", ErrorCategory.OperationStopped, null)); + } } } } diff --git a/Module/powershellYK.psd1 b/Module/powershellYK.psd1 index 0213d7a..9f9303c 100644 --- a/Module/powershellYK.psd1 +++ b/Module/powershellYK.psd1 @@ -108,6 +108,7 @@ CmdletsToExport = @( 'Request-YubiKeyOTPChallange', 'Switch-YubiKeyOTP', 'Set-YubiKeyOTP', + 'Set-YubiKeyOTPSlotAccessCode', 'Assert-YubiKeyPIV', 'Block-YubiKeyPIV', 'Build-YubiKeyPIVCertificateSigningRequest', diff --git a/Module/support/Hex.cs b/Module/support/Hex.cs index 6156cfd..0f13b18 100644 --- a/Module/support/Hex.cs +++ b/Module/support/Hex.cs @@ -50,7 +50,14 @@ public static byte[] Decode(string hexString) for (int i = 0; i < result.Length; i++) { string hexPair = hexString.Substring(i * 2, 2); - result[i] = Convert.ToByte(hexPair, 16); + try + { + result[i] = Convert.ToByte(hexPair, 16); + } + catch (FormatException e) + { + throw new ArgumentException("The hex string contains invalid characters.", nameof(hexString), e); + } } return result;