From d5ed19e3bb4c6f9f769a2853f22c582bcf66e1d8 Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Fri, 20 Dec 2024 17:02:55 +0100 Subject: [PATCH] Use Google Java Style, reformat --- .../android/ExampleInstrumentedTest.java | 22 +- .../yubikit/android/YubiKitManager.java | 158 +- .../android/internal/Base64CodecImpl.java | 39 +- .../android/internal/package-info.java | 2 +- .../yubico/yubikit/android/package-info.java | 2 +- .../transport/nfc/NfcConfiguration.java | 132 +- .../android/transport/nfc/NfcDispatcher.java | 10 +- .../transport/nfc/NfcNotAvailable.java | 29 +- .../transport/nfc/NfcReaderDispatcher.java | 83 +- .../transport/nfc/NfcSmartCardConnection.java | 90 +- .../transport/nfc/NfcYubiKeyDevice.java | 273 +- .../transport/nfc/NfcYubiKeyManager.java | 152 +- .../android/transport/nfc/package-info.java | 2 +- .../android/transport/package-info.java | 2 +- .../transport/usb/NoPermissionsException.java | 19 +- .../transport/usb/UsbConfiguration.java | 37 +- .../transport/usb/UsbDeviceManager.java | 377 +-- .../transport/usb/UsbYubiKeyDevice.java | 342 +- .../transport/usb/UsbYubiKeyManager.java | 165 +- .../usb/connection/ConnectionHandler.java | 9 +- .../usb/connection/ConnectionManager.java | 138 +- .../usb/connection/FidoConnectionHandler.java | 87 +- .../InterfaceConnectionHandler.java | 71 +- .../usb/connection/OtpConnectionHandler.java | 17 +- .../SmartCardConnectionHandler.java | 53 +- .../usb/connection/UsbFidoConnection.java | 56 +- .../usb/connection/UsbOtpConnection.java | 126 +- .../connection/UsbSmartCardConnection.java | 521 ++-- .../usb/connection/UsbYubiKeyConnection.java | 52 +- .../usb/connection/package-info.java | 2 +- .../android/transport/usb/package-info.java | 2 +- .../yubikit/android/ui/OtpActivity.java | 136 +- .../yubikit/android/ui/OtpKeyListener.java | 118 +- .../android/ui/YubiKeyPromptAction.java | 63 +- .../android/ui/YubiKeyPromptActivity.java | 538 ++-- .../ui/YubiKeyPromptConnectionAction.java | 125 +- .../yubikit/android/ui/package-info.java | 2 +- .../yubikit/android/YubikitManagerTest.java | 245 +- .../UsbSmartCardConnectionTest.java | 199 +- .../groovy/project-convention-spotless.gradle | 1 - .../java/com/yubico/yubikit/core/Logger.java | 98 +- .../yubikit/core/PackageNonnullByDefault.java | 13 +- .../com/yubico/yubikit/core/Transport.java | 16 +- .../com/yubico/yubikit/core/UsbInterface.java | 68 +- .../java/com/yubico/yubikit/core/UsbPid.java | 70 +- .../java/com/yubico/yubikit/core/Version.java | 204 +- .../yubikit/core/YubiKeyConnection.java | 7 +- .../yubico/yubikit/core/YubiKeyDevice.java | 34 +- .../com/yubico/yubikit/core/YubiKeyType.java | 18 +- .../ApplicationNotAvailableException.java | 16 +- .../core/application/ApplicationSession.java | 45 +- .../application/BadResponseException.java | 16 +- .../core/application/CommandException.java | 16 +- .../core/application/CommandState.java | 69 +- .../yubikit/core/application/Feature.java | 82 +- .../core/application/InvalidPinException.java | 26 +- .../application/SessionVersionOverride.java | 50 +- .../core/application/TimeoutException.java | 16 +- .../core/application/package-info.java | 2 +- .../yubikit/core/fido/CtapException.java | 138 +- .../yubikit/core/fido/FidoConnection.java | 19 +- .../yubikit/core/fido/FidoProtocol.java | 228 +- .../yubikit/core/fido/package-info.java | 2 +- .../yubico/yubikit/core/internal/Logger.java | 417 ++- .../yubikit/core/internal/codec/Base64.java | 74 +- .../core/internal/codec/Base64Codec.java | 48 +- .../internal/codec/DefaultBase64Codec.java | 34 +- .../core/internal/codec/package-info.java | 2 +- .../yubikit/core/internal/package-info.java | 2 +- .../core/keys/EllipticCurveValues.java | 102 +- .../yubikit/core/keys/PrivateKeyValues.java | 425 +-- .../yubikit/core/keys/PublicKeyValues.java | 453 +-- .../yubikit/core/keys/package-info.java | 2 +- .../yubikit/core/otp/ChecksumUtils.java | 76 +- .../core/otp/CommandRejectedException.java | 10 +- .../com/yubico/yubikit/core/otp/Modhex.java | 85 +- .../yubikit/core/otp/OtpConnection.java | 35 +- .../yubico/yubikit/core/otp/OtpProtocol.java | 404 +-- .../yubico/yubikit/core/otp/package-info.java | 2 +- .../com/yubico/yubikit/core/package-info.java | 1 - .../yubico/yubikit/core/smartcard/Apdu.java | 182 +- .../yubikit/core/smartcard/ApduException.java | 46 +- .../yubikit/core/smartcard/ApduFormat.java | 7 +- .../core/smartcard/ApduFormatProcessor.java | 32 +- .../yubikit/core/smartcard/ApduProcessor.java | 3 +- .../yubikit/core/smartcard/ApduResponse.java | 62 +- .../yubico/yubikit/core/smartcard/AppId.java | 19 +- .../smartcard/ChainedResponseProcessor.java | 64 +- .../core/smartcard/ExtendedApduProcessor.java | 53 +- .../yubikit/core/smartcard/MaxApduSize.java | 12 +- .../com/yubico/yubikit/core/smartcard/SW.java | 46 +- .../yubikit/core/smartcard/ScpProcessor.java | 72 +- .../core/smartcard/ShortApduProcessor.java | 100 +- .../core/smartcard/SmartCardConnection.java | 63 +- .../core/smartcard/SmartCardProtocol.java | 395 +-- .../smartcard/TouchWorkaroundProcessor.java | 37 +- .../yubikit/core/smartcard/package-info.java | 2 +- .../core/smartcard/scp/DataEncryptor.java | 6 +- .../yubikit/core/smartcard/scp/KeyRef.java | 81 +- .../core/smartcard/scp/Scp03KeyParams.java | 36 +- .../core/smartcard/scp/Scp11KeyParams.java | 82 +- .../core/smartcard/scp/ScpKeyParams.java | 12 +- .../yubikit/core/smartcard/scp/ScpKid.java | 14 +- .../yubikit/core/smartcard/scp/ScpState.java | 606 ++-- .../smartcard/scp/SecurityDomainSession.java | 870 +++--- .../core/smartcard/scp/SessionKeys.java | 24 +- .../core/smartcard/scp/StaticKeys.java | 97 +- .../core/smartcard/scp/package-info.java | 2 +- .../yubico/yubikit/core/util/ByteUtils.java | 46 +- .../yubico/yubikit/core/util/Callback.java | 2 +- .../yubico/yubikit/core/util/NdefUtils.java | 91 +- .../com/yubico/yubikit/core/util/Pair.java | 16 +- .../yubico/yubikit/core/util/RandomUtils.java | 37 +- .../com/yubico/yubikit/core/util/Result.java | 113 +- .../yubico/yubikit/core/util/StringUtils.java | 56 +- .../com/yubico/yubikit/core/util/Tlv.java | 213 +- .../com/yubico/yubikit/core/util/Tlvs.java | 153 +- .../yubikit/core/util/package-info.java | 2 +- .../com/yubico/yubikit/core/VersionTest.java | 96 +- .../core/keys/PrivateKeyValuesTest.java | 32 +- .../core/keys/PublicKeyValuesTest.java | 104 +- .../yubikit/core/otp/ChecksumUtilsTest.java | 110 +- .../yubico/yubikit/core/otp/ModhexTest.java | 56 +- .../yubikit/core/smartcard/ApduTest.java | 24 +- .../yubico/yubikit/core/util/TlvsTest.java | 61 +- .../java/com/yubico/yubikit/fido/Cbor.java | 426 ++- .../java/com/yubico/yubikit/fido/Cose.java | 172 +- .../fido/client/BasicWebAuthnClient.java | 1568 +++++----- .../yubikit/fido/client/ClientError.java | 154 +- .../fido/client/CredentialManager.java | 219 +- .../client/MultipleAssertionsAvailable.java | 136 +- .../fido/client/PinInvalidClientError.java | 21 +- .../fido/client/PinRequiredClientError.java | 13 +- .../UserInformationNotAvailableError.java | 12 +- .../client/extensions/CredBlobExtension.java | 89 +- .../client/extensions/CredPropsExtension.java | 62 +- .../extensions/CredProtectExtension.java | 95 +- .../fido/client/extensions/Extension.java | 329 +- .../extensions/HmacSecretExtension.java | 582 ++-- .../client/extensions/LargeBlobExtension.java | 292 +- .../fido/client/extensions/LargeBlobs.java | 603 ++-- .../extensions/MinPinLengthExtension.java | 60 +- .../fido/client/extensions/package-info.java | 6 +- .../yubikit/fido/client/package-info.java | 6 +- .../yubikit/fido/ctap/BioEnrollment.java | 81 +- .../yubico/yubikit/fido/ctap/ClientPin.java | 691 ++--- .../com/yubico/yubikit/fido/ctap/Config.java | 317 +- .../fido/ctap/CredentialManagement.java | 529 ++-- .../yubikit/fido/ctap/Ctap2Session.java | 2233 +++++++------- .../fido/ctap/FingerprintBioEnrollment.java | 808 +++-- .../com/yubico/yubikit/fido/ctap/Hkdf.java | 84 +- .../fido/ctap/PinUvAuthDummyProtocol.java | 66 +- .../yubikit/fido/ctap/PinUvAuthProtocol.java | 83 +- .../fido/ctap/PinUvAuthProtocolV1.java | 258 +- .../fido/ctap/PinUvAuthProtocolV2.java | 224 +- .../UnsupportedPinUvAuthProtocolError.java | 6 +- .../yubico/yubikit/fido/ctap/UserVerify.java | 48 +- .../yubikit/fido/ctap/package-info.java | 10 +- .../com/yubico/yubikit/fido/package-info.java | 6 +- .../AttestationConveyancePreference.java | 8 +- .../fido/webauthn/AttestationObject.java | 199 +- .../fido/webauthn/AttestedCredentialData.java | 109 +- .../AuthenticatorAssertionResponse.java | 146 +- .../webauthn/AuthenticatorAttachment.java | 4 +- .../AuthenticatorAttestationResponse.java | 277 +- .../fido/webauthn/AuthenticatorData.java | 315 +- .../fido/webauthn/AuthenticatorResponse.java | 18 +- .../AuthenticatorSelectionCriteria.java | 153 +- .../ClientExtensionResultProvider.java | 2 +- .../fido/webauthn/ClientExtensionResults.java | 20 +- .../yubikit/fido/webauthn/Credential.java | 42 +- .../yubikit/fido/webauthn/Extensions.java | 67 +- .../fido/webauthn/PublicKeyCredential.java | 373 ++- .../PublicKeyCredentialCreationOptions.java | 292 +- .../PublicKeyCredentialDescriptor.java | 143 +- .../webauthn/PublicKeyCredentialEntity.java | 16 +- .../PublicKeyCredentialParameters.java | 82 +- .../PublicKeyCredentialRequestOptions.java | 248 +- .../webauthn/PublicKeyCredentialRpEntity.java | 71 +- .../webauthn/PublicKeyCredentialType.java | 2 +- .../PublicKeyCredentialUserEntity.java | 86 +- .../fido/webauthn/ResidentKeyRequirement.java | 6 +- .../fido/webauthn/SerializationType.java | 6 +- .../fido/webauthn/SerializationUtils.java | 42 +- .../webauthn/UserVerificationRequirement.java | 6 +- .../yubikit/fido/webauthn/package-info.java | 8 +- .../com/yubico/yubikit/fido/CborTest.java | 468 ++- .../com/yubico/yubikit/fido/CoseTest.java | 336 +- .../com/yubico/yubikit/fido/TestUtils.java | 22 +- .../client/BasicWebAuthnClientUtilsTest.java | 709 ++--- .../yubikit/fido/ctap/ClientPinTest.java | 80 +- .../yubico/yubikit/fido/ctap/HkdfTest.java | 215 +- .../fido/ctap/PinUvAuthProtocolV1Test.java | 160 +- .../fido/ctap/PinUvAuthProtocolV2Test.java | 187 +- .../fido/webauthn/SerializationTest.java | 718 ++--- .../yubico/yubikit/management/Capability.java | 53 +- .../yubikit/management/DeviceConfig.java | 411 ++- .../yubico/yubikit/management/DeviceInfo.java | 924 +++--- .../yubico/yubikit/management/FormFactor.java | 79 +- .../yubikit/management/ManagementSession.java | 756 ++--- .../yubikit/management/UsbInterface.java | 81 +- .../yubikit/management/package-info.java | 2 +- .../management/DeviceConfigBuilderTest.java | 58 +- .../yubikit/management/DeviceConfigTest.java | 60 +- .../management/DeviceInfoBuilderTest.java | 198 +- .../yubikit/management/DeviceInfoTest.java | 293 +- .../yubico/yubikit/management/TestUtil.java | 52 +- .../com/yubico/yubikit/oath/AccessKey.java | 22 +- .../java/com/yubico/yubikit/oath/Base32.java | 240 +- .../java/com/yubico/yubikit/oath/Code.java | 51 +- .../com/yubico/yubikit/oath/Credential.java | 261 +- .../yubico/yubikit/oath/CredentialData.java | 495 ++- .../yubikit/oath/CredentialIdUtils.java | 135 +- .../yubico/yubikit/oath/HashAlgorithm.java | 86 +- .../com/yubico/yubikit/oath/OathSession.java | 1482 ++++----- .../com/yubico/yubikit/oath/OathType.java | 60 +- .../yubikit/oath/ParseUriException.java | 12 +- .../com/yubico/yubikit/oath/package-info.java | 2 +- .../com/yubico/yubikit/oath/Base32Test.java | 253 +- .../yubikit/oath/CredentialDataTest.java | 107 +- .../yubikit/oath/CredentialIdUtilsTest.java | 101 +- .../yubikit/openpgp/AlgorithmAttributes.java | 384 +-- .../openpgp/ApplicationRelatedData.java | 131 +- .../openpgp/CardholderRelatedData.java | 58 +- .../java/com/yubico/yubikit/openpgp/Crt.java | 9 +- .../openpgp/DiscretionaryDataObjects.java | 337 +- .../java/com/yubico/yubikit/openpgp/Do.java | 84 +- .../yubikit/openpgp/ExtendedCapabilities.java | 124 +- .../openpgp/ExtendedCapabilityFlag.java | 24 +- .../yubikit/openpgp/ExtendedLengthInfo.java | 52 +- .../openpgp/GeneralFeatureManagement.java | 24 +- .../java/com/yubico/yubikit/openpgp/Kdf.java | 377 ++- .../com/yubico/yubikit/openpgp/KeyRef.java | 116 +- .../com/yubico/yubikit/openpgp/KeyStatus.java | 30 +- .../yubico/yubikit/openpgp/OpenPgpAid.java | 91 +- .../yubico/yubikit/openpgp/OpenPgpCurve.java | 77 +- .../yubikit/openpgp/OpenPgpSession.java | 2381 +++++++------- .../yubico/yubikit/openpgp/OpenPgpUtils.java | 14 +- .../com/yubico/yubikit/openpgp/PinPolicy.java | 22 +- .../yubikit/openpgp/PrivateKeyTemplate.java | 249 +- .../java/com/yubico/yubikit/openpgp/Pw.java | 22 +- .../com/yubico/yubikit/openpgp/PwStatus.java | 133 +- .../openpgp/SecuritySupportTemplate.java | 34 +- .../java/com/yubico/yubikit/openpgp/Uif.java | 72 +- .../yubico/yubikit/openpgp/package-info.java | 2 +- .../com/yubico/yubikit/piv/BioMetadata.java | 78 +- .../com/yubico/yubikit/piv/GzipUtils.java | 76 +- .../yubikit/piv/InvalidPinException.java | 6 +- .../java/com/yubico/yubikit/piv/KeyType.java | 270 +- .../yubikit/piv/ManagementKeyMetadata.java | 71 +- .../yubico/yubikit/piv/ManagementKeyType.java | 62 +- .../java/com/yubico/yubikit/piv/ObjectId.java | 100 +- .../java/com/yubico/yubikit/piv/Padding.java | 191 +- .../com/yubico/yubikit/piv/PinMetadata.java | 58 +- .../com/yubico/yubikit/piv/PinPolicy.java | 62 +- .../com/yubico/yubikit/piv/PivSession.java | 2747 +++++++++-------- .../java/com/yubico/yubikit/piv/Slot.java | 120 +- .../com/yubico/yubikit/piv/SlotMetadata.java | 115 +- .../com/yubico/yubikit/piv/TouchPolicy.java | 62 +- .../piv/jca/PivAlgorithmParameterSpec.java | 58 +- .../yubico/yubikit/piv/jca/PivCipherSpi.java | 277 +- .../yubikit/piv/jca/PivEcSignatureSpi.java | 212 +- .../yubikit/piv/jca/PivKeyAgreementSpi.java | 153 +- .../yubico/yubikit/piv/jca/PivKeyManager.java | 61 +- .../piv/jca/PivKeyPairGeneratorSpi.java | 108 +- .../piv/jca/PivKeyStoreKeyParameters.java | 13 +- .../yubikit/piv/jca/PivKeyStoreSpi.java | 562 ++-- .../yubico/yubikit/piv/jca/PivPrivateKey.java | 349 ++- .../yubico/yubikit/piv/jca/PivProvider.java | 350 ++- .../yubikit/piv/jca/PivRsaSignatureSpi.java | 240 +- .../yubico/yubikit/piv/jca/package-info.java | 2 +- .../com/yubico/yubikit/piv/package-info.java | 2 +- .../com/yubico/yubikit/piv/GzipUtilsTest.java | 96 +- .../com/yubico/yubikit/piv/KeyTypeTest.java | 128 +- .../com/yubico/yubikit/piv/PaddingTest.java | 93 +- .../yubico/yubikit/piv/PivProviderTest.java | 35 +- .../yubico/yubikit/support/DeviceUtil.java | 925 +++--- .../yubico/yubikit/support/package-info.java | 2 +- .../yubikit/support/AdjustDeviceInfoTest.java | 796 ++--- .../yubikit/support/DeviceUtilTest.java | 33 +- .../yubico/yubikit/support/GetNameTest.java | 1063 ++++--- .../com/yubico/yubikit/support/TestUtil.java | 32 +- .../com/yubico/yubikit/testing/Codec.java | 6 +- .../yubico/yubikit/testing/ScpParameters.java | 92 +- .../com/yubico/yubikit/testing/TestState.java | 255 +- .../fido/BasicWebAuthnClientTests.java | 1439 +++++---- .../testing/fido/Ctap2BioEnrollmentTests.java | 185 +- .../testing/fido/Ctap2ClientPinTests.java | 131 +- .../testing/fido/Ctap2ConfigTests.java | 96 +- .../fido/Ctap2CredentialManagementTests.java | 262 +- .../testing/fido/Ctap2SessionTests.java | 188 +- .../fido/EnterpriseAttestationTests.java | 340 +- .../yubikit/testing/fido/FidoTestState.java | 372 ++- .../yubico/yubikit/testing/fido/TestData.java | 112 +- .../extensions/CredBlobExtensionTests.java | 228 +- .../extensions/CredPropsExtensionTests.java | 110 +- .../extensions/CredProtectExtensionTests.java | 135 +- .../extensions/HmacSecretExtensionTests.java | 445 +-- .../extensions/LargeBlobExtensionTests.java | 335 +- .../MinPinLengthExtensionTests.java | 116 +- .../fido/extensions/PrfExtensionTests.java | 522 ++-- .../testing/fido/utils/ClientHelper.java | 111 +- .../testing/fido/utils/ConfigHelper.java | 15 +- .../fido/utils/CreationOptionsBuilder.java | 163 +- .../fido/utils/RequestOptionsBuilder.java | 89 +- .../yubikit/testing/fido/utils/TestData.java | 112 +- .../management/ManagementDeviceTests.java | 15 +- .../yubikit/testing/mpe/MpeTestState.java | 95 +- .../mpe/MultiProtocolResetDeviceTests.java | 64 +- .../yubikit/testing/oath/OathDeviceTests.java | 160 +- .../yubikit/testing/oath/OathTestState.java | 129 +- .../testing/openpgp/OpenPgpDeviceTests.java | 1108 +++---- .../testing/openpgp/OpenPgpTestState.java | 163 +- .../piv/PivBioMultiProtocolDeviceTests.java | 52 +- .../testing/piv/PivCertificateTests.java | 74 +- .../yubikit/testing/piv/PivDeviceTests.java | 268 +- .../testing/piv/PivJcaDecryptTests.java | 132 +- .../testing/piv/PivJcaDeviceTests.java | 287 +- .../testing/piv/PivJcaSigningTests.java | 367 ++- .../yubikit/testing/piv/PivJcaUtils.java | 59 +- .../yubikit/testing/piv/PivMoveKeyTests.java | 143 +- .../piv/PivPinComplexityDeviceTests.java | 83 +- .../yubikit/testing/piv/PivTestState.java | 204 +- .../yubikit/testing/piv/PivTestUtils.java | 826 ++--- .../yubikit/testing/sd/Scp03DeviceTests.java | 223 +- .../yubikit/testing/sd/Scp11DeviceTests.java | 473 +-- .../yubikit/testing/sd/Scp11TestData.java | 136 +- .../yubikit/testing/sd/ScpCertificates.java | 154 +- .../testing/sd/SecurityDomainTestState.java | 144 +- .../yubiotp/BaseSlotConfiguration.java | 251 +- .../yubikit/yubiotp/ConfigurationState.java | 111 +- .../yubiotp/HmacSha1SlotConfiguration.java | 108 +- .../yubiotp/HotpSlotConfiguration.java | 127 +- .../yubiotp/KeyboardSlotConfiguration.java | 99 +- .../java/com/yubico/yubikit/yubiotp/Slot.java | 46 +- .../yubikit/yubiotp/SlotConfiguration.java | 272 +- .../StaticPasswordSlotConfiguration.java | 50 +- .../StaticTicketSlotConfiguration.java | 129 +- .../yubikit/yubiotp/UpdateConfiguration.java | 130 +- .../yubikit/yubiotp/YubiOtpSession.java | 1021 +++--- .../yubiotp/YubiOtpSlotConfiguration.java | 108 +- .../yubico/yubikit/yubiotp/package-info.java | 2 +- .../yubiotp/BaseSlotConfigurationTest.java | 40 +- 343 files changed, 31474 insertions(+), 31051 deletions(-) diff --git a/android/src/androidTest/java/com/yubico/yubikit/android/ExampleInstrumentedTest.java b/android/src/androidTest/java/com/yubico/yubikit/android/ExampleInstrumentedTest.java index 47cda267..439b0690 100755 --- a/android/src/androidTest/java/com/yubico/yubikit/android/ExampleInstrumentedTest.java +++ b/android/src/androidTest/java/com/yubico/yubikit/android/ExampleInstrumentedTest.java @@ -16,16 +16,14 @@ package com.yubico.yubikit.android; -import android.content.Context; +import static org.junit.Assert.assertEquals; -import androidx.test.platform.app.InstrumentationRegistry; +import android.content.Context; import androidx.test.ext.junit.runners.AndroidJUnit4; - +import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Test; import org.junit.runner.RunWith; -import static org.junit.Assert.assertEquals; - /** * Instrumented test, which will execute on an Android device. * @@ -33,10 +31,10 @@ */ @RunWith(AndroidJUnit4.class) public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - assertEquals("com.yubico.yubikit.android.test", appContext.getPackageName()); - } -} \ No newline at end of file + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.yubico.yubikit.android.test", appContext.getPackageName()); + } +} diff --git a/android/src/main/java/com/yubico/yubikit/android/YubiKitManager.java b/android/src/main/java/com/yubico/yubikit/android/YubiKitManager.java index 6ca7b3c8..42eca070 100755 --- a/android/src/main/java/com/yubico/yubikit/android/YubiKitManager.java +++ b/android/src/main/java/com/yubico/yubikit/android/YubiKitManager.java @@ -18,7 +18,6 @@ import android.app.Activity; import android.content.Context; - import com.yubico.yubikit.android.transport.nfc.NfcConfiguration; import com.yubico.yubikit.android.transport.nfc.NfcNotAvailable; import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice; @@ -27,96 +26,99 @@ import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice; import com.yubico.yubikit.android.transport.usb.UsbYubiKeyManager; import com.yubico.yubikit.core.util.Callback; - import javax.annotation.Nullable; /** - * Starting point for YubiKey device discovery over both USB and NFC. - * Use this class to listen for YubiKeys and get a {@link com.yubico.yubikit.core.YubiKeyDevice} reference. + * Starting point for YubiKey device discovery over both USB and NFC. Use this class to listen for + * YubiKeys and get a {@link com.yubico.yubikit.core.YubiKeyDevice} reference. */ public final class YubiKitManager { - private final UsbYubiKeyManager usbYubiKeyManager; - @Nullable - private final NfcYubiKeyManager nfcYubiKeyManager; + private final UsbYubiKeyManager usbYubiKeyManager; + @Nullable private final NfcYubiKeyManager nfcYubiKeyManager; - @Nullable - private static NfcYubiKeyManager buildNfcDeviceManager(Context context) { - try { - return new NfcYubiKeyManager(context, null); - } catch (NfcNotAvailable e) { - return null; - } + @Nullable private static NfcYubiKeyManager buildNfcDeviceManager(Context context) { + try { + return new NfcYubiKeyManager(context, null); + } catch (NfcNotAvailable e) { + return null; } + } - /** - * Initialize instance of {@link YubiKitManager} - * - * @param context application context - */ - public YubiKitManager(Context context) { - this(new UsbYubiKeyManager(context.getApplicationContext()), buildNfcDeviceManager(context.getApplicationContext())); - } + /** + * Initialize instance of {@link YubiKitManager} + * + * @param context application context + */ + public YubiKitManager(Context context) { + this( + new UsbYubiKeyManager(context.getApplicationContext()), + buildNfcDeviceManager(context.getApplicationContext())); + } - /** - * Initialize an instance of {@link YubiKitManager}, providing the USB and NFC YubiKey managers to use for device discovery. - * - * @param usbYubiKeyManager UsbYubiKeyManager instance to use for USB communication - * @param nfcYubiKeyManager NfcYubiKeyManager instance to use for NFC communication - */ - public YubiKitManager(UsbYubiKeyManager usbYubiKeyManager, @Nullable NfcYubiKeyManager nfcYubiKeyManager) { - this.usbYubiKeyManager = usbYubiKeyManager; - this.nfcYubiKeyManager = nfcYubiKeyManager; - } + /** + * Initialize an instance of {@link YubiKitManager}, providing the USB and NFC YubiKey managers to + * use for device discovery. + * + * @param usbYubiKeyManager UsbYubiKeyManager instance to use for USB communication + * @param nfcYubiKeyManager NfcYubiKeyManager instance to use for NFC communication + */ + public YubiKitManager( + UsbYubiKeyManager usbYubiKeyManager, @Nullable NfcYubiKeyManager nfcYubiKeyManager) { + this.usbYubiKeyManager = usbYubiKeyManager; + this.nfcYubiKeyManager = nfcYubiKeyManager; + } + /** + * Subscribe on changes that happen via USB and detect if there any Yubikeys got connected + * + *

This registers broadcast receivers, to unsubscribe from receiver use {@link + * YubiKitManager#stopUsbDiscovery()} + * + * @param usbConfiguration additional configurations on how USB discovery should be handled + * @param listener listener that is going to be invoked upon successful discovery of key session + * or failure to detect any session (lack of permissions) + */ + public void startUsbDiscovery( + final UsbConfiguration usbConfiguration, Callback listener) { + usbYubiKeyManager.enable(usbConfiguration, listener); + } - /** - * Subscribe on changes that happen via USB and detect if there any Yubikeys got connected - *

- * This registers broadcast receivers, to unsubscribe from receiver use {@link YubiKitManager#stopUsbDiscovery()} - * - * @param usbConfiguration additional configurations on how USB discovery should be handled - * @param listener listener that is going to be invoked upon successful discovery of key session - * or failure to detect any session (lack of permissions) - */ - public void startUsbDiscovery(final UsbConfiguration usbConfiguration, Callback listener) { - usbYubiKeyManager.enable(usbConfiguration, listener); + /** + * Subscribe on changes that happen via NFC and detect if there any Yubikeys tags got passed + * + *

This registers broadcast receivers and blocks Ndef tags to be passed to activity, to + * unsubscribe use {@link YubiKitManager#stopNfcDiscovery(Activity)} + * + * @param nfcConfiguration additional configurations on how NFC discovery should be handled + * @param listener listener that is going to be invoked upon successful discovery of YubiKeys or + * failure to detect any device (setting if off or no nfc adapter on device) + * @param activity active (not finished) activity required for nfc foreground dispatch + * @throws NfcNotAvailable in case if NFC not available on android device + */ + public void startNfcDiscovery( + final NfcConfiguration nfcConfiguration, + Activity activity, + Callback listener) + throws NfcNotAvailable { + if (nfcYubiKeyManager == null) { + throw new NfcNotAvailable("NFC is not available on this device", false); } + nfcYubiKeyManager.enable(activity, nfcConfiguration, listener); + } - /** - * Subscribe on changes that happen via NFC and detect if there any Yubikeys tags got passed - *

- * This registers broadcast receivers and blocks Ndef tags to be passed to activity, - * to unsubscribe use {@link YubiKitManager#stopNfcDiscovery(Activity)} - * - * @param nfcConfiguration additional configurations on how NFC discovery should be handled - * @param listener listener that is going to be invoked upon successful discovery of YubiKeys - * or failure to detect any device (setting if off or no nfc adapter on device) - * @param activity active (not finished) activity required for nfc foreground dispatch - * @throws NfcNotAvailable in case if NFC not available on android device - */ - public void startNfcDiscovery(final NfcConfiguration nfcConfiguration, Activity activity, Callback listener) - throws NfcNotAvailable { - if (nfcYubiKeyManager == null) { - throw new NfcNotAvailable("NFC is not available on this device", false); - } - nfcYubiKeyManager.enable(activity, nfcConfiguration, listener); - } - - /** - * Unsubscribe from changes that happen via USB - */ - public void stopUsbDiscovery() { - usbYubiKeyManager.disable(); - } + /** Unsubscribe from changes that happen via USB */ + public void stopUsbDiscovery() { + usbYubiKeyManager.disable(); + } - /** - * Unsubscribe from changes that happen via NFC - * - * @param activity active (not finished) activity required for nfc foreground dispatch - */ - public void stopNfcDiscovery(Activity activity) { - if (nfcYubiKeyManager != null) { - nfcYubiKeyManager.disable(activity); - } + /** + * Unsubscribe from changes that happen via NFC + * + * @param activity active (not finished) activity required for nfc foreground dispatch + */ + public void stopNfcDiscovery(Activity activity) { + if (nfcYubiKeyManager != null) { + nfcYubiKeyManager.disable(activity); } + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/internal/Base64CodecImpl.java b/android/src/main/java/com/yubico/yubikit/android/internal/Base64CodecImpl.java index 92bb4a5c..50da4c08 100644 --- a/android/src/main/java/com/yubico/yubikit/android/internal/Base64CodecImpl.java +++ b/android/src/main/java/com/yubico/yubikit/android/internal/Base64CodecImpl.java @@ -20,24 +20,23 @@ public class Base64CodecImpl implements com.yubico.yubikit.core.internal.codec.Base64Codec { - @Override - public String toUrlSafeString(byte[] data) { - return Base64.encodeToString(data, Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE); - } - - @Override - public String toString(byte[] data) { - return Base64.encodeToString(data, Base64.NO_WRAP | Base64.NO_PADDING); - } - - @Override - public byte[] fromUrlSafeString(String data) { - return Base64.decode(data, Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE); - } - - @Override - public byte[] fromString(String data) { - return Base64.decode(data, Base64.NO_WRAP | Base64.NO_PADDING); - } + @Override + public String toUrlSafeString(byte[] data) { + return Base64.encodeToString(data, Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE); + } + + @Override + public String toString(byte[] data) { + return Base64.encodeToString(data, Base64.NO_WRAP | Base64.NO_PADDING); + } + + @Override + public byte[] fromUrlSafeString(String data) { + return Base64.decode(data, Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE); + } + + @Override + public byte[] fromString(String data) { + return Base64.decode(data, Base64.NO_WRAP | Base64.NO_PADDING); + } } - diff --git a/android/src/main/java/com/yubico/yubikit/android/internal/package-info.java b/android/src/main/java/com/yubico/yubikit/android/internal/package-info.java index f35e99b3..3ef8275e 100755 --- a/android/src/main/java/com/yubico/yubikit/android/internal/package-info.java +++ b/android/src/main/java/com/yubico/yubikit/android/internal/package-info.java @@ -17,4 +17,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.android.internal; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/android/src/main/java/com/yubico/yubikit/android/package-info.java b/android/src/main/java/com/yubico/yubikit/android/package-info.java index bc840d40..a4f0bb40 100755 --- a/android/src/main/java/com/yubico/yubikit/android/package-info.java +++ b/android/src/main/java/com/yubico/yubikit/android/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.android; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcConfiguration.java b/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcConfiguration.java index a54809a1..a0931e3a 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcConfiguration.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcConfiguration.java @@ -15,85 +15,81 @@ */ package com.yubico.yubikit.android.transport.nfc; -/** - * Additional configurations for NFC discovery - */ +/** Additional configurations for NFC discovery */ public class NfcConfiguration { - // system sound that emitted when tag is discovered - private boolean disableNfcDiscoverySound = false; - - // skip ndef check for discovered tag - private boolean skipNdefCheck = false; + // system sound that emitted when tag is discovered + private boolean disableNfcDiscoverySound = false; - // show settings dialog in case if NFC setting is not enabled - private boolean handleUnavailableNfc = false; + // skip ndef check for discovered tag + private boolean skipNdefCheck = false; - private int timeout = 1000; + // show settings dialog in case if NFC setting is not enabled + private boolean handleUnavailableNfc = false; + private int timeout = 1000; - public boolean isDisableNfcDiscoverySound() { - return disableNfcDiscoverySound; - } + public boolean isDisableNfcDiscoverySound() { + return disableNfcDiscoverySound; + } - public boolean isSkipNdefCheck() { - return skipNdefCheck; - } + public boolean isSkipNdefCheck() { + return skipNdefCheck; + } - public boolean isHandleUnavailableNfc() { - return handleUnavailableNfc; - } + public boolean isHandleUnavailableNfc() { + return handleUnavailableNfc; + } - public int getTimeout() { - return timeout; - } + public int getTimeout() { + return timeout; + } - /** - * Setting this flag allows the caller to prevent the - * platform from playing sounds when it discovers a tag. - * - * @param disableNfcDiscoverySound new value of this property - * @return configuration object - */ - public NfcConfiguration disableNfcDiscoverySound(boolean disableNfcDiscoverySound) { - this.disableNfcDiscoverySound = disableNfcDiscoverySound; - return this; - } + /** + * Setting this flag allows the caller to prevent the platform from playing sounds when it + * discovers a tag. + * + * @param disableNfcDiscoverySound new value of this property + * @return configuration object + */ + public NfcConfiguration disableNfcDiscoverySound(boolean disableNfcDiscoverySound) { + this.disableNfcDiscoverySound = disableNfcDiscoverySound; + return this; + } - /** - * Setting this flag allows the caller to prevent the - * platform from performing an NDEF check on the tags it - * - * @param skipNdefCheck new value of this property - * @return configuration object - */ - public NfcConfiguration skipNdefCheck(boolean skipNdefCheck) { - this.skipNdefCheck = skipNdefCheck; - return this; - } + /** + * Setting this flag allows the caller to prevent the platform from performing an NDEF check on + * the tags it + * + * @param skipNdefCheck new value of this property + * @return configuration object + */ + public NfcConfiguration skipNdefCheck(boolean skipNdefCheck) { + this.skipNdefCheck = skipNdefCheck; + return this; + } - /** - * Set it to true to shows view with settings nfc setting if NFC is disabled, - * otherwise start of NFC session will return error in callback if no permissions/setting - * and allows user to handle disabled NFC reader (show error or snackbar or refer to settings) - * Default value is false - * - * @param handleUnavailableNfc new value of this property - * @return configuration object - */ - public NfcConfiguration handleUnavailableNfc(boolean handleUnavailableNfc) { - this.handleUnavailableNfc = handleUnavailableNfc; - return this; - } + /** + * Set it to true to shows view with settings nfc setting if NFC is disabled, otherwise start of + * NFC session will return error in callback if no permissions/setting and allows user to handle + * disabled NFC reader (show error or snackbar or refer to settings) Default value is false + * + * @param handleUnavailableNfc new value of this property + * @return configuration object + */ + public NfcConfiguration handleUnavailableNfc(boolean handleUnavailableNfc) { + this.handleUnavailableNfc = handleUnavailableNfc; + return this; + } - /** - * The timeout to use for wireless communication. - * - * @param timeout the timeout in milliseconds - * @return configuration object - */ - public NfcConfiguration timeout(int timeout) { - this.timeout = timeout; - return this; - } + /** + * The timeout to use for wireless communication. + * + * @param timeout the timeout in milliseconds + * @return configuration object + */ + public NfcConfiguration timeout(int timeout) { + this.timeout = timeout; + return this; + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcDispatcher.java b/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcDispatcher.java index be03362e..1d398312 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcDispatcher.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcDispatcher.java @@ -20,11 +20,11 @@ import android.nfc.Tag; public interface NfcDispatcher { - void enable(Activity activity, NfcConfiguration nfcConfiguration, OnTagHandler handler); + void enable(Activity activity, NfcConfiguration nfcConfiguration, OnTagHandler handler); - void disable(Activity activity); + void disable(Activity activity); - interface OnTagHandler { - void onTag(Tag tag); - } + interface OnTagHandler { + void onTag(Tag tag); + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcNotAvailable.java b/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcNotAvailable.java index 4882d27c..87dc406f 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcNotAvailable.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcNotAvailable.java @@ -17,21 +17,22 @@ package com.yubico.yubikit.android.transport.nfc; public class NfcNotAvailable extends Exception { - static final long serialVersionUID = 1L; + static final long serialVersionUID = 1L; - private final boolean disabled; + private final boolean disabled; - public NfcNotAvailable(String message, boolean disabled) { - super(message); - this.disabled = disabled; - } + public NfcNotAvailable(String message, boolean disabled) { + super(message); + this.disabled = disabled; + } - /** - * If true, the NFC functionality is disabled and can be enabled. If false, the device lacks NFC functionality. - * - * @return true if NFC is disabled - */ - public boolean isDisabled() { - return disabled; - } + /** + * If true, the NFC functionality is disabled and can be enabled. If false, the device lacks NFC + * functionality. + * + * @return true if NFC is disabled + */ + public boolean isDisabled() { + return disabled; + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcReaderDispatcher.java b/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcReaderDispatcher.java index 8ad3f21e..bcd7821c 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcReaderDispatcher.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcReaderDispatcher.java @@ -21,52 +21,53 @@ import android.os.Bundle; public class NfcReaderDispatcher implements NfcDispatcher { - private final NfcAdapter adapter; + private final NfcAdapter adapter; - public NfcReaderDispatcher(NfcAdapter adapter) { - this.adapter = adapter; - } + public NfcReaderDispatcher(NfcAdapter adapter) { + this.adapter = adapter; + } - @Override - public void enable(Activity activity, NfcConfiguration nfcConfiguration, OnTagHandler handler) { - // restart nfc watching services - disableReaderMode(activity); - enableReaderMode(activity, nfcConfiguration, handler); - } + @Override + public void enable(Activity activity, NfcConfiguration nfcConfiguration, OnTagHandler handler) { + // restart nfc watching services + disableReaderMode(activity); + enableReaderMode(activity, nfcConfiguration, handler); + } - @Override - public void disable(Activity activity) { - disableReaderMode(activity); - } + @Override + public void disable(Activity activity) { + disableReaderMode(activity); + } - /** - * Start intercepting nfc events - * - * @param activity activity that is going to receive nfc events - * Note: invoke that while activity is in foreground - * @param handler the handler for new tags - */ - private void enableReaderMode(Activity activity, final NfcConfiguration nfcConfiguration, OnTagHandler handler) { - Bundle options = new Bundle(); - options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 50); - int READER_FLAGS = NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_NFC_B; - if (nfcConfiguration.isDisableNfcDiscoverySound()) { - READER_FLAGS |= NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS; - } - - if (nfcConfiguration.isSkipNdefCheck()) { - READER_FLAGS |= NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK; - } - adapter.enableReaderMode(activity, handler::onTag, READER_FLAGS, options); + /** + * Start intercepting nfc events + * + * @param activity activity that is going to receive nfc events Note: invoke that while activity + * is in foreground + * @param handler the handler for new tags + */ + private void enableReaderMode( + Activity activity, final NfcConfiguration nfcConfiguration, OnTagHandler handler) { + Bundle options = new Bundle(); + options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 50); + int READER_FLAGS = NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_NFC_B; + if (nfcConfiguration.isDisableNfcDiscoverySound()) { + READER_FLAGS |= NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS; } - /** - * Stop intercepting nfc events - * - * @param activity activity that was receiving nfc events - * Note: invoke that while activity is still in foreground - */ - private void disableReaderMode(Activity activity) { - adapter.disableReaderMode(activity); + if (nfcConfiguration.isSkipNdefCheck()) { + READER_FLAGS |= NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK; } + adapter.enableReaderMode(activity, handler::onTag, READER_FLAGS, options); + } + + /** + * Stop intercepting nfc events + * + * @param activity activity that was receiving nfc events Note: invoke that while activity is + * still in foreground + */ + private void disableReaderMode(Activity activity) { + adapter.disableReaderMode(activity); + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcSmartCardConnection.java b/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcSmartCardConnection.java index 38458a59..d13b11f7 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcSmartCardConnection.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcSmartCardConnection.java @@ -18,65 +18,59 @@ import android.nfc.Tag; import android.nfc.tech.IsoDep; - -import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.Transport; +import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.core.util.StringUtils; - -import org.slf4j.LoggerFactory; - import java.io.IOException; +import org.slf4j.LoggerFactory; -/** - * NFC service for interacting with the YubiKey - */ +/** NFC service for interacting with the YubiKey */ public class NfcSmartCardConnection implements SmartCardConnection { - /** - * Provides access to ISO-DEP (ISO 14443-4) properties and I/O operations on a {@link Tag}. - */ - private final IsoDep card; + /** Provides access to ISO-DEP (ISO 14443-4) properties and I/O operations on a {@link Tag}. */ + private final IsoDep card; - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(NfcSmartCardConnection.class); + private static final org.slf4j.Logger logger = + LoggerFactory.getLogger(NfcSmartCardConnection.class); - /** - * Instantiates session for nfc tag interaction - * - * @param card the tag that has been discovered - */ - NfcSmartCardConnection(IsoDep card) { - this.card = card; - Logger.debug(logger, "nfc connection opened"); - } + /** + * Instantiates session for nfc tag interaction + * + * @param card the tag that has been discovered + */ + NfcSmartCardConnection(IsoDep card) { + this.card = card; + Logger.debug(logger, "nfc connection opened"); + } - @Override - public Transport getTransport() { - return Transport.NFC; - } + @Override + public Transport getTransport() { + return Transport.NFC; + } - @Override - public boolean isExtendedLengthApduSupported() { - return card.isExtendedLengthApduSupported(); - } + @Override + public boolean isExtendedLengthApduSupported() { + return card.isExtendedLengthApduSupported(); + } - @Override - public byte[] sendAndReceive(byte[] apdu) throws IOException { - Logger.trace(logger, "sent: {}", StringUtils.bytesToHex(apdu)); - byte[] received = card.transceive(apdu); - Logger.trace(logger, "received: {}", StringUtils.bytesToHex(received)); - return received; - } + @Override + public byte[] sendAndReceive(byte[] apdu) throws IOException { + Logger.trace(logger, "sent: {}", StringUtils.bytesToHex(apdu)); + byte[] received = card.transceive(apdu); + Logger.trace(logger, "received: {}", StringUtils.bytesToHex(received)); + return received; + } - @Override - public void close() throws IOException { - card.close(); - Logger.debug(logger, "nfc connection closed"); - } + @Override + public void close() throws IOException { + card.close(); + Logger.debug(logger, "nfc connection closed"); + } - @Override - public byte[] getAtr() { - byte[] historicalBytes = card.getHistoricalBytes(); - return historicalBytes != null ? historicalBytes.clone() : new byte[]{}; - } -} \ No newline at end of file + @Override + public byte[] getAtr() { + byte[] historicalBytes = card.getHistoricalBytes(); + return historicalBytes != null ? historicalBytes.clone() : new byte[] {}; + } +} diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcYubiKeyDevice.java b/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcYubiKeyDevice.java index a84e922a..39b6e351 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcYubiKeyDevice.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcYubiKeyDevice.java @@ -21,7 +21,6 @@ import android.nfc.Tag; import android.nfc.tech.IsoDep; import android.nfc.tech.Ndef; - import com.yubico.yubikit.core.Transport; import com.yubico.yubikit.core.YubiKeyConnection; import com.yubico.yubikit.core.YubiKeyDevice; @@ -31,153 +30,159 @@ import com.yubico.yubikit.core.smartcard.SmartCardProtocol; import com.yubico.yubikit.core.util.Callback; import com.yubico.yubikit.core.util.Result; - +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.IOException; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - public class NfcYubiKeyDevice implements YubiKeyDevice { - private final AtomicBoolean removed = new AtomicBoolean(); - private final ExecutorService executorService; - private final Tag tag; - private final int timeout; - - /** - * Instantiates session for nfc tag interaction - * - * @param tag the tag that has been discovered - * @param timeout timeout, in milliseconds, to use for NFC communication - */ - public NfcYubiKeyDevice(Tag tag, int timeout, ExecutorService executorService) { - this.executorService = executorService; - this.tag = tag; - this.timeout = timeout; - } - - /** - * @return NFC tag that has been discovered - */ - public Tag getTag() { - return tag; + private final AtomicBoolean removed = new AtomicBoolean(); + private final ExecutorService executorService; + private final Tag tag; + private final int timeout; + + /** + * Instantiates session for nfc tag interaction + * + * @param tag the tag that has been discovered + * @param timeout timeout, in milliseconds, to use for NFC communication + */ + public NfcYubiKeyDevice(Tag tag, int timeout, ExecutorService executorService) { + this.executorService = executorService; + this.tag = tag; + this.timeout = timeout; + } + + /** + * @return NFC tag that has been discovered + */ + public Tag getTag() { + return tag; + } + + private NfcSmartCardConnection openIso7816Connection() throws IOException { + IsoDep card = IsoDep.get(tag); + if (card == null) { + throw new IOException("the tag does not support ISO-DEP"); } - - private NfcSmartCardConnection openIso7816Connection() throws IOException { - IsoDep card = IsoDep.get(tag); - if (card == null) { - throw new IOException("the tag does not support ISO-DEP"); + card.setTimeout(timeout); + card.connect(); + return new NfcSmartCardConnection(card); + } + + @SuppressFBWarnings("RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE") + public byte[] readNdef() throws IOException { + try (Ndef ndef = Ndef.get(tag)) { + if (ndef != null) { + ndef.connect(); + NdefMessage message = ndef.getNdefMessage(); + if (message != null) { + return message.toByteArray(); } - card.setTimeout(timeout); - card.connect(); - return new NfcSmartCardConnection(card); + } + } catch (FormatException e) { + throw new IOException(e); } - - @SuppressFBWarnings("RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE") - public byte[] readNdef() throws IOException { - try (Ndef ndef = Ndef.get(tag)) { - if (ndef != null) { - ndef.connect(); - NdefMessage message = ndef.getNdefMessage(); - if (message != null) { - return message.toByteArray(); - } - } - } catch (FormatException e) { - throw new IOException(e); - } - throw new IOException("NDEF data missing or invalid"); - } - - /** - * Closes the device and waits for physical removal. - * This method signals that we are done with the device and can be used to wait for the user to - * physically remove the YubiKey from NFC scan range, to avoid triggering NFC YubiKey detection - * multiple times in quick succession. - */ - public void remove(Runnable onRemoved) { - removed.set(true); - executorService.submit(() -> { - try { - IsoDep isoDep = IsoDep.get(tag); - isoDep.connect(); - while (isoDep.isConnected()) { - //noinspection BusyWait - Thread.sleep(250); - } - } catch (SecurityException | InterruptedException | IOException e) { - // Ignore + throw new IOException("NDEF data missing or invalid"); + } + + /** + * Closes the device and waits for physical removal. + * + *

This method signals that we are done with the device and can be used to wait for the user to + * physically remove the YubiKey from NFC scan range, to avoid triggering NFC YubiKey detection + * multiple times in quick succession. + */ + public void remove(Runnable onRemoved) { + removed.set(true); + executorService.submit( + () -> { + try { + IsoDep isoDep = IsoDep.get(tag); + isoDep.connect(); + while (isoDep.isConnected()) { + //noinspection BusyWait + Thread.sleep(250); } - onRemoved.run(); + } catch (SecurityException | InterruptedException | IOException e) { + // Ignore + } + onRemoved.run(); }); + } + + @Override + public Transport getTransport() { + return Transport.NFC; + } + + @Override + public boolean supportsConnection(Class connectionType) { + return connectionType.isAssignableFrom(NfcSmartCardConnection.class); + } + + public T openConnection(Class connectionType) + throws IOException { + if (connectionType.isAssignableFrom(NfcSmartCardConnection.class)) { + return Objects.requireNonNull(connectionType.cast(openIso7816Connection())); } - - @Override - public Transport getTransport() { - return Transport.NFC; - } - - @Override - public boolean supportsConnection(Class connectionType) { - return connectionType.isAssignableFrom(NfcSmartCardConnection.class); - } - - public T openConnection(Class connectionType) throws IOException { - if (connectionType.isAssignableFrom(NfcSmartCardConnection.class)) { - return Objects.requireNonNull(connectionType.cast(openIso7816Connection())); - } - throw new IllegalStateException("The connection type is not supported by this session"); - } - - @Override - public void requestConnection(Class connectionType, Callback> callback) { - if (removed.get()) { - callback.invoke(Result.failure(new IOException("Can't requestConnection after calling remove()"))); - } else - executorService.submit(() -> { - try (T connection = openConnection(connectionType)) { - callback.invoke(Result.success(connection)); - } catch (IOException ioException) { - callback.invoke(Result.failure(ioException)); - } catch (Exception exception) { - callback.invoke(Result.failure(new IOException("openConnection(" + - connectionType.getSimpleName() + ") exception: " + exception.getMessage()) - )); - } - }); - } - - /** - * Probe the nfc device whether it is a Yubico hardware. - * @return true if this device is a YubiKey or a Security Key by Yubico. - */ - public boolean isYubiKey() { - try (SmartCardConnection connection = openConnection(SmartCardConnection.class)) { - SmartCardProtocol protocol = new SmartCardProtocol(connection); - try { - protocol.select(AppId.MANAGEMENT); - return true; - } catch (ApplicationNotAvailableException managementNotAvailable) { - try { - protocol.select(AppId.OTP); - return true; - } catch (ApplicationNotAvailableException otpNotAvailable) { - // ignored - } + throw new IllegalStateException("The connection type is not supported by this session"); + } + + @Override + public void requestConnection( + Class connectionType, Callback> callback) { + if (removed.get()) { + callback.invoke( + Result.failure(new IOException("Can't requestConnection after calling remove()"))); + } else + executorService.submit( + () -> { + try (T connection = openConnection(connectionType)) { + callback.invoke(Result.success(connection)); + } catch (IOException ioException) { + callback.invoke(Result.failure(ioException)); + } catch (Exception exception) { + callback.invoke( + Result.failure( + new IOException( + "openConnection(" + + connectionType.getSimpleName() + + ") exception: " + + exception.getMessage()))); } - } catch (IOException ioException) { - // ignored + }); + } + + /** + * Probe the nfc device whether it is a Yubico hardware. + * + * @return true if this device is a YubiKey or a Security Key by Yubico. + */ + public boolean isYubiKey() { + try (SmartCardConnection connection = openConnection(SmartCardConnection.class)) { + SmartCardProtocol protocol = new SmartCardProtocol(connection); + try { + protocol.select(AppId.MANAGEMENT); + return true; + } catch (ApplicationNotAvailableException managementNotAvailable) { + try { + protocol.select(AppId.OTP); + return true; + } catch (ApplicationNotAvailableException otpNotAvailable) { + // ignored } - - return false; + } + } catch (IOException ioException) { + // ignored } - @Override - public String toString() { - return "NfcYubiKeyDevice{" + - "tag=" + tag + - ", timeout=" + timeout + - '}'; - } + return false; + } + + @Override + public String toString() { + return "NfcYubiKeyDevice{" + "tag=" + tag + ", timeout=" + timeout + '}'; + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcYubiKeyManager.java b/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcYubiKeyManager.java index bc19907c..0534ea5b 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcYubiKeyManager.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/nfc/NfcYubiKeyManager.java @@ -20,94 +20,98 @@ import android.content.Context; import android.content.Intent; import android.nfc.NfcAdapter; - import com.yubico.yubikit.core.util.Callback; - import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; - import javax.annotation.Nullable; -/** - * This class allows you to communicate with local NFC adapter - */ +/** This class allows you to communicate with local NFC adapter */ public class NfcYubiKeyManager { - /** - * Action for intent to tweak NFC settings in Android settings view - * on Q Android supports Settings.Panel.ACTION_NFC, we might update with release on Q - */ - public static final String NFC_SETTINGS_ACTION = "android.settings.NFC_SETTINGS"; + /** + * Action for intent to tweak NFC settings in Android settings view on Q Android supports + * Settings.Panel.ACTION_NFC, we might update with release on Q + */ + public static final String NFC_SETTINGS_ACTION = "android.settings.NFC_SETTINGS"; - private final Context context; - private final NfcAdapter adapter; - private final NfcDispatcher dispatcher; - @Nullable - private ExecutorService executorService = null; + private final Context context; + private final NfcAdapter adapter; + private final NfcDispatcher dispatcher; + @Nullable private ExecutorService executorService = null; - /** - * Creates instance of {@link NfcYubiKeyManager} - * - * @param context the application context - * @param dispatcher optional implementation of NfcDispatcher to use instead of the default. - * @throws NfcNotAvailable if the Android device does not support NFC - */ - public NfcYubiKeyManager(Context context, @Nullable NfcDispatcher dispatcher) throws NfcNotAvailable { - adapter = NfcAdapter.getDefaultAdapter(context); - if (adapter == null) { - throw new NfcNotAvailable("NFC unavailable on this device", false); - } - if (dispatcher == null) { - dispatcher = new NfcReaderDispatcher(adapter); - } - this.dispatcher = dispatcher; - this.context = context; + /** + * Creates instance of {@link NfcYubiKeyManager} + * + * @param context the application context + * @param dispatcher optional implementation of NfcDispatcher to use instead of the default. + * @throws NfcNotAvailable if the Android device does not support NFC + */ + public NfcYubiKeyManager(Context context, @Nullable NfcDispatcher dispatcher) + throws NfcNotAvailable { + adapter = NfcAdapter.getDefaultAdapter(context); + if (adapter == null) { + throw new NfcNotAvailable("NFC unavailable on this device", false); + } + if (dispatcher == null) { + dispatcher = new NfcReaderDispatcher(adapter); } + this.dispatcher = dispatcher; + this.context = context; + } - /** - * Enable discovery of nfc tags for foreground activity - * - * @param activity activity that is going to dispatch nfc tags - * @param nfcConfiguration additional configurations for NFC discovery - * @param listener the listener to invoke on NFC sessions - * @throws NfcNotAvailable in case NFC is turned off (but available) - */ - public void enable(Activity activity, NfcConfiguration nfcConfiguration, Callback listener) throws NfcNotAvailable { - if (checkAvailability(nfcConfiguration.isHandleUnavailableNfc())) { - ExecutorService executor = Executors.newSingleThreadExecutor(); - dispatcher.enable(activity, nfcConfiguration, tag -> listener.invoke(new NfcYubiKeyDevice(tag, nfcConfiguration.getTimeout(), executor))); - executorService = executor; - } + /** + * Enable discovery of nfc tags for foreground activity + * + * @param activity activity that is going to dispatch nfc tags + * @param nfcConfiguration additional configurations for NFC discovery + * @param listener the listener to invoke on NFC sessions + * @throws NfcNotAvailable in case NFC is turned off (but available) + */ + public void enable( + Activity activity, + NfcConfiguration nfcConfiguration, + Callback listener) + throws NfcNotAvailable { + if (checkAvailability(nfcConfiguration.isHandleUnavailableNfc())) { + ExecutorService executor = Executors.newSingleThreadExecutor(); + dispatcher.enable( + activity, + nfcConfiguration, + tag -> + listener.invoke(new NfcYubiKeyDevice(tag, nfcConfiguration.getTimeout(), executor))); + executorService = executor; } + } - /** - * Disable active listening of nfc events - * - * @param activity activity that goes to background or want to stop dispatching nfc tags - */ - public void disable(Activity activity) { - if (executorService != null) { - executorService.shutdown(); - executorService = null; - } - dispatcher.disable(activity); + /** + * Disable active listening of nfc events + * + * @param activity activity that goes to background or want to stop dispatching nfc tags + */ + public void disable(Activity activity) { + if (executorService != null) { + executorService.shutdown(); + executorService = null; } + dispatcher.disable(activity); + } - /** - * Checks if user turned on NFC_TRANSPORT and returns result via listener callbacks - * - * @param handleUnavailableNfc true if prompt user for turning on settings with UI dialog, otherwise returns error if no settings on or NFC_TRANSPORT is not available - * @throws NfcNotAvailable in case if NFC turned off - */ - private boolean checkAvailability(boolean handleUnavailableNfc) throws NfcNotAvailable { - if (adapter.isEnabled()) { - return true; - } - if (handleUnavailableNfc) { - context.startActivity(new Intent(NFC_SETTINGS_ACTION)); - return false; - } else { - throw new NfcNotAvailable("Please activate NFC_TRANSPORT", true); - } + /** + * Checks if user turned on NFC_TRANSPORT and returns result via listener callbacks + * + * @param handleUnavailableNfc true if prompt user for turning on settings with UI dialog, + * otherwise returns error if no settings on or NFC_TRANSPORT is not available + * @throws NfcNotAvailable in case if NFC turned off + */ + private boolean checkAvailability(boolean handleUnavailableNfc) throws NfcNotAvailable { + if (adapter.isEnabled()) { + return true; + } + if (handleUnavailableNfc) { + context.startActivity(new Intent(NFC_SETTINGS_ACTION)); + return false; + } else { + throw new NfcNotAvailable("Please activate NFC_TRANSPORT", true); } + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/nfc/package-info.java b/android/src/main/java/com/yubico/yubikit/android/transport/nfc/package-info.java index 9731ec44..901180a3 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/nfc/package-info.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/nfc/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.android.transport.nfc; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/package-info.java b/android/src/main/java/com/yubico/yubikit/android/transport/package-info.java index c304b10f..961be459 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/package-info.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.android.transport; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/usb/NoPermissionsException.java b/android/src/main/java/com/yubico/yubikit/android/transport/usb/NoPermissionsException.java index 36494acd..a1380660 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/usb/NoPermissionsException.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/usb/NoPermissionsException.java @@ -17,17 +17,18 @@ package com.yubico.yubikit.android.transport.usb; import android.hardware.usb.UsbDevice; - import java.io.IOException; -/** - * Exception that thrown when user didn't provide permissions to connect to USB device - */ +/** Exception that thrown when user didn't provide permissions to connect to USB device */ public class NoPermissionsException extends IOException { - static final long serialVersionUID = 1L; + static final long serialVersionUID = 1L; - public NoPermissionsException(UsbDevice usbDevice) { - // with L+ devices we can get more verbal device name - super("No permission granted to communicate with device " + (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP ? usbDevice.getProductName() : usbDevice.getDeviceName())); - } + public NoPermissionsException(UsbDevice usbDevice) { + // with L+ devices we can get more verbal device name + super( + "No permission granted to communicate with device " + + (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP + ? usbDevice.getProductName() + : usbDevice.getDeviceName())); + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/usb/UsbConfiguration.java b/android/src/main/java/com/yubico/yubikit/android/transport/usb/UsbConfiguration.java index 467c363c..1f2b592b 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/usb/UsbConfiguration.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/usb/UsbConfiguration.java @@ -15,28 +15,25 @@ */ package com.yubico.yubikit.android.transport.usb; -/** - * Additional configurations for USB discovery management - */ +/** Additional configurations for USB discovery management */ public class UsbConfiguration { - // whether to prompt permissions when application needs them - private boolean handlePermissions = true; + // whether to prompt permissions when application needs them + private boolean handlePermissions = true; - boolean isHandlePermissions() { - return handlePermissions; - } + boolean isHandlePermissions() { + return handlePermissions; + } - /** - * Set YubiKitManager to show dialog for permissions on USB connection - * - * @param handlePermissions true to show dialog for permissions - * otherwise it's delegated on user to make sure that application - * has permissions to communicate with device - * @return the UsbConfiguration, for chaining - */ - public UsbConfiguration handlePermissions(boolean handlePermissions) { - this.handlePermissions = handlePermissions; - return this; - } + /** + * Set YubiKitManager to show dialog for permissions on USB connection + * + * @param handlePermissions true to show dialog for permissions otherwise it's delegated on user + * to make sure that application has permissions to communicate with device + * @return the UsbConfiguration, for chaining + */ + public UsbConfiguration handlePermissions(boolean handlePermissions) { + this.handlePermissions = handlePermissions; + return this; + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/usb/UsbDeviceManager.java b/android/src/main/java/com/yubico/yubikit/android/transport/usb/UsbDeviceManager.java index 145d0ed6..84644f64 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/usb/UsbDeviceManager.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/usb/UsbDeviceManager.java @@ -25,213 +25,218 @@ import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbManager; import android.os.Build; - import com.yubico.yubikit.core.internal.Logger; - -import org.slf4j.LoggerFactory; - import java.util.Collection; import java.util.HashSet; import java.util.Objects; import java.util.Set; import java.util.WeakHashMap; - import javax.annotation.Nullable; +import org.slf4j.LoggerFactory; final class UsbDeviceManager { - private final static String ACTION_USB_PERMISSION = "com.yubico.yubikey.USB_PERMISSION"; - public final static int YUBICO_VENDOR_ID = 0x1050; - - @Nullable - private static UsbDeviceManager instance; - - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(UsbDeviceManager.class); - - private static synchronized UsbDeviceManager getInstance() { - if (instance == null) { - instance = new UsbDeviceManager(); + private static final String ACTION_USB_PERMISSION = "com.yubico.yubikey.USB_PERMISSION"; + public static final int YUBICO_VENDOR_ID = 0x1050; + + @Nullable private static UsbDeviceManager instance; + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(UsbDeviceManager.class); + + private static synchronized UsbDeviceManager getInstance() { + if (instance == null) { + instance = new UsbDeviceManager(); + } + return instance; + } + + static void registerUsbListener(Context context, UsbDeviceListener listener) { + getInstance().addUsbListener(context, listener); + } + + static void unregisterUsbListener(Context context, UsbDeviceListener listener) { + getInstance().removeUsbListener(context, listener); + } + + static void requestPermission( + Context context, UsbDevice usbDevice, PermissionResultListener listener) { + getInstance().requestDevicePermission(context, usbDevice, listener); + } + + private final DeviceBroadcastReceiver broadcastReceiver = new DeviceBroadcastReceiver(); + private final PermissionBroadcastReceiver permissionReceiver = new PermissionBroadcastReceiver(); + private final Set deviceListeners = new HashSet<>(); + private final WeakHashMap> contexts = + new WeakHashMap<>(); + private final Set awaitingPermissions = new HashSet<>(); + + private synchronized void addUsbListener(Context context, UsbDeviceListener listener) { + if (deviceListeners.isEmpty()) { + UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); + Collection usbDevices = usbManager.getDeviceList().values(); + IntentFilter intentFilter = new IntentFilter(UsbManager.ACTION_USB_DEVICE_ATTACHED); + intentFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); + context.registerReceiver(broadcastReceiver, intentFilter); + for (UsbDevice usbDevice : usbDevices) { + if (usbDevice.getVendorId() == YUBICO_VENDOR_ID) { + onDeviceAttach(usbDevice); } - return instance; - } - - static void registerUsbListener(Context context, UsbDeviceListener listener) { - getInstance().addUsbListener(context, listener); + } } - - static void unregisterUsbListener(Context context, UsbDeviceListener listener) { - getInstance().removeUsbListener(context, listener); + deviceListeners.add(listener); + for (UsbDevice usbDevice : contexts.keySet()) { + listener.deviceAttached(usbDevice); } + } - static void requestPermission(Context context, UsbDevice usbDevice, PermissionResultListener listener) { - getInstance().requestDevicePermission(context, usbDevice, listener); + private synchronized void removeUsbListener(Context context, UsbDeviceListener listener) { + deviceListeners.remove(listener); + for (UsbDevice usbDevice : contexts.keySet()) { + listener.deviceRemoved(usbDevice); } - - private final DeviceBroadcastReceiver broadcastReceiver = new DeviceBroadcastReceiver(); - private final PermissionBroadcastReceiver permissionReceiver = new PermissionBroadcastReceiver(); - private final Set deviceListeners = new HashSet<>(); - private final WeakHashMap> contexts = new WeakHashMap<>(); - private final Set awaitingPermissions = new HashSet<>(); - - private synchronized void addUsbListener(Context context, UsbDeviceListener listener) { - if (deviceListeners.isEmpty()) { - UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); - Collection usbDevices = usbManager.getDeviceList().values(); - IntentFilter intentFilter = new IntentFilter(UsbManager.ACTION_USB_DEVICE_ATTACHED); - intentFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); - context.registerReceiver(broadcastReceiver, intentFilter); - for (UsbDevice usbDevice : usbDevices) { - if (usbDevice.getVendorId() == YUBICO_VENDOR_ID) { - onDeviceAttach(usbDevice); - } - } - } - deviceListeners.add(listener); - for (UsbDevice usbDevice : contexts.keySet()) { - listener.deviceAttached(usbDevice); - } + if (deviceListeners.isEmpty()) { + context.unregisterReceiver(broadcastReceiver); + contexts.clear(); } + } - private synchronized void removeUsbListener(Context context, UsbDeviceListener listener) { - deviceListeners.remove(listener); - for (UsbDevice usbDevice : contexts.keySet()) { - listener.deviceRemoved(usbDevice); - } - if (deviceListeners.isEmpty()) { - context.unregisterReceiver(broadcastReceiver); - contexts.clear(); - } + private synchronized void requestDevicePermission( + Context context, UsbDevice usbDevice, PermissionResultListener listener) { + Set permissionListeners = + Objects.requireNonNull(contexts.get(usbDevice)); + synchronized (permissionListeners) { + permissionListeners.add(listener); } - - private synchronized void requestDevicePermission(Context context, UsbDevice usbDevice, PermissionResultListener listener) { - Set permissionListeners = Objects.requireNonNull(contexts.get(usbDevice)); - synchronized (permissionListeners) { - permissionListeners.add(listener); + synchronized (awaitingPermissions) { + if (!awaitingPermissions.contains(usbDevice)) { + if (awaitingPermissions.isEmpty()) { + registerPermissionsReceiver(context, permissionReceiver); } - synchronized (awaitingPermissions) { - if (!awaitingPermissions.contains(usbDevice)) { - if (awaitingPermissions.isEmpty()) { - registerPermissionsReceiver(context, permissionReceiver); - } - Logger.debug(logger, "Requesting permission for UsbDevice: {}", usbDevice.getDeviceName()); - int flags = 0; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { - flags |= PendingIntent.FLAG_MUTABLE; - } - - Intent intent = new Intent(ACTION_USB_PERMISSION); - intent.setPackage(context.getPackageName()); - - PendingIntent pendingUsbPermissionIntent = PendingIntent.getBroadcast(context, 0, intent, flags); - UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); - usbManager.requestPermission(usbDevice, pendingUsbPermissionIntent); - awaitingPermissions.add(usbDevice); - } + Logger.debug(logger, "Requesting permission for UsbDevice: {}", usbDevice.getDeviceName()); + int flags = 0; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + flags |= PendingIntent.FLAG_MUTABLE; } - } - - private void onDeviceAttach(UsbDevice usbDevice) { - Logger.debug(logger, "UsbDevice attached: {}", usbDevice.getDeviceName()); - contexts.put(usbDevice, new HashSet<>()); - for (UsbDeviceListener listener : deviceListeners) { - listener.deviceAttached(usbDevice); - } - } - private void onPermission(Context context, UsbDevice usbDevice, boolean permission) { - Logger.debug(logger, "Permission result for {}, permitted: {}", usbDevice.getDeviceName(), permission); - Set permissionListeners = contexts.get(usbDevice); - if (permissionListeners != null) { - synchronized (permissionListeners) { - for (PermissionResultListener listener : permissionListeners) { - listener.onPermissionResult(usbDevice, permission); - } - permissionListeners.clear(); - } + Intent intent = new Intent(ACTION_USB_PERMISSION); + intent.setPackage(context.getPackageName()); + + PendingIntent pendingUsbPermissionIntent = + PendingIntent.getBroadcast(context, 0, intent, flags); + UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); + usbManager.requestPermission(usbDevice, pendingUsbPermissionIntent); + awaitingPermissions.add(usbDevice); + } + } + } + + private void onDeviceAttach(UsbDevice usbDevice) { + Logger.debug(logger, "UsbDevice attached: {}", usbDevice.getDeviceName()); + contexts.put(usbDevice, new HashSet<>()); + for (UsbDeviceListener listener : deviceListeners) { + listener.deviceAttached(usbDevice); + } + } + + private void onPermission(Context context, UsbDevice usbDevice, boolean permission) { + Logger.debug( + logger, "Permission result for {}, permitted: {}", usbDevice.getDeviceName(), permission); + Set permissionListeners = contexts.get(usbDevice); + if (permissionListeners != null) { + synchronized (permissionListeners) { + for (PermissionResultListener listener : permissionListeners) { + listener.onPermissionResult(usbDevice, permission); } - synchronized (awaitingPermissions) { - if (awaitingPermissions.remove(usbDevice) && awaitingPermissions.isEmpty()) { - context.unregisterReceiver(permissionReceiver); - } + permissionListeners.clear(); + } + } + synchronized (awaitingPermissions) { + if (awaitingPermissions.remove(usbDevice) && awaitingPermissions.isEmpty()) { + context.unregisterReceiver(permissionReceiver); + } + } + } + + private void onDeviceDetach(Context context, UsbDevice usbDevice) { + Logger.debug(logger, "UsbDevice detached: {}", usbDevice.getDeviceName()); + if (contexts.remove(usbDevice) != null) { + for (UsbDeviceListener listener : deviceListeners) { + listener.deviceRemoved(usbDevice); + } + } + synchronized (awaitingPermissions) { + if (awaitingPermissions.remove(usbDevice) && awaitingPermissions.isEmpty()) { + context.unregisterReceiver(permissionReceiver); + } + } + } + + interface UsbDeviceListener { + void deviceAttached(UsbDevice usbDevice); + + void deviceRemoved(UsbDevice usbDevice); + } + + private class DeviceBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + UsbDevice usbDevice = getUsbManagerExtraDevice(intent); + if (usbDevice == null || usbDevice.getVendorId() != YUBICO_VENDOR_ID) { + return; + } + + if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) { + onDeviceAttach(usbDevice); + } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) { + onDeviceDetach(context, usbDevice); + } + } + } + + interface PermissionResultListener { + void onPermissionResult(UsbDevice usbDevice, boolean hasPermission); + } + + private class PermissionBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (ACTION_USB_PERMISSION.equals(intent.getAction())) { + UsbDevice device = getUsbManagerExtraDevice(intent); + UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); + if (device != null) { + onPermission(context, device, usbManager.hasPermission(device)); } - } - - private void onDeviceDetach(Context context, UsbDevice usbDevice) { - Logger.debug(logger, "UsbDevice detached: {}", usbDevice.getDeviceName()); - if (contexts.remove(usbDevice) != null) { - for (UsbDeviceListener listener : deviceListeners) { - listener.deviceRemoved(usbDevice); - } - } - synchronized (awaitingPermissions) { - if (awaitingPermissions.remove(usbDevice) && awaitingPermissions.isEmpty()) { - context.unregisterReceiver(permissionReceiver); - } - } - } - - interface UsbDeviceListener { - void deviceAttached(UsbDevice usbDevice); - - void deviceRemoved(UsbDevice usbDevice); - } - - private class DeviceBroadcastReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - UsbDevice usbDevice = getUsbManagerExtraDevice(intent); - if (usbDevice == null || usbDevice.getVendorId() != YUBICO_VENDOR_ID) { - return; - } - - if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) { - onDeviceAttach(usbDevice); - } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) { - onDeviceDetach(context, usbDevice); - } - } - } - - interface PermissionResultListener { - void onPermissionResult(UsbDevice usbDevice, boolean hasPermission); - } - - private class PermissionBroadcastReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - if (ACTION_USB_PERMISSION.equals(intent.getAction())) { - UsbDevice device = getUsbManagerExtraDevice(intent); - UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); - if (device != null) { - onPermission(context, device, usbManager.hasPermission(device)); - } - } - } - } - - @SuppressLint("UnspecifiedRegisterReceiverFlag") - private static void registerPermissionsReceiver(Context context, PermissionBroadcastReceiver permissionReceiver) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.registerReceiver(permissionReceiver, new IntentFilter(ACTION_USB_PERMISSION), Context.RECEIVER_NOT_EXPORTED); - } else { - context.registerReceiver(permissionReceiver, new IntentFilter(ACTION_USB_PERMISSION)); - } - } - - /** - * Helper method to call {@code Intent.getParcelableExtra} based on build version - * - * @implNote The new API is used only on 34+ devices because of bug in API 33 - * @see The new Intent.getParcelableExtra(String,Class) throws an NPE internally - * @param intent Intent to get the usb device from - * @return UsbDevice from intent's parcelable - */ - @Nullable - @SuppressWarnings("deprecation") - private static UsbDevice getUsbManagerExtraDevice(Intent intent) { - return (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) - ? intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice.class) - : intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); - } + } + } + } + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + private static void registerPermissionsReceiver( + Context context, PermissionBroadcastReceiver permissionReceiver) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.registerReceiver( + permissionReceiver, + new IntentFilter(ACTION_USB_PERMISSION), + Context.RECEIVER_NOT_EXPORTED); + } else { + context.registerReceiver(permissionReceiver, new IntentFilter(ACTION_USB_PERMISSION)); + } + } + + /** + * Helper method to call {@code Intent.getParcelableExtra} based on build version + * + * @implNote The new API is used only on 34+ devices because of bug in API 33 + * @see The new + * Intent.getParcelableExtra(String,Class) throws an NPE internally + * @param intent Intent to get the usb device from + * @return UsbDevice from intent's parcelable + */ + @Nullable @SuppressWarnings("deprecation") + private static UsbDevice getUsbManagerExtraDevice(Intent intent) { + return (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) + ? intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice.class) + : intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/usb/UsbYubiKeyDevice.java b/android/src/main/java/com/yubico/yubikit/android/transport/usb/UsbYubiKeyDevice.java index 7ec07427..08a544ff 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/usb/UsbYubiKeyDevice.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/usb/UsbYubiKeyDevice.java @@ -20,213 +20,211 @@ import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbManager; - import com.yubico.yubikit.android.transport.usb.connection.ConnectionManager; -import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.Transport; import com.yubico.yubikit.core.UsbPid; import com.yubico.yubikit.core.YubiKeyConnection; import com.yubico.yubikit.core.YubiKeyDevice; +import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.otp.OtpConnection; import com.yubico.yubikit.core.util.Callback; import com.yubico.yubikit.core.util.Result; - -import org.slf4j.LoggerFactory; - import java.io.Closeable; import java.io.IOException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; - import javax.annotation.Nonnull; import javax.annotation.Nullable; +import org.slf4j.LoggerFactory; public class UsbYubiKeyDevice implements YubiKeyDevice, Closeable { - private final ExecutorService executorService = Executors.newSingleThreadExecutor(); - private final ConnectionManager connectionManager; - private final UsbManager usbManager; - private final UsbDevice usbDevice; - private final UsbPid usbPid; - - @Nullable - private CachedOtpConnection otpConnection = null; + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + private final ConnectionManager connectionManager; + private final UsbManager usbManager; + private final UsbDevice usbDevice; + private final UsbPid usbPid; - @Nullable - private Runnable onClosed = null; + @Nullable private CachedOtpConnection otpConnection = null; - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(UsbYubiKeyDevice.class); + @Nullable private Runnable onClosed = null; - /** - * Creates the instance of usb session to interact with the yubikey device. - * - * @param usbManager UsbManager for accessing USB devices - * @param usbDevice device connected over usb that has permissions to interact with - * @throws IllegalArgumentException when the usbDevice is not a recognized YubiKey - */ - public UsbYubiKeyDevice(UsbManager usbManager, UsbDevice usbDevice) - throws IllegalArgumentException { + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(UsbYubiKeyDevice.class); - if (usbDevice.getVendorId() != YUBICO_VENDOR_ID) { - throw new IllegalArgumentException("Invalid vendor id"); - } + /** + * Creates the instance of usb session to interact with the yubikey device. + * + * @param usbManager UsbManager for accessing USB devices + * @param usbDevice device connected over usb that has permissions to interact with + * @throws IllegalArgumentException when the usbDevice is not a recognized YubiKey + */ + public UsbYubiKeyDevice(UsbManager usbManager, UsbDevice usbDevice) + throws IllegalArgumentException { - this.usbPid = UsbPid.fromValue(usbDevice.getProductId()); - - this.connectionManager = new ConnectionManager(usbManager, usbDevice); - this.usbDevice = usbDevice; - this.usbManager = usbManager; + if (usbDevice.getVendorId() != YUBICO_VENDOR_ID) { + throw new IllegalArgumentException("Invalid vendor id"); } - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public boolean hasPermission() { - return usbManager.hasPermission(usbDevice); + this.usbPid = UsbPid.fromValue(usbDevice.getProductId()); + + this.connectionManager = new ConnectionManager(usbManager, usbDevice); + this.usbDevice = usbDevice; + this.usbManager = usbManager; + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean hasPermission() { + return usbManager.hasPermission(usbDevice); + } + + /** + * Returns yubikey device attached to the android device with the android device acting as the USB + * host. It describes the capabilities of the USB device and allows to get properties/name/product + * id/manufacturer of device + * + * @return yubikey device connected over USB + */ + public UsbDevice getUsbDevice() { + return usbDevice; + } + + /** + * @return {@link UsbPid} for the device's product id + */ + public UsbPid getPid() { + return usbPid; + } + + @Override + public Transport getTransport() { + return Transport.USB; + } + + @Override + public boolean supportsConnection(Class connectionType) { + return connectionManager.supportsConnection(connectionType); + } + + @Override + public void requestConnection( + Class connectionType, Callback> callback) { + verifyAccess(connectionType); + + // Keep UsbOtpConnection open until another connection is needed, to prevent re-enumeration of + // the USB device. + if (OtpConnection.class.isAssignableFrom(connectionType)) { + @SuppressWarnings("unchecked") + Callback> otpCallback = + value -> callback.invoke((Result) value); + if (otpConnection == null) { + otpConnection = new CachedOtpConnection(otpCallback); + } else { + otpConnection.queue.offer(otpCallback); + } + } else { + if (otpConnection != null) { + otpConnection.close(); + otpConnection = null; + } + executorService.submit( + () -> { + try (T connection = connectionManager.openConnection(connectionType)) { + callback.invoke(Result.success(connection)); + } catch (IOException e) { + callback.invoke(Result.failure(e)); + } + }); } + } - /** - * Returns yubikey device attached to the android device with the android device acting as the USB host. - * It describes the capabilities of the USB device and allows to get properties/name/product id/manufacturer of device - * - * @return yubikey device connected over USB - */ - public UsbDevice getUsbDevice() { - return usbDevice; - } + @Override + public T openConnection(Class connectionType) + throws IOException { + verifyAccess(connectionType); - /** - * @return {@link UsbPid} for the device's product id - */ - public UsbPid getPid() { - return usbPid; - } + return connectionManager.openConnection(connectionType); + } - @Override - public Transport getTransport() { - return Transport.USB; + public void setOnClosed(Runnable onClosed) { + if (executorService.isTerminated()) { + onClosed.run(); + } else { + this.onClosed = onClosed; } - - @Override - public boolean supportsConnection(Class connectionType) { - return connectionManager.supportsConnection(connectionType); - } - - @Override - public void requestConnection(Class connectionType, Callback> callback) { - verifyAccess(connectionType); - - // Keep UsbOtpConnection open until another connection is needed, to prevent re-enumeration of the USB device. - if (OtpConnection.class.isAssignableFrom(connectionType)) { - @SuppressWarnings("unchecked") - Callback> otpCallback = value -> callback.invoke((Result) value); - if (otpConnection == null) { - otpConnection = new CachedOtpConnection(otpCallback); - } else { - otpConnection.queue.offer(otpCallback); - } - } else { - if (otpConnection != null) { - otpConnection.close(); - otpConnection = null; - } - executorService.submit(() -> { - try (T connection = connectionManager.openConnection(connectionType)) { - callback.invoke(Result.success(connection)); - } catch (IOException e) { - callback.invoke(Result.failure(e)); - } - }); - } + } + + @Override + public void close() { + Logger.debug(logger, "Closing YubiKey device"); + if (otpConnection != null) { + otpConnection.close(); + otpConnection = null; } - - @Override - public T openConnection(Class connectionType) throws IOException { - verifyAccess(connectionType); - - return connectionManager.openConnection(connectionType); + if (onClosed != null) { + executorService.submit(onClosed); } - - public void setOnClosed(Runnable onClosed) { - if (executorService.isTerminated()) { - onClosed.run(); - } else { - this.onClosed = onClosed; - } + executorService.shutdown(); + } + + private static final Callback> CLOSE_OTP = value -> {}; + + private class CachedOtpConnection implements Closeable { + private final LinkedBlockingQueue>> queue = + new LinkedBlockingQueue<>(); + + private CachedOtpConnection(Callback> callback) { + Logger.debug(logger, "Creating new CachedOtpConnection"); + queue.offer(callback); + executorService.submit( + () -> { + try (OtpConnection connection = connectionManager.openConnection(OtpConnection.class)) { + while (true) { + try { + Callback> action = queue.take(); + if (action == CLOSE_OTP) { + Logger.debug(logger, "Closing CachedOtpConnection"); + break; + } + try { + action.invoke(Result.success(connection)); + } catch (Exception e) { + Logger.error(logger, "OtpConnection callback threw an exception", e); + } + } catch (InterruptedException e) { + Logger.error(logger, "InterruptedException when processing OtpConnection: ", e); + } + } + } catch (IOException e) { + callback.invoke(Result.failure(e)); + } + }); } @Override public void close() { - Logger.debug(logger, "Closing YubiKey device"); - if (otpConnection != null) { - otpConnection.close(); - otpConnection = null; - } - if (onClosed != null) { - executorService.submit(onClosed); - } - executorService.shutdown(); - } - - private static final Callback> CLOSE_OTP = value -> { - }; - - private class CachedOtpConnection implements Closeable { - private final LinkedBlockingQueue>> queue = new LinkedBlockingQueue<>(); - - private CachedOtpConnection(Callback> callback) { - Logger.debug(logger, "Creating new CachedOtpConnection"); - queue.offer(callback); - executorService.submit(() -> { - try (OtpConnection connection = connectionManager.openConnection(OtpConnection.class)) { - while (true) { - try { - Callback> action = queue.take(); - if (action == CLOSE_OTP) { - Logger.debug(logger, "Closing CachedOtpConnection"); - break; - } - try { - action.invoke(Result.success(connection)); - } catch (Exception e) { - Logger.error(logger, "OtpConnection callback threw an exception", e); - } - } catch (InterruptedException e) { - Logger.error(logger, "InterruptedException when processing OtpConnection: ", e); - } - } - } catch (IOException e) { - callback.invoke(Result.failure(e)); - } - }); - } - - @Override - public void close() { - queue.offer(CLOSE_OTP); - } + queue.offer(CLOSE_OTP); } - - /** - * Throw if the device cannot create connections of the specified type. - * - * @param connectionType type of connection to verify - * @throws IllegalStateException if the USB permission has not been granted - * @throws IllegalStateException if the connectionType is not supported - */ - private void verifyAccess(Class connectionType) { - if (!hasPermission()) { - throw new IllegalStateException("Device access not permitted"); - } else if (!supportsConnection(connectionType)) { - throw new IllegalStateException("Unsupported connection type"); - } - } - - @Nonnull - @Override - public String toString() { - return "UsbYubiKeyDevice{" + - "usbDevice=" + usbDevice + - ", usbPid=" + usbPid + - '}'; + } + + /** + * Throw if the device cannot create connections of the specified type. + * + * @param connectionType type of connection to verify + * @throws IllegalStateException if the USB permission has not been granted + * @throws IllegalStateException if the connectionType is not supported + */ + private void verifyAccess(Class connectionType) { + if (!hasPermission()) { + throw new IllegalStateException("Device access not permitted"); + } else if (!supportsConnection(connectionType)) { + throw new IllegalStateException("Unsupported connection type"); } -} \ No newline at end of file + } + + @Nonnull + @Override + public String toString() { + return "UsbYubiKeyDevice{" + "usbDevice=" + usbDevice + ", usbPid=" + usbPid + '}'; + } +} diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/usb/UsbYubiKeyManager.java b/android/src/main/java/com/yubico/yubikit/android/transport/usb/UsbYubiKeyManager.java index f650f6ac..a064db2d 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/usb/UsbYubiKeyManager.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/usb/UsbYubiKeyManager.java @@ -19,7 +19,6 @@ import android.content.Context; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbManager; - import com.yubico.yubikit.android.transport.usb.connection.ConnectionManager; import com.yubico.yubikit.android.transport.usb.connection.FidoConnectionHandler; import com.yubico.yubikit.android.transport.usb.connection.OtpConnectionHandler; @@ -29,97 +28,103 @@ import com.yubico.yubikit.android.transport.usb.connection.UsbSmartCardConnection; import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.util.Callback; - -import org.slf4j.LoggerFactory; - import java.util.HashMap; import java.util.Map; - import javax.annotation.Nullable; +import org.slf4j.LoggerFactory; public class UsbYubiKeyManager { - static { - ConnectionManager.registerConnectionHandler(UsbSmartCardConnection.class, new SmartCardConnectionHandler()); - ConnectionManager.registerConnectionHandler(UsbOtpConnection.class, new OtpConnectionHandler()); - ConnectionManager.registerConnectionHandler(UsbFidoConnection.class, new FidoConnectionHandler()); - } - - private final Context context; - private final UsbManager usbManager; - @Nullable - private MyDeviceListener internalListener = null; - - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(UsbYubiKeyManager.class); - - public UsbYubiKeyManager(Context context) { - this.context = context; - usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); + static { + ConnectionManager.registerConnectionHandler( + UsbSmartCardConnection.class, new SmartCardConnectionHandler()); + ConnectionManager.registerConnectionHandler(UsbOtpConnection.class, new OtpConnectionHandler()); + ConnectionManager.registerConnectionHandler( + UsbFidoConnection.class, new FidoConnectionHandler()); + } + + private final Context context; + private final UsbManager usbManager; + @Nullable private MyDeviceListener internalListener = null; + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(UsbYubiKeyManager.class); + + public UsbYubiKeyManager(Context context) { + this.context = context; + usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); + } + + /** + * Registers receiver on usb connection event + * + * @param usbConfiguration contains information if device manager also registers receiver on + * permissions grant from user + * @param listener the UsbSessionListener to react to changes + */ + public synchronized void enable( + UsbConfiguration usbConfiguration, Callback listener) { + disable(); + internalListener = new MyDeviceListener(usbConfiguration, listener); + UsbDeviceManager.registerUsbListener(context, internalListener); + } + + public synchronized void disable() { + if (internalListener != null) { + UsbDeviceManager.unregisterUsbListener(context, internalListener); + internalListener = null; } + } - /** - * Registers receiver on usb connection event - * - * @param usbConfiguration contains information if device manager also registers receiver on permissions grant from user - * @param listener the UsbSessionListener to react to changes - */ - public synchronized void enable(UsbConfiguration usbConfiguration, Callback listener) { - disable(); - internalListener = new MyDeviceListener(usbConfiguration, listener); - UsbDeviceManager.registerUsbListener(context, internalListener); - } + private class MyDeviceListener implements UsbDeviceManager.UsbDeviceListener { + private final Callback listener; + private final UsbConfiguration usbConfiguration; + private final Map devices = new HashMap<>(); - public synchronized void disable() { - if (internalListener != null) { - UsbDeviceManager.unregisterUsbListener(context, internalListener); - internalListener = null; - } + private MyDeviceListener( + UsbConfiguration usbConfiguration, Callback listener) { + this.usbConfiguration = usbConfiguration; + this.listener = listener; } - private class MyDeviceListener implements UsbDeviceManager.UsbDeviceListener { - private final Callback listener; - private final UsbConfiguration usbConfiguration; - private final Map devices = new HashMap<>(); - - private MyDeviceListener(UsbConfiguration usbConfiguration, Callback listener) { - this.usbConfiguration = usbConfiguration; - this.listener = listener; - } - - @Override - public void deviceAttached(UsbDevice usbDevice) { - - try { - UsbYubiKeyDevice yubikey = new UsbYubiKeyDevice(usbManager, usbDevice); - devices.put(usbDevice, yubikey); - - if (usbConfiguration.isHandlePermissions() && !yubikey.hasPermission()) { - Logger.debug(logger, "request permission"); - UsbDeviceManager.requestPermission(context, usbDevice, (usbDevice1, hasPermission) -> { - Logger.debug(logger, "permission result {}", hasPermission); - if (hasPermission) { - synchronized (UsbYubiKeyManager.this) { - if (internalListener == this) { - listener.invoke(yubikey); - } - } - } - }); - } else { - listener.invoke(yubikey); + @Override + public void deviceAttached(UsbDevice usbDevice) { + + try { + UsbYubiKeyDevice yubikey = new UsbYubiKeyDevice(usbManager, usbDevice); + devices.put(usbDevice, yubikey); + + if (usbConfiguration.isHandlePermissions() && !yubikey.hasPermission()) { + Logger.debug(logger, "request permission"); + UsbDeviceManager.requestPermission( + context, + usbDevice, + (usbDevice1, hasPermission) -> { + Logger.debug(logger, "permission result {}", hasPermission); + if (hasPermission) { + synchronized (UsbYubiKeyManager.this) { + if (internalListener == this) { + listener.invoke(yubikey); + } + } } - } catch (IllegalArgumentException ignored) { - Logger.debug(logger, "Attached usbDevice(vid={},pid={}) is not recognized as a valid YubiKey", - usbDevice.getVendorId(), usbDevice.getProductId()); - } - + }); + } else { + listener.invoke(yubikey); } + } catch (IllegalArgumentException ignored) { + Logger.debug( + logger, + "Attached usbDevice(vid={},pid={}) is not recognized as a valid YubiKey", + usbDevice.getVendorId(), + usbDevice.getProductId()); + } + } - @Override - public void deviceRemoved(UsbDevice usbDevice) { - UsbYubiKeyDevice yubikey = devices.remove(usbDevice); - if (yubikey != null) { - yubikey.close(); - } - } + @Override + public void deviceRemoved(UsbDevice usbDevice) { + UsbYubiKeyDevice yubikey = devices.remove(usbDevice); + if (yubikey != null) { + yubikey.close(); + } } + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/ConnectionHandler.java b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/ConnectionHandler.java index 662cae52..2f6674b8 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/ConnectionHandler.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/ConnectionHandler.java @@ -18,13 +18,12 @@ import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; - import com.yubico.yubikit.core.YubiKeyConnection; - import java.io.IOException; public interface ConnectionHandler { - boolean isAvailable(UsbDevice usbDevice); + boolean isAvailable(UsbDevice usbDevice); - T createConnection(UsbDevice usbDevice, UsbDeviceConnection usbDeviceConnection) throws IOException; -} \ No newline at end of file + T createConnection(UsbDevice usbDevice, UsbDeviceConnection usbDeviceConnection) + throws IOException; +} diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/ConnectionManager.java b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/ConnectionManager.java index b51b5de1..cdfd7b82 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/ConnectionManager.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/ConnectionManager.java @@ -19,93 +19,95 @@ import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbManager; - import androidx.annotation.WorkerThread; - import com.yubico.yubikit.android.transport.usb.NoPermissionsException; import com.yubico.yubikit.core.YubiKeyConnection; - import java.io.IOException; import java.util.HashMap; import java.util.Map; - import javax.annotation.Nullable; public class ConnectionManager { - private static final Map, ConnectionHandler> handlers = new HashMap<>(); + private static final Map, ConnectionHandler> handlers = + new HashMap<>(); - /** - * Registers a new ConnectionHandler for creating YubiKeyConnections. - * - * @param connectionClass the type of connection created by the handler - * @param handler the handler responsible for creating connections - * @param the type of connection created by the handler - */ - public static void registerConnectionHandler(Class connectionClass, ConnectionHandler handler) { - synchronized (handlers) { - handlers.put(connectionClass, handler); - } + /** + * Registers a new ConnectionHandler for creating YubiKeyConnections. + * + * @param connectionClass the type of connection created by the handler + * @param handler the handler responsible for creating connections + * @param the type of connection created by the handler + */ + public static void registerConnectionHandler( + Class connectionClass, ConnectionHandler handler) { + synchronized (handlers) { + handlers.put(connectionClass, handler); } + } - private final UsbManager usbManager; - private final UsbDevice usbDevice; + private final UsbManager usbManager; + private final UsbDevice usbDevice; - public ConnectionManager(UsbManager usbManager, UsbDevice usbDevice) { - this.usbManager = usbManager; - this.usbDevice = usbDevice; - } + public ConnectionManager(UsbManager usbManager, UsbDevice usbDevice) { + this.usbManager = usbManager; + this.usbDevice = usbDevice; + } - /** - * Checks to see if a given connection type is supported - * - * @param connectionType the type of connection to check support for - * @return true if the connection type is supported - */ - public boolean supportsConnection(Class connectionType) { - ConnectionHandler handler = getHandler(connectionType); - return handler != null && handler.isAvailable(usbDevice); - } + /** + * Checks to see if a given connection type is supported + * + * @param connectionType the type of connection to check support for + * @return true if the connection type is supported + */ + public boolean supportsConnection(Class connectionType) { + ConnectionHandler handler = getHandler(connectionType); + return handler != null && handler.isAvailable(usbDevice); + } - /** - * TODO: fixme - * Checks if a connection type is supported by the device, attempts to acquire the connection lock, and returns a connection. - * - * @param connectionType the type of connection to open - * @param the type of connection to open - */ - @WorkerThread - public T openConnection(Class connectionType) throws IOException { - ConnectionHandler handler = getHandler(connectionType); - if (handler != null) { - UsbDeviceConnection usbDeviceConnection = openDeviceConnection(usbDevice); - try { - return handler.createConnection(usbDevice, usbDeviceConnection); - } catch (IOException e) { - usbDeviceConnection.close(); - throw e; - } - } - throw new IllegalStateException("The connection type is not available via this transport"); + /** + * TODO: fixme + * + *

Checks if a connection type is supported by the device, attempts to acquire the connection + * lock, and returns a connection. + * + * @param connectionType the type of connection to open + * @param the type of connection to open + */ + @WorkerThread + public T openConnection(Class connectionType) + throws IOException { + ConnectionHandler handler = getHandler(connectionType); + if (handler != null) { + UsbDeviceConnection usbDeviceConnection = openDeviceConnection(usbDevice); + try { + return handler.createConnection(usbDevice, usbDeviceConnection); + } catch (IOException e) { + usbDeviceConnection.close(); + throw e; + } } + throw new IllegalStateException("The connection type is not available via this transport"); + } - @Nullable - private ConnectionHandler getHandler(Class connectionType) { - synchronized (handlers) { - for (Map.Entry, ConnectionHandler> entry : handlers.entrySet()) { - if (connectionType.isAssignableFrom(entry.getKey())) { - @SuppressWarnings("unchecked") - ConnectionHandler entryValue = (ConnectionHandler) entry.getValue(); - return entryValue; - } - } + @Nullable private ConnectionHandler getHandler(Class connectionType) { + synchronized (handlers) { + for (Map.Entry< + Class, ConnectionHandler> + entry : handlers.entrySet()) { + if (connectionType.isAssignableFrom(entry.getKey())) { + @SuppressWarnings("unchecked") + ConnectionHandler entryValue = (ConnectionHandler) entry.getValue(); + return entryValue; } - return null; + } } + return null; + } - private UsbDeviceConnection openDeviceConnection(UsbDevice usbDevice) throws IOException { - if (!usbManager.hasPermission(usbDevice)) { - throw new NoPermissionsException(usbDevice); - } - return usbManager.openDevice(usbDevice); + private UsbDeviceConnection openDeviceConnection(UsbDevice usbDevice) throws IOException { + if (!usbManager.hasPermission(usbDevice)) { + throw new NoPermissionsException(usbDevice); } + return usbManager.openDevice(usbDevice); + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/FidoConnectionHandler.java b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/FidoConnectionHandler.java index ed1e43ef..bf2f0770 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/FidoConnectionHandler.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/FidoConnectionHandler.java @@ -22,60 +22,61 @@ import android.hardware.usb.UsbEndpoint; import android.hardware.usb.UsbInterface; import android.util.Pair; - -import javax.annotation.Nullable; - import java.io.IOException; import java.util.Objects; +import javax.annotation.Nullable; public class FidoConnectionHandler extends InterfaceConnectionHandler { - public FidoConnectionHandler() { - super(UsbConstants.USB_CLASS_HID, 0); - } + public FidoConnectionHandler() { + super(UsbConstants.USB_CLASS_HID, 0); + } - @Override - public UsbFidoConnection createConnection(UsbDevice usbDevice, UsbDeviceConnection usbDeviceConnection) throws IOException { - UsbInterface usbInterface = getClaimedInterface(usbDevice, usbDeviceConnection); - Pair endpoints = findEndpoints(usbInterface); - return new UsbFidoConnection(usbDeviceConnection, usbInterface, endpoints.first, endpoints.second); - } + @Override + public UsbFidoConnection createConnection( + UsbDevice usbDevice, UsbDeviceConnection usbDeviceConnection) throws IOException { + UsbInterface usbInterface = getClaimedInterface(usbDevice, usbDeviceConnection); + Pair endpoints = findEndpoints(usbInterface); + return new UsbFidoConnection( + usbDeviceConnection, usbInterface, endpoints.first, endpoints.second); + } - protected UsbInterface getClaimedInterface(UsbDevice usbDevice, UsbDeviceConnection usbDeviceConnection) throws IOException { - UsbInterface usbInterface = getInterface(usbDevice); - if (usbInterface != null) { - if (!usbDeviceConnection.claimInterface(usbInterface, true)) { - throw new IOException("Unable to claim interface"); - } - return usbInterface; - } - throw new IllegalStateException("The connection type is not available via this transport"); + protected UsbInterface getClaimedInterface( + UsbDevice usbDevice, UsbDeviceConnection usbDeviceConnection) throws IOException { + UsbInterface usbInterface = getInterface(usbDevice); + if (usbInterface != null) { + if (!usbDeviceConnection.claimInterface(usbInterface, true)) { + throw new IOException("Unable to claim interface"); + } + return usbInterface; } + throw new IllegalStateException("The connection type is not available via this transport"); + } - @Nullable - private UsbInterface getInterface(UsbDevice usbDevice) { - for (int i = 0; i < usbDevice.getInterfaceCount(); i++) { - UsbInterface usbInterface = usbDevice.getInterface(i); - if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID && usbInterface.getInterfaceSubclass() == 0) { - return usbInterface; - } - } - return null; + @Nullable private UsbInterface getInterface(UsbDevice usbDevice) { + for (int i = 0; i < usbDevice.getInterfaceCount(); i++) { + UsbInterface usbInterface = usbDevice.getInterface(i); + if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID + && usbInterface.getInterfaceSubclass() == 0) { + return usbInterface; + } } + return null; + } - private static Pair findEndpoints(UsbInterface usbInterface) { - UsbEndpoint endpointIn = null; - UsbEndpoint endpointOut = null; + private static Pair findEndpoints(UsbInterface usbInterface) { + UsbEndpoint endpointIn = null; + UsbEndpoint endpointOut = null; - for (int i = 0; i < usbInterface.getEndpointCount(); i++) { - UsbEndpoint endpoint = usbInterface.getEndpoint(i); - if (endpoint.getType() == UsbConstants.USB_ENDPOINT_XFER_INT) { - if (endpoint.getDirection() == UsbConstants.USB_DIR_IN) { - endpointIn = endpoint; - } else { - endpointOut = endpoint; - } - } + for (int i = 0; i < usbInterface.getEndpointCount(); i++) { + UsbEndpoint endpoint = usbInterface.getEndpoint(i); + if (endpoint.getType() == UsbConstants.USB_ENDPOINT_XFER_INT) { + if (endpoint.getDirection() == UsbConstants.USB_DIR_IN) { + endpointIn = endpoint; + } else { + endpointOut = endpoint; } - return new Pair<>(Objects.requireNonNull(endpointIn), Objects.requireNonNull(endpointOut)); + } } + return new Pair<>(Objects.requireNonNull(endpointIn), Objects.requireNonNull(endpointOut)); + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/InterfaceConnectionHandler.java b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/InterfaceConnectionHandler.java index 9d86ad24..6f6154c2 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/InterfaceConnectionHandler.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/InterfaceConnectionHandler.java @@ -19,46 +19,45 @@ import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbInterface; - import com.yubico.yubikit.core.YubiKeyConnection; - import java.io.IOException; - import javax.annotation.Nullable; -abstract class InterfaceConnectionHandler implements ConnectionHandler { - private final int interfaceClass; - private final int interfaceSubclass; - - protected InterfaceConnectionHandler(int interfaceClass, int interfaceSubclass) { - this.interfaceClass = interfaceClass; - this.interfaceSubclass = interfaceSubclass; +abstract class InterfaceConnectionHandler + implements ConnectionHandler { + private final int interfaceClass; + private final int interfaceSubclass; + + protected InterfaceConnectionHandler(int interfaceClass, int interfaceSubclass) { + this.interfaceClass = interfaceClass; + this.interfaceSubclass = interfaceSubclass; + } + + @Override + public boolean isAvailable(UsbDevice usbDevice) { + return getInterface(usbDevice) != null; + } + + protected UsbInterface getClaimedInterface( + UsbDevice usbDevice, UsbDeviceConnection usbDeviceConnection) throws IOException { + UsbInterface usbInterface = getInterface(usbDevice); + if (usbInterface != null) { + if (!usbDeviceConnection.claimInterface(usbInterface, true)) { + throw new IOException("Unable to claim interface"); + } + return usbInterface; } - - @Override - public boolean isAvailable(UsbDevice usbDevice) { - return getInterface(usbDevice) != null; - } - - protected UsbInterface getClaimedInterface(UsbDevice usbDevice, UsbDeviceConnection usbDeviceConnection) throws IOException { - UsbInterface usbInterface = getInterface(usbDevice); - if (usbInterface != null) { - if (!usbDeviceConnection.claimInterface(usbInterface, true)) { - throw new IOException("Unable to claim interface"); - } - return usbInterface; - } - throw new IllegalStateException("The connection type is not available via this transport"); - } - - @Nullable - private UsbInterface getInterface(UsbDevice usbDevice) { - for (int i = 0; i < usbDevice.getInterfaceCount(); i++) { - UsbInterface usbInterface = usbDevice.getInterface(i); - if (usbInterface.getInterfaceClass() == interfaceClass && usbInterface.getInterfaceSubclass() == interfaceSubclass) { - return usbInterface; - } - } - return null; + throw new IllegalStateException("The connection type is not available via this transport"); + } + + @Nullable private UsbInterface getInterface(UsbDevice usbDevice) { + for (int i = 0; i < usbDevice.getInterfaceCount(); i++) { + UsbInterface usbInterface = usbDevice.getInterface(i); + if (usbInterface.getInterfaceClass() == interfaceClass + && usbInterface.getInterfaceSubclass() == interfaceSubclass) { + return usbInterface; + } } + return null; + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/OtpConnectionHandler.java b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/OtpConnectionHandler.java index 16d2e3d3..4b168d99 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/OtpConnectionHandler.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/OtpConnectionHandler.java @@ -19,16 +19,17 @@ import android.hardware.usb.UsbConstants; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; - import java.io.IOException; public class OtpConnectionHandler extends InterfaceConnectionHandler { - public OtpConnectionHandler() { - super(UsbConstants.USB_CLASS_HID, UsbConstants.USB_INTERFACE_SUBCLASS_BOOT); - } + public OtpConnectionHandler() { + super(UsbConstants.USB_CLASS_HID, UsbConstants.USB_INTERFACE_SUBCLASS_BOOT); + } - @Override - public UsbOtpConnection createConnection(UsbDevice usbDevice, UsbDeviceConnection usbDeviceConnection) throws IOException { - return new UsbOtpConnection(usbDeviceConnection, getClaimedInterface(usbDevice, usbDeviceConnection)); - } + @Override + public UsbOtpConnection createConnection( + UsbDevice usbDevice, UsbDeviceConnection usbDeviceConnection) throws IOException { + return new UsbOtpConnection( + usbDeviceConnection, getClaimedInterface(usbDevice, usbDeviceConnection)); + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/SmartCardConnectionHandler.java b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/SmartCardConnectionHandler.java index 32e0103f..27a558e5 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/SmartCardConnectionHandler.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/SmartCardConnectionHandler.java @@ -22,38 +22,39 @@ import android.hardware.usb.UsbEndpoint; import android.hardware.usb.UsbInterface; import android.util.Pair; - import java.io.IOException; public class SmartCardConnectionHandler extends InterfaceConnectionHandler { - public SmartCardConnectionHandler() { - super(UsbConstants.USB_CLASS_CSCID, 0); - } + public SmartCardConnectionHandler() { + super(UsbConstants.USB_CLASS_CSCID, 0); + } - @Override - public UsbSmartCardConnection createConnection(UsbDevice usbDevice, UsbDeviceConnection usbDeviceConnection) throws IOException { - UsbInterface usbInterface = getClaimedInterface(usbDevice, usbDeviceConnection); - Pair endpoints = findEndpoints(usbInterface); - return new UsbSmartCardConnection(usbDeviceConnection, usbInterface, endpoints.first, endpoints.second); - } + @Override + public UsbSmartCardConnection createConnection( + UsbDevice usbDevice, UsbDeviceConnection usbDeviceConnection) throws IOException { + UsbInterface usbInterface = getClaimedInterface(usbDevice, usbDeviceConnection); + Pair endpoints = findEndpoints(usbInterface); + return new UsbSmartCardConnection( + usbDeviceConnection, usbInterface, endpoints.first, endpoints.second); + } - private Pair findEndpoints(UsbInterface usbInterface) { - UsbEndpoint endpointIn = null; - UsbEndpoint endpointOut = null; + private Pair findEndpoints(UsbInterface usbInterface) { + UsbEndpoint endpointIn = null; + UsbEndpoint endpointOut = null; - for (int i = 0; i < usbInterface.getEndpointCount(); i++) { - UsbEndpoint endpoint = usbInterface.getEndpoint(i); - if (endpoint.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) { - if (endpoint.getDirection() == UsbConstants.USB_DIR_IN) { - endpointIn = endpoint; - } else { - endpointOut = endpoint; - } - } + for (int i = 0; i < usbInterface.getEndpointCount(); i++) { + UsbEndpoint endpoint = usbInterface.getEndpoint(i); + if (endpoint.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) { + if (endpoint.getDirection() == UsbConstants.USB_DIR_IN) { + endpointIn = endpoint; + } else { + endpointOut = endpoint; } - if (endpointIn != null && endpointOut != null) { - return new Pair<>(endpointIn, endpointOut); - } - throw new IllegalStateException("Missing CCID bulk endpoints"); + } + } + if (endpointIn != null && endpointOut != null) { + return new Pair<>(endpointIn, endpointOut); } + throw new IllegalStateException("Missing CCID bulk endpoints"); + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/UsbFidoConnection.java b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/UsbFidoConnection.java index 1df67fbf..7999c331 100644 --- a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/UsbFidoConnection.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/UsbFidoConnection.java @@ -8,38 +8,40 @@ import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbEndpoint; import android.hardware.usb.UsbInterface; - import com.yubico.yubikit.core.fido.FidoConnection; - import java.io.IOException; public class UsbFidoConnection extends UsbYubiKeyConnection implements FidoConnection { - private static final int TIMEOUT = 1000; - - private final UsbDeviceConnection connection; - private final UsbEndpoint bulkIn; - private final UsbEndpoint bulkOut; - - UsbFidoConnection(UsbDeviceConnection connection, UsbInterface intf, UsbEndpoint endpointIn, UsbEndpoint endpointOut) { - super(connection, intf); - this.connection = connection; - this.bulkIn = endpointIn; - this.bulkOut = endpointOut; - } - - @Override - public void send(byte[] packet) throws IOException { - int sent = connection.bulkTransfer(bulkOut, packet, packet.length, TIMEOUT); - if (sent != FidoConnection.PACKET_SIZE) { - throw new IOException("Failed to send full packed"); - } + private static final int TIMEOUT = 1000; + + private final UsbDeviceConnection connection; + private final UsbEndpoint bulkIn; + private final UsbEndpoint bulkOut; + + UsbFidoConnection( + UsbDeviceConnection connection, + UsbInterface intf, + UsbEndpoint endpointIn, + UsbEndpoint endpointOut) { + super(connection, intf); + this.connection = connection; + this.bulkIn = endpointIn; + this.bulkOut = endpointOut; + } + + @Override + public void send(byte[] packet) throws IOException { + int sent = connection.bulkTransfer(bulkOut, packet, packet.length, TIMEOUT); + if (sent != FidoConnection.PACKET_SIZE) { + throw new IOException("Failed to send full packed"); } + } - @Override - public void receive(byte[] packet) throws IOException { - int read = connection.bulkTransfer(bulkIn, packet, packet.length, TIMEOUT); - if (read != FidoConnection.PACKET_SIZE) { - throw new IOException("Failed to read full packed"); - } + @Override + public void receive(byte[] packet) throws IOException { + int read = connection.bulkTransfer(bulkIn, packet, packet.length, TIMEOUT); + if (read != FidoConnection.PACKET_SIZE) { + throw new IOException("Failed to read full packed"); } + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/UsbOtpConnection.java b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/UsbOtpConnection.java index 1a83d5f4..7656358f 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/UsbOtpConnection.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/UsbOtpConnection.java @@ -19,81 +19,87 @@ import android.hardware.usb.UsbConstants; import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbInterface; - import com.yubico.yubikit.core.otp.OtpConnection; - import java.io.IOException; /** * Class that provides interface to read and send data over YubiKey HID (keyboard) interface - *

- * NOTE: when we release HID interface YubiKey will be recognized as keyboard again, - * it may give you a flash of UI on Android (notification how to handle Keyboard) - * which means your active Activity may got to background for a moment - * be aware of that and make sure that your app can handle that. + * + *

NOTE: when we release HID interface YubiKey will be recognized as keyboard again, it may give + * you a flash of UI on Android (notification how to handle Keyboard) which means your active + * Activity may got to background for a moment. Be aware of that and make sure that your app can + * handle that. */ public class UsbOtpConnection extends UsbYubiKeyConnection implements OtpConnection { - private static final int TIMEOUT = 1000; + private static final int TIMEOUT = 1000; - private static final int TYPE_CLASS = 0x20; - private static final int RECIPIENT_INTERFACE = 0x01; - private static final int HID_GET_REPORT = 0x01; - private static final int HID_SET_REPORT = 0x09; - private static final int REPORT_TYPE_FEATURE = 0x03; + private static final int TYPE_CLASS = 0x20; + private static final int RECIPIENT_INTERFACE = 0x01; + private static final int HID_GET_REPORT = 0x01; + private static final int HID_SET_REPORT = 0x09; + private static final int REPORT_TYPE_FEATURE = 0x03; - private final UsbDeviceConnection connection; - private final UsbInterface hidInterface; + private final UsbDeviceConnection connection; + private final UsbInterface hidInterface; - private boolean closed = false; + private boolean closed = false; - /** - * Sets endpoints and connection - * - * @param connection open usb connection - * @param hidInterface HID interface that was claimed - */ - UsbOtpConnection(UsbDeviceConnection connection, UsbInterface hidInterface) { - super(connection, hidInterface); - this.connection = connection; - this.hidInterface = hidInterface; - } + /** + * Sets endpoints and connection + * + * @param connection open usb connection + * @param hidInterface HID interface that was claimed + */ + UsbOtpConnection(UsbDeviceConnection connection, UsbInterface hidInterface) { + super(connection, hidInterface); + this.connection = connection; + this.hidInterface = hidInterface; + } - @Override - public void receive(byte[] report) throws IOException { - int received = connection.controlTransfer(UsbConstants.USB_DIR_IN | TYPE_CLASS | RECIPIENT_INTERFACE, HID_GET_REPORT, - REPORT_TYPE_FEATURE << 8, hidInterface.getId(), report, report.length, TIMEOUT); - if (received != FEATURE_REPORT_SIZE) { - throw new IOException("Unexpected amount of data read: " + received); - } + @Override + public void receive(byte[] report) throws IOException { + int received = + connection.controlTransfer( + UsbConstants.USB_DIR_IN | TYPE_CLASS | RECIPIENT_INTERFACE, + HID_GET_REPORT, + REPORT_TYPE_FEATURE << 8, + hidInterface.getId(), + report, + report.length, + TIMEOUT); + if (received != FEATURE_REPORT_SIZE) { + throw new IOException("Unexpected amount of data read: " + received); } + } - /** - * Write single feature report - * - * @param report blob size of FEATURE_RPT_SIZE - */ - @Override - public void send(byte[] report) throws IOException { - int sent = connection.controlTransfer( - UsbConstants.USB_DIR_OUT | TYPE_CLASS | RECIPIENT_INTERFACE, - HID_SET_REPORT, REPORT_TYPE_FEATURE << 8, - hidInterface.getId(), - report, - report.length, - TIMEOUT - ); - if (sent != FEATURE_REPORT_SIZE) { - throw new IOException("Unexpected amount of data sent: " + sent); - } + /** + * Write single feature report + * + * @param report blob size of FEATURE_RPT_SIZE + */ + @Override + public void send(byte[] report) throws IOException { + int sent = + connection.controlTransfer( + UsbConstants.USB_DIR_OUT | TYPE_CLASS | RECIPIENT_INTERFACE, + HID_SET_REPORT, + REPORT_TYPE_FEATURE << 8, + hidInterface.getId(), + report, + report.length, + TIMEOUT); + if (sent != FEATURE_REPORT_SIZE) { + throw new IOException("Unexpected amount of data sent: " + sent); } + } - @Override - public void close() { - closed = true; - super.close(); - } + @Override + public void close() { + closed = true; + super.close(); + } - public boolean isClosed() { - return closed; - } + public boolean isClosed() { + return closed; + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/UsbSmartCardConnection.java b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/UsbSmartCardConnection.java index 0ef2485d..532c6171 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/UsbSmartCardConnection.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/UsbSmartCardConnection.java @@ -19,279 +19,308 @@ import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbEndpoint; import android.hardware.usb.UsbInterface; - -import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.Transport; +import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.core.util.StringUtils; - -import org.slf4j.LoggerFactory; - +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; import java.util.Locale; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.slf4j.LoggerFactory; /** * USB service for interacting with the YubiKey - * https://www.usb.org/sites/default/files/DWG_Smart-Card_CCID_Rev110.pdf + * + * @see https://www.usb.org/sites/default/files/DWG_Smart-Card_CCID_Rev110.pdf */ public class UsbSmartCardConnection extends UsbYubiKeyConnection implements SmartCardConnection { - private static final int TIMEOUT = 1000; - - /** - * Command Pipe, Bulk-OUT Messages - * Message Name type - * PC_to_RDR_IccPowerOn 62h - * PC_to_RDR_IccPowerOff 63h - * PC_to_RDR_GetSlotStatus 65h - * PC_to_RDR_XfrBlock 6Fh - * PC_to_RDR_GetParameters 6Ch - * PC_to_RDR_ResetParameters 6Dh - * PC_to_RDR_SetParameters 61h - * PC_to_RDR_Escape 6Bh - * PC_to_RDR_IccClock 6Eh - * PC_to_RDR_T0APDU 6Ah - * PC_to_RDR_Secure 69h - * PC_to_RDR_Mechanical 71h - * PC_to_RDR_Abort 72h - * PC_to_RDR_SetDataRateAndClockFrequency 73h - */ - private static final byte POWER_ON_MESSAGE_TYPE = (byte) 0x62; - private static final byte REQUEST_MESSAGE_TYPE = (byte) 0x6f; - private static final byte RESPONSE_DATA_BLOCK = (byte) 0x80; - - private static final byte STATUS_TIME_EXTENSION = (byte) 0x80; - - private final UsbDeviceConnection connection; - private final UsbEndpoint endpointOut, endpointIn; - private final byte[] atr; - - private byte sequence = 0; - - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(UsbSmartCardConnection.class); - - /** - * Sets endpoints and connection and sends power on command - * if ATR is invalid then throws YubikeyCommunicationException - * - * @param connection open usb connection - * @param ccidInterface ccid interface that was claimed - * @param endpointIn channel for sending data over USB. - * @param endpointOut channel for receiving data over USB. - */ - UsbSmartCardConnection(UsbDeviceConnection connection, UsbInterface ccidInterface, UsbEndpoint endpointIn, UsbEndpoint endpointOut) throws IOException { - super(connection, ccidInterface); - - this.connection = connection; - this.endpointIn = endpointIn; - this.endpointOut = endpointOut; - // PC_to_RDR_IccPowerOn command makes the slot "active" if it was "inactive" - atr = transceive(POWER_ON_MESSAGE_TYPE, new byte[0]); + private static final int TIMEOUT = 1000; + + /** + * Command Pipe, Bulk-OUT Messages + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Message Nametype
PC_to_RDR_IccPowerOn62h
PC_to_RDR_IccPowerOff63h
PC_to_RDR_GetSlotStatus65h
PC_to_RDR_XfrBlock6Fh
PC_to_RDR_GetParameters6Ch
PC_to_RDR_ResetParameters6Dh
PC_to_RDR_SetParameters61h
PC_to_RDR_Escape6Bh
PC_to_RDR_IccClock6Eh
PC_to_RDR_T0APDU6Ah
PC_to_RDR_Secure69h
PC_to_RDR_Mechanical71h
PC_to_RDR_Abort72h
PC_to_RDR_SetDataRateAndClockFrequency73h
+ */ + private static final byte POWER_ON_MESSAGE_TYPE = (byte) 0x62; + + private static final byte REQUEST_MESSAGE_TYPE = (byte) 0x6f; + private static final byte RESPONSE_DATA_BLOCK = (byte) 0x80; + + private static final byte STATUS_TIME_EXTENSION = (byte) 0x80; + + private final UsbDeviceConnection connection; + private final UsbEndpoint endpointOut, endpointIn; + private final byte[] atr; + + private byte sequence = 0; + + private static final org.slf4j.Logger logger = + LoggerFactory.getLogger(UsbSmartCardConnection.class); + + /** + * Sets endpoints and connection and sends power on command if ATR is invalid then throws + * YubikeyCommunicationException + * + * @param connection open usb connection + * @param ccidInterface ccid interface that was claimed + * @param endpointIn channel for sending data over USB. + * @param endpointOut channel for receiving data over USB. + */ + UsbSmartCardConnection( + UsbDeviceConnection connection, + UsbInterface ccidInterface, + UsbEndpoint endpointIn, + UsbEndpoint endpointOut) + throws IOException { + super(connection, ccidInterface); + + this.connection = connection; + this.endpointIn = endpointIn; + this.endpointOut = endpointOut; + // PC_to_RDR_IccPowerOn command makes the slot "active" if it was "inactive" + atr = transceive(POWER_ON_MESSAGE_TYPE, new byte[0]); + } + + @Override + public Transport getTransport() { + return Transport.USB; + } + + /** + * This connection generally supports Extended length APDUs. This can be limited by firmware + * version of connected YubiKey. + */ + @Override + public boolean isExtendedLengthApduSupported() { + return true; + } + + @Override + public byte[] sendAndReceive(byte[] apdu) throws IOException { + return transceive(REQUEST_MESSAGE_TYPE, apdu); + } + + @Override + public byte[] getAtr() { + return atr.clone(); + } + + /** + * Does the data exchange between phone and connected usb device with bulk messages All bulk + * messages begin with a 10-bytes header, followed by message-specific data. + * + * @param type the message type identifies the message + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Message Nametype
PC_to_RDR_IccPowerOn62h
PC_to_RDR_IccPowerOff63h
PC_to_RDR_GetSlotStatus65h
PC_to_RDR_XfrBlock6Fh
PC_to_RDR_GetParameters6Ch
PC_to_RDR_ResetParameters6Dh
PC_to_RDR_SetParameters61h
PC_to_RDR_Escape6Bh
PC_to_RDR_IccClock6Eh
PC_to_RDR_T0APDU6Ah
PC_to_RDR_Secure69h
PC_to_RDR_Mechanical71h
PC_to_RDR_Abort72h
PC_to_RDR_SetDataRateAndClockFrequency73h
+ * + * @param data message-specific data that needs to be sent to usb device + * @return received message-specific data from usb device + * @throws IOException in case if there is communication error occurs or received data is invalid + */ + private byte[] transceive(byte type, byte[] data) throws IOException { + // 1. prepare data for sending + MessageHeader prefix = new MessageHeader(type, data.length, sequence++); + ByteBuffer byteBuffer = + ByteBuffer.allocate(prefix.size() + data.length) + .order(ByteOrder.LITTLE_ENDIAN) + .put(prefix.array()) + .put(data); + + // 2. sent data to device + byte[] bufferOut = byteBuffer.array(); + int bytesSent = 0; + int bytesSentPackage = 0; + while (bytesSent < bufferOut.length || bytesSentPackage == endpointOut.getMaxPacketSize()) { + bytesSentPackage = + connection.bulkTransfer( + endpointOut, bufferOut, bytesSent, bufferOut.length - bytesSent, TIMEOUT); + if (bytesSentPackage > 0) { + Logger.trace( + logger, + "{} bytes sent over ccid: {}", + bytesSentPackage, + StringUtils.bytesToHex(bufferOut, bytesSent, bytesSentPackage)); + bytesSent += bytesSentPackage; + } else if (bytesSentPackage < 0) { + throw new IOException("Failed to send " + (bufferOut.length - bytesSent) + " bytes"); + } else { + // 0 is still considered as success in bulkTransfer description + // Scenario: if last package size was equal to endpointOut.getMaxPacketSize() + // we are sending empty package after that to notify end of bulk transfer + break; + } } - @Override - public Transport getTransport() { - return Transport.USB; + // 3. read data from device until we receive non-full packet/blob + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + int bytesRead; + MessageHeader messageHeader = null; + + boolean receivedExpectedPrefix = false; + byte[] bufferRead = new byte[endpointIn.getMaxPacketSize()]; + boolean responseRequiresTimeExtension = false; + do { + bytesRead = connection.bulkTransfer(endpointIn, bufferRead, bufferRead.length, TIMEOUT); + if (bytesRead > 0) { + Logger.trace( + logger, + "{} bytes received: {}", + bytesRead, + StringUtils.bytesToHex(bufferRead, 0, bytesRead)); + + if (receivedExpectedPrefix) { + stream.write(bufferRead, 0, bytesRead); + } else { + // 4. parse received data and make sure it's proper format + messageHeader = new MessageHeader(bufferRead); + responseRequiresTimeExtension = + (messageHeader.status & STATUS_TIME_EXTENSION) == STATUS_TIME_EXTENSION; + if (messageHeader.verify((byte) (sequence - 1))) { + // if we received expected prefix we can save the rest of received data without + // verification + receivedExpectedPrefix = true; + stream.write(bufferRead, 0, bytesRead); + } else if (messageHeader.error != 0 && !responseRequiresTimeExtension) { + Logger.debug( + logger, + "Invalid response from card reader bStatus={} and bError={}", + String.format(Locale.ROOT, "0x%02X", messageHeader.status), + String.format(Locale.ROOT, "0x%02X", messageHeader.error)); + throw new IOException("Invalid response from card reader"); + } + } + } else if (bytesRead < 0) { + throw new IOException("Failed to read response"); + } + } while ((bytesRead > 0 && bytesRead == bufferRead.length) || responseRequiresTimeExtension); + + // 5. prepare data for returning to user + byte[] output = stream.toByteArray(); + if (messageHeader == null || output.length < messageHeader.size()) { + throw new IOException("Response is invalid"); } - - /** - * This connection generally supports Extended length APDUs. This can be limited by firmware - * version of connected YubiKey. - */ - @Override - public boolean isExtendedLengthApduSupported() { - return true; + int dataLength = Math.min(output.length - messageHeader.size(), messageHeader.dataLength); + return Arrays.copyOfRange(output, messageHeader.size(), messageHeader.size() + dataLength); + } + + /** + * Class parses 10-bytes header of CCID message + * + *

The header consists of a message type (1 byte), a dataLength field (four bytes), the slot + * number (1 byte), a sequence number field (1 byte), and either three message specific bytes, or + * a status field (1 byte), an error field and one message specific byte. The purpose of the + * 10-byte header is to provide a constant offset at which message data begins across all + * messages. + */ + private static class MessageHeader { + private static final int SIZE_OF_CCID_PREFIX = 10; + private static final byte[] MESSAGE_SPECIFIC_BYTES = new byte[] {0, 0, 0}; + private static final byte SLOT_NUMBER = 0; + + private byte type; + private int dataLength; + private byte slot; + private byte sequence; + private byte status; + private byte error; + + @SuppressFBWarnings("URF_UNREAD_FIELD") + private byte messageSpecificByte; + + private MessageHeader(byte[] buffer) { + if (buffer.length > SIZE_OF_CCID_PREFIX) { + ByteBuffer responseBuffer = + ByteBuffer.wrap(buffer, 0, SIZE_OF_CCID_PREFIX).order(ByteOrder.LITTLE_ENDIAN); + type = responseBuffer.get(); + dataLength = responseBuffer.getInt(); + slot = responseBuffer.get(); + sequence = responseBuffer.get(); + status = responseBuffer.get(); + error = responseBuffer.get(); + messageSpecificByte = responseBuffer.get(); + } } - @Override - public byte[] sendAndReceive(byte[] apdu) throws IOException { - return transceive(REQUEST_MESSAGE_TYPE, apdu); + private MessageHeader(byte type, int length, byte sequence) { + this.type = type; + this.dataLength = length; + this.slot = SLOT_NUMBER; + this.sequence = sequence; } - @Override - public byte[] getAtr() { - return atr.clone(); + private byte[] array() { + ByteBuffer byteBuffer = + ByteBuffer.allocate(SIZE_OF_CCID_PREFIX) + .order(ByteOrder.LITTLE_ENDIAN) + .put(type) + .putInt(dataLength) + .put(slot) + .put(sequence) + .put(MESSAGE_SPECIFIC_BYTES); + return byteBuffer.array(); } - /** - * Does the data exchange between phone and connected usb device with bulk messages - * All bulk messages begin with a 10-bytes header, followed by message-specific data. - * - * @param type the message type identifies the message - * Message Name type - * PC_to_RDR_IccPowerOn 62h - * PC_to_RDR_IccPowerOff 63h - * PC_to_RDR_GetSlotStatus 65h - * PC_to_RDR_XfrBlock 6Fh - * PC_to_RDR_GetParameters 6Ch - * PC_to_RDR_ResetParameters 6Dh - * PC_to_RDR_SetParameters 61h - * PC_to_RDR_Escape 6Bh - * PC_to_RDR_IccClock 6Eh - * PC_to_RDR_T0APDU 6Ah - * PC_to_RDR_Secure 69h - * PC_to_RDR_Mechanical 71h - * PC_to_RDR_Abort 72h - * PC_to_RDR_SetDataRateAndClockFrequency 73h - * @param data message-specific data that needs to be sent to usb device - * @return received message-specific data from usb device - * @throws IOException in case if there is communication error occurs or received data is invalid - */ - private byte[] transceive(byte type, byte[] data) throws IOException { - // 1. prepare data for sending - MessageHeader prefix = new MessageHeader(type, data.length, sequence++); - ByteBuffer byteBuffer = ByteBuffer.allocate(prefix.size() + data.length).order(ByteOrder.LITTLE_ENDIAN) - .put(prefix.array()) - .put(data); - - // 2. sent data to device - byte[] bufferOut = byteBuffer.array(); - int bytesSent = 0; - int bytesSentPackage = 0; - while (bytesSent < bufferOut.length || bytesSentPackage == endpointOut.getMaxPacketSize()) { - bytesSentPackage = connection.bulkTransfer(endpointOut, bufferOut, bytesSent, bufferOut.length - bytesSent, TIMEOUT); - if (bytesSentPackage > 0) { - Logger.trace(logger, "{} bytes sent over ccid: {}", bytesSentPackage, StringUtils.bytesToHex(bufferOut, bytesSent, bytesSentPackage)); - bytesSent += bytesSentPackage; - } else if (bytesSentPackage < 0) { - throw new IOException("Failed to send " + (bufferOut.length - bytesSent) + " bytes"); - } else { - // 0 is still considered as success in bulkTransfer description - // Scenario: if last package size was equal to endpointOut.getMaxPacketSize() - // we are sending empty package after that to notify end of bulk transfer - break; - } - } - - // 3. read data from device until we receive non-full packet/blob - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - int bytesRead; - MessageHeader messageHeader = null; - - boolean receivedExpectedPrefix = false; - byte[] bufferRead = new byte[endpointIn.getMaxPacketSize()]; - boolean responseRequiresTimeExtension = false; - do { - bytesRead = connection.bulkTransfer(endpointIn, bufferRead, bufferRead.length, TIMEOUT); - if (bytesRead > 0) { - Logger.trace(logger, "{} bytes received: {}", bytesRead, StringUtils.bytesToHex(bufferRead, 0, bytesRead)); - - if (receivedExpectedPrefix) { - stream.write(bufferRead, 0, bytesRead); - } else { - // 4. parse received data and make sure it's proper format - messageHeader = new MessageHeader(bufferRead); - responseRequiresTimeExtension = (messageHeader.status & STATUS_TIME_EXTENSION) == STATUS_TIME_EXTENSION; - if (messageHeader.verify((byte) (sequence - 1))) { - // if we received expected prefix we can save the rest of received data without verification - receivedExpectedPrefix = true; - stream.write(bufferRead, 0, bytesRead); - } else if (messageHeader.error != 0 && !responseRequiresTimeExtension) { - Logger.debug(logger, "Invalid response from card reader bStatus={} and bError={}", - String.format(Locale.ROOT, "0x%02X", messageHeader.status), - String.format(Locale.ROOT, "0x%02X", messageHeader.error)); - throw new IOException("Invalid response from card reader"); - } - } - } else if (bytesRead < 0) { - throw new IOException("Failed to read response"); - } - } while ((bytesRead > 0 && bytesRead == bufferRead.length) || responseRequiresTimeExtension); - - - // 5. prepare data for returning to user - byte[] output = stream.toByteArray(); - if (messageHeader == null || output.length < messageHeader.size()) { - throw new IOException("Response is invalid"); - } - int dataLength = Math.min(output.length - messageHeader.size(), messageHeader.dataLength); - return Arrays.copyOfRange(output, messageHeader.size(), messageHeader.size() + dataLength); + private int size() { + return SIZE_OF_CCID_PREFIX; } /** - * Class parses 10-bytes header of CCID message - * The header consists of a message type (1 byte), a dataLength field (four bytes), the slot number - * (1 byte), a sequence number field (1 byte), and either three message specific bytes, or a - * status field (1 byte), an error field and one message specific byte. The purpose of the - * 10-byte header is to provide a constant offset at which message data begins across all - * messages. + * The response (Bulk-IN message) always contains the exact same slot number, and sequence + * number fields from the header that was contained in the Bulk-OUT command message. + * + * @param sequence Bulk-OUT message sequence + * @return true if prefix has expected format */ - private static class MessageHeader { - private static final int SIZE_OF_CCID_PREFIX = 10; - private static final byte[] MESSAGE_SPECIFIC_BYTES = new byte[]{0, 0, 0}; - private static final byte SLOT_NUMBER = 0; - - private byte type; - private int dataLength; - private byte slot; - private byte sequence; - private byte status; - private byte error; - @SuppressFBWarnings("URF_UNREAD_FIELD") - private byte messageSpecificByte; - - private MessageHeader(byte[] buffer) { - if (buffer.length > SIZE_OF_CCID_PREFIX) { - ByteBuffer responseBuffer = ByteBuffer.wrap(buffer, 0, SIZE_OF_CCID_PREFIX).order(ByteOrder.LITTLE_ENDIAN); - type = responseBuffer.get(); - dataLength = responseBuffer.getInt(); - slot = responseBuffer.get(); - sequence = responseBuffer.get(); - status = responseBuffer.get(); - error = responseBuffer.get(); - messageSpecificByte = responseBuffer.get(); - } - } - - private MessageHeader(byte type, int length, byte sequence) { - this.type = type; - this.dataLength = length; - this.slot = SLOT_NUMBER; - this.sequence = sequence; - } - - private byte[] array() { - ByteBuffer byteBuffer = ByteBuffer.allocate(SIZE_OF_CCID_PREFIX).order(ByteOrder.LITTLE_ENDIAN) - .put(type) - .putInt(dataLength) - .put(slot) - .put(sequence) - .put(MESSAGE_SPECIFIC_BYTES); - return byteBuffer.array(); - } - - private int size() { - return SIZE_OF_CCID_PREFIX; - } - - /** - * The response (Bulk-IN message) always contains the exact same slot number, and - * sequence number fields from the header that was contained in the Bulk-OUT command - * message. - * - * @param sequence Bulk-OUT message sequence - * @return true if prefix has expected format - */ - private boolean verify(byte sequence) { - if (this.type != RESPONSE_DATA_BLOCK) { - return false; - } - if (this.slot != SLOT_NUMBER) { - return false; - } - if (this.sequence != sequence) { - return false; - } - - // Note: according to documentation ignore error if status is 0 - return this.status == 0; - } + private boolean verify(byte sequence) { + if (this.type != RESPONSE_DATA_BLOCK) { + return false; + } + if (this.slot != SLOT_NUMBER) { + return false; + } + if (this.sequence != sequence) { + return false; + } + + // Note: according to documentation ignore error if status is 0 + return this.status == 0; } - + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/UsbYubiKeyConnection.java b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/UsbYubiKeyConnection.java index 19a4f676..e13c7896 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/UsbYubiKeyConnection.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/UsbYubiKeyConnection.java @@ -18,34 +18,34 @@ import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbInterface; - -import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.YubiKeyConnection; - +import com.yubico.yubikit.core.internal.Logger; import org.slf4j.LoggerFactory; abstract class UsbYubiKeyConnection implements YubiKeyConnection { - private final UsbDeviceConnection usbDeviceConnection; - private final UsbInterface usbInterface; - - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(UsbYubiKeyConnection.class); - - /** - * Base class for USB based Connections. - * - * @param usbDeviceConnection connection, which should already be open - * @param usbInterface USB interface, which should already be claimed - */ - protected UsbYubiKeyConnection(UsbDeviceConnection usbDeviceConnection, UsbInterface usbInterface) { - this.usbDeviceConnection = usbDeviceConnection; - this.usbInterface = usbInterface; - Logger.debug(logger, "USB connection opened: {}", this); - } - - @Override - public void close() { - usbDeviceConnection.releaseInterface(usbInterface); - usbDeviceConnection.close(); - Logger.debug(logger, "USB connection closed: {}", this); - } + private final UsbDeviceConnection usbDeviceConnection; + private final UsbInterface usbInterface; + + private static final org.slf4j.Logger logger = + LoggerFactory.getLogger(UsbYubiKeyConnection.class); + + /** + * Base class for USB based Connections. + * + * @param usbDeviceConnection connection, which should already be open + * @param usbInterface USB interface, which should already be claimed + */ + protected UsbYubiKeyConnection( + UsbDeviceConnection usbDeviceConnection, UsbInterface usbInterface) { + this.usbDeviceConnection = usbDeviceConnection; + this.usbInterface = usbInterface; + Logger.debug(logger, "USB connection opened: {}", this); + } + + @Override + public void close() { + usbDeviceConnection.releaseInterface(usbInterface); + usbDeviceConnection.close(); + Logger.debug(logger, "USB connection closed: {}", this); + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/package-info.java b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/package-info.java index e0afb07d..10e6fcf2 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/package-info.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/usb/connection/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.android.transport.usb.connection; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/android/src/main/java/com/yubico/yubikit/android/transport/usb/package-info.java b/android/src/main/java/com/yubico/yubikit/android/transport/usb/package-info.java index 1e6ec188..52b61927 100755 --- a/android/src/main/java/com/yubico/yubikit/android/transport/usb/package-info.java +++ b/android/src/main/java/com/yubico/yubikit/android/transport/usb/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.android.transport.usb; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/android/src/main/java/com/yubico/yubikit/android/ui/OtpActivity.java b/android/src/main/java/com/yubico/yubikit/android/ui/OtpActivity.java index e64d004e..60406501 100755 --- a/android/src/main/java/com/yubico/yubikit/android/ui/OtpActivity.java +++ b/android/src/main/java/com/yubico/yubikit/android/ui/OtpActivity.java @@ -19,7 +19,6 @@ import android.content.Intent; import android.os.Bundle; import android.view.KeyEvent; - import com.yubico.yubikit.android.R; import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice; import com.yubico.yubikit.android.transport.usb.UsbConfiguration; @@ -28,83 +27,94 @@ import com.yubico.yubikit.core.util.Callback; import com.yubico.yubikit.core.util.NdefUtils; import com.yubico.yubikit.core.util.Pair; - import java.io.IOException; - import javax.annotation.Nullable; -/** - * An Activity to prompt the user for a YubiKey to retrieve an OTP from a YubiOTP slot. - */ +/** An Activity to prompt the user for a YubiKey to retrieve an OTP from a YubiOTP slot. */ public class OtpActivity extends YubiKeyPromptActivity { - public static final int RESULT_ERROR = RESULT_FIRST_USER; - - public static final String EXTRA_OTP = "otp"; - public static final String EXTRA_ERROR = "error"; - - private OtpKeyListener keyListener; - - private int usbSessionCounter = 0; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - getIntent().putExtra(ARG_ACTION_CLASS, YubiKeyNdefAction.class); - getIntent().putExtra(ARG_ALLOW_USB, false); // Custom USB handling for keyboard. - - super.onCreate(savedInstanceState); - - getYubiKitManager().startUsbDiscovery(new UsbConfiguration().handlePermissions(false), device -> { - usbSessionCounter++; - device.setOnClosed(() -> { - usbSessionCounter--; - if (usbSessionCounter == 0) { - runOnUiThread(() -> helpTextView.setText(isNfcEnabled() ? R.string.yubikit_prompt_plug_in_or_tap : R.string.yubikit_prompt_plug_in)); - } + public static final int RESULT_ERROR = RESULT_FIRST_USER; + + public static final String EXTRA_OTP = "otp"; + public static final String EXTRA_ERROR = "error"; + + private OtpKeyListener keyListener; + + private int usbSessionCounter = 0; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + getIntent().putExtra(ARG_ACTION_CLASS, YubiKeyNdefAction.class); + getIntent().putExtra(ARG_ALLOW_USB, false); // Custom USB handling for keyboard. + + super.onCreate(savedInstanceState); + + getYubiKitManager() + .startUsbDiscovery( + new UsbConfiguration().handlePermissions(false), + device -> { + usbSessionCounter++; + device.setOnClosed( + () -> { + usbSessionCounter--; + if (usbSessionCounter == 0) { + runOnUiThread( + () -> + helpTextView.setText( + isNfcEnabled() + ? R.string.yubikit_prompt_plug_in_or_tap + : R.string.yubikit_prompt_plug_in)); + } + }); + runOnUiThread(() -> helpTextView.setText(R.string.yubikit_otp_touch)); }); - runOnUiThread(() -> helpTextView.setText(R.string.yubikit_otp_touch)); - }); - keyListener = new OtpKeyListener(new OtpKeyListener.OtpListener() { - @Override - public void onCaptureStarted() { + keyListener = + new OtpKeyListener( + new OtpKeyListener.OtpListener() { + @Override + public void onCaptureStarted() { helpTextView.setText(R.string.yubikit_prompt_wait); - } + } - @Override - public void onCaptureComplete(String capture) { + @Override + public void onCaptureComplete(String capture) { Intent intent = new Intent(); intent.putExtra(EXTRA_OTP, capture); setResult(Activity.RESULT_OK, intent); finish(); - } - }); - } + } + }); + } - @Override - protected void onDestroy() { - getYubiKitManager().stopUsbDiscovery(); - super.onDestroy(); - } + @Override + protected void onDestroy() { + getYubiKitManager().stopUsbDiscovery(); + super.onDestroy(); + } - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - return keyListener.onKeyEvent(event); - } + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return keyListener.onKeyEvent(event); + } - static class YubiKeyNdefAction extends YubiKeyPromptAction { - @Override - void onYubiKey(YubiKeyDevice device, Bundle extras, CommandState commandState, Callback> callback) { - if (device instanceof NfcYubiKeyDevice) { - Intent intent = new Intent(); - try { - String credential = NdefUtils.getNdefPayload(((NfcYubiKeyDevice) device).readNdef()); - intent.putExtra(EXTRA_OTP, credential); - callback.invoke(new Pair<>(RESULT_OK, intent)); - } catch (IOException e) { - intent.putExtra(EXTRA_ERROR, e); - callback.invoke(new Pair<>(RESULT_ERROR, intent)); - } - } + static class YubiKeyNdefAction extends YubiKeyPromptAction { + @Override + void onYubiKey( + YubiKeyDevice device, + Bundle extras, + CommandState commandState, + Callback> callback) { + if (device instanceof NfcYubiKeyDevice) { + Intent intent = new Intent(); + try { + String credential = NdefUtils.getNdefPayload(((NfcYubiKeyDevice) device).readNdef()); + intent.putExtra(EXTRA_OTP, credential); + callback.invoke(new Pair<>(RESULT_OK, intent)); + } catch (IOException e) { + intent.putExtra(EXTRA_ERROR, e); + callback.invoke(new Pair<>(RESULT_ERROR, intent)); } + } } + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/ui/OtpKeyListener.java b/android/src/main/java/com/yubico/yubikit/android/ui/OtpKeyListener.java index fb4afc7e..58989aa5 100755 --- a/android/src/main/java/com/yubico/yubikit/android/ui/OtpKeyListener.java +++ b/android/src/main/java/com/yubico/yubikit/android/ui/OtpKeyListener.java @@ -22,74 +22,76 @@ import android.view.KeyEvent; /** - * A helper class that is used to intercept keyboard event from a YubiKey to capture an OTP. - * Use it directly in an Activity in {@link android.app.Activity#onKeyUp}, or in a - * {@link android.view.View.OnKeyListener}. + * A helper class that is used to intercept keyboard event from a YubiKey to capture an OTP. Use it + * directly in an Activity in {@link android.app.Activity#onKeyUp}, or in a {@link + * android.view.View.OnKeyListener}. */ public class OtpKeyListener { - private static final int OTP_DELAY_MS = 1000; - private static final int YUBICO_VID = 0x1050; + private static final int OTP_DELAY_MS = 1000; + private static final int YUBICO_VID = 0x1050; - private final SparseArray inputBuffers = new SparseArray<>(); - private final Handler handler = new Handler(Looper.getMainLooper()); - private final OtpListener listener; + private final SparseArray inputBuffers = new SparseArray<>(); + private final Handler handler = new Handler(Looper.getMainLooper()); + private final OtpListener listener; - public OtpKeyListener(OtpListener listener) { - this.listener = listener; - } + public OtpKeyListener(OtpListener listener) { + this.listener = listener; + } - public boolean onKeyEvent(KeyEvent event) { - InputDevice device = event.getDevice(); - if (device == null || device.getVendorId() != YUBICO_VID) { - // Don't handle non-Yubico devices - return false; - } + public boolean onKeyEvent(KeyEvent event) { + InputDevice device = event.getDevice(); + if (device == null || device.getVendorId() != YUBICO_VID) { + // Don't handle non-Yubico devices + return false; + } - if (event.getAction() == KeyEvent.ACTION_UP) { - // use id of keyboard device to distinguish current input device - // in case of multiple keys inserted - int deviceId = event.getDeviceId(); - StringBuilder otpBuffer = inputBuffers.get(deviceId, new StringBuilder()); - if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getKeyCode() == KeyEvent.KEYCODE_NUMPAD_ENTER) { - // Carriage return seen. Assume this is the end of the OTP credential and notify immediately. - listener.onCaptureComplete(otpBuffer.toString()); - inputBuffers.delete(deviceId); - } else { - if (otpBuffer.length() == 0) { - // in case if we never get keycode enter (which is pretty generic scenario) we set timer for 1 sec - // upon expiration we assume that we have no more input from key - handler.postDelayed(() -> { - StringBuilder otpBuffer1 = inputBuffers.get(deviceId, new StringBuilder()); - // if buffer is empty it means that we sent it to user already, avoid double invocation - if (otpBuffer1.length() > 0) { - listener.onCaptureComplete(otpBuffer1.toString()); - inputBuffers.delete(deviceId); - } - }, OTP_DELAY_MS); - listener.onCaptureStarted(); + if (event.getAction() == KeyEvent.ACTION_UP) { + // use id of keyboard device to distinguish current input device + // in case of multiple keys inserted + int deviceId = event.getDeviceId(); + StringBuilder otpBuffer = inputBuffers.get(deviceId, new StringBuilder()); + if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER + || event.getKeyCode() == KeyEvent.KEYCODE_NUMPAD_ENTER) { + // Carriage return seen. Assume this is the end of the OTP credential and notify + // immediately. + listener.onCaptureComplete(otpBuffer.toString()); + inputBuffers.delete(deviceId); + } else { + if (otpBuffer.length() == 0) { + // in case if we never get keycode enter (which is pretty generic scenario) we set timer + // for 1 sec + // upon expiration we assume that we have no more input from key + handler.postDelayed( + () -> { + StringBuilder otpBuffer1 = inputBuffers.get(deviceId, new StringBuilder()); + // if buffer is empty it means that we sent it to user already, avoid double + // invocation + if (otpBuffer1.length() > 0) { + listener.onCaptureComplete(otpBuffer1.toString()); + inputBuffers.delete(deviceId); } - otpBuffer.append((char) event.getUnicodeChar()); - inputBuffers.put(deviceId, otpBuffer); - } + }, + OTP_DELAY_MS); + listener.onCaptureStarted(); } - - return true; + otpBuffer.append((char) event.getUnicodeChar()); + inputBuffers.put(deviceId, otpBuffer); + } } + return true; + } + + /** Listener interface to react to events. */ + public interface OtpListener { + /** Called when the user has triggered OTP output and capture has started. */ + void onCaptureStarted(); + /** - * Listener interface to react to events. + * Called when OTP capture has completed. + * + * @param capture the captured OTP */ - public interface OtpListener { - /** - * Called when the user has triggered OTP output and capture has started. - */ - void onCaptureStarted(); - - /** - * Called when OTP capture has completed. - * - * @param capture the captured OTP - */ - void onCaptureComplete(String capture); - } + void onCaptureComplete(String capture); + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/ui/YubiKeyPromptAction.java b/android/src/main/java/com/yubico/yubikit/android/ui/YubiKeyPromptAction.java index f93c980b..3f58141e 100755 --- a/android/src/main/java/com/yubico/yubikit/android/ui/YubiKeyPromptAction.java +++ b/android/src/main/java/com/yubico/yubikit/android/ui/YubiKeyPromptAction.java @@ -19,44 +19,47 @@ import android.app.Activity; import android.content.Intent; import android.os.Bundle; - import com.yubico.yubikit.core.YubiKeyDevice; import com.yubico.yubikit.core.application.CommandState; import com.yubico.yubikit.core.util.Callback; import com.yubico.yubikit.core.util.Pair; /** - * Action to be performed by a {@link YubiKeyPromptActivity} when a YubiKey is attached. - * Extend this class to handle an attached YubiKey from a YubiKeyPromptActivity. - *

- * See also {@link YubiKeyPromptConnectionAction} for an alternative which handles YubiKeys for a + * Action to be performed by a {@link YubiKeyPromptActivity} when a YubiKey is attached. Extend this + * class to handle an attached YubiKey from a YubiKeyPromptActivity. + * + *

See also {@link YubiKeyPromptConnectionAction} for an alternative which handles YubiKeys for a * specific connection type. */ public abstract class YubiKeyPromptAction { - /** - * A special result code which will reset the dialog state to continue processing additional YubiKeys. - */ - public static final int RESULT_CONTINUE = Activity.RESULT_FIRST_USER + 100; + /** + * A special result code which will reset the dialog state to continue processing additional + * YubiKeys. + */ + public static final int RESULT_CONTINUE = Activity.RESULT_FIRST_USER + 100; + + /** A result Pair used to keep the dialog open to continue processing YubiKeys. */ + public static final Pair CONTINUE = new Pair<>(RESULT_CONTINUE, new Intent()); - /** - * A result Pair used to keep the dialog open to continue processing YubiKeys. - */ - public static final Pair CONTINUE = new Pair<>(RESULT_CONTINUE, new Intent()); - /** - * Called when a YubiKey is connected. - *

- * Subclasses should override this method to react to a connected YubiKey. - * Use the callback to signal when the method is done handling the YubiKey, with a result - * (a pair of resultCode, Intent) to return to the caller, closing the dialog. - * Use the special {@link #CONTINUE} result to leave the dialog open, without returning to the - * caller, and continue to process additional YubiKeys. - * The CommandState can be used to update the dialog UI based on status of the - * operation, and is cancelled if the user presses the cancel button. - * - * @param device A YubiKeyDevice - * @param extras the extras the Activity was called with - * @param commandState a CommandState that is hooked up to the activity. - * @param callback a callback to invoke to provide the result of the operation, as a Pair of result code and Intent with extras - */ - abstract void onYubiKey(YubiKeyDevice device, Bundle extras, CommandState commandState, Callback> callback); + /** + * Called when a YubiKey is connected. + * + *

Subclasses should override this method to react to a connected YubiKey. Use the callback to + * signal when the method is done handling the YubiKey, with a result (a pair of resultCode, + * Intent) to return to the caller, closing the dialog. Use the special {@link #CONTINUE} result + * to leave the dialog open, without returning to the caller, and continue to process additional + * YubiKeys. The CommandState can be used to update the dialog UI based on status of the + * operation, and is cancelled if the user presses the cancel button. + * + * @param device A YubiKeyDevice + * @param extras the extras the Activity was called with + * @param commandState a CommandState that is hooked up to the activity. + * @param callback a callback to invoke to provide the result of the operation, as a Pair of + * result code and Intent with extras + */ + abstract void onYubiKey( + YubiKeyDevice device, + Bundle extras, + CommandState commandState, + Callback> callback); } diff --git a/android/src/main/java/com/yubico/yubikit/android/ui/YubiKeyPromptActivity.java b/android/src/main/java/com/yubico/yubikit/android/ui/YubiKeyPromptActivity.java index 77afcf4c..88c9037f 100755 --- a/android/src/main/java/com/yubico/yubikit/android/ui/YubiKeyPromptActivity.java +++ b/android/src/main/java/com/yubico/yubikit/android/ui/YubiKeyPromptActivity.java @@ -24,308 +24,320 @@ import android.view.View; import android.widget.Button; import android.widget.TextView; - import androidx.annotation.StringRes; - import com.yubico.yubikit.android.R; import com.yubico.yubikit.android.YubiKitManager; import com.yubico.yubikit.android.transport.nfc.NfcConfiguration; import com.yubico.yubikit.android.transport.nfc.NfcNotAvailable; import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyManager; import com.yubico.yubikit.android.transport.usb.UsbConfiguration; -import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.YubiKeyDevice; import com.yubico.yubikit.core.application.CommandState; - -import org.slf4j.LoggerFactory; - +import com.yubico.yubikit.core.internal.Logger; import java.lang.reflect.InvocationTargetException; import java.util.Objects; - import javax.annotation.Nullable; +import org.slf4j.LoggerFactory; /** - * A dialog for interacting with a YubiKey. - * To use, start this activity with a subclass of {@link YubiKeyPromptAction} passed using the - * ARG_ACTION_CLASS extra in the intent. This can be done by using the {@link #createIntent} method: - *

- * {@code
+ * A dialog for interacting with a YubiKey. To use, start this activity with a subclass of {@link
+ * YubiKeyPromptAction} passed using the ARG_ACTION_CLASS extra in the intent. This can be done by
+ * using the {@link #createIntent} method:
+ *
+ * 
{@code
  * Intent intent = YubiKeyPromptActivity.createIntent(context, MyConnectionAction.class);
  * startActivityForResult(intent, requestCode);
- * }
- * 
- *

- * The dialog can be customized by passing additional values in the intent. + * }

+ * + *

The dialog can be customized by passing additional values in the intent. */ public class YubiKeyPromptActivity extends Activity { - /** - * Helper method to create an Intent to start the YubiKeyPromptActivity with a ConnectionAction. - * - * @param context the Context to use for Intent creation - * @param action the ConnectionAction to use for handing YubiKey connections. - * @param titleRes a string resource to use for the title of the dialog. - * @return an Intent which can be passed to startActivity(). - */ - public static Intent createIntent(Context context, Class action, @StringRes int titleRes) { - Intent intent = createIntent(context, action); - intent.putExtra(ARG_TITLE_ID, titleRes); - return intent; - } - - /** - * Helper method to create an Intent to start the YubiKeyPromptActivity with a ConnectionAction. - * - * @param context the Context to use for Intent creation - * @param action the ConnectionAction to use for handing YubiKey connections. - * @return an Intent which can be passed to startActivity(). - */ - public static Intent createIntent(Context context, Class action) { - Intent intent = new Intent(context, YubiKeyPromptActivity.class); - intent.putExtra(ARG_ACTION_CLASS, action); - return intent; - } - - /** - * The YubiKeyPromptAction subclass to use when a YubiKey is attached. - */ - public static final String ARG_ACTION_CLASS = "ACTION_CLASS"; - - /** - * Whether or not to listen for YubiKeys over USB (default: true). - */ - public static final String ARG_ALLOW_USB = "ALLOW_USB"; - - /** - * Whether or not to listen for YubiKeys over NFC (default: true). - */ - public static final String ARG_ALLOW_NFC = "ALLOW_NFC"; - - /** - * A string resource to use as the title of the dialog. - */ - public static final String ARG_TITLE_ID = "TITLE_ID"; - - /** - * A layout resource to use as the content of the dialog. - */ - public static final String ARG_CONTENT_VIEW_ID = "CONTENT_VIEW_ID"; - - /** - * A view ID of a Button to use for cancelling the action. - */ - public static final String ARG_CANCEL_BUTTON_ID = "CANCEL_BUTTON_ID"; - - /** - * A view ID of a Button to use to enable NFC, if NFC is disabled. - */ - public static final String ARG_ENABLE_NFC_BUTTON_ID = "ENABLE_NFC_BUTTON_ID"; - - /** - * A view ID of a TextView where helpful information is displayed. - */ - public static final String ARG_HELP_TEXT_VIEW_ID = "HELP_TEXT_VIEW_ID"; - - private final MyCommandState commandState = new MyCommandState(); - - private YubiKitManager yubiKit; - private YubiKeyPromptAction action; - - private boolean hasNfc = true; - private int usbSessionCounter = 0; - private boolean isDone = false; - protected Button cancelButton; - protected Button enableNfcButton; - protected TextView helpTextView; - - private boolean allowUsb; - private boolean allowNfc; - - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(YubiKeyPromptActivity.class); - - /** - * Get the YubiKitManager used by this activity. - * - * @return a YubiKitManager - */ - protected YubiKitManager getYubiKitManager() { - return yubiKit; - } - - /** - * Get a CommandState for use with some blocking YubiKey actions. - * The dialog will react to KEEPALIVE_UPNEEDED, and the state will be cancelled if the user presses the cancel button. - * - * @return a CommandState - */ - protected CommandState getCommandState() { - return commandState; - } - - protected boolean isNfcEnabled() { - return hasNfc; - } - - /** - * Called when a YubiKey is attached. - *

- * If {@link #provideResult(int, Intent)} has been called once this method returns, the Activity will finish. - * - * @param device a connected YubiKey - */ - protected void onYubiKeyDevice(YubiKeyDevice device, Runnable onDone) { - action.onYubiKey(device, getIntent().getExtras(), commandState, value -> { - if (value.first == YubiKeyPromptAction.RESULT_CONTINUE) { - // Keep processing additional YubiKeys - if (commandState.awaitingTouch) { - // Reset the help text if touch was prompted for - runOnUiThread(() -> helpTextView.setText(hasNfc ? R.string.yubikit_prompt_plug_in_or_tap : R.string.yubikit_prompt_plug_in)); - commandState.awaitingTouch = false; - } - } else { - provideResult(value.first, value.second); + /** + * Helper method to create an Intent to start the YubiKeyPromptActivity with a ConnectionAction. + * + * @param context the Context to use for Intent creation + * @param action the ConnectionAction to use for handing YubiKey connections. + * @param titleRes a string resource to use for the title of the dialog. + * @return an Intent which can be passed to startActivity(). + */ + public static Intent createIntent( + Context context, Class action, @StringRes int titleRes) { + Intent intent = createIntent(context, action); + intent.putExtra(ARG_TITLE_ID, titleRes); + return intent; + } + + /** + * Helper method to create an Intent to start the YubiKeyPromptActivity with a ConnectionAction. + * + * @param context the Context to use for Intent creation + * @param action the ConnectionAction to use for handing YubiKey connections. + * @return an Intent which can be passed to startActivity(). + */ + public static Intent createIntent(Context context, Class action) { + Intent intent = new Intent(context, YubiKeyPromptActivity.class); + intent.putExtra(ARG_ACTION_CLASS, action); + return intent; + } + + /** The YubiKeyPromptAction subclass to use when a YubiKey is attached. */ + public static final String ARG_ACTION_CLASS = "ACTION_CLASS"; + + /** Whether or not to listen for YubiKeys over USB (default: true). */ + public static final String ARG_ALLOW_USB = "ALLOW_USB"; + + /** Whether or not to listen for YubiKeys over NFC (default: true). */ + public static final String ARG_ALLOW_NFC = "ALLOW_NFC"; + + /** A string resource to use as the title of the dialog. */ + public static final String ARG_TITLE_ID = "TITLE_ID"; + + /** A layout resource to use as the content of the dialog. */ + public static final String ARG_CONTENT_VIEW_ID = "CONTENT_VIEW_ID"; + + /** A view ID of a Button to use for cancelling the action. */ + public static final String ARG_CANCEL_BUTTON_ID = "CANCEL_BUTTON_ID"; + + /** A view ID of a Button to use to enable NFC, if NFC is disabled. */ + public static final String ARG_ENABLE_NFC_BUTTON_ID = "ENABLE_NFC_BUTTON_ID"; + + /** A view ID of a TextView where helpful information is displayed. */ + public static final String ARG_HELP_TEXT_VIEW_ID = "HELP_TEXT_VIEW_ID"; + + private final MyCommandState commandState = new MyCommandState(); + + private YubiKitManager yubiKit; + private YubiKeyPromptAction action; + + private boolean hasNfc = true; + private int usbSessionCounter = 0; + private boolean isDone = false; + protected Button cancelButton; + protected Button enableNfcButton; + protected TextView helpTextView; + + private boolean allowUsb; + private boolean allowNfc; + + private static final org.slf4j.Logger logger = + LoggerFactory.getLogger(YubiKeyPromptActivity.class); + + /** + * Get the YubiKitManager used by this activity. + * + * @return a YubiKitManager + */ + protected YubiKitManager getYubiKitManager() { + return yubiKit; + } + + /** + * Get a CommandState for use with some blocking YubiKey actions. The dialog will react to + * KEEPALIVE_UPNEEDED, and the state will be cancelled if the user presses the cancel button. + * + * @return a CommandState + */ + protected CommandState getCommandState() { + return commandState; + } + + protected boolean isNfcEnabled() { + return hasNfc; + } + + /** + * Called when a YubiKey is attached. + * + *

If {@link #provideResult(int, Intent)} has been called once this method returns, the + * Activity will finish. + * + * @param device a connected YubiKey + */ + protected void onYubiKeyDevice(YubiKeyDevice device, Runnable onDone) { + action.onYubiKey( + device, + getIntent().getExtras(), + commandState, + value -> { + if (value.first == YubiKeyPromptAction.RESULT_CONTINUE) { + // Keep processing additional YubiKeys + if (commandState.awaitingTouch) { + // Reset the help text if touch was prompted for + runOnUiThread( + () -> + helpTextView.setText( + hasNfc + ? R.string.yubikit_prompt_plug_in_or_tap + : R.string.yubikit_prompt_plug_in)); + commandState.awaitingTouch = false; } - onDone.run(); + } else { + provideResult(value.first, value.second); + } + onDone.run(); }); + } + + /** + * Provides a result to return to the caller of the Activity. + * + *

Internally this calls {@link #setResult(int, Intent)} with the given arguments, as well as + * informing this Activity that it should finish once it is done handling any connected YubiKey. + * + * @param resultCode The result code to propagate back to the originating activity, often + * RESULT_CANCELED or RESULT_OK + * @param data The data to propagate back to the originating activity. + */ + protected void provideResult(int resultCode, Intent data) { + setResult(resultCode, data); + isDone = true; + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Handle options + Bundle args = Objects.requireNonNull(getIntent().getExtras()); + + allowUsb = args.getBoolean(ARG_ALLOW_USB, true); + allowNfc = args.getBoolean(ARG_ALLOW_NFC, true); + + // Get the action to perform on YubiKey connected + @SuppressWarnings("deprecation") + Class actionType = + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + ? (Class) args.getSerializable(ARG_ACTION_CLASS, Class.class) + : (Class) args.getSerializable(ARG_ACTION_CLASS); + try { + if (actionType != null && YubiKeyPromptAction.class.isAssignableFrom(actionType)) { + action = (YubiKeyPromptAction) actionType.getDeclaredConstructor().newInstance(); + } else { + throw new IllegalStateException("Missing or invalid ConnectionAction class"); + } + } catch (IllegalStateException + | IllegalAccessException + | InstantiationException + | NoSuchMethodException + | InvocationTargetException e) { + Logger.error(logger, "Unable to instantiate ConnectionAction", e); + finish(); } - /** - * Provides a result to return to the caller of the Activity. - * Internally this calls {@link #setResult(int, Intent)} with the given arguments, as well as informing this - * Activity that it should finish once it is done handling any connected YubiKey. - * - * @param resultCode The result code to propagate back to the originating - * activity, often RESULT_CANCELED or RESULT_OK - * @param data The data to propagate back to the originating activity. - */ - protected void provideResult(int resultCode, Intent data) { - setResult(resultCode, data); - isDone = true; - } - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // Handle options - Bundle args = Objects.requireNonNull(getIntent().getExtras()); - - allowUsb = args.getBoolean(ARG_ALLOW_USB, true); - allowNfc = args.getBoolean(ARG_ALLOW_NFC, true); - - // Get the action to perform on YubiKey connected - @SuppressWarnings("deprecation") - Class actionType = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) - ? (Class) args.getSerializable(ARG_ACTION_CLASS, Class.class) - : (Class) args.getSerializable(ARG_ACTION_CLASS); - try { - if (actionType != null && YubiKeyPromptAction.class.isAssignableFrom(actionType)) { - action = (YubiKeyPromptAction) actionType.getDeclaredConstructor().newInstance(); - } else { - throw new IllegalStateException("Missing or invalid ConnectionAction class"); - } - } catch (IllegalStateException | IllegalAccessException | InstantiationException | - NoSuchMethodException | InvocationTargetException e) { - Logger.error(logger, "Unable to instantiate ConnectionAction", e); - finish(); - } + // Set up the view + setContentView(args.getInt(ARG_CONTENT_VIEW_ID, R.layout.yubikit_yubikey_prompt_content)); - // Set up the view - setContentView(args.getInt(ARG_CONTENT_VIEW_ID, R.layout.yubikit_yubikey_prompt_content)); - - if (args.containsKey(ARG_TITLE_ID)) { - setTitle(args.getInt(ARG_TITLE_ID)); - } + if (args.containsKey(ARG_TITLE_ID)) { + setTitle(args.getInt(ARG_TITLE_ID)); + } - // We draw our own title - TextView titleText = findViewById(R.id.yubikit_prompt_title); - if (titleText != null) { - titleText.setText(getTitle()); - } + // We draw our own title + TextView titleText = findViewById(R.id.yubikit_prompt_title); + if (titleText != null) { + titleText.setText(getTitle()); + } - helpTextView = findViewById(args.getInt(ARG_HELP_TEXT_VIEW_ID, R.id.yubikit_prompt_help_text_view)); - cancelButton = findViewById(args.getInt(ARG_CANCEL_BUTTON_ID, R.id.yubikit_prompt_cancel_btn)); - cancelButton.setFocusable(false); - cancelButton.setOnClickListener(v -> { - commandState.cancel(); - setResult(Activity.RESULT_CANCELED); - finish(); + helpTextView = + findViewById(args.getInt(ARG_HELP_TEXT_VIEW_ID, R.id.yubikit_prompt_help_text_view)); + cancelButton = findViewById(args.getInt(ARG_CANCEL_BUTTON_ID, R.id.yubikit_prompt_cancel_btn)); + cancelButton.setFocusable(false); + cancelButton.setOnClickListener( + v -> { + commandState.cancel(); + setResult(Activity.RESULT_CANCELED); + finish(); }); - yubiKit = new YubiKitManager(this); - if (allowUsb) { - yubiKit.startUsbDiscovery(new UsbConfiguration(), device -> { - usbSessionCounter++; - device.setOnClosed(() -> { - usbSessionCounter--; - if (usbSessionCounter == 0) { - runOnUiThread(() -> helpTextView.setText(hasNfc ? R.string.yubikit_prompt_plug_in_or_tap : R.string.yubikit_prompt_plug_in)); - } + yubiKit = new YubiKitManager(this); + if (allowUsb) { + yubiKit.startUsbDiscovery( + new UsbConfiguration(), + device -> { + usbSessionCounter++; + device.setOnClosed( + () -> { + usbSessionCounter--; + if (usbSessionCounter == 0) { + runOnUiThread( + () -> + helpTextView.setText( + hasNfc + ? R.string.yubikit_prompt_plug_in_or_tap + : R.string.yubikit_prompt_plug_in)); + } }); - runOnUiThread(() -> helpTextView.setText(R.string.yubikit_prompt_wait)); - onYubiKeyDevice(device, YubiKeyPromptActivity.this::finishIfDone); - }); - } - - if (allowNfc) { - enableNfcButton = findViewById(args.getInt(ARG_ENABLE_NFC_BUTTON_ID, R.id.yubikit_prompt_enable_nfc_btn)); - enableNfcButton.setFocusable(false); - enableNfcButton.setOnClickListener(v -> startActivity(new Intent(NfcYubiKeyManager.NFC_SETTINGS_ACTION))); - } + runOnUiThread(() -> helpTextView.setText(R.string.yubikit_prompt_wait)); + onYubiKeyDevice(device, YubiKeyPromptActivity.this::finishIfDone); + }); } - @Override - protected void onResume() { - super.onResume(); - - if (allowNfc) { - enableNfcButton.setVisibility(View.GONE); - try { - yubiKit.startNfcDiscovery(new NfcConfiguration(), this, device -> onYubiKeyDevice(device, () -> { - runOnUiThread(() -> helpTextView.setText(R.string.yubikit_prompt_remove)); - device.remove(this::finishIfDone); - })); - } catch (NfcNotAvailable e) { - hasNfc = false; - helpTextView.setText(R.string.yubikit_prompt_plug_in); - if (e.isDisabled()) { - enableNfcButton.setVisibility(View.VISIBLE); - } - } - } + if (allowNfc) { + enableNfcButton = + findViewById(args.getInt(ARG_ENABLE_NFC_BUTTON_ID, R.id.yubikit_prompt_enable_nfc_btn)); + enableNfcButton.setFocusable(false); + enableNfcButton.setOnClickListener( + v -> startActivity(new Intent(NfcYubiKeyManager.NFC_SETTINGS_ACTION))); } - - @Override - protected void onPause() { - if (allowNfc) { - yubiKit.stopNfcDiscovery(this); + } + + @Override + protected void onResume() { + super.onResume(); + + if (allowNfc) { + enableNfcButton.setVisibility(View.GONE); + try { + yubiKit.startNfcDiscovery( + new NfcConfiguration(), + this, + device -> + onYubiKeyDevice( + device, + () -> { + runOnUiThread(() -> helpTextView.setText(R.string.yubikit_prompt_remove)); + device.remove(this::finishIfDone); + })); + } catch (NfcNotAvailable e) { + hasNfc = false; + helpTextView.setText(R.string.yubikit_prompt_plug_in); + if (e.isDisabled()) { + enableNfcButton.setVisibility(View.VISIBLE); } - super.onPause(); + } } + } - @Override - protected void onDestroy() { - if (allowUsb) { - yubiKit.stopUsbDiscovery(); - } - super.onDestroy(); + @Override + protected void onPause() { + if (allowNfc) { + yubiKit.stopNfcDiscovery(this); } + super.onPause(); + } + @Override + protected void onDestroy() { + if (allowUsb) { + yubiKit.stopUsbDiscovery(); + } + super.onDestroy(); + } - private void finishIfDone() { - if (isDone) { - finish(); - } + private void finishIfDone() { + if (isDone) { + finish(); } + } - private class MyCommandState extends CommandState { - boolean awaitingTouch = false; + private class MyCommandState extends CommandState { + boolean awaitingTouch = false; - @Override - public void onKeepAliveStatus(byte status) { - if (!awaitingTouch && status == CommandState.STATUS_UPNEEDED) { - awaitingTouch = true; - runOnUiThread(() -> helpTextView.setText(R.string.yubikit_prompt_uv)); - } - } + @Override + public void onKeepAliveStatus(byte status) { + if (!awaitingTouch && status == CommandState.STATUS_UPNEEDED) { + awaitingTouch = true; + runOnUiThread(() -> helpTextView.setText(R.string.yubikit_prompt_uv)); + } } + } } diff --git a/android/src/main/java/com/yubico/yubikit/android/ui/YubiKeyPromptConnectionAction.java b/android/src/main/java/com/yubico/yubikit/android/ui/YubiKeyPromptConnectionAction.java index 0b1e9c1c..bbdc26f0 100755 --- a/android/src/main/java/com/yubico/yubikit/android/ui/YubiKeyPromptConnectionAction.java +++ b/android/src/main/java/com/yubico/yubikit/android/ui/YubiKeyPromptConnectionAction.java @@ -18,82 +18,89 @@ import android.content.Intent; import android.os.Bundle; - import androidx.annotation.WorkerThread; - -import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.YubiKeyConnection; import com.yubico.yubikit.core.YubiKeyDevice; import com.yubico.yubikit.core.application.CommandState; +import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.util.Callback; import com.yubico.yubikit.core.util.Pair; - -import org.slf4j.LoggerFactory; - import java.io.IOException; +import org.slf4j.LoggerFactory; /** * Action to be performed by a {@link YubiKeyPromptActivity} when a YubiKey is attached. - * Extend this class to handle an attached YubiKey from a YubiKeyPromptActivity, capable of providing a specific type of connection. + * + *

Extend this class to handle an attached YubiKey from a YubiKeyPromptActivity, capable of + * providing a specific type of connection. * * @param The connection type to handle */ -public abstract class YubiKeyPromptConnectionAction extends YubiKeyPromptAction { +public abstract class YubiKeyPromptConnectionAction + extends YubiKeyPromptAction { - final Class connectionType; + final Class connectionType; - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(YubiKeyPromptConnectionAction.class); + private static final org.slf4j.Logger logger = + LoggerFactory.getLogger(YubiKeyPromptConnectionAction.class); - /** - * Subclasses need to provide a default (no-arg) constructor which calls this parent constructor. - * - * @param connectionType the type of connection used - */ - protected YubiKeyPromptConnectionAction(Class connectionType) { - this.connectionType = connectionType; - } + /** + * Subclasses need to provide a default (no-arg) constructor which calls this parent constructor. + * + * @param connectionType the type of connection used + */ + protected YubiKeyPromptConnectionAction(Class connectionType) { + this.connectionType = connectionType; + } - @Override - final void onYubiKey(YubiKeyDevice device, Bundle extras, CommandState commandState, Callback> callback) { - if (device.supportsConnection(connectionType)) { - device.requestConnection(connectionType, value -> { - try { - callback.invoke(onYubiKeyConnection(value.getValue(), extras, commandState)); - } catch (IOException exception) { - onError(exception); - } - }); - } else { - Logger.debug(logger, "Connected YubiKey does not support desired connection type"); - callback.invoke(CONTINUE); - } + @Override + final void onYubiKey( + YubiKeyDevice device, + Bundle extras, + CommandState commandState, + Callback> callback) { + if (device.supportsConnection(connectionType)) { + device.requestConnection( + connectionType, + value -> { + try { + callback.invoke(onYubiKeyConnection(value.getValue(), extras, commandState)); + } catch (IOException exception) { + onError(exception); + } + }); + } else { + Logger.debug(logger, "Connected YubiKey does not support desired connection type"); + callback.invoke(CONTINUE); } + } - /** - * Called when a YubiKey supporting the desired connection type is connected. - *

- * Subclasses should override this method to react to a connected YubiKey. - * Return a value to cause the dialog to finish, returning the Intent to the caller, using - * the given result code. Return {@link #CONTINUE} to keep the dialog open to process additional - * connections. The CommandState can be used to update the dialog UI based on status of the - * operation, and is cancelled if the user presses the cancel button. - * NOTE: Subclasses should not close the connection, as it will be closed automatically. - * - * @param connection A YubiKey connection - * @param extras the extras the Activity was called with - * @param commandState a CommandState that is hooked up to the activity. - * @return the result of the operation, as a Pair of result code and Intent with extras, or null - */ - @WorkerThread - protected abstract Pair onYubiKeyConnection(T connection, Bundle extras, CommandState commandState); + /** + * Called when a YubiKey supporting the desired connection type is connected. + * + *

Subclasses should override this method to react to a connected YubiKey. Return a value to + * cause the dialog to finish, returning the Intent to the caller, using the given result code. + * Return {@link #CONTINUE} to keep the dialog open to process additional connections. The + * CommandState can be used to update the dialog UI based on status of the operation, and is + * cancelled if the user presses the cancel button. NOTE: Subclasses should not close the + * connection, as it will be closed automatically. + * + * @param connection A YubiKey connection + * @param extras the extras the Activity was called with + * @param commandState a CommandState that is hooked up to the activity. + * @return the result of the operation, as a Pair of result code and Intent with extras, or null + */ + @WorkerThread + protected abstract Pair onYubiKeyConnection( + T connection, Bundle extras, CommandState commandState); - /** - * Overridable method called if opening a connection to a YubiKey throws an error. - * - * @param exception the Exception raised - */ - @WorkerThread - protected void onError(Exception exception) { - Logger.error(logger, "Error connecting to YubiKey", exception); - } -} \ No newline at end of file + /** + * Overridable method called if opening a connection to a YubiKey throws an error. + * + * @param exception the Exception raised + */ + @WorkerThread + protected void onError(Exception exception) { + Logger.error(logger, "Error connecting to YubiKey", exception); + } +} diff --git a/android/src/main/java/com/yubico/yubikit/android/ui/package-info.java b/android/src/main/java/com/yubico/yubikit/android/ui/package-info.java index 3da6c310..36677d96 100755 --- a/android/src/main/java/com/yubico/yubikit/android/ui/package-info.java +++ b/android/src/main/java/com/yubico/yubikit/android/ui/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.android.ui; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/android/src/test/java/com/yubico/yubikit/android/YubikitManagerTest.java b/android/src/test/java/com/yubico/yubikit/android/YubikitManagerTest.java index 65f97fda..71a9eac6 100755 --- a/android/src/test/java/com/yubico/yubikit/android/YubikitManagerTest.java +++ b/android/src/test/java/com/yubico/yubikit/android/YubikitManagerTest.java @@ -16,9 +16,7 @@ package com.yubico.yubikit.android; import android.app.Activity; - import androidx.test.ext.junit.runners.AndroidJUnit4; - import com.yubico.yubikit.android.transport.nfc.NfcConfiguration; import com.yubico.yubikit.android.transport.nfc.NfcNotAvailable; import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice; @@ -27,7 +25,11 @@ import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice; import com.yubico.yubikit.android.transport.usb.UsbYubiKeyManager; import com.yubico.yubikit.core.util.Callback; - +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -38,147 +40,148 @@ import org.mockito.stubbing.Answer; import org.robolectric.annotation.Config; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import javax.annotation.Nullable; - @RunWith(AndroidJUnit4.class) @Config(manifest = Config.NONE) public class YubikitManagerTest { - private final UsbYubiKeyManager mockUsb = Mockito.mock(UsbYubiKeyManager.class); - private final NfcYubiKeyManager mockNfc = Mockito.mock(NfcYubiKeyManager.class); - private final Activity mockActivity = Mockito.mock(Activity.class); - - private final UsbYubiKeyDevice usbSession = Mockito.mock(UsbYubiKeyDevice.class); - private final NfcYubiKeyDevice nfcSession = Mockito.mock(NfcYubiKeyDevice.class); - - private final CountDownLatch signal = new CountDownLatch(2); - private final YubiKitManager yubiKitManager = new YubiKitManager(mockUsb, mockNfc); - - @Before - public void setUp() throws NfcNotAvailable { - Mockito.doAnswer(new UsbListenerInvocationTest(usbSession)).when(mockUsb).enable(Mockito.any(), ArgumentMatchers.>any()); - Mockito.doAnswer(new NfcListenerInvocationTest(nfcSession)).when(mockNfc).enable(Mockito.any(), Mockito.any(), ArgumentMatchers.>any()); + private final UsbYubiKeyManager mockUsb = Mockito.mock(UsbYubiKeyManager.class); + private final NfcYubiKeyManager mockNfc = Mockito.mock(NfcYubiKeyManager.class); + private final Activity mockActivity = Mockito.mock(Activity.class); + + private final UsbYubiKeyDevice usbSession = Mockito.mock(UsbYubiKeyDevice.class); + private final NfcYubiKeyDevice nfcSession = Mockito.mock(NfcYubiKeyDevice.class); + + private final CountDownLatch signal = new CountDownLatch(2); + private final YubiKitManager yubiKitManager = new YubiKitManager(mockUsb, mockNfc); + + @Before + public void setUp() throws NfcNotAvailable { + Mockito.doAnswer(new UsbListenerInvocationTest(usbSession)) + .when(mockUsb) + .enable(Mockito.any(), ArgumentMatchers.>any()); + Mockito.doAnswer(new NfcListenerInvocationTest(nfcSession)) + .when(mockNfc) + .enable(Mockito.any(), Mockito.any(), ArgumentMatchers.>any()); + } + + @Test + public void discoverSession() throws NfcNotAvailable { + yubiKitManager.startNfcDiscovery(new NfcConfiguration(), mockActivity, new NfcListener()); + yubiKitManager.startUsbDiscovery(new UsbConfiguration(), new UsbListener()); + + // wait until listener will be invoked + try { + final boolean ignoredResult = signal.await(1, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Assert.fail(); } - @Test - public void discoverSession() throws NfcNotAvailable { - yubiKitManager.startNfcDiscovery(new NfcConfiguration(), mockActivity, new NfcListener()); - yubiKitManager.startUsbDiscovery(new UsbConfiguration(), new UsbListener()); - - // wait until listener will be invoked - try { - final boolean ignoredResult = signal.await(1, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Assert.fail(); - } - - yubiKitManager.stopUsbDiscovery(); - yubiKitManager.stopNfcDiscovery(mockActivity); - Mockito.verify(mockUsb).disable(); - Mockito.verify(mockNfc).disable(mockActivity); - - // expected to discover 2 sessions - Assert.assertEquals(0, signal.getCount()); + yubiKitManager.stopUsbDiscovery(); + yubiKitManager.stopNfcDiscovery(mockActivity); + Mockito.verify(mockUsb).disable(); + Mockito.verify(mockNfc).disable(mockActivity); + + // expected to discover 2 sessions + Assert.assertEquals(0, signal.getCount()); + } + + @Test + public void discoverUsbSession() throws NfcNotAvailable { + UsbConfiguration configuration = new UsbConfiguration(); + yubiKitManager.startUsbDiscovery(configuration, new UsbListener()); + + Mockito.verify(mockUsb) + .enable(Mockito.eq(configuration), ArgumentMatchers.>any()); + Mockito.verify(mockNfc, Mockito.never()).enable(Mockito.any(), Mockito.any(), Mockito.any()); + + // wait until listener will be invoked + try { + final boolean ignoredResult = signal.await(1, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Assert.fail(); } - @Test - public void discoverUsbSession() throws NfcNotAvailable { - UsbConfiguration configuration = new UsbConfiguration(); - yubiKitManager.startUsbDiscovery(configuration, new UsbListener()); - - Mockito.verify(mockUsb).enable(Mockito.eq(configuration), ArgumentMatchers.>any()); - Mockito.verify(mockNfc, Mockito.never()).enable(Mockito.any(), Mockito.any(), Mockito.any()); - - // wait until listener will be invoked - try { - final boolean ignoredResult = signal.await(1, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Assert.fail(); - } - - yubiKitManager.stopUsbDiscovery(); - Mockito.verify(mockUsb).disable(); - Mockito.verify(mockNfc, Mockito.never()).disable(mockActivity); - - // expected to discover 1 session - Assert.assertEquals(1, signal.getCount()); + yubiKitManager.stopUsbDiscovery(); + Mockito.verify(mockUsb).disable(); + Mockito.verify(mockNfc, Mockito.never()).disable(mockActivity); + + // expected to discover 1 session + Assert.assertEquals(1, signal.getCount()); + } + + @Test + public void discoverNfcSession() throws NfcNotAvailable { + NfcConfiguration configuration = new NfcConfiguration(); + yubiKitManager.startNfcDiscovery(configuration, mockActivity, new NfcListener()); + + Mockito.verify(mockUsb, Mockito.never()) + .enable(Mockito.any(), ArgumentMatchers.>any()); + Mockito.verify(mockNfc) + .enable(Mockito.eq(mockActivity), Mockito.eq(configuration), Mockito.any()); + + // wait until listener will be invoked + try { + final boolean ignoredResult = signal.await(1, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Assert.fail(); } - @Test - public void discoverNfcSession() throws NfcNotAvailable { - NfcConfiguration configuration = new NfcConfiguration(); - yubiKitManager.startNfcDiscovery(configuration, mockActivity, new NfcListener()); - - Mockito.verify(mockUsb, Mockito.never()).enable(Mockito.any(), ArgumentMatchers.>any()); - Mockito.verify(mockNfc).enable(Mockito.eq(mockActivity), Mockito.eq(configuration), Mockito.any()); + yubiKitManager.stopNfcDiscovery(mockActivity); + Mockito.verify(mockUsb, Mockito.never()).disable(); + Mockito.verify(mockNfc).disable(mockActivity); - // wait until listener will be invoked - try { - final boolean ignoredResult = signal.await(1, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Assert.fail(); - } + // expected to discover 1 session + Assert.assertEquals(1, signal.getCount()); + } - yubiKitManager.stopNfcDiscovery(mockActivity); - Mockito.verify(mockUsb, Mockito.never()).disable(); - Mockito.verify(mockNfc).disable(mockActivity); - - // expected to discover 1 session - Assert.assertEquals(1, signal.getCount()); + private class UsbListener implements Callback { + @Override + public void invoke(UsbYubiKeyDevice value) { + signal.countDown(); } + } - private class UsbListener implements Callback { - @Override - public void invoke(UsbYubiKeyDevice value) { - signal.countDown(); - } + private class NfcListener implements Callback { + @Override + public void invoke(NfcYubiKeyDevice value) { + signal.countDown(); } + } - private class NfcListener implements Callback { - @Override - public void invoke(NfcYubiKeyDevice value) { - signal.countDown(); - } - } + private static class UsbListenerInvocationTest implements Answer { + private final UsbYubiKeyDevice session; - private static class UsbListenerInvocationTest implements Answer { - private final UsbYubiKeyDevice session; - - private UsbListenerInvocationTest(UsbYubiKeyDevice session) { - this.session = session; - } + private UsbListenerInvocationTest(UsbYubiKeyDevice session) { + this.session = session; + } - @Nullable - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - Callback internalListener = invocation.getArgument(1); - new Timer().schedule(new TimerTask() { + @Nullable @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Callback internalListener = invocation.getArgument(1); + new Timer() + .schedule( + new TimerTask() { @Override public void run() { - internalListener.invoke(session); + internalListener.invoke(session); } - }, 100); // emulating that discovery of session took some time - return null; - } + }, + 100); // emulating that discovery of session took some time + return null; } + } - private static class NfcListenerInvocationTest implements Answer { - private final NfcYubiKeyDevice session; + private static class NfcListenerInvocationTest implements Answer { + private final NfcYubiKeyDevice session; - private NfcListenerInvocationTest(NfcYubiKeyDevice session) { - this.session = session; - } + private NfcListenerInvocationTest(NfcYubiKeyDevice session) { + this.session = session; + } - @Nullable - @Override - public Object answer(InvocationOnMock invocation) { - Callback internalListener = invocation.getArgument(2); - internalListener.invoke(session); - return null; - } + @Nullable @Override + public Object answer(InvocationOnMock invocation) { + Callback internalListener = invocation.getArgument(2); + internalListener.invoke(session); + return null; } + } } diff --git a/android/src/test/java/com/yubico/yubikit/android/transport/usb/connection/UsbSmartCardConnectionTest.java b/android/src/test/java/com/yubico/yubikit/android/transport/usb/connection/UsbSmartCardConnectionTest.java index 053abb07..3629bbb5 100755 --- a/android/src/test/java/com/yubico/yubikit/android/transport/usb/connection/UsbSmartCardConnectionTest.java +++ b/android/src/test/java/com/yubico/yubikit/android/transport/usb/connection/UsbSmartCardConnectionTest.java @@ -16,106 +16,121 @@ package com.yubico.yubikit.android.transport.usb.connection; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbEndpoint; import android.hardware.usb.UsbInterface; - import com.yubico.yubikit.testing.Codec; - -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; public class UsbSmartCardConnectionTest { - private final UsbDeviceConnection usbDeviceConnection = mock(UsbDeviceConnection.class); - private final UsbInterface usbInterface = mock(UsbInterface.class); - private final UsbEndpoint usbEndpointIn = mock(UsbEndpoint.class); - private final UsbEndpoint usbEndpointOut = mock(UsbEndpoint.class); - private final List packetsIn = new ArrayList<>(); - private final List packetsOut = new ArrayList<>(); - - private void assertSent(String hex) { - Assert.assertArrayEquals("Unexpected packet sent", Codec.fromHex(hex), packetsOut.remove(0)); - } - - @Before - public void setup() { - when(usbEndpointIn.getMaxPacketSize()).thenReturn(64); - when(usbEndpointOut.getMaxPacketSize()).thenReturn(64); - - when(usbDeviceConnection.bulkTransfer(eq(usbEndpointOut), any(), anyInt(), anyInt(), anyInt())).then(invocation -> { - byte[] buffer = invocation.getArgument(1); - int offset = invocation.getArgument(2); - int length = invocation.getArgument(3); - int bytesSent = Math.min(64, length); - packetsOut.add(Arrays.copyOfRange(buffer, offset, offset + bytesSent)); - return bytesSent; - }); - - when(usbDeviceConnection.bulkTransfer(eq(usbEndpointIn), any(), anyInt(), anyInt())).then(invocation -> { - byte[] buffer = invocation.getArgument(1); - byte[] packet = Codec.fromHex(packetsIn.remove(0)); - System.arraycopy(packet, 0, buffer, 0, packet.length); - return packet.length; - }); - } - - @After - public void teardown() { - Assert.assertTrue(packetsIn.size() + " un-asserted packets in read buffer", packetsIn.isEmpty()); - Assert.assertTrue(packetsOut.size() + " un-asserted packets in sent buffer", packetsOut.isEmpty()); - } - - private UsbSmartCardConnection getConnection() throws IOException { - // ATR - response to power on - packetsIn.add("801700000000000000003bfd1300008131fe158073c021c057597562694b657940"); - UsbSmartCardConnection connection = new UsbSmartCardConnection(usbDeviceConnection, usbInterface, usbEndpointIn, usbEndpointOut); - assertSent("62000000000000000000"); // Power on command - return connection; - } - - @Test - public void testSendAndReceive() throws IOException { - UsbSmartCardConnection connection = getConnection(); - - packetsIn.add("800500000000010000000102039000"); - byte[] response = connection.sendAndReceive(Codec.fromHex("0001020300")); - assertSent("6f0500000000010000000001020300"); - - Assert.assertArrayEquals(Codec.fromHex("0102039000"), response); - } - - @Test - public void testSendChunked() throws IOException { - UsbSmartCardConnection connection = getConnection(); - - packetsIn.add("800200000000010000009000"); - connection.sendAndReceive(Codec.fromHex("00010000320001020304050607080900010203040506070809000102030405060708090001020304050607080900010203040506070809")); - - assertSent("6f370000000001000000000100003200010203040506070809000102030405060708090001020304050607080900010203040506070809000102030405060708"); - assertSent("09"); - } - - @Test - public void testSendEmptyPacketOnExactMultiple() throws IOException { - UsbSmartCardConnection connection = getConnection(); - - packetsIn.add("800200000000010000009000"); - connection.sendAndReceive(Codec.fromHex("000100003100010203040506070809000102030405060708090001020304050607080900010203040506070809000102030405060708")); - - assertSent("6f360000000001000000000100003100010203040506070809000102030405060708090001020304050607080900010203040506070809000102030405060708"); - assertSent(""); // An empty packet must be sent when the last packet ends on a boundary - } + private final UsbDeviceConnection usbDeviceConnection = mock(UsbDeviceConnection.class); + private final UsbInterface usbInterface = mock(UsbInterface.class); + private final UsbEndpoint usbEndpointIn = mock(UsbEndpoint.class); + private final UsbEndpoint usbEndpointOut = mock(UsbEndpoint.class); + private final List packetsIn = new ArrayList<>(); + private final List packetsOut = new ArrayList<>(); + + private void assertSent(String hex) { + Assert.assertArrayEquals("Unexpected packet sent", Codec.fromHex(hex), packetsOut.remove(0)); + } + + @Before + public void setup() { + when(usbEndpointIn.getMaxPacketSize()).thenReturn(64); + when(usbEndpointOut.getMaxPacketSize()).thenReturn(64); + + when(usbDeviceConnection.bulkTransfer(eq(usbEndpointOut), any(), anyInt(), anyInt(), anyInt())) + .then( + invocation -> { + byte[] buffer = invocation.getArgument(1); + int offset = invocation.getArgument(2); + int length = invocation.getArgument(3); + int bytesSent = Math.min(64, length); + packetsOut.add(Arrays.copyOfRange(buffer, offset, offset + bytesSent)); + return bytesSent; + }); + + when(usbDeviceConnection.bulkTransfer(eq(usbEndpointIn), any(), anyInt(), anyInt())) + .then( + invocation -> { + byte[] buffer = invocation.getArgument(1); + byte[] packet = Codec.fromHex(packetsIn.remove(0)); + System.arraycopy(packet, 0, buffer, 0, packet.length); + return packet.length; + }); + } + + @After + public void teardown() { + Assert.assertTrue( + packetsIn.size() + " un-asserted packets in read buffer", packetsIn.isEmpty()); + Assert.assertTrue( + packetsOut.size() + " un-asserted packets in sent buffer", packetsOut.isEmpty()); + } + + private UsbSmartCardConnection getConnection() throws IOException { + // ATR - response to power on + packetsIn.add("801700000000000000003bfd1300008131fe158073c021c057597562694b657940"); + UsbSmartCardConnection connection = + new UsbSmartCardConnection( + usbDeviceConnection, usbInterface, usbEndpointIn, usbEndpointOut); + assertSent("62000000000000000000"); // Power on command + return connection; + } + + @Test + public void testSendAndReceive() throws IOException { + UsbSmartCardConnection connection = getConnection(); + + packetsIn.add("800500000000010000000102039000"); + byte[] response = connection.sendAndReceive(Codec.fromHex("0001020300")); + assertSent("6f0500000000010000000001020300"); + + Assert.assertArrayEquals(Codec.fromHex("0102039000"), response); + } + + @Test + public void testSendChunked() throws IOException { + UsbSmartCardConnection connection = getConnection(); + + packetsIn.add("800200000000010000009000"); + connection.sendAndReceive( + Codec.fromHex( + "0001000032000102030405060708090001020304050607080900010203040506070809000102030405060" + + "7080900010203040506070809")); + + assertSent( + "6f370000000001000000000100003200010203040506070809000102030405060708090001020304050607080" + + "900010203040506070809000102030405060708"); + assertSent("09"); + } + + @Test + public void testSendEmptyPacketOnExactMultiple() throws IOException { + UsbSmartCardConnection connection = getConnection(); + + packetsIn.add("800200000000010000009000"); + connection.sendAndReceive( + Codec.fromHex( + "0001000031000102030405060708090001020304050607080900010203040506070809000102030405060" + + "70809000102030405060708")); + + assertSent( + "6f360000000001000000000100003100010203040506070809000102030405060708090001020304050607080" + + "900010203040506070809000102030405060708"); + assertSent(""); // An empty packet must be sent when the last packet ends on a boundary + } } diff --git a/buildSrc/src/main/groovy/project-convention-spotless.gradle b/buildSrc/src/main/groovy/project-convention-spotless.gradle index 404a9356..a242c154 100644 --- a/buildSrc/src/main/groovy/project-convention-spotless.gradle +++ b/buildSrc/src/main/groovy/project-convention-spotless.gradle @@ -27,7 +27,6 @@ spotless { importOrder() toggleOffOn() googleJavaFormat('1.25.2') - .aosp() .reflowLongStrings(true) .formatJavadoc(true) .reorderImports(true) diff --git a/core/src/main/java/com/yubico/yubikit/core/Logger.java b/core/src/main/java/com/yubico/yubikit/core/Logger.java index 4790d7b6..cadf56da 100755 --- a/core/src/main/java/com/yubico/yubikit/core/Logger.java +++ b/core/src/main/java/com/yubico/yubikit/core/Logger.java @@ -19,68 +19,62 @@ import javax.annotation.Nullable; /** - * Helper class allows to customize logs within the SDK - * SDK has only 2 levels of logging: debug information and error - * If a Logger implementation is not provided the SDK won't produce any logs + * Helper class allows to customize logs within the SDK SDK has only 2 levels of logging: debug + * information and error If a Logger implementation is not provided the SDK won't produce any logs * - * @see Logging Migration - * contains information about logging in YubiKit, best practices and migration from Logger. - * @deprecated This class and all its public methods have been deprecated in YubiKit 2.3.0 and will be removed - * in future release. + * @see Logging + * Migration contains information about logging in YubiKit, best practices and migration + * from Logger. + * @deprecated This class and all its public methods have been deprecated in YubiKit 2.3.0 and will + * be removed in future release. */ @Deprecated public abstract class Logger { - /** - * Specifies how debug messages are logged. - *

- * If this method is not overridden, then debug messages will not be logged. - * - * @param message the message can to be logged - */ - protected void logDebug(String message) { - } + /** + * Specifies how debug messages are logged. + * + *

If this method is not overridden, then debug messages will not be logged. + * + * @param message the message can to be logged + */ + protected void logDebug(String message) {} - /** - * Specifies how error messages (with exceptions) are logged. - *

- * If this method is not overridden, then error messages will not be logged. - * - * @param message the message can to be logged - * @param throwable the exception that can to be logged or counted - */ - protected void logError(String message, Throwable throwable) { - } + /** + * Specifies how error messages (with exceptions) are logged. + * + *

If this method is not overridden, then error messages will not be logged. + * + * @param message the message can to be logged + * @param throwable the exception that can to be logged or counted + */ + protected void logError(String message, Throwable throwable) {} - @Nullable - static Logger instance = null; + @Nullable static Logger instance = null; - /** - * Set the Logger implementation to use. Override the logDebug and logError methods to produce - * logs. Call with null to disable logging. - * - * @param logger the Logger implementation to use - */ - public static void setLogger(@Nullable Logger logger) { - instance = logger; - com.yubico.yubikit.core.internal.Logger.setLogger(instance); - } + /** + * Set the Logger implementation to use. Override the logDebug and logError methods to produce + * logs. Call with null to disable logging. + * + * @param logger the Logger implementation to use + */ + public static void setLogger(@Nullable Logger logger) { + instance = logger; + com.yubico.yubikit.core.internal.Logger.setLogger(instance); + } - /** - * Log a debug message. - */ - public static void d(String message) { - if (instance != null) { - instance.logDebug(message); - } + /** Log a debug message. */ + public static void d(String message) { + if (instance != null) { + instance.logDebug(message); } + } - /** - * Log an error message, together with an exception. - */ - public static void e(String message, Throwable throwable) { - if (instance != null) { - instance.logError(message, throwable); - } + /** Log an error message, together with an exception. */ + public static void e(String message, Throwable throwable) { + if (instance != null) { + instance.logError(message, throwable); } + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/PackageNonnullByDefault.java b/core/src/main/java/com/yubico/yubikit/core/PackageNonnullByDefault.java index 25bc568c..e4b0ce13 100755 --- a/core/src/main/java/com/yubico/yubikit/core/PackageNonnullByDefault.java +++ b/core/src/main/java/com/yubico/yubikit/core/PackageNonnullByDefault.java @@ -19,20 +19,15 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; - import javax.annotation.Nonnull; import javax.annotation.meta.TypeQualifierDefault; /** - * Annotation used in package-info.java to indicate that all fields, methods, and parameters are Nonnull by default. + * Annotation used in package-info.java to indicate that all fields, methods, and parameters are + * Nonnull by default. */ @Documented @Nonnull -@TypeQualifierDefault({ - ElementType.FIELD, - ElementType.METHOD, - ElementType.PARAMETER -}) +@TypeQualifierDefault({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) -public @interface PackageNonnullByDefault { -} +public @interface PackageNonnullByDefault {} diff --git a/core/src/main/java/com/yubico/yubikit/core/Transport.java b/core/src/main/java/com/yubico/yubikit/core/Transport.java index 741a1694..f732f819 100755 --- a/core/src/main/java/com/yubico/yubikit/core/Transport.java +++ b/core/src/main/java/com/yubico/yubikit/core/Transport.java @@ -16,16 +16,10 @@ package com.yubico.yubikit.core; -/** - * Physical transports which can be used to connect to a YubiKey. - */ +/** Physical transports which can be used to connect to a YubiKey. */ public enum Transport { - /** - * A USB-A or USB-C connector. - */ - USB, - /** - * Near-field communication, using a built-in antenna. - */ - NFC + /** A USB-A or USB-C connector. */ + USB, + /** Near-field communication, using a built-in antenna. */ + NFC } diff --git a/core/src/main/java/com/yubico/yubikit/core/UsbInterface.java b/core/src/main/java/com/yubico/yubikit/core/UsbInterface.java index e7fb532b..bdf8e2ba 100755 --- a/core/src/main/java/com/yubico/yubikit/core/UsbInterface.java +++ b/core/src/main/java/com/yubico/yubikit/core/UsbInterface.java @@ -16,48 +16,46 @@ package com.yubico.yubikit.core; /** - * Provides constants for the different YubiKey USB interfaces, and the Mode enum for combinations of enabled interfaces. + * Provides constants for the different YubiKey USB interfaces, and the Mode enum for combinations + * of enabled interfaces. */ public final class UsbInterface { - public static final int OTP = 0x01; - public static final int FIDO = 0x02; - public static final int CCID = 0x04; + public static final int OTP = 0x01; + public static final int FIDO = 0x02; + public static final int CCID = 0x04; - private UsbInterface() { - } + private UsbInterface() {} - /** - * Used for configuring USB Mode for YubiKey 3 and 4. - *

- * This is replaced by DeviceConfig starting with YubiKey 5. - */ - public enum Mode { - OTP((byte) 0x00, UsbInterface.OTP), - CCID((byte) 0x01, UsbInterface.CCID), - OTP_CCID((byte) 0x02, UsbInterface.OTP | UsbInterface.CCID), - FIDO((byte) 0x03, UsbInterface.FIDO), - OTP_FIDO((byte) 0x04, UsbInterface.OTP | UsbInterface.FIDO), - FIDO_CCID((byte) 0x05, UsbInterface.FIDO | UsbInterface.CCID), - OTP_FIDO_CCID((byte) 0x06, UsbInterface.OTP | UsbInterface.FIDO | UsbInterface.CCID); + /** + * Used for configuring USB Mode for YubiKey 3 and 4. + * + *

This is replaced by DeviceConfig starting with YubiKey 5. + */ + public enum Mode { + OTP((byte) 0x00, UsbInterface.OTP), + CCID((byte) 0x01, UsbInterface.CCID), + OTP_CCID((byte) 0x02, UsbInterface.OTP | UsbInterface.CCID), + FIDO((byte) 0x03, UsbInterface.FIDO), + OTP_FIDO((byte) 0x04, UsbInterface.OTP | UsbInterface.FIDO), + FIDO_CCID((byte) 0x05, UsbInterface.FIDO | UsbInterface.CCID), + OTP_FIDO_CCID((byte) 0x06, UsbInterface.OTP | UsbInterface.FIDO | UsbInterface.CCID); - public final byte value; - public final int interfaces; + public final byte value; + public final int interfaces; - Mode(byte value, int interfaces) { - this.value = value; - this.interfaces = interfaces; - } + Mode(byte value, int interfaces) { + this.value = value; + this.interfaces = interfaces; + } - /** - * Returns the USB Mode given the enabled USB interfaces it has. - */ - public static Mode getMode(int interfaces) { - for (Mode mode : Mode.values()) { - if (mode.interfaces == interfaces) { - return mode; - } - } - throw new IllegalArgumentException("Invalid interfaces for Mode"); + /** Returns the USB Mode given the enabled USB interfaces it has. */ + public static Mode getMode(int interfaces) { + for (Mode mode : Mode.values()) { + if (mode.interfaces == interfaces) { + return mode; } + } + throw new IllegalArgumentException("Invalid interfaces for Mode"); } + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/UsbPid.java b/core/src/main/java/com/yubico/yubikit/core/UsbPid.java index 93a5f89d..814f573a 100644 --- a/core/src/main/java/com/yubico/yubikit/core/UsbPid.java +++ b/core/src/main/java/com/yubico/yubikit/core/UsbPid.java @@ -17,41 +17,43 @@ package com.yubico.yubikit.core; public enum UsbPid { - YKS_OTP(0x0010, YubiKeyType.YKS, UsbInterface.OTP), - NEO_OTP(0x0110, YubiKeyType.NEO, UsbInterface.OTP), - NEO_OTP_CCID(0x0111, YubiKeyType.NEO, UsbInterface.OTP | UsbInterface.CCID), - NEO_CCID(0x0112, YubiKeyType.NEO, UsbInterface.CCID), - NEO_FIDO(0x0113, YubiKeyType.NEO, UsbInterface.FIDO), - NEO_OTP_FIDO(0x0114, YubiKeyType.NEO, UsbInterface.OTP | UsbInterface.FIDO), - NEO_FIDO_CCID(0x0115, YubiKeyType.NEO, UsbInterface.CCID | UsbInterface.FIDO), - NEO_OTP_FIDO_CCID(0x0116, YubiKeyType.NEO, UsbInterface.OTP | UsbInterface.FIDO | UsbInterface.CCID), - SKY_FIDO(0x0120, YubiKeyType.SKY, UsbInterface.FIDO), - YK4_OTP(0x0401, YubiKeyType.YK4, UsbInterface.OTP), - YK4_FIDO(0x0402, YubiKeyType.YK4, UsbInterface.FIDO), - YK4_OTP_FIDO(0x0403, YubiKeyType.YK4, UsbInterface.OTP | UsbInterface.FIDO), - YK4_CCID(0x0404, YubiKeyType.YK4, UsbInterface.CCID), - YK4_OTP_CCID(0x0405, YubiKeyType.YK4, UsbInterface.OTP | UsbInterface.CCID), - YK4_FIDO_CCID(0x0406, YubiKeyType.YK4, UsbInterface.FIDO | UsbInterface.CCID), - YK4_OTP_FIDO_CCID(0x0407, YubiKeyType.YK4, UsbInterface.OTP | UsbInterface.FIDO | UsbInterface.CCID), - YKP_OTP_FIDO(0x0410, YubiKeyType.YKP, UsbInterface.OTP | UsbInterface.FIDO); + YKS_OTP(0x0010, YubiKeyType.YKS, UsbInterface.OTP), + NEO_OTP(0x0110, YubiKeyType.NEO, UsbInterface.OTP), + NEO_OTP_CCID(0x0111, YubiKeyType.NEO, UsbInterface.OTP | UsbInterface.CCID), + NEO_CCID(0x0112, YubiKeyType.NEO, UsbInterface.CCID), + NEO_FIDO(0x0113, YubiKeyType.NEO, UsbInterface.FIDO), + NEO_OTP_FIDO(0x0114, YubiKeyType.NEO, UsbInterface.OTP | UsbInterface.FIDO), + NEO_FIDO_CCID(0x0115, YubiKeyType.NEO, UsbInterface.CCID | UsbInterface.FIDO), + NEO_OTP_FIDO_CCID( + 0x0116, YubiKeyType.NEO, UsbInterface.OTP | UsbInterface.FIDO | UsbInterface.CCID), + SKY_FIDO(0x0120, YubiKeyType.SKY, UsbInterface.FIDO), + YK4_OTP(0x0401, YubiKeyType.YK4, UsbInterface.OTP), + YK4_FIDO(0x0402, YubiKeyType.YK4, UsbInterface.FIDO), + YK4_OTP_FIDO(0x0403, YubiKeyType.YK4, UsbInterface.OTP | UsbInterface.FIDO), + YK4_CCID(0x0404, YubiKeyType.YK4, UsbInterface.CCID), + YK4_OTP_CCID(0x0405, YubiKeyType.YK4, UsbInterface.OTP | UsbInterface.CCID), + YK4_FIDO_CCID(0x0406, YubiKeyType.YK4, UsbInterface.FIDO | UsbInterface.CCID), + YK4_OTP_FIDO_CCID( + 0x0407, YubiKeyType.YK4, UsbInterface.OTP | UsbInterface.FIDO | UsbInterface.CCID), + YKP_OTP_FIDO(0x0410, YubiKeyType.YKP, UsbInterface.OTP | UsbInterface.FIDO); - public final int value; - public final YubiKeyType type; - public final int usbInterfaces; + public final int value; + public final YubiKeyType type; + public final int usbInterfaces; - UsbPid(int value, YubiKeyType type, int usbInterfaces) { - this.value = value; - this.type = type; - this.usbInterfaces = usbInterfaces; - } - - static public UsbPid fromValue(int value) throws IllegalArgumentException { - for (UsbPid pid : UsbPid.values()) { - if (pid.value == value) { - return pid; - } - } + UsbPid(int value, YubiKeyType type, int usbInterfaces) { + this.value = value; + this.type = type; + this.usbInterfaces = usbInterfaces; + } - throw new IllegalArgumentException("invalid pid value"); + public static UsbPid fromValue(int value) throws IllegalArgumentException { + for (UsbPid pid : UsbPid.values()) { + if (pid.value == value) { + return pid; + } } -} \ No newline at end of file + + throw new IllegalArgumentException("invalid pid value"); + } +} diff --git a/core/src/main/java/com/yubico/yubikit/core/Version.java b/core/src/main/java/com/yubico/yubikit/core/Version.java index 270e273f..67f20d3d 100755 --- a/core/src/main/java/com/yubico/yubikit/core/Version.java +++ b/core/src/main/java/com/yubico/yubikit/core/Version.java @@ -16,124 +16,112 @@ package com.yubico.yubikit.core; - import java.util.Locale; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; -/** - * A 3-part version number, used by the YubiKey firmware and its various applications. - */ +/** A 3-part version number, used by the YubiKey firmware and its various applications. */ public final class Version implements Comparable { - private static final Pattern VERSION_STRING_PATTERN = Pattern.compile("\\b(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\b"); - - public final byte major; - public final byte minor; - public final byte micro; - - private static byte checkRange(int value) { - if (value < 0 || value > Byte.MAX_VALUE) { - throw new IllegalArgumentException("Version component out of supported range (0-127)"); - } - return (byte) value; - } - - /** - * Constructor using int's for convenience. - *

- * Each version component will be checked to ensure it falls within an acceptable range. - */ - public Version(int major, int minor, int micro) { - this(checkRange(major), checkRange(minor), checkRange(micro)); - } - - /** - * Constructs a new Version object. - */ - public Version(byte major, byte minor, byte micro) { - this.major = major; - this.minor = minor; - this.micro = micro; - } + private static final Pattern VERSION_STRING_PATTERN = + Pattern.compile("\\b(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\b"); - /** - * Returns the version components as a byte array of size 3. - */ - public byte[] getBytes() { - return new byte[]{major, minor, micro}; - } - - private int compareToVersion(int major, int minor, int micro) { - return Integer.compare(this.major << 16 | this.minor << 8 | this.micro, major << 16 | minor << 8 | micro); - } + public final byte major; + public final byte minor; + public final byte micro; - @Override - public int compareTo(Version other) { - return compareToVersion(other.major, other.minor, other.micro); + private static byte checkRange(int value) { + if (value < 0 || value > Byte.MAX_VALUE) { + throw new IllegalArgumentException("Version component out of supported range (0-127)"); } - - /** - * Returns whether or not the Version is less than a given version. - */ - public boolean isLessThan(int major, int minor, int micro) { - return compareToVersion(major, minor, micro) < 0; - } - - /** - * Returns whether or not the Version is greater than or equal to a given version. - */ - public boolean isAtLeast(int major, int minor, int micro) { - return compareToVersion(major, minor, micro) >= 0; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Version version = (Version) o; - return major == version.major && - minor == version.minor && - micro == version.micro; - } - - @Override - public int hashCode() { - return Objects.hash(major, minor, micro); - } - - @Override - public String toString() { - return String.format(Locale.ROOT, "%d.%d.%d", 0xff & major, 0xff & minor, 0xff & micro); - } - - /** - * Parses a Version from a byte array by taking the first three bytes. - *

- * Additional bytes in the array are ignored. - */ - public static Version fromBytes(byte[] bytes) { - if (bytes.length < 3) { - throw new IllegalArgumentException("Version byte array must contain 3 bytes."); - } - - return new Version(bytes[0], bytes[1], bytes[2]); + return (byte) value; + } + + /** + * Constructor using int's for convenience. + * + *

Each version component will be checked to ensure it falls within an acceptable range. + */ + public Version(int major, int minor, int micro) { + this(checkRange(major), checkRange(minor), checkRange(micro)); + } + + /** Constructs a new Version object. */ + public Version(byte major, byte minor, byte micro) { + this.major = major; + this.minor = minor; + this.micro = micro; + } + + /** Returns the version components as a byte array of size 3. */ + public byte[] getBytes() { + return new byte[] {major, minor, micro}; + } + + private int compareToVersion(int major, int minor, int micro) { + return Integer.compare( + this.major << 16 | this.minor << 8 | this.micro, major << 16 | minor << 8 | micro); + } + + @Override + public int compareTo(Version other) { + return compareToVersion(other.major, other.minor, other.micro); + } + + /** Returns whether or not the Version is less than a given version. */ + public boolean isLessThan(int major, int minor, int micro) { + return compareToVersion(major, minor, micro) < 0; + } + + /** Returns whether or not the Version is greater than or equal to a given version. */ + public boolean isAtLeast(int major, int minor, int micro) { + return compareToVersion(major, minor, micro) >= 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Version version = (Version) o; + return major == version.major && minor == version.minor && micro == version.micro; + } + + @Override + public int hashCode() { + return Objects.hash(major, minor, micro); + } + + @Override + public String toString() { + return String.format(Locale.ROOT, "%d.%d.%d", 0xff & major, 0xff & minor, 0xff & micro); + } + + /** + * Parses a Version from a byte array by taking the first three bytes. + * + *

Additional bytes in the array are ignored. + */ + public static Version fromBytes(byte[] bytes) { + if (bytes.length < 3) { + throw new IllegalArgumentException("Version byte array must contain 3 bytes."); } - /** - * Parses a Version from a String (eg. "Firmware version 5.2.1") - * - * @param versionString string that contains a 3-part version, separated by dots. - */ - public static Version parse(String versionString) { - Matcher match = VERSION_STRING_PATTERN.matcher(versionString); - if (match.find()) { - return new Version( - Byte.parseByte(match.group(1)), - Byte.parseByte(match.group(2)), - Byte.parseByte(match.group(3)) - ); - } - throw new IllegalArgumentException("Invalid version string"); + return new Version(bytes[0], bytes[1], bytes[2]); + } + + /** + * Parses a Version from a String (eg. "Firmware version 5.2.1") + * + * @param versionString string that contains a 3-part version, separated by dots. + */ + public static Version parse(String versionString) { + Matcher match = VERSION_STRING_PATTERN.matcher(versionString); + if (match.find()) { + return new Version( + Byte.parseByte(match.group(1)), + Byte.parseByte(match.group(2)), + Byte.parseByte(match.group(3))); } + throw new IllegalArgumentException("Invalid version string"); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/YubiKeyConnection.java b/core/src/main/java/com/yubico/yubikit/core/YubiKeyConnection.java index d2baa26d..91ff906f 100755 --- a/core/src/main/java/com/yubico/yubikit/core/YubiKeyConnection.java +++ b/core/src/main/java/com/yubico/yubikit/core/YubiKeyConnection.java @@ -17,8 +17,5 @@ import java.io.Closeable; -/** - * A connection to a YubiKey, which typically exposes a way to send and receive data. - */ -public interface YubiKeyConnection extends Closeable { -} +/** A connection to a YubiKey, which typically exposes a way to send and receive data. */ +public interface YubiKeyConnection extends Closeable {} diff --git a/core/src/main/java/com/yubico/yubikit/core/YubiKeyDevice.java b/core/src/main/java/com/yubico/yubikit/core/YubiKeyDevice.java index 9c9ddc64..d727b371 100755 --- a/core/src/main/java/com/yubico/yubikit/core/YubiKeyDevice.java +++ b/core/src/main/java/com/yubico/yubikit/core/YubiKeyDevice.java @@ -17,31 +17,23 @@ import com.yubico.yubikit.core.util.Callback; import com.yubico.yubikit.core.util.Result; - import java.io.IOException; -/** - * A reference to a physical YubiKey. - */ +/** A reference to a physical YubiKey. */ public interface YubiKeyDevice { - /** - * Returns the transport used for communication - */ - Transport getTransport(); - - /** - * Returns whether or not a specific connection type is supported for this YubiKey, over this transport. - */ - boolean supportsConnection(Class connectionType); + /** Returns the transport used for communication */ + Transport getTransport(); - /** - * Requests a new connection of the given connection type. - */ - void requestConnection(Class connectionType, Callback> callback); + /** + * Returns whether or not a specific connection type is supported for this YubiKey, over this + * transport. + */ + boolean supportsConnection(Class connectionType); + /** Requests a new connection of the given connection type. */ + void requestConnection( + Class connectionType, Callback> callback); - /** - * Returns a new connection of the given connection type. - */ - T openConnection(Class connectionType) throws IOException; + /** Returns a new connection of the given connection type. */ + T openConnection(Class connectionType) throws IOException; } diff --git a/core/src/main/java/com/yubico/yubikit/core/YubiKeyType.java b/core/src/main/java/com/yubico/yubikit/core/YubiKeyType.java index 8e696166..9cabc11e 100644 --- a/core/src/main/java/com/yubico/yubikit/core/YubiKeyType.java +++ b/core/src/main/java/com/yubico/yubikit/core/YubiKeyType.java @@ -17,15 +17,15 @@ package com.yubico.yubikit.core; public enum YubiKeyType { - YKS("YubiKey Standard"), - NEO("YubiKey NEO"), - SKY("Security Key by Yubico"), - YKP("YubiKey Plus"), - YK4("YubiKey"); + YKS("YubiKey Standard"), + NEO("YubiKey NEO"), + SKY("Security Key by Yubico"), + YKP("YubiKey Plus"), + YK4("YubiKey"); - public final String name; + public final String name; - YubiKeyType(String name) { - this.name = name; - } + YubiKeyType(String name) { + this.name = name; + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/application/ApplicationNotAvailableException.java b/core/src/main/java/com/yubico/yubikit/core/application/ApplicationNotAvailableException.java index e887799b..8983b5c1 100755 --- a/core/src/main/java/com/yubico/yubikit/core/application/ApplicationNotAvailableException.java +++ b/core/src/main/java/com/yubico/yubikit/core/application/ApplicationNotAvailableException.java @@ -15,15 +15,13 @@ */ package com.yubico.yubikit.core.application; -/** - * The application is either disabled or not supported on the connected YubiKey. - */ +/** The application is either disabled or not supported on the connected YubiKey. */ public class ApplicationNotAvailableException extends CommandException { - public ApplicationNotAvailableException(String message) { - super(message); - } + public ApplicationNotAvailableException(String message) { + super(message); + } - public ApplicationNotAvailableException(String message, Throwable cause) { - super(message, cause); - } + public ApplicationNotAvailableException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/application/ApplicationSession.java b/core/src/main/java/com/yubico/yubikit/core/application/ApplicationSession.java index 3b7758eb..ded25a78 100755 --- a/core/src/main/java/com/yubico/yubikit/core/application/ApplicationSession.java +++ b/core/src/main/java/com/yubico/yubikit/core/application/ApplicationSession.java @@ -17,38 +17,39 @@ package com.yubico.yubikit.core.application; import com.yubico.yubikit.core.Version; - import java.io.Closeable; /** * A base class for Sessions with a YubiKey. - *

- * Subclasses should use their own type as the parameter T: + * + *

Subclasses should use their own type as the parameter T: + * *

{@code class FooSession extends ApplicationSession}
* * @param the type of the subclass */ public abstract class ApplicationSession> implements Closeable { - /** - * Get the version of the Application from the YubiKey. This is typically the same as the YubiKey firmware, but can be versioned separately as well. - * - * @return the Application version - */ - public abstract Version getVersion(); + /** + * Get the version of the Application from the YubiKey. This is typically the same as the YubiKey + * firmware, but can be versioned separately as well. + * + * @return the Application version + */ + public abstract Version getVersion(); - /** - * Check if a Feature is supported by the YubiKey. - * - * @param feature the Feature to check support for. - * @return true if the Feature is supported, false if not. - */ - public boolean supports(Feature feature) { - return feature.isSupportedBy(getVersion()); - } + /** + * Check if a Feature is supported by the YubiKey. + * + * @param feature the Feature to check support for. + * @return true if the Feature is supported, false if not. + */ + public boolean supports(Feature feature) { + return feature.isSupportedBy(getVersion()); + } - protected void require(Feature feature) { - if (!supports(feature)) { - throw new UnsupportedOperationException(feature.getRequiredMessage()); - } + protected void require(Feature feature) { + if (!supports(feature)) { + throw new UnsupportedOperationException(feature.getRequiredMessage()); } + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/application/BadResponseException.java b/core/src/main/java/com/yubico/yubikit/core/application/BadResponseException.java index b91f0021..0476df54 100755 --- a/core/src/main/java/com/yubico/yubikit/core/application/BadResponseException.java +++ b/core/src/main/java/com/yubico/yubikit/core/application/BadResponseException.java @@ -16,15 +16,13 @@ package com.yubico.yubikit.core.application; -/** - * The data contained in a YubiKey response was invalid. - */ +/** The data contained in a YubiKey response was invalid. */ public class BadResponseException extends CommandException { - public BadResponseException(String message) { - super(message); - } + public BadResponseException(String message) { + super(message); + } - public BadResponseException(String message, Throwable cause) { - super(message, cause); - } + public BadResponseException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/application/CommandException.java b/core/src/main/java/com/yubico/yubikit/core/application/CommandException.java index f1f1ae02..69af7a4a 100755 --- a/core/src/main/java/com/yubico/yubikit/core/application/CommandException.java +++ b/core/src/main/java/com/yubico/yubikit/core/application/CommandException.java @@ -15,15 +15,13 @@ */ package com.yubico.yubikit.core.application; -/** - * An error response from a YubiKey. - */ +/** An error response from a YubiKey. */ public class CommandException extends Exception { - public CommandException(String message) { - super(message); - } + public CommandException(String message) { + super(message); + } - public CommandException(String message, Throwable cause) { - super(message, cause); - } + public CommandException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/application/CommandState.java b/core/src/main/java/com/yubico/yubikit/core/application/CommandState.java index 98bcb659..9ed8ed2d 100755 --- a/core/src/main/java/com/yubico/yubikit/core/application/CommandState.java +++ b/core/src/main/java/com/yubico/yubikit/core/application/CommandState.java @@ -16,53 +16,52 @@ package com.yubico.yubikit.core.application; import com.yubico.yubikit.core.internal.Logger; - import org.slf4j.LoggerFactory; /** * Provides control over an ongoing YubiKey operation. - *

- * Override onKeepAliveMessage to react to keepalive messages send periodically from the YubiKey. + * + *

Override onKeepAliveMessage to react to keepalive messages send periodically from the YubiKey. * Call {@link #cancel()} to cancel an ongoing operation. */ public class CommandState { - public static final byte STATUS_PROCESSING = 1; - public static final byte STATUS_UPNEEDED = 2; + public static final byte STATUS_PROCESSING = 1; + public static final byte STATUS_UPNEEDED = 2; - private boolean cancelled = false; + private boolean cancelled = false; - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(CommandState.class); + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(CommandState.class); - /** - * Override this method to handle keep-alive messages sent from the YubiKey. - * The default implementation will log the event. - * - * @param status The keep alive status byte - */ - public void onKeepAliveStatus(byte status) { - Logger.debug(logger, "received keepalive status: {}", status); - } + /** + * Override this method to handle keep-alive messages sent from the YubiKey. The default + * implementation will log the event. + * + * @param status The keep alive status byte + */ + public void onKeepAliveStatus(byte status) { + Logger.debug(logger, "received keepalive status: {}", status); + } - /** - * Cancel an ongoing CTAP2 command, by sending a CTAP cancel command. This will cause the - * YubiKey to return a CtapError with the error code 0x2d (ERR_KEEPALIVE_CANCEL). - */ - public final synchronized void cancel() { - cancelled = true; - } + /** + * Cancel an ongoing CTAP2 command, by sending a CTAP cancel command. This will cause the YubiKey + * to return a CtapError with the error code 0x2d (ERR_KEEPALIVE_CANCEL). + */ + public final synchronized void cancel() { + cancelled = true; + } - /* Internal use only */ - public final synchronized boolean waitForCancel(long ms) { - if (!cancelled && ms > 0) { - try { - wait(ms); - } catch (InterruptedException e) { - Logger.debug(logger, "Thread interrupted, cancelling command"); - cancelled = true; - Thread.currentThread().interrupt(); - } - } - return cancelled; + /* Internal use only */ + public final synchronized boolean waitForCancel(long ms) { + if (!cancelled && ms > 0) { + try { + wait(ms); + } catch (InterruptedException e) { + Logger.debug(logger, "Thread interrupted, cancelling command"); + cancelled = true; + Thread.currentThread().interrupt(); + } } + return cancelled; + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/application/Feature.java b/core/src/main/java/com/yubico/yubikit/core/application/Feature.java index 7caf32c1..7cebce2f 100755 --- a/core/src/main/java/com/yubico/yubikit/core/application/Feature.java +++ b/core/src/main/java/com/yubico/yubikit/core/application/Feature.java @@ -26,54 +26,54 @@ * @param The type of Session for which the Feature is relevant. */ public abstract class Feature> { - protected final String featureName; + protected final String featureName; - protected Feature(String featureName) { - this.featureName = featureName; - } + protected Feature(String featureName) { + this.featureName = featureName; + } - /** - * Get a human readable name of the feature. - * - * @return the name of the feature - */ - public String getFeatureName() { - return featureName; - } + /** + * Get a human readable name of the feature. + * + * @return the name of the feature + */ + public String getFeatureName() { + return featureName; + } - /** - * Checks if the Feature is supported by the given Application version. - * - * @param version the version of the Application to check support for. - * @return true if the Feature is supported, false if not - */ - public abstract boolean isSupportedBy(Version version); + /** + * Checks if the Feature is supported by the given Application version. + * + * @param version the version of the Application to check support for. + * @return true if the Feature is supported, false if not + */ + public abstract boolean isSupportedBy(Version version); - protected String getRequiredMessage() { - return String.format("%s is not supported by this YubiKey", featureName); - } + protected String getRequiredMessage() { + return String.format("%s is not supported by this YubiKey", featureName); + } - /** - * A Feature which has a minimum version which it checks against. - * - * @param The type of Session for which the Feature is relevant. - */ - public static class Versioned> extends Feature { - private final Version requiredVersion; + /** + * A Feature which has a minimum version which it checks against. + * + * @param The type of Session for which the Feature is relevant. + */ + public static class Versioned> extends Feature { + private final Version requiredVersion; - public Versioned(String featureName, int major, int minor, int micro) { - super(featureName); - requiredVersion = new Version(major, minor, micro); - } + public Versioned(String featureName, int major, int minor, int micro) { + super(featureName); + requiredVersion = new Version(major, minor, micro); + } - @Override - protected String getRequiredMessage() { - return String.format("%s requires YubiKey %s or later", featureName, requiredVersion); - } + @Override + protected String getRequiredMessage() { + return String.format("%s requires YubiKey %s or later", featureName, requiredVersion); + } - @Override - public boolean isSupportedBy(Version version) { - return overrideOf(version).compareTo(requiredVersion) >= 0; - } + @Override + public boolean isSupportedBy(Version version) { + return overrideOf(version).compareTo(requiredVersion) >= 0; } + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/application/InvalidPinException.java b/core/src/main/java/com/yubico/yubikit/core/application/InvalidPinException.java index b4f26db8..ba15efdb 100644 --- a/core/src/main/java/com/yubico/yubikit/core/application/InvalidPinException.java +++ b/core/src/main/java/com/yubico/yubikit/core/application/InvalidPinException.java @@ -16,22 +16,20 @@ package com.yubico.yubikit.core.application; -/** - * Thrown when the wrong PIN or PUK is used (or when the PIN or PUK is in a blocked state). - */ +/** Thrown when the wrong PIN or PUK is used (or when the PIN or PUK is in a blocked state). */ public class InvalidPinException extends CommandException { - private final int attemptsRemaining; + private final int attemptsRemaining; - public InvalidPinException(int attemptsRemaining, String message) { - super(message); - this.attemptsRemaining = attemptsRemaining; - } + public InvalidPinException(int attemptsRemaining, String message) { + super(message); + this.attemptsRemaining = attemptsRemaining; + } - public InvalidPinException(int attemptsRemaining) { - this(attemptsRemaining, "Invalid PIN/PUK. Remaining attempts: " + attemptsRemaining); - } + public InvalidPinException(int attemptsRemaining) { + this(attemptsRemaining, "Invalid PIN/PUK. Remaining attempts: " + attemptsRemaining); + } - public int getAttemptsRemaining() { - return attemptsRemaining; - } + public int getAttemptsRemaining() { + return attemptsRemaining; + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/application/SessionVersionOverride.java b/core/src/main/java/com/yubico/yubikit/core/application/SessionVersionOverride.java index f2f21fea..862dc550 100644 --- a/core/src/main/java/com/yubico/yubikit/core/application/SessionVersionOverride.java +++ b/core/src/main/java/com/yubico/yubikit/core/application/SessionVersionOverride.java @@ -17,40 +17,36 @@ package com.yubico.yubikit.core.application; import com.yubico.yubikit.core.Version; - import javax.annotation.Nullable; /** * Adds support for overriding YubiKey session version number. - *

- * Internal use only. + * + *

Internal use only. */ public class SessionVersionOverride { - @Nullable - private static Version versionOverride = null; + @Nullable private static Version versionOverride = null; - /** - * Internal use only. - *

- * Override version of connected YubiKey with the specified version. - * - * @param version version to use instead of YubiKey version. Only applies if the major version - * of the YubiKey is 0. - */ - public static void set(@Nullable Version version) { - versionOverride = version; - } + /** + * Internal use only. + * + *

Override version of connected YubiKey with the specified version. + * + * @param version version to use instead of YubiKey version. Only applies if the major version of + * the YubiKey is 0. + */ + public static void set(@Nullable Version version) { + versionOverride = version; + } - /** - * Returns an applicable override of version. - * - * @param version The version which might be overridden. - * @return Version to use. - */ - static Version overrideOf(Version version) { - return (versionOverride != null && version.major == 0) - ? versionOverride - : version; - } + /** + * Returns an applicable override of version. + * + * @param version The version which might be overridden. + * @return Version to use. + */ + static Version overrideOf(Version version) { + return (versionOverride != null && version.major == 0) ? versionOverride : version; + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/application/TimeoutException.java b/core/src/main/java/com/yubico/yubikit/core/application/TimeoutException.java index 2a80a85a..48f4320f 100755 --- a/core/src/main/java/com/yubico/yubikit/core/application/TimeoutException.java +++ b/core/src/main/java/com/yubico/yubikit/core/application/TimeoutException.java @@ -15,15 +15,13 @@ */ package com.yubico.yubikit.core.application; -/** - * The operation timed out waiting for something. - */ +/** The operation timed out waiting for something. */ public class TimeoutException extends CommandException { - public TimeoutException(String message) { - super(message); - } + public TimeoutException(String message) { + super(message); + } - public TimeoutException(String message, Throwable cause) { - super(message, cause); - } + public TimeoutException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/application/package-info.java b/core/src/main/java/com/yubico/yubikit/core/application/package-info.java index d898a599..25b03f7c 100755 --- a/core/src/main/java/com/yubico/yubikit/core/application/package-info.java +++ b/core/src/main/java/com/yubico/yubikit/core/application/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.core.application; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/core/src/main/java/com/yubico/yubikit/core/fido/CtapException.java b/core/src/main/java/com/yubico/yubikit/core/fido/CtapException.java index ef46c547..5ae1dc0e 100755 --- a/core/src/main/java/com/yubico/yubikit/core/fido/CtapException.java +++ b/core/src/main/java/com/yubico/yubikit/core/fido/CtapException.java @@ -16,83 +16,83 @@ package com.yubico.yubikit.core.fido; import com.yubico.yubikit.core.application.CommandException; - import java.util.Locale; /** * An error on the CTAP-level, returned from the Authenticator. - *

- * These error codes are defined by the - * CTAP2 specification + * + *

These error codes are defined by the CTAP2 + * specification */ public class CtapException extends CommandException { - public static final byte ERR_SUCCESS = 0x00; - public static final byte ERR_INVALID_COMMAND = 0x01; - public static final byte ERR_INVALID_PARAMETER = 0x02; - public static final byte ERR_INVALID_LENGTH = 0x03; - public static final byte ERR_INVALID_SEQ = 0x04; - public static final byte ERR_TIMEOUT = 0x05; - public static final byte ERR_CHANNEL_BUSY = 0x06; - public static final byte ERR_LOCK_REQUIRED = 0x0A; - public static final byte ERR_INVALID_CHANNEL = 0x0B; - public static final byte ERR_CBOR_UNEXPECTED_TYPE = 0x11; - public static final byte ERR_INVALID_CBOR = 0x12; - public static final byte ERR_MISSING_PARAMETER = 0x14; - public static final byte ERR_LIMIT_EXCEEDED = 0x15; - public static final byte ERR_UNSUPPORTED_EXTENSION = 0x16; - public static final byte ERR_FP_DATABASE_FULL = 0x17; - public static final byte ERR_LARGE_BLOB_STORAGE_FULL = 0x18; - public static final byte ERR_CREDENTIAL_EXCLUDED = 0x19; - public static final byte ERR_PROCESSING = 0x21; - public static final byte ERR_INVALID_CREDENTIAL = 0x22; - public static final byte ERR_USER_ACTION_PENDING = 0x23; - public static final byte ERR_OPERATION_PENDING = 0x24; - public static final byte ERR_NO_OPERATIONS = 0x25; - public static final byte ERR_UNSUPPORTED_ALGORITHM = 0x26; - public static final byte ERR_OPERATION_DENIED = 0x27; - public static final byte ERR_KEY_STORE_FULL = 0x28; - public static final byte ERR_NOT_BUSY = 0x29; - public static final byte ERR_NO_OPERATION_PENDING = 0x2A; - public static final byte ERR_UNSUPPORTED_OPTION = 0x2B; - public static final byte ERR_INVALID_OPTION = 0x2C; - public static final byte ERR_KEEPALIVE_CANCEL = 0x2D; - public static final byte ERR_NO_CREDENTIALS = 0x2E; - public static final byte ERR_USER_ACTION_TIMEOUT = 0x2F; - public static final byte ERR_NOT_ALLOWED = 0x30; - public static final byte ERR_PIN_INVALID = 0x31; - public static final byte ERR_PIN_BLOCKED = 0x32; - public static final byte ERR_PIN_AUTH_INVALID = 0x33; - public static final byte ERR_PIN_AUTH_BLOCKED = 0x34; - public static final byte ERR_PIN_NOT_SET = 0x35; - @Deprecated // use ERR_PUAT_REQUIRED - public static final byte ERR_PIN_REQUIRED = 0x36; - public static final byte ERR_PUAT_REQUIRED = 0x36; // CTAP2.1 naming - public static final byte ERR_PIN_POLICY_VIOLATION = 0x37; - public static final byte ERR_PIN_TOKEN_EXPIRED = 0x38; - public static final byte ERR_REQUEST_TOO_LARGE = 0x39; - public static final byte ERR_ACTION_TIMEOUT = 0x3A; - public static final byte ERR_UP_REQUIRED = 0x3B; - public static final byte ERR_UV_BLOCKED = 0x3C; - public static final byte ERR_INTEGRITY_FAILURE = 0x3D; - public static final byte ERR_INVALID_SUBCOMMAND = 0x3E; - public static final byte ERR_UV_INVALID = 0x3F; - public static final byte ERR_UNAUTHORIZED_PERMISSION = 0x40; - public static final byte ERR_OTHER = 0x7F; - public static final byte ERR_SPEC_LAST = (byte) 0xDF; - public static final byte ERR_EXTENSION_FIRST = (byte) 0xE0; - public static final byte ERR_EXTENSION_LAST = (byte) 0xEF; - public static final byte ERR_VENDOR_FIRST = (byte) 0xF0; - public static final byte ERR_VENDOR_LAST = (byte) 0xFF; + public static final byte ERR_SUCCESS = 0x00; + public static final byte ERR_INVALID_COMMAND = 0x01; + public static final byte ERR_INVALID_PARAMETER = 0x02; + public static final byte ERR_INVALID_LENGTH = 0x03; + public static final byte ERR_INVALID_SEQ = 0x04; + public static final byte ERR_TIMEOUT = 0x05; + public static final byte ERR_CHANNEL_BUSY = 0x06; + public static final byte ERR_LOCK_REQUIRED = 0x0A; + public static final byte ERR_INVALID_CHANNEL = 0x0B; + public static final byte ERR_CBOR_UNEXPECTED_TYPE = 0x11; + public static final byte ERR_INVALID_CBOR = 0x12; + public static final byte ERR_MISSING_PARAMETER = 0x14; + public static final byte ERR_LIMIT_EXCEEDED = 0x15; + public static final byte ERR_UNSUPPORTED_EXTENSION = 0x16; + public static final byte ERR_FP_DATABASE_FULL = 0x17; + public static final byte ERR_LARGE_BLOB_STORAGE_FULL = 0x18; + public static final byte ERR_CREDENTIAL_EXCLUDED = 0x19; + public static final byte ERR_PROCESSING = 0x21; + public static final byte ERR_INVALID_CREDENTIAL = 0x22; + public static final byte ERR_USER_ACTION_PENDING = 0x23; + public static final byte ERR_OPERATION_PENDING = 0x24; + public static final byte ERR_NO_OPERATIONS = 0x25; + public static final byte ERR_UNSUPPORTED_ALGORITHM = 0x26; + public static final byte ERR_OPERATION_DENIED = 0x27; + public static final byte ERR_KEY_STORE_FULL = 0x28; + public static final byte ERR_NOT_BUSY = 0x29; + public static final byte ERR_NO_OPERATION_PENDING = 0x2A; + public static final byte ERR_UNSUPPORTED_OPTION = 0x2B; + public static final byte ERR_INVALID_OPTION = 0x2C; + public static final byte ERR_KEEPALIVE_CANCEL = 0x2D; + public static final byte ERR_NO_CREDENTIALS = 0x2E; + public static final byte ERR_USER_ACTION_TIMEOUT = 0x2F; + public static final byte ERR_NOT_ALLOWED = 0x30; + public static final byte ERR_PIN_INVALID = 0x31; + public static final byte ERR_PIN_BLOCKED = 0x32; + public static final byte ERR_PIN_AUTH_INVALID = 0x33; + public static final byte ERR_PIN_AUTH_BLOCKED = 0x34; + public static final byte ERR_PIN_NOT_SET = 0x35; + @Deprecated // use ERR_PUAT_REQUIRED + public static final byte ERR_PIN_REQUIRED = 0x36; + public static final byte ERR_PUAT_REQUIRED = 0x36; // CTAP2.1 naming + public static final byte ERR_PIN_POLICY_VIOLATION = 0x37; + public static final byte ERR_PIN_TOKEN_EXPIRED = 0x38; + public static final byte ERR_REQUEST_TOO_LARGE = 0x39; + public static final byte ERR_ACTION_TIMEOUT = 0x3A; + public static final byte ERR_UP_REQUIRED = 0x3B; + public static final byte ERR_UV_BLOCKED = 0x3C; + public static final byte ERR_INTEGRITY_FAILURE = 0x3D; + public static final byte ERR_INVALID_SUBCOMMAND = 0x3E; + public static final byte ERR_UV_INVALID = 0x3F; + public static final byte ERR_UNAUTHORIZED_PERMISSION = 0x40; + public static final byte ERR_OTHER = 0x7F; + public static final byte ERR_SPEC_LAST = (byte) 0xDF; + public static final byte ERR_EXTENSION_FIRST = (byte) 0xE0; + public static final byte ERR_EXTENSION_LAST = (byte) 0xEF; + public static final byte ERR_VENDOR_FIRST = (byte) 0xF0; + public static final byte ERR_VENDOR_LAST = (byte) 0xFF; - private final byte ctapError; + private final byte ctapError; - public CtapException(byte ctapError) { - super(String.format(Locale.ROOT, "CTAP error: 0x%02x", ctapError)); + public CtapException(byte ctapError) { + super(String.format(Locale.ROOT, "CTAP error: 0x%02x", ctapError)); - this.ctapError = ctapError; - } + this.ctapError = ctapError; + } - public byte getCtapError() { - return ctapError; - } + public byte getCtapError() { + return ctapError; + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/fido/FidoConnection.java b/core/src/main/java/com/yubico/yubikit/core/fido/FidoConnection.java index c2a6a169..937e047c 100755 --- a/core/src/main/java/com/yubico/yubikit/core/fido/FidoConnection.java +++ b/core/src/main/java/com/yubico/yubikit/core/fido/FidoConnection.java @@ -16,22 +16,15 @@ package com.yubico.yubikit.core.fido; import com.yubico.yubikit.core.YubiKeyConnection; - import java.io.IOException; -/** - * A HID CTAP connection to a YubiKey. - */ +/** A HID CTAP connection to a YubiKey. */ public interface FidoConnection extends YubiKeyConnection { - int PACKET_SIZE = 64; + int PACKET_SIZE = 64; - /** - * Sends a HID CTAP packet to the YubiKey. - */ - void send(byte[] packet) throws IOException; + /** Sends a HID CTAP packet to the YubiKey. */ + void send(byte[] packet) throws IOException; - /** - * Receives a HID CTAP packet from the YubiKey. - */ - void receive(byte[] packet) throws IOException; + /** Receives a HID CTAP packet from the YubiKey. */ + void receive(byte[] packet) throws IOException; } diff --git a/core/src/main/java/com/yubico/yubikit/core/fido/FidoProtocol.java b/core/src/main/java/com/yubico/yubikit/core/fido/FidoProtocol.java index f2f6bce5..20aae904 100755 --- a/core/src/main/java/com/yubico/yubikit/core/fido/FidoProtocol.java +++ b/core/src/main/java/com/yubico/yubikit/core/fido/FidoProtocol.java @@ -15,140 +15,142 @@ */ package com.yubico.yubikit.core.fido; -import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.Version; import com.yubico.yubikit.core.application.CommandState; +import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.util.RandomUtils; import com.yubico.yubikit.core.util.StringUtils; - -import org.slf4j.LoggerFactory; - import java.io.Closeable; import java.io.IOException; import java.nio.ByteBuffer; import java.security.MessageDigest; -import java.security.SecureRandom; import java.util.Arrays; - import javax.annotation.Nullable; +import org.slf4j.LoggerFactory; public class FidoProtocol implements Closeable { - private static final byte TYPE_INIT = (byte) 0x80; - - private static final byte CTAPHID_PING = TYPE_INIT | 0x01; - private static final byte CTAPHID_MSG = TYPE_INIT | 0x03; - private static final byte CTAPHID_LOCK = TYPE_INIT | 0x04; - private static final byte CTAPHID_INIT = TYPE_INIT | 0x06; - private static final byte CTAPHID_WINK = TYPE_INIT | 0x08; - private static final byte CTAPHID_CBOR = TYPE_INIT | 0x10; - private static final byte CTAPHID_CANCEL = TYPE_INIT | 0x11; + private static final byte TYPE_INIT = (byte) 0x80; - private static final byte CTAPHID_ERROR = TYPE_INIT | 0x3f; - private static final byte CTAPHID_KEEPALIVE = TYPE_INIT | 0x3b; + private static final byte CTAPHID_PING = TYPE_INIT | 0x01; + private static final byte CTAPHID_MSG = TYPE_INIT | 0x03; + private static final byte CTAPHID_LOCK = TYPE_INIT | 0x04; + private static final byte CTAPHID_INIT = TYPE_INIT | 0x06; + private static final byte CTAPHID_WINK = TYPE_INIT | 0x08; + private static final byte CTAPHID_CBOR = TYPE_INIT | 0x10; + private static final byte CTAPHID_CANCEL = TYPE_INIT | 0x11; - private final CommandState defaultState = new CommandState(); + private static final byte CTAPHID_ERROR = TYPE_INIT | 0x3f; + private static final byte CTAPHID_KEEPALIVE = TYPE_INIT | 0x3b; - private final FidoConnection connection; + private final CommandState defaultState = new CommandState(); - private final Version version; - private int channelId; + private final FidoConnection connection; - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(FidoProtocol.class); + private final Version version; + private int channelId; - public FidoProtocol(FidoConnection connection) throws IOException { - this.connection = connection; + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(FidoProtocol.class); - // init - byte[] nonce = RandomUtils.getRandomBytes(8); + public FidoProtocol(FidoConnection connection) throws IOException { + this.connection = connection; - channelId = 0xffffffff; - ByteBuffer buffer = ByteBuffer.wrap(sendAndReceive(CTAPHID_INIT, nonce, null)); - byte[] responseNonce = new byte[nonce.length]; - buffer.get(responseNonce); - if (!MessageDigest.isEqual(nonce, responseNonce)) { - throw new IOException("Got wrong nonce!"); - } - - channelId = buffer.getInt(); - buffer.get(); // U2F HID version - byte[] versionBytes = new byte[3]; - buffer.get(versionBytes); - version = Version.fromBytes(versionBytes); - buffer.get(); // Capabilities - Logger.debug(logger, "FIDO connection set up with channel ID: {}", String.format("0x%08x", channelId)); - } - - public byte[] sendAndReceive(byte cmd, byte[] payload, @Nullable CommandState state) throws IOException { - state = state != null ? state : defaultState; - - ByteBuffer toSend = ByteBuffer.wrap(payload); - byte[] buffer = new byte[FidoConnection.PACKET_SIZE]; - ByteBuffer packet = ByteBuffer.wrap(buffer); - byte seq = 0; - - // Send request - packet.putInt(channelId).put(cmd).putShort((short) toSend.remaining()); - do { - toSend.get(buffer, packet.position(), Math.min(toSend.remaining(), packet.remaining())); - connection.send(buffer); - Logger.trace(logger, "{} bytes sent over fido: {}", buffer.length, StringUtils.bytesToHex(buffer)); - Arrays.fill(buffer, (byte) 0); - packet.clear(); - packet.putInt(channelId).put((byte) (0x7f & seq++)); - } while (toSend.hasRemaining()); - - // Read response - seq = 0; - ByteBuffer response = null; - do { - packet.clear(); - if (state.waitForCancel(0)) { - Logger.debug(logger, "sending CTAP cancel..."); - Arrays.fill(buffer, (byte) 0); - packet.putInt(channelId).put(CTAPHID_CANCEL); - connection.send(buffer); - Logger.trace(logger, "Sent over fido: {}", StringUtils.bytesToHex(buffer)); - packet.clear(); - } - - connection.receive(buffer); - Logger.trace(logger, "Received over fido: {}", StringUtils.bytesToHex(buffer)); - int responseChannel = packet.getInt(); - if (responseChannel != channelId) { - throw new IOException(String.format("Wrong Channel ID. Expecting: %d, Got: %d", channelId, responseChannel)); - } - if (response == null) { - byte responseCmd = packet.get(); - if (responseCmd == cmd) { - response = ByteBuffer.allocate(packet.getShort()); - } else if (responseCmd == CTAPHID_KEEPALIVE) { - state.onKeepAliveStatus(packet.get()); - continue; - } else if (responseCmd == CTAPHID_ERROR) { - throw new IOException(String.format("CTAPHID error: %02x", packet.get())); - } else { - throw new IOException(String.format("Wrong response command. Expecting: %x, Got: %x", cmd, responseCmd)); - } - } else { - byte responseSeq = packet.get(); - if (responseSeq != seq++) { - throw new IOException(String.format("Wrong sequence number. Expecting %d, Got: %d", seq - 1, responseSeq)); - } - } - response.put(buffer, packet.position(), Math.min(packet.remaining(), response.remaining())); - } while (response == null || response.hasRemaining()); - - return response.array(); - } + // init + byte[] nonce = RandomUtils.getRandomBytes(8); - public Version getVersion() { - return version; + channelId = 0xffffffff; + ByteBuffer buffer = ByteBuffer.wrap(sendAndReceive(CTAPHID_INIT, nonce, null)); + byte[] responseNonce = new byte[nonce.length]; + buffer.get(responseNonce); + if (!MessageDigest.isEqual(nonce, responseNonce)) { + throw new IOException("Got wrong nonce!"); } - @Override - public void close() throws IOException { - connection.close(); - Logger.debug(logger, "fido connection closed"); - } -} \ No newline at end of file + channelId = buffer.getInt(); + buffer.get(); // U2F HID version + byte[] versionBytes = new byte[3]; + buffer.get(versionBytes); + version = Version.fromBytes(versionBytes); + buffer.get(); // Capabilities + Logger.debug( + logger, "FIDO connection set up with channel ID: {}", String.format("0x%08x", channelId)); + } + + public byte[] sendAndReceive(byte cmd, byte[] payload, @Nullable CommandState state) + throws IOException { + state = state != null ? state : defaultState; + + ByteBuffer toSend = ByteBuffer.wrap(payload); + byte[] buffer = new byte[FidoConnection.PACKET_SIZE]; + ByteBuffer packet = ByteBuffer.wrap(buffer); + byte seq = 0; + + // Send request + packet.putInt(channelId).put(cmd).putShort((short) toSend.remaining()); + do { + toSend.get(buffer, packet.position(), Math.min(toSend.remaining(), packet.remaining())); + connection.send(buffer); + Logger.trace( + logger, "{} bytes sent over fido: {}", buffer.length, StringUtils.bytesToHex(buffer)); + Arrays.fill(buffer, (byte) 0); + packet.clear(); + packet.putInt(channelId).put((byte) (0x7f & seq++)); + } while (toSend.hasRemaining()); + + // Read response + seq = 0; + ByteBuffer response = null; + do { + packet.clear(); + if (state.waitForCancel(0)) { + Logger.debug(logger, "sending CTAP cancel..."); + Arrays.fill(buffer, (byte) 0); + packet.putInt(channelId).put(CTAPHID_CANCEL); + connection.send(buffer); + Logger.trace(logger, "Sent over fido: {}", StringUtils.bytesToHex(buffer)); + packet.clear(); + } + + connection.receive(buffer); + Logger.trace(logger, "Received over fido: {}", StringUtils.bytesToHex(buffer)); + int responseChannel = packet.getInt(); + if (responseChannel != channelId) { + throw new IOException( + String.format("Wrong Channel ID. Expecting: %d, Got: %d", channelId, responseChannel)); + } + if (response == null) { + byte responseCmd = packet.get(); + if (responseCmd == cmd) { + response = ByteBuffer.allocate(packet.getShort()); + } else if (responseCmd == CTAPHID_KEEPALIVE) { + state.onKeepAliveStatus(packet.get()); + continue; + } else if (responseCmd == CTAPHID_ERROR) { + throw new IOException(String.format("CTAPHID error: %02x", packet.get())); + } else { + throw new IOException( + String.format("Wrong response command. Expecting: %x, Got: %x", cmd, responseCmd)); + } + } else { + byte responseSeq = packet.get(); + if (responseSeq != seq++) { + throw new IOException( + String.format("Wrong sequence number. Expecting %d, Got: %d", seq - 1, responseSeq)); + } + } + response.put(buffer, packet.position(), Math.min(packet.remaining(), response.remaining())); + } while (response == null || response.hasRemaining()); + + return response.array(); + } + + public Version getVersion() { + return version; + } + + @Override + public void close() throws IOException { + connection.close(); + Logger.debug(logger, "fido connection closed"); + } +} diff --git a/core/src/main/java/com/yubico/yubikit/core/fido/package-info.java b/core/src/main/java/com/yubico/yubikit/core/fido/package-info.java index ecbc9da9..7de7de39 100755 --- a/core/src/main/java/com/yubico/yubikit/core/fido/package-info.java +++ b/core/src/main/java/com/yubico/yubikit/core/fido/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.core.fido; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/core/src/main/java/com/yubico/yubikit/core/internal/Logger.java b/core/src/main/java/com/yubico/yubikit/core/internal/Logger.java index 91ac22a1..4fcebbf1 100644 --- a/core/src/main/java/com/yubico/yubikit/core/internal/Logger.java +++ b/core/src/main/java/com/yubico/yubikit/core/internal/Logger.java @@ -16,226 +16,223 @@ package com.yubico.yubikit.core.internal; +import javax.annotation.Nullable; import org.slf4j.event.Level; import org.slf4j.helpers.FormattingTuple; import org.slf4j.helpers.MessageFormatter; -import javax.annotation.Nullable; - -/** - * Used internally in YubiKit, don't use from applications. - */ +/** Used internally in YubiKit, don't use from applications. */ @SuppressWarnings({"unused", "deprecation"}) public final class Logger { - @Nullable - private static com.yubico.yubikit.core.Logger instance = null; - - public static void setLogger(@Nullable com.yubico.yubikit.core.Logger logger) { - instance = logger; - } - - public static void trace(org.slf4j.Logger logger, String message) { - log(Level.TRACE, logger, message); - } - - public static void trace(org.slf4j.Logger logger, String format, Object arg) { - log(Level.TRACE, logger, format, arg); - } - - public static void trace(org.slf4j.Logger logger, String format, Object arg1, Object arg2) { - log(Level.TRACE, logger, format, arg1, arg2); - } - - public static void trace(org.slf4j.Logger logger, String format, Object... args) { - log(Level.TRACE, logger, format, args); - } - - public static void debug(org.slf4j.Logger logger, String message) { - log(Level.DEBUG, logger, message); - } - - public static void debug(org.slf4j.Logger logger, String format, Object arg) { - log(Level.DEBUG, logger, format, arg); - } - - public static void debug(org.slf4j.Logger logger, String format, Object arg1, Object arg2) { - log(Level.DEBUG, logger, format, arg1, arg2); - } - - public static void debug(org.slf4j.Logger logger, String format, Object... args) { - log(Level.DEBUG, logger, format, args); - } - - public static void info(org.slf4j.Logger logger, String message) { - log(Level.INFO, logger, message); - } - - public static void info(org.slf4j.Logger logger, String format, Object arg) { - log(Level.INFO, logger, format, arg); - } - - public static void info(org.slf4j.Logger logger, String format, Object arg1, Object arg2) { - log(Level.INFO, logger, format, arg1, arg2); - } - - public static void info(org.slf4j.Logger logger, String format, Object... args) { - log(Level.INFO, logger, format, args); - } - - public static void warn(org.slf4j.Logger logger, String message) { - log(Level.WARN, logger, message); - } - - public static void warn(org.slf4j.Logger logger, String format, Object arg) { - log(Level.WARN, logger, format, arg); - } - - public static void warn(org.slf4j.Logger logger, String format, Object arg1, Object arg2) { - log(Level.WARN, logger, format, arg1, arg2); - } - - public static void warn(org.slf4j.Logger logger, String format, Object... args) { - log(Level.WARN, logger, format, args); - } - - public static void error(org.slf4j.Logger logger, String message) { - log(Level.ERROR, logger, message); - } - - public static void error(org.slf4j.Logger logger, String format, Object arg) { - Logger.log(Level.ERROR, logger, format, arg); - } - - public static void error(org.slf4j.Logger logger, String format, Object arg1, Object arg2) { - Logger.log(Level.ERROR, logger, format, arg1, arg2); - } - - public static void error(org.slf4j.Logger logger, String format, Object... args) { - Logger.log(Level.ERROR, logger, format, args); - } - - private static void log(Level level, org.slf4j.Logger logger, String message) { - if (instance != null) { - if (Level.ERROR == level) { - com.yubico.yubikit.core.Logger.e(message, new Exception("Throwable missing in logger.error")); - } else { - com.yubico.yubikit.core.Logger.d(message); - } + @Nullable private static com.yubico.yubikit.core.Logger instance = null; + + public static void setLogger(@Nullable com.yubico.yubikit.core.Logger logger) { + instance = logger; + } + + public static void trace(org.slf4j.Logger logger, String message) { + log(Level.TRACE, logger, message); + } + + public static void trace(org.slf4j.Logger logger, String format, Object arg) { + log(Level.TRACE, logger, format, arg); + } + + public static void trace(org.slf4j.Logger logger, String format, Object arg1, Object arg2) { + log(Level.TRACE, logger, format, arg1, arg2); + } + + public static void trace(org.slf4j.Logger logger, String format, Object... args) { + log(Level.TRACE, logger, format, args); + } + + public static void debug(org.slf4j.Logger logger, String message) { + log(Level.DEBUG, logger, message); + } + + public static void debug(org.slf4j.Logger logger, String format, Object arg) { + log(Level.DEBUG, logger, format, arg); + } + + public static void debug(org.slf4j.Logger logger, String format, Object arg1, Object arg2) { + log(Level.DEBUG, logger, format, arg1, arg2); + } + + public static void debug(org.slf4j.Logger logger, String format, Object... args) { + log(Level.DEBUG, logger, format, args); + } + + public static void info(org.slf4j.Logger logger, String message) { + log(Level.INFO, logger, message); + } + + public static void info(org.slf4j.Logger logger, String format, Object arg) { + log(Level.INFO, logger, format, arg); + } + + public static void info(org.slf4j.Logger logger, String format, Object arg1, Object arg2) { + log(Level.INFO, logger, format, arg1, arg2); + } + + public static void info(org.slf4j.Logger logger, String format, Object... args) { + log(Level.INFO, logger, format, args); + } + + public static void warn(org.slf4j.Logger logger, String message) { + log(Level.WARN, logger, message); + } + + public static void warn(org.slf4j.Logger logger, String format, Object arg) { + log(Level.WARN, logger, format, arg); + } + + public static void warn(org.slf4j.Logger logger, String format, Object arg1, Object arg2) { + log(Level.WARN, logger, format, arg1, arg2); + } + + public static void warn(org.slf4j.Logger logger, String format, Object... args) { + log(Level.WARN, logger, format, args); + } + + public static void error(org.slf4j.Logger logger, String message) { + log(Level.ERROR, logger, message); + } + + public static void error(org.slf4j.Logger logger, String format, Object arg) { + Logger.log(Level.ERROR, logger, format, arg); + } + + public static void error(org.slf4j.Logger logger, String format, Object arg1, Object arg2) { + Logger.log(Level.ERROR, logger, format, arg1, arg2); + } + + public static void error(org.slf4j.Logger logger, String format, Object... args) { + Logger.log(Level.ERROR, logger, format, args); + } + + private static void log(Level level, org.slf4j.Logger logger, String message) { + if (instance != null) { + if (Level.ERROR == level) { + com.yubico.yubikit.core.Logger.e( + message, new Exception("Throwable missing in logger.error")); + } else { + com.yubico.yubikit.core.Logger.d(message); + } + } else { + switch (level) { + case TRACE: + logger.trace(message); + break; + case DEBUG: + logger.debug(message); + break; + case INFO: + logger.info(message); + break; + case WARN: + logger.warn(message); + break; + case ERROR: + logger.error(message); + break; + } + } + } + + private static void log(Level level, org.slf4j.Logger logger, String format, Object arg) { + if (instance != null) { + logToInstance(level, MessageFormatter.format(format, arg)); + } else { + switch (level) { + case TRACE: + logger.trace(format, arg); + break; + case DEBUG: + logger.debug(format, arg); + break; + case INFO: + logger.info(format, arg); + break; + case WARN: + logger.warn(format, arg); + break; + case ERROR: + logger.error(format, arg); + break; + } + } + } + + private static void log( + Level level, org.slf4j.Logger logger, String format, Object arg1, Object arg2) { + if (instance != null) { + logToInstance(level, MessageFormatter.format(format, arg1, arg2)); + } else { + switch (level) { + case TRACE: + logger.trace(format, arg1, arg2); + break; + case DEBUG: + logger.debug(format, arg1, arg2); + break; + case INFO: + logger.info(format, arg1, arg2); + break; + case WARN: + logger.warn(format, arg1, arg2); + break; + case ERROR: + logger.error(format, arg1, arg2); + break; + } + } + } + + private static void log(Level level, org.slf4j.Logger logger, String format, Object... args) { + if (instance != null) { + logToInstance(level, MessageFormatter.arrayFormat(format, args)); + } else { + switch (level) { + case TRACE: + logger.trace(format, args); + break; + case DEBUG: + logger.debug(format, args); + break; + case INFO: + logger.info(format, args); + break; + case WARN: + logger.warn(format, args); + break; + case ERROR: + logger.error(format, args); + break; + } + } + } + + private static void logToInstance(Level level, FormattingTuple formattingTuple) { + if (instance != null) { + + Throwable throwable = formattingTuple.getThrowable(); + String message = formattingTuple.getMessage(); + + if (Level.ERROR == level) { + if (throwable != null) { + com.yubico.yubikit.core.Logger.e(message, throwable); } else { - switch (level) { - case TRACE: - logger.trace(message); - break; - case DEBUG: - logger.debug(message); - break; - case INFO: - logger.info(message); - break; - case WARN: - logger.warn(message); - break; - case ERROR: - logger.error(message); - break; - } + com.yubico.yubikit.core.Logger.e( + message, new Throwable("Throwable missing in logger.error")); } - } - - private static void log(Level level, org.slf4j.Logger logger, String format, Object arg) { - if (instance != null) { - logToInstance(level, MessageFormatter.format(format, arg)); - } else { - switch (level) { - case TRACE: - logger.trace(format, arg); - break; - case DEBUG: - logger.debug(format, arg); - break; - case INFO: - logger.info(format, arg); - break; - case WARN: - logger.warn(format, arg); - break; - case ERROR: - logger.error(format, arg); - break; - } - } - } - - private static void log(Level level, org.slf4j.Logger logger, String format, Object arg1, Object arg2) { - if (instance != null) { - logToInstance(level, MessageFormatter.format(format, arg1, arg2)); + } else { + if (throwable != null) { + com.yubico.yubikit.core.Logger.d(message + " Throwable: " + throwable.getMessage()); } else { - switch (level) { - case TRACE: - logger.trace(format, arg1, arg2); - break; - case DEBUG: - logger.debug(format, arg1, arg2); - break; - case INFO: - logger.info(format, arg1, arg2); - break; - case WARN: - logger.warn(format, arg1, arg2); - break; - case ERROR: - logger.error(format, arg1, arg2); - break; - } + com.yubico.yubikit.core.Logger.d(message); } + } } - - private static void log(Level level, org.slf4j.Logger logger, String format, Object... args) { - if (instance != null) { - logToInstance(level, MessageFormatter.arrayFormat(format, args)); - } else { - switch (level) { - case TRACE: - logger.trace(format, args); - break; - case DEBUG: - logger.debug(format, args); - break; - case INFO: - logger.info(format, args); - break; - case WARN: - logger.warn(format, args); - break; - case ERROR: - logger.error(format, args); - break; - } - } - } - - private static void logToInstance(Level level, FormattingTuple formattingTuple) { - if (instance != null) { - - Throwable throwable = formattingTuple.getThrowable(); - String message = formattingTuple.getMessage(); - - if (Level.ERROR == level) { - if (throwable != null) { - com.yubico.yubikit.core.Logger.e(message, throwable); - } else { - com.yubico.yubikit.core.Logger.e(message, new Throwable("Throwable missing in logger.error")); - } - } else { - if (throwable != null) { - com.yubico.yubikit.core.Logger.d(message + " Throwable: " + throwable.getMessage()); - } else { - com.yubico.yubikit.core.Logger.d(message); - } - } - } - } - - + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/internal/codec/Base64.java b/core/src/main/java/com/yubico/yubikit/core/internal/codec/Base64.java index 58d22719..fa9767c9 100644 --- a/core/src/main/java/com/yubico/yubikit/core/internal/codec/Base64.java +++ b/core/src/main/java/com/yubico/yubikit/core/internal/codec/Base64.java @@ -21,47 +21,49 @@ /** * Loads and provides Base64 implementation - *

- * Only for internal use. + * + *

Only for internal use. */ public class Base64 { - private static final Base64Codec base64Codec; + private static final Base64Codec base64Codec; - static { - ServiceLoader codecLoader = ServiceLoader.load(Base64Codec.class); - final Iterator iterator = codecLoader.iterator(); - base64Codec = iterator.hasNext() ? iterator.next() : new DefaultBase64Codec(); - } + static { + ServiceLoader codecLoader = ServiceLoader.load(Base64Codec.class); + final Iterator iterator = codecLoader.iterator(); + base64Codec = iterator.hasNext() ? iterator.next() : new DefaultBase64Codec(); + } - /** - * Encodes binary data to Base64 URL safe format. - *

- * Internal use only. - * @param data date to encode - * @return Encoded data in Base64 URL safe format - */ - public static String toUrlSafeString(byte[] data) { - return base64Codec.toUrlSafeString(data); - } + /** + * Encodes binary data to Base64 URL safe format. + * + *

Internal use only. + * + * @param data date to encode + * @return Encoded data in Base64 URL safe format + */ + public static String toUrlSafeString(byte[] data) { + return base64Codec.toUrlSafeString(data); + } - /** - * Decodes Base64 URL safe formatted string to binary data. - *

- * Internal use only. - * @param data data to decode in Base64 URL safe format - * @return decoded data - */ - public static byte[] fromUrlSafeString(String data) { - return base64Codec.fromUrlSafeString(data); - } + /** + * Decodes Base64 URL safe formatted string to binary data. + * + *

Internal use only. + * + * @param data data to decode in Base64 URL safe format + * @return decoded data + */ + public static byte[] fromUrlSafeString(String data) { + return base64Codec.fromUrlSafeString(data); + } - /** - * Returns Base64Codec - *

- * Internal use only. - */ - public static Base64Codec getBase64Codec() { - return base64Codec; - } + /** + * Returns Base64Codec + * + *

Internal use only. + */ + public static Base64Codec getBase64Codec() { + return base64Codec; + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/internal/codec/Base64Codec.java b/core/src/main/java/com/yubico/yubikit/core/internal/codec/Base64Codec.java index 895db9cb..3e9a70cc 100644 --- a/core/src/main/java/com/yubico/yubikit/core/internal/codec/Base64Codec.java +++ b/core/src/main/java/com/yubico/yubikit/core/internal/codec/Base64Codec.java @@ -18,33 +18,33 @@ /** * Helper for performing Base64 data conversions. - *

- * Only for internal use. + * + *

Only for internal use. */ public interface Base64Codec { - /** - * @param data binary data - * @return String with no wrapped base64 data without padding - */ - String toString(byte[] data); + /** + * @param data binary data + * @return String with no wrapped base64 data without padding + */ + String toString(byte[] data); - /** - * @param data String with no wrapped base64 content - * @return decoded binary data - */ - byte[] fromString(String data); + /** + * @param data String with no wrapped base64 content + * @return decoded binary data + */ + byte[] fromString(String data); - /** - * @param data binary data - * @return String with no wrapped base64 data without padding, with only safe characters as defined - * in RFC 4648 - */ - String toUrlSafeString(byte[] data); + /** + * @param data binary data + * @return String with no wrapped base64 data without padding, with only safe characters as + * defined in RFC 4648 + */ + String toUrlSafeString(byte[] data); - /** - * @param data String with no wrapped base64 data without padding, with only safe characters as defined - * in RFC 4648 - * @return decoded binary data - */ - byte[] fromUrlSafeString(String data); + /** + * @param data String with no wrapped base64 data without padding, with only safe characters as + * defined in RFC 4648 + * @return decoded binary data + */ + byte[] fromUrlSafeString(String data); } diff --git a/core/src/main/java/com/yubico/yubikit/core/internal/codec/DefaultBase64Codec.java b/core/src/main/java/com/yubico/yubikit/core/internal/codec/DefaultBase64Codec.java index 946932f2..b9a77c9b 100644 --- a/core/src/main/java/com/yubico/yubikit/core/internal/codec/DefaultBase64Codec.java +++ b/core/src/main/java/com/yubico/yubikit/core/internal/codec/DefaultBase64Codec.java @@ -20,26 +20,26 @@ /** * Default implementation of Base64Codec - *

- * Only for internal use. + * + *

Only for internal use. */ public class DefaultBase64Codec implements Base64Codec { - @Override - public String toUrlSafeString(byte[] data) { - return new String(Base64.getUrlEncoder().withoutPadding().encode(data)); - } + @Override + public String toUrlSafeString(byte[] data) { + return new String(Base64.getUrlEncoder().withoutPadding().encode(data)); + } - public String toString(byte[] data) { - return new String(Base64.getEncoder().withoutPadding().encode(data)); - } + public String toString(byte[] data) { + return new String(Base64.getEncoder().withoutPadding().encode(data)); + } - @Override - public byte[] fromUrlSafeString(String data) { - return Base64.getUrlDecoder().decode(data); - } + @Override + public byte[] fromUrlSafeString(String data) { + return Base64.getUrlDecoder().decode(data); + } - public byte[] fromString(String data) { - return Base64.getDecoder().decode(data); - } -} \ No newline at end of file + public byte[] fromString(String data) { + return Base64.getDecoder().decode(data); + } +} diff --git a/core/src/main/java/com/yubico/yubikit/core/internal/codec/package-info.java b/core/src/main/java/com/yubico/yubikit/core/internal/codec/package-info.java index a7f32bda..163f6ae2 100755 --- a/core/src/main/java/com/yubico/yubikit/core/internal/codec/package-info.java +++ b/core/src/main/java/com/yubico/yubikit/core/internal/codec/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.core.internal.codec; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/core/src/main/java/com/yubico/yubikit/core/internal/package-info.java b/core/src/main/java/com/yubico/yubikit/core/internal/package-info.java index be5a6904..1acf0318 100755 --- a/core/src/main/java/com/yubico/yubikit/core/internal/package-info.java +++ b/core/src/main/java/com/yubico/yubikit/core/internal/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.core.internal; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/core/src/main/java/com/yubico/yubikit/core/keys/EllipticCurveValues.java b/core/src/main/java/com/yubico/yubikit/core/keys/EllipticCurveValues.java index e3328bbb..070fa04e 100644 --- a/core/src/main/java/com/yubico/yubikit/core/keys/EllipticCurveValues.java +++ b/core/src/main/java/com/yubico/yubikit/core/keys/EllipticCurveValues.java @@ -17,77 +17,53 @@ package com.yubico.yubikit.core.keys; import com.yubico.yubikit.core.util.StringUtils; - import java.util.Arrays; public enum EllipticCurveValues { - SECP256R1( - 256, - new byte[]{0x2a, (byte) 0x86, 0x48, (byte) 0xce, 0x3d, 0x03, 0x01, 0x07} - ), - SECP256K1( - 256, - new byte[]{0x2b, (byte) 0x81, 0x04, 0x00, 0x0a} - ), - SECP384R1( - 384, - new byte[]{0x2b, (byte) 0x81, 0x04, 0x00, 0x22} - ), - SECP521R1(521, - new byte[]{0x2b, (byte) 0x81, 0x04, 0x00, 0x23} - ), - BrainpoolP256R1( - 256, - new byte[]{0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x07} - ), - BrainpoolP384R1( - 384, - new byte[]{0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0b} - ), - BrainpoolP512R1( - 512, - new byte[]{0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0d} - ), - X25519( - 256, - new byte[]{0x2b, 0x65, 0x6e} - ), - Ed25519( - 256, - new byte[]{0x2b, 0x65, 0x70} - ); + SECP256R1(256, new byte[] {0x2a, (byte) 0x86, 0x48, (byte) 0xce, 0x3d, 0x03, 0x01, 0x07}), + SECP256K1(256, new byte[] {0x2b, (byte) 0x81, 0x04, 0x00, 0x0a}), + SECP384R1(384, new byte[] {0x2b, (byte) 0x81, 0x04, 0x00, 0x22}), + SECP521R1(521, new byte[] {0x2b, (byte) 0x81, 0x04, 0x00, 0x23}), + BrainpoolP256R1(256, new byte[] {0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x07}), + BrainpoolP384R1(384, new byte[] {0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0b}), + BrainpoolP512R1(512, new byte[] {0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0d}), + X25519(256, new byte[] {0x2b, 0x65, 0x6e}), + Ed25519(256, new byte[] {0x2b, 0x65, 0x70}); - private final int bitLength; - private final byte[] oid; + private final int bitLength; + private final byte[] oid; - EllipticCurveValues(int bitLength, byte[] oid) { - this.bitLength = bitLength; - this.oid = oid; - } + EllipticCurveValues(int bitLength, byte[] oid) { + this.bitLength = bitLength; + this.oid = oid; + } - public int getBitLength() { - return bitLength; - } + public int getBitLength() { + return bitLength; + } - byte[] getOid() { - return Arrays.copyOf(oid, oid.length); - } + byte[] getOid() { + return Arrays.copyOf(oid, oid.length); + } - @Override - public String toString() { - return "EllipticCurveValues{" + - "name=" + name() + - ", bitLength=" + bitLength + - ", oid=" + StringUtils.bytesToHex(oid) + - '}'; - } + @Override + public String toString() { + return "EllipticCurveValues{" + + "name=" + + name() + + ", bitLength=" + + bitLength + + ", oid=" + + StringUtils.bytesToHex(oid) + + '}'; + } - public static EllipticCurveValues fromOid(byte[] oid) { - for (EllipticCurveValues match : EllipticCurveValues.values()) { - if (Arrays.equals(oid, match.oid)) { - return match; - } - } - throw new IllegalArgumentException("Not a supported EllipticCurve"); + public static EllipticCurveValues fromOid(byte[] oid) { + for (EllipticCurveValues match : EllipticCurveValues.values()) { + if (Arrays.equals(oid, match.oid)) { + return match; + } } + throw new IllegalArgumentException("Not a supported EllipticCurve"); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/keys/PrivateKeyValues.java b/core/src/main/java/com/yubico/yubikit/core/keys/PrivateKeyValues.java index 318e9b03..1a5b95d0 100644 --- a/core/src/main/java/com/yubico/yubikit/core/keys/PrivateKeyValues.java +++ b/core/src/main/java/com/yubico/yubikit/core/keys/PrivateKeyValues.java @@ -19,7 +19,6 @@ import com.yubico.yubikit.core.application.BadResponseException; import com.yubico.yubikit.core.util.Tlv; import com.yubico.yubikit.core.util.Tlvs; - import java.math.BigInteger; import java.security.PrivateKey; import java.security.interfaces.RSAPrivateCrtKey; @@ -28,251 +27,255 @@ import java.util.Arrays; import java.util.List; import java.util.Map; - import javax.annotation.Nullable; import javax.security.auth.DestroyFailedException; import javax.security.auth.Destroyable; /** * Contains private key values to be imported into a YubiKey. - *

- * Can be created from a {@link PrivateKey} by using {@link #fromPrivateKey(PrivateKey)}. - *

- * Once used, clear the secret keying material by calling {@link #destroy()}. + * + *

Can be created from a {@link PrivateKey} by using {@link #fromPrivateKey(PrivateKey)}. + * + *

Once used, clear the secret keying material by calling {@link #destroy()}. */ public abstract class PrivateKeyValues implements Destroyable { - private static final byte[] OID_ECDSA = new byte[]{0x2a, (byte) 0x86, 0x48, (byte) 0xce, 0x3d, 0x02, 0x01}; - final int bitLength; - private boolean destroyed = false; + private static final byte[] OID_ECDSA = + new byte[] {0x2a, (byte) 0x86, 0x48, (byte) 0xce, 0x3d, 0x02, 0x01}; + final int bitLength; + private boolean destroyed = false; + + protected PrivateKeyValues(int bitLength) { + this.bitLength = bitLength; + } + + public final int getBitLength() { + return bitLength; + } + + @Override + public final boolean isDestroyed() { + return destroyed; + } + + @Override + public void destroy() throws DestroyFailedException { + destroyed = true; + } + + /** + * Constructs a PrivateKeyValues instance using values from a JCA {@link PrivateKey}. + * + * @param privateKey the private key to extract values from + * @return private key values + */ + public static PrivateKeyValues fromPrivateKey(PrivateKey privateKey) { + if (privateKey instanceof RSAPrivateKey) { + return Rsa.fromRsaPrivateKey((RSAPrivateKey) privateKey); + } else { + byte[] encoded = privateKey.getEncoded(); + try { + Map tlvs = Tlvs.decodeMap(Tlvs.unpackValue(0x30, encoded)); + List sequence = Tlvs.decodeList(tlvs.get(0x30)); + byte[] algorithm = sequence.get(0).getValue(); + if (Arrays.equals(OID_ECDSA, algorithm)) { + byte[] parameter = sequence.get(1).getValue(); + EllipticCurveValues curve = EllipticCurveValues.fromOid(parameter); + sequence = Tlvs.decodeList(Tlvs.unpackValue(0x30, tlvs.get(0x04))); + return new Ec(curve, sequence.get(1).getValue()); + } else { + for (EllipticCurveValues curve : + Arrays.asList(EllipticCurveValues.Ed25519, EllipticCurveValues.X25519)) { + if (Arrays.equals(curve.getOid(), algorithm)) { + return new Ec(curve, Tlvs.unpackValue(0x04, tlvs.get(0x04))); + } + } + } + } catch (BadResponseException e) { + // ignore, fall through to exception + } + } - protected PrivateKeyValues(int bitLength) { - this.bitLength = bitLength; + throw new IllegalArgumentException("Unsupported private key type"); + } + + public static class Ec extends PrivateKeyValues { + private final EllipticCurveValues ellipticCurveValues; + private final byte[] secret; + + protected Ec(EllipticCurveValues ellipticCurveValues, byte[] secret) { + super(ellipticCurveValues.getBitLength()); + this.ellipticCurveValues = ellipticCurveValues; + this.secret = Arrays.copyOf(secret, secret.length); } - public final int getBitLength() { - return bitLength; + public EllipticCurveValues getCurveParams() { + return ellipticCurveValues; } - @Override - public final boolean isDestroyed() { - return destroyed; + public byte[] getSecret() { + return Arrays.copyOf(secret, secret.length); } @Override public void destroy() throws DestroyFailedException { - destroyed = true; + Arrays.fill(secret, (byte) 0); + super.destroy(); } - /** - * Constructs a PrivateKeyValues instance using values from a JCA {@link PrivateKey}. - * - * @param privateKey the private key to extract values from - * @return private key values - */ - public static PrivateKeyValues fromPrivateKey(PrivateKey privateKey) { - if (privateKey instanceof RSAPrivateKey) { - return Rsa.fromRsaPrivateKey((RSAPrivateKey) privateKey); - } else { - byte[] encoded = privateKey.getEncoded(); - try { - Map tlvs = Tlvs.decodeMap(Tlvs.unpackValue(0x30, encoded)); - List sequence = Tlvs.decodeList(tlvs.get(0x30)); - byte[] algorithm = sequence.get(0).getValue(); - if (Arrays.equals(OID_ECDSA, algorithm)) { - byte[] parameter = sequence.get(1).getValue(); - EllipticCurveValues curve = EllipticCurveValues.fromOid(parameter); - sequence = Tlvs.decodeList(Tlvs.unpackValue(0x30, tlvs.get(0x04))); - return new Ec(curve, sequence.get(1).getValue()); - } else { - for (EllipticCurveValues curve : Arrays.asList(EllipticCurveValues.Ed25519, EllipticCurveValues.X25519)) { - if (Arrays.equals(curve.getOid(), algorithm)) { - return new Ec(curve, Tlvs.unpackValue(0x04, tlvs.get(0x04))); - } - } - } - } catch (BadResponseException e) { - // ignore, fall through to exception - } - } - - throw new IllegalArgumentException("Unsupported private key type"); + @Override + public String toString() { + return "PrivateKeyValues.Ec{" + + "curve=" + + ellipticCurveValues.name() + + ", bitLength=" + + bitLength + + ", destroyed=" + + isDestroyed() + + '}'; } + } - public static class Ec extends PrivateKeyValues { - private final EllipticCurveValues ellipticCurveValues; - private final byte[] secret; - - protected Ec(EllipticCurveValues ellipticCurveValues, byte[] secret) { - super(ellipticCurveValues.getBitLength()); - this.ellipticCurveValues = ellipticCurveValues; - this.secret = Arrays.copyOf(secret, secret.length); - } - - public EllipticCurveValues getCurveParams() { - return ellipticCurveValues; - } - - public byte[] getSecret() { - return Arrays.copyOf(secret, secret.length); - } - - @Override - public void destroy() throws DestroyFailedException { - Arrays.fill(secret, (byte) 0); - super.destroy(); - } + public static class Rsa extends PrivateKeyValues { + private final BigInteger modulus; + private final BigInteger publicExponent; + private BigInteger primeP; + private BigInteger primeQ; + @Nullable private BigInteger primeExponentP; + @Nullable private BigInteger primeExponentQ; + @Nullable private BigInteger crtCoefficient; - @Override - public String toString() { - return "PrivateKeyValues.Ec{" + - "curve=" + ellipticCurveValues.name() + - ", bitLength=" + bitLength + - ", destroyed=" + isDestroyed() + - '}'; - } + @Override + public String toString() { + boolean hasCrt = crtCoefficient != null; + return "PrivateKeyValues.Rsa{" + + "modulus=" + + modulus + + ", publicExponent=" + + publicExponent + + ", bitLength=" + + bitLength + + ", hasCrtValues=" + + hasCrt + + ", destroyed=" + + isDestroyed() + + '}'; } - public static class Rsa extends PrivateKeyValues { - private final BigInteger modulus; - private final BigInteger publicExponent; - private BigInteger primeP; - private BigInteger primeQ; - @Nullable - private BigInteger primeExponentP; - @Nullable - private BigInteger primeExponentQ; - @Nullable - private BigInteger crtCoefficient; - - @Override - public String toString() { - boolean hasCrt = crtCoefficient != null; - return "PrivateKeyValues.Rsa{" + - "modulus=" + modulus + - ", publicExponent=" + publicExponent + - ", bitLength=" + bitLength + - ", hasCrtValues=" + hasCrt + - ", destroyed=" + isDestroyed() + - '}'; - } - - protected Rsa(BigInteger modulus, BigInteger publicExponent, BigInteger primeP, BigInteger primeQ, @Nullable BigInteger primeExponentP, @Nullable BigInteger primeExponentQ, @Nullable BigInteger crtCoefficient) { - super(modulus.bitLength()); - this.modulus = modulus; - this.publicExponent = publicExponent; - this.primeP = primeP; - this.primeQ = primeQ; - this.primeExponentP = primeExponentP; - this.primeExponentQ = primeExponentQ; - this.crtCoefficient = crtCoefficient; - - if (!( - (primeExponentP != null && primeExponentQ != null && crtCoefficient != null) - || (primeExponentP == null && primeExponentQ == null && crtCoefficient == null) - )) { - throw new IllegalArgumentException("All CRT values must either be present or omitted"); - } - } + protected Rsa( + BigInteger modulus, + BigInteger publicExponent, + BigInteger primeP, + BigInteger primeQ, + @Nullable BigInteger primeExponentP, + @Nullable BigInteger primeExponentQ, + @Nullable BigInteger crtCoefficient) { + super(modulus.bitLength()); + this.modulus = modulus; + this.publicExponent = publicExponent; + this.primeP = primeP; + this.primeQ = primeQ; + this.primeExponentP = primeExponentP; + this.primeExponentQ = primeExponentQ; + this.crtCoefficient = crtCoefficient; + + if (!((primeExponentP != null && primeExponentQ != null && crtCoefficient != null) + || (primeExponentP == null && primeExponentQ == null && crtCoefficient == null))) { + throw new IllegalArgumentException("All CRT values must either be present or omitted"); + } + } - public BigInteger getModulus() { - return modulus; - } + public BigInteger getModulus() { + return modulus; + } - public BigInteger getPublicExponent() { - return publicExponent; - } + public BigInteger getPublicExponent() { + return publicExponent; + } - public BigInteger getPrimeP() { - return primeP; - } + public BigInteger getPrimeP() { + return primeP; + } - public BigInteger getPrimeQ() { - return primeQ; - } + public BigInteger getPrimeQ() { + return primeQ; + } - @Nullable - public BigInteger getPrimeExponentP() { - return primeExponentP; - } + @Nullable public BigInteger getPrimeExponentP() { + return primeExponentP; + } - @Nullable - public BigInteger getPrimeExponentQ() { - return primeExponentQ; - } + @Nullable public BigInteger getPrimeExponentQ() { + return primeExponentQ; + } - @Nullable - public BigInteger getCrtCoefficient() { - return crtCoefficient; - } + @Nullable public BigInteger getCrtCoefficient() { + return crtCoefficient; + } - @Override - public void destroy() throws DestroyFailedException { - primeP = BigInteger.ZERO; - primeQ = BigInteger.ZERO; - primeExponentP = null; - primeExponentQ = null; - crtCoefficient = null; - super.destroy(); - } + @Override + public void destroy() throws DestroyFailedException { + primeP = BigInteger.ZERO; + primeQ = BigInteger.ZERO; + primeExponentP = null; + primeExponentQ = null; + crtCoefficient = null; + super.destroy(); + } - private static Rsa fromRsaPrivateKey(RSAPrivateKey key) { - List values; - if (key instanceof RSAPrivateCrtKey) { - RSAPrivateCrtKey rsaPrivateKey = (RSAPrivateCrtKey) key; - values = Arrays.asList( - rsaPrivateKey.getModulus(), - rsaPrivateKey.getPublicExponent(), - rsaPrivateKey.getPrivateExponent(), - rsaPrivateKey.getPrimeP(), - rsaPrivateKey.getPrimeQ(), - rsaPrivateKey.getPrimeExponentP(), - rsaPrivateKey.getPrimeExponentQ(), - rsaPrivateKey.getCrtCoefficient() - ); - } else if ("PKCS#8".equals(key.getFormat())) { - values = parsePkcs8RsaKeyValues(key.getEncoded()); - } else { - throw new IllegalArgumentException("Unsupported private key encoding"); - } - if (values.get(1).intValue() != 65537) { - throw new IllegalArgumentException("Unsupported RSA public exponent"); - } + private static Rsa fromRsaPrivateKey(RSAPrivateKey key) { + List values; + if (key instanceof RSAPrivateCrtKey) { + RSAPrivateCrtKey rsaPrivateKey = (RSAPrivateCrtKey) key; + values = + Arrays.asList( + rsaPrivateKey.getModulus(), + rsaPrivateKey.getPublicExponent(), + rsaPrivateKey.getPrivateExponent(), + rsaPrivateKey.getPrimeP(), + rsaPrivateKey.getPrimeQ(), + rsaPrivateKey.getPrimeExponentP(), + rsaPrivateKey.getPrimeExponentQ(), + rsaPrivateKey.getCrtCoefficient()); + } else if ("PKCS#8".equals(key.getFormat())) { + values = parsePkcs8RsaKeyValues(key.getEncoded()); + } else { + throw new IllegalArgumentException("Unsupported private key encoding"); + } + if (values.get(1).intValue() != 65537) { + throw new IllegalArgumentException("Unsupported RSA public exponent"); + } + + return new Rsa( + values.get(0), // n + values.get(1), // e + values.get(3), // p + values.get(4), // q + values.get(5), // dmp1 + values.get(6), // dmq1 + values.get(7) // iqmp + ); + } - return new Rsa( - values.get(0), // n - values.get(1), // e - values.get(3), // p - values.get(4), // q - values.get(5), // dmp1 - values.get(6), // dmq1 - values.get(7) // iqmp - ); + /* + Parse a DER encoded PKCS#8 RSA key + */ + static List parsePkcs8RsaKeyValues(byte[] derKey) { + try { + List numbers = + Tlvs.decodeList( + Tlvs.decodeMap(Tlvs.decodeMap(Tlvs.unpackValue(0x30, derKey)).get(0x04)).get(0x30)); + List values = new ArrayList<>(); + for (Tlv number : numbers) { + values.add(new BigInteger(number.getValue())); } - - /* - Parse a DER encoded PKCS#8 RSA key - */ - static List parsePkcs8RsaKeyValues(byte[] derKey) { - try { - List numbers = Tlvs.decodeList( - Tlvs.decodeMap( - Tlvs.decodeMap( - Tlvs.unpackValue(0x30, derKey) - ).get(0x04) - ).get(0x30) - ); - List values = new ArrayList<>(); - for (Tlv number : numbers) { - values.add(new BigInteger(number.getValue())); - } - BigInteger first = values.remove(0); - if (first.intValue() != 0) { - throw new IllegalArgumentException("Expected value 0"); - } - return values; - } catch (BadResponseException e) { - throw new IllegalArgumentException(e.getMessage()); - } + BigInteger first = values.remove(0); + if (first.intValue() != 0) { + throw new IllegalArgumentException("Expected value 0"); } + return values; + } catch (BadResponseException e) { + throw new IllegalArgumentException(e.getMessage()); + } } + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/keys/PublicKeyValues.java b/core/src/main/java/com/yubico/yubikit/core/keys/PublicKeyValues.java index 733e32c0..f8df30ad 100644 --- a/core/src/main/java/com/yubico/yubikit/core/keys/PublicKeyValues.java +++ b/core/src/main/java/com/yubico/yubikit/core/keys/PublicKeyValues.java @@ -22,7 +22,6 @@ import com.yubico.yubikit.core.util.StringUtils; import com.yubico.yubikit.core.util.Tlv; import com.yubico.yubikit.core.util.Tlvs; - import java.math.BigInteger; import java.nio.ByteBuffer; import java.security.KeyFactory; @@ -37,238 +36,278 @@ import java.util.List; import java.util.Map; -/** - * Values defining a public key, such as an RSA or EC key. - */ +/** Values defining a public key, such as an RSA or EC key. */ public abstract class PublicKeyValues { - private static final byte[] OID_ECDSA = new byte[]{0x2a, (byte) 0x86, 0x48, (byte) 0xce, 0x3d, 0x02, 0x01}; - private static final byte[] OID_RSA_ENCRYPTION = new byte[]{0x2a, (byte) 0x86, 0x48, (byte) 0x86, (byte) 0xf7, 0x0d, 0x01, 0x01, 0x01}; - - protected final int bitLength; - - protected PublicKeyValues(int bitLength) { - this.bitLength = bitLength; + private static final byte[] OID_ECDSA = + new byte[] {0x2a, (byte) 0x86, 0x48, (byte) 0xce, 0x3d, 0x02, 0x01}; + private static final byte[] OID_RSA_ENCRYPTION = + new byte[] {0x2a, (byte) 0x86, 0x48, (byte) 0x86, (byte) 0xf7, 0x0d, 0x01, 0x01, 0x01}; + + protected final int bitLength; + + protected PublicKeyValues(int bitLength) { + this.bitLength = bitLength; + } + + public final int getBitLength() { + return bitLength; + } + + public abstract byte[] getEncoded(); + + /** + * Instantiates a JCA PublicKey using the contained parameters. + * + *

This requires a SecurityProvider capable of handling the key type. + * + * @return a public key, usable for cryptographic operations + * @throws NoSuchAlgorithmException if no Provider supports an implementation for the specified + * algorithm. + * @throws InvalidKeySpecException if the given key specification is inappropriate for this key + * factory to produce a public key. + */ + public abstract PublicKey toPublicKey() throws NoSuchAlgorithmException, InvalidKeySpecException; + + public static PublicKeyValues fromPublicKey(PublicKey publicKey) { + if (publicKey instanceof RSAPublicKey) { + return new Rsa( + ((RSAPublicKey) publicKey).getModulus(), ((RSAPublicKey) publicKey).getPublicExponent()); } - - public final int getBitLength() { - return bitLength; - } - - public abstract byte[] getEncoded(); - - /** - * Instantiates a JCA PublicKey using the contained parameters. - * This requires a SecurityProvider capable of handling the key type. - * - * @return a public key, usable for cryptographic operations - * @throws NoSuchAlgorithmException if no Provider supports an implementation for the specified algorithm. - * @throws InvalidKeySpecException if the given key specification is inappropriate for this key factory to produce a public key. - */ - public abstract PublicKey toPublicKey() throws NoSuchAlgorithmException, InvalidKeySpecException; - - public static PublicKeyValues fromPublicKey(PublicKey publicKey) { - if (publicKey instanceof RSAPublicKey) { - return new Rsa(((RSAPublicKey) publicKey).getModulus(), ((RSAPublicKey) publicKey).getPublicExponent()); + byte[] encoded = publicKey.getEncoded(); + try { + Map tlvs = Tlvs.decodeMap(Tlvs.unpackValue(0x30, encoded)); + List sequence = Tlvs.decodeList(tlvs.get(0x30)); + byte[] algorithm = sequence.get(0).getValue(); + byte[] bitString = tlvs.get(0x03); + byte[] encodedKey = Arrays.copyOfRange(bitString, 1, bitString.length); + if (Arrays.equals(OID_ECDSA, algorithm)) { + byte[] parameter = sequence.get(1).getValue(); + EllipticCurveValues curve = EllipticCurveValues.fromOid(parameter); + return Ec.fromEncodedPoint(curve, encodedKey); + } else { + for (EllipticCurveValues curve : + Arrays.asList(EllipticCurveValues.Ed25519, EllipticCurveValues.X25519)) { + if (Arrays.equals(curve.getOid(), algorithm)) { + return new Cv25519(curve, encodedKey); + } } - byte[] encoded = publicKey.getEncoded(); - try { - Map tlvs = Tlvs.decodeMap(Tlvs.unpackValue(0x30, encoded)); - List sequence = Tlvs.decodeList(tlvs.get(0x30)); - byte[] algorithm = sequence.get(0).getValue(); - byte[] bitString = tlvs.get(0x03); - byte[] encodedKey = Arrays.copyOfRange(bitString, 1, bitString.length); - if (Arrays.equals(OID_ECDSA, algorithm)) { - byte[] parameter = sequence.get(1).getValue(); - EllipticCurveValues curve = EllipticCurveValues.fromOid(parameter); - return Ec.fromEncodedPoint(curve, encodedKey); - } else { - for (EllipticCurveValues curve : Arrays.asList(EllipticCurveValues.Ed25519, EllipticCurveValues.X25519)) { - if (Arrays.equals(curve.getOid(), algorithm)) { - return new Cv25519(curve, encodedKey); - } - } - } - } catch (BadResponseException e) { - throw new RuntimeException(e); - } - - throw new IllegalStateException(); + } + } catch (BadResponseException e) { + throw new RuntimeException(e); } - public static class Cv25519 extends PublicKeyValues { - private final EllipticCurveValues ellipticCurveValues; - private final byte[] bytes; - - public Cv25519(EllipticCurveValues ellipticCurveValues, byte[] bytes) { - super(ellipticCurveValues.getBitLength()); - if (!(ellipticCurveValues == EllipticCurveValues.Ed25519 || ellipticCurveValues == EllipticCurveValues.X25519)) { - throw new IllegalArgumentException("InvalidCurve"); - } - this.ellipticCurveValues = ellipticCurveValues; - this.bytes = Arrays.copyOf(bytes, bytes.length); - } - - public EllipticCurveValues getCurveParams() { - return ellipticCurveValues; - } + throw new IllegalStateException(); + } + + public static class Cv25519 extends PublicKeyValues { + private final EllipticCurveValues ellipticCurveValues; + private final byte[] bytes; + + public Cv25519(EllipticCurveValues ellipticCurveValues, byte[] bytes) { + super(ellipticCurveValues.getBitLength()); + if (!(ellipticCurveValues == EllipticCurveValues.Ed25519 + || ellipticCurveValues == EllipticCurveValues.X25519)) { + throw new IllegalArgumentException("InvalidCurve"); + } + this.ellipticCurveValues = ellipticCurveValues; + this.bytes = Arrays.copyOf(bytes, bytes.length); + } - public byte[] getBytes() { - return Arrays.copyOf(bytes, bytes.length); - } + public EllipticCurveValues getCurveParams() { + return ellipticCurveValues; + } - @Override - public byte[] getEncoded() { - return new Tlv(0x30, Tlvs.encodeList(Arrays.asList( - new Tlv(0x30, new Tlv(0x06, ellipticCurveValues.getOid()).getBytes()), - new Tlv(0x03, ByteBuffer.allocate(1 + bytes.length).put((byte) 0).put(bytes).array()) - ))).getBytes(); - } + public byte[] getBytes() { + return Arrays.copyOf(bytes, bytes.length); + } - @Override - public PublicKey toPublicKey() throws NoSuchAlgorithmException, InvalidKeySpecException { - KeyFactory keyFactory = KeyFactory.getInstance(ellipticCurveValues.name()); - return keyFactory.generatePublic(new X509EncodedKeySpec(getEncoded())); - } + @Override + public byte[] getEncoded() { + return new Tlv( + 0x30, + Tlvs.encodeList( + Arrays.asList( + new Tlv(0x30, new Tlv(0x06, ellipticCurveValues.getOid()).getBytes()), + new Tlv( + 0x03, + ByteBuffer.allocate(1 + bytes.length).put((byte) 0).put(bytes).array())))) + .getBytes(); + } - @Override - public String toString() { - return "PublicKeyValues.Cv25519{" + - "curve=" + ellipticCurveValues.name() + - ", publicKey=" + StringUtils.bytesToHex(bytes) + - ", bitLength=" + bitLength + - '}'; - } + @Override + public PublicKey toPublicKey() throws NoSuchAlgorithmException, InvalidKeySpecException { + KeyFactory keyFactory = KeyFactory.getInstance(ellipticCurveValues.name()); + return keyFactory.generatePublic(new X509EncodedKeySpec(getEncoded())); } - public static class Ec extends PublicKeyValues { - private final EllipticCurveValues ellipticCurveValues; - private final BigInteger x; - private final BigInteger y; - - public Ec(EllipticCurveValues ellipticCurveValues, BigInteger x, BigInteger y) { - super(ellipticCurveValues.getBitLength()); - if (ellipticCurveValues == EllipticCurveValues.Ed25519 || ellipticCurveValues == EllipticCurveValues.X25519) { - throw new IllegalArgumentException("InvalidCurve"); - } - this.ellipticCurveValues = ellipticCurveValues; - this.x = x; - this.y = y; - } + @Override + public String toString() { + return "PublicKeyValues.Cv25519{" + + "curve=" + + ellipticCurveValues.name() + + ", publicKey=" + + StringUtils.bytesToHex(bytes) + + ", bitLength=" + + bitLength + + '}'; + } + } + + public static class Ec extends PublicKeyValues { + private final EllipticCurveValues ellipticCurveValues; + private final BigInteger x; + private final BigInteger y; + + public Ec(EllipticCurveValues ellipticCurveValues, BigInteger x, BigInteger y) { + super(ellipticCurveValues.getBitLength()); + if (ellipticCurveValues == EllipticCurveValues.Ed25519 + || ellipticCurveValues == EllipticCurveValues.X25519) { + throw new IllegalArgumentException("InvalidCurve"); + } + this.ellipticCurveValues = ellipticCurveValues; + this.x = x; + this.y = y; + } - public EllipticCurveValues getCurveParams() { - return ellipticCurveValues; - } + public EllipticCurveValues getCurveParams() { + return ellipticCurveValues; + } - public BigInteger getX() { - return x; - } + public BigInteger getX() { + return x; + } - public BigInteger getY() { - return y; - } + public BigInteger getY() { + return y; + } - public byte[] getEncodedPoint() { - int coordSize = (int) Math.ceil(ellipticCurveValues.getBitLength() / 8.0); - return ByteBuffer.allocate(1 + 2 * coordSize) - .put((byte) 0x04) - .put(intToLength(x, coordSize)) - .put(intToLength(y, coordSize)) - .array(); - } + public byte[] getEncodedPoint() { + int coordSize = (int) Math.ceil(ellipticCurveValues.getBitLength() / 8.0); + return ByteBuffer.allocate(1 + 2 * coordSize) + .put((byte) 0x04) + .put(intToLength(x, coordSize)) + .put(intToLength(y, coordSize)) + .array(); + } - @Override - public byte[] getEncoded() { - byte[] encodedPoint = getEncodedPoint(); - return new Tlv(0x30, Tlvs.encodeList(Arrays.asList( - new Tlv(0x30, Tlvs.encodeList(Arrays.asList( - new Tlv(0x06, OID_ECDSA), - new Tlv(0x06, ellipticCurveValues.getOid()) - ))), - new Tlv(0x03, ByteBuffer.allocate(1 + encodedPoint.length).put((byte) 0).put(encodedPoint).array()) - ))).getBytes(); - } + @Override + public byte[] getEncoded() { + byte[] encodedPoint = getEncodedPoint(); + return new Tlv( + 0x30, + Tlvs.encodeList( + Arrays.asList( + new Tlv( + 0x30, + Tlvs.encodeList( + Arrays.asList( + new Tlv(0x06, OID_ECDSA), + new Tlv(0x06, ellipticCurveValues.getOid())))), + new Tlv( + 0x03, + ByteBuffer.allocate(1 + encodedPoint.length) + .put((byte) 0) + .put(encodedPoint) + .array())))) + .getBytes(); + } - @Override - public ECPublicKey toPublicKey() throws NoSuchAlgorithmException, InvalidKeySpecException { - KeyFactory keyFactory = KeyFactory.getInstance("EC"); - return (ECPublicKey) keyFactory.generatePublic(new X509EncodedKeySpec(getEncoded())); - } + @Override + public ECPublicKey toPublicKey() throws NoSuchAlgorithmException, InvalidKeySpecException { + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return (ECPublicKey) keyFactory.generatePublic(new X509EncodedKeySpec(getEncoded())); + } - @Override - public String toString() { - return "PublicKeyValues.Ec{" + - "curve=" + ellipticCurveValues.name() + - ", x=" + x + - ", y=" + y + - ", bitLength=" + bitLength + - '}'; - } + @Override + public String toString() { + return "PublicKeyValues.Ec{" + + "curve=" + + ellipticCurveValues.name() + + ", x=" + + x + + ", y=" + + y + + ", bitLength=" + + bitLength + + '}'; + } - public static Ec fromEncodedPoint(EllipticCurveValues curve, byte[] encoded) { - ByteBuffer buf = ByteBuffer.wrap(encoded); - if (buf.get() != 0x04) { - throw new IllegalArgumentException("Only uncompressed public keys are supported"); - } - byte[] coordBuf = new byte[(encoded.length - 1) / 2]; - buf.get(coordBuf); - BigInteger x = new BigInteger(1, coordBuf); - buf.get(coordBuf); - BigInteger y = new BigInteger(1, coordBuf); - return new Ec(curve, x, y); - } + public static Ec fromEncodedPoint(EllipticCurveValues curve, byte[] encoded) { + ByteBuffer buf = ByteBuffer.wrap(encoded); + if (buf.get() != 0x04) { + throw new IllegalArgumentException("Only uncompressed public keys are supported"); + } + byte[] coordBuf = new byte[(encoded.length - 1) / 2]; + buf.get(coordBuf); + BigInteger x = new BigInteger(1, coordBuf); + buf.get(coordBuf); + BigInteger y = new BigInteger(1, coordBuf); + return new Ec(curve, x, y); } + } - public static class Rsa extends PublicKeyValues { - private final BigInteger modulus; - private final BigInteger publicExponent; + public static class Rsa extends PublicKeyValues { + private final BigInteger modulus; + private final BigInteger publicExponent; - public Rsa(BigInteger modulus, BigInteger publicExponent) { - super(modulus.bitLength()); - this.modulus = modulus; - this.publicExponent = publicExponent; - } + public Rsa(BigInteger modulus, BigInteger publicExponent) { + super(modulus.bitLength()); + this.modulus = modulus; + this.publicExponent = publicExponent; + } - public BigInteger getModulus() { - return modulus; - } + public BigInteger getModulus() { + return modulus; + } - public BigInteger getPublicExponent() { - return publicExponent; - } + public BigInteger getPublicExponent() { + return publicExponent; + } - @Override - public byte[] getEncoded() { - byte[] bitstring = new Tlv(0x30, Tlvs.encodeList(Arrays.asList( - new Tlv(0x02, modulus.toByteArray()), - new Tlv(0x02, publicExponent.toByteArray()) - ))).getBytes(); - return new Tlv(0x30, Tlvs.encodeList(Arrays.asList( - new Tlv(0x30, Tlvs.encodeList(Arrays.asList( - new Tlv(0x06, OID_RSA_ENCRYPTION), - new Tlv(0x05, new byte[0]) - ))), - new Tlv(0x03, ByteBuffer - .allocate(1 + bitstring.length) - .put((byte) 0) - .put(bitstring) - .array() - ) - ))).getBytes(); - } + @Override + public byte[] getEncoded() { + byte[] bitstring = + new Tlv( + 0x30, + Tlvs.encodeList( + Arrays.asList( + new Tlv(0x02, modulus.toByteArray()), + new Tlv(0x02, publicExponent.toByteArray())))) + .getBytes(); + return new Tlv( + 0x30, + Tlvs.encodeList( + Arrays.asList( + new Tlv( + 0x30, + Tlvs.encodeList( + Arrays.asList( + new Tlv(0x06, OID_RSA_ENCRYPTION), new Tlv(0x05, new byte[0])))), + new Tlv( + 0x03, + ByteBuffer.allocate(1 + bitstring.length) + .put((byte) 0) + .put(bitstring) + .array())))) + .getBytes(); + } - @Override - public RSAPublicKey toPublicKey() throws NoSuchAlgorithmException, InvalidKeySpecException { - KeyFactory factory = KeyFactory.getInstance("RSA"); - return (RSAPublicKey) factory.generatePublic(new RSAPublicKeySpec(modulus, publicExponent)); - } + @Override + public RSAPublicKey toPublicKey() throws NoSuchAlgorithmException, InvalidKeySpecException { + KeyFactory factory = KeyFactory.getInstance("RSA"); + return (RSAPublicKey) factory.generatePublic(new RSAPublicKeySpec(modulus, publicExponent)); + } - @Override - public String toString() { - return "PublicKeyValues.Rsa{" + - "modulus=" + modulus + - ", publicExponent=" + publicExponent + - ", bitLength=" + bitLength + - '}'; - } + @Override + public String toString() { + return "PublicKeyValues.Rsa{" + + "modulus=" + + modulus + + ", publicExponent=" + + publicExponent + + ", bitLength=" + + bitLength + + '}'; } + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/keys/package-info.java b/core/src/main/java/com/yubico/yubikit/core/keys/package-info.java index 6f7215c1..2113becc 100644 --- a/core/src/main/java/com/yubico/yubikit/core/keys/package-info.java +++ b/core/src/main/java/com/yubico/yubikit/core/keys/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.core.keys; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/core/src/main/java/com/yubico/yubikit/core/otp/ChecksumUtils.java b/core/src/main/java/com/yubico/yubikit/core/otp/ChecksumUtils.java index 276b5588..c2c779db 100755 --- a/core/src/main/java/com/yubico/yubikit/core/otp/ChecksumUtils.java +++ b/core/src/main/java/com/yubico/yubikit/core/otp/ChecksumUtils.java @@ -16,50 +16,48 @@ package com.yubico.yubikit.core.otp; -/** - * Utility methods for calculating and verifying the CRC13239 checksum used by YubiKeys. - */ +/** Utility methods for calculating and verifying the CRC13239 checksum used by YubiKeys. */ public class ChecksumUtils { - // When verifying a checksum the CRC_OK_RESIDUAL should be the remainder. - private static final short CRC_OK_RESIDUAL = (short) 0xf0b8; + // When verifying a checksum the CRC_OK_RESIDUAL should be the remainder. + private static final short CRC_OK_RESIDUAL = (short) 0xf0b8; - /** - * Calculate the CRC13239 checksum for a byte buffer. - * - * @param data byte buffer to be checksummed. - * @param length how much of the buffer should be checksummed - * @return the calculated checksum - */ - static public short calculateCrc(byte[] data, int length) { - int crc = 0xffff; + /** + * Calculate the CRC13239 checksum for a byte buffer. + * + * @param data byte buffer to be checksummed. + * @param length how much of the buffer should be checksummed + * @return the calculated checksum + */ + public static short calculateCrc(byte[] data, int length) { + int crc = 0xffff; - for (int index = 0; index < length; index++) { - int i, j; - crc ^= data[index] & 0xFF; - for (i = 0; i < 8; i++) { - j = crc & 1; - crc >>= 1; - if (j == 1) { - crc ^= 0x8408; - } - } + for (int index = 0; index < length; index++) { + int i, j; + crc ^= data[index] & 0xFF; + for (i = 0; i < 8; i++) { + j = crc & 1; + crc >>= 1; + if (j == 1) { + crc ^= 0x8408; } - - return (short) (crc & 0xFFFF); + } } - /** - * Verifies a checksum. - * - * @param data the data, ending in the 2 byte CRC checksum to verify - * @param length The length of the data, including the checksum at the end - * @return true if the checksum is correct, false if not - */ - static public boolean checkCrc(byte[] data, int length) { - return calculateCrc(data, length) == CRC_OK_RESIDUAL; - } + return (short) (crc & 0xFFFF); + } - private ChecksumUtils() { - throw new IllegalStateException(); - } + /** + * Verifies a checksum. + * + * @param data the data, ending in the 2 byte CRC checksum to verify + * @param length The length of the data, including the checksum at the end + * @return true if the checksum is correct, false if not + */ + public static boolean checkCrc(byte[] data, int length) { + return calculateCrc(data, length) == CRC_OK_RESIDUAL; + } + + private ChecksumUtils() { + throw new IllegalStateException(); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/otp/CommandRejectedException.java b/core/src/main/java/com/yubico/yubikit/core/otp/CommandRejectedException.java index 90b2a601..ae4d1f18 100755 --- a/core/src/main/java/com/yubico/yubikit/core/otp/CommandRejectedException.java +++ b/core/src/main/java/com/yubico/yubikit/core/otp/CommandRejectedException.java @@ -17,11 +17,9 @@ import com.yubico.yubikit.core.application.CommandException; -/** - * Thrown if a command is rejected by the YubiKey. - */ +/** Thrown if a command is rejected by the YubiKey. */ public class CommandRejectedException extends CommandException { - public CommandRejectedException(String message) { - super(message); - } + public CommandRejectedException(String message) { + super(message); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/otp/Modhex.java b/core/src/main/java/com/yubico/yubikit/core/otp/Modhex.java index b41e1879..eb146fff 100755 --- a/core/src/main/java/com/yubico/yubikit/core/otp/Modhex.java +++ b/core/src/main/java/com/yubico/yubikit/core/otp/Modhex.java @@ -22,60 +22,57 @@ /** * Methods for encoding and decoding Modhex encoded Strings. - *

- * See: Modhex specification. + * + *

See: Modhex + * specification. */ public class Modhex { - @SuppressWarnings("SpellCheckingInspection") - private final static char[] ALPHABET = "cbdefghijklnrtuv".toCharArray(); + @SuppressWarnings("SpellCheckingInspection") + private static final char[] ALPHABET = "cbdefghijklnrtuv".toCharArray(); - private static final Map table = new HashMap<>(); + private static final Map table = new HashMap<>(); - static { - for (int i = 0; i < ALPHABET.length; i++) { - table.put(ALPHABET[i], i); - } + static { + for (int i = 0; i < ALPHABET.length; i++) { + table.put(ALPHABET[i], i); } + } - /** - * Decodes Modhex encoded string. - */ - public static byte[] decode(String modhex) { - if (modhex.length() % 2 != 0) { - throw new IllegalArgumentException("Input string length is not a multiple of 2"); - } + /** Decodes Modhex encoded string. */ + public static byte[] decode(String modhex) { + if (modhex.length() % 2 != 0) { + throw new IllegalArgumentException("Input string length is not a multiple of 2"); + } - byte byteValue = 0; - char[] chars = modhex.toLowerCase().toCharArray(); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte byteValue = 0; + char[] chars = modhex.toLowerCase().toCharArray(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - for (int i = 0; i < chars.length; i++) { - // find hex code for each symbol - Integer code = table.get(chars[i]); - if (code == null) { - throw new IllegalArgumentException("Input string contains non-modhex character(s)."); - } + for (int i = 0; i < chars.length; i++) { + // find hex code for each symbol + Integer code = table.get(chars[i]); + if (code == null) { + throw new IllegalArgumentException("Input string contains non-modhex character(s)."); + } - // 2 symbols merged into 1 byte - boolean shift = i % 2 == 0; - if (shift) { - byteValue = (byte) (code << 4); - } else { - byteValue |= code; - outputStream.write(byteValue); - } - } - return outputStream.toByteArray(); + // 2 symbols merged into 1 byte + boolean shift = i % 2 == 0; + if (shift) { + byteValue = (byte) (code << 4); + } else { + byteValue |= code; + outputStream.write(byteValue); + } } + return outputStream.toByteArray(); + } - /** - * Encodes data to Modhex. - */ - public static String encode(byte[] bytes) { - StringBuilder output = new StringBuilder(); - for (byte b : bytes) { - output.append(ALPHABET[(b >> 4) & 0xF]).append(ALPHABET[b & 0xF]); - } - return output.toString(); + /** Encodes data to Modhex. */ + public static String encode(byte[] bytes) { + StringBuilder output = new StringBuilder(); + for (byte b : bytes) { + output.append(ALPHABET[(b >> 4) & 0xF]).append(ALPHABET[b & 0xF]); } + return output.toString(); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/otp/OtpConnection.java b/core/src/main/java/com/yubico/yubikit/core/otp/OtpConnection.java index be6557f3..3c58034d 100755 --- a/core/src/main/java/com/yubico/yubikit/core/otp/OtpConnection.java +++ b/core/src/main/java/com/yubico/yubikit/core/otp/OtpConnection.java @@ -16,28 +16,25 @@ package com.yubico.yubikit.core.otp; import com.yubico.yubikit.core.YubiKeyConnection; - import java.io.IOException; -/** - * A HID keyboard connection to a YubiKey, which uses feature reports to send and receive data. - */ +/** A HID keyboard connection to a YubiKey, which uses feature reports to send and receive data. */ public interface OtpConnection extends YubiKeyConnection { - int FEATURE_REPORT_SIZE = 8; + int FEATURE_REPORT_SIZE = 8; - /** - * Writes an 8 byte feature report to the YubiKey. - * - * @param report the feature report data to write. - * @throws IOException in case of a write failure - */ - void send(byte[] report) throws IOException; + /** + * Writes an 8 byte feature report to the YubiKey. + * + * @param report the feature report data to write. + * @throws IOException in case of a write failure + */ + void send(byte[] report) throws IOException; - /** - * Read an 8 byte feature report from the YubiKey - * - * @param report a buffer to read into - * @throws IOException in case of a read failure - */ - void receive(byte[] report) throws IOException; + /** + * Read an 8 byte feature report from the YubiKey + * + * @param report a buffer to read into + * @throws IOException in case of a read failure + */ + void receive(byte[] report) throws IOException; } diff --git a/core/src/main/java/com/yubico/yubikit/core/otp/OtpProtocol.java b/core/src/main/java/com/yubico/yubikit/core/otp/OtpProtocol.java index 4351f0f7..71c6add5 100755 --- a/core/src/main/java/com/yubico/yubikit/core/otp/OtpProtocol.java +++ b/core/src/main/java/com/yubico/yubikit/core/otp/OtpProtocol.java @@ -15,242 +15,256 @@ */ package com.yubico.yubikit.core.otp; -import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.Version; import com.yubico.yubikit.core.application.CommandException; import com.yubico.yubikit.core.application.CommandState; import com.yubico.yubikit.core.application.TimeoutException; +import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.util.StringUtils; - -import org.slf4j.LoggerFactory; - import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; - import javax.annotation.Nullable; +import org.slf4j.LoggerFactory; public class OtpProtocol implements Closeable { - private static final int FEATURE_RPT_SIZE = 8; - private static final int FEATURE_RPT_DATA_SIZE = FEATURE_RPT_SIZE - 1; + private static final int FEATURE_RPT_SIZE = 8; + private static final int FEATURE_RPT_DATA_SIZE = FEATURE_RPT_SIZE - 1; - private static final int SLOT_DATA_SIZE = 64; - private static final int FRAME_SIZE = SLOT_DATA_SIZE + 6; + private static final int SLOT_DATA_SIZE = 64; + private static final int FRAME_SIZE = SLOT_DATA_SIZE + 6; - private static final int RESP_PENDING_FLAG = 0x40; /* Response pending flag */ - private static final int SLOT_WRITE_FLAG = 0x80; /* Write flag - set by app - cleared by device */ - private static final int RESP_TIMEOUT_WAIT_FLAG = 0x20; /* Waiting for timeout operation - seconds left in lower 5 bits */ - private static final int DUMMY_REPORT_WRITE = 0x8f; /* Write a dummy report to force update or abort */ + private static final int RESP_PENDING_FLAG = 0x40; /* Response pending flag */ + private static final int SLOT_WRITE_FLAG = 0x80; /* Write flag - set by app - cleared by device */ + private static final int RESP_TIMEOUT_WAIT_FLAG = + 0x20; /* Waiting for timeout operation - seconds left in lower 5 bits */ + private static final int DUMMY_REPORT_WRITE = + 0x8f; /* Write a dummy report to force update or abort */ - private static final int SEQUENCE_MASK = 0x1f; - private static final int SEQUENCE_OFFSET = 0x4; + private static final int SEQUENCE_MASK = 0x1f; + private static final int SEQUENCE_OFFSET = 0x4; - private final CommandState defaultState = new CommandState(); + private final CommandState defaultState = new CommandState(); - private final OtpConnection connection; - private final Version version; + private final OtpConnection connection; + private final Version version; - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(OtpProtocol.class); + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(OtpProtocol.class); - public OtpProtocol(OtpConnection connection) throws IOException { - this.connection = connection; + public OtpProtocol(OtpConnection connection) throws IOException { + this.connection = connection; - byte[] featureReport = readFeatureReport(); - if (featureReport[4] == 3) { - /* NEO, may have cached pgmSeq in arbitrator. - Force communication with applet to refresh pgmSeq by - writing an invalid scan map (which should fail). */ - byte[] scanMap = new byte[51]; - Arrays.fill(scanMap, (byte) 'c'); - try { - sendAndReceive((byte) 0x12, scanMap, null); - } catch (CommandException e) { - // We expect this to fail - } - } - version = Version.fromBytes(Arrays.copyOfRange(featureReport, 1, 4)); + byte[] featureReport = readFeatureReport(); + if (featureReport[4] == 3) { + /* NEO, may have cached pgmSeq in arbitrator. + Force communication with applet to refresh pgmSeq by + writing an invalid scan map (which should fail). */ + byte[] scanMap = new byte[51]; + Arrays.fill(scanMap, (byte) 'c'); + try { + sendAndReceive((byte) 0x12, scanMap, null); + } catch (CommandException e) { + // We expect this to fail + } } + version = Version.fromBytes(Arrays.copyOfRange(featureReport, 1, 4)); + } - public Version getVersion() { - return version; - } + public Version getVersion() { + return version; + } - @Override - public void close() throws IOException { - connection.close(); - } + @Override + public void close() throws IOException { + connection.close(); + } - /** - * Sends a command to the YubiKey, and reads the response. - * If the command results in a configuration update, the programming sequence number is verified - * and the updated status bytes are returned. - * - * @param slot the slot to send to - * @param data the data payload to send - * @param state optional CommandState for listening for user presence requirement and for cancelling a command - * @return response data (including CRC) in the case of data, or an updated status struct - * @throws IOException in case of communication error - * @throws CommandException in case the command failed - */ - public byte[] sendAndReceive(byte slot, @Nullable byte[] data, @Nullable CommandState state) throws IOException, CommandException { - byte[] payload; - if (data == null) { - payload = new byte[SLOT_DATA_SIZE]; - } else if (data.length > SLOT_DATA_SIZE) { - throw new IllegalArgumentException("Payload too large for HID frame!"); - } else { - payload = Arrays.copyOf(data, SLOT_DATA_SIZE); - } - return readFrame(sendFrame(slot, payload), state != null ? state : defaultState); + /** + * Sends a command to the YubiKey, and reads the response. If the command results in a + * configuration update, the programming sequence number is verified and the updated status bytes + * are returned. + * + * @param slot the slot to send to + * @param data the data payload to send + * @param state optional CommandState for listening for user presence requirement and for + * cancelling a command + * @return response data (including CRC) in the case of data, or an updated status struct + * @throws IOException in case of communication error + * @throws CommandException in case the command failed + */ + public byte[] sendAndReceive(byte slot, @Nullable byte[] data, @Nullable CommandState state) + throws IOException, CommandException { + byte[] payload; + if (data == null) { + payload = new byte[SLOT_DATA_SIZE]; + } else if (data.length > SLOT_DATA_SIZE) { + throw new IllegalArgumentException("Payload too large for HID frame!"); + } else { + payload = Arrays.copyOf(data, SLOT_DATA_SIZE); } + return readFrame(sendFrame(slot, payload), state != null ? state : defaultState); + } - /** - * Receive status bytes from YubiKey - * - * @return status bytes (first 3 bytes are the firmware version) - * @throws IOException in case of communication error - */ - public byte[] readStatus() throws IOException { - byte[] featureReport = readFeatureReport(); - // disregard the first and last byte in the feature report - return Arrays.copyOfRange(featureReport, 1, featureReport.length - 1); - } + /** + * Receive status bytes from YubiKey + * + * @return status bytes (first 3 bytes are the firmware version) + * @throws IOException in case of communication error + */ + public byte[] readStatus() throws IOException { + byte[] featureReport = readFeatureReport(); + // disregard the first and last byte in the feature report + return Arrays.copyOfRange(featureReport, 1, featureReport.length - 1); + } - /* Read a single 8 byte feature report */ - private byte[] readFeatureReport() throws IOException { - byte[] bufferRead = new byte[FEATURE_RPT_SIZE]; - connection.receive(bufferRead); - Logger.trace(logger, "READ FEATURE REPORT: {}", StringUtils.bytesToHex(bufferRead)); - return bufferRead; - } + /* Read a single 8 byte feature report */ + private byte[] readFeatureReport() throws IOException { + byte[] bufferRead = new byte[FEATURE_RPT_SIZE]; + connection.receive(bufferRead); + Logger.trace(logger, "READ FEATURE REPORT: {}", StringUtils.bytesToHex(bufferRead)); + return bufferRead; + } - /* Write a single 8 byte feature report */ - private void writeFeatureReport(byte[] buffer) throws IOException { - Logger.trace(logger, "WRITE FEATURE REPORT: {}", StringUtils.bytesToHex(buffer)); - connection.send(buffer); - } + /* Write a single 8 byte feature report */ + private void writeFeatureReport(byte[] buffer) throws IOException { + Logger.trace(logger, "WRITE FEATURE REPORT: {}", StringUtils.bytesToHex(buffer)); + connection.send(buffer); + } - /* Sleep for up to ~1s waiting for the WRITE flag to be unset */ - private void awaitReadyToWrite() throws IOException { - for (int i = 0; i < 20; i++) { - if ((readFeatureReport()[FEATURE_RPT_DATA_SIZE] & SLOT_WRITE_FLAG) == 0) { - return; - } - try { - Thread.sleep(50); - } catch (InterruptedException e) { - //Ignore - } - } - throw new IOException("Timeout waiting for YubiKey to become ready to receive"); + /* Sleep for up to ~1s waiting for the WRITE flag to be unset */ + private void awaitReadyToWrite() throws IOException { + for (int i = 0; i < 20; i++) { + if ((readFeatureReport()[FEATURE_RPT_DATA_SIZE] & SLOT_WRITE_FLAG) == 0) { + return; + } + try { + Thread.sleep(50); + } catch (InterruptedException e) { + // Ignore + } } + throw new IOException("Timeout waiting for YubiKey to become ready to receive"); + } - /* All-zero packets are skipped, except for the very first and last packets */ - private static boolean shouldSend(byte[] packet, byte seq) { - if (seq == 0 || seq == 9) { - return true; - } - for (int i = 0; i < 7; i++) { - if (packet[i] != 0) { - return true; - } - } - return false; + /* All-zero packets are skipped, except for the very first and last packets */ + private static boolean shouldSend(byte[] packet, byte seq) { + if (seq == 0 || seq == 9) { + return true; + } + for (int i = 0; i < 7; i++) { + if (packet[i] != 0) { + return true; + } } + return false; + } - /* Packs and sends one 70 byte frame */ - private int sendFrame(byte slot, byte[] payload) throws IOException { - Logger.trace(logger, "Sending payload over HID to slot {}: {}", String.format("0x%02x", 0xff & slot), StringUtils.bytesToHex(payload)); + /* Packs and sends one 70 byte frame */ + private int sendFrame(byte slot, byte[] payload) throws IOException { + Logger.trace( + logger, + "Sending payload over HID to slot {}: {}", + String.format("0x%02x", 0xff & slot), + StringUtils.bytesToHex(payload)); - // Format Frame - ByteBuffer buf = ByteBuffer.allocate(FRAME_SIZE) - .order(ByteOrder.LITTLE_ENDIAN) - .put(payload) - .put(slot) - .putShort(ChecksumUtils.calculateCrc(payload, payload.length)) - .put(new byte[3]); // 3-byte filler - buf.flip(); + // Format Frame + ByteBuffer buf = + ByteBuffer.allocate(FRAME_SIZE) + .order(ByteOrder.LITTLE_ENDIAN) + .put(payload) + .put(slot) + .putShort(ChecksumUtils.calculateCrc(payload, payload.length)) + .put(new byte[3]); // 3-byte filler + buf.flip(); - // Send frame - int programmingSequence = readFeatureReport()[SEQUENCE_OFFSET]; - byte seq = 0; - byte[] report = new byte[FEATURE_RPT_SIZE]; - while (buf.hasRemaining()) { - buf.get(report, 0, FEATURE_RPT_DATA_SIZE); - if (shouldSend(report, seq)) { - report[FEATURE_RPT_DATA_SIZE] = (byte) (0x80 | seq); - awaitReadyToWrite(); - writeFeatureReport(report); - } - seq++; - } - return programmingSequence; + // Send frame + int programmingSequence = readFeatureReport()[SEQUENCE_OFFSET]; + byte seq = 0; + byte[] report = new byte[FEATURE_RPT_SIZE]; + while (buf.hasRemaining()) { + buf.get(report, 0, FEATURE_RPT_DATA_SIZE); + if (shouldSend(report, seq)) { + report[FEATURE_RPT_DATA_SIZE] = (byte) (0x80 | seq); + awaitReadyToWrite(); + writeFeatureReport(report); + } + seq++; } + return programmingSequence; + } - /* Reads one frame */ - private byte[] readFrame(int programmingSequence, CommandState state) throws IOException, CommandException { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - byte seq = 0; - boolean needsTouch = false; + /* Reads one frame */ + private byte[] readFrame(int programmingSequence, CommandState state) + throws IOException, CommandException { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + byte seq = 0; + boolean needsTouch = false; - while (true) { - byte[] report = readFeatureReport(); - byte statusByte = report[FEATURE_RPT_DATA_SIZE]; - if ((statusByte & RESP_PENDING_FLAG) != 0) { // Response packet - if (seq == (statusByte & SEQUENCE_MASK)) { - // Correct sequence - stream.write(report, 0, FEATURE_RPT_DATA_SIZE); - seq++; - } else if (0 == (statusByte & SEQUENCE_MASK)) { - // Transmission complete - resetState(); - byte[] response = stream.toByteArray(); - Logger.trace(logger, "{} bytes read over HID: {}", response.length, StringUtils.bytesToHex(response)); - return response; - } - } else if (statusByte == 0) { // Status response - int prgSeq = report[SEQUENCE_OFFSET]; - if (stream.size() > 0) { - throw new IOException("Incomplete transfer"); - } else if ((prgSeq == programmingSequence + 1) || (programmingSequence > 0 && prgSeq == 0 && report[SEQUENCE_OFFSET + 1] == 0)) { - // Sequence updated, return status. - // Note that when deleting the "last" slot so no slots are valid, the programming sequence is set to 0. - byte[] status = Arrays.copyOfRange(report, 1, 7); // Skip first and last bytes - Logger.trace(logger, "HID programming sequence updated. New status: {}", StringUtils.bytesToHex(status)); - return status; - } else if (needsTouch) { - throw new TimeoutException("Timed out waiting for touch"); - } else { - throw new CommandRejectedException("No data"); - } - } else { // Need to wait - long timeout; - if ((statusByte & RESP_TIMEOUT_WAIT_FLAG) != 0) { - state.onKeepAliveStatus(CommandState.STATUS_UPNEEDED); - needsTouch = true; - timeout = 100; - } else { - state.onKeepAliveStatus(CommandState.STATUS_PROCESSING); - timeout = 20; - } - if (state.waitForCancel(timeout)) { - resetState(); - throw new TimeoutException("Command cancelled by CommandState"); - } - } + while (true) { + byte[] report = readFeatureReport(); + byte statusByte = report[FEATURE_RPT_DATA_SIZE]; + if ((statusByte & RESP_PENDING_FLAG) != 0) { // Response packet + if (seq == (statusByte & SEQUENCE_MASK)) { + // Correct sequence + stream.write(report, 0, FEATURE_RPT_DATA_SIZE); + seq++; + } else if (0 == (statusByte & SEQUENCE_MASK)) { + // Transmission complete + resetState(); + byte[] response = stream.toByteArray(); + Logger.trace( + logger, + "{} bytes read over HID: {}", + response.length, + StringUtils.bytesToHex(response)); + return response; } + } else if (statusByte == 0) { // Status response + int prgSeq = report[SEQUENCE_OFFSET]; + if (stream.size() > 0) { + throw new IOException("Incomplete transfer"); + } else if ((prgSeq == programmingSequence + 1) + || (programmingSequence > 0 && prgSeq == 0 && report[SEQUENCE_OFFSET + 1] == 0)) { + // Sequence updated, return status. + // Note that when deleting the "last" slot so no slots are valid, the programming sequence + // is set to 0. + byte[] status = Arrays.copyOfRange(report, 1, 7); // Skip first and last bytes + Logger.trace( + logger, + "HID programming sequence updated. New status: {}", + StringUtils.bytesToHex(status)); + return status; + } else if (needsTouch) { + throw new TimeoutException("Timed out waiting for touch"); + } else { + throw new CommandRejectedException("No data"); + } + } else { // Need to wait + long timeout; + if ((statusByte & RESP_TIMEOUT_WAIT_FLAG) != 0) { + state.onKeepAliveStatus(CommandState.STATUS_UPNEEDED); + needsTouch = true; + timeout = 100; + } else { + state.onKeepAliveStatus(CommandState.STATUS_PROCESSING); + timeout = 20; + } + if (state.waitForCancel(timeout)) { + resetState(); + throw new TimeoutException("Command cancelled by CommandState"); + } + } } + } - /** - * Reset the state of YubiKey from reading/means that there won't be any data returned - */ - private void resetState() throws IOException { - byte[] buffer = new byte[FEATURE_RPT_SIZE]; - buffer[FEATURE_RPT_SIZE - 1] = (byte) DUMMY_REPORT_WRITE; /* Invalid sequence = update only */ - writeFeatureReport(buffer); - } -} \ No newline at end of file + /** Reset the state of YubiKey from reading/means that there won't be any data returned */ + private void resetState() throws IOException { + byte[] buffer = new byte[FEATURE_RPT_SIZE]; + buffer[FEATURE_RPT_SIZE - 1] = (byte) DUMMY_REPORT_WRITE; /* Invalid sequence = update only */ + writeFeatureReport(buffer); + } +} diff --git a/core/src/main/java/com/yubico/yubikit/core/otp/package-info.java b/core/src/main/java/com/yubico/yubikit/core/otp/package-info.java index 033df0cd..8c41a6d5 100755 --- a/core/src/main/java/com/yubico/yubikit/core/otp/package-info.java +++ b/core/src/main/java/com/yubico/yubikit/core/otp/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.core.otp; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/core/src/main/java/com/yubico/yubikit/core/package-info.java b/core/src/main/java/com/yubico/yubikit/core/package-info.java index 1e0a679c..46455688 100755 --- a/core/src/main/java/com/yubico/yubikit/core/package-info.java +++ b/core/src/main/java/com/yubico/yubikit/core/package-info.java @@ -15,4 +15,3 @@ */ @PackageNonnullByDefault package com.yubico.yubikit.core; - diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/Apdu.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/Apdu.java index 150f4d18..bf09cb71 100755 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/Apdu.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/Apdu.java @@ -17,107 +17,91 @@ package com.yubico.yubikit.core.smartcard; import java.util.Arrays; - import javax.annotation.Nullable; -/** - * Data model for encapsulating an APDU command, as defined by ISO/IEC 7816-4 standard. - */ +/** Data model for encapsulating an APDU command, as defined by ISO/IEC 7816-4 standard. */ public class Apdu { - private final byte cla; - private final byte ins; - private final byte p1; - private final byte p2; - private final byte[] data; - private final int le; - - /** - * Creates a new command APDU from a list of parameters specified by the ISO/IEC 7816-4 standard. - * - * @param cla the instruction class - * @param ins the instruction number - * @param p1 the first instruction parameter byte - * @param p2 the second instruction parameter byte - * @param data the command data - * @param le the length of expected data in the response - */ - private Apdu(byte cla, byte ins, byte p1, byte p2, @Nullable byte[] data, int le) { - this.cla = cla; - this.ins = ins; - this.p1 = p1; - this.p2 = p2; - this.data = data == null ? new byte[0] : data; - this.le = le; - } - - private Apdu(byte cla, byte ins, byte p1, byte p2, @Nullable byte[] data) { - this(cla, ins, p1, p2, data, 0); - } - - /** - * Constructor using int's for convenience. See {@link #Apdu(byte, byte, byte, byte, byte[])}. - */ - public Apdu(int cla, int ins, int p1, int p2, @Nullable byte[] data, int le) { - this(validateByte(cla, "CLA"), - validateByte(ins, "INS"), - validateByte(p1, "P1"), - validateByte(p2, "P2"), - data, - le - ); - } - public Apdu(int cla, int ins, int p1, int p2, @Nullable byte[] data) { - this(cla, ins, p1, p2, data, 0); - } - - /** - * Returns the data payload of the APDU. - */ - public byte[] getData() { - return Arrays.copyOf(data, data.length); - } - - /** - * Returns the CLA of the APDU. - */ - public byte getCla() { - return cla; - } - - /** - * Returns the INS of the APDU. - */ - public byte getIns() { - return ins; - } - - /** - * Returns the parameter P1 of the APDU. - */ - public byte getP1() { - return p1; - } - - /** - * Returns the parameter P2 of the APDU. - */ - public byte getP2() { - return p2; - } - - public int getLe() { - return le; - } - - /* - * Validates that integer passed fits into byte and converts to byte - */ - private static byte validateByte(int byteInt, String name) { - if (byteInt > 255 || byteInt < Byte.MIN_VALUE) { - throw new IllegalArgumentException("Invalid value for " + name + ", must fit in a byte"); - } - return (byte) byteInt; + private final byte cla; + private final byte ins; + private final byte p1; + private final byte p2; + private final byte[] data; + private final int le; + + /** + * Creates a new command APDU from a list of parameters specified by the ISO/IEC 7816-4 standard. + * + * @param cla the instruction class + * @param ins the instruction number + * @param p1 the first instruction parameter byte + * @param p2 the second instruction parameter byte + * @param data the command data + * @param le the length of expected data in the response + */ + private Apdu(byte cla, byte ins, byte p1, byte p2, @Nullable byte[] data, int le) { + this.cla = cla; + this.ins = ins; + this.p1 = p1; + this.p2 = p2; + this.data = data == null ? new byte[0] : data; + this.le = le; + } + + private Apdu(byte cla, byte ins, byte p1, byte p2, @Nullable byte[] data) { + this(cla, ins, p1, p2, data, 0); + } + + /** Constructor using int's for convenience. See {@link #Apdu(byte, byte, byte, byte, byte[])}. */ + public Apdu(int cla, int ins, int p1, int p2, @Nullable byte[] data, int le) { + this( + validateByte(cla, "CLA"), + validateByte(ins, "INS"), + validateByte(p1, "P1"), + validateByte(p2, "P2"), + data, + le); + } + + public Apdu(int cla, int ins, int p1, int p2, @Nullable byte[] data) { + this(cla, ins, p1, p2, data, 0); + } + + /** Returns the data payload of the APDU. */ + public byte[] getData() { + return Arrays.copyOf(data, data.length); + } + + /** Returns the CLA of the APDU. */ + public byte getCla() { + return cla; + } + + /** Returns the INS of the APDU. */ + public byte getIns() { + return ins; + } + + /** Returns the parameter P1 of the APDU. */ + public byte getP1() { + return p1; + } + + /** Returns the parameter P2 of the APDU. */ + public byte getP2() { + return p2; + } + + public int getLe() { + return le; + } + + /* + * Validates that integer passed fits into byte and converts to byte + */ + private static byte validateByte(int byteInt, String name) { + if (byteInt > 255 || byteInt < Byte.MIN_VALUE) { + throw new IllegalArgumentException("Invalid value for " + name + ", must fit in a byte"); } + return (byte) byteInt; + } } - - diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduException.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduException.java index d92e1106..00a8f742 100755 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduException.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduException.java @@ -17,33 +17,31 @@ package com.yubico.yubikit.core.smartcard; import com.yubico.yubikit.core.application.CommandException; - import java.util.Locale; /** - * Thrown when an APDU command fails with an error code. - * See {@link SW} for a list of status codes. + * Thrown when an APDU command fails with an error code. See {@link SW} for a list of status codes. */ public class ApduException extends CommandException { - private static final long serialVersionUID = 1L; - - private final short sw; - - public ApduException(short sw) { - this(sw, String.format(Locale.ROOT, "APDU error: 0x%04x", sw)); - } - - public ApduException(short sw, String message) { - super(message); - this.sw = sw; - } - - /** - * Gets error code that received via APDU response - * - * @return error code - */ - public short getSw() { - return sw; - } + private static final long serialVersionUID = 1L; + + private final short sw; + + public ApduException(short sw) { + this(sw, String.format(Locale.ROOT, "APDU error: 0x%04x", sw)); + } + + public ApduException(short sw, String message) { + super(message); + this.sw = sw; + } + + /** + * Gets error code that received via APDU response + * + * @return error code + */ + public short getSw() { + return sw; + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduFormat.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduFormat.java index ee6c0bbe..1624c427 100755 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduFormat.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduFormat.java @@ -16,9 +16,8 @@ package com.yubico.yubikit.core.smartcard; -/** - * APDU encoding format. - */ +/** APDU encoding format. */ public enum ApduFormat { - SHORT, EXTENDED + SHORT, + EXTENDED } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduFormatProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduFormatProcessor.java index 676d332d..54500044 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduFormatProcessor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduFormatProcessor.java @@ -19,18 +19,28 @@ import java.io.IOException; abstract class ApduFormatProcessor implements ApduProcessor { - protected final SmartCardConnection connection; + protected final SmartCardConnection connection; - ApduFormatProcessor(SmartCardConnection connection) { - this.connection = connection; - } + ApduFormatProcessor(SmartCardConnection connection) { + this.connection = connection; + } - abstract byte[] formatApdu(byte cla, byte ins, byte p1, byte p2, byte[] data, int offset, int length, int le); + abstract byte[] formatApdu( + byte cla, byte ins, byte p1, byte p2, byte[] data, int offset, int length, int le); - @Override - public ApduResponse sendApdu(Apdu apdu) throws IOException { - byte[] data = apdu.getData(); - byte[] payload = formatApdu(apdu.getCla(), apdu.getIns(), apdu.getP1(), apdu.getP2(), data, 0, data.length, apdu.getLe()); - return new ApduResponse(connection.sendAndReceive(payload)); - } + @Override + public ApduResponse sendApdu(Apdu apdu) throws IOException { + byte[] data = apdu.getData(); + byte[] payload = + formatApdu( + apdu.getCla(), + apdu.getIns(), + apdu.getP1(), + apdu.getP2(), + data, + 0, + data.length, + apdu.getLe()); + return new ApduResponse(connection.sendAndReceive(payload)); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduProcessor.java index 4303bf9e..f082a329 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduProcessor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduProcessor.java @@ -17,10 +17,9 @@ package com.yubico.yubikit.core.smartcard; import com.yubico.yubikit.core.application.BadResponseException; - import java.io.Closeable; import java.io.IOException; public interface ApduProcessor extends Closeable { - ApduResponse sendApdu(Apdu apdu) throws IOException, BadResponseException; + ApduResponse sendApdu(Apdu apdu) throws IOException, BadResponseException; } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduResponse.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduResponse.java index 14158e39..6fc79350 100755 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduResponse.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ApduResponse.java @@ -18,42 +18,40 @@ import java.util.Arrays; -/** - * An APDU response from a YubiKey, comprising response data, and a status code. - */ +/** An APDU response from a YubiKey, comprising response data, and a status code. */ public class ApduResponse { - private final byte[] bytes; + private final byte[] bytes; - /** - * Creates a new response from a key - * - * @param bytes data received from key within session/service provider - */ - public ApduResponse(byte[] bytes) { - if (bytes.length < 2) { - throw new IllegalArgumentException("Invalid APDU response data"); - } - this.bytes = Arrays.copyOf(bytes, bytes.length); + /** + * Creates a new response from a key + * + * @param bytes data received from key within session/service provider + */ + public ApduResponse(byte[] bytes) { + if (bytes.length < 2) { + throw new IllegalArgumentException("Invalid APDU response data"); } + this.bytes = Arrays.copyOf(bytes, bytes.length); + } - /** - * @return the SW from a key response (see {@link SW}). - */ - public short getSw() { - return (short) (((0xff & bytes[bytes.length - 2]) << 8) | (0xff & bytes[bytes.length - 1])); - } + /** + * @return the SW from a key response (see {@link SW}). + */ + public short getSw() { + return (short) (((0xff & bytes[bytes.length - 2]) << 8) | (0xff & bytes[bytes.length - 1])); + } - /** - * @return the data from a key response without the SW. - */ - public byte[] getData() { - return Arrays.copyOfRange(bytes, 0, bytes.length - 2); - } + /** + * @return the data from a key response without the SW. + */ + public byte[] getData() { + return Arrays.copyOfRange(bytes, 0, bytes.length - 2); + } - /** - * @return raw data from a key response - */ - public byte[] getBytes() { - return Arrays.copyOf(bytes, bytes.length); - } + /** + * @return raw data from a key response + */ + public byte[] getBytes() { + return Arrays.copyOf(bytes, bytes.length); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/AppId.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/AppId.java index 1c182ff2..412ae639 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/AppId.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/AppId.java @@ -17,13 +17,14 @@ package com.yubico.yubikit.core.smartcard; public final class AppId { - public static final byte[] MANAGEMENT = {(byte) 0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17}; - public static final byte[] OTP = {(byte) 0xa0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01}; - public static final byte[] OATH = {(byte) 0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01, 0x01}; - public static final byte[] PIV = {(byte) 0xa0, 0x00, 0x00, 0x03, 0x08}; - public static final byte[] FIDO = {(byte) 0xa0, 0x00, 0x00, 0x06, 0x47, 0x2f, 0x00, 0x01}; - public static final byte[] OPENPGP = {(byte) 0xd2, 0x76, 0x00, 0x01, 0x24, 0x01}; - public static final byte[] HSMAUTH = {(byte) 0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x07, 0x01}; - public static final byte[] SECURITYDOMAIN = {(byte) 0xa0, 0x00, 0x00, 0x01, 0x51, 0x00, 0x00, 0x00}; - + public static final byte[] MANAGEMENT = {(byte) 0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17}; + public static final byte[] OTP = {(byte) 0xa0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01}; + public static final byte[] OATH = {(byte) 0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01, 0x01}; + public static final byte[] PIV = {(byte) 0xa0, 0x00, 0x00, 0x03, 0x08}; + public static final byte[] FIDO = {(byte) 0xa0, 0x00, 0x00, 0x06, 0x47, 0x2f, 0x00, 0x01}; + public static final byte[] OPENPGP = {(byte) 0xd2, 0x76, 0x00, 0x01, 0x24, 0x01}; + public static final byte[] HSMAUTH = {(byte) 0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x07, 0x01}; + public static final byte[] SECURITYDOMAIN = { + (byte) 0xa0, 0x00, 0x00, 0x01, 0x51, 0x00, 0x00, 0x00 + }; } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ChainedResponseProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ChainedResponseProcessor.java index d3147a1b..6d66604a 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ChainedResponseProcessor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ChainedResponseProcessor.java @@ -17,44 +17,48 @@ package com.yubico.yubikit.core.smartcard; import com.yubico.yubikit.core.application.BadResponseException; - import java.io.ByteArrayOutputStream; import java.io.IOException; class ChainedResponseProcessor implements ApduProcessor { - private static final byte SW1_HAS_MORE_DATA = 0x61; + private static final byte SW1_HAS_MORE_DATA = 0x61; - private final SmartCardConnection connection; - protected final ApduFormatProcessor processor; - private final byte[] getData; + private final SmartCardConnection connection; + protected final ApduFormatProcessor processor; + private final byte[] getData; - ChainedResponseProcessor(SmartCardConnection connection, boolean extendedApdus, int maxApduSize, byte insSendRemaining) { - this.connection = connection; - if (extendedApdus) { - processor = new ExtendedApduProcessor(connection, maxApduSize); - } else { - processor = new ShortApduProcessor(connection); - } - getData = processor.formatApdu((byte)0, insSendRemaining, (byte)0, (byte)0, new byte[0], 0, 0, 0); + ChainedResponseProcessor( + SmartCardConnection connection, + boolean extendedApdus, + int maxApduSize, + byte insSendRemaining) { + this.connection = connection; + if (extendedApdus) { + processor = new ExtendedApduProcessor(connection, maxApduSize); + } else { + processor = new ShortApduProcessor(connection); } + getData = + processor.formatApdu((byte) 0, insSendRemaining, (byte) 0, (byte) 0, new byte[0], 0, 0, 0); + } - @Override - public ApduResponse sendApdu(Apdu apdu) throws IOException, BadResponseException { - ApduResponse response = processor.sendApdu(apdu); - // Read full response - ByteArrayOutputStream readBuffer = new ByteArrayOutputStream(); - while (response.getSw() >> 8 == SW1_HAS_MORE_DATA) { - readBuffer.write(response.getData()); - response = new ApduResponse(connection.sendAndReceive(getData)); - } - readBuffer.write(response.getData()); - readBuffer.write(response.getSw() >> 8); - readBuffer.write(response.getSw() & 0xff); - return new ApduResponse(readBuffer.toByteArray()); + @Override + public ApduResponse sendApdu(Apdu apdu) throws IOException, BadResponseException { + ApduResponse response = processor.sendApdu(apdu); + // Read full response + ByteArrayOutputStream readBuffer = new ByteArrayOutputStream(); + while (response.getSw() >> 8 == SW1_HAS_MORE_DATA) { + readBuffer.write(response.getData()); + response = new ApduResponse(connection.sendAndReceive(getData)); } + readBuffer.write(response.getData()); + readBuffer.write(response.getSw() >> 8); + readBuffer.write(response.getSw() & 0xff); + return new ApduResponse(readBuffer.toByteArray()); + } - @Override - public void close() throws IOException { - processor.close(); - } + @Override + public void close() throws IOException { + processor.close(); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ExtendedApduProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ExtendedApduProcessor.java index d235d3f9..b3465a13 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ExtendedApduProcessor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ExtendedApduProcessor.java @@ -20,34 +20,35 @@ import java.nio.ByteBuffer; class ExtendedApduProcessor extends ApduFormatProcessor { - private final int maxApduSize; + private final int maxApduSize; - ExtendedApduProcessor(SmartCardConnection connection, int maxApduSize) { - super(connection); - this.maxApduSize = maxApduSize; - } + ExtendedApduProcessor(SmartCardConnection connection, int maxApduSize) { + super(connection); + this.maxApduSize = maxApduSize; + } - @Override - byte[] formatApdu(byte cla, byte ins, byte p1, byte p2, byte[] data, int offset, int length, int le) { - ByteBuffer buf = ByteBuffer.allocate(5 + (data.length > 0 ? 2 : 0) + data.length + (le > 0 ? 2 : 0)) - .put(cla) - .put(ins) - .put(p1) - .put(p2) - .put((byte) 0x00); - if (data.length > 0) { - buf.putShort((short) data.length).put(data); - } - if (le > 0) { - buf.putShort((short) le); - } - if (buf.limit() > maxApduSize) { - throw new UnsupportedOperationException("APDU length exceeds YubiKey capability"); - } - return buf.array(); + @Override + byte[] formatApdu( + byte cla, byte ins, byte p1, byte p2, byte[] data, int offset, int length, int le) { + ByteBuffer buf = + ByteBuffer.allocate(5 + (data.length > 0 ? 2 : 0) + data.length + (le > 0 ? 2 : 0)) + .put(cla) + .put(ins) + .put(p1) + .put(p2) + .put((byte) 0x00); + if (data.length > 0) { + buf.putShort((short) data.length).put(data); } - - @Override - public void close() throws IOException { + if (le > 0) { + buf.putShort((short) le); } + if (buf.limit() > maxApduSize) { + throw new UnsupportedOperationException("APDU length exceeds YubiKey capability"); + } + return buf.array(); + } + + @Override + public void close() throws IOException {} } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/MaxApduSize.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/MaxApduSize.java index 7b655f42..2d06ab11 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/MaxApduSize.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/MaxApduSize.java @@ -17,11 +17,11 @@ package com.yubico.yubikit.core.smartcard; final class MaxApduSize { - static final int NEO = 1390; - static final int YK4 = 2038; - static final int YK4_3 = 3062; + static final int NEO = 1390; + static final int YK4 = 2038; + static final int YK4_3 = 3062; - private MaxApduSize() { - throw new IllegalStateException(); - } + private MaxApduSize() { + throw new IllegalStateException(); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/SW.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/SW.java index a3b5cac3..97a27a66 100755 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/SW.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/SW.java @@ -15,30 +15,28 @@ */ package com.yubico.yubikit.core.smartcard; -/** - * Contains constants for APDU status codes (SW1, SW2). - */ +/** Contains constants for APDU status codes (SW1, SW2). */ public final class SW { - public static final short NO_INPUT_DATA = 0x6285; - public static final short VERIFY_FAIL_NO_RETRY = 0x63C0; - public static final short MEMORY_ERROR = 0x6581; - public static final short WRONG_LENGTH = 0x6700; - public static final short SECURITY_CONDITION_NOT_SATISFIED = 0x6982; - public static final short AUTH_METHOD_BLOCKED = 0x6983; - public static final short DATA_INVALID = 0x6984; - public static final short CONDITIONS_NOT_SATISFIED = 0x6985; - public static final short COMMAND_NOT_ALLOWED = 0x6986; - public static final short INCORRECT_PARAMETERS = 0x6A80; - public static final short FILE_NOT_FOUND = 0x6A82; - public static final short NO_SPACE = 0x6A84; - public static final short REFERENCED_DATA_NOT_FOUND = 0x6A88; - public static final short WRONG_PARAMETERS_P1P2 = 0x6B00; - public static final short INVALID_INSTRUCTION = 0x6D00; - public static final short CLASS_NOT_SUPPORTED = 0x6E00; - public static final short COMMAND_ABORTED = 0x6F00; - public static final short OK = (short) 0x9000; + public static final short NO_INPUT_DATA = 0x6285; + public static final short VERIFY_FAIL_NO_RETRY = 0x63C0; + public static final short MEMORY_ERROR = 0x6581; + public static final short WRONG_LENGTH = 0x6700; + public static final short SECURITY_CONDITION_NOT_SATISFIED = 0x6982; + public static final short AUTH_METHOD_BLOCKED = 0x6983; + public static final short DATA_INVALID = 0x6984; + public static final short CONDITIONS_NOT_SATISFIED = 0x6985; + public static final short COMMAND_NOT_ALLOWED = 0x6986; + public static final short INCORRECT_PARAMETERS = 0x6A80; + public static final short FILE_NOT_FOUND = 0x6A82; + public static final short NO_SPACE = 0x6A84; + public static final short REFERENCED_DATA_NOT_FOUND = 0x6A88; + public static final short WRONG_PARAMETERS_P1P2 = 0x6B00; + public static final short INVALID_INSTRUCTION = 0x6D00; + public static final short CLASS_NOT_SUPPORTED = 0x6E00; + public static final short COMMAND_ABORTED = 0x6F00; + public static final short OK = (short) 0x9000; - private SW() { - throw new IllegalStateException(); - } + private SW() { + throw new IllegalStateException(); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java index 96f82245..3a427591 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ScpProcessor.java @@ -18,49 +18,55 @@ import com.yubico.yubikit.core.application.BadResponseException; import com.yubico.yubikit.core.smartcard.scp.ScpState; - import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; public class ScpProcessor extends ChainedResponseProcessor { - private final ScpState state; - - ScpProcessor(SmartCardConnection connection, ScpState state, int maxApduSize, byte insSendRemaining) { - super(connection, true, maxApduSize, insSendRemaining); - this.state = state; - } + private final ScpState state; - @Override - public ApduResponse sendApdu(Apdu apdu) throws IOException, BadResponseException { - return sendApdu(apdu, true); - } + ScpProcessor( + SmartCardConnection connection, ScpState state, int maxApduSize, byte insSendRemaining) { + super(connection, true, maxApduSize, insSendRemaining); + this.state = state; + } - public ApduResponse sendApdu(Apdu apdu, boolean encrypt) throws IOException, BadResponseException { - byte[] data = apdu.getData(); - if (encrypt) { - data = state.encrypt(data); - } - byte cla = (byte) (apdu.getCla() | 0x04); + @Override + public ApduResponse sendApdu(Apdu apdu) throws IOException, BadResponseException { + return sendApdu(apdu, true); + } - // Calculate and add MAC to data - byte[] macedData = new byte[data.length + 8]; - System.arraycopy(data, 0, macedData, 0, data.length); - byte[] apduData = processor.formatApdu(cla, apdu.getIns(), apdu.getP1(), apdu.getP2(), macedData, 0, macedData.length, 0); - byte[] mac = state.mac(Arrays.copyOf(apduData, apduData.length - 8)); - System.arraycopy(mac, 0, macedData, macedData.length - 8, 8); + public ApduResponse sendApdu(Apdu apdu, boolean encrypt) + throws IOException, BadResponseException { + byte[] data = apdu.getData(); + if (encrypt) { + data = state.encrypt(data); + } + byte cla = (byte) (apdu.getCla() | 0x04); - ApduResponse resp = super.sendApdu(new Apdu(cla, apdu.getIns(), apdu.getP1(), apdu.getP2(), macedData, apdu.getLe())); - byte[] respData = resp.getData(); + // Calculate and add MAC to data + byte[] macedData = new byte[data.length + 8]; + System.arraycopy(data, 0, macedData, 0, data.length); + byte[] apduData = + processor.formatApdu( + cla, apdu.getIns(), apdu.getP1(), apdu.getP2(), macedData, 0, macedData.length, 0); + byte[] mac = state.mac(Arrays.copyOf(apduData, apduData.length - 8)); + System.arraycopy(mac, 0, macedData, macedData.length - 8, 8); - // Un-MAC and decrypt, if needed - if (respData.length > 0) { - respData = state.unmac(respData, resp.getSw()); - } - if (respData.length > 0) { - respData = state.decrypt(respData); - } + ApduResponse resp = + super.sendApdu( + new Apdu(cla, apdu.getIns(), apdu.getP1(), apdu.getP2(), macedData, apdu.getLe())); + byte[] respData = resp.getData(); - return new ApduResponse(ByteBuffer.allocate(respData.length + 2).put(respData).putShort(resp.getSw()).array()); + // Un-MAC and decrypt, if needed + if (respData.length > 0) { + respData = state.unmac(respData, resp.getSw()); + } + if (respData.length > 0) { + respData = state.decrypt(respData); } + + return new ApduResponse( + ByteBuffer.allocate(respData.length + 2).put(respData).putShort(resp.getSw()).array()); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/ShortApduProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/ShortApduProcessor.java index 20088bc6..6bb76e68 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/ShortApduProcessor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/ShortApduProcessor.java @@ -20,50 +20,72 @@ import java.nio.ByteBuffer; class ShortApduProcessor extends ApduFormatProcessor { - private static final int SHORT_APDU_MAX_CHUNK = 0xff; + private static final int SHORT_APDU_MAX_CHUNK = 0xff; - ShortApduProcessor(SmartCardConnection connection) { - super(connection); - } - - @Override - byte[] formatApdu(byte cla, byte ins, byte p1, byte p2, byte[] data, int offset, int length, int le) { - if (length > SHORT_APDU_MAX_CHUNK) { - throw new IllegalArgumentException("Length must be no greater than " + SHORT_APDU_MAX_CHUNK); - } - if (le < 0 || le > SHORT_APDU_MAX_CHUNK) { - throw new IllegalArgumentException("Le must be between 0 and " + SHORT_APDU_MAX_CHUNK); - } + ShortApduProcessor(SmartCardConnection connection) { + super(connection); + } - ByteBuffer buf = ByteBuffer.allocate(4 + (length > 0 ? 1 : 0) + length + (le > 0 ? 1 : 0)) - .put(cla) - .put(ins) - .put(p1) - .put(p2); - if (length > 0) { - buf.put((byte) length).put(data, offset, length); - } - if (le > 0) { - buf.put((byte) le); - } - return buf.array(); + @Override + byte[] formatApdu( + byte cla, byte ins, byte p1, byte p2, byte[] data, int offset, int length, int le) { + if (length > SHORT_APDU_MAX_CHUNK) { + throw new IllegalArgumentException("Length must be no greater than " + SHORT_APDU_MAX_CHUNK); + } + if (le < 0 || le > SHORT_APDU_MAX_CHUNK) { + throw new IllegalArgumentException("Le must be between 0 and " + SHORT_APDU_MAX_CHUNK); } - @Override - public ApduResponse sendApdu(Apdu apdu) throws IOException { - byte[] data = apdu.getData(); - int offset = 0; - while (data.length - offset > SHORT_APDU_MAX_CHUNK) { - ApduResponse response = new ApduResponse(connection.sendAndReceive(formatApdu((byte) (apdu.getCla() | 0x10), apdu.getIns(), apdu.getP1(), apdu.getP2(), data, offset, SHORT_APDU_MAX_CHUNK, apdu.getLe()))); - if (response.getSw() != SW.OK) { - return response; - } - offset += SHORT_APDU_MAX_CHUNK; - } - return new ApduResponse(connection.sendAndReceive(formatApdu(apdu.getCla(), apdu.getIns(), apdu.getP1(), apdu.getP2(), data, offset, data.length - offset, apdu.getLe()))); + ByteBuffer buf = + ByteBuffer.allocate(4 + (length > 0 ? 1 : 0) + length + (le > 0 ? 1 : 0)) + .put(cla) + .put(ins) + .put(p1) + .put(p2); + if (length > 0) { + buf.put((byte) length).put(data, offset, length); } + if (le > 0) { + buf.put((byte) le); + } + return buf.array(); + } - @Override - public void close() throws IOException { + @Override + public ApduResponse sendApdu(Apdu apdu) throws IOException { + byte[] data = apdu.getData(); + int offset = 0; + while (data.length - offset > SHORT_APDU_MAX_CHUNK) { + ApduResponse response = + new ApduResponse( + connection.sendAndReceive( + formatApdu( + (byte) (apdu.getCla() | 0x10), + apdu.getIns(), + apdu.getP1(), + apdu.getP2(), + data, + offset, + SHORT_APDU_MAX_CHUNK, + apdu.getLe()))); + if (response.getSw() != SW.OK) { + return response; + } + offset += SHORT_APDU_MAX_CHUNK; } + return new ApduResponse( + connection.sendAndReceive( + formatApdu( + apdu.getCla(), + apdu.getIns(), + apdu.getP1(), + apdu.getP2(), + data, + offset, + data.length - offset, + apdu.getLe()))); + } + + @Override + public void close() throws IOException {} } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardConnection.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardConnection.java index 2f044088..c37b15e2 100755 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardConnection.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardConnection.java @@ -18,42 +18,39 @@ import com.yubico.yubikit.core.Transport; import com.yubico.yubikit.core.YubiKeyConnection; - import java.io.IOException; -/** - * A connection capable of sending APDUs and receiving their responses. - */ +/** A connection capable of sending APDUs and receiving their responses. */ public interface SmartCardConnection extends YubiKeyConnection { - /** - * Sends a command APDU to the YubiKey, and reads a response. - * - * @param apdu The binary APDU data to be sent. - * @return The response back from the YubiKey. - * @throws IOException in case of communication error - */ - byte[] sendAndReceive(byte[] apdu) throws IOException; + /** + * Sends a command APDU to the YubiKey, and reads a response. + * + * @param apdu The binary APDU data to be sent. + * @return The response back from the YubiKey. + * @throws IOException in case of communication error + */ + byte[] sendAndReceive(byte[] apdu) throws IOException; - /** - * Checks what transport the connection is using (USB or NFC). - * - * @return the physical transport used for the connection. - */ - Transport getTransport(); + /** + * Checks what transport the connection is using (USB or NFC). + * + * @return the physical transport used for the connection. + */ + Transport getTransport(); - /** - * Standard APDUs have a 1-byte length field, allowing a maximum of 255 payload bytes, - * which results in a maximum APDU length of 261 bytes. Extended length APDUs have a 3-byte length field, - * allowing 65535 payload bytes. - * - * @return true if this connection object supports Extended length APDUs. - */ - boolean isExtendedLengthApduSupported(); + /** + * Standard APDUs have a 1-byte length field, allowing a maximum of 255 payload bytes, which + * results in a maximum APDU length of 261 bytes. Extended length APDUs have a 3-byte length + * field, allowing 65535 payload bytes. + * + * @return true if this connection object supports Extended length APDUs. + */ + boolean isExtendedLengthApduSupported(); - /** - * Retrieve Answer to reset (or answer to select for NFC) - * - * @return data block returned for reset command - */ - byte[] getAtr(); -} \ No newline at end of file + /** + * Retrieve Answer to reset (or answer to select for NFC) + * + * @return data block returned for reset command + */ + byte[] getAtr(); +} diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java index c4944fe1..cc2248de 100755 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/SmartCardProtocol.java @@ -26,237 +26,244 @@ import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; import com.yubico.yubikit.core.smartcard.scp.ScpState; import com.yubico.yubikit.core.util.Pair; - import java.io.Closeable; import java.io.IOException; - import javax.annotation.Nullable; /** * Support class for communication over a SmartCardConnection. - *

- * This class handles APDU encoding and chaining, and implements workarounds for known issues. + * + *

This class handles APDU encoding and chaining, and implements workarounds for known issues. */ public class SmartCardProtocol implements Closeable { - private static final byte INS_SELECT = (byte) 0xa4; - private static final byte P1_SELECT = (byte) 0x04; - private static final byte P2_SELECT = (byte) 0x00; + private static final byte INS_SELECT = (byte) 0xa4; + private static final byte P1_SELECT = (byte) 0x04; + private static final byte P2_SELECT = (byte) 0x00; - private static final byte INS_SEND_REMAINING = (byte) 0xc0; + private static final byte INS_SEND_REMAINING = (byte) 0xc0; - private final byte insSendRemaining; + private final byte insSendRemaining; - private final SmartCardConnection connection; + private final SmartCardConnection connection; - private boolean extendedApdus = false; + private boolean extendedApdus = false; - private int maxApduSize = MaxApduSize.NEO; + private int maxApduSize = MaxApduSize.NEO; - private ApduProcessor processor; + private ApduProcessor processor; - /** - * Create new instance of {@link SmartCardProtocol} - * and selects the application for use - * - * @param connection connection to the YubiKey - */ - public SmartCardProtocol(SmartCardConnection connection) { - this(connection, INS_SEND_REMAINING); - } + /** + * Create new instance of {@link SmartCardProtocol} and selects the application for use + * + * @param connection connection to the YubiKey + */ + public SmartCardProtocol(SmartCardConnection connection) { + this(connection, INS_SEND_REMAINING); + } - public SmartCardProtocol(SmartCardConnection connection, byte insSendRemaining) { - this.connection = connection; - this.insSendRemaining = insSendRemaining; - processor = new ChainedResponseProcessor(connection, false, maxApduSize, insSendRemaining); - } + public SmartCardProtocol(SmartCardConnection connection, byte insSendRemaining) { + this.connection = connection; + this.insSendRemaining = insSendRemaining; + processor = new ChainedResponseProcessor(connection, false, maxApduSize, insSendRemaining); + } - private void resetProcessor(@Nullable ApduProcessor processor) throws IOException { - this.processor.close(); - if (processor != null) { - this.processor = processor; - } else { - this.processor = new ChainedResponseProcessor(connection, extendedApdus, maxApduSize, insSendRemaining); - } + private void resetProcessor(@Nullable ApduProcessor processor) throws IOException { + this.processor.close(); + if (processor != null) { + this.processor = processor; + } else { + this.processor = + new ChainedResponseProcessor(connection, extendedApdus, maxApduSize, insSendRemaining); } + } - @Override - public void close() throws IOException { - processor.close(); - connection.close(); - } + @Override + public void close() throws IOException { + processor.close(); + connection.close(); + } - /** - * Enable all relevant settings and workarounds given the firmware version of the YubiKey. - * - * @param firmwareVersion the firmware version to use to configure relevant settings - */ - public void configure(Version firmwareVersion) throws IOException { - if (connection.getTransport() == Transport.USB - && firmwareVersion.isAtLeast(4, 2, 0) - && firmwareVersion.isLessThan(4, 2, 7)) { - //noinspection deprecation - setEnableTouchWorkaround(true); - } else if (firmwareVersion.isAtLeast(4, 0, 0) && !(processor instanceof ScpProcessor)) { - extendedApdus = connection.isExtendedLengthApduSupported(); - maxApduSize = firmwareVersion.isAtLeast(4, 3, 0) ? MaxApduSize.YK4_3 : MaxApduSize.YK4; - resetProcessor(null); - } + /** + * Enable all relevant settings and workarounds given the firmware version of the YubiKey. + * + * @param firmwareVersion the firmware version to use to configure relevant settings + */ + public void configure(Version firmwareVersion) throws IOException { + if (connection.getTransport() == Transport.USB + && firmwareVersion.isAtLeast(4, 2, 0) + && firmwareVersion.isLessThan(4, 2, 7)) { + //noinspection deprecation + setEnableTouchWorkaround(true); + } else if (firmwareVersion.isAtLeast(4, 0, 0) && !(processor instanceof ScpProcessor)) { + extendedApdus = connection.isExtendedLengthApduSupported(); + maxApduSize = firmwareVersion.isAtLeast(4, 3, 0) ? MaxApduSize.YK4_3 : MaxApduSize.YK4; + resetProcessor(null); } + } - /** - * Enable all relevant workarounds given the firmware version of the YubiKey. - * - * @param firmwareVersion the firmware version to use for detection to enable the workarounds - * @deprecated use {@link #configure(Version)} instead. - */ - @Deprecated - public void enableWorkarounds(Version firmwareVersion) { - try { - configure(firmwareVersion); - } catch (IOException e) { - throw new RuntimeException(e); - } + /** + * Enable all relevant workarounds given the firmware version of the YubiKey. + * + * @param firmwareVersion the firmware version to use for detection to enable the workarounds + * @deprecated use {@link #configure(Version)} instead. + */ + @Deprecated + public void enableWorkarounds(Version firmwareVersion) { + try { + configure(firmwareVersion); + } catch (IOException e) { + throw new RuntimeException(e); } + } - /** - * YubiKey 4.2.0 - 4.2.6 have an issue with the touch timeout being too short in certain cases. Enable this workaround - * on such devices to trigger sending a dummy command which mitigates the issue. - * - * @param enableTouchWorkaround true to enable the workaround, false to disable it - * @deprecated use {@link #configure(Version)} instead. - */ - @Deprecated - public void setEnableTouchWorkaround(boolean enableTouchWorkaround) { - try { - if (enableTouchWorkaround) { - extendedApdus = true; - maxApduSize = MaxApduSize.YK4; - resetProcessor(new TouchWorkaroundProcessor(connection, insSendRemaining)); - } else { - resetProcessor(null); - } - } catch (IOException e) { - throw new RuntimeException(e); - } + /** + * YubiKey 4.2.0 - 4.2.6 have an issue with the touch timeout being too short in certain cases. + * Enable this workaround on such devices to trigger sending a dummy command which mitigates the + * issue. + * + * @param enableTouchWorkaround true to enable the workaround, false to disable it + * @deprecated use {@link #configure(Version)} instead. + */ + @Deprecated + public void setEnableTouchWorkaround(boolean enableTouchWorkaround) { + try { + if (enableTouchWorkaround) { + extendedApdus = true; + maxApduSize = MaxApduSize.YK4; + resetProcessor(new TouchWorkaroundProcessor(connection, insSendRemaining)); + } else { + resetProcessor(null); + } + } catch (IOException e) { + throw new RuntimeException(e); } + } - /** - * YubiKey NEO doesn't support extended APDU's for most applications. - * - * @param apduFormat the APDU encoding to use when sending commands - * @deprecated use {@link #configure(Version)} instead. - */ - @Deprecated - public void setApduFormat(ApduFormat apduFormat) { - switch (apduFormat) { - case SHORT: - if (extendedApdus) { - throw new UnsupportedOperationException("Cannot change from EXTENDED to SHORT APDU format"); - } - break; - case EXTENDED: - if (!extendedApdus) { - extendedApdus = true; - try { - resetProcessor(null); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - break; + /** + * YubiKey NEO doesn't support extended APDU's for most applications. + * + * @param apduFormat the APDU encoding to use when sending commands + * @deprecated use {@link #configure(Version)} instead. + */ + @Deprecated + public void setApduFormat(ApduFormat apduFormat) { + switch (apduFormat) { + case SHORT: + if (extendedApdus) { + throw new UnsupportedOperationException( + "Cannot change from EXTENDED to SHORT APDU format"); + } + break; + case EXTENDED: + if (!extendedApdus) { + extendedApdus = true; + try { + resetProcessor(null); + } catch (IOException e) { + throw new RuntimeException(e); + } } + break; } + } - /** - * @return the underlying connection - */ - public SmartCardConnection getConnection() { - return connection; - } + /** + * @return the underlying connection + */ + public SmartCardConnection getConnection() { + return connection; + } - /** - * Sends an APDU to SELECT an Application. - * - * @param aid the AID to select. - * @return the response data from selecting the Application - * @throws IOException in case of connection or communication error - * @throws ApplicationNotAvailableException in case the AID doesn't match an available application - */ - public byte[] select(byte[] aid) throws IOException, ApplicationNotAvailableException { - resetProcessor(null); - try { - return sendAndReceive(new Apdu(0, INS_SELECT, P1_SELECT, P2_SELECT, aid)); - } catch (ApduException e) { - // NEO sometimes returns INVALID_INSTRUCTION instead of FILE_NOT_FOUND - if (e.getSw() == SW.FILE_NOT_FOUND || e.getSw() == SW.INVALID_INSTRUCTION) { - throw new ApplicationNotAvailableException("The application couldn't be selected", e); - } - throw new IOException("Unexpected SW", e); - } + /** + * Sends an APDU to SELECT an Application. + * + * @param aid the AID to select. + * @return the response data from selecting the Application + * @throws IOException in case of connection or communication error + * @throws ApplicationNotAvailableException in case the AID doesn't match an available application + */ + public byte[] select(byte[] aid) throws IOException, ApplicationNotAvailableException { + resetProcessor(null); + try { + return sendAndReceive(new Apdu(0, INS_SELECT, P1_SELECT, P2_SELECT, aid)); + } catch (ApduException e) { + // NEO sometimes returns INVALID_INSTRUCTION instead of FILE_NOT_FOUND + if (e.getSw() == SW.FILE_NOT_FOUND || e.getSw() == SW.INVALID_INSTRUCTION) { + throw new ApplicationNotAvailableException("The application couldn't be selected", e); + } + throw new IOException("Unexpected SW", e); } + } - /** - * Sends APDU command and receives byte array from connection - * In case if output has status code that it has remaining info sends another APDU command to receive what's remaining - * - * @param command well structured command that needs to be send - * @return data blob concatenated from all APDU commands that were sent *set of output commands and send remaining commands) - * @throws IOException in case of connection and communication error - * @throws ApduException in case if received error in APDU response - */ - public byte[] sendAndReceive(Apdu command) throws IOException, ApduException { - try { - ApduResponse response = processor.sendApdu(command); - if (response.getSw() != SW.OK) { - throw new ApduException(response.getSw()); - } - return response.getData(); - } catch (BadResponseException e) { - throw new IOException(e); - } + /** + * Sends APDU command and receives byte array from connection + * + *

In case if output has status code that it has remaining info sends another APDU command to + * receive what's remaining + * + * @param command well structured command that needs to be send + * @return data blob concatenated from all APDU commands that were sent *set of output commands + * and send remaining commands) + * @throws IOException in case of connection and communication error + * @throws ApduException in case if received error in APDU response + */ + public byte[] sendAndReceive(Apdu command) throws IOException, ApduException { + try { + ApduResponse response = processor.sendApdu(command); + if (response.getSw() != SW.OK) { + throw new ApduException(response.getSw()); + } + return response.getData(); + } catch (BadResponseException e) { + throw new IOException(e); } + } - public @Nullable DataEncryptor initScp(ScpKeyParams keyParams) throws IOException, ApduException, BadResponseException { - try { - ScpState state; - if (keyParams instanceof Scp03KeyParams) { - state = initScp03((Scp03KeyParams) keyParams); - } else if (keyParams instanceof Scp11KeyParams) { - state = initScp11((Scp11KeyParams) keyParams); - } else { - throw new IllegalArgumentException("Unsupported ScpKeyParams"); - } - if (!connection.isExtendedLengthApduSupported()) { - throw new IllegalStateException("SCP requires extended APDU support"); - } - extendedApdus = true; - maxApduSize = MaxApduSize.YK4_3; - return state.getDataEncryptor(); - } catch (ApduException e) { - if (e.getSw() == SW.CLASS_NOT_SUPPORTED) { - throw new UnsupportedOperationException("This YubiKey does not support secure messaging"); - } - throw e; - } + public @Nullable DataEncryptor initScp(ScpKeyParams keyParams) + throws IOException, ApduException, BadResponseException { + try { + ScpState state; + if (keyParams instanceof Scp03KeyParams) { + state = initScp03((Scp03KeyParams) keyParams); + } else if (keyParams instanceof Scp11KeyParams) { + state = initScp11((Scp11KeyParams) keyParams); + } else { + throw new IllegalArgumentException("Unsupported ScpKeyParams"); + } + if (!connection.isExtendedLengthApduSupported()) { + throw new IllegalStateException("SCP requires extended APDU support"); + } + extendedApdus = true; + maxApduSize = MaxApduSize.YK4_3; + return state.getDataEncryptor(); + } catch (ApduException e) { + if (e.getSw() == SW.CLASS_NOT_SUPPORTED) { + throw new UnsupportedOperationException("This YubiKey does not support secure messaging"); + } + throw e; } + } - private ScpState initScp03(Scp03KeyParams keyParams) throws IOException, ApduException, BadResponseException { - Pair pair = ScpState.scp03Init(processor, keyParams, null); - ScpProcessor processor = new ScpProcessor(connection, pair.first, MaxApduSize.YK4_3, insSendRemaining); + private ScpState initScp03(Scp03KeyParams keyParams) + throws IOException, ApduException, BadResponseException { + Pair pair = ScpState.scp03Init(processor, keyParams, null); + ScpProcessor processor = + new ScpProcessor(connection, pair.first, MaxApduSize.YK4_3, insSendRemaining); - // Send EXTERNAL AUTHENTICATE - // P1 = C-DECRYPTION, R-ENCRYPTION, C-MAC, and R-MAC - ApduResponse resp = processor.sendApdu(new Apdu(0x84, 0x82, 0x33, 0, pair.second), false); - if (resp.getSw() != SW.OK) { - throw new ApduException(resp.getSw()); - } - resetProcessor(processor); - return pair.first; + // Send EXTERNAL AUTHENTICATE + // P1 = C-DECRYPTION, R-ENCRYPTION, C-MAC, and R-MAC + ApduResponse resp = processor.sendApdu(new Apdu(0x84, 0x82, 0x33, 0, pair.second), false); + if (resp.getSw() != SW.OK) { + throw new ApduException(resp.getSw()); } + resetProcessor(processor); + return pair.first; + } - private ScpState initScp11(Scp11KeyParams keyParams) throws IOException, ApduException, BadResponseException { - ScpState scp = ScpState.scp11Init(processor, keyParams); - resetProcessor(new ScpProcessor(connection, scp, MaxApduSize.YK4_3, insSendRemaining)); - return scp; - } + private ScpState initScp11(Scp11KeyParams keyParams) + throws IOException, ApduException, BadResponseException { + ScpState scp = ScpState.scp11Init(processor, keyParams); + resetProcessor(new ScpProcessor(connection, scp, MaxApduSize.YK4_3, insSendRemaining)); + return scp; + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/TouchWorkaroundProcessor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/TouchWorkaroundProcessor.java index eccf0f00..308c49d5 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/TouchWorkaroundProcessor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/TouchWorkaroundProcessor.java @@ -17,30 +17,29 @@ package com.yubico.yubikit.core.smartcard; import com.yubico.yubikit.core.application.BadResponseException; - import java.io.IOException; class TouchWorkaroundProcessor extends ChainedResponseProcessor { - private long lastLongResponse = 0; - - TouchWorkaroundProcessor(SmartCardConnection connection, byte insSendRemaining) { - super(connection, true, MaxApduSize.YK4, insSendRemaining); - } + private long lastLongResponse = 0; - @Override - public ApduResponse sendApdu(Apdu apdu) throws IOException, BadResponseException { - if (lastLongResponse > 0 && System.currentTimeMillis() - lastLongResponse < 2000) { - super.sendApdu(new Apdu(0, 0, 0, 0, null)); // Dummy APDU; returns an error - lastLongResponse = 0; - } - ApduResponse response = super.sendApdu(apdu); + TouchWorkaroundProcessor(SmartCardConnection connection, byte insSendRemaining) { + super(connection, true, MaxApduSize.YK4, insSendRemaining); + } - if (response.getBytes().length > 54) { - lastLongResponse = System.currentTimeMillis(); - } else { - lastLongResponse = 0; - } + @Override + public ApduResponse sendApdu(Apdu apdu) throws IOException, BadResponseException { + if (lastLongResponse > 0 && System.currentTimeMillis() - lastLongResponse < 2000) { + super.sendApdu(new Apdu(0, 0, 0, 0, null)); // Dummy APDU; returns an error + lastLongResponse = 0; + } + ApduResponse response = super.sendApdu(apdu); - return response; + if (response.getBytes().length > 54) { + lastLongResponse = System.currentTimeMillis(); + } else { + lastLongResponse = 0; } + + return response; + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/package-info.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/package-info.java index 330d08c6..b1579051 100755 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/package-info.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.core.smartcard; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/DataEncryptor.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/DataEncryptor.java index 33e1aa29..60b97416 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/DataEncryptor.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/DataEncryptor.java @@ -16,9 +16,7 @@ package com.yubico.yubikit.core.smartcard.scp; -/** - * Encrypts data using the DEK (data encryption key) of a current SCP session. - */ +/** Encrypts data using the DEK (data encryption key) of a current SCP session. */ public interface DataEncryptor { - byte[] encrypt(byte[] data); + byte[] encrypt(byte[] data); } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/KeyRef.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/KeyRef.java index e8fd75c5..6d261044 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/KeyRef.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/KeyRef.java @@ -20,53 +20,54 @@ import java.util.Objects; /** - * Reference to an SCP key. Each key is uniquely identified by the combination of KID and KVN. Related keys typically share a KVN. + * Reference to an SCP key. Each key is uniquely identified by the combination of KID and KVN. + * Related keys typically share a KVN. */ public class KeyRef { - private final byte kid; - private final byte kvn; + private final byte kid; + private final byte kvn; - public KeyRef(byte kid, byte kvn) { - this.kid = kid; - this.kvn = kvn; - } + public KeyRef(byte kid, byte kvn) { + this.kid = kid; + this.kvn = kvn; + } - /** - * @return the KID of the SCP key - */ - public byte getKid() { - return kid; - } + /** + * @return the KID of the SCP key + */ + public byte getKid() { + return kid; + } - /** - * @return the KVN of the SCP key. - */ - public byte getKvn() { - return kvn; - } + /** + * @return the KVN of the SCP key. + */ + public byte getKvn() { + return kvn; + } - /** - * @return the byte[] representation of the KID-KVN pair. - */ - public byte[] getBytes() { - return new byte[]{kid, kvn}; - } + /** + * @return the byte[] representation of the KID-KVN pair. + */ + public byte[] getBytes() { + return new byte[] {kid, kvn}; + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - KeyRef keyRef = (KeyRef) o; - return kid == keyRef.kid && kvn == keyRef.kvn; - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + KeyRef keyRef = (KeyRef) o; + return kid == keyRef.kid && kvn == keyRef.kvn; + } - @Override - public int hashCode() { - return Objects.hash(kid, kvn); - } + @Override + public int hashCode() { + return Objects.hash(kid, kvn); + } - @Override - public String toString() { - return String.format(Locale.ROOT, "KeyRef{kid=0x%02x, kvn=0x%02x}", 0xff & kid, 0xff & kvn); - } + @Override + public String toString() { + return String.format(Locale.ROOT, "KeyRef{kid=0x%02x, kvn=0x%02x}", 0xff & kid, 0xff & kvn); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp03KeyParams.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp03KeyParams.java index 4102299c..80388f6e 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp03KeyParams.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp03KeyParams.java @@ -17,27 +17,27 @@ package com.yubico.yubikit.core.smartcard.scp; /** - * SCP key parameters for performing an SCP03 authentication. - * SCP03 uses a set of three keys, each with their own KID, but a shared KVN. + * SCP key parameters for performing an SCP03 authentication. SCP03 uses a set of three keys, each + * with their own KID, but a shared KVN. */ public class Scp03KeyParams implements ScpKeyParams { - private final KeyRef keyRef; - final StaticKeys keys; + private final KeyRef keyRef; + final StaticKeys keys; - /** - * @param keyRef the reference to the key set to authenticate with. - * @param keys the key material for authentication. - */ - public Scp03KeyParams(KeyRef keyRef, StaticKeys keys) { - if ((0xff & keyRef.getKid()) > 3) { - throw new IllegalArgumentException("Invalid KID for SCP03"); - } - this.keyRef = keyRef; - this.keys = keys; + /** + * @param keyRef the reference to the key set to authenticate with. + * @param keys the key material for authentication. + */ + public Scp03KeyParams(KeyRef keyRef, StaticKeys keys) { + if ((0xff & keyRef.getKid()) > 3) { + throw new IllegalArgumentException("Invalid KID for SCP03"); } + this.keyRef = keyRef; + this.keys = keys; + } - @Override - public KeyRef getKeyRef() { - return keyRef; - } + @Override + public KeyRef getKeyRef() { + return keyRef; + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp11KeyParams.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp11KeyParams.java index d79dcee2..1603c8da 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp11KeyParams.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/Scp11KeyParams.java @@ -22,52 +22,60 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; - import javax.annotation.Nullable; /** * SCP key parameters for performing SCP11 authentication. - * For SCP11b only keyRef and pkSdEcka are required. Note that this does not authenticate the off-card entity. - * For SCP11a and SCP11c the off-card entity CA key reference must be provided, as well as the off-card entity secret key and certificate chain. + * + *

For SCP11b only keyRef and pkSdEcka are required. Note that this does not authenticate the + * off-card entity. + * + *

For SCP11a and SCP11c the off-card entity CA key reference must be provided, as well as the + * off-card entity secret key and certificate chain. */ public class Scp11KeyParams implements ScpKeyParams { - private final KeyRef keyRef; - final PublicKey pkSdEcka; - @Nullable - final KeyRef oceKeyRef; - @Nullable - final PrivateKey skOceEcka; - final List certificates; + private final KeyRef keyRef; + final PublicKey pkSdEcka; + @Nullable final KeyRef oceKeyRef; + @Nullable final PrivateKey skOceEcka; + final List certificates; - public Scp11KeyParams(KeyRef keyRef, PublicKey pkSdEcka, @Nullable KeyRef oceKeyRef, @Nullable PrivateKey skOceEcka, List certificates) { - this.keyRef = keyRef; - this.pkSdEcka = pkSdEcka; - this.oceKeyRef = oceKeyRef; - this.skOceEcka = skOceEcka; - this.certificates = Collections.unmodifiableList(new ArrayList<>(certificates)); - switch (keyRef.getKid()) { - case ScpKid.SCP11b: - if (oceKeyRef != null || skOceEcka != null || !certificates.isEmpty()) { - throw new IllegalArgumentException("Cannot provide oceKeyRef, skOceEcka or certificates for SCP11b"); - } - break; - case ScpKid.SCP11a: - case ScpKid.SCP11c: - if (oceKeyRef == null || skOceEcka == null || certificates.isEmpty()) { - throw new IllegalArgumentException("Must provide oceKeyRef, skOceEcka or certificates for SCP11a/c"); - } - break; - default: - throw new IllegalArgumentException("KID must be 0x11, 0x13, or 0x15 for SCP11"); + public Scp11KeyParams( + KeyRef keyRef, + PublicKey pkSdEcka, + @Nullable KeyRef oceKeyRef, + @Nullable PrivateKey skOceEcka, + List certificates) { + this.keyRef = keyRef; + this.pkSdEcka = pkSdEcka; + this.oceKeyRef = oceKeyRef; + this.skOceEcka = skOceEcka; + this.certificates = Collections.unmodifiableList(new ArrayList<>(certificates)); + switch (keyRef.getKid()) { + case ScpKid.SCP11b: + if (oceKeyRef != null || skOceEcka != null || !certificates.isEmpty()) { + throw new IllegalArgumentException( + "Cannot provide oceKeyRef, skOceEcka or certificates for SCP11b"); } + break; + case ScpKid.SCP11a: + case ScpKid.SCP11c: + if (oceKeyRef == null || skOceEcka == null || certificates.isEmpty()) { + throw new IllegalArgumentException( + "Must provide oceKeyRef, skOceEcka or certificates for SCP11a/c"); + } + break; + default: + throw new IllegalArgumentException("KID must be 0x11, 0x13, or 0x15 for SCP11"); } + } - public Scp11KeyParams(KeyRef keyRef, PublicKey pkSdEcka) { - this(keyRef, pkSdEcka, null, null, Collections.emptyList()); - } + public Scp11KeyParams(KeyRef keyRef, PublicKey pkSdEcka) { + this(keyRef, pkSdEcka, null, null, Collections.emptyList()); + } - @Override - public KeyRef getKeyRef() { - return keyRef; - } + @Override + public KeyRef getKeyRef() { + return keyRef; + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKeyParams.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKeyParams.java index 16d13b82..4fa907ab 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKeyParams.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKeyParams.java @@ -16,12 +16,10 @@ package com.yubico.yubikit.core.smartcard.scp; -/** - * SCP key parameters for performing an SCP authentication with a YubiKey. - */ +/** SCP key parameters for performing an SCP authentication with a YubiKey. */ public interface ScpKeyParams { - /** - * @return the identifier of the SCP key to target on the YubiKey. - */ - KeyRef getKeyRef(); + /** + * @return the identifier of the SCP key to target on the YubiKey. + */ + KeyRef getKeyRef(); } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKid.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKid.java index a916ffe4..8e64e0f8 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKid.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpKid.java @@ -17,12 +17,12 @@ package com.yubico.yubikit.core.smartcard.scp; public final class ScpKid { - public static final byte SCP03 = 0x1; - public static final byte SCP11a = 0x11; - public static final byte SCP11b = 0x13; - public static final byte SCP11c = 0x15; + public static final byte SCP03 = 0x1; + public static final byte SCP11a = 0x11; + public static final byte SCP11b = 0x13; + public static final byte SCP11c = 0x15; - private ScpKid() { - throw new IllegalStateException(); - } + private ScpKid() { + throw new IllegalStateException(); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java index 5533d45c..78215673 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/ScpState.java @@ -29,9 +29,6 @@ import com.yubico.yubikit.core.util.StringUtils; import com.yubico.yubikit.core.util.Tlv; import com.yubico.yubikit.core.util.Tlvs; - -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.nio.ByteBuffer; import java.security.InvalidAlgorithmParameterException; @@ -48,7 +45,6 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; - import javax.annotation.Nullable; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; @@ -59,313 +55,341 @@ import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; +import org.slf4j.LoggerFactory; -/** - * Internal SCP state class for managing SCP state, handling encryption/decryption and MAC. - */ +/** Internal SCP state class for managing SCP state, handling encryption/decryption and MAC. */ public class ScpState { - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(ScpState.class); + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(ScpState.class); - private final SessionKeys keys; - private byte[] macChain; - private int encCounter = 1; + private final SessionKeys keys; + private byte[] macChain; + private int encCounter = 1; - public ScpState(SessionKeys keys, byte[] macChain) { - this.keys = keys; - this.macChain = macChain; - } + public ScpState(SessionKeys keys, byte[] macChain) { + this.keys = keys; + this.macChain = macChain; + } - public @Nullable DataEncryptor getDataEncryptor() { - if (keys.dek == null) { - return null; - } - return data -> cbcEncrypt(keys.dek, data); + public @Nullable DataEncryptor getDataEncryptor() { + if (keys.dek == null) { + return null; } - - public byte[] encrypt(byte[] data) { - // Pad the data - Logger.trace(logger, "Plaintext data: {}", StringUtils.bytesToHex(data)); - int padLen = 16 - (data.length % 16); - byte[] padded = Arrays.copyOf(data, data.length + padLen); - padded[data.length] = (byte) 0x80; - - // Encrypt - try { - @SuppressWarnings("GetInstance") Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); - cipher.init(Cipher.ENCRYPT_MODE, keys.senc); - byte[] ivData = ByteBuffer.allocate(16).put(new byte[12]).putInt(encCounter++).array(); - byte[] iv = cipher.doFinal(ivData); - - cipher = Cipher.getInstance("AES/CBC/NoPadding"); - cipher.init(Cipher.ENCRYPT_MODE, keys.senc, new IvParameterSpec(iv)); - return cipher.doFinal(padded); - } catch (InvalidKeyException | NoSuchPaddingException | NoSuchAlgorithmException | - IllegalBlockSizeException | BadPaddingException | - InvalidAlgorithmParameterException e) { - //This should never happen - throw new RuntimeException(e); - } finally { - Arrays.fill(padded, (byte) 0); - } + return data -> cbcEncrypt(keys.dek, data); + } + + public byte[] encrypt(byte[] data) { + // Pad the data + Logger.trace(logger, "Plaintext data: {}", StringUtils.bytesToHex(data)); + int padLen = 16 - (data.length % 16); + byte[] padded = Arrays.copyOf(data, data.length + padLen); + padded[data.length] = (byte) 0x80; + + // Encrypt + try { + @SuppressWarnings("GetInstance") + Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, keys.senc); + byte[] ivData = ByteBuffer.allocate(16).put(new byte[12]).putInt(encCounter++).array(); + byte[] iv = cipher.doFinal(ivData); + + cipher = Cipher.getInstance("AES/CBC/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, keys.senc, new IvParameterSpec(iv)); + return cipher.doFinal(padded); + } catch (InvalidKeyException + | NoSuchPaddingException + | NoSuchAlgorithmException + | IllegalBlockSizeException + | BadPaddingException + | InvalidAlgorithmParameterException e) { + // This should never happen + throw new RuntimeException(e); + } finally { + Arrays.fill(padded, (byte) 0); } - - public byte[] decrypt(byte[] encrypted) throws BadResponseException { - // Decrypt - byte[] decrypted = null; - try { - @SuppressWarnings("GetInstance") - Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); - cipher.init(Cipher.ENCRYPT_MODE, keys.senc); - byte[] ivData = ByteBuffer.allocate(16).put((byte) 0x80).put(new byte[11]) - .putInt(encCounter - 1).array(); - byte[] iv = cipher.doFinal(ivData); - - cipher = Cipher.getInstance("AES/CBC/NoPadding"); - cipher.init(Cipher.DECRYPT_MODE, keys.senc, new IvParameterSpec(iv)); - decrypted = cipher.doFinal(encrypted); - for (int i = decrypted.length - 1; i > 0; i--) { - if (decrypted[i] == (byte) 0x80) { - Logger.trace(logger, "Plaintext resp: {}", StringUtils.bytesToHex(decrypted)); - return Arrays.copyOf(decrypted, i); - } else if (decrypted[i] != 0x00) { - break; - } - } - throw new BadResponseException("Bad padding"); - } catch (InvalidKeyException | NoSuchPaddingException | NoSuchAlgorithmException | - IllegalBlockSizeException | BadPaddingException | - InvalidAlgorithmParameterException e) { - //This should never happen - throw new RuntimeException(e); - } finally { - if (decrypted != null) { - Arrays.fill(decrypted, (byte) 0); - } + } + + public byte[] decrypt(byte[] encrypted) throws BadResponseException { + // Decrypt + byte[] decrypted = null; + try { + @SuppressWarnings("GetInstance") + Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, keys.senc); + byte[] ivData = + ByteBuffer.allocate(16).put((byte) 0x80).put(new byte[11]).putInt(encCounter - 1).array(); + byte[] iv = cipher.doFinal(ivData); + + cipher = Cipher.getInstance("AES/CBC/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, keys.senc, new IvParameterSpec(iv)); + decrypted = cipher.doFinal(encrypted); + for (int i = decrypted.length - 1; i > 0; i--) { + if (decrypted[i] == (byte) 0x80) { + Logger.trace(logger, "Plaintext resp: {}", StringUtils.bytesToHex(decrypted)); + return Arrays.copyOf(decrypted, i); + } else if (decrypted[i] != 0x00) { + break; } + } + throw new BadResponseException("Bad padding"); + } catch (InvalidKeyException + | NoSuchPaddingException + | NoSuchAlgorithmException + | IllegalBlockSizeException + | BadPaddingException + | InvalidAlgorithmParameterException e) { + // This should never happen + throw new RuntimeException(e); + } finally { + if (decrypted != null) { + Arrays.fill(decrypted, (byte) 0); + } } - - public byte[] mac(byte[] data) { - try { - Mac mac = Mac.getInstance("AESCMAC"); - mac.init(keys.smac); - mac.update(macChain); - macChain = mac.doFinal(data); - return Arrays.copyOf(macChain, 8); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - throw new UnsupportedOperationException("Cryptography provider does not support AESCMAC", e); - } + } + + public byte[] mac(byte[] data) { + try { + Mac mac = Mac.getInstance("AESCMAC"); + mac.init(keys.smac); + mac.update(macChain); + macChain = mac.doFinal(data); + return Arrays.copyOf(macChain, 8); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new UnsupportedOperationException("Cryptography provider does not support AESCMAC", e); } - - public byte[] unmac(byte[] data, short sw) throws BadResponseException { - byte[] msg = ByteBuffer.allocate(data.length - 8 + 2).put(data, 0, data.length - 8) - .putShort(sw).array(); - - try { - Mac mac = Mac.getInstance("AESCMAC"); - mac.init(keys.srmac); - mac.update(macChain); - - byte[] rmac = Arrays.copyOf(mac.doFinal(msg), 8); - if (MessageDigest.isEqual(rmac, Arrays.copyOfRange(data, data.length - 8, data.length))) { - return Arrays.copyOf(msg, msg.length - 2); - } - throw new BadResponseException("Wrong MAC"); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - throw new UnsupportedOperationException("Cryptography provider does not support AESCMAC", e); - } + } + + public byte[] unmac(byte[] data, short sw) throws BadResponseException { + byte[] msg = + ByteBuffer.allocate(data.length - 8 + 2).put(data, 0, data.length - 8).putShort(sw).array(); + + try { + Mac mac = Mac.getInstance("AESCMAC"); + mac.init(keys.srmac); + mac.update(macChain); + + byte[] rmac = Arrays.copyOf(mac.doFinal(msg), 8); + if (MessageDigest.isEqual(rmac, Arrays.copyOfRange(data, data.length - 8, data.length))) { + return Arrays.copyOf(msg, msg.length - 2); + } + throw new BadResponseException("Wrong MAC"); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new UnsupportedOperationException("Cryptography provider does not support AESCMAC", e); } + } - public static Pair scp03Init(ApduProcessor processor, Scp03KeyParams keyParams, @Nullable byte[] hostChallenge) throws BadResponseException, IOException, ApduException { - if (hostChallenge == null) { - hostChallenge = RandomUtils.getRandomBytes(8); - } - - ApduResponse resp = processor.sendApdu(new Apdu(0x80, SecurityDomainSession.INS_INITIALIZE_UPDATE, keyParams.getKeyRef() - .getKvn(), 0x00, hostChallenge)); - if (resp.getSw() != SW.OK) { - throw new ApduException(resp.getSw()); - } - - byte[] diversificationData = new byte[10]; - byte[] keyInfo = new byte[3]; - byte[] cardChallenge = new byte[8]; - byte[] cardCryptogram = new byte[8]; - ByteBuffer.wrap(resp.getData()) - .get(diversificationData) - .get(keyInfo) - .get(cardChallenge) - .get(cardCryptogram); - - byte[] context = ByteBuffer.allocate(16).put(hostChallenge).put(cardChallenge).array(); - SessionKeys sessionKeys = keyParams.keys.derive(context); - - byte[] genCardCryptogram = StaticKeys.deriveKey(sessionKeys.smac, (byte) 0x00, context, (byte) 0x40) - .getEncoded(); - if (!MessageDigest.isEqual(genCardCryptogram, cardCryptogram)) { - throw new BadResponseException("Wrong SCP03 key set"); - } - - byte[] hostCryptogram = StaticKeys.deriveKey(sessionKeys.smac, (byte) 0x01, context, (byte) 0x40) - .getEncoded(); - return new Pair<>(new ScpState(sessionKeys, new byte[16]), hostCryptogram); + public static Pair scp03Init( + ApduProcessor processor, Scp03KeyParams keyParams, @Nullable byte[] hostChallenge) + throws BadResponseException, IOException, ApduException { + if (hostChallenge == null) { + hostChallenge = RandomUtils.getRandomBytes(8); } - public static ScpState scp11Init(ApduProcessor processor, Scp11KeyParams keyParams) throws BadResponseException, IOException, ApduException { - // GPC v2.3 Amendment F (SCP11) v1.4 §7.1.1 - byte params; - byte kid = keyParams.getKeyRef().getKid(); - switch (kid) { - case ScpKid.SCP11a: - params = 0b01; - break; - case ScpKid.SCP11b: - params = 0b00; - break; - case ScpKid.SCP11c: - params = 0b11; - break; - default: - throw new IllegalArgumentException("Invalid SCP11 KID"); - } + ApduResponse resp = + processor.sendApdu( + new Apdu( + 0x80, + SecurityDomainSession.INS_INITIALIZE_UPDATE, + keyParams.getKeyRef().getKvn(), + 0x00, + hostChallenge)); + if (resp.getSw() != SW.OK) { + throw new ApduException(resp.getSw()); + } - if (kid == ScpKid.SCP11a || kid == ScpKid.SCP11c) { - // GPC v2.3 Amendment F (SCP11) v1.4 §7.5 - Objects.requireNonNull(keyParams.skOceEcka); - int n = keyParams.certificates.size() - 1; - if (n < 0) { - throw new IllegalArgumentException("SCP11a and SCP11c require a certificate chain"); - } - KeyRef oceRef = keyParams.oceKeyRef != null ? keyParams.oceKeyRef : new KeyRef((byte) 0, (byte) 0); - for (int i = 0; i <= n; i++) { - try { - byte[] data = keyParams.certificates.get(i).getEncoded(); - byte p2 = (byte) (oceRef.getKid() | (i < n ? 0x80 : 0x00)); - ApduResponse resp = processor.sendApdu(new Apdu(0x80, SecurityDomainSession.INS_PERFORM_SECURITY_OPERATION, oceRef.getKvn(), p2, data)); - if (resp.getSw() != SW.OK) { - throw new ApduException(resp.getSw()); - } - } catch (CertificateEncodingException e) { - throw new IllegalArgumentException("Invalid certificate encoding", e); - } - } - } + byte[] diversificationData = new byte[10]; + byte[] keyInfo = new byte[3]; + byte[] cardChallenge = new byte[8]; + byte[] cardCryptogram = new byte[8]; + ByteBuffer.wrap(resp.getData()) + .get(diversificationData) + .get(keyInfo) + .get(cardChallenge) + .get(cardCryptogram); + + byte[] context = ByteBuffer.allocate(16).put(hostChallenge).put(cardChallenge).array(); + SessionKeys sessionKeys = keyParams.keys.derive(context); + + byte[] genCardCryptogram = + StaticKeys.deriveKey(sessionKeys.smac, (byte) 0x00, context, (byte) 0x40).getEncoded(); + if (!MessageDigest.isEqual(genCardCryptogram, cardCryptogram)) { + throw new BadResponseException("Wrong SCP03 key set"); + } - byte[] keyUsage = new byte[]{0x3C}; // AUTHENTICATED | C_MAC | C_DECRYPTION | R_MAC | R_ENCRYPTION - byte[] keyType = new byte[]{(byte) 0x88}; // AES - byte[] keyLen = new byte[]{16}; // 128-bit + byte[] hostCryptogram = + StaticKeys.deriveKey(sessionKeys.smac, (byte) 0x01, context, (byte) 0x40).getEncoded(); + return new Pair<>(new ScpState(sessionKeys, new byte[16]), hostCryptogram); + } + + public static ScpState scp11Init(ApduProcessor processor, Scp11KeyParams keyParams) + throws BadResponseException, IOException, ApduException { + // GPC v2.3 Amendment F (SCP11) v1.4 §7.1.1 + byte params; + byte kid = keyParams.getKeyRef().getKid(); + switch (kid) { + case ScpKid.SCP11a: + params = 0b01; + break; + case ScpKid.SCP11b: + params = 0b00; + break; + case ScpKid.SCP11c: + params = 0b11; + break; + default: + throw new IllegalArgumentException("Invalid SCP11 KID"); + } - // Host ephemeral key + if (kid == ScpKid.SCP11a || kid == ScpKid.SCP11c) { + // GPC v2.3 Amendment F (SCP11) v1.4 §7.5 + Objects.requireNonNull(keyParams.skOceEcka); + int n = keyParams.certificates.size() - 1; + if (n < 0) { + throw new IllegalArgumentException("SCP11a and SCP11c require a certificate chain"); + } + KeyRef oceRef = + keyParams.oceKeyRef != null ? keyParams.oceKeyRef : new KeyRef((byte) 0, (byte) 0); + for (int i = 0; i <= n; i++) { try { - KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); - ECPublicKey pk = (ECPublicKey) keyParams.pkSdEcka; - kpg.initialize(pk.getParams()); - KeyPair ephemeralOceEcka = kpg.generateKeyPair(); - PublicKeyValues.Ec epkOceEcka = (PublicKeyValues.Ec) PublicKeyValues.fromPublicKey(ephemeralOceEcka.getPublic()); - - // GPC v2.3 Amendment F (SCP11) v1.4 §7.6.2.3 - byte[] data = Tlvs.encodeList( - Arrays.asList( - new Tlv( - 0xA6, - Tlvs.encodeList( - Arrays.asList( - new Tlv(0x90, new byte[]{0x11, params}), - new Tlv(0x95, keyUsage), - new Tlv(0x80, keyType), - new Tlv(0x81, keyLen) - ) - ) - ), - new Tlv( - 0x5F49, - epkOceEcka.getEncodedPoint() - ) - ) - ); - - // Static host key (SCP11a/c), or ephemeral key again (SCP11b) - PrivateKey skOceEcka = keyParams.skOceEcka != null ? keyParams.skOceEcka : ephemeralOceEcka.getPrivate(); - int ins = keyParams.getKeyRef() - .getKid() == ScpKid.SCP11b ? SecurityDomainSession.INS_INTERNAL_AUTHENTICATE : SecurityDomainSession.INS_EXTERNAL_AUTHENTICATE; - ApduResponse resp = processor.sendApdu(new Apdu(0x80, ins, keyParams.getKeyRef() - .getKvn(), keyParams.getKeyRef().getKid(), data)); - if (resp.getSw() != SW.OK) { - throw new ApduException(resp.getSw()); - } - List tlvs = Tlvs.decodeList(resp.getData()); - Tlv epkSdEckaTlv = tlvs.get(0); - byte[] epkSdEckaEncodedPoint = Tlvs.unpackValue(0x5F49, epkSdEckaTlv.getBytes()); - byte[] receipt = Tlvs.unpackValue(0x86, tlvs.get(1).getBytes()); - - // GPC v2.3 Amendment F (SCP11) v1.3 §3.1.2 Key Derivation - byte[] keyAgreementData = ByteBuffer.allocate(data.length + epkSdEckaTlv.getBytes().length) - .put(data) - .put(epkSdEckaTlv.getBytes()) - .array(); - byte[] sharedInfo = ByteBuffer.allocate(keyUsage.length + keyType.length + keyLen.length) - .put(keyUsage) - .put(keyType) - .put(keyLen) - .array(); - - KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH"); - - keyAgreement.init(ephemeralOceEcka.getPrivate()); - keyAgreement.doPhase(PublicKeyValues.Ec.fromEncodedPoint(epkOceEcka.getCurveParams(), epkSdEckaEncodedPoint) - .toPublicKey(), true); - byte[] ka1 = keyAgreement.generateSecret(); - - keyAgreement.init(skOceEcka); - keyAgreement.doPhase(pk, true); - byte[] ka2 = keyAgreement.generateSecret(); - - byte[] keyMaterial = ByteBuffer.allocate(ka1.length + ka2.length).put(ka1).put(ka2) - .array(); - - List keys = new ArrayList<>(); - int counter = 1; - // We need 5 16-byte keys, which requires 3 iterations of SHA256 - for (int i = 0; i < 3; i++) { - MessageDigest hash = MessageDigest.getInstance("SHA256"); - hash.update(keyMaterial); - hash.update(ByteBuffer.allocate(4).putInt(counter++).array()); - hash.update(sharedInfo); - // Each iteration gives us 2 keys - byte[] digest = hash.digest(); - keys.add(new SecretKeySpec(digest, 0, 16, "AES")); - keys.add(new SecretKeySpec(digest, 16, 16, "AES")); - Arrays.fill(digest, (byte) 0); - } - - // 6 keys were derived. one for verification of receipt, 4 keys to use, and 1 which is discarded - SecretKey key = keys.get(0); - Mac mac = Mac.getInstance("AESCMAC"); - mac.init(key); - byte[] genReceipt = mac.doFinal(keyAgreementData); - if (!MessageDigest.isEqual(receipt, genReceipt)) { - throw new BadResponseException("Receipt does not match"); - } - return new ScpState(new SessionKeys( - keys.get(1), - keys.get(2), - keys.get(3), - keys.get(4) - ), receipt); - } catch (NoSuchAlgorithmException | InvalidKeySpecException | - InvalidAlgorithmParameterException | InvalidKeyException e) { - throw new RuntimeException(e); + byte[] data = keyParams.certificates.get(i).getEncoded(); + byte p2 = (byte) (oceRef.getKid() | (i < n ? 0x80 : 0x00)); + ApduResponse resp = + processor.sendApdu( + new Apdu( + 0x80, + SecurityDomainSession.INS_PERFORM_SECURITY_OPERATION, + oceRef.getKvn(), + p2, + data)); + if (resp.getSw() != SW.OK) { + throw new ApduException(resp.getSw()); + } + } catch (CertificateEncodingException e) { + throw new IllegalArgumentException("Invalid certificate encoding", e); } + } } - static byte[] cbcEncrypt(SecretKey key, byte[] data) { - try { - Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); - cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(new byte[16])); - return cipher.doFinal(data); - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | - InvalidAlgorithmParameterException | IllegalBlockSizeException | - BadPaddingException e) { - throw new RuntimeException(e); - } + byte[] keyUsage = + new byte[] {0x3C}; // AUTHENTICATED | C_MAC | C_DECRYPTION | R_MAC | R_ENCRYPTION + byte[] keyType = new byte[] {(byte) 0x88}; // AES + byte[] keyLen = new byte[] {16}; // 128-bit + + // Host ephemeral key + try { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); + ECPublicKey pk = (ECPublicKey) keyParams.pkSdEcka; + kpg.initialize(pk.getParams()); + KeyPair ephemeralOceEcka = kpg.generateKeyPair(); + PublicKeyValues.Ec epkOceEcka = + (PublicKeyValues.Ec) PublicKeyValues.fromPublicKey(ephemeralOceEcka.getPublic()); + + // GPC v2.3 Amendment F (SCP11) v1.4 §7.6.2.3 + byte[] data = + Tlvs.encodeList( + Arrays.asList( + new Tlv( + 0xA6, + Tlvs.encodeList( + Arrays.asList( + new Tlv(0x90, new byte[] {0x11, params}), + new Tlv(0x95, keyUsage), + new Tlv(0x80, keyType), + new Tlv(0x81, keyLen)))), + new Tlv(0x5F49, epkOceEcka.getEncodedPoint()))); + + // Static host key (SCP11a/c), or ephemeral key again (SCP11b) + PrivateKey skOceEcka = + keyParams.skOceEcka != null ? keyParams.skOceEcka : ephemeralOceEcka.getPrivate(); + int ins = + keyParams.getKeyRef().getKid() == ScpKid.SCP11b + ? SecurityDomainSession.INS_INTERNAL_AUTHENTICATE + : SecurityDomainSession.INS_EXTERNAL_AUTHENTICATE; + ApduResponse resp = + processor.sendApdu( + new Apdu( + 0x80, ins, keyParams.getKeyRef().getKvn(), keyParams.getKeyRef().getKid(), data)); + if (resp.getSw() != SW.OK) { + throw new ApduException(resp.getSw()); + } + List tlvs = Tlvs.decodeList(resp.getData()); + Tlv epkSdEckaTlv = tlvs.get(0); + byte[] epkSdEckaEncodedPoint = Tlvs.unpackValue(0x5F49, epkSdEckaTlv.getBytes()); + byte[] receipt = Tlvs.unpackValue(0x86, tlvs.get(1).getBytes()); + + // GPC v2.3 Amendment F (SCP11) v1.3 §3.1.2 Key Derivation + byte[] keyAgreementData = + ByteBuffer.allocate(data.length + epkSdEckaTlv.getBytes().length) + .put(data) + .put(epkSdEckaTlv.getBytes()) + .array(); + byte[] sharedInfo = + ByteBuffer.allocate(keyUsage.length + keyType.length + keyLen.length) + .put(keyUsage) + .put(keyType) + .put(keyLen) + .array(); + + KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH"); + + keyAgreement.init(ephemeralOceEcka.getPrivate()); + keyAgreement.doPhase( + PublicKeyValues.Ec.fromEncodedPoint(epkOceEcka.getCurveParams(), epkSdEckaEncodedPoint) + .toPublicKey(), + true); + byte[] ka1 = keyAgreement.generateSecret(); + + keyAgreement.init(skOceEcka); + keyAgreement.doPhase(pk, true); + byte[] ka2 = keyAgreement.generateSecret(); + + byte[] keyMaterial = ByteBuffer.allocate(ka1.length + ka2.length).put(ka1).put(ka2).array(); + + List keys = new ArrayList<>(); + int counter = 1; + // We need 5 16-byte keys, which requires 3 iterations of SHA256 + for (int i = 0; i < 3; i++) { + MessageDigest hash = MessageDigest.getInstance("SHA256"); + hash.update(keyMaterial); + hash.update(ByteBuffer.allocate(4).putInt(counter++).array()); + hash.update(sharedInfo); + // Each iteration gives us 2 keys + byte[] digest = hash.digest(); + keys.add(new SecretKeySpec(digest, 0, 16, "AES")); + keys.add(new SecretKeySpec(digest, 16, 16, "AES")); + Arrays.fill(digest, (byte) 0); + } + + // 6 keys were derived. one for verification of receipt, 4 keys to use, and 1 which is + // discarded + SecretKey key = keys.get(0); + Mac mac = Mac.getInstance("AESCMAC"); + mac.init(key); + byte[] genReceipt = mac.doFinal(keyAgreementData); + if (!MessageDigest.isEqual(receipt, genReceipt)) { + throw new BadResponseException("Receipt does not match"); + } + return new ScpState( + new SessionKeys(keys.get(1), keys.get(2), keys.get(3), keys.get(4)), receipt); + } catch (NoSuchAlgorithmException + | InvalidKeySpecException + | InvalidAlgorithmParameterException + | InvalidKeyException e) { + throw new RuntimeException(e); + } + } + + static byte[] cbcEncrypt(SecretKey key, byte[] data) { + try { + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(new byte[16])); + return cipher.doFinal(data); + } catch (NoSuchAlgorithmException + | NoSuchPaddingException + | InvalidKeyException + | InvalidAlgorithmParameterException + | IllegalBlockSizeException + | BadPaddingException e) { + throw new RuntimeException(e); } + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java index f5abdc11..0e6871bd 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SecurityDomainSession.java @@ -33,9 +33,6 @@ import com.yubico.yubikit.core.util.StringUtils; import com.yubico.yubikit.core.util.Tlv; import com.yubico.yubikit.core.util.Tlvs; - -import org.slf4j.LoggerFactory; - import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -53,445 +50,494 @@ import java.util.List; import java.util.Locale; import java.util.Map; - import javax.annotation.Nullable; import javax.crypto.SecretKey; +import org.slf4j.LoggerFactory; public class SecurityDomainSession extends ApplicationSession { - private static final byte[] DEFAULT_KCV_IV = new byte[]{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; - - private static final byte INS_GET_DATA = (byte) 0xCA; - private static final byte INS_PUT_KEY = (byte) 0xD8; - private static final byte INS_STORE_DATA = (byte) 0xE2; - private static final byte INS_DELETE = (byte) 0xE4; - private static final byte INS_GENERATE_KEY = (byte) 0xF1; - - static final byte INS_INITIALIZE_UPDATE = (byte) 0x50; - static final byte INS_EXTERNAL_AUTHENTICATE = (byte) 0x82; - static final byte INS_INTERNAL_AUTHENTICATE = (byte) 0x88; - static final byte INS_PERFORM_SECURITY_OPERATION = (byte) 0x2A; - - private static final short TAG_KEY_INFORMATION = 0xE0; - private static final short TAG_CARD_RECOGNITION_DATA = 0x66; - private static final short TAG_CA_KLOC_IDENTIFIERS = (short) 0xFF33; - private static final short TAG_CA_KLCC_IDENTIFIERS = (short) 0xFF34; - private static final short TAG_CERTIFICATE_STORE = (short) 0xBF21; - - private static final int KEY_TYPE_AES = 0x88; - private static final int KEY_TYPE_ECC_PUBLIC_KEY = 0xB0; - private static final int KEY_TYPE_ECC_PRIVATE_KEY = 0xB1; - private static final int KEY_TYPE_ECC_KEY_PARAMS = 0xF0; - - private final SmartCardProtocol protocol; - @Nullable - private DataEncryptor dataEncryptor; - - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(SecurityDomainSession.class); - - public SecurityDomainSession(SmartCardConnection connection) throws IOException, ApplicationNotAvailableException { - this(connection, null); + private static final byte[] DEFAULT_KCV_IV = + new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; + + private static final byte INS_GET_DATA = (byte) 0xCA; + private static final byte INS_PUT_KEY = (byte) 0xD8; + private static final byte INS_STORE_DATA = (byte) 0xE2; + private static final byte INS_DELETE = (byte) 0xE4; + private static final byte INS_GENERATE_KEY = (byte) 0xF1; + + static final byte INS_INITIALIZE_UPDATE = (byte) 0x50; + static final byte INS_EXTERNAL_AUTHENTICATE = (byte) 0x82; + static final byte INS_INTERNAL_AUTHENTICATE = (byte) 0x88; + static final byte INS_PERFORM_SECURITY_OPERATION = (byte) 0x2A; + + private static final short TAG_KEY_INFORMATION = 0xE0; + private static final short TAG_CARD_RECOGNITION_DATA = 0x66; + private static final short TAG_CA_KLOC_IDENTIFIERS = (short) 0xFF33; + private static final short TAG_CA_KLCC_IDENTIFIERS = (short) 0xFF34; + private static final short TAG_CERTIFICATE_STORE = (short) 0xBF21; + + private static final int KEY_TYPE_AES = 0x88; + private static final int KEY_TYPE_ECC_PUBLIC_KEY = 0xB0; + private static final int KEY_TYPE_ECC_PRIVATE_KEY = 0xB1; + private static final int KEY_TYPE_ECC_KEY_PARAMS = 0xF0; + + private final SmartCardProtocol protocol; + @Nullable private DataEncryptor dataEncryptor; + + private static final org.slf4j.Logger logger = + LoggerFactory.getLogger(SecurityDomainSession.class); + + public SecurityDomainSession(SmartCardConnection connection) + throws IOException, ApplicationNotAvailableException { + this(connection, null); + } + + public SecurityDomainSession(SmartCardConnection connection, @Nullable ScpKeyParams scpKeyParams) + throws IOException, ApplicationNotAvailableException { + protocol = new SmartCardProtocol(connection); + protocol.select(AppId.SECURITYDOMAIN); + // We don't know the version, but we know it's at least 5.3.0 + protocol.configure(new Version(5, 3, 0)); + if (scpKeyParams != null) { + try { + protocol.initScp(scpKeyParams); + } catch (BadResponseException | ApduException e) { + throw new IllegalStateException(e); + } } - - public SecurityDomainSession(SmartCardConnection connection, @Nullable ScpKeyParams scpKeyParams) throws IOException, ApplicationNotAvailableException { - protocol = new SmartCardProtocol(connection); - protocol.select(AppId.SECURITYDOMAIN); - // We don't know the version, but we know it's at least 5.3.0 - protocol.configure(new Version(5, 3, 0)); - if (scpKeyParams != null) { - try { - protocol.initScp(scpKeyParams); - } catch (BadResponseException | ApduException e) { - throw new IllegalStateException(e); - } - } - Logger.debug(logger, "Security Domain session initialized"); + Logger.debug(logger, "Security Domain session initialized"); + } + + @Override + public Version getVersion() { + throw new UnsupportedOperationException( + "Version cannot be read from Security Domain application"); + } + + @Override + public void close() throws IOException { + protocol.close(); + } + + /** + * Initialize SCP and authenticate the session. SCP11b does not authenticate the off-card entity, + * and will not allow the usage of commands which require such authentication. + */ + public void authenticate(ScpKeyParams keyParams) + throws BadResponseException, ApduException, IOException { + dataEncryptor = protocol.initScp(keyParams); + } + + public byte[] getData(short tag, @Nullable byte[] data) throws ApduException, IOException { + return protocol.sendAndReceive(new Apdu(0, INS_GET_DATA, tag >> 8, tag & 0xff, data)); + } + + public byte[] getCardRecognitionData() throws ApduException, IOException, BadResponseException { + return Tlvs.unpackValue(0x73, getData(TAG_CARD_RECOGNITION_DATA, null)); + } + + public Map> getKeyInformation() + throws ApduException, IOException, BadResponseException { + Map> keys = new HashMap<>(); + for (Tlv tlv : Tlvs.decodeList(getData(TAG_KEY_INFORMATION, null))) { + ByteBuffer data = ByteBuffer.wrap(Tlvs.unpackValue(0xC0, tlv.getBytes())); + KeyRef keyRef = new KeyRef(data.get(), data.get()); + Map components = new HashMap<>(); + while (data.hasRemaining()) { + components.put(data.get(), data.get()); + } + keys.put(keyRef, components); } - - @Override - public Version getVersion() { - throw new UnsupportedOperationException("Version cannot be read from Security Domain application"); + return keys; + } + + public List getCertificateBundle(KeyRef keyRef) + throws ApduException, IOException, CertificateException { + Logger.debug(logger, "Getting certificate bundle for key={}", keyRef); + List certificates = new ArrayList<>(); + try { + byte[] resp = + getData( + TAG_CERTIFICATE_STORE, + new Tlv(0xA6, new Tlv(0x83, keyRef.getBytes()).getBytes()).getBytes()); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + for (Tlv der : Tlvs.decodeList(resp)) { + InputStream stream = new ByteArrayInputStream(der.getBytes()); + certificates.add((X509Certificate) cf.generateCertificate(stream)); + } + } catch (ApduException e) { + // On REFERENCED_DATA_NOT_FOUND return empty list + if (e.getSw() != SW.REFERENCED_DATA_NOT_FOUND) { + throw e; + } } + return certificates; + } - @Override - public void close() throws IOException { - protocol.close(); + public Map getSupportedCaIdentifiers(boolean kloc, boolean klcc) + throws ApduException, IOException { + if (!kloc && !klcc) { + throw new IllegalArgumentException("At least one of kloc and klcc must be true"); } - - /** - * Initialize SCP and authenticate the session. - * SCP11b does not authenticate the off-card entity, and will not allow the usage of commands which require such authentication. - */ - public void authenticate(ScpKeyParams keyParams) throws BadResponseException, ApduException, IOException { - dataEncryptor = protocol.initScp(keyParams); - } - - public byte[] getData(short tag, @Nullable byte[] data) throws ApduException, IOException { - return protocol.sendAndReceive(new Apdu(0, INS_GET_DATA, tag >> 8, tag & 0xff, data)); - } - - public byte[] getCardRecognitionData() throws ApduException, IOException, BadResponseException { - return Tlvs.unpackValue(0x73, getData(TAG_CARD_RECOGNITION_DATA, null)); - } - - public Map> getKeyInformation() throws ApduException, IOException, BadResponseException { - Map> keys = new HashMap<>(); - for (Tlv tlv : Tlvs.decodeList(getData(TAG_KEY_INFORMATION, null))) { - ByteBuffer data = ByteBuffer.wrap(Tlvs.unpackValue(0xC0, tlv.getBytes())); - KeyRef keyRef = new KeyRef(data.get(), data.get()); - Map components = new HashMap<>(); - while (data.hasRemaining()) { - components.put(data.get(), data.get()); - } - keys.put(keyRef, components); + Logger.debug(logger, "Getting CA identifiers KLOC={}, KLCC={}", kloc, klcc); + ByteArrayOutputStream data = new ByteArrayOutputStream(); + if (kloc) { + try { + data.write(getData(TAG_CA_KLOC_IDENTIFIERS, null)); + } catch (ApduException e) { + if (e.getSw() != SW.REFERENCED_DATA_NOT_FOUND) { + throw e; } - return keys; + } } - - public List getCertificateBundle(KeyRef keyRef) throws ApduException, IOException, CertificateException { - Logger.debug(logger, "Getting certificate bundle for key={}", keyRef); - List certificates = new ArrayList<>(); - try { - byte[] resp = getData(TAG_CERTIFICATE_STORE, new Tlv(0xA6, new Tlv(0x83, keyRef.getBytes()).getBytes()).getBytes()); - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - for (Tlv der : Tlvs.decodeList(resp)) { - InputStream stream = new ByteArrayInputStream(der.getBytes()); - certificates.add((X509Certificate) cf.generateCertificate(stream)); - } - } catch (ApduException e) { - // On REFERENCED_DATA_NOT_FOUND return empty list - if (e.getSw() != SW.REFERENCED_DATA_NOT_FOUND) { - throw e; - } + if (klcc) { + try { + data.write(getData(TAG_CA_KLCC_IDENTIFIERS, null)); + } catch (ApduException e) { + if (e.getSw() != SW.REFERENCED_DATA_NOT_FOUND) { + throw e; } - return certificates; + } } - - public Map getSupportedCaIdentifiers(boolean kloc, boolean klcc) throws ApduException, IOException { - if (!kloc && !klcc) { - throw new IllegalArgumentException("At least one of kloc and klcc must be true"); - } - Logger.debug(logger, "Getting CA identifiers KLOC={}, KLCC={}", kloc, klcc); - ByteArrayOutputStream data = new ByteArrayOutputStream(); - if (kloc) { - try { - data.write(getData(TAG_CA_KLOC_IDENTIFIERS, null)); - } catch (ApduException e) { - if (e.getSw() != SW.REFERENCED_DATA_NOT_FOUND) { - throw e; - } - } - } - if (klcc) { - try { - data.write(getData(TAG_CA_KLCC_IDENTIFIERS, null)); - } catch (ApduException e) { - if (e.getSw() != SW.REFERENCED_DATA_NOT_FOUND) { - throw e; - } - } - } - List tlvs = Tlvs.decodeList(data.toByteArray()); - Map identifiers = new HashMap<>(); - for (int i = 0; i < tlvs.size(); i += 2) { - ByteBuffer ref = ByteBuffer.wrap(tlvs.get(i + 1).getValue()); - identifiers.put(new KeyRef(ref.get(), ref.get()), tlvs.get(i).getValue()); - } - return identifiers; + List tlvs = Tlvs.decodeList(data.toByteArray()); + Map identifiers = new HashMap<>(); + for (int i = 0; i < tlvs.size(); i += 2) { + ByteBuffer ref = ByteBuffer.wrap(tlvs.get(i + 1).getValue()); + identifiers.put(new KeyRef(ref.get(), ref.get()), tlvs.get(i).getValue()); } - - public void storeData(byte[] data) throws ApduException, IOException { - protocol.sendAndReceive(new Apdu(0, INS_STORE_DATA, 0x90, 0x00, data)); + return identifiers; + } + + public void storeData(byte[] data) throws ApduException, IOException { + protocol.sendAndReceive(new Apdu(0, INS_STORE_DATA, 0x90, 0x00, data)); + } + + /** + * Store the certificate chain for a given key. + * + *

Requires off-card entity verification. + * + *

Certificates should be in order, with the leaf certificate last. + * + * @param keyRef a reference to the key for which to store the certificates + * @param certificates the certificates to store + */ + public void storeCertificateBundle(KeyRef keyRef, List certificates) + throws ApduException, IOException { + Logger.debug(logger, "Storing certificate bundle for {}", keyRef); + ByteArrayOutputStream data = new ByteArrayOutputStream(); + for (X509Certificate cert : certificates) { + try { + data.write(cert.getEncoded()); + } catch (CertificateEncodingException e) { + throw new IllegalArgumentException("Failed to get encoded version of certificate", e); + } } - - /** - * Store the certificate chain for a given key. - * Requires off-card entity verification. - * Certificates should be in order, with the leaf certificate last. - * - * @param keyRef a reference to the key for which to store the certificates - * @param certificates the certificates to store - */ - public void storeCertificateBundle(KeyRef keyRef, List certificates) throws ApduException, IOException { - Logger.debug(logger, "Storing certificate bundle for {}", keyRef); - ByteArrayOutputStream data = new ByteArrayOutputStream(); - for (X509Certificate cert : certificates) { - try { - data.write(cert.getEncoded()); - } catch (CertificateEncodingException e) { - throw new IllegalArgumentException("Failed to get encoded version of certificate", e); - } - } - storeData(Tlvs.encodeList(Arrays.asList( + storeData( + Tlvs.encodeList( + Arrays.asList( new Tlv(0xA6, new Tlv(0x83, keyRef.getBytes()).getBytes()), - new Tlv(TAG_CERTIFICATE_STORE, data.toByteArray()) - ))); - Logger.info(logger, "Certificate bundle stored"); + new Tlv(TAG_CERTIFICATE_STORE, data.toByteArray())))); + Logger.info(logger, "Certificate bundle stored"); + } + + /** + * Store which certificate serial numbers that can be used for a given key. + * + *

Requires off-card entity verification. + * + *

If no allowlist is stored, any certificate signed by the CA can be used. + * + * @param keyRef a reference to the key for which to store the allowlist + * @param serials the list of serial numbers to store + */ + public void storeAllowlist(KeyRef keyRef, List serials) + throws ApduException, IOException { + Logger.debug(logger, "Storing serial allowlist for {}", keyRef); + ByteArrayOutputStream data = new ByteArrayOutputStream(); + for (BigInteger serial : serials) { + data.write(new Tlv(0x93, serial.toByteArray()).getBytes()); } - - /** - * Store which certificate serial numbers that can be used for a given key. - * Requires off-card entity verification. - * If no allowlist is stored, any certificate signed by the CA can be used. - * - * @param keyRef a reference to the key for which to store the allowlist - * @param serials the list of serial numbers to store - */ - public void storeAllowlist(KeyRef keyRef, List serials) throws ApduException, IOException { - Logger.debug(logger, "Storing serial allowlist for {}", keyRef); - ByteArrayOutputStream data = new ByteArrayOutputStream(); - for (BigInteger serial : serials) { - data.write(new Tlv(0x93, serial.toByteArray()).getBytes()); - } - storeData(Tlvs.encodeList(Arrays.asList( + storeData( + Tlvs.encodeList( + Arrays.asList( new Tlv(0xA6, new Tlv(0x83, keyRef.getBytes()).getBytes()), - new Tlv(0x70, data.toByteArray()) - ))); - Logger.info(logger, "Serial allowlist stored"); + new Tlv(0x70, data.toByteArray())))); + Logger.info(logger, "Serial allowlist stored"); + } + + /** + * Store the SKI (Subject Key Identifier) for the CA of a given key. + * + *

Requires off-card entity verification. + * + * @param keyRef a reference to the key for which to store the CA issuer + * @param ski the Subject Key Identifier to store + */ + public void storeCaIssuer(KeyRef keyRef, byte[] ski) throws ApduException, IOException { + Logger.debug(logger, "Storing CA issuer SKI for {}: {}", keyRef, StringUtils.bytesToHex(ski)); + byte klcc = 0; + switch (keyRef.getKid()) { + case ScpKid.SCP11a: + case ScpKid.SCP11b: + case ScpKid.SCP11c: + klcc = 1; } - - /** - * Store the SKI (Subject Key Identifier) for the CA of a given key. - * Requires off-card entity verification. - * - * @param keyRef a reference to the key for which to store the CA issuer - * @param ski the Subject Key Identifier to store - */ - public void storeCaIssuer(KeyRef keyRef, byte[] ski) throws ApduException, IOException { - Logger.debug(logger, "Storing CA issuer SKI for {}: {}", keyRef, StringUtils.bytesToHex(ski)); - byte klcc = 0; - switch (keyRef.getKid()) { - case ScpKid.SCP11a: - case ScpKid.SCP11b: - case ScpKid.SCP11c: - klcc = 1; - } - storeData(new Tlv(0xA6, Tlvs.encodeList(Arrays.asList( - new Tlv(0x80, new byte[]{klcc}), - new Tlv(0x42, ski), - new Tlv(0x83, keyRef.getBytes()) - ))).getBytes()); - Logger.info(logger, "CA issuer SKI stored"); + storeData( + new Tlv( + 0xA6, + Tlvs.encodeList( + Arrays.asList( + new Tlv(0x80, new byte[] {klcc}), + new Tlv(0x42, ski), + new Tlv(0x83, keyRef.getBytes())))) + .getBytes()); + Logger.info(logger, "CA issuer SKI stored"); + } + + /** + * Delete one (or more) keys. + * + *

Requires off-card entity verification. + * + *

All keys matching the given KID and/or KVN will be deleted (0 is treated as a wildcard). To + * delete the final key you must set deleteLast = true. + * + * @param keyRef a reference to the key to delete + * @param deleteLast must be true if deleting the final key, false otherwise + */ + public void deleteKey(KeyRef keyRef, boolean deleteLast) throws ApduException, IOException { + byte kid = keyRef.getKid(); + byte kvn = keyRef.getKvn(); + if (kid == 0 && kvn == 0) { + throw new IllegalArgumentException("At least one of KID, KVN must be nonzero"); } - - /** - * Delete one (or more) keys. - * Requires off-card entity verification. - * All keys matching the given KID and/or KVN will be deleted (0 is treated as a wildcard). - * To delete the final key you must set deleteLast = true. - * - * @param keyRef a reference to the key to delete - * @param deleteLast must be true if deleting the final key, false otherwise - */ - public void deleteKey(KeyRef keyRef, boolean deleteLast) throws ApduException, IOException { - byte kid = keyRef.getKid(); - byte kvn = keyRef.getKvn(); - if (kid == 0 && kvn == 0) { - throw new IllegalArgumentException("At least one of KID, KVN must be nonzero"); - } - if (kid == 1 || kid == 2 || kid == 3) { - if (kvn != 0) { - kid = 0; - } else { - throw new IllegalArgumentException("SCP03 keys can only be deleted by KVN"); - } - } - Logger.debug(logger, "Deleting keys matching {}", keyRef); - List tlvs = new ArrayList<>(); - if (kid != 0) { - tlvs.add(new Tlv(0xD0, new byte[]{kid})); - } - if (kvn != 0) { - tlvs.add(new Tlv(0xD2, new byte[]{kvn})); - } - protocol.sendAndReceive(new Apdu(0x80, INS_DELETE, 0, deleteLast ? 1 : 0, Tlvs.encodeList(tlvs))); - Logger.info(logger, "Keys deleted"); + if (kid == 1 || kid == 2 || kid == 3) { + if (kvn != 0) { + kid = 0; + } else { + throw new IllegalArgumentException("SCP03 keys can only be deleted by KVN"); + } } - - /** - * Generate a new SCP11 key. - * Requires off-card entity verification. - * - * @param keyRef the KID-KVN pair to assign the new key - * @param replaceKvn 0 to generate a new keypair, non-zero to replace an existing KVN - * @return the public key from the generated key pair - */ - public PublicKeyValues.Ec generateEcKey(KeyRef keyRef, int replaceKvn) throws ApduException, IOException, BadResponseException { - Logger.debug(logger, "Generating new key for {}" + - (replaceKvn == 0 ? "" : String.format(Locale.ROOT, ", replacing KVN=0x%02x", replaceKvn)), keyRef); - - byte[] params = new Tlv(KEY_TYPE_ECC_KEY_PARAMS, new byte[]{0}).getBytes(); - byte[] data = ByteBuffer - .allocate(params.length + 1) - .put(keyRef.getKvn()) - .put(params) - .array(); - byte[] resp = protocol.sendAndReceive(new Apdu(0x80, INS_GENERATE_KEY, replaceKvn, keyRef.getKid(), data)); - byte[] encodedPoint = Tlvs.unpackValue(KEY_TYPE_ECC_PUBLIC_KEY, resp); - return PublicKeyValues.Ec.fromEncodedPoint(EllipticCurveValues.SECP256R1, encodedPoint); + Logger.debug(logger, "Deleting keys matching {}", keyRef); + List tlvs = new ArrayList<>(); + if (kid != 0) { + tlvs.add(new Tlv(0xD0, new byte[] {kid})); } - - /** - * Imports an SCP03 key set. - * Requires off-card entity verification. - * - * @param keyRef the KID-KVN pair to assign the new key set, KID must be 1 - * @param keys the key material to import - * @param replaceKvn 0 to generate a new keypair, non-zero to replace an existing KVN - */ - public void putKey(KeyRef keyRef, StaticKeys keys, int replaceKvn) throws ApduException, IOException, BadResponseException { - Logger.debug(logger, "Importing SCP03 key set into {}", keyRef); - if (keyRef.getKid() != ScpKid.SCP03) { - throw new IllegalArgumentException("KID must be 0x01 for SCP03 key sets"); - } - if (keys.dek == null) { - throw new IllegalArgumentException("New DEK must be set in static keys"); - } - if (dataEncryptor == null) { - throw new IllegalStateException("No session DEK key available"); - } - - ByteBuffer data = ByteBuffer.allocate(1 + 3 * (18 + 4)).put(keyRef.getKvn()); - ByteBuffer expected = ByteBuffer.allocate(1 + 3 * 3).put(keyRef.getKvn()); - for (SecretKey key : Arrays.asList(keys.enc, keys.mac, keys.dek)) { - byte[] kcv = Arrays.copyOf(ScpState.cbcEncrypt(key, DEFAULT_KCV_IV), 3); - byte[] keyBytes = key.getEncoded(); - try { - data.put(new Tlv(KEY_TYPE_AES, dataEncryptor.encrypt(keyBytes)).getBytes()) - .put((byte) kcv.length) - .put(kcv); - } finally { - Arrays.fill(keyBytes, (byte) 0); - } - expected.put(kcv); - } - - byte[] resp = protocol.sendAndReceive(new Apdu(0x80, INS_PUT_KEY, replaceKvn, 0x80 | keyRef.getKid(), data.array())); - if (!MessageDigest.isEqual(resp, expected.array())) { - throw new BadResponseException("Incorrect key check value"); - } - Logger.info(logger, "SCP03 Key set imported"); + if (kvn != 0) { + tlvs.add(new Tlv(0xD2, new byte[] {kvn})); } - - /** - * Imports a secret key for SCP11. - * Requires off-card entity verification. - * - * @param keyRef the KID-KVN pair to assign the new secret key, KID must be 0x11, 0x13, or 0x15 - * @param secretKey a private EC key used to authenticate the SD - * @param replaceKvn 0 to generate a new keypair, non-zero to replace an existing KVN - */ - public void putKey(KeyRef keyRef, PrivateKeyValues secretKey, int replaceKvn) throws ApduException, IOException, BadResponseException { - Logger.debug(logger, "Importing SCP11 private key into {}", keyRef); - if (!(secretKey instanceof PrivateKeyValues.Ec) || - !((PrivateKeyValues.Ec) secretKey).getCurveParams() - .equals(EllipticCurveValues.SECP256R1)) { - throw new IllegalArgumentException("Private key must be of type SECP256R1"); - } - if (dataEncryptor == null) { - throw new IllegalStateException("No session DEK key available"); - } - - ByteArrayOutputStream data = new ByteArrayOutputStream(); - data.write(keyRef.getKvn()); - byte[] expected = new byte[]{keyRef.getKvn()}; - - byte[] keyBytes = ((PrivateKeyValues.Ec) secretKey).getSecret(); - try { - data.write(new Tlv(KEY_TYPE_ECC_PRIVATE_KEY, dataEncryptor.encrypt(keyBytes)).getBytes()); - } finally { - Arrays.fill(keyBytes, (byte) 0); - } - data.write(new Tlv(KEY_TYPE_ECC_KEY_PARAMS, new byte[]{0x00}).getBytes()); - data.write((byte) 0); - - byte[] resp = protocol.sendAndReceive(new Apdu(0x80, INS_PUT_KEY, replaceKvn, keyRef.getKid(), data.toByteArray())); - if (!MessageDigest.isEqual(resp, expected)) { - throw new BadResponseException("Incorrect key check value"); - } - Logger.info(logger, "SCP11 private key imported"); + protocol.sendAndReceive( + new Apdu(0x80, INS_DELETE, 0, deleteLast ? 1 : 0, Tlvs.encodeList(tlvs))); + Logger.info(logger, "Keys deleted"); + } + + /** + * Generate a new SCP11 key. + * + *

Requires off-card entity verification. + * + * @param keyRef the KID-KVN pair to assign the new key + * @param replaceKvn 0 to generate a new keypair, non-zero to replace an existing KVN + * @return the public key from the generated key pair + */ + public PublicKeyValues.Ec generateEcKey(KeyRef keyRef, int replaceKvn) + throws ApduException, IOException, BadResponseException { + Logger.debug( + logger, + "Generating new key for {}" + + (replaceKvn == 0 + ? "" + : String.format(Locale.ROOT, ", replacing KVN=0x%02x", replaceKvn)), + keyRef); + + byte[] params = new Tlv(KEY_TYPE_ECC_KEY_PARAMS, new byte[] {0}).getBytes(); + byte[] data = ByteBuffer.allocate(params.length + 1).put(keyRef.getKvn()).put(params).array(); + byte[] resp = + protocol.sendAndReceive( + new Apdu(0x80, INS_GENERATE_KEY, replaceKvn, keyRef.getKid(), data)); + byte[] encodedPoint = Tlvs.unpackValue(KEY_TYPE_ECC_PUBLIC_KEY, resp); + return PublicKeyValues.Ec.fromEncodedPoint(EllipticCurveValues.SECP256R1, encodedPoint); + } + + /** + * Imports an SCP03 key set. + * + *

Requires off-card entity verification. + * + * @param keyRef the KID-KVN pair to assign the new key set, KID must be 1 + * @param keys the key material to import + * @param replaceKvn 0 to generate a new keypair, non-zero to replace an existing KVN + */ + public void putKey(KeyRef keyRef, StaticKeys keys, int replaceKvn) + throws ApduException, IOException, BadResponseException { + Logger.debug(logger, "Importing SCP03 key set into {}", keyRef); + if (keyRef.getKid() != ScpKid.SCP03) { + throw new IllegalArgumentException("KID must be 0x01 for SCP03 key sets"); + } + if (keys.dek == null) { + throw new IllegalArgumentException("New DEK must be set in static keys"); + } + if (dataEncryptor == null) { + throw new IllegalStateException("No session DEK key available"); } - /** - * Imports a public key for authentication of the off-card entity for SCP11a/c. - * Requires off-card entity verification. - * - * @param keyRef the KID-KVN pair to assign the new public key - * @param publicKey a public EC key used as CA to authenticate the off-card entity - * @param replaceKvn 0 to generate a new keypair, non-zero to replace an existing KVN - */ - public void putKey(KeyRef keyRef, PublicKeyValues publicKey, int replaceKvn) throws ApduException, IOException, BadResponseException { - Logger.debug(logger, "Importing SCP11 public key into {}", keyRef); - if (!(publicKey instanceof PublicKeyValues.Ec) || - !((PublicKeyValues.Ec) publicKey).getCurveParams() - .equals(EllipticCurveValues.SECP256R1)) { - throw new IllegalArgumentException("Public key must be of type SECP256R1"); - } + ByteBuffer data = ByteBuffer.allocate(1 + 3 * (18 + 4)).put(keyRef.getKvn()); + ByteBuffer expected = ByteBuffer.allocate(1 + 3 * 3).put(keyRef.getKvn()); + for (SecretKey key : Arrays.asList(keys.enc, keys.mac, keys.dek)) { + byte[] kcv = Arrays.copyOf(ScpState.cbcEncrypt(key, DEFAULT_KCV_IV), 3); + byte[] keyBytes = key.getEncoded(); + try { + data.put(new Tlv(KEY_TYPE_AES, dataEncryptor.encrypt(keyBytes)).getBytes()) + .put((byte) kcv.length) + .put(kcv); + } finally { + Arrays.fill(keyBytes, (byte) 0); + } + expected.put(kcv); + } - ByteArrayOutputStream data = new ByteArrayOutputStream(); - data.write(keyRef.getKvn()); - byte[] expected = new byte[]{keyRef.getKvn()}; + byte[] resp = + protocol.sendAndReceive( + new Apdu(0x80, INS_PUT_KEY, replaceKvn, 0x80 | keyRef.getKid(), data.array())); + if (!MessageDigest.isEqual(resp, expected.array())) { + throw new BadResponseException("Incorrect key check value"); + } + Logger.info(logger, "SCP03 Key set imported"); + } + + /** + * Imports a secret key for SCP11. + * + *

Requires off-card entity verification. + * + * @param keyRef the KID-KVN pair to assign the new secret key, KID must be 0x11, 0x13, or 0x15 + * @param secretKey a private EC key used to authenticate the SD + * @param replaceKvn 0 to generate a new keypair, non-zero to replace an existing KVN + */ + public void putKey(KeyRef keyRef, PrivateKeyValues secretKey, int replaceKvn) + throws ApduException, IOException, BadResponseException { + Logger.debug(logger, "Importing SCP11 private key into {}", keyRef); + if (!(secretKey instanceof PrivateKeyValues.Ec) + || !((PrivateKeyValues.Ec) secretKey) + .getCurveParams() + .equals(EllipticCurveValues.SECP256R1)) { + throw new IllegalArgumentException("Private key must be of type SECP256R1"); + } + if (dataEncryptor == null) { + throw new IllegalStateException("No session DEK key available"); + } - data.write(new Tlv(KEY_TYPE_ECC_PUBLIC_KEY, ((PublicKeyValues.Ec) publicKey).getEncodedPoint()).getBytes()); - data.write(new Tlv(KEY_TYPE_ECC_KEY_PARAMS, new byte[]{0x00}).getBytes()); - data.write((byte) 0); + ByteArrayOutputStream data = new ByteArrayOutputStream(); + data.write(keyRef.getKvn()); + byte[] expected = new byte[] {keyRef.getKvn()}; - byte[] resp = protocol.sendAndReceive(new Apdu(0x80, INS_PUT_KEY, replaceKvn, keyRef.getKid(), data.toByteArray())); - if (!MessageDigest.isEqual(resp, expected)) { - throw new BadResponseException("Incorrect key check value"); - } - Logger.info(logger, "SCP11 public key imported"); + byte[] keyBytes = ((PrivateKeyValues.Ec) secretKey).getSecret(); + try { + data.write(new Tlv(KEY_TYPE_ECC_PRIVATE_KEY, dataEncryptor.encrypt(keyBytes)).getBytes()); + } finally { + Arrays.fill(keyBytes, (byte) 0); + } + data.write(new Tlv(KEY_TYPE_ECC_KEY_PARAMS, new byte[] {0x00}).getBytes()); + data.write((byte) 0); + + byte[] resp = + protocol.sendAndReceive( + new Apdu(0x80, INS_PUT_KEY, replaceKvn, keyRef.getKid(), data.toByteArray())); + if (!MessageDigest.isEqual(resp, expected)) { + throw new BadResponseException("Incorrect key check value"); + } + Logger.info(logger, "SCP11 private key imported"); + } + + /** + * Imports a public key for authentication of the off-card entity for SCP11a/c. + * + *

Requires off-card entity verification. + * + * @param keyRef the KID-KVN pair to assign the new public key + * @param publicKey a public EC key used as CA to authenticate the off-card entity + * @param replaceKvn 0 to generate a new keypair, non-zero to replace an existing KVN + */ + public void putKey(KeyRef keyRef, PublicKeyValues publicKey, int replaceKvn) + throws ApduException, IOException, BadResponseException { + Logger.debug(logger, "Importing SCP11 public key into {}", keyRef); + if (!(publicKey instanceof PublicKeyValues.Ec) + || !((PublicKeyValues.Ec) publicKey) + .getCurveParams() + .equals(EllipticCurveValues.SECP256R1)) { + throw new IllegalArgumentException("Public key must be of type SECP256R1"); } - /** - * Perform a factory reset of the Security Domain. - * This will remove all keys and associated data, as well as restore the default SCP03 static keys, - * and generate a new (attestable) SCP11b key. - */ - public void reset() throws BadResponseException, ApduException, IOException { - Logger.debug(logger, "Resetting all SCP keys"); - // Reset is done by blocking all available keys - byte[] data = new byte[8]; - for (KeyRef keyRef : getKeyInformation().keySet()) { - byte ins; - switch (keyRef.getKid()) { - case ScpKid.SCP03: - // SCP03 uses KID=0, we use KVN=0 to allow deleting the default keys - // which have an invalid KVN (0xff). - keyRef = new KeyRef((byte) 0, (byte) 0); - ins = INS_INITIALIZE_UPDATE; - break; - case 0x02: - case 0x03: - continue; // Skip these as they are deleted by 0x01 - case ScpKid.SCP11a: - case ScpKid.SCP11c: - ins = INS_EXTERNAL_AUTHENTICATE; - break; - case ScpKid.SCP11b: - ins = INS_INTERNAL_AUTHENTICATE; - break; - default: // 0x10, 0x20-0x2F - ins = INS_PERFORM_SECURITY_OPERATION; - } - - // Keys have 65 attempts before blocking (and thus removal) - for (int i = 0; i < 65; i++) { - try { - protocol.sendAndReceive(new Apdu(0x80, ins, keyRef.getKvn(), keyRef.getKid(), data)); - } catch (ApduException e) { - switch (e.getSw()) { - case SW.AUTH_METHOD_BLOCKED: - case SW.SECURITY_CONDITION_NOT_SATISFIED: - i = 65; - break; - case SW.INCORRECT_PARAMETERS: - continue; - default: - throw e; - } - } - } + ByteArrayOutputStream data = new ByteArrayOutputStream(); + data.write(keyRef.getKvn()); + byte[] expected = new byte[] {keyRef.getKvn()}; + + data.write( + new Tlv(KEY_TYPE_ECC_PUBLIC_KEY, ((PublicKeyValues.Ec) publicKey).getEncodedPoint()) + .getBytes()); + data.write(new Tlv(KEY_TYPE_ECC_KEY_PARAMS, new byte[] {0x00}).getBytes()); + data.write((byte) 0); + + byte[] resp = + protocol.sendAndReceive( + new Apdu(0x80, INS_PUT_KEY, replaceKvn, keyRef.getKid(), data.toByteArray())); + if (!MessageDigest.isEqual(resp, expected)) { + throw new BadResponseException("Incorrect key check value"); + } + Logger.info(logger, "SCP11 public key imported"); + } + + /** + * Perform a factory reset of the Security Domain. + * + *

This will remove all keys and associated data, as well as restore the default SCP03 static + * keys, and generate a new (attestable) SCP11b key. + */ + public void reset() throws BadResponseException, ApduException, IOException { + Logger.debug(logger, "Resetting all SCP keys"); + // Reset is done by blocking all available keys + byte[] data = new byte[8]; + for (KeyRef keyRef : getKeyInformation().keySet()) { + byte ins; + switch (keyRef.getKid()) { + case ScpKid.SCP03: + // SCP03 uses KID=0, we use KVN=0 to allow deleting the default keys + // which have an invalid KVN (0xff). + keyRef = new KeyRef((byte) 0, (byte) 0); + ins = INS_INITIALIZE_UPDATE; + break; + case 0x02: + case 0x03: + continue; // Skip these as they are deleted by 0x01 + case ScpKid.SCP11a: + case ScpKid.SCP11c: + ins = INS_EXTERNAL_AUTHENTICATE; + break; + case ScpKid.SCP11b: + ins = INS_INTERNAL_AUTHENTICATE; + break; + default: // 0x10, 0x20-0x2F + ins = INS_PERFORM_SECURITY_OPERATION; + } + + // Keys have 65 attempts before blocking (and thus removal) + for (int i = 0; i < 65; i++) { + try { + protocol.sendAndReceive(new Apdu(0x80, ins, keyRef.getKvn(), keyRef.getKid(), data)); + } catch (ApduException e) { + switch (e.getSw()) { + case SW.AUTH_METHOD_BLOCKED: + case SW.SECURITY_CONDITION_NOT_SATISFIED: + i = 65; + break; + case SW.INCORRECT_PARAMETERS: + continue; + default: + throw e; + } } - Logger.info(logger, "SCP keys reset"); + } } + Logger.info(logger, "SCP keys reset"); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SessionKeys.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SessionKeys.java index 2a10ad6b..8f2fe1b4 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SessionKeys.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/SessionKeys.java @@ -20,19 +20,19 @@ import javax.crypto.SecretKey; /** - * Session keys for SCP. DEK only needs to be provided if you need to call {@link SecurityDomainSession#putKey}. + * Session keys for SCP. DEK only needs to be provided if you need to call {@link + * SecurityDomainSession#putKey}. */ public class SessionKeys { - final SecretKey senc; - final SecretKey smac; - final SecretKey srmac; - @Nullable - final SecretKey dek; + final SecretKey senc; + final SecretKey smac; + final SecretKey srmac; + @Nullable final SecretKey dek; - public SessionKeys(SecretKey senc, SecretKey smac, SecretKey srmac, @Nullable SecretKey dek) { - this.senc = senc; - this.smac = smac; - this.srmac = srmac; - this.dek = dek; - } + public SessionKeys(SecretKey senc, SecretKey smac, SecretKey srmac, @Nullable SecretKey dek) { + this.senc = senc; + this.smac = smac; + this.srmac = srmac; + this.dek = dek; + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/StaticKeys.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/StaticKeys.java index 2db6b586..ca233827 100644 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/StaticKeys.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/StaticKeys.java @@ -20,65 +20,66 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; - import javax.annotation.Nullable; import javax.crypto.Mac; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; public class StaticKeys { - private static final byte[] DEFAULT_KEY = new byte[]{ - 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, - 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f - }; + private static final byte[] DEFAULT_KEY = + new byte[] { + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f + }; - final SecretKey enc; - final SecretKey mac; - @Nullable final SecretKey dek; + final SecretKey enc; + final SecretKey mac; + @Nullable final SecretKey dek; - public StaticKeys(byte[] enc, byte[] mac, @Nullable byte[] dek) { - this.enc = new SecretKeySpec(enc, "AES"); - this.mac = new SecretKeySpec(mac, "AES"); - this.dek = dek != null ? new SecretKeySpec(dek, "AES") : null; - } + public StaticKeys(byte[] enc, byte[] mac, @Nullable byte[] dek) { + this.enc = new SecretKeySpec(enc, "AES"); + this.mac = new SecretKeySpec(mac, "AES"); + this.dek = dek != null ? new SecretKeySpec(dek, "AES") : null; + } - public SessionKeys derive(byte[] context) { - return new SessionKeys( - deriveKey(enc, (byte) 0x4, context, (short) 0x80), - deriveKey(mac, (byte) 0x6, context, (short) 0x80), - deriveKey(mac, (byte) 0x7, context, (short) 0x80), - dek - ); - } + public SessionKeys derive(byte[] context) { + return new SessionKeys( + deriveKey(enc, (byte) 0x4, context, (short) 0x80), + deriveKey(mac, (byte) 0x6, context, (short) 0x80), + deriveKey(mac, (byte) 0x7, context, (short) 0x80), + dek); + } - public static StaticKeys getDefaultKeys() { - return new StaticKeys(DEFAULT_KEY, DEFAULT_KEY, DEFAULT_KEY); - } + public static StaticKeys getDefaultKeys() { + return new StaticKeys(DEFAULT_KEY, DEFAULT_KEY, DEFAULT_KEY); + } - static SecretKey deriveKey(SecretKey key, byte t, byte[] context, short l) { - if (!(l == 0x40 || l == 0x80)) { - throw new IllegalArgumentException("l must be 0x40 or 0x80"); - } - byte[] i = ByteBuffer.allocate(16 + context.length) - .put(new byte[11]) - .put(t).put((byte) 0) - .putShort(l) - .put((byte) 1) - .put(context) - .array(); + static SecretKey deriveKey(SecretKey key, byte t, byte[] context, short l) { + if (!(l == 0x40 || l == 0x80)) { + throw new IllegalArgumentException("l must be 0x40 or 0x80"); + } + byte[] i = + ByteBuffer.allocate(16 + context.length) + .put(new byte[11]) + .put(t) + .put((byte) 0) + .putShort(l) + .put((byte) 1) + .put(context) + .array(); - byte[] digest = null; - try { - Mac mac = Mac.getInstance("AESCMAC"); - mac.init(key); - digest = mac.doFinal(i); - return new SecretKeySpec(digest, 0, l / 8, "AES"); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - throw new UnsupportedOperationException("Cryptography provider does not support AESCMAC", e); - } finally { - if (digest != null) { - Arrays.fill(digest, (byte) 0); - } - } + byte[] digest = null; + try { + Mac mac = Mac.getInstance("AESCMAC"); + mac.init(key); + digest = mac.doFinal(i); + return new SecretKeySpec(digest, 0, l / 8, "AES"); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new UnsupportedOperationException("Cryptography provider does not support AESCMAC", e); + } finally { + if (digest != null) { + Arrays.fill(digest, (byte) 0); + } } + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/package-info.java b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/package-info.java index 5b461322..21045b33 100755 --- a/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/package-info.java +++ b/core/src/main/java/com/yubico/yubikit/core/smartcard/scp/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.core.smartcard.scp; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/core/src/main/java/com/yubico/yubikit/core/util/ByteUtils.java b/core/src/main/java/com/yubico/yubikit/core/util/ByteUtils.java index 7a998847..b2ad2c9c 100644 --- a/core/src/main/java/com/yubico/yubikit/core/util/ByteUtils.java +++ b/core/src/main/java/com/yubico/yubikit/core/util/ByteUtils.java @@ -19,29 +19,29 @@ import java.math.BigInteger; import java.util.Arrays; -/** - * Used internally in YubiKit, don't use from applications. - */ +/** Used internally in YubiKit, don't use from applications. */ public final class ByteUtils { - /** - * Serializes a BigInteger as an unsigned integer of the given length. - * @param value the integer to serialize - * @param length the length of the byte[] to return - * @return the value as an unsigned integer - */ - public static byte[] intToLength(BigInteger value, int length) { - byte[] data = value.toByteArray(); - if (data.length == length) { - return data; - } else if (data.length < length) { - byte[] padded = new byte[length]; - System.arraycopy(data, 0, padded, length - data.length, data.length); - return padded; - } else if (data.length == length + 1 && data[0] == 0) { - // BigInteger may have a leading zero, since it's signed. - return Arrays.copyOfRange(data, 1, data.length); - } else { - throw new IllegalArgumentException("value is too large to be represented in " + length + " bytes"); - } + /** + * Serializes a BigInteger as an unsigned integer of the given length. + * + * @param value the integer to serialize + * @param length the length of the byte[] to return + * @return the value as an unsigned integer + */ + public static byte[] intToLength(BigInteger value, int length) { + byte[] data = value.toByteArray(); + if (data.length == length) { + return data; + } else if (data.length < length) { + byte[] padded = new byte[length]; + System.arraycopy(data, 0, padded, length - data.length, data.length); + return padded; + } else if (data.length == length + 1 && data[0] == 0) { + // BigInteger may have a leading zero, since it's signed. + return Arrays.copyOfRange(data, 1, data.length); + } else { + throw new IllegalArgumentException( + "value is too large to be represented in " + length + " bytes"); } + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/util/Callback.java b/core/src/main/java/com/yubico/yubikit/core/util/Callback.java index 8c8b99e4..45d1ea8a 100755 --- a/core/src/main/java/com/yubico/yubikit/core/util/Callback.java +++ b/core/src/main/java/com/yubico/yubikit/core/util/Callback.java @@ -22,5 +22,5 @@ * @param the type of the value expected as input to the callback. */ public interface Callback { - void invoke(T value); + void invoke(T value); } diff --git a/core/src/main/java/com/yubico/yubikit/core/util/NdefUtils.java b/core/src/main/java/com/yubico/yubikit/core/util/NdefUtils.java index d7292e34..8a4f7afa 100755 --- a/core/src/main/java/com/yubico/yubikit/core/util/NdefUtils.java +++ b/core/src/main/java/com/yubico/yubikit/core/util/NdefUtils.java @@ -20,56 +20,57 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; -/** - * Parser that helps to extract OTP from nfc tag. - */ +/** Parser that helps to extract OTP from nfc tag. */ public class NdefUtils { - private static final byte[] HEADER = new byte[]{(byte) 0xd1, 0x55, 0x04}; // NDEF, URI, HTTPS - private static final byte NDEF_RECORD = (byte) 0xd1; - private static final byte TYPE_LENGTH = 0x01; - private static final byte URL_TYPE = (byte) 0x55; - private static final byte HTTPS_PROTOCOL = (byte) 0x04; - private static final byte[] DOMAIN = "my.yubico.com".getBytes(StandardCharsets.UTF_8); - private static final byte[] NEO_REMAINDER_PREFIX = "/neo/".getBytes(StandardCharsets.UTF_8); + private static final byte[] HEADER = new byte[] {(byte) 0xd1, 0x55, 0x04}; // NDEF, URI, HTTPS + private static final byte NDEF_RECORD = (byte) 0xd1; + private static final byte TYPE_LENGTH = 0x01; + private static final byte URL_TYPE = (byte) 0x55; + private static final byte HTTPS_PROTOCOL = (byte) 0x04; + private static final byte[] DOMAIN = "my.yubico.com".getBytes(StandardCharsets.UTF_8); + private static final byte[] NEO_REMAINDER_PREFIX = "/neo/".getBytes(StandardCharsets.UTF_8); - /** - * Returns the String payload portion (an OTP, for example) of a YubiKey's NDEF data. - */ - public static String getNdefPayload(byte[] ndefData) { - return new String(getNdefPayloadBytes(ndefData), StandardCharsets.UTF_8); - } + /** Returns the String payload portion (an OTP, for example) of a YubiKey's NDEF data. */ + public static String getNdefPayload(byte[] ndefData) { + return new String(getNdefPayloadBytes(ndefData), StandardCharsets.UTF_8); + } - /** - * Returns the byte payload portion (static password scan codes, for example) of a YubiKey's NDEF data. - */ - public static byte[] getNdefPayloadBytes(byte[] ndefData) { - ByteBuffer data = ByteBuffer.wrap(ndefData); - byte record = data.get(); - byte typeLength = data.get(); - int dataLength = 0xff & data.get() - typeLength; - byte recordType = data.get(); - byte protocol = data.get(); + /** + * Returns the byte payload portion (static password scan codes, for example) of a YubiKey's NDEF + * data. + */ + public static byte[] getNdefPayloadBytes(byte[] ndefData) { + ByteBuffer data = ByteBuffer.wrap(ndefData); + byte record = data.get(); + byte typeLength = data.get(); + int dataLength = 0xff & data.get() - typeLength; + byte recordType = data.get(); + byte protocol = data.get(); - if (record != NDEF_RECORD || typeLength != TYPE_LENGTH || recordType != URL_TYPE || protocol != HTTPS_PROTOCOL) { - throw new IllegalArgumentException("Not a HTTPS URL NDEF record"); - } + if (record != NDEF_RECORD + || typeLength != TYPE_LENGTH + || recordType != URL_TYPE + || protocol != HTTPS_PROTOCOL) { + throw new IllegalArgumentException("Not a HTTPS URL NDEF record"); + } - byte[] domain = new byte[DOMAIN.length]; - data.get(domain); - if (!Arrays.equals(DOMAIN, domain)) { - throw new IllegalArgumentException("Incorrect URL domain"); - } - byte[] remaining = new byte[dataLength - DOMAIN.length]; - data.get(remaining); + byte[] domain = new byte[DOMAIN.length]; + data.get(domain); + if (!Arrays.equals(DOMAIN, domain)) { + throw new IllegalArgumentException("Incorrect URL domain"); + } + byte[] remaining = new byte[dataLength - DOMAIN.length]; + data.get(remaining); - if (Arrays.equals(NEO_REMAINDER_PREFIX, Arrays.copyOf(remaining, NEO_REMAINDER_PREFIX.length))) { - return Arrays.copyOfRange(remaining, NEO_REMAINDER_PREFIX.length, remaining.length); - } - for (int i = 0; i < remaining.length; i++) { - if (remaining[i] == '#') { - return Arrays.copyOfRange(remaining, i + 1, remaining.length); - } - } - throw new IllegalArgumentException("Incorrect URL format"); + if (Arrays.equals( + NEO_REMAINDER_PREFIX, Arrays.copyOf(remaining, NEO_REMAINDER_PREFIX.length))) { + return Arrays.copyOfRange(remaining, NEO_REMAINDER_PREFIX.length, remaining.length); + } + for (int i = 0; i < remaining.length; i++) { + if (remaining[i] == '#') { + return Arrays.copyOfRange(remaining, i + 1, remaining.length); + } } + throw new IllegalArgumentException("Incorrect URL format"); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/util/Pair.java b/core/src/main/java/com/yubico/yubikit/core/util/Pair.java index b05bc3b8..43f74885 100755 --- a/core/src/main/java/com/yubico/yubikit/core/util/Pair.java +++ b/core/src/main/java/com/yubico/yubikit/core/util/Pair.java @@ -15,15 +15,13 @@ */ package com.yubico.yubikit.core.util; -/** - * Utility class to hold two values. - */ +/** Utility class to hold two values. */ public class Pair { - public final A first; - public final B second; + public final A first; + public final B second; - public Pair(A first, B second) { - this.first = first; - this.second = second; - } + public Pair(A first, B second) { + this.first = first; + this.second = second; + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/util/RandomUtils.java b/core/src/main/java/com/yubico/yubikit/core/util/RandomUtils.java index 990a3cbf..9d4f864a 100755 --- a/core/src/main/java/com/yubico/yubikit/core/util/RandomUtils.java +++ b/core/src/main/java/com/yubico/yubikit/core/util/RandomUtils.java @@ -18,27 +18,24 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -/** - * Utility class to generate random data. - */ +/** Utility class to generate random data. */ public class RandomUtils { - private static final SecureRandom secureRandom = new SecureRandom(); - /** - * Returns a byte array containing random values. - */ - @SuppressWarnings("NewApi") - public static byte[] getRandomBytes(int length) { - byte[] bytes = new byte[length]; - try { - SecureRandom.getInstanceStrong().nextBytes(bytes); - } catch (NoSuchMethodError | NoSuchAlgorithmException e) { - // Fallback for older Android versions - secureRandom.nextBytes(bytes); - } - return bytes; - } + private static final SecureRandom secureRandom = new SecureRandom(); - private RandomUtils() { - throw new IllegalStateException(); + /** Returns a byte array containing random values. */ + @SuppressWarnings("NewApi") + public static byte[] getRandomBytes(int length) { + byte[] bytes = new byte[length]; + try { + SecureRandom.getInstanceStrong().nextBytes(bytes); + } catch (NoSuchMethodError | NoSuchAlgorithmException e) { + // Fallback for older Android versions + secureRandom.nextBytes(bytes); } + return bytes; + } + + private RandomUtils() { + throw new IllegalStateException(); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/util/Result.java b/core/src/main/java/com/yubico/yubikit/core/util/Result.java index 8a168972..505eaec4 100755 --- a/core/src/main/java/com/yubico/yubikit/core/util/Result.java +++ b/core/src/main/java/com/yubico/yubikit/core/util/Result.java @@ -17,7 +17,6 @@ package com.yubico.yubikit.core.util; import java.util.concurrent.Callable; - import javax.annotation.Nullable; /** @@ -27,72 +26,66 @@ * @param the type of the exception thrown */ public class Result { - @Nullable - private final T value; - @Nullable - private final E error; + @Nullable private final T value; + @Nullable private final E error; - private Result(@Nullable T value, @Nullable E error) { - this.value = value; - this.error = error; - } + private Result(@Nullable T value, @Nullable E error) { + this.value = value; + this.error = error; + } - /** - * Gets the held value, if the Result is successful, or throws the error on failure. - * - * @return the held value on success - * @throws E the held exception on failure - */ - public T getValue() throws E { - if (value != null) { - return value; - } - assert error != null; - throw error; + /** + * Gets the held value, if the Result is successful, or throws the error on failure. + * + * @return the held value on success + * @throws E the held exception on failure + */ + public T getValue() throws E { + if (value != null) { + return value; } + assert error != null; + throw error; + } - /** - * Checks if the Result is successful. - */ - public boolean isSuccess() { - return value != null; - } + /** Checks if the Result is successful. */ + public boolean isSuccess() { + return value != null; + } - /** - * Checks if the Result is a failure. - */ - public boolean isError() { - return error != null; - } + /** Checks if the Result is a failure. */ + public boolean isError() { + return error != null; + } - /** - * Constructs a Result for a value (success). - * - * @param value the value to hold - */ - public static Result success(T value) { - return new Result<>(value, null); - } + /** + * Constructs a Result for a value (success). + * + * @param value the value to hold + */ + public static Result success(T value) { + return new Result<>(value, null); + } - /** - * Constructs a Result for an Exception (failure). - * - * @param error the error to hold - */ - public static Result failure(E error) { - return new Result<>(null, error); - } + /** + * Constructs a Result for an Exception (failure). + * + * @param error the error to hold + */ + public static Result failure(E error) { + return new Result<>(null, error); + } - /** - * Runs the given callable, creating a Result of its value, if run successfully, or its Exception. - * - * @param call callable to invoke, resulting in a value - */ - public static Result of(Callable call) { - try { - return Result.success(call.call()); - } catch (Exception e) { - return Result.failure(e); - } + /** + * Runs the given callable, creating a Result of its value, if run successfully, or its Exception. + * + * @param call callable to invoke, resulting in a value + */ + public static Result of(Callable call) { + try { + return Result.success(call.call()); + } catch (Exception e) { + return Result.failure(e); } + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/util/StringUtils.java b/core/src/main/java/com/yubico/yubikit/core/util/StringUtils.java index 4daa2453..1bc9078e 100755 --- a/core/src/main/java/com/yubico/yubikit/core/util/StringUtils.java +++ b/core/src/main/java/com/yubico/yubikit/core/util/StringUtils.java @@ -16,37 +16,35 @@ package com.yubico.yubikit.core.util; -/** - * Utility methods for Strings. - */ +/** Utility methods for Strings. */ public class StringUtils { - /** - * Helper method that convert byte array into string for logging - * - * @param byteArray array of bytes - * @return string representation of byte array - */ - public static String bytesToHex(byte[] byteArray) { - return bytesToHex(byteArray, 0, byteArray.length); - } + /** + * Helper method that convert byte array into string for logging + * + * @param byteArray array of bytes + * @return string representation of byte array + */ + public static String bytesToHex(byte[] byteArray) { + return bytesToHex(byteArray, 0, byteArray.length); + } - /** - * Helper method that convert byte array into string for logging - * - * @param byteArray array of bytes - * @param offset the offset within byteArray - * @param size the size of array - * @return string representation of byte array - */ - public static String bytesToHex(byte[] byteArray, int offset, int size) { - StringBuilder sb = new StringBuilder(); - for (int i = offset; i < size; i++) { - sb.append(String.format("%02x ", byteArray[i])); - } - return sb.toString(); + /** + * Helper method that convert byte array into string for logging + * + * @param byteArray array of bytes + * @param offset the offset within byteArray + * @param size the size of array + * @return string representation of byte array + */ + public static String bytesToHex(byte[] byteArray, int offset, int size) { + StringBuilder sb = new StringBuilder(); + for (int i = offset; i < size; i++) { + sb.append(String.format("%02x ", byteArray[i])); } + return sb.toString(); + } - private StringUtils() { - throw new IllegalStateException(); - } + private StringUtils() { + throw new IllegalStateException(); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/util/Tlv.java b/core/src/main/java/com/yubico/yubikit/core/util/Tlv.java index e55f20f8..e6323276 100755 --- a/core/src/main/java/com/yubico/yubikit/core/util/Tlv.java +++ b/core/src/main/java/com/yubico/yubikit/core/util/Tlv.java @@ -21,129 +21,120 @@ import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Locale; - import javax.annotation.Nullable; /** * Tag, length, Value structure that helps to parse APDU response data. - * This class handles BER-TLV encoded data with determinate length. + * + *

This class handles BER-TLV encoded data with determinate length. */ public class Tlv { - private final int tag; - private final int length; - private final byte[] bytes; - private final int offset; - - /** - * Creates a new Tlv given a tag and a value. - */ - public Tlv(int tag, @Nullable byte[] value) { - this.tag = tag; - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - - byte[] tagBytes = BigInteger.valueOf(tag).toByteArray(); - int stripLeading = tagBytes[0] == 0 ? 1 : 0; - stream.write(tagBytes, stripLeading, tagBytes.length - stripLeading); - - length = value == null ? 0 : value.length; - if (length < 0x80) { - stream.write(length); - } else { - byte[] lnBytes = BigInteger.valueOf(length).toByteArray(); - stripLeading = lnBytes[0] == 0 ? 1 : 0; - stream.write(0x80 | lnBytes.length - stripLeading); - stream.write(lnBytes, stripLeading, lnBytes.length - stripLeading); - } - - offset = stream.size(); - if (value != null) { - stream.write(value, 0, length); - } - bytes = stream.toByteArray(); - } - - /** - * Returns the tag. - */ - public int getTag() { - return tag; + private final int tag; + private final int length; + private final byte[] bytes; + private final int offset; + + /** Creates a new Tlv given a tag and a value. */ + public Tlv(int tag, @Nullable byte[] value) { + this.tag = tag; + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + + byte[] tagBytes = BigInteger.valueOf(tag).toByteArray(); + int stripLeading = tagBytes[0] == 0 ? 1 : 0; + stream.write(tagBytes, stripLeading, tagBytes.length - stripLeading); + + length = value == null ? 0 : value.length; + if (length < 0x80) { + stream.write(length); + } else { + byte[] lnBytes = BigInteger.valueOf(length).toByteArray(); + stripLeading = lnBytes[0] == 0 ? 1 : 0; + stream.write(0x80 | lnBytes.length - stripLeading); + stream.write(lnBytes, stripLeading, lnBytes.length - stripLeading); } - /** - * returns the value. - */ - public byte[] getValue() { - return Arrays.copyOfRange(bytes, offset, offset + length); + offset = stream.size(); + if (value != null) { + stream.write(value, 0, length); } - - /** - * Returns the length of the value. - */ - public int getLength() { - return length; + bytes = stream.toByteArray(); + } + + /** Returns the tag. */ + public int getTag() { + return tag; + } + + /** returns the value. */ + public byte[] getValue() { + return Arrays.copyOfRange(bytes, offset, offset + length); + } + + /** Returns the length of the value. */ + public int getLength() { + return length; + } + + /** Returns the Tlv as a BER-TLV encoded byte array. */ + public byte[] getBytes() { + return Arrays.copyOf(bytes, bytes.length); + } + + @Override + public String toString() { + return String.format( + Locale.ROOT, "Tlv(0x%x, %d, %s)", tag, length, StringUtils.bytesToHex(getValue())); + } + + /** + * Parse a Tlv from a BER-TLV encoded byte array. + * + * @param data a byte array containing the TLV encoded data. + * @param offset the offset in data where the TLV data begins. + * @param length the length of the TLV encoded data. + * @return The parsed Tlv + */ + public static Tlv parse(byte[] data, int offset, int length) { + ByteBuffer buffer = ByteBuffer.wrap(data, offset, length); + Tlv tlv = parseFrom(buffer); + if (buffer.hasRemaining()) { + throw new IllegalArgumentException("Extra data remaining"); } - - /** - * Returns the Tlv as a BER-TLV encoded byte array. - */ - public byte[] getBytes() { - return Arrays.copyOf(bytes, bytes.length); + return tlv; + } + + /** + * Parse a Tlv from a BER-TLV encoded byte array. + * + * @param data a byte array containing the TLV encoded data (and nothing more). + * @return The parsed Tlv + */ + public static Tlv parse(byte[] data) { + return parse(data, 0, data.length); + } + + static Tlv parseFrom(ByteBuffer buffer) { + int tag = buffer.get() & 0xFF; + if ((tag & 0x1F) == 0x1F) { // Long form tag + tag = (tag << 8) | (buffer.get() & 0xFF); + while ((tag & 0x80) == 0x80) { + tag = (tag << 8) | (buffer.get() & 0xFF); + } } - @Override - public String toString() { - return String.format(Locale.ROOT, "Tlv(0x%x, %d, %s)", tag, length, StringUtils.bytesToHex(getValue())); + int length = buffer.get() & 0xFF; + if (length == 0x80) { + throw new IllegalArgumentException("Indefinite length not supported"); + } else if (length > 0x80) { + int lengthLn = length - 0x80; + length = 0; + for (int i = 0; i < lengthLn; i++) { + length = (length << 8) | (buffer.get() & 0xff); + } } - /** - * Parse a Tlv from a BER-TLV encoded byte array. - * - * @param data a byte array containing the TLV encoded data. - * @param offset the offset in data where the TLV data begins. - * @param length the length of the TLV encoded data. - * @return The parsed Tlv - */ - public static Tlv parse(byte[] data, int offset, int length) { - ByteBuffer buffer = ByteBuffer.wrap(data, offset, length); - Tlv tlv = parseFrom(buffer); - if (buffer.hasRemaining()) { - throw new IllegalArgumentException("Extra data remaining"); - } - return tlv; - } - - /** - * Parse a Tlv from a BER-TLV encoded byte array. - * - * @param data a byte array containing the TLV encoded data (and nothing more). - * @return The parsed Tlv - */ - public static Tlv parse(byte[] data) { - return parse(data, 0, data.length); - } - - static Tlv parseFrom(ByteBuffer buffer) { - int tag = buffer.get() & 0xFF; - if ((tag & 0x1F) == 0x1F) { // Long form tag - tag = (tag << 8) | (buffer.get() & 0xFF); - while ((tag & 0x80) == 0x80) { - tag = (tag << 8) | (buffer.get() & 0xFF); - } - } - - int length = buffer.get() & 0xFF; - if (length == 0x80) { - throw new IllegalArgumentException("Indefinite length not supported"); - } else if (length > 0x80) { - int lengthLn = length - 0x80; - length = 0; - for (int i = 0; i < lengthLn; i++) { - length = (length << 8) | (buffer.get() & 0xff); - } - } - - byte[] value = new byte[length]; - buffer.get(value); - return new Tlv(tag, value); - } + byte[] value = new byte[length]; + buffer.get(value); + return new Tlv(tag, value); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/util/Tlvs.java b/core/src/main/java/com/yubico/yubikit/core/util/Tlvs.java index 8c578d24..7d80be5c 100755 --- a/core/src/main/java/com/yubico/yubikit/core/util/Tlvs.java +++ b/core/src/main/java/com/yubico/yubikit/core/util/Tlvs.java @@ -17,7 +17,6 @@ package com.yubico.yubikit.core.util; import com.yubico.yubikit.core.application.BadResponseException; - import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -25,90 +24,92 @@ import java.util.List; import java.util.Map; -/** - * Utility methods to encode and decode BER-TLV data. - */ +/** Utility methods to encode and decode BER-TLV data. */ public class Tlvs { - /** - * Decodes a sequence of BER-TLV encoded data into a list of Tlvs. - * - * @param data sequence of TLV encoded data - * @return list of Tlvs - */ - public static List decodeList(byte[] data) { - ByteBuffer buffer = ByteBuffer.wrap(data); - List tlvs = new ArrayList<>(); - while (buffer.hasRemaining()) { - Tlv tlv = Tlv.parseFrom(buffer); - tlvs.add(tlv); - } - return tlvs; + /** + * Decodes a sequence of BER-TLV encoded data into a list of Tlvs. + * + * @param data sequence of TLV encoded data + * @return list of Tlvs + */ + public static List decodeList(byte[] data) { + ByteBuffer buffer = ByteBuffer.wrap(data); + List tlvs = new ArrayList<>(); + while (buffer.hasRemaining()) { + Tlv tlv = Tlv.parseFrom(buffer); + tlvs.add(tlv); } + return tlvs; + } - /** - * Decodes a sequence of BER-TLV encoded data into a mapping of Tag-Value pairs. - *

- * Iteration order is preserved. If the same tag occurs more than once only the latest will be kept. - * - * @param data sequence of TLV encoded data - * @return map of Tag-Value pairs - */ - public static Map decodeMap(byte[] data) { - ByteBuffer buffer = ByteBuffer.wrap(data); - Map tlvs = new LinkedHashMap<>(); - while (buffer.hasRemaining()) { - Tlv tlv = Tlv.parseFrom(buffer); - tlvs.put(tlv.getTag(), tlv.getValue()); - } - return tlvs; + /** + * Decodes a sequence of BER-TLV encoded data into a mapping of Tag-Value pairs. + * + *

Iteration order is preserved. If the same tag occurs more than once only the latest will be + * kept. + * + * @param data sequence of TLV encoded data + * @return map of Tag-Value pairs + */ + public static Map decodeMap(byte[] data) { + ByteBuffer buffer = ByteBuffer.wrap(data); + Map tlvs = new LinkedHashMap<>(); + while (buffer.hasRemaining()) { + Tlv tlv = Tlv.parseFrom(buffer); + tlvs.put(tlv.getTag(), tlv.getValue()); } + return tlvs; + } - /** - * Encodes a List of Tlvs into an array of bytes. - * - * @param list list of Tlvs - * @return the data encoded as a sequence of TLV values - */ - public static byte[] encodeList(Iterable list) { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - for (Tlv tlv : list) { - byte[] tlvBytes = tlv.getBytes(); - stream.write(tlvBytes, 0, tlvBytes.length); - } - return stream.toByteArray(); + /** + * Encodes a List of Tlvs into an array of bytes. + * + * @param list list of Tlvs + * @return the data encoded as a sequence of TLV values + */ + public static byte[] encodeList(Iterable list) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + for (Tlv tlv : list) { + byte[] tlvBytes = tlv.getBytes(); + stream.write(tlvBytes, 0, tlvBytes.length); } + return stream.toByteArray(); + } - /** - * Encodes a Map of Tag-Value pairs into an array of bytes. - * NOTE: If order is important use a Map implementation that preserves order, such as LinkedHashMap. - * - * @param map the tag-value mappings - * @return the data encoded as a sequence of TLV values - */ - public static byte[] encodeMap(Map map) { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - for (Map.Entry entry : map.entrySet()) { - Tlv tlv = new Tlv(entry.getKey(), entry.getValue()); - byte[] tlvBytes = tlv.getBytes(); - stream.write(tlvBytes, 0, tlvBytes.length); - } - return stream.toByteArray(); + /** + * Encodes a Map of Tag-Value pairs into an array of bytes. + * + *

NOTE: If order is important use a Map implementation that preserves order, such as + * LinkedHashMap. + * + * @param map the tag-value mappings + * @return the data encoded as a sequence of TLV values + */ + public static byte[] encodeMap(Map map) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + for (Map.Entry entry : map.entrySet()) { + Tlv tlv = new Tlv(entry.getKey(), entry.getValue()); + byte[] tlvBytes = tlv.getBytes(); + stream.write(tlvBytes, 0, tlvBytes.length); } + return stream.toByteArray(); + } - /** - * Decode a single TLV encoded object, returning only the value. - * - * @param expectedTag the expected tag value of the given TLV data - * @param tlvData the TLV data - * @return the value of the TLV - * @throws BadResponseException if the TLV tag differs from expectedTag - */ - public static byte[] unpackValue(int expectedTag, byte[] tlvData) throws BadResponseException { - Tlv tlv = Tlv.parse(tlvData, 0, tlvData.length); - if (tlv.getTag() != expectedTag) { - throw new BadResponseException(String.format("Expected tag: %02x, got %02x", expectedTag, tlv.getTag())); - } - return tlv.getValue(); + /** + * Decode a single TLV encoded object, returning only the value. + * + * @param expectedTag the expected tag value of the given TLV data + * @param tlvData the TLV data + * @return the value of the TLV + * @throws BadResponseException if the TLV tag differs from expectedTag + */ + public static byte[] unpackValue(int expectedTag, byte[] tlvData) throws BadResponseException { + Tlv tlv = Tlv.parse(tlvData, 0, tlvData.length); + if (tlv.getTag() != expectedTag) { + throw new BadResponseException( + String.format("Expected tag: %02x, got %02x", expectedTag, tlv.getTag())); } + return tlv.getValue(); + } } diff --git a/core/src/main/java/com/yubico/yubikit/core/util/package-info.java b/core/src/main/java/com/yubico/yubikit/core/util/package-info.java index 670ba905..b3fa5995 100755 --- a/core/src/main/java/com/yubico/yubikit/core/util/package-info.java +++ b/core/src/main/java/com/yubico/yubikit/core/util/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.core.util; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/core/src/test/java/com/yubico/yubikit/core/VersionTest.java b/core/src/test/java/com/yubico/yubikit/core/VersionTest.java index 5b725393..be732044 100755 --- a/core/src/test/java/com/yubico/yubikit/core/VersionTest.java +++ b/core/src/test/java/com/yubico/yubikit/core/VersionTest.java @@ -15,63 +15,65 @@ */ package com.yubico.yubikit.core; +import java.util.Arrays; import org.junit.Assert; import org.junit.Test; -import java.util.Arrays; - public class VersionTest { - @Test - public void testCompare() { - Version v100 = new Version(1, 0, 0); - Version v102 = new Version(1, 0, 2); - Version v110 = new Version(1, 1, 0); - Version v123 = new Version(1, 2, 3); + @Test + public void testCompare() { + Version v100 = new Version(1, 0, 0); + Version v102 = new Version(1, 0, 2); + Version v110 = new Version(1, 1, 0); + Version v123 = new Version(1, 2, 3); - Assert.assertTrue(v100.isAtLeast(1, 0, 0)); - Assert.assertFalse(v100.isAtLeast(2, 0, 0)); + Assert.assertTrue(v100.isAtLeast(1, 0, 0)); + Assert.assertFalse(v100.isAtLeast(2, 0, 0)); - Assert.assertTrue(v100.isLessThan(1, 0, 1)); - Assert.assertTrue(v100.isLessThan(1, 1, 0)); - Assert.assertTrue(v100.isLessThan(2, 0, 0)); - Assert.assertFalse(v100.isLessThan(1, 0, 0)); + Assert.assertTrue(v100.isLessThan(1, 0, 1)); + Assert.assertTrue(v100.isLessThan(1, 1, 0)); + Assert.assertTrue(v100.isLessThan(2, 0, 0)); + Assert.assertFalse(v100.isLessThan(1, 0, 0)); - Assert.assertTrue(v102.isAtLeast(1, 0, 0)); - Assert.assertTrue(v102.isLessThan(1, 1, 0)); - Assert.assertFalse(v102.isLessThan(1, 0, 2)); + Assert.assertTrue(v102.isAtLeast(1, 0, 0)); + Assert.assertTrue(v102.isLessThan(1, 1, 0)); + Assert.assertFalse(v102.isLessThan(1, 0, 2)); - Assert.assertTrue(v110.compareTo(v123) < 0); - Assert.assertTrue(v123.compareTo(v102) > 0); + Assert.assertTrue(v110.compareTo(v123) < 0); + Assert.assertTrue(v123.compareTo(v102) > 0); - //noinspection EqualsWithItself - Assert.assertEquals(0, v123.compareTo(v123)); - } + //noinspection EqualsWithItself + Assert.assertEquals(0, v123.compareTo(v123)); + } - @Test - public void testOrdering() { - Version[] versions = new Version[]{ - new Version(2, 0, 1), - new Version(1, 0, 0), - new Version(1, 2, 3), - new Version(1, 0, 1), - new Version(2, 0, 0), - new Version(2, 1, 0), - new Version(1, 1, 0), - new Version(3, 0, 0), - new Version(1, 0, 0), + @Test + public void testOrdering() { + Version[] versions = + new Version[] { + new Version(2, 0, 1), + new Version(1, 0, 0), + new Version(1, 2, 3), + new Version(1, 0, 1), + new Version(2, 0, 0), + new Version(2, 1, 0), + new Version(1, 1, 0), + new Version(3, 0, 0), + new Version(1, 0, 0), }; - Arrays.sort(versions); - Assert.assertArrayEquals(new Version[]{ - new Version(1, 0, 0), - new Version(1, 0, 0), - new Version(1, 0, 1), - new Version(1, 1, 0), - new Version(1, 2, 3), - new Version(2, 0, 0), - new Version(2, 0, 1), - new Version(2, 1, 0), - new Version(3, 0, 0), - }, versions); - } + Arrays.sort(versions); + Assert.assertArrayEquals( + new Version[] { + new Version(1, 0, 0), + new Version(1, 0, 0), + new Version(1, 0, 1), + new Version(1, 1, 0), + new Version(1, 2, 3), + new Version(2, 0, 0), + new Version(2, 0, 1), + new Version(2, 1, 0), + new Version(3, 0, 0), + }, + versions); + } } diff --git a/core/src/test/java/com/yubico/yubikit/core/keys/PrivateKeyValuesTest.java b/core/src/test/java/com/yubico/yubikit/core/keys/PrivateKeyValuesTest.java index 3f4ca9c3..9e424fed 100644 --- a/core/src/test/java/com/yubico/yubikit/core/keys/PrivateKeyValuesTest.java +++ b/core/src/test/java/com/yubico/yubikit/core/keys/PrivateKeyValuesTest.java @@ -17,24 +17,24 @@ package com.yubico.yubikit.core.keys; import com.yubico.yubikit.core.internal.codec.Base64; - import org.junit.Test; @SuppressWarnings("SpellCheckingInspection") public class PrivateKeyValuesTest { - @Test - public void testParsePkcs8RsaKeyValues() { - PrivateKeyValues.Rsa.parsePkcs8RsaKeyValues(Base64.fromUrlSafeString("MIICdQIBADANBgkqhk" + - "iG9w0BAQEFAASCAl8wggJbAgEAAoGBALWeZ0E5O2l_iHfck9mokf1iWH2eZDWQoJoQKUOAeVoKUecNp" + - "250J5tL3EHONqWoF6VLO-B-6jTET4Iz97BeUj7gOJHmEw-nqFfguTVmNeeiZ711TNYNpF7kwW7yWghW" + - "G-Q7iQEoMXfY3x4BL33H2gKRWtMHK66GJViL1l9s3qDXAgMBAAECgYBO753pFzrfS3LAxbns6_snqcr" + - "ULjdXoJhs3YFRuVEE9V9LkP-oXguoz3vXjgzqSvib-ur3U7HvZTM5X-TTXutXdQ5CyORLLtXEZcyCKQ" + - "I9ihH5fSNJRWRbJ3xe-xi5NANRkRDkro7tm4a5ZD4PYvO4r29yVB5PXlMkOTLoxNSwwQJBAN5lW93Ag" + - "i9Ge5B2-B2EnKSlUvj0-jJBkHYAFTiHyTZVEj6baeHBvJklhVczpWvTXb6Nr8cjAKVshFbdQoBwHmkC" + - "QQDRD7djZGIWH1Lz0rkL01nDj4z4QYMgUs3AQhnrXPBjEgNzphtJ2u7QrCSOBQQHlmAPBDJ_MTxFJMz" + - "DIJGDA10_AkATJjEZz_ilr3D2SHgmuoNuXdneG-HrL-ALeQhavL5jkkGm6GTejnr5yNRJZOYKecGppb" + - "OL9wSYOdbPT-_o9T55AkATXCY6cRBYRhxTcf8q5i6Y2pFOaBqxgpmFJVnrHtcwBXoGWqqKQ1j8QAS-l" + - "h5SaY2JtnTKrI-NQ6Qmqbxv6n7XAkBkhLO7pplInVh2WjqXOV4ZAoOAAJlfpG5-z6mWzCZ9-286OJQL" + - "r6OVVQMcYExUO9yVocZQX-4XqEIF0qAB7m31")); - } + @Test + public void testParsePkcs8RsaKeyValues() { + PrivateKeyValues.Rsa.parsePkcs8RsaKeyValues( + Base64.fromUrlSafeString( + "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBALWeZ0E5O2l_iHfck9mokf1iWH2eZDWQoJoQK" + + "UOAeVoKUecNp250J5tL3EHONqWoF6VLO-B-6jTET4Iz97BeUj7gOJHmEw-nqFfguTVmNeeiZ711TNYN" + + "pF7kwW7yWghWG-Q7iQEoMXfY3x4BL33H2gKRWtMHK66GJViL1l9s3qDXAgMBAAECgYBO753pFzrfS3L" + + "Axbns6_snqcrULjdXoJhs3YFRuVEE9V9LkP-oXguoz3vXjgzqSvib-ur3U7HvZTM5X-TTXutXdQ5CyO" + + "RLLtXEZcyCKQI9ihH5fSNJRWRbJ3xe-xi5NANRkRDkro7tm4a5ZD4PYvO4r29yVB5PXlMkOTLoxNSww" + + "QJBAN5lW93Agi9Ge5B2-B2EnKSlUvj0-jJBkHYAFTiHyTZVEj6baeHBvJklhVczpWvTXb6Nr8cjAKVs" + + "hFbdQoBwHmkCQQDRD7djZGIWH1Lz0rkL01nDj4z4QYMgUs3AQhnrXPBjEgNzphtJ2u7QrCSOBQQHlmA" + + "PBDJ_MTxFJMzDIJGDA10_AkATJjEZz_ilr3D2SHgmuoNuXdneG-HrL-ALeQhavL5jkkGm6GTejnr5yN" + + "RJZOYKecGppbOL9wSYOdbPT-_o9T55AkATXCY6cRBYRhxTcf8q5i6Y2pFOaBqxgpmFJVnrHtcwBXoGW" + + "qqKQ1j8QAS-lh5SaY2JtnTKrI-NQ6Qmqbxv6n7XAkBkhLO7pplInVh2WjqXOV4ZAoOAAJlfpG5-z6mW" + + "zCZ9-286OJQLr6OVVQMcYExUO9yVocZQX-4XqEIF0qAB7m31")); + } } diff --git a/core/src/test/java/com/yubico/yubikit/core/keys/PublicKeyValuesTest.java b/core/src/test/java/com/yubico/yubikit/core/keys/PublicKeyValuesTest.java index 1085b20c..1c39939e 100644 --- a/core/src/test/java/com/yubico/yubikit/core/keys/PublicKeyValuesTest.java +++ b/core/src/test/java/com/yubico/yubikit/core/keys/PublicKeyValuesTest.java @@ -20,55 +20,77 @@ import static org.hamcrest.MatcherAssert.assertThat; import com.yubico.yubikit.testing.Codec; - -import org.junit.Test; - import java.math.BigInteger; import java.security.NoSuchAlgorithmException; import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.InvalidKeySpecException; +import org.junit.Test; public class PublicKeyValuesTest { - @Test - public void testDecodeP256Key() throws InvalidKeySpecException, NoSuchAlgorithmException { - @SuppressWarnings("SpellCheckingInspection") - byte[] encoded = Codec.fromHex("046B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C2964FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5"); - ECPublicKey key = PublicKeyValues.Ec.fromEncodedPoint(EllipticCurveValues.SECP256R1, encoded).toPublicKey(); - assertThat(key.getAlgorithm(), equalTo("EC")); - assertThat(key.getParams().getCurve().getField().getFieldSize(), equalTo(256)); - } + @Test + public void testDecodeP256Key() throws InvalidKeySpecException, NoSuchAlgorithmException { + @SuppressWarnings("SpellCheckingInspection") + byte[] encoded = + Codec.fromHex( + "046B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C2964FE342E2FE1A7F9B8EE" + + "7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5"); + ECPublicKey key = + PublicKeyValues.Ec.fromEncodedPoint(EllipticCurveValues.SECP256R1, encoded).toPublicKey(); + assertThat(key.getAlgorithm(), equalTo("EC")); + assertThat(key.getParams().getCurve().getField().getFieldSize(), equalTo(256)); + } - @Test - public void testDecodeP384Key() throws InvalidKeySpecException, NoSuchAlgorithmException { - @SuppressWarnings("SpellCheckingInspection") - byte[] encoded = Codec.fromHex("0408D999057BA3D2D969260045C55B97F089025959A6F434D651D207D19FB96E9E4FE0E86EBE0E64F85B96A9C75295DF618E80F1FA5B1B3CEDB7BFE8DFFD6DBA74B275D875BC6CC43E904E505F256AB4255FFD43E94D39E22D61501E700A940E80"); - ECPublicKey key = PublicKeyValues.Ec.fromEncodedPoint(EllipticCurveValues.SECP384R1, encoded).toPublicKey(); - assertThat(key.getAlgorithm(), equalTo("EC")); - assertThat(key.getParams().getCurve().getField().getFieldSize(), equalTo(384)); - } + @Test + public void testDecodeP384Key() throws InvalidKeySpecException, NoSuchAlgorithmException { + @SuppressWarnings("SpellCheckingInspection") + byte[] encoded = + Codec.fromHex( + "0408D999057BA3D2D969260045C55B97F089025959A6F434D651D207D19FB96E9E4FE0E86EBE0E64F85B9" + + "6A9C75295DF618E80F1FA5B1B3CEDB7BFE8DFFD6DBA74B275D875BC6CC43E904E505F256AB4255F" + + "FD43E94D39E22D61501E700A940E80"); + ECPublicKey key = + PublicKeyValues.Ec.fromEncodedPoint(EllipticCurveValues.SECP384R1, encoded).toPublicKey(); + assertThat(key.getAlgorithm(), equalTo("EC")); + assertThat(key.getParams().getCurve().getField().getFieldSize(), equalTo(384)); + } - @Test - public void testDecodeRsa1024() throws InvalidKeySpecException, NoSuchAlgorithmException { - @SuppressWarnings("SpellCheckingInspection") - BigInteger modulus = new BigInteger(Codec.fromHex("00C061DB5C051CE961F42898068E084D81EAB3245A6884CF8F8B379587E81C87A96CD4DC83FED14DB5EB6AC60B173797F6692B93AC285CDBD4F91F4968E65CDA579F82D2071ADFFE85F5FF424E8D9A33BFAC1B56C0975BC5B15710F475D45923880575F15B326314251C4DA5C9640EF240F3EF49E61398F700449F16C6F06D532D")); - BigInteger exponent = BigInteger.valueOf(65537); - RSAPublicKey key = new PublicKeyValues.Rsa(modulus, exponent).toPublicKey(); - assertThat(key.getAlgorithm(), equalTo("RSA")); - assertThat(key.getModulus(), equalTo(modulus)); - assertThat(key.getPublicExponent(), equalTo(exponent)); - assertThat(key.getModulus().bitLength(), equalTo(1024)); - } + @Test + public void testDecodeRsa1024() throws InvalidKeySpecException, NoSuchAlgorithmException { + @SuppressWarnings("SpellCheckingInspection") + BigInteger modulus = + new BigInteger( + Codec.fromHex( + "00C061DB5C051CE961F42898068E084D81EAB3245A6884CF8F8B379587E81C87A96CD4DC83FED14DB" + + "5EB6AC60B173797F6692B93AC285CDBD4F91F4968E65CDA579F82D2071ADFFE85F5FF424E8D" + + "9A33BFAC1B56C0975BC5B15710F475D45923880575F15B326314251C4DA5C9640EF240F3EF4" + + "9E61398F700449F16C6F06D532D")); + BigInteger exponent = BigInteger.valueOf(65537); + RSAPublicKey key = new PublicKeyValues.Rsa(modulus, exponent).toPublicKey(); + assertThat(key.getAlgorithm(), equalTo("RSA")); + assertThat(key.getModulus(), equalTo(modulus)); + assertThat(key.getPublicExponent(), equalTo(exponent)); + assertThat(key.getModulus().bitLength(), equalTo(1024)); + } - @Test - public void testDecodeRsa2048() throws InvalidKeySpecException, NoSuchAlgorithmException { - @SuppressWarnings("SpellCheckingInspection") - BigInteger modulus = new BigInteger(Codec.fromHex("00C6FC5B4D5C28B9CDD9047C5481B1F6A6A66683E3B9566E91CBBC9E852EAD96796C914A92315C1B408045270D3C672FC7DA97F2258DBDE0681BD4E5D1112EEBB75AACDC712E62FCD4391513AE867C0E3C70B77032FBBEF774AADE544C6D76B0D296FEC3A5E2BF8ED7BFD3A0F9E48CA60F4CD36162DC3AEE6A0CC47E6BA92704E88E6A110622B3E9FC0C7CAA083A9D93BEB2902F16D06261751E5FA5B8F65E56A6C37B4EA27704AC2FCC7309211022ECFF04BF874A33ACB905699A40A617AF95EDE3308B3B438BFA888B5E82E3CFA7D403E2D32A7B554736ED947FC245943B656B1893032B82F82B6CAFB65BC491AFC645CD676B776F61A0B99FCB990606DA43E5")); - BigInteger exponent = BigInteger.valueOf(65537); - RSAPublicKey key = new PublicKeyValues.Rsa(modulus, exponent).toPublicKey(); - assertThat(key.getAlgorithm(), equalTo("RSA")); - assertThat(key.getModulus(), equalTo(modulus)); - assertThat(key.getPublicExponent(), equalTo(exponent)); - assertThat(key.getModulus().bitLength(), equalTo(2048)); - } + @Test + public void testDecodeRsa2048() throws InvalidKeySpecException, NoSuchAlgorithmException { + @SuppressWarnings("SpellCheckingInspection") + BigInteger modulus = + new BigInteger( + Codec.fromHex( + "00C6FC5B4D5C28B9CDD9047C5481B1F6A6A66683E3B9566E91CBBC9E852EAD96796C914A92315C1B4" + + "08045270D3C672FC7DA97F2258DBDE0681BD4E5D1112EEBB75AACDC712E62FCD4391513AE86" + + "7C0E3C70B77032FBBEF774AADE544C6D76B0D296FEC3A5E2BF8ED7BFD3A0F9E48CA60F4CD36" + + "162DC3AEE6A0CC47E6BA92704E88E6A110622B3E9FC0C7CAA083A9D93BEB2902F16D0626175" + + "1E5FA5B8F65E56A6C37B4EA27704AC2FCC7309211022ECFF04BF874A33ACB905699A40A617A" + + "F95EDE3308B3B438BFA888B5E82E3CFA7D403E2D32A7B554736ED947FC245943B656B189303" + + "2B82F82B6CAFB65BC491AFC645CD676B776F61A0B99FCB990606DA43E5")); + BigInteger exponent = BigInteger.valueOf(65537); + RSAPublicKey key = new PublicKeyValues.Rsa(modulus, exponent).toPublicKey(); + assertThat(key.getAlgorithm(), equalTo("RSA")); + assertThat(key.getModulus(), equalTo(modulus)); + assertThat(key.getPublicExponent(), equalTo(exponent)); + assertThat(key.getModulus().bitLength(), equalTo(2048)); + } } diff --git a/core/src/test/java/com/yubico/yubikit/core/otp/ChecksumUtilsTest.java b/core/src/test/java/com/yubico/yubikit/core/otp/ChecksumUtilsTest.java index 9ec38c79..1f570012 100755 --- a/core/src/test/java/com/yubico/yubikit/core/otp/ChecksumUtilsTest.java +++ b/core/src/test/java/com/yubico/yubikit/core/otp/ChecksumUtilsTest.java @@ -15,54 +15,82 @@ */ package com.yubico.yubikit.core.otp; -import org.junit.Assert; -import org.junit.Test; - import java.nio.ByteBuffer; import java.nio.ByteOrder; +import org.junit.Assert; +import org.junit.Test; public class ChecksumUtilsTest { - @Test - public void testCrc1() { - byte[] data = {0x0, 0x1, 0x2, 0x3, 0x4}; - short crc = ChecksumUtils.calculateCrc(data, data.length); - Assert.assertEquals((short) 62919, crc); - byte[] verifyingData = ByteBuffer.allocate(data.length + 2).put(data).order(ByteOrder.LITTLE_ENDIAN).putShort((short) (0xffff - crc)).array(); - Assert.assertTrue(ChecksumUtils.checkCrc(verifyingData, verifyingData.length)); - } + @Test + public void testCrc1() { + byte[] data = {0x0, 0x1, 0x2, 0x3, 0x4}; + short crc = ChecksumUtils.calculateCrc(data, data.length); + Assert.assertEquals((short) 62919, crc); + byte[] verifyingData = + ByteBuffer.allocate(data.length + 2) + .put(data) + .order(ByteOrder.LITTLE_ENDIAN) + .putShort((short) (0xffff - crc)) + .array(); + Assert.assertTrue(ChecksumUtils.checkCrc(verifyingData, verifyingData.length)); + } - @Test - public void testCrc2() { - byte[] data = {(byte) 0xfe}; - short crc = ChecksumUtils.calculateCrc(data, data.length); - Assert.assertEquals((short) 4470, crc); - byte[] verifyingData = ByteBuffer.allocate(data.length + 2).put(data).order(ByteOrder.LITTLE_ENDIAN).putShort((short) (0xffff - crc)).array(); - Assert.assertTrue(ChecksumUtils.checkCrc(verifyingData, verifyingData.length)); - } + @Test + public void testCrc2() { + byte[] data = {(byte) 0xfe}; + short crc = ChecksumUtils.calculateCrc(data, data.length); + Assert.assertEquals((short) 4470, crc); + byte[] verifyingData = + ByteBuffer.allocate(data.length + 2) + .put(data) + .order(ByteOrder.LITTLE_ENDIAN) + .putShort((short) (0xffff - crc)) + .array(); + Assert.assertTrue(ChecksumUtils.checkCrc(verifyingData, verifyingData.length)); + } - @Test - public void testCrc3() { - byte[] data = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, /* uid */ - 0x30, 0x75, /* use_ctr */ - 0x00, 0x09, /* ts_low */ - 0x3d, /* ts_high */ - (byte) 0xfa, /* session_ctr */ - 0x60, (byte) 0xea /* rnd */ - }; - short crc = ChecksumUtils.calculateCrc(data, data.length); + @Test + public void testCrc3() { + byte[] data = { + 0x01, + 0x02, + 0x03, + 0x04, + 0x05, + 0x06, /* uid */ + 0x30, + 0x75, /* use_ctr */ + 0x00, + 0x09, /* ts_low */ + 0x3d, /* ts_high */ + (byte) 0xfa, /* session_ctr */ + 0x60, + (byte) 0xea /* rnd */ + }; + short crc = ChecksumUtils.calculateCrc(data, data.length); - Assert.assertEquals((short) 35339, crc); - byte[] verifyingData = ByteBuffer.allocate(data.length + 2).put(data).order(ByteOrder.LITTLE_ENDIAN).putShort((short) (0xffff - crc)).array(); - Assert.assertTrue(ChecksumUtils.checkCrc(verifyingData, verifyingData.length)); - } + Assert.assertEquals((short) 35339, crc); + byte[] verifyingData = + ByteBuffer.allocate(data.length + 2) + .put(data) + .order(ByteOrder.LITTLE_ENDIAN) + .putShort((short) (0xffff - crc)) + .array(); + Assert.assertTrue(ChecksumUtils.checkCrc(verifyingData, verifyingData.length)); + } - @Test - public void testCrc4() { - byte[] data = {0x55, (byte) 0xaa, 0x00, (byte) 0xff}; - short crc = ChecksumUtils.calculateCrc(data, data.length); - Assert.assertEquals((short) 52149, crc); - byte[] verifyingData = ByteBuffer.allocate(data.length + 2).put(data).order(ByteOrder.LITTLE_ENDIAN).putShort((short) (0xffff - crc)).array(); - Assert.assertTrue(ChecksumUtils.checkCrc(verifyingData, verifyingData.length)); - } + @Test + public void testCrc4() { + byte[] data = {0x55, (byte) 0xaa, 0x00, (byte) 0xff}; + short crc = ChecksumUtils.calculateCrc(data, data.length); + Assert.assertEquals((short) 52149, crc); + byte[] verifyingData = + ByteBuffer.allocate(data.length + 2) + .put(data) + .order(ByteOrder.LITTLE_ENDIAN) + .putShort((short) (0xffff - crc)) + .array(); + Assert.assertTrue(ChecksumUtils.checkCrc(verifyingData, verifyingData.length)); + } } diff --git a/core/src/test/java/com/yubico/yubikit/core/otp/ModhexTest.java b/core/src/test/java/com/yubico/yubikit/core/otp/ModhexTest.java index 7b682085..c92960a9 100755 --- a/core/src/test/java/com/yubico/yubikit/core/otp/ModhexTest.java +++ b/core/src/test/java/com/yubico/yubikit/core/otp/ModhexTest.java @@ -16,37 +16,43 @@ package com.yubico.yubikit.core.otp; import com.yubico.yubikit.testing.Codec; - +import java.nio.charset.StandardCharsets; import org.junit.Assert; import org.junit.Test; -import java.nio.charset.StandardCharsets; - @SuppressWarnings("SpellCheckingInspection") public class ModhexTest { - @Test - public void testDecode() { - Assert.assertArrayEquals(Codec.fromHex("2d344e83"), Modhex.decode("DTEFFUJE")); - Assert.assertArrayEquals(Codec.fromHex("69b6481c8baba2b60e8f22179b58cd56"), Modhex.decode("hknhfjbrjnlnldnhcujvddbikngjrtgh")); - Assert.assertArrayEquals(Codec.fromHex("ecde18dbe76fbd0c33330f1c354871db"), Modhex.decode("urtubjtnuihvntcreeeecvbregfjibtn")); - Assert.assertArrayEquals("test".getBytes(StandardCharsets.UTF_8), Modhex.decode("iFHgiEiF")); - } + @Test + public void testDecode() { + Assert.assertArrayEquals(Codec.fromHex("2d344e83"), Modhex.decode("DTEFFUJE")); + Assert.assertArrayEquals( + Codec.fromHex("69b6481c8baba2b60e8f22179b58cd56"), + Modhex.decode("hknhfjbrjnlnldnhcujvddbikngjrtgh")); + Assert.assertArrayEquals( + Codec.fromHex("ecde18dbe76fbd0c33330f1c354871db"), + Modhex.decode("urtubjtnuihvntcreeeecvbregfjibtn")); + Assert.assertArrayEquals("test".getBytes(StandardCharsets.UTF_8), Modhex.decode("iFHgiEiF")); + } - @Test(expected = IllegalArgumentException.class) - public void testOddLengthString() { - Modhex.decode("theincrediblehulk"); - } + @Test(expected = IllegalArgumentException.class) + public void testOddLengthString() { + Modhex.decode("theincrediblehulk"); + } - @Test(expected = IllegalArgumentException.class) - public void testIllegalCharacter() { - Modhex.decode("theincrediblehulk!"); - } + @Test(expected = IllegalArgumentException.class) + public void testIllegalCharacter() { + Modhex.decode("theincrediblehulk!"); + } - @Test - public void testEncode() { - Assert.assertEquals("dteffuje", Modhex.encode(Codec.fromHex("2d344e83"))); - Assert.assertEquals("hknhfjbrjnlnldnhcujvddbikngjrtgh", Modhex.encode(Codec.fromHex("69b6481c8baba2b60e8f22179b58cd56"))); - Assert.assertEquals("urtubjtnuihvntcreeeecvbregfjibtn", Modhex.encode(Codec.fromHex("ecde18dbe76fbd0c33330f1c354871db"))); - Assert.assertEquals("ifhgieif", Modhex.encode("test".getBytes(StandardCharsets.UTF_8))); - } + @Test + public void testEncode() { + Assert.assertEquals("dteffuje", Modhex.encode(Codec.fromHex("2d344e83"))); + Assert.assertEquals( + "hknhfjbrjnlnldnhcujvddbikngjrtgh", + Modhex.encode(Codec.fromHex("69b6481c8baba2b60e8f22179b58cd56"))); + Assert.assertEquals( + "urtubjtnuihvntcreeeecvbregfjibtn", + Modhex.encode(Codec.fromHex("ecde18dbe76fbd0c33330f1c354871db"))); + Assert.assertEquals("ifhgieif", Modhex.encode("test".getBytes(StandardCharsets.UTF_8))); + } } diff --git a/core/src/test/java/com/yubico/yubikit/core/smartcard/ApduTest.java b/core/src/test/java/com/yubico/yubikit/core/smartcard/ApduTest.java index 2f9bc9a1..76492f9e 100755 --- a/core/src/test/java/com/yubico/yubikit/core/smartcard/ApduTest.java +++ b/core/src/test/java/com/yubico/yubikit/core/smartcard/ApduTest.java @@ -20,17 +20,17 @@ import org.junit.Test; public class ApduTest { - @Test - public void testMixedBytesAndInts() { - byte cla = 0x7f; - byte ins = (byte) 0xff; - int p1 = 0x7f; - int p2 = 0xff; - Apdu apdu = new Apdu(cla, ins, p1, p2, null); + @Test + public void testMixedBytesAndInts() { + byte cla = 0x7f; + byte ins = (byte) 0xff; + int p1 = 0x7f; + int p2 = 0xff; + Apdu apdu = new Apdu(cla, ins, p1, p2, null); - Assert.assertEquals(cla, apdu.getCla()); - Assert.assertEquals(ins, apdu.getIns()); - Assert.assertEquals(cla, apdu.getP1()); - Assert.assertEquals(ins, apdu.getP2()); - } + Assert.assertEquals(cla, apdu.getCla()); + Assert.assertEquals(ins, apdu.getIns()); + Assert.assertEquals(cla, apdu.getP1()); + Assert.assertEquals(ins, apdu.getP2()); + } } diff --git a/core/src/test/java/com/yubico/yubikit/core/util/TlvsTest.java b/core/src/test/java/com/yubico/yubikit/core/util/TlvsTest.java index 3ed57faf..26423d7d 100755 --- a/core/src/test/java/com/yubico/yubikit/core/util/TlvsTest.java +++ b/core/src/test/java/com/yubico/yubikit/core/util/TlvsTest.java @@ -16,39 +16,38 @@ package com.yubico.yubikit.core.util; import com.yubico.yubikit.core.application.BadResponseException; - import org.junit.Assert; import org.junit.Test; public class TlvsTest { - @Test - public void testDoubleByteTags() { - Tlv tlv = Tlv.parse(new byte[]{0x7F, 0x49, 0}); - Assert.assertEquals(0x7F49, tlv.getTag()); - Assert.assertEquals(0, tlv.getLength()); - - tlv = Tlv.parse(new byte[]{(byte) 0x80, 0}); - Assert.assertEquals(0x80, tlv.getTag()); - Assert.assertEquals(0, tlv.getLength()); - - tlv = new Tlv(0x7F49, null); - Assert.assertEquals(0x7F49, tlv.getTag()); - Assert.assertEquals(0, tlv.getLength()); - Assert.assertArrayEquals(new byte[]{0x7F, 0x49, 0}, tlv.getBytes()); - - tlv = new Tlv(0x80, null); - Assert.assertEquals(0x80, tlv.getTag()); - Assert.assertEquals(0, tlv.getLength()); - Assert.assertArrayEquals(new byte[]{(byte) 0x80, 0}, tlv.getBytes()); - } - - @Test - public void testUnwrap() throws BadResponseException { - Tlvs.unpackValue(0x80, new byte[]{(byte) 0x80, 0}); - - Tlvs.unpackValue(0x7F49, new byte[]{0x7F, 0x49, 0}); - - byte[] value = Tlvs.unpackValue(0x7F49, new byte[]{0x7F, 0x49, 3, 1, 2, 3}); - Assert.assertArrayEquals(new byte[]{1, 2, 3}, value); - } + @Test + public void testDoubleByteTags() { + Tlv tlv = Tlv.parse(new byte[] {0x7F, 0x49, 0}); + Assert.assertEquals(0x7F49, tlv.getTag()); + Assert.assertEquals(0, tlv.getLength()); + + tlv = Tlv.parse(new byte[] {(byte) 0x80, 0}); + Assert.assertEquals(0x80, tlv.getTag()); + Assert.assertEquals(0, tlv.getLength()); + + tlv = new Tlv(0x7F49, null); + Assert.assertEquals(0x7F49, tlv.getTag()); + Assert.assertEquals(0, tlv.getLength()); + Assert.assertArrayEquals(new byte[] {0x7F, 0x49, 0}, tlv.getBytes()); + + tlv = new Tlv(0x80, null); + Assert.assertEquals(0x80, tlv.getTag()); + Assert.assertEquals(0, tlv.getLength()); + Assert.assertArrayEquals(new byte[] {(byte) 0x80, 0}, tlv.getBytes()); + } + + @Test + public void testUnwrap() throws BadResponseException { + Tlvs.unpackValue(0x80, new byte[] {(byte) 0x80, 0}); + + Tlvs.unpackValue(0x7F49, new byte[] {0x7F, 0x49, 0}); + + byte[] value = Tlvs.unpackValue(0x7F49, new byte[] {0x7F, 0x49, 3, 1, 2, 3}); + Assert.assertArrayEquals(new byte[] {1, 2, 3}, value); + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/Cbor.java b/fido/src/main/java/com/yubico/yubikit/fido/Cbor.java index bc4f020e..0e8dc136 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/Cbor.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/Cbor.java @@ -27,256 +27,254 @@ import java.util.List; import java.util.Locale; import java.util.Map; - import javax.annotation.Nullable; /** * Provides canonical CBOR encoding and decoding. - *

- * Only a small subset of CBOR is implemented, sufficient for CTAP2 functionality. - *

- * Note that while any integer type can be encoded into canonical CBOR, but all CBOR integers will - * decode to an int. Thus, numeric map keys can use any integer type (byte, short, int, long) + * + *

Only a small subset of CBOR is implemented, sufficient for CTAP2 functionality. + * + *

Note that while any integer type can be encoded into canonical CBOR, but all CBOR integers + * will decode to an int. Thus, numeric map keys can use any integer type (byte, short, int, long) * when encoding to send to a device, but any response will have ints for keys. */ public class Cbor { - /** - * Encodes an object into canonical CBOR. - * - * @param value the Object to encode. - * @return CBOR encoded bytes. - */ - public static byte[] encode(Object value) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try { - encodeTo(baos, value); - } catch (IOException e) { - throw new RuntimeException(e); // Shouldn't happen - } - return baos.toByteArray(); + /** + * Encodes an object into canonical CBOR. + * + * @param value the Object to encode. + * @return CBOR encoded bytes. + */ + public static byte[] encode(Object value) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + encodeTo(baos, value); + } catch (IOException e) { + throw new RuntimeException(e); // Shouldn't happen } + return baos.toByteArray(); + } - /** - * Encodes an object into canonical CBOR, to an OutputStream. - * - * @param stream the output stream to write to - * @param value the Object to encode. - * @throws IOException A communication error in the transport layer. - */ - public static void encodeTo(OutputStream stream, @Nullable Object value) throws IOException { - if(value == null) { - dumpSimple(stream, null); - } else if (value instanceof Number) { - dumpInt(stream, ((Number) value).intValue(), 0); - } else if (value instanceof Boolean) { - dumpSimple(stream, (Boolean) value); - } else if (value instanceof List) { - dumpList(stream, (List) value); - } else if (value instanceof Map) { - dumpMap(stream, (Map) value); - } else if (value instanceof byte[]) { - dumpBytes(stream, (byte[]) value); - } else if (value instanceof String) { - dumpText(stream, (String) value); - } else { - throw new IllegalArgumentException(String.format(Locale.ROOT, "Unsupported object type: %s", value.getClass())); - } + /** + * Encodes an object into canonical CBOR, to an OutputStream. + * + * @param stream the output stream to write to + * @param value the Object to encode. + * @throws IOException A communication error in the transport layer. + */ + public static void encodeTo(OutputStream stream, @Nullable Object value) throws IOException { + if (value == null) { + dumpSimple(stream, null); + } else if (value instanceof Number) { + dumpInt(stream, ((Number) value).intValue(), 0); + } else if (value instanceof Boolean) { + dumpSimple(stream, (Boolean) value); + } else if (value instanceof List) { + dumpList(stream, (List) value); + } else if (value instanceof Map) { + dumpMap(stream, (Map) value); + } else if (value instanceof byte[]) { + dumpBytes(stream, (byte[]) value); + } else if (value instanceof String) { + dumpText(stream, (String) value); + } else { + throw new IllegalArgumentException( + String.format(Locale.ROOT, "Unsupported object type: %s", value.getClass())); } + } - /** - * Decodes an Object from CBOR data. - * - * @param data The CBOR encoded byte array. - * @param offset The offset of where the CBOR encoded data is in the given byte array. - * @param length The length of CBOR encoded data. - * @return The decoded Object. - */ - @Nullable - public static Object decode(byte[] data, int offset, int length) { - ByteBuffer buf = ByteBuffer.wrap(data, offset, length); - Object decoded = decodeFrom(buf); - if (buf.hasRemaining()) { - throw new IllegalArgumentException("Extraneous data"); - } - return decoded; + /** + * Decodes an Object from CBOR data. + * + * @param data The CBOR encoded byte array. + * @param offset The offset of where the CBOR encoded data is in the given byte array. + * @param length The length of CBOR encoded data. + * @return The decoded Object. + */ + @Nullable public static Object decode(byte[] data, int offset, int length) { + ByteBuffer buf = ByteBuffer.wrap(data, offset, length); + Object decoded = decodeFrom(buf); + if (buf.hasRemaining()) { + throw new IllegalArgumentException("Extraneous data"); } + return decoded; + } - /** - * Decodes an Object from CBOR data. - * - * @param data The CBOR encoded byte array. - * @return The decoded Object. - */ - @Nullable - public static Object decode(byte[] data) { - return decode(data, 0, data.length); - } + /** + * Decodes an Object from CBOR data. + * + * @param data The CBOR encoded byte array. + * @return The decoded Object. + */ + @Nullable public static Object decode(byte[] data) { + return decode(data, 0, data.length); + } - /** - * Decodes a single Object from a ByteBuffer containing CBOR encoded data at the buffers current - * position. The position will be updated to point to the end of the CBOR data. - * - * @param buf the ByteBuffer from where the Object should be decoded. - * @return The decoded object. - */ - @Nullable - public static Object decodeFrom(ByteBuffer buf) { - int head = 0xff & buf.get(); - byte additionalInfo = (byte) (head & 0b11111); - switch (head >> 5) { - case 0: - return loadInt(additionalInfo, buf); - case 1: - return loadNint(additionalInfo, buf); - case 2: - return loadBytes(additionalInfo, buf); - case 3: - return loadString(additionalInfo, buf); - case 4: - return loadList(additionalInfo, buf); - case 5: - return loadMap(additionalInfo, buf); - case 7: - return loadSimple(additionalInfo); - } - throw new IllegalArgumentException("Unsupported major type"); + /** + * Decodes a single Object from a ByteBuffer containing CBOR encoded data at the buffers current + * position. The position will be updated to point to the end of the CBOR data. + * + * @param buf the ByteBuffer from where the Object should be decoded. + * @return The decoded object. + */ + @Nullable public static Object decodeFrom(ByteBuffer buf) { + int head = 0xff & buf.get(); + byte additionalInfo = (byte) (head & 0b11111); + switch (head >> 5) { + case 0: + return loadInt(additionalInfo, buf); + case 1: + return loadNint(additionalInfo, buf); + case 2: + return loadBytes(additionalInfo, buf); + case 3: + return loadString(additionalInfo, buf); + case 4: + return loadList(additionalInfo, buf); + case 5: + return loadMap(additionalInfo, buf); + case 7: + return loadSimple(additionalInfo); } + throw new IllegalArgumentException("Unsupported major type"); + } - private static void dumpInt(OutputStream stream, int value, int majorType) throws IOException { - if (value < 0) { - majorType = 1; - value = -1 - value; - } + private static void dumpInt(OutputStream stream, int value, int majorType) throws IOException { + if (value < 0) { + majorType = 1; + value = -1 - value; + } - byte head = (byte) (majorType << 5); - if (value <= 23) { - stream.write((byte) (head | value)); - } else if (value <= 0xff) { - stream.write((byte) (head | 24)); - stream.write((byte) value); - } else if (value <= 0xffff) { - stream.write((byte) (head | 25)); - stream.write(ByteBuffer.allocate(2).putShort((short) value).array()); - } else { - stream.write((byte) (head | 26)); - stream.write(ByteBuffer.allocate(4).putInt(value).array()); - } + byte head = (byte) (majorType << 5); + if (value <= 23) { + stream.write((byte) (head | value)); + } else if (value <= 0xff) { + stream.write((byte) (head | 24)); + stream.write((byte) value); + } else if (value <= 0xffff) { + stream.write((byte) (head | 25)); + stream.write(ByteBuffer.allocate(2).putShort((short) value).array()); + } else { + stream.write((byte) (head | 26)); + stream.write(ByteBuffer.allocate(4).putInt(value).array()); } + } - private static void dumpSimple(OutputStream stream, @Nullable Boolean value) throws IOException { - if (value == null) { - stream.write((byte) 0xf6); - } else { - stream.write((byte) (value ? 0xf5 : 0xf4)); - } + private static void dumpSimple(OutputStream stream, @Nullable Boolean value) throws IOException { + if (value == null) { + stream.write((byte) 0xf6); + } else { + stream.write((byte) (value ? 0xf5 : 0xf4)); } + } - private static void dumpList(OutputStream stream, List value) throws IOException { - dumpInt(stream, value.size(), 4); - for (Object item : value) { - stream.write(encode(item)); - } + private static void dumpList(OutputStream stream, List value) throws IOException { + dumpInt(stream, value.size(), 4); + for (Object item : value) { + stream.write(encode(item)); } + } - private static void dumpMap(OutputStream stream, Map value) throws IOException { - dumpInt(stream, value.size(), 5); - List entries = new ArrayList<>(); - for (Map.Entry entry : value.entrySet()) { - entries.add(new byte[][]{encode(entry.getKey()), encode(entry.getValue())}); - } - // Canonical order of map keys, as specified here: - // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#ctap2-canonical-cbor-encoding-form - // Corresponds to lexicographical comparison. - //noinspection Java8ListSort // List.sort(Comparator<>) is available first in Android API 24 - Collections.sort(entries, (o1, o2) -> { - byte[] key1 = o1[0]; - byte[] key2 = o2[0]; - int minLength = Math.min(key1.length, key2.length); - for (int i = 0; i < minLength; i++) { - int a = 0xff & key1[i]; - int b = 0xff & key2[i]; - if (a != b) { - return a - b; - } + private static void dumpMap(OutputStream stream, Map value) throws IOException { + dumpInt(stream, value.size(), 5); + List entries = new ArrayList<>(); + for (Map.Entry entry : value.entrySet()) { + entries.add(new byte[][] {encode(entry.getKey()), encode(entry.getValue())}); + } + // Canonical order of map keys, as specified here: + // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#ctap2-canonical-cbor-encoding-form + // Corresponds to lexicographical comparison. + //noinspection Java8ListSort // List.sort(Comparator<>) is available first in Android API 24 + Collections.sort( + entries, + (o1, o2) -> { + byte[] key1 = o1[0]; + byte[] key2 = o2[0]; + int minLength = Math.min(key1.length, key2.length); + for (int i = 0; i < minLength; i++) { + int a = 0xff & key1[i]; + int b = 0xff & key2[i]; + if (a != b) { + return a - b; } - return key1.length - key2.length; + } + return key1.length - key2.length; }); - for (byte[][] entry : entries) { - stream.write(entry[0]); - stream.write(entry[1]); - } + for (byte[][] entry : entries) { + stream.write(entry[0]); + stream.write(entry[1]); } + } - private static void dumpBytes(OutputStream stream, byte[] value) throws IOException { - dumpInt(stream, value.length, 2); - stream.write(value); - } + private static void dumpBytes(OutputStream stream, byte[] value) throws IOException { + dumpInt(stream, value.length, 2); + stream.write(value); + } - private static void dumpText(OutputStream stream, String value) throws IOException { - byte[] data = value.getBytes(StandardCharsets.UTF_8); - dumpInt(stream, data.length, 3); - stream.write(data); - } + private static void dumpText(OutputStream stream, String value) throws IOException { + byte[] data = value.getBytes(StandardCharsets.UTF_8); + dumpInt(stream, data.length, 3); + stream.write(data); + } - private static int loadInt(byte additionalInfo, ByteBuffer buf) { - if (additionalInfo < 24) { - return 0xff & additionalInfo; - } else if (additionalInfo == 24) { - return 0xff & buf.get(); - } else if (additionalInfo == 25) { - return 0xffff & buf.getShort(); - } else if (additionalInfo == 26) { - int value = buf.getInt(); - if (value < 0) { - throw new IllegalArgumentException("Unsupported integer size"); - } - return value; - } - throw new IllegalArgumentException("Unable to load integer"); + private static int loadInt(byte additionalInfo, ByteBuffer buf) { + if (additionalInfo < 24) { + return 0xff & additionalInfo; + } else if (additionalInfo == 24) { + return 0xff & buf.get(); + } else if (additionalInfo == 25) { + return 0xffff & buf.getShort(); + } else if (additionalInfo == 26) { + int value = buf.getInt(); + if (value < 0) { + throw new IllegalArgumentException("Unsupported integer size"); + } + return value; } + throw new IllegalArgumentException("Unable to load integer"); + } - private static int loadNint(byte additionalInfo, ByteBuffer buf) { - return -1 - loadInt(additionalInfo, buf); - } + private static int loadNint(byte additionalInfo, ByteBuffer buf) { + return -1 - loadInt(additionalInfo, buf); + } - @Nullable - private static Boolean loadSimple(byte additionalInfo) { - switch (additionalInfo) { - case 20: - return false; - case 21: - return true; - case 22: - case 23: - return null; - default: - throw new IllegalArgumentException("Unsupported simple type: " + additionalInfo); - } + @Nullable private static Boolean loadSimple(byte additionalInfo) { + switch (additionalInfo) { + case 20: + return false; + case 21: + return true; + case 22: + case 23: + return null; + default: + throw new IllegalArgumentException("Unsupported simple type: " + additionalInfo); } + } - private static byte[] loadBytes(byte additionalInfo, ByteBuffer buf) { - byte[] value = new byte[(int) loadInt(additionalInfo, buf)]; - buf.get(value); - return value; - } + private static byte[] loadBytes(byte additionalInfo, ByteBuffer buf) { + byte[] value = new byte[(int) loadInt(additionalInfo, buf)]; + buf.get(value); + return value; + } - private static String loadString(byte additionalInfo, ByteBuffer buf) { - return new String(loadBytes(additionalInfo, buf), StandardCharsets.UTF_8); - } + private static String loadString(byte additionalInfo, ByteBuffer buf) { + return new String(loadBytes(additionalInfo, buf), StandardCharsets.UTF_8); + } - private static List loadList(byte additionalInfo, ByteBuffer buf) { - List list = new ArrayList<>(); - for (int i = loadInt(additionalInfo, buf); i > 0; i--) { - list.add(decodeFrom(buf)); - } - return list; + private static List loadList(byte additionalInfo, ByteBuffer buf) { + List list = new ArrayList<>(); + for (int i = loadInt(additionalInfo, buf); i > 0; i--) { + list.add(decodeFrom(buf)); } + return list; + } - private static Map loadMap(byte additionalInfo, ByteBuffer buf) { - Map map = new HashMap<>(); - for (int i = loadInt(additionalInfo, buf); i > 0; i--) { - map.put(decodeFrom(buf), decodeFrom(buf)); - } - return map; + private static Map loadMap(byte additionalInfo, ByteBuffer buf) { + Map map = new HashMap<>(); + for (int i = loadInt(additionalInfo, buf); i > 0; i--) { + map.put(decodeFrom(buf), decodeFrom(buf)); } -} \ No newline at end of file + return map; + } +} diff --git a/fido/src/main/java/com/yubico/yubikit/fido/Cose.java b/fido/src/main/java/com/yubico/yubikit/fido/Cose.java index e78f2938..d8562028 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/Cose.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/Cose.java @@ -22,113 +22,109 @@ import com.yubico.yubikit.core.keys.PublicKeyValues.Cv25519; import com.yubico.yubikit.core.keys.PublicKeyValues.Ec; import com.yubico.yubikit.core.keys.PublicKeyValues.Rsa; - -import org.slf4j.LoggerFactory; - import java.math.BigInteger; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.spec.InvalidKeySpecException; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; +import org.slf4j.LoggerFactory; public class Cose { - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(Cose.class); + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(Cose.class); - public static Integer getAlgorithm(Map cosePublicKey) { - Integer alg = (Integer) Objects.requireNonNull(cosePublicKey.get(3)); - Logger.debug(logger, "alg: {}", alg); - return alg; - } + public static Integer getAlgorithm(Map cosePublicKey) { + Integer alg = (Integer) Objects.requireNonNull(cosePublicKey.get(3)); + Logger.debug(logger, "alg: {}", alg); + return alg; + } - @Nullable - public static PublicKey getPublicKey(@Nullable Map cosePublicKey) - throws InvalidKeySpecException, NoSuchAlgorithmException { - - if (cosePublicKey == null) { - return null; - } - - final Integer kty = (Integer) Objects.requireNonNull(cosePublicKey.get(1)); - Logger.debug(logger, "kty: {}", kty); - PublicKey publicKey; - switch (kty) { - case 1: - publicKey = importCoseEdDsaPublicKey(cosePublicKey); - break; - case 2: - publicKey = importCoseEcdsaPublicKey(cosePublicKey); - break; - case 3: - publicKey = importCoseRsaPublicKey(cosePublicKey); - break; - default: - throw new IllegalArgumentException("Unsupported key type: " + kty); - } - - Logger.debug(logger, "publicKey: {}", Base64.toUrlSafeString(publicKey.getEncoded())); - - return publicKey; - } + @Nullable public static PublicKey getPublicKey(@Nullable Map cosePublicKey) + throws InvalidKeySpecException, NoSuchAlgorithmException { - private static PublicKey importCoseEdDsaPublicKey(Map cosePublicKey) - throws InvalidKeySpecException, NoSuchAlgorithmException { - final Integer crv = (Integer) Objects.requireNonNull(cosePublicKey.get(-1)); - Logger.debug(logger, "crv: {}", crv); - if (crv == 6) { - return importCoseEd25519PublicKey(cosePublicKey); - } - throw new IllegalArgumentException("Unsupported EdDSA curve: " + crv); + if (cosePublicKey == null) { + return null; } - private static PublicKey importCoseEd25519PublicKey(Map cosePublicKey) - throws InvalidKeySpecException, NoSuchAlgorithmException { - final byte[] rawKey = (byte[]) Objects.requireNonNull(cosePublicKey.get(-2)); - Logger.debug(logger, "raw: {}", Base64.toUrlSafeString(rawKey)); - return new Cv25519(EllipticCurveValues.Ed25519, rawKey).toPublicKey(); + final Integer kty = (Integer) Objects.requireNonNull(cosePublicKey.get(1)); + Logger.debug(logger, "kty: {}", kty); + PublicKey publicKey; + switch (kty) { + case 1: + publicKey = importCoseEdDsaPublicKey(cosePublicKey); + break; + case 2: + publicKey = importCoseEcdsaPublicKey(cosePublicKey); + break; + case 3: + publicKey = importCoseRsaPublicKey(cosePublicKey); + break; + default: + throw new IllegalArgumentException("Unsupported key type: " + kty); } - private static PublicKey importCoseEcdsaPublicKey(Map cosePublicKey) - throws NoSuchAlgorithmException, InvalidKeySpecException { - final Integer crv = (Integer) Objects.requireNonNull(cosePublicKey.get(-1)); - final byte[] x = (byte[]) Objects.requireNonNull(cosePublicKey.get(-2)); - final byte[] y = (byte[]) Objects.requireNonNull(cosePublicKey.get(-3)); + Logger.debug(logger, "publicKey: {}", Base64.toUrlSafeString(publicKey.getEncoded())); - Logger.debug(logger, "crv: {}", crv); - Logger.debug(logger, "x: {}", Base64.toUrlSafeString(x)); - Logger.debug(logger, "y: {}", Base64.toUrlSafeString(y)); + return publicKey; + } - EllipticCurveValues ellipticCurveValues; - - switch (crv) { - case 1: - ellipticCurveValues = EllipticCurveValues.SECP256R1; - break; - - case 2: - ellipticCurveValues = EllipticCurveValues.SECP384R1; - break; - - case 3: - ellipticCurveValues = EllipticCurveValues.SECP521R1; - break; - - default: - throw new IllegalArgumentException("Unknown COSE EC2 curve: " + crv); - } - - return new Ec(ellipticCurveValues, new BigInteger(x), new BigInteger(y)).toPublicKey(); + private static PublicKey importCoseEdDsaPublicKey(Map cosePublicKey) + throws InvalidKeySpecException, NoSuchAlgorithmException { + final Integer crv = (Integer) Objects.requireNonNull(cosePublicKey.get(-1)); + Logger.debug(logger, "crv: {}", crv); + if (crv == 6) { + return importCoseEd25519PublicKey(cosePublicKey); } - - private static PublicKey importCoseRsaPublicKey(Map cosePublicKey) - throws NoSuchAlgorithmException, InvalidKeySpecException { - byte[] n = (byte[]) Objects.requireNonNull(cosePublicKey.get(-1)); - byte[] e = (byte[]) Objects.requireNonNull(cosePublicKey.get(-2)); - Logger.debug(logger, "n: {}", Base64.toUrlSafeString(n)); - Logger.debug(logger, "e: {}", Base64.toUrlSafeString(e)); - return new Rsa(new BigInteger(1, n), new BigInteger(1, e)).toPublicKey(); + throw new IllegalArgumentException("Unsupported EdDSA curve: " + crv); + } + + private static PublicKey importCoseEd25519PublicKey(Map cosePublicKey) + throws InvalidKeySpecException, NoSuchAlgorithmException { + final byte[] rawKey = (byte[]) Objects.requireNonNull(cosePublicKey.get(-2)); + Logger.debug(logger, "raw: {}", Base64.toUrlSafeString(rawKey)); + return new Cv25519(EllipticCurveValues.Ed25519, rawKey).toPublicKey(); + } + + private static PublicKey importCoseEcdsaPublicKey(Map cosePublicKey) + throws NoSuchAlgorithmException, InvalidKeySpecException { + final Integer crv = (Integer) Objects.requireNonNull(cosePublicKey.get(-1)); + final byte[] x = (byte[]) Objects.requireNonNull(cosePublicKey.get(-2)); + final byte[] y = (byte[]) Objects.requireNonNull(cosePublicKey.get(-3)); + + Logger.debug(logger, "crv: {}", crv); + Logger.debug(logger, "x: {}", Base64.toUrlSafeString(x)); + Logger.debug(logger, "y: {}", Base64.toUrlSafeString(y)); + + EllipticCurveValues ellipticCurveValues; + + switch (crv) { + case 1: + ellipticCurveValues = EllipticCurveValues.SECP256R1; + break; + + case 2: + ellipticCurveValues = EllipticCurveValues.SECP384R1; + break; + + case 3: + ellipticCurveValues = EllipticCurveValues.SECP521R1; + break; + + default: + throw new IllegalArgumentException("Unknown COSE EC2 curve: " + crv); } + + return new Ec(ellipticCurveValues, new BigInteger(x), new BigInteger(y)).toPublicKey(); + } + + private static PublicKey importCoseRsaPublicKey(Map cosePublicKey) + throws NoSuchAlgorithmException, InvalidKeySpecException { + byte[] n = (byte[]) Objects.requireNonNull(cosePublicKey.get(-1)); + byte[] e = (byte[]) Objects.requireNonNull(cosePublicKey.get(-2)); + Logger.debug(logger, "n: {}", Base64.toUrlSafeString(n)); + Logger.debug(logger, "e: {}", Base64.toUrlSafeString(e)); + return new Rsa(new BigInteger(1, n), new BigInteger(1, e)).toPublicKey(); + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/client/BasicWebAuthnClient.java b/fido/src/main/java/com/yubico/yubikit/fido/client/BasicWebAuthnClient.java index 313047d5..f5e35dad 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/client/BasicWebAuthnClient.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/client/BasicWebAuthnClient.java @@ -29,7 +29,6 @@ import com.yubico.yubikit.fido.client.extensions.HmacSecretExtension; import com.yubico.yubikit.fido.client.extensions.LargeBlobExtension; import com.yubico.yubikit.fido.client.extensions.MinPinLengthExtension; -import com.yubico.yubikit.fido.webauthn.ClientExtensionResults; import com.yubico.yubikit.fido.ctap.ClientPin; import com.yubico.yubikit.fido.ctap.CredentialManagement; import com.yubico.yubikit.fido.ctap.Ctap2Session; @@ -41,6 +40,7 @@ import com.yubico.yubikit.fido.webauthn.AttestationObject; import com.yubico.yubikit.fido.webauthn.AuthenticatorAttestationResponse; import com.yubico.yubikit.fido.webauthn.AuthenticatorSelectionCriteria; +import com.yubico.yubikit.fido.webauthn.ClientExtensionResults; import com.yubico.yubikit.fido.webauthn.PublicKeyCredential; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialCreationOptions; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialDescriptor; @@ -49,9 +49,6 @@ import com.yubico.yubikit.fido.webauthn.ResidentKeyRequirement; import com.yubico.yubikit.fido.webauthn.SerializationType; import com.yubico.yubikit.fido.webauthn.UserVerificationRequirement; - -import org.slf4j.LoggerFactory; - import java.io.Closeable; import java.io.IOException; import java.security.MessageDigest; @@ -63,879 +60,870 @@ import java.util.List; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; +import org.slf4j.LoggerFactory; /** * A "basic" WebAuthn client implementation which wraps a YubiKeySession. - *

- * Provides the following functionality: + * + *

Provides the following functionality: + * *

    - *
  • MakeCredential: Registers a new credential. If a PIN is needed, it is passed to this method.
  • - *
  • GetAssertion: Authenticate an existing credential. If a PIN is needed, it is passed to this method.
  • - *
  • PIN Management: Set or change the PIN code of an Authenticator, or see its state.
  • - *
  • Credential Management: List or delete resident credentials of an Authenticator.
  • + *
  • MakeCredential: Registers a new credential. If a PIN is needed, it is passed to this + * method. + *
  • GetAssertion: Authenticate an existing credential. If a PIN is needed, it is passed to this + * method. + *
  • PIN Management: Set or change the PIN code of an Authenticator, or see its state. + *
  • Credential Management: List or delete resident credentials of an Authenticator. *
- * The timeout parameter in the request options is ignored. To cancel a request pass a {@link CommandState} - * instance to the call and use its cancel method. + * + * The timeout parameter in the request options is ignored. To cancel a request pass a {@link + * CommandState} instance to the call and use its cancel method. */ @SuppressWarnings("unused") public class BasicWebAuthnClient implements Closeable { - private static final String OPTION_CLIENT_PIN = "clientPin"; - private static final String OPTION_USER_VERIFICATION = "uv"; - private static final String OPTION_RESIDENT_KEY = "rk"; - private static final String OPTION_EP = "ep"; - - private final UserAgentConfiguration userAgentConfiguration = new UserAgentConfiguration(); + private static final String OPTION_CLIENT_PIN = "clientPin"; + private static final String OPTION_USER_VERIFICATION = "uv"; + private static final String OPTION_RESIDENT_KEY = "rk"; + private static final String OPTION_EP = "ep"; - private final Ctap2Session ctap; + private final UserAgentConfiguration userAgentConfiguration = new UserAgentConfiguration(); - private final boolean pinSupported; - private final boolean uvSupported; - private final boolean rkSupported; + private final Ctap2Session ctap; - private final ClientPin clientPin; + private final boolean pinSupported; + private final boolean uvSupported; + private final boolean rkSupported; - private boolean pinConfigured; - private final boolean uvConfigured; + private final ClientPin clientPin; - final private boolean enterpriseAttestationSupported; + private boolean pinConfigured; + private final boolean uvConfigured; - private final List extensions; + private final boolean enterpriseAttestationSupported; - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(BasicWebAuthnClient.class); + private final List extensions; - public static class UserAgentConfiguration { - private List epSupportedRpIds = new ArrayList<>(); + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(BasicWebAuthnClient.class); - public void setEpSupportedRpIds(List epSupportedRpIds) { - this.epSupportedRpIds = epSupportedRpIds; - } + public static class UserAgentConfiguration { + private List epSupportedRpIds = new ArrayList<>(); - boolean supportsEpForRpId(@Nullable String rpId) { - return epSupportedRpIds.contains(rpId); - } + public void setEpSupportedRpIds(List epSupportedRpIds) { + this.epSupportedRpIds = epSupportedRpIds; } - private static class AuthParams { - @Nullable - private final byte[] pinToken; - @Nullable - private final PinUvAuthProtocol pinUvAuthProtocol; - @Nullable - private final byte[] pinUvAuthParam; - - AuthParams( - @Nullable byte[] pinToken, - @Nullable PinUvAuthProtocol pinUvAuthProtocol, - @Nullable byte[] pinUvAuthParam) { - this.pinToken = pinToken; - this.pinUvAuthProtocol = pinUvAuthProtocol; - this.pinUvAuthParam = pinUvAuthParam; - } + boolean supportsEpForRpId(@Nullable String rpId) { + return epSupportedRpIds.contains(rpId); } - - /** - * Create a new Webauthn client. - *

- * This client will process all extensions. - * - * @param session CTAP session - * @throws IOException A communication error in the transport layer - * @throws CommandException A communication in the protocol layer - * - * @see Webauthn client - * @see Webauthn extensions - * @see CTAP extensions - */ - public BasicWebAuthnClient(Ctap2Session session) throws IOException, CommandException { - this(session, defaultExtensions); + } + + private static class AuthParams { + @Nullable private final byte[] pinToken; + @Nullable private final PinUvAuthProtocol pinUvAuthProtocol; + @Nullable private final byte[] pinUvAuthParam; + + AuthParams( + @Nullable byte[] pinToken, + @Nullable PinUvAuthProtocol pinUvAuthProtocol, + @Nullable byte[] pinUvAuthParam) { + this.pinToken = pinToken; + this.pinUvAuthProtocol = pinUvAuthProtocol; + this.pinUvAuthParam = pinUvAuthParam; } - - /** - * Create a new Webauthn client. - *

- * This client will only process provided extensions. - * - * @param session CTAP2 session - * @param extensions List of extensions - * @throws IOException A communication error in the transport layer - * @throws CommandException A communication in the protocol layer - * - * @see Webauthn client - * @see Webauthn extensions - * @see CTAP extensions - */ - public BasicWebAuthnClient( - Ctap2Session session, - List extensions) throws IOException, CommandException { - this.ctap = session; - this.extensions = extensions; - - Ctap2Session.InfoData info = ctap.getInfo(); - - Map options = info.getOptions(); - - final Boolean optionClientPin = (Boolean) options.get(OPTION_CLIENT_PIN); - pinSupported = optionClientPin != null; - - this.clientPin = - new ClientPin(ctap, getPreferredPinUvAuthProtocol(info.getPinUvAuthProtocols())); - - pinConfigured = pinSupported && Boolean.TRUE.equals(optionClientPin); - - Boolean uv = (Boolean) options.get(OPTION_USER_VERIFICATION); - uvSupported = uv != null; - uvConfigured = uvSupported && uv; - rkSupported = Boolean.TRUE.equals(options.get(OPTION_RESIDENT_KEY)); - - enterpriseAttestationSupported = Boolean.TRUE.equals(options.get(OPTION_EP)); + } + + /** + * Create a new Webauthn client. + * + *

This client will process all extensions. + * + * @param session CTAP session + * @throws IOException A communication error in the transport layer + * @throws CommandException A communication in the protocol layer + * @see Webauthn client + * @see Webauthn extensions + * @see CTAP + * extensions + */ + public BasicWebAuthnClient(Ctap2Session session) throws IOException, CommandException { + this(session, defaultExtensions); + } + + /** + * Create a new Webauthn client. + * + *

This client will only process provided extensions. + * + * @param session CTAP2 session + * @param extensions List of extensions + * @throws IOException A communication error in the transport layer + * @throws CommandException A communication in the protocol layer + * @see Webauthn client + * @see Webauthn extensions + * @see CTAP + * extensions + */ + public BasicWebAuthnClient(Ctap2Session session, List extensions) + throws IOException, CommandException { + this.ctap = session; + this.extensions = extensions; + + Ctap2Session.InfoData info = ctap.getInfo(); + + Map options = info.getOptions(); + + final Boolean optionClientPin = (Boolean) options.get(OPTION_CLIENT_PIN); + pinSupported = optionClientPin != null; + + this.clientPin = + new ClientPin(ctap, getPreferredPinUvAuthProtocol(info.getPinUvAuthProtocols())); + + pinConfigured = pinSupported && Boolean.TRUE.equals(optionClientPin); + + Boolean uv = (Boolean) options.get(OPTION_USER_VERIFICATION); + uvSupported = uv != null; + uvConfigured = uvSupported && uv; + rkSupported = Boolean.TRUE.equals(options.get(OPTION_RESIDENT_KEY)); + + enterpriseAttestationSupported = Boolean.TRUE.equals(options.get(OPTION_EP)); + } + + @Override + public void close() throws IOException { + ctap.close(); + } + + public UserAgentConfiguration getUserAgentConfiguration() { + return userAgentConfiguration; + } + + /** + * Create a new WebAuthn credential. + * + *

PIN is always required if a PIN is configured. + * + * @param clientDataJson The UTF-8 encoded ClientData JSON object. + * @param options The options for creating the credential. + * @param effectiveDomain The effective domain for the request, which is used to validate the RP + * ID against. + * @param pin If needed, the PIN to authorize the credential creation. + * @param state If needed, the state to provide control over the ongoing operation + * @return A WebAuthn public key credential. + * @throws IOException A communication error in the transport layer + * @throws CommandException A communication in the protocol layer + * @throws ClientError A higher level error + */ + public PublicKeyCredential makeCredential( + byte[] clientDataJson, + PublicKeyCredentialCreationOptions options, + String effectiveDomain, + @Nullable char[] pin, + @Nullable Integer enterpriseAttestation, + @Nullable CommandState state) + throws IOException, CommandException, ClientError { + byte[] clientDataHash = Utils.hash(clientDataJson); + + try { + Pair result = + ctapMakeCredential( + clientDataHash, options, effectiveDomain, pin, enterpriseAttestation, state); + final Ctap2Session.CredentialData credential = result.first; + final ClientExtensionResults clientExtensionResults = result.second; + + final AttestationObject attestationObject = AttestationObject.fromCredential(credential); + + AuthenticatorAttestationResponse response = + new AuthenticatorAttestationResponse( + clientDataJson, ctap.getCachedInfo().getTransports(), attestationObject); + + return new PublicKeyCredential( + Objects.requireNonNull( + attestationObject.getAuthenticatorData().getAttestedCredentialData()) + .getCredentialId(), + response, + clientExtensionResults); + } catch (CtapException e) { + if (e.getCtapError() == CtapException.ERR_PIN_INVALID) { + throw new PinInvalidClientError(e, clientPin.getPinRetries().getCount()); + } + throw ClientError.wrapCtapException(e); } - - @Override - public void close() throws IOException { - ctap.close(); + } + + /** + * Authenticate an existing WebAuthn credential. PIN is required if UV is "required", or if UV is + * "preferred" and a PIN is configured. If no allowCredentials list is provided (which is the case + * for a passwordless flow) the Authenticator may contain multiple discoverable credentials for + * the given RP. In such cases MultipleAssertionsAvailable will be thrown, and can be handled to + * select an assertion. + * + * @param clientDataJson The UTF-8 encoded ClientData JSON object. + * @param options The options for authenticating the credential. + * @param effectiveDomain The effective domain for the request, which is used to validate the RP + * ID against. + * @param pin If needed, the PIN to authorize the credential creation. + * @param state If needed, the state to provide control over the ongoing operation + * @return Webauthn public key credential with assertion response data. + * @throws MultipleAssertionsAvailable In case of multiple assertions, catch this to make a + * selection and get the result. + * @throws IOException A communication error in the transport layer + * @throws CommandException A communication in the protocol layer + * @throws ClientError A higher level error + */ + public PublicKeyCredential getAssertion( + byte[] clientDataJson, + PublicKeyCredentialRequestOptions options, + String effectiveDomain, + @Nullable char[] pin, + @Nullable CommandState state) + throws MultipleAssertionsAvailable, IOException, CommandException, ClientError { + byte[] clientDataHash = Utils.hash(clientDataJson); + try { + final List> results = + ctapGetAssertions(clientDataHash, options, effectiveDomain, pin, state); + + final List allowCredentials = + removeUnsupportedCredentials(options.getAllowCredentials()); + + if (results.size() == 1) { + final Ctap2Session.AssertionData assertion = results.get(0).first; + final ClientExtensionResults clientExtensionResults = results.get(0).second; + + return PublicKeyCredential.fromAssertion( + assertion, clientDataJson, allowCredentials, clientExtensionResults); + } else { + throw new MultipleAssertionsAvailable(clientDataJson, results); + } + + } catch (CtapException e) { + if (e.getCtapError() == CtapException.ERR_PIN_INVALID) { + throw new PinInvalidClientError(e, clientPin.getPinRetries().getCount()); + } + throw ClientError.wrapCtapException(e); } - - public UserAgentConfiguration getUserAgentConfiguration() { - return userAgentConfiguration; + } + + /** + * Check if the Authenticator supports external PIN. + * + * @return If PIN is supported. + */ + public boolean isPinSupported() { + return pinSupported; + } + + /** + * Check if the Authenticator has been configured with a PIN. + * + * @return If a PIN is configured. + */ + public boolean isPinConfigured() { + return pinConfigured; + } + + /** + * Check if the Authenticator supports Enterprise Attestation feature. + * + * @return true if the authenticator is enterprise attestation capable and enterprise attestation + * is enabled. + * @see Enterprise + * Attestation + */ + public boolean isEnterpriseAttestationSupported() { + return enterpriseAttestationSupported; + } + + /** + * Set the PIN for an Authenticator which supports PIN, but doesn't have one configured. + * + * @param pin The PIN to set. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @throws ClientError A higher level error. + */ + public void setPin(char[] pin) throws IOException, CommandException, ClientError { + if (!pinSupported) { + throw new ClientError(ClientError.Code.BAD_REQUEST, "PIN is not supported on this device"); } - - /** - * Create a new WebAuthn credential. - *

- * PIN is always required if a PIN is configured. - * - * @param clientDataJson The UTF-8 encoded ClientData JSON object. - * @param options The options for creating the credential. - * @param effectiveDomain The effective domain for the request, which is used to validate the RP ID against. - * @param pin If needed, the PIN to authorize the credential creation. - * @param state If needed, the state to provide control over the ongoing operation - * @return A WebAuthn public key credential. - * @throws IOException A communication error in the transport layer - * @throws CommandException A communication in the protocol layer - * @throws ClientError A higher level error - */ - public PublicKeyCredential makeCredential( - byte[] clientDataJson, - PublicKeyCredentialCreationOptions options, - String effectiveDomain, - @Nullable char[] pin, - @Nullable Integer enterpriseAttestation, - @Nullable CommandState state - ) throws IOException, CommandException, ClientError { - byte[] clientDataHash = Utils.hash(clientDataJson); - - try { - Pair result = ctapMakeCredential( - clientDataHash, - options, - effectiveDomain, - pin, - enterpriseAttestation, - state - ); - final Ctap2Session.CredentialData credential = result.first; - final ClientExtensionResults clientExtensionResults = result.second; - - final AttestationObject attestationObject = AttestationObject.fromCredential(credential); - - AuthenticatorAttestationResponse response = new AuthenticatorAttestationResponse( - clientDataJson, - ctap.getCachedInfo().getTransports(), - attestationObject - ); - - return new PublicKeyCredential( - Objects.requireNonNull(attestationObject.getAuthenticatorData() - .getAttestedCredentialData()).getCredentialId(), - response, - clientExtensionResults); - } catch (CtapException e) { - if (e.getCtapError() == CtapException.ERR_PIN_INVALID) { - throw new PinInvalidClientError(e, clientPin.getPinRetries().getCount()); - } - throw ClientError.wrapCtapException(e); - } + if (pinConfigured) { + throw new ClientError( + ClientError.Code.BAD_REQUEST, "A PIN is already configured on this device"); } - - /** - * Authenticate an existing WebAuthn credential. - * PIN is required if UV is "required", or if UV is "preferred" and a PIN is configured. - * If no allowCredentials list is provided (which is the case for a passwordless flow) the Authenticator may contain multiple discoverable credentials for the given RP. - * In such cases MultipleAssertionsAvailable will be thrown, and can be handled to select an assertion. - * - * @param clientDataJson The UTF-8 encoded ClientData JSON object. - * @param options The options for authenticating the credential. - * @param effectiveDomain The effective domain for the request, which is used to validate the RP ID against. - * @param pin If needed, the PIN to authorize the credential creation. - * @param state If needed, the state to provide control over the ongoing operation - * @return Webauthn public key credential with assertion response data. - * @throws MultipleAssertionsAvailable In case of multiple assertions, catch this to make a selection and get the result. - * @throws IOException A communication error in the transport layer - * @throws CommandException A communication in the protocol layer - * @throws ClientError A higher level error - */ - public PublicKeyCredential getAssertion( - byte[] clientDataJson, - PublicKeyCredentialRequestOptions options, - String effectiveDomain, - @Nullable char[] pin, - @Nullable CommandState state - ) throws MultipleAssertionsAvailable, IOException, CommandException, ClientError { - byte[] clientDataHash = Utils.hash(clientDataJson); - try { - final List> results = ctapGetAssertions( - clientDataHash, - options, - effectiveDomain, - pin, - state - ); - - final List allowCredentials = removeUnsupportedCredentials( - options.getAllowCredentials() - ); - - if (results.size() == 1) { - final Ctap2Session.AssertionData assertion = results.get(0).first; - final ClientExtensionResults clientExtensionResults = results.get(0).second; - - return PublicKeyCredential.fromAssertion( - assertion, - clientDataJson, - allowCredentials, - clientExtensionResults); - } else { - throw new MultipleAssertionsAvailable(clientDataJson, results); - } - - } catch (CtapException e) { - if (e.getCtapError() == CtapException.ERR_PIN_INVALID) { - throw new PinInvalidClientError(e, clientPin.getPinRetries().getCount()); - } - throw ClientError.wrapCtapException(e); - } + try { + clientPin.setPin(pin); + pinConfigured = true; + } catch (CtapException e) { + throw ClientError.wrapCtapException(e); } - /** - * Check if the Authenticator supports external PIN. - * - * @return If PIN is supported. - */ - public boolean isPinSupported() { - return pinSupported; + } + + /** + * Change the PIN for an Authenticator which already has a PIN configured. + * + * @param currentPin The current PIN, to authorize the action. + * @param newPin The new PIN to set. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @throws ClientError A higher level error. + */ + public void changePin(char[] currentPin, char[] newPin) + throws IOException, CommandException, ClientError { + if (!pinSupported) { + throw new ClientError(ClientError.Code.BAD_REQUEST, "PIN is not supported on this device"); } - - /** - * Check if the Authenticator has been configured with a PIN. - * - * @return If a PIN is configured. - */ - public boolean isPinConfigured() { - return pinConfigured; + if (!pinConfigured) { + throw new ClientError( + ClientError.Code.BAD_REQUEST, "No PIN currently configured on this device"); } - - /** - * Check if the Authenticator supports Enterprise Attestation feature. - * - * @return true if the authenticator is enterprise attestation capable and enterprise - * attestation is enabled. - * @see Enterprise Attestation - */ - public boolean isEnterpriseAttestationSupported() { - return enterpriseAttestationSupported; + try { + clientPin.changePin(currentPin, newPin); + } catch (CtapException e) { + throw ClientError.wrapCtapException(e); } - - /** - * Set the PIN for an Authenticator which supports PIN, but doesn't have one configured. - * - * @param pin The PIN to set. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @throws ClientError A higher level error. - */ - public void setPin(char[] pin) throws IOException, CommandException, ClientError { - if (!pinSupported) { - throw new ClientError(ClientError.Code.BAD_REQUEST, "PIN is not supported on this device"); - } - if (pinConfigured) { - throw new ClientError(ClientError.Code.BAD_REQUEST, "A PIN is already configured on this device"); - } - try { - clientPin.setPin(pin); - pinConfigured = true; - } catch (CtapException e) { - throw ClientError.wrapCtapException(e); - } + } + + /** + * Return an object that provides management of resident key type credentials stored on a YubiKey + * + * @param pin The configured PIN + * @return Credential manager + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @throws ClientError A higher level error. + */ + public CredentialManager getCredentialManager(char[] pin) + throws IOException, CommandException, ClientError { + if (!pinConfigured) { + throw new ClientError( + ClientError.Code.BAD_REQUEST, "No PIN currently configured on this device"); } - - /** - * Change the PIN for an Authenticator which already has a PIN configured. - * - * @param currentPin The current PIN, to authorize the action. - * @param newPin The new PIN to set. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @throws ClientError A higher level error. - */ - public void changePin(char[] currentPin, char[] newPin) throws IOException, CommandException, ClientError { - if (!pinSupported) { - throw new ClientError(ClientError.Code.BAD_REQUEST, "PIN is not supported on this device"); - } - if (!pinConfigured) { - throw new ClientError(ClientError.Code.BAD_REQUEST, "No PIN currently configured on this device"); - } - try { - clientPin.changePin(currentPin, newPin); - } catch (CtapException e) { - throw ClientError.wrapCtapException(e); - } + try { + return new CredentialManager( + new CredentialManagement( + ctap, + clientPin.getPinUvAuth(), + clientPin.getPinToken(pin, ClientPin.PIN_PERMISSION_CM, null))); + } catch (CtapException e) { + throw ClientError.wrapCtapException(e); } - - /** - * Return an object that provides management of resident key type credentials stored on a YubiKey - * - * @param pin The configured PIN - * @return Credential manager - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @throws ClientError A higher level error. - */ - public CredentialManager getCredentialManager(char[] pin) - throws IOException, CommandException, ClientError { - if (!pinConfigured) { - throw new ClientError(ClientError.Code.BAD_REQUEST, - "No PIN currently configured on this device"); - } - try { - return new CredentialManager( - new CredentialManagement( - ctap, - clientPin.getPinUvAuth(), - clientPin.getPinToken(pin, ClientPin.PIN_PERMISSION_CM, null) - ) - ); - } catch (CtapException e) { - throw ClientError.wrapCtapException(e); - } + } + + /** + * Create a new WebAuthn credential. + * + *

This method is used internally in YubiKit and is not part of the public API. It may be + * changed or removed at any time. + * + *

PIN is always required if a PIN is configured. + * + * @param clientDataHash Hash of client data. + * @param options The options for creating the credential. + * @param effectiveDomain The effective domain for the request, which is used to validate the RP + * ID against. + * @param pin If needed, the PIN to authorize the credential creation. + * @param state If needed, the state to provide control over the ongoing operation + * @return A pair of credential data and client extension results. + * @throws IOException A communication error in the transport layer + * @throws CommandException A communication in the protocol layer + * @throws ClientError A higher level error + */ + @SuppressWarnings("unchecked") + protected Pair ctapMakeCredential( + byte[] clientDataHash, + PublicKeyCredentialCreationOptions options, + String effectiveDomain, + @Nullable char[] pin, + @Nullable Integer enterpriseAttestation, + @Nullable CommandState state) + throws IOException, CommandException, ClientError { + + final SerializationType serializationType = SerializationType.CBOR; + + Map rp = options.getRp().toMap(serializationType); + String rpId = options.getRp().getId(); + if (rpId == null) { + ((Map) rp).put("id", effectiveDomain); + } else if (!(effectiveDomain.equals(rpId) || effectiveDomain.endsWith("." + rpId))) { + throw new ClientError( + ClientError.Code.BAD_REQUEST, "RP ID is not valid for effective domain"); } - /** - * Create a new WebAuthn credential. - *

- * This method is used internally in YubiKit and is not part of the public API. It may be changed - * or removed at any time. - *

- * PIN is always required if a PIN is configured. - * - * @param clientDataHash Hash of client data. - * @param options The options for creating the credential. - * @param effectiveDomain The effective domain for the request, which is used to validate the RP ID against. - * @param pin If needed, the PIN to authorize the credential creation. - * @param state If needed, the state to provide control over the ongoing operation - * @return A pair of credential data and client extension results. - * @throws IOException A communication error in the transport layer - * @throws CommandException A communication in the protocol layer - * @throws ClientError A higher level error - */ - @SuppressWarnings("unchecked") - protected Pair ctapMakeCredential( - byte[] clientDataHash, - PublicKeyCredentialCreationOptions options, - String effectiveDomain, - @Nullable char[] pin, - @Nullable Integer enterpriseAttestation, - @Nullable CommandState state - ) throws IOException, CommandException, ClientError { - - final SerializationType serializationType = SerializationType.CBOR; - - Map rp = options.getRp().toMap(serializationType); - String rpId = options.getRp().getId(); - if (rpId == null) { - ((Map) rp).put("id", effectiveDomain); - } else if (!(effectiveDomain.equals(rpId) || effectiveDomain.endsWith("." + rpId))) { - throw new ClientError( - ClientError.Code.BAD_REQUEST, - "RP ID is not valid for effective domain"); - } - - Map ctapOptions = getCreateCtapOptions(options, pin); + Map ctapOptions = getCreateCtapOptions(options, pin); - int permissions = ClientPin.PIN_PERMISSION_MC; - if (!options.getExcludeCredentials().isEmpty()) { - permissions |= ClientPin.PIN_PERMISSION_GA; - } - List registrationProcessors = new ArrayList<>(); - for (Extension extension : extensions) { - Extension.RegistrationProcessor processor = - extension.makeCredential(ctap, options, clientPin.getPinUvAuth()); - if (processor != null) { - registrationProcessors.add(processor); - permissions |= processor.getPermissions(); - } - } - - final AuthParams authParams = getAuthParams( - clientDataHash, - ctapOptions.containsKey(OPTION_USER_VERIFICATION), - pin, - permissions, - rpId); + int permissions = ClientPin.PIN_PERMISSION_MC; + if (!options.getExcludeCredentials().isEmpty()) { + permissions |= ClientPin.PIN_PERMISSION_GA; + } + List registrationProcessors = new ArrayList<>(); + for (Extension extension : extensions) { + Extension.RegistrationProcessor processor = + extension.makeCredential(ctap, options, clientPin.getPinUvAuth()); + if (processor != null) { + registrationProcessors.add(processor); + permissions |= processor.getPermissions(); + } + } - HashMap authenticatorInputs = new HashMap<>(); - for (Extension.RegistrationProcessor processor : registrationProcessors) { - authenticatorInputs.putAll(processor.getInput(authParams.pinToken)); - } + final AuthParams authParams = + getAuthParams( + clientDataHash, + ctapOptions.containsKey(OPTION_USER_VERIFICATION), + pin, + permissions, + rpId); + + HashMap authenticatorInputs = new HashMap<>(); + for (Extension.RegistrationProcessor processor : registrationProcessors) { + authenticatorInputs.putAll(processor.getInput(authParams.pinToken)); + } - final List excludeCredentials = - removeUnsupportedCredentials( - options.getExcludeCredentials() - ); + final List excludeCredentials = + removeUnsupportedCredentials(options.getExcludeCredentials()); - PublicKeyCredentialDescriptor credToExclude = excludeCredentials != null - ? Utils.filterCreds( + PublicKeyCredentialDescriptor credToExclude = + excludeCredentials != null + ? Utils.filterCreds( ctap, rpId, excludeCredentials, effectiveDomain, authParams.pinUvAuthProtocol, authParams.pinToken) - : null; - - final Map user = options.getUser().toMap(serializationType); + : null; - List> pubKeyCredParams = new ArrayList<>(); - for (PublicKeyCredentialParameters param : options.getPubKeyCredParams()) { - if (isPublicKeyCredentialTypeSupported(param.getType())) { - pubKeyCredParams.add(param.toMap(serializationType)); - } - } + final Map user = options.getUser().toMap(serializationType); - @Nullable Integer validatedEnterpriseAttestation = null; - if (isEnterpriseAttestationSupported() && - AttestationConveyancePreference.ENTERPRISE.equals(options.getAttestation()) && - userAgentConfiguration.supportsEpForRpId(rpId) && - enterpriseAttestation != null && - (enterpriseAttestation == 1 || enterpriseAttestation == 2)) { - validatedEnterpriseAttestation = enterpriseAttestation; - } - - Ctap2Session.CredentialData credentialData = ctap.makeCredential( - clientDataHash, - rp, - user, - pubKeyCredParams, - credToExclude != null - ? Utils.getCredentialList(Collections.singletonList(credToExclude)) - : null, - authenticatorInputs, - ctapOptions.isEmpty() ? null : ctapOptions, - authParams.pinUvAuthParam, - authParams.pinUvAuthParam != null && authParams.pinUvAuthProtocol != null - ? authParams.pinUvAuthProtocol.getVersion() - : null, - validatedEnterpriseAttestation, - state - ); - - ClientExtensionResults clientExtensionResults = new ClientExtensionResults(); - for (Extension.RegistrationProcessor processor : registrationProcessors) { - AttestationObject attestationObject = AttestationObject.fromCredential(credentialData); - clientExtensionResults.add(processor.getOutput(attestationObject, authParams.pinToken)); - } - return new Pair<>(credentialData, clientExtensionResults); + List> pubKeyCredParams = new ArrayList<>(); + for (PublicKeyCredentialParameters param : options.getPubKeyCredParams()) { + if (isPublicKeyCredentialTypeSupported(param.getType())) { + pubKeyCredParams.add(param.toMap(serializationType)); + } } - /** - * Authenticate an existing WebAuthn credential. - *

- * This method is used internally in YubiKit and is not part of the public API. It may be changed - * or removed at any time. - *

- * PIN is required if UV is "required", or if UV is "preferred" and a PIN is configured. - * If no allowCredentials list is provided (which is the case for a passwordless flow) the Authenticator may contain multiple discoverable credentials for the given RP. - * In such cases MultipleAssertionsAvailable will be thrown, and can be handled to select an assertion. - * - * @param clientDataHash Hash of client data. - * @param options The options for authenticating the credential. - * @param effectiveDomain The effective domain for the request, which is used to validate the RP ID against. - * @param pin If needed, the PIN to authorize the credential creation. - * @param state If needed, the state to provide control over the ongoing operation - * @return List of pairs containing assertion response data and client extension results. - * @throws IOException A communication error in the transport layer - * @throws CommandException A communication in the protocol layer - * @throws ClientError A higher level error - */ - protected List> ctapGetAssertions( - byte[] clientDataHash, - PublicKeyCredentialRequestOptions options, - String effectiveDomain, - @Nullable char[] pin, - @Nullable CommandState state - ) throws IOException, CommandException, ClientError { - String rpId = options.getRpId(); - if (rpId == null) { - rpId = effectiveDomain; - } else if (!(effectiveDomain.equals(rpId) || effectiveDomain.endsWith("." + rpId))) { - throw new ClientError(ClientError.Code.BAD_REQUEST, "RP ID is not valid for effective domain"); - } - Map ctapOptions = getRequestCtapOptions(options, pin); - final List allowCredentials = removeUnsupportedCredentials( - options.getAllowCredentials() - ); - - int permissions = ClientPin.PIN_PERMISSION_GA; - List authenticationProcessors = new ArrayList<>(); - for (Extension extension : extensions) { - Extension.AuthenticationProcessor processor = - extension.getAssertion(ctap, options, clientPin.getPinUvAuth()); - if (processor != null) { - authenticationProcessors.add(processor); - permissions |= processor.getPermissions(); - } - } + @Nullable Integer validatedEnterpriseAttestation = null; + if (isEnterpriseAttestationSupported() + && AttestationConveyancePreference.ENTERPRISE.equals(options.getAttestation()) + && userAgentConfiguration.supportsEpForRpId(rpId) + && enterpriseAttestation != null + && (enterpriseAttestation == 1 || enterpriseAttestation == 2)) { + validatedEnterpriseAttestation = enterpriseAttestation; + } - final AuthParams authParams = getAuthParams( - clientDataHash, - ctapOptions.containsKey(OPTION_USER_VERIFICATION), - pin, - permissions, - rpId); + Ctap2Session.CredentialData credentialData = + ctap.makeCredential( + clientDataHash, + rp, + user, + pubKeyCredParams, + credToExclude != null + ? Utils.getCredentialList(Collections.singletonList(credToExclude)) + : null, + authenticatorInputs, + ctapOptions.isEmpty() ? null : ctapOptions, + authParams.pinUvAuthParam, + authParams.pinUvAuthParam != null && authParams.pinUvAuthProtocol != null + ? authParams.pinUvAuthProtocol.getVersion() + : null, + validatedEnterpriseAttestation, + state); + + ClientExtensionResults clientExtensionResults = new ClientExtensionResults(); + for (Extension.RegistrationProcessor processor : registrationProcessors) { + AttestationObject attestationObject = AttestationObject.fromCredential(credentialData); + clientExtensionResults.add(processor.getOutput(attestationObject, authParams.pinToken)); + } + return new Pair<>(credentialData, clientExtensionResults); + } + + /** + * Authenticate an existing WebAuthn credential. + * + *

This method is used internally in YubiKit and is not part of the public API. It may be + * changed or removed at any time. + * + *

PIN is required if UV is "required", or if UV is "preferred" and a PIN is configured. If no + * allowCredentials list is provided (which is the case for a passwordless flow) the Authenticator + * may contain multiple discoverable credentials for the given RP. In such cases + * MultipleAssertionsAvailable will be thrown, and can be handled to select an assertion. + * + * @param clientDataHash Hash of client data. + * @param options The options for authenticating the credential. + * @param effectiveDomain The effective domain for the request, which is used to validate the RP + * ID against. + * @param pin If needed, the PIN to authorize the credential creation. + * @param state If needed, the state to provide control over the ongoing operation + * @return List of pairs containing assertion response data and client extension results. + * @throws IOException A communication error in the transport layer + * @throws CommandException A communication in the protocol layer + * @throws ClientError A higher level error + */ + protected List> ctapGetAssertions( + byte[] clientDataHash, + PublicKeyCredentialRequestOptions options, + String effectiveDomain, + @Nullable char[] pin, + @Nullable CommandState state) + throws IOException, CommandException, ClientError { + String rpId = options.getRpId(); + if (rpId == null) { + rpId = effectiveDomain; + } else if (!(effectiveDomain.equals(rpId) || effectiveDomain.endsWith("." + rpId))) { + throw new ClientError( + ClientError.Code.BAD_REQUEST, "RP ID is not valid for effective domain"); + } + Map ctapOptions = getRequestCtapOptions(options, pin); + final List allowCredentials = + removeUnsupportedCredentials(options.getAllowCredentials()); + + int permissions = ClientPin.PIN_PERMISSION_GA; + List authenticationProcessors = new ArrayList<>(); + for (Extension extension : extensions) { + Extension.AuthenticationProcessor processor = + extension.getAssertion(ctap, options, clientPin.getPinUvAuth()); + if (processor != null) { + authenticationProcessors.add(processor); + permissions |= processor.getPermissions(); + } + } - PublicKeyCredentialDescriptor selectedCred = allowCredentials != null && !allowCredentials.isEmpty() - ? Utils.filterCreds( + final AuthParams authParams = + getAuthParams( + clientDataHash, + ctapOptions.containsKey(OPTION_USER_VERIFICATION), + pin, + permissions, + rpId); + + PublicKeyCredentialDescriptor selectedCred = + allowCredentials != null && !allowCredentials.isEmpty() + ? Utils.filterCreds( ctap, rpId, allowCredentials, effectiveDomain, authParams.pinUvAuthProtocol, authParams.pinToken) - : null; + : null; - HashMap authenticatorInputs = new HashMap<>(); - for (Extension.AuthenticationProcessor processor : authenticationProcessors) { - authenticatorInputs.putAll(processor.getInput(selectedCred, authParams.pinToken)); - } - - try { - List assertions = ctap.getAssertions( - rpId, - clientDataHash, - selectedCred != null - ? Utils.getCredentialList(Collections.singletonList(selectedCred)) - : null, - authenticatorInputs, - ctapOptions.isEmpty() ? null : ctapOptions, - authParams.pinUvAuthParam, - authParams.pinUvAuthParam != null && authParams.pinUvAuthProtocol != null - ? authParams.pinUvAuthProtocol.getVersion() - : null, - state - ); - - List> result = new ArrayList<>(); - for(final Ctap2Session.AssertionData assertionData : assertions) { - ClientExtensionResults clientExtensionResults = new ClientExtensionResults(); - for (Extension.AuthenticationProcessor processor : authenticationProcessors) { - clientExtensionResults.add(processor.getOutput(assertionData, authParams.pinToken)); - } - result.add(new Pair<>(assertionData, clientExtensionResults)); - } - return result; - - } catch (CtapException exc) { - if (exc.getCtapError() == CtapException.ERR_PIN_INVALID) { - throw new PinInvalidClientError(exc, clientPin.getPinRetries().getCount()); - } - throw ClientError.wrapCtapException(exc); - } + HashMap authenticatorInputs = new HashMap<>(); + for (Extension.AuthenticationProcessor processor : authenticationProcessors) { + authenticatorInputs.putAll(processor.getInput(selectedCred, authParams.pinToken)); } - /* - * Calculates what the CTAP "uv" option should be based on the configuration of the authenticator, - * the UserVerification parameter to the request, and whether or not a PIN was provided. - */ - private boolean getCtapUv(String userVerification, boolean pinProvided) throws ClientError { - if (pinProvided) { - if (!pinConfigured) { - throw new ClientError(ClientError.Code.BAD_REQUEST, "PIN provided but not configured"); - } - // If a PIN was provided this will satisfy the UserVerification requirement regardless of what it is, without requiring uv. - return false; + try { + List assertions = + ctap.getAssertions( + rpId, + clientDataHash, + selectedCred != null + ? Utils.getCredentialList(Collections.singletonList(selectedCred)) + : null, + authenticatorInputs, + ctapOptions.isEmpty() ? null : ctapOptions, + authParams.pinUvAuthParam, + authParams.pinUvAuthParam != null && authParams.pinUvAuthProtocol != null + ? authParams.pinUvAuthProtocol.getVersion() + : null, + state); + + List> result = new ArrayList<>(); + for (final Ctap2Session.AssertionData assertionData : assertions) { + ClientExtensionResults clientExtensionResults = new ClientExtensionResults(); + for (Extension.AuthenticationProcessor processor : authenticationProcessors) { + clientExtensionResults.add(processor.getOutput(assertionData, authParams.pinToken)); } + result.add(new Pair<>(assertionData, clientExtensionResults)); + } + return result; - boolean pinUvSupported = pinSupported || uvSupported; - - // No PIN provided - switch (userVerification) { - case UserVerificationRequirement.DISCOURAGED: - // Discouraged, uv = false. - return false; - default: - case UserVerificationRequirement.PREFERRED: - if (!pinUvSupported) { - // No Authenticator support, uv = false - return false; - } - //Fall through to REQUIRED since we have support for either PIN or uv. - case UserVerificationRequirement.REQUIRED: - if (!uvConfigured) { - // Can't satisfy UserVerification, fail. - if (pinConfigured) { - throw new PinRequiredClientError(); - } else { - if (pinUvSupported) { - throw new ClientError(ClientError.Code.BAD_REQUEST, "User verification not configured"); - } - throw new ClientError(ClientError.Code.CONFIGURATION_UNSUPPORTED, "User verification not supported"); - } - } - // uv is configured, uv = true. - return true; - } + } catch (CtapException exc) { + if (exc.getCtapError() == CtapException.ERR_PIN_INVALID) { + throw new PinInvalidClientError(exc, clientPin.getPinRetries().getCount()); + } + throw ClientError.wrapCtapException(exc); + } + } + + /* + * Calculates what the CTAP "uv" option should be based on the configuration of the authenticator, + * the UserVerification parameter to the request, and whether or not a PIN was provided. + */ + private boolean getCtapUv(String userVerification, boolean pinProvided) throws ClientError { + if (pinProvided) { + if (!pinConfigured) { + throw new ClientError(ClientError.Code.BAD_REQUEST, "PIN provided but not configured"); + } + // If a PIN was provided this will satisfy the UserVerification requirement regardless of what + // it is, without requiring uv. + return false; } - private Map getCreateCtapOptions( - PublicKeyCredentialCreationOptions publicKeyCredentialCreationOptions, - @Nullable char[] pin) throws ClientError { - Map ctapOptions = new HashMap<>(); - AuthenticatorSelectionCriteria authenticatorSelection = - publicKeyCredentialCreationOptions.getAuthenticatorSelection(); - if (authenticatorSelection != null) { - String residentKeyRequirement = authenticatorSelection.getResidentKey(); - if (ResidentKeyRequirement.REQUIRED.equals(residentKeyRequirement) || - (ResidentKeyRequirement.PREFERRED.equals(residentKeyRequirement) && rkSupported) - ) { - ctapOptions.put(OPTION_RESIDENT_KEY, true); - } - if (getCtapUv(authenticatorSelection.getUserVerification(), pin != null)) { - ctapOptions.put(OPTION_USER_VERIFICATION, true); - } - } else { - if (getCtapUv(UserVerificationRequirement.PREFERRED, pin != null)) { - ctapOptions.put(OPTION_USER_VERIFICATION, true); + boolean pinUvSupported = pinSupported || uvSupported; + + // No PIN provided + switch (userVerification) { + case UserVerificationRequirement.DISCOURAGED: + // Discouraged, uv = false. + return false; + default: + case UserVerificationRequirement.PREFERRED: + if (!pinUvSupported) { + // No Authenticator support, uv = false + return false; + } + // Fall through to REQUIRED since we have support for either PIN or uv. + case UserVerificationRequirement.REQUIRED: + if (!uvConfigured) { + // Can't satisfy UserVerification, fail. + if (pinConfigured) { + throw new PinRequiredClientError(); + } else { + if (pinUvSupported) { + throw new ClientError( + ClientError.Code.BAD_REQUEST, "User verification not configured"); } + throw new ClientError( + ClientError.Code.CONFIGURATION_UNSUPPORTED, "User verification not supported"); + } } - return ctapOptions; + // uv is configured, uv = true. + return true; + } + } + + private Map getCreateCtapOptions( + PublicKeyCredentialCreationOptions publicKeyCredentialCreationOptions, @Nullable char[] pin) + throws ClientError { + Map ctapOptions = new HashMap<>(); + AuthenticatorSelectionCriteria authenticatorSelection = + publicKeyCredentialCreationOptions.getAuthenticatorSelection(); + if (authenticatorSelection != null) { + String residentKeyRequirement = authenticatorSelection.getResidentKey(); + if (ResidentKeyRequirement.REQUIRED.equals(residentKeyRequirement) + || (ResidentKeyRequirement.PREFERRED.equals(residentKeyRequirement) && rkSupported)) { + ctapOptions.put(OPTION_RESIDENT_KEY, true); + } + if (getCtapUv(authenticatorSelection.getUserVerification(), pin != null)) { + ctapOptions.put(OPTION_USER_VERIFICATION, true); + } + } else { + if (getCtapUv(UserVerificationRequirement.PREFERRED, pin != null)) { + ctapOptions.put(OPTION_USER_VERIFICATION, true); + } } + return ctapOptions; + } + + private Map getRequestCtapOptions( + PublicKeyCredentialRequestOptions options, @Nullable char[] pin) throws ClientError { + Map ctapOptions = new HashMap<>(); + if (getCtapUv(options.getUserVerification(), pin != null)) { + ctapOptions.put(OPTION_USER_VERIFICATION, true); + } + return ctapOptions; + } + + private AuthParams getAuthParams( + byte[] clientDataHash, + boolean shouldUv, + @Nullable char[] pin, + @Nullable Integer permissions, + @Nullable String rpId) + throws ClientError, IOException, CommandException { + @Nullable byte[] authToken = null; + @Nullable byte[] authParam = null; + + if (pin != null) { + authToken = clientPin.getPinToken(pin, permissions, rpId); + authParam = clientPin.getPinUvAuth().authenticate(authToken, clientDataHash); + } else if (pinConfigured) { + if (shouldUv && uvConfigured) { + if (ClientPin.isTokenSupported(ctap.getCachedInfo())) { + authToken = clientPin.getUvToken(permissions, rpId, null); + authParam = clientPin.getPinUvAuth().authenticate(authToken, clientDataHash); + } + // no authToken is created means that internal UV is used + } else { + // the authenticator supports pin but no PIN was provided + throw new PinRequiredClientError(); + } + } + return new AuthParams(authToken, clientPin.getPinUvAuth(), authParam); + } - private Map getRequestCtapOptions( - PublicKeyCredentialRequestOptions options, - @Nullable char[] pin) throws ClientError { - Map ctapOptions = new HashMap<>(); - if (getCtapUv(options.getUserVerification(), pin != null)) { - ctapOptions.put(OPTION_USER_VERIFICATION, true); + /** + * Calculates the preferred pinUvAuth protocol for authenticator provided list. Returns + * PinUvAuthDummyProtocol if the authenticator does not support any of the SDK supported + * protocols. + */ + private PinUvAuthProtocol getPreferredPinUvAuthProtocol(List pinUvAuthProtocols) { + if (pinSupported) { + for (int protocol : pinUvAuthProtocols) { + if (protocol == PinUvAuthProtocolV1.VERSION) { + return new PinUvAuthProtocolV1(); } - return ctapOptions; - } - - private AuthParams getAuthParams( - byte[] clientDataHash, - boolean shouldUv, - @Nullable char[] pin, - @Nullable Integer permissions, - @Nullable String rpId - ) throws ClientError, IOException, CommandException { - @Nullable byte[] authToken = null; - @Nullable byte[] authParam = null; - - if (pin != null) { - authToken = clientPin.getPinToken(pin, permissions, rpId); - authParam = clientPin.getPinUvAuth().authenticate(authToken, clientDataHash); - } else if (pinConfigured) { - if (shouldUv && uvConfigured) { - if (ClientPin.isTokenSupported(ctap.getCachedInfo())) { - authToken = clientPin.getUvToken(permissions, rpId, null); - authParam = clientPin.getPinUvAuth().authenticate(authToken, clientDataHash); - } - // no authToken is created means that internal UV is used - } else { - // the authenticator supports pin but no PIN was provided - throw new PinRequiredClientError(); - } + + if (protocol == PinUvAuthProtocolV2.VERSION) { + return new PinUvAuthProtocolV2(); } - return new AuthParams( - authToken, - clientPin.getPinUvAuth(), - authParam); + } } - /** - * Calculates the preferred pinUvAuth protocol for authenticator provided list. - * Returns PinUvAuthDummyProtocol if the authenticator does not support any of the SDK - * supported protocols. - */ - private PinUvAuthProtocol getPreferredPinUvAuthProtocol(List pinUvAuthProtocols) { - if (pinSupported) { - for (int protocol : pinUvAuthProtocols) { - if (protocol == PinUvAuthProtocolV1.VERSION) { - return new PinUvAuthProtocolV1(); - } - - if (protocol == PinUvAuthProtocolV2.VERSION) { - return new PinUvAuthProtocolV2(); - } - } - } + return new PinUvAuthDummyProtocol(); + } - return new PinUvAuthDummyProtocol(); - } + private static boolean isPublicKeyCredentialTypeSupported(String type) { + return PUBLIC_KEY.equals(type); + } - private static boolean isPublicKeyCredentialTypeSupported(String type) { - return PUBLIC_KEY.equals(type); + /** + * @return new list containing only descriptors with valid {@code PublicKeyCredentialType} type + */ + @Nullable private static List removeUnsupportedCredentials( + @Nullable List descriptors) { + if (descriptors == null || descriptors.isEmpty()) { + return descriptors; } - /** - * @return new list containing only descriptors with valid {@code PublicKeyCredentialType} type - */ - @Nullable - private static List removeUnsupportedCredentials( - @Nullable List descriptors - ) { - if (descriptors == null || descriptors.isEmpty()) { - return descriptors; - } - - final List list = new ArrayList<>(); - for (PublicKeyCredentialDescriptor credential : descriptors) { - if (isPublicKeyCredentialTypeSupported(credential.getType())) { - list.add(credential); - } - } - return list; - } - - private static final List defaultExtensions = Arrays.asList( - new CredPropsExtension(), - new CredBlobExtension(), - new CredProtectExtension(), - new HmacSecretExtension(), - new MinPinLengthExtension(), - new LargeBlobExtension() - ); - - static class Utils { - - /** - * @return first acceptable credential from the list available on the authenticator - */ - @Nullable - static PublicKeyCredentialDescriptor filterCreds( - Ctap2Session ctap, - @Nullable String rpId, - List descriptors, - String effectiveDomain, - @Nullable PinUvAuthProtocol pinUvAuthProtocol, - @Nullable byte[] pinUvAuthToken - ) throws IOException, CommandException, ClientError { - - if (rpId == null) { - rpId = effectiveDomain; - } else if (!(effectiveDomain.equals(rpId) || effectiveDomain.endsWith("." + rpId))) { - throw new ClientError(ClientError.Code.BAD_REQUEST, "RP ID is not valid for effective domain"); - } + final List list = new ArrayList<>(); + for (PublicKeyCredentialDescriptor credential : descriptors) { + if (isPublicKeyCredentialTypeSupported(credential.getType())) { + list.add(credential); + } + } + return list; + } - List creds; - - // filter out credential IDs which are too long - Ctap2Session.InfoData info = ctap.getCachedInfo(); - Integer maxCredIdLength = info.getMaxCredentialIdLength(); - if (maxCredIdLength != null) { - creds = new ArrayList<>(); - for (PublicKeyCredentialDescriptor desc : descriptors) { - if (desc.getId().length <= maxCredIdLength) { - creds.add(desc); - } - } - } else { - creds = descriptors; - } + private static final List defaultExtensions = + Arrays.asList( + new CredPropsExtension(), + new CredBlobExtension(), + new CredProtectExtension(), + new HmacSecretExtension(), + new MinPinLengthExtension(), + new LargeBlobExtension()); - int maxCreds = info.getMaxCredentialCountInList() != null - ? info.getMaxCredentialCountInList() - : 1; + static class Utils { - List> chunks = new ArrayList<>(); - for (int i = 0; i < creds.size(); i += maxCreds) { - int last = Math.min(i + maxCreds, creds.size()); - chunks.add(creds.subList(i, last)); - } + /** + * @return first acceptable credential from the list available on the authenticator + */ + @Nullable static PublicKeyCredentialDescriptor filterCreds( + Ctap2Session ctap, + @Nullable String rpId, + List descriptors, + String effectiveDomain, + @Nullable PinUvAuthProtocol pinUvAuthProtocol, + @Nullable byte[] pinUvAuthToken) + throws IOException, CommandException, ClientError { + + if (rpId == null) { + rpId = effectiveDomain; + } else if (!(effectiveDomain.equals(rpId) || effectiveDomain.endsWith("." + rpId))) { + throw new ClientError( + ClientError.Code.BAD_REQUEST, "RP ID is not valid for effective domain"); + } + + List creds; + + // filter out credential IDs which are too long + Ctap2Session.InfoData info = ctap.getCachedInfo(); + Integer maxCredIdLength = info.getMaxCredentialIdLength(); + if (maxCredIdLength != null) { + creds = new ArrayList<>(); + for (PublicKeyCredentialDescriptor desc : descriptors) { + if (desc.getId().length <= maxCredIdLength) { + creds.add(desc); + } + } + } else { + creds = descriptors; + } + + int maxCreds = + info.getMaxCredentialCountInList() != null ? info.getMaxCredentialCountInList() : 1; + + List> chunks = new ArrayList<>(); + for (int i = 0; i < creds.size(); i += maxCreds) { + int last = Math.min(i + maxCreds, creds.size()); + chunks.add(creds.subList(i, last)); + } + + byte[] clientDataHash = new byte[32]; + Arrays.fill(clientDataHash, (byte) 0x00); + + byte[] pinAuth = null; + Integer pinUvAuthVersion = null; + if (pinUvAuthToken != null && pinUvAuthProtocol != null) { + pinAuth = pinUvAuthProtocol.authenticate(pinUvAuthToken, clientDataHash); + pinUvAuthVersion = pinUvAuthProtocol.getVersion(); + } + + for (List chunk : chunks) { + try { + List assertions = + ctap.getAssertions( + rpId, + clientDataHash, + getCredentialList(chunk), + null, + Collections.singletonMap("up", false), + pinAuth, + pinUvAuthVersion, + null); - byte[] clientDataHash = new byte[32]; - Arrays.fill(clientDataHash, (byte) 0x00); + if (chunk.size() == 1) { + return chunk.get(0); + } - byte[] pinAuth = null; - Integer pinUvAuthVersion = null; - if (pinUvAuthToken != null && pinUvAuthProtocol != null) { - pinAuth = pinUvAuthProtocol.authenticate(pinUvAuthToken, clientDataHash); - pinUvAuthVersion = pinUvAuthProtocol.getVersion(); - } + final Ctap2Session.AssertionData assertion = assertions.get(0); + final byte[] id = assertion.getCredentialId(null); - for (List chunk : chunks) { - try { - List assertions = ctap.getAssertions( - rpId, - clientDataHash, - getCredentialList(chunk), - null, - Collections.singletonMap("up", false), - pinAuth, - pinUvAuthVersion, - null - ); - - if (chunk.size() == 1) { - return chunk.get(0); - } - - final Ctap2Session.AssertionData assertion = assertions.get(0); - final byte[] id = assertion.getCredentialId(null); - - return new PublicKeyCredentialDescriptor(PUBLIC_KEY, id); - - } catch (CtapException ctapException) { - if (ctapException.getCtapError() == CtapException.ERR_NO_CREDENTIALS) { - continue; - } - - throw ctapException; - } - } + return new PublicKeyCredentialDescriptor(PUBLIC_KEY, id); - return null; - } + } catch (CtapException ctapException) { + if (ctapException.getCtapError() == CtapException.ERR_NO_CREDENTIALS) { + continue; + } - /** - * @return new list of Credential descriptors for CBOR serialization. - */ - @Nullable - static List> getCredentialList( - @Nullable List descriptors) { - if (descriptors == null || descriptors.isEmpty()) { - return null; - } - List> creds = new ArrayList<>(); - for (PublicKeyCredentialDescriptor descriptor : descriptors) { - creds.add(descriptor.toMap(SerializationType.CBOR)); - } - return creds; + throw ctapException; } + } - /** - * Return SHA-256 hash of the provided input - * - * @param message The hash input - * @return SHA-256 of the input - */ - static byte[] hash(byte[] message) { - try { - return MessageDigest.getInstance("SHA-256").digest(message); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } + return null; + } + /** + * @return new list of Credential descriptors for CBOR serialization. + */ + @Nullable static List> getCredentialList( + @Nullable List descriptors) { + if (descriptors == null || descriptors.isEmpty()) { + return null; + } + List> creds = new ArrayList<>(); + for (PublicKeyCredentialDescriptor descriptor : descriptors) { + creds.add(descriptor.toMap(SerializationType.CBOR)); + } + return creds; } + /** + * Return SHA-256 hash of the provided input + * + * @param message The hash input + * @return SHA-256 of the input + */ + static byte[] hash(byte[] message) { + try { + return MessageDigest.getInstance("SHA-256").digest(message); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/client/ClientError.java b/fido/src/main/java/com/yubico/yubikit/fido/client/ClientError.java index d3a56e32..b3397a49 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/client/ClientError.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/client/ClientError.java @@ -17,95 +17,95 @@ package com.yubico.yubikit.fido.client; import com.yubico.yubikit.core.fido.CtapException; - import java.util.Locale; -/** - * An error thrown by the WebAuthn client upon failure to complete a command. - */ +/** An error thrown by the WebAuthn client upon failure to complete a command. */ public class ClientError extends Exception { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - /** - * Client error types - */ - public enum Code { - OTHER_ERROR(1), BAD_REQUEST(2), CONFIGURATION_UNSUPPORTED(3), DEVICE_INELIGIBLE(4), TIMEOUT(5); + /** Client error types */ + public enum Code { + OTHER_ERROR(1), + BAD_REQUEST(2), + CONFIGURATION_UNSUPPORTED(3), + DEVICE_INELIGIBLE(4), + TIMEOUT(5); - private final int errorCode; + private final int errorCode; - Code(int errorCode) { - this.errorCode = errorCode; - } + Code(int errorCode) { + this.errorCode = errorCode; + } - @SuppressWarnings("unused") - public int getErrorCode() { - return errorCode; - } + @SuppressWarnings("unused") + public int getErrorCode() { + return errorCode; + } - @Override - public String toString() { - return String.format(Locale.ROOT, "%s (code %d)", name(), errorCode); - } + @Override + public String toString() { + return String.format(Locale.ROOT, "%s (code %d)", name(), errorCode); } + } - private final Code errorCode; + private final Code errorCode; - public ClientError(Code errorCode, String message) { - super(errorCode + " - " + message); - this.errorCode = errorCode; - } + public ClientError(Code errorCode, String message) { + super(errorCode + " - " + message); + this.errorCode = errorCode; + } - public ClientError(Code errorCode, Throwable cause) { - super(errorCode.toString(), cause); - this.errorCode = errorCode; - } + public ClientError(Code errorCode, Throwable cause) { + super(errorCode.toString(), cause); + this.errorCode = errorCode; + } - @SuppressWarnings("unused") - public Code getErrorCode() { - return errorCode; - } - /** - * Translate CTAP errors into client errors - * - * @param error CTAP Error - * @return The equivalent ClientError - */ - static ClientError wrapCtapException(CtapException error) { - switch (error.getCtapError()) { - case CtapException.ERR_CREDENTIAL_EXCLUDED: - case CtapException.ERR_NO_CREDENTIALS: - return new ClientError(Code.DEVICE_INELIGIBLE, error); - case CtapException.ERR_TIMEOUT: - case CtapException.ERR_KEEPALIVE_CANCEL: - case CtapException.ERR_ACTION_TIMEOUT: - case CtapException.ERR_USER_ACTION_TIMEOUT: - return new ClientError(Code.TIMEOUT, error); - case CtapException.ERR_UNSUPPORTED_ALGORITHM: - case CtapException.ERR_UNSUPPORTED_OPTION: - case CtapException.ERR_UNSUPPORTED_EXTENSION: - case CtapException.ERR_KEY_STORE_FULL: - return new ClientError(Code.CONFIGURATION_UNSUPPORTED, error); - case CtapException.ERR_INVALID_COMMAND: - case CtapException.ERR_CBOR_UNEXPECTED_TYPE: - case CtapException.ERR_INVALID_CBOR: - case CtapException.ERR_MISSING_PARAMETER: - case CtapException.ERR_INVALID_OPTION: - case CtapException.ERR_PUAT_REQUIRED: - case CtapException.ERR_PIN_INVALID: - case CtapException.ERR_PIN_BLOCKED: - case CtapException.ERR_PIN_NOT_SET: - case CtapException.ERR_PIN_POLICY_VIOLATION: - case CtapException.ERR_PIN_TOKEN_EXPIRED: - case CtapException.ERR_PIN_AUTH_INVALID: - case CtapException.ERR_PIN_AUTH_BLOCKED: - case CtapException.ERR_UV_BLOCKED: - case CtapException.ERR_UV_INVALID: - case CtapException.ERR_REQUEST_TOO_LARGE: - case CtapException.ERR_OPERATION_DENIED: - return new ClientError(Code.BAD_REQUEST, error); - default: - return new ClientError(Code.OTHER_ERROR, error); - } + @SuppressWarnings("unused") + public Code getErrorCode() { + return errorCode; + } + + /** + * Translate CTAP errors into client errors + * + * @param error CTAP Error + * @return The equivalent ClientError + */ + static ClientError wrapCtapException(CtapException error) { + switch (error.getCtapError()) { + case CtapException.ERR_CREDENTIAL_EXCLUDED: + case CtapException.ERR_NO_CREDENTIALS: + return new ClientError(Code.DEVICE_INELIGIBLE, error); + case CtapException.ERR_TIMEOUT: + case CtapException.ERR_KEEPALIVE_CANCEL: + case CtapException.ERR_ACTION_TIMEOUT: + case CtapException.ERR_USER_ACTION_TIMEOUT: + return new ClientError(Code.TIMEOUT, error); + case CtapException.ERR_UNSUPPORTED_ALGORITHM: + case CtapException.ERR_UNSUPPORTED_OPTION: + case CtapException.ERR_UNSUPPORTED_EXTENSION: + case CtapException.ERR_KEY_STORE_FULL: + return new ClientError(Code.CONFIGURATION_UNSUPPORTED, error); + case CtapException.ERR_INVALID_COMMAND: + case CtapException.ERR_CBOR_UNEXPECTED_TYPE: + case CtapException.ERR_INVALID_CBOR: + case CtapException.ERR_MISSING_PARAMETER: + case CtapException.ERR_INVALID_OPTION: + case CtapException.ERR_PUAT_REQUIRED: + case CtapException.ERR_PIN_INVALID: + case CtapException.ERR_PIN_BLOCKED: + case CtapException.ERR_PIN_NOT_SET: + case CtapException.ERR_PIN_POLICY_VIOLATION: + case CtapException.ERR_PIN_TOKEN_EXPIRED: + case CtapException.ERR_PIN_AUTH_INVALID: + case CtapException.ERR_PIN_AUTH_BLOCKED: + case CtapException.ERR_UV_BLOCKED: + case CtapException.ERR_UV_INVALID: + case CtapException.ERR_REQUEST_TOO_LARGE: + case CtapException.ERR_OPERATION_DENIED: + return new ClientError(Code.BAD_REQUEST, error); + default: + return new ClientError(Code.OTHER_ERROR, error); } + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/client/CredentialManager.java b/fido/src/main/java/com/yubico/yubikit/fido/client/CredentialManager.java index e5038900..e0155769 100755 --- a/fido/src/main/java/com/yubico/yubikit/fido/client/CredentialManager.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/client/CredentialManager.java @@ -22,7 +22,6 @@ import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialDescriptor; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialUserEntity; import com.yubico.yubikit.fido.webauthn.SerializationType; - import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -31,127 +30,131 @@ import java.util.Map; /** - * Provides management of resident key type credentials, which are stored on a YubiKey. - * An instance of this class can be obtained by calling {@link BasicWebAuthnClient#getCredentialManager(char[])}. + * Provides management of resident key type credentials, which are stored on a YubiKey. An instance + * of this class can be obtained by calling {@link + * BasicWebAuthnClient#getCredentialManager(char[])}. */ @SuppressWarnings("unused") public class CredentialManager { - private final Map rpIdHashes = new HashMap<>(); - private final CredentialManagement credentialManagement; + private final Map rpIdHashes = new HashMap<>(); + private final CredentialManagement credentialManagement; - CredentialManager(CredentialManagement credentialManagement) { - this.credentialManagement = credentialManagement; - } + CredentialManager(CredentialManagement credentialManagement) { + this.credentialManagement = credentialManagement; + } - /** - * Get the number of credentials currently stored on the YubiKey. - * - * @return The total number of resident credentials existing on the authenticator. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @throws ClientError A higher level error. - */ - public int getCredentialCount() throws IOException, CommandException, ClientError { - try { - return credentialManagement.getMetadata().getExistingResidentCredentialsCount(); - } catch (CtapException e) { - throw ClientError.wrapCtapException(e); - } + /** + * Get the number of credentials currently stored on the YubiKey. + * + * @return The total number of resident credentials existing on the authenticator. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @throws ClientError A higher level error. + */ + public int getCredentialCount() throws IOException, CommandException, ClientError { + try { + return credentialManagement.getMetadata().getExistingResidentCredentialsCount(); + } catch (CtapException e) { + throw ClientError.wrapCtapException(e); } + } - /** - * Get a List of RP IDs for which this YubiKey has stored credentials. - * - * @return A list of RP IDs, which can be used to call {@link #getCredentials(String)}. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @throws ClientError A higher level error. - */ - public List getRpIdList() throws IOException, CommandException, ClientError { - try { - List rpIds = new ArrayList<>(); - rpIdHashes.clear(); - for (CredentialManagement.RpData rpData : credentialManagement.enumerateRps()) { - String rpId = (String) rpData.getRp().get("id"); - rpIdHashes.put(rpId, rpData.getRpIdHash()); - rpIds.add(rpId); - } - return rpIds; - } catch (CtapException e) { - throw ClientError.wrapCtapException(e); - } + /** + * Get a List of RP IDs for which this YubiKey has stored credentials. + * + * @return A list of RP IDs, which can be used to call {@link #getCredentials(String)}. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @throws ClientError A higher level error. + */ + public List getRpIdList() throws IOException, CommandException, ClientError { + try { + List rpIds = new ArrayList<>(); + rpIdHashes.clear(); + for (CredentialManagement.RpData rpData : credentialManagement.enumerateRps()) { + String rpId = (String) rpData.getRp().get("id"); + rpIdHashes.put(rpId, rpData.getRpIdHash()); + rpIds.add(rpId); + } + return rpIds; + } catch (CtapException e) { + throw ClientError.wrapCtapException(e); } + } - /** - * Get resident key credentials stored for a particular RP. - * - * @param rpId The ID of the RP to get credentials for. - * @return A mapping between {@link PublicKeyCredentialDescriptor}s to their associated {@link PublicKeyCredentialUserEntity} - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @throws ClientError A higher level error. - */ - public Map getCredentials(String rpId) throws IOException, CommandException, ClientError { - try { - Map credentials = new HashMap<>(); - byte[] rpIdHash = rpIdHashes.get(rpId); - if (rpIdHash == null) { - rpIdHash = BasicWebAuthnClient.Utils.hash(rpId.getBytes(StandardCharsets.UTF_8)); - } + /** + * Get resident key credentials stored for a particular RP. + * + * @param rpId The ID of the RP to get credentials for. + * @return A mapping between {@link PublicKeyCredentialDescriptor}s to their associated {@link + * PublicKeyCredentialUserEntity} + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @throws ClientError A higher level error. + */ + public Map getCredentials( + String rpId) throws IOException, CommandException, ClientError { + try { + Map credentials = + new HashMap<>(); + byte[] rpIdHash = rpIdHashes.get(rpId); + if (rpIdHash == null) { + rpIdHash = BasicWebAuthnClient.Utils.hash(rpId.getBytes(StandardCharsets.UTF_8)); + } - for (CredentialManagement.CredentialData credData : credentialManagement.enumerateCredentials(rpIdHash)) { - final Map credentialIdMap = credData.getCredentialId(); - credentials.put( - PublicKeyCredentialDescriptor.fromMap(credData.getCredentialId(), SerializationType.CBOR), - PublicKeyCredentialUserEntity.fromMap(credData.getUser(), SerializationType.CBOR) - ); - } + for (CredentialManagement.CredentialData credData : + credentialManagement.enumerateCredentials(rpIdHash)) { + final Map credentialIdMap = credData.getCredentialId(); + credentials.put( + PublicKeyCredentialDescriptor.fromMap( + credData.getCredentialId(), SerializationType.CBOR), + PublicKeyCredentialUserEntity.fromMap(credData.getUser(), SerializationType.CBOR)); + } - return credentials; - } catch (CtapException e) { - throw ClientError.wrapCtapException(e); - } + return credentials; + } catch (CtapException e) { + throw ClientError.wrapCtapException(e); } + } - /** - * Delete a stored credential from the YubiKey. - * - * @param credential A {@link PublicKeyCredentialDescriptor} which can be gotten from {@link #getCredentials(String)}. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @throws ClientError A higher level error. - */ - public void deleteCredential(PublicKeyCredentialDescriptor credential) throws IOException, CommandException, ClientError { - try { - credentialManagement.deleteCredential(credential.toMap(SerializationType.CBOR)); - } catch (CtapException e) { - throw ClientError.wrapCtapException(e); - } + /** + * Delete a stored credential from the YubiKey. + * + * @param credential A {@link PublicKeyCredentialDescriptor} which can be gotten from {@link + * #getCredentials(String)}. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @throws ClientError A higher level error. + */ + public void deleteCredential(PublicKeyCredentialDescriptor credential) + throws IOException, CommandException, ClientError { + try { + credentialManagement.deleteCredential(credential.toMap(SerializationType.CBOR)); + } catch (CtapException e) { + throw ClientError.wrapCtapException(e); } + } - /** - * Update user information associated to a credential. Only name and displayName can be changed. - * - * @param credential A {@link PublicKeyCredentialDescriptor} which can be gotten from - * {@link #getCredentials(String)}. - * @param user A {@link PublicKeyCredentialUserEntity} containing updated data. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @throws ClientError A higher level error. - * @throws UnsupportedOperationException If the authenticator does not support updating user - * information. - */ - public void updateUserInformation( - PublicKeyCredentialDescriptor credential, - PublicKeyCredentialUserEntity user) - throws IOException, CommandException, ClientError, UnsupportedOperationException { - try { - credentialManagement.updateUserInformation( - credential.toMap(SerializationType.CBOR), - user.toMap(SerializationType.CBOR) - ); - } catch (CtapException e) { - throw ClientError.wrapCtapException(e); - } + /** + * Update user information associated to a credential. Only name and displayName can be changed. + * + * @param credential A {@link PublicKeyCredentialDescriptor} which can be gotten from {@link + * #getCredentials(String)}. + * @param user A {@link PublicKeyCredentialUserEntity} containing updated data. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @throws ClientError A higher level error. + * @throws UnsupportedOperationException If the authenticator does not support updating user + * information. + */ + public void updateUserInformation( + PublicKeyCredentialDescriptor credential, PublicKeyCredentialUserEntity user) + throws IOException, CommandException, ClientError, UnsupportedOperationException { + try { + credentialManagement.updateUserInformation( + credential.toMap(SerializationType.CBOR), user.toMap(SerializationType.CBOR)); + } catch (CtapException e) { + throw ClientError.wrapCtapException(e); } + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/client/MultipleAssertionsAvailable.java b/fido/src/main/java/com/yubico/yubikit/fido/client/MultipleAssertionsAvailable.java index ad510fe3..d7aabdd5 100755 --- a/fido/src/main/java/com/yubico/yubikit/fido/client/MultipleAssertionsAvailable.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/client/MultipleAssertionsAvailable.java @@ -24,88 +24,88 @@ import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialDescriptor; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialUserEntity; import com.yubico.yubikit.fido.webauthn.SerializationType; - import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; /** - * The request generated multiple assertions, and a choice must be made by the user. - * Once selected, call {@link #select(int)} to get an assertion. + * The request generated multiple assertions, and a choice must be made by the user. Once selected, + * call {@link #select(int)} to get an assertion. */ public class MultipleAssertionsAvailable extends Throwable { - private final byte[] clientDataJson; - private final List> assertions; + private final byte[] clientDataJson; + private final List> assertions; - MultipleAssertionsAvailable(byte[] clientDataJson, List> assertions) { - super("Request returned multiple assertions"); + MultipleAssertionsAvailable( + byte[] clientDataJson, + List> assertions) { + super("Request returned multiple assertions"); - this.clientDataJson = clientDataJson; - this.assertions = assertions; - } + this.clientDataJson = clientDataJson; + this.assertions = assertions; + } - /** - * Get the number of assertions returned by the Authenticators. - * - * @return the number of assertions available - */ - public int getAssertionCount() { - return assertions.size(); - } + /** + * Get the number of assertions returned by the Authenticators. + * + * @return the number of assertions available + */ + public int getAssertionCount() { + return assertions.size(); + } - /** - * The list of users for which credentials are stored by the Authenticator. - * The indexes of the user objects correspond to the value which should be passed to select() - * to select a response. - *

- * NOTE: If PIV/UV wasn't provided to the call to {@link BasicWebAuthnClient#getAssertion} - * then user information may not be available, in which case this method will throw an exception. - * - * @return a list of available users. - * @throws UserInformationNotAvailableError in case PIN/UV wasn't provided - */ - public List getUsers() throws UserInformationNotAvailableError { - List users = new ArrayList<>(); - for (Pair assertion : assertions) { - Map user = assertion.first.getUser(); - if (user == null) { - throw new UserInformationNotAvailableError(); - } + /** + * The list of users for which credentials are stored by the Authenticator. The indexes of the + * user objects correspond to the value which should be passed to select() to select a response. + * + *

NOTE: If PIV/UV wasn't provided to the call to {@link BasicWebAuthnClient#getAssertion} then + * user information may not be available, in which case this method will throw an exception. + * + * @return a list of available users. + * @throws UserInformationNotAvailableError in case PIN/UV wasn't provided + */ + public List getUsers() throws UserInformationNotAvailableError { + List users = new ArrayList<>(); + for (Pair assertion : assertions) { + Map user = assertion.first.getUser(); + if (user == null) { + throw new UserInformationNotAvailableError(); + } - users.add(PublicKeyCredentialUserEntity.fromMap(user, SerializationType.CBOR)); - } - return users; + users.add(PublicKeyCredentialUserEntity.fromMap(user, SerializationType.CBOR)); } + return users; + } - /** - * Selects which assertion to use by index. These indices correspond to those of the List - * returned by {@link #getUsers()}. This method can only be called once to get a single response. - * - * @param index The index of the assertion to return. - * @return A WebAuthn public key credential. - */ - public PublicKeyCredential select(int index) { - if (assertions.isEmpty()) { - throw new IllegalStateException("Assertion has already been selected"); - } - Pair assertionPair = assertions.get(index); - assertions.clear(); + /** + * Selects which assertion to use by index. These indices correspond to those of the List returned + * by {@link #getUsers()}. This method can only be called once to get a single response. + * + * @param index The index of the assertion to return. + * @return A WebAuthn public key credential. + */ + public PublicKeyCredential select(int index) { + if (assertions.isEmpty()) { + throw new IllegalStateException("Assertion has already been selected"); + } + Pair assertionPair = assertions.get(index); + assertions.clear(); - final Ctap2Session.AssertionData assertion = assertionPair.first; - final ClientExtensionResults clientExtensionResults = assertionPair.second; + final Ctap2Session.AssertionData assertion = assertionPair.first; + final ClientExtensionResults clientExtensionResults = assertionPair.second; - final Map user = Objects.requireNonNull(assertion.getUser()); - final Map credential = Objects.requireNonNull(assertion.getCredential()); - final byte[] credentialId = Objects.requireNonNull((byte[]) credential.get(PublicKeyCredentialDescriptor.ID)); - return new PublicKeyCredential( - credentialId, - new AuthenticatorAssertionResponse( - clientDataJson, - assertion.getAuthenticatorData(), - assertion.getSignature(), - Objects.requireNonNull((byte[]) user.get(PublicKeyCredentialUserEntity.ID)) - ), - clientExtensionResults); - } -} \ No newline at end of file + final Map user = Objects.requireNonNull(assertion.getUser()); + final Map credential = Objects.requireNonNull(assertion.getCredential()); + final byte[] credentialId = + Objects.requireNonNull((byte[]) credential.get(PublicKeyCredentialDescriptor.ID)); + return new PublicKeyCredential( + credentialId, + new AuthenticatorAssertionResponse( + clientDataJson, + assertion.getAuthenticatorData(), + assertion.getSignature(), + Objects.requireNonNull((byte[]) user.get(PublicKeyCredentialUserEntity.ID))), + clientExtensionResults); + } +} diff --git a/fido/src/main/java/com/yubico/yubikit/fido/client/PinInvalidClientError.java b/fido/src/main/java/com/yubico/yubikit/fido/client/PinInvalidClientError.java index 92d89fb9..7317a78c 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/client/PinInvalidClientError.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/client/PinInvalidClientError.java @@ -19,17 +19,18 @@ import com.yubico.yubikit.core.fido.CtapException; /** - * A subclass of {@link ClientError} used by {@link BasicWebAuthnClient} to indicate that makeCredential or - * getAssertion was called with an invalid PIN. + * A subclass of {@link ClientError} used by {@link BasicWebAuthnClient} to indicate that + * makeCredential or getAssertion was called with an invalid PIN. */ public class PinInvalidClientError extends ClientError { - public final int pinRetries; - /** - * @param pinRetries number of retries left before the authenticator is blocked - */ - public PinInvalidClientError(CtapException cause, int pinRetries) { - super(Code.BAD_REQUEST, cause); - this.pinRetries = pinRetries; - } + public final int pinRetries; + + /** + * @param pinRetries number of retries left before the authenticator is blocked + */ + public PinInvalidClientError(CtapException cause, int pinRetries) { + super(Code.BAD_REQUEST, cause); + this.pinRetries = pinRetries; + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/client/PinRequiredClientError.java b/fido/src/main/java/com/yubico/yubikit/fido/client/PinRequiredClientError.java index 75879b95..53b5e36e 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/client/PinRequiredClientError.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/client/PinRequiredClientError.java @@ -17,12 +17,13 @@ package com.yubico.yubikit.fido.client; /** - * A subclass of {@link ClientError} used by {@link BasicWebAuthnClient} to indicate that makeCredential or - * getAssertion was called without a PIN even though a PIN is required to complete the operation. - * Client implementations may want to catch this and handle it differently than other ClientErrors. + * A subclass of {@link ClientError} used by {@link BasicWebAuthnClient} to indicate that + * makeCredential or getAssertion was called without a PIN even though a PIN is required to complete + * the operation. Client implementations may want to catch this and handle it differently than other + * ClientErrors. */ public class PinRequiredClientError extends ClientError { - public PinRequiredClientError() { - super(Code.BAD_REQUEST, "PIN required but not provided"); - } + public PinRequiredClientError() { + super(Code.BAD_REQUEST, "PIN required but not provided"); + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/client/UserInformationNotAvailableError.java b/fido/src/main/java/com/yubico/yubikit/fido/client/UserInformationNotAvailableError.java index f1a69734..ccda31b2 100755 --- a/fido/src/main/java/com/yubico/yubikit/fido/client/UserInformationNotAvailableError.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/client/UserInformationNotAvailableError.java @@ -17,12 +17,12 @@ package com.yubico.yubikit.fido.client; /** - * A ClientError indicating that UserEntity information isn't available for assertions returned by the Authenticator. - * This happens when {@link BasicWebAuthnClient#getAssertion} - * is called without providing PIV or UV, when returning discoverable credentials. + * A ClientError indicating that UserEntity information isn't available for assertions returned by + * the Authenticator. This happens when {@link BasicWebAuthnClient#getAssertion} is called without + * providing PIV or UV, when returning discoverable credentials. */ public class UserInformationNotAvailableError extends ClientError { - public UserInformationNotAvailableError() { - super(Code.OTHER_ERROR, "User information is not available unless PIN/UV is provided"); - } + public UserInformationNotAvailableError() { + super(Code.OTHER_ERROR, "User information is not available unless PIN/UV is provided"); + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/CredBlobExtension.java b/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/CredBlobExtension.java index 872d4422..e5b33d74 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/CredBlobExtension.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/CredBlobExtension.java @@ -23,67 +23,62 @@ import com.yubico.yubikit.fido.webauthn.Extensions; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialCreationOptions; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialRequestOptions; - import java.util.Collections; - import javax.annotation.Nullable; /** * Implements the Credential Blob (credBlob) CTAP2 extension. - * @see Credential Blob (credBlob) + * + * @see Credential + * Blob (credBlob) */ public class CredBlobExtension extends Extension { - public CredBlobExtension() { - super("credBlob"); - } - - @Nullable - @Override - public RegistrationProcessor makeCredential( - Ctap2Session ctap, - PublicKeyCredentialCreationOptions options, - PinUvAuthProtocol pinUvAuthProtocol) { + public CredBlobExtension() { + super("credBlob"); + } - if (!isSupported(ctap)) { - return null; - } + @Nullable @Override + public RegistrationProcessor makeCredential( + Ctap2Session ctap, + PublicKeyCredentialCreationOptions options, + PinUvAuthProtocol pinUvAuthProtocol) { - Extensions extensions = options.getExtensions(); - if (extensions == null) { - return null; - } + if (!isSupported(ctap)) { + return null; + } - String b64Blob = (String) extensions.get("credBlob"); - if (b64Blob != null) { - byte[] blob = fromUrlSafeString(b64Blob); - if (blob.length <= ctap.getCachedInfo().getMaxCredBlobLength()) { - return new RegistrationProcessor( - pinToken -> Collections.singletonMap(name, blob) - ); - } - } + Extensions extensions = options.getExtensions(); + if (extensions == null) { + return null; + } - return null; + String b64Blob = (String) extensions.get("credBlob"); + if (b64Blob != null) { + byte[] blob = fromUrlSafeString(b64Blob); + if (blob.length <= ctap.getCachedInfo().getMaxCredBlobLength()) { + return new RegistrationProcessor(pinToken -> Collections.singletonMap(name, blob)); + } } - @Nullable - @Override - public AuthenticationProcessor getAssertion( - Ctap2Session ctap, - PublicKeyCredentialRequestOptions options, - PinUvAuthProtocol pinUvAuthProtocol) { + return null; + } - Extensions extensions = options.getExtensions(); - if (extensions == null) { - return null; - } - if (isSupported(ctap) && - Boolean.TRUE.equals(extensions.get("getCredBlob"))) { - return new AuthenticationProcessor( - (AuthenticationInput) (selected, pinToken) -> Collections.singletonMap(name, true) - ); - } - return null; + @Nullable @Override + public AuthenticationProcessor getAssertion( + Ctap2Session ctap, + PublicKeyCredentialRequestOptions options, + PinUvAuthProtocol pinUvAuthProtocol) { + + Extensions extensions = options.getExtensions(); + if (extensions == null) { + return null; + } + if (isSupported(ctap) && Boolean.TRUE.equals(extensions.get("getCredBlob"))) { + return new AuthenticationProcessor( + (AuthenticationInput) (selected, pinToken) -> Collections.singletonMap(name, true)); } + return null; + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/CredPropsExtension.java b/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/CredPropsExtension.java index cda75c2d..ea242cae 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/CredPropsExtension.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/CredPropsExtension.java @@ -22,48 +22,48 @@ import com.yubico.yubikit.fido.webauthn.Extensions; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialCreationOptions; import com.yubico.yubikit.fido.webauthn.ResidentKeyRequirement; - import java.util.Collections; - import javax.annotation.Nullable; /** * Implements the Credential Properties (credProps) WebAuthn extension. - * @see Credential Properties Extension (credProps) + * + * @see Credential + * Properties Extension (credProps) */ public class CredPropsExtension extends Extension { - public CredPropsExtension() { - super("credProps"); - } + public CredPropsExtension() { + super("credProps"); + } - @Nullable - @Override - public RegistrationProcessor makeCredential( - Ctap2Session ctap, - PublicKeyCredentialCreationOptions options, - PinUvAuthProtocol pinUvAuthProtocol) { + @Nullable @Override + public RegistrationProcessor makeCredential( + Ctap2Session ctap, + PublicKeyCredentialCreationOptions options, + PinUvAuthProtocol pinUvAuthProtocol) { - Extensions extensions = options.getExtensions(); - if (extensions == null) { - return null; - } + Extensions extensions = options.getExtensions(); + if (extensions == null) { + return null; + } - if (extensions.has(name)) { - AuthenticatorSelectionCriteria authenticatorSelection = options.getAuthenticatorSelection(); - String optionsRk = authenticatorSelection != null - ? authenticatorSelection.getResidentKey() - : null; - Boolean authenticatorRk = (Boolean) ctap.getCachedInfo().getOptions().get("rk"); - boolean rk = (ResidentKeyRequirement.REQUIRED.equals(optionsRk) || - (ResidentKeyRequirement.PREFERRED.equals(optionsRk) && - Boolean.TRUE.equals(authenticatorRk))); + if (extensions.has(name)) { + AuthenticatorSelectionCriteria authenticatorSelection = options.getAuthenticatorSelection(); + String optionsRk = + authenticatorSelection != null ? authenticatorSelection.getResidentKey() : null; + Boolean authenticatorRk = (Boolean) ctap.getCachedInfo().getOptions().get("rk"); + boolean rk = + (ResidentKeyRequirement.REQUIRED.equals(optionsRk) + || (ResidentKeyRequirement.PREFERRED.equals(optionsRk) + && Boolean.TRUE.equals(authenticatorRk))); - return new RegistrationProcessor( - (attestationObject, pinToken) -> - serializationType -> Collections.singletonMap(name, - Collections.singletonMap("rk", rk))); - } - return null; + return new RegistrationProcessor( + (attestationObject, pinToken) -> + serializationType -> + Collections.singletonMap(name, Collections.singletonMap("rk", rk))); } + return null; + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/CredProtectExtension.java b/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/CredProtectExtension.java index d916c96b..d22bd61c 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/CredProtectExtension.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/CredProtectExtension.java @@ -20,68 +20,67 @@ import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import com.yubico.yubikit.fido.webauthn.Extensions; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialCreationOptions; - import java.util.Collections; - import javax.annotation.Nullable; /** * Implements the Credential Protection CTAP2 extension. - * @see Credential Protection (credProtect) + * + * @see Credential + * Protection (credProtect) */ public class CredProtectExtension extends Extension { - private static final String POLICY = "credentialProtectionPolicy"; - private static final String OPTIONAL = "userVerificationOptional"; - private static final String OPTIONAL_WITH_LIST = "userVerificationOptionalWithCredentialIDList"; - private static final String REQUIRED = "userVerificationRequired"; - private static final String ENFORCE = "enforceCredentialProtectionPolicy"; + private static final String POLICY = "credentialProtectionPolicy"; + private static final String OPTIONAL = "userVerificationOptional"; + private static final String OPTIONAL_WITH_LIST = "userVerificationOptionalWithCredentialIDList"; + private static final String REQUIRED = "userVerificationRequired"; + private static final String ENFORCE = "enforceCredentialProtectionPolicy"; - public CredProtectExtension() { - super("credProtect"); - } + public CredProtectExtension() { + super("credProtect"); + } - @Nullable - @Override - public RegistrationProcessor makeCredential( - Ctap2Session ctap, - PublicKeyCredentialCreationOptions options, - PinUvAuthProtocol pinUvAuthProtocol) { + @Nullable @Override + public RegistrationProcessor makeCredential( + Ctap2Session ctap, + PublicKeyCredentialCreationOptions options, + PinUvAuthProtocol pinUvAuthProtocol) { - Extensions extensions = options.getExtensions(); - if (extensions == null) { - return null; - } + Extensions extensions = options.getExtensions(); + if (extensions == null) { + return null; + } - String credentialProtectionPolicy = (String) extensions.get(POLICY); - if (credentialProtectionPolicy == null) { - return null; - } + String credentialProtectionPolicy = (String) extensions.get(POLICY); + if (credentialProtectionPolicy == null) { + return null; + } - Integer credProtect = credProtectValue(credentialProtectionPolicy); - Boolean enforce = (Boolean) extensions.get(ENFORCE); - if (Boolean.TRUE.equals(enforce) && - !isSupported(ctap) && - credProtect != null && - credProtect > 0x01) { - throw new IllegalArgumentException("No Credential Protection support"); - } - return credProtect != null - ? new RegistrationProcessor(pinToken -> Collections.singletonMap(name, credProtect)) - : null; + Integer credProtect = credProtectValue(credentialProtectionPolicy); + Boolean enforce = (Boolean) extensions.get(ENFORCE); + if (Boolean.TRUE.equals(enforce) + && !isSupported(ctap) + && credProtect != null + && credProtect > 0x01) { + throw new IllegalArgumentException("No Credential Protection support"); } + return credProtect != null + ? new RegistrationProcessor(pinToken -> Collections.singletonMap(name, credProtect)) + : null; + } - @Nullable - private Integer credProtectValue(String optionsValue) { - switch(optionsValue) { - case OPTIONAL: - return 0x01; - case OPTIONAL_WITH_LIST: - return 0x02; - case REQUIRED: - return 0x03; - default: - return null; - } + @Nullable private Integer credProtectValue(String optionsValue) { + switch (optionsValue) { + case OPTIONAL: + return 0x01; + case OPTIONAL_WITH_LIST: + return 0x02; + case REQUIRED: + return 0x03; + default: + return null; } + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/Extension.java b/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/Extension.java index 4088b419..56870d3e 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/Extension.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/Extension.java @@ -24,199 +24,160 @@ import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialCreationOptions; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialDescriptor; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialRequestOptions; - import java.util.Collections; import java.util.Map; - import javax.annotation.Nullable; /** * Base class for FIDO2 extensions. + * * @see Webauthn Extensions */ public abstract class Extension { - protected final String name; + protected final String name; + + protected Extension(String name) { + this.name = name; + } + + protected boolean isSupported(Ctap2Session ctap) { + return ctap.getCachedInfo().getExtensions().contains(name); + } + + @Nullable public RegistrationProcessor makeCredential( + Ctap2Session ctap, + PublicKeyCredentialCreationOptions options, + PinUvAuthProtocol pinUvAuthProtocol) { + return null; + } + + @Nullable public AuthenticationProcessor getAssertion( + Ctap2Session ctap, + PublicKeyCredentialRequestOptions options, + PinUvAuthProtocol pinUvAuthProtocol) { + return null; + } + + public interface RegistrationInput { + @Nullable Map prepareInput(@Nullable byte[] pinToken); + } + + public interface RegistrationOutput { + @Nullable ClientExtensionResultProvider prepareOutput( + AttestationObject attestationObject, @Nullable byte[] pinToken); + } + + public interface AuthenticationInput { + @Nullable Map prepareInput( + @Nullable PublicKeyCredentialDescriptor selected, @Nullable byte[] pinToken); + } + + public interface AuthenticationOutput { + @Nullable ClientExtensionResultProvider prepareOutput( + Ctap2Session.AssertionData assertionData, @Nullable byte[] pinToken); + } + + public static class RegistrationProcessor { + @Nullable private final RegistrationInput input; + @Nullable private final RegistrationOutput output; + private final int permissions; + + public RegistrationProcessor( + @Nullable RegistrationInput input, @Nullable RegistrationOutput output, int permissions) { + this.input = input; + this.output = output; + this.permissions = permissions; + } + + public RegistrationProcessor( + @Nullable RegistrationInput input, @Nullable RegistrationOutput output) { + this(input, output, ClientPin.PIN_PERMISSION_NONE); + } + + public RegistrationProcessor(@Nullable RegistrationInput input, int permissions) { + this(input, null, permissions); + } + + public RegistrationProcessor(@Nullable RegistrationInput input) { + this(input, null); + } + + public RegistrationProcessor(@Nullable RegistrationOutput output, int permissions) { + this(null, output, permissions); + } + + public RegistrationProcessor(@Nullable RegistrationOutput output) { + this(null, output); + } + + public Map getInput(@Nullable byte[] pinToken) { + Map registrationInput = input != null ? input.prepareInput(pinToken) : null; + return registrationInput != null ? registrationInput : Collections.emptyMap(); + } + + public ClientExtensionResultProvider getOutput( + AttestationObject attestationObject, @Nullable byte[] pinToken) { + ClientExtensionResultProvider resultProvider = + output != null ? output.prepareOutput(attestationObject, pinToken) : null; + return resultProvider != null ? resultProvider : serializationType -> Collections.emptyMap(); + } + + public int getPermissions() { + return permissions; + } + } + + public static class AuthenticationProcessor { + @Nullable private final AuthenticationInput input; + @Nullable private final AuthenticationOutput output; + private final int permissions; + + public AuthenticationProcessor( + @Nullable AuthenticationInput input, + @Nullable AuthenticationOutput output, + int permissions) { + this.input = input; + this.output = output; + this.permissions = permissions; + } + + public AuthenticationProcessor( + @Nullable AuthenticationInput input, @Nullable AuthenticationOutput output) { + this(input, output, ClientPin.PIN_PERMISSION_NONE); + } + + public AuthenticationProcessor(@Nullable AuthenticationInput input, int permissions) { + this(input, null, permissions); + } + + public AuthenticationProcessor(@Nullable AuthenticationInput input) { + this(input, null); + } + + public AuthenticationProcessor(@Nullable AuthenticationOutput output, int permissions) { + this(null, output, permissions); + } + + public AuthenticationProcessor(@Nullable AuthenticationOutput output) { + this(null, output); + } + + public Map getInput( + @Nullable PublicKeyCredentialDescriptor selected, @Nullable byte[] pinToken) { + Map authenticatorInput = + input != null ? input.prepareInput(selected, pinToken) : null; + return authenticatorInput != null ? authenticatorInput : Collections.emptyMap(); + } + + public ClientExtensionResultProvider getOutput( + Ctap2Session.AssertionData assertionData, @Nullable byte[] pinToken) { + ClientExtensionResultProvider resultProvider = + output != null ? output.prepareOutput(assertionData, pinToken) : null; + return resultProvider != null ? resultProvider : serializationType -> Collections.emptyMap(); + } - protected Extension(String name) { - this.name = name; - } - - protected boolean isSupported(Ctap2Session ctap) { - return ctap.getCachedInfo().getExtensions().contains(name); - } - - @Nullable - public RegistrationProcessor makeCredential( - Ctap2Session ctap, - PublicKeyCredentialCreationOptions options, - PinUvAuthProtocol pinUvAuthProtocol) { - return null; - } - - @Nullable - public AuthenticationProcessor getAssertion( - Ctap2Session ctap, - PublicKeyCredentialRequestOptions options, - PinUvAuthProtocol pinUvAuthProtocol) { - return null; - } - - public interface RegistrationInput { - @Nullable Map prepareInput( - @Nullable byte[] pinToken); - } - - public interface RegistrationOutput { - @Nullable - ClientExtensionResultProvider prepareOutput( - AttestationObject attestationObject, - @Nullable byte[] pinToken); - } - - public interface AuthenticationInput { - @Nullable Map prepareInput( - @Nullable PublicKeyCredentialDescriptor selected, - @Nullable byte[] pinToken); - } - - public interface AuthenticationOutput { - @Nullable - ClientExtensionResultProvider prepareOutput( - Ctap2Session.AssertionData assertionData, - @Nullable byte[] pinToken); - } - - public static class RegistrationProcessor { - @Nullable private final RegistrationInput input; - @Nullable private final RegistrationOutput output; - private final int permissions; - - public RegistrationProcessor( - @Nullable RegistrationInput input, - @Nullable RegistrationOutput output, - int permissions) { - this.input = input; - this.output = output; - this.permissions = permissions; - } - - public RegistrationProcessor( - @Nullable RegistrationInput input, - @Nullable RegistrationOutput output) { - this(input, output, ClientPin.PIN_PERMISSION_NONE); - } - - public RegistrationProcessor( - @Nullable RegistrationInput input, int permissions) { - this(input, null, permissions); - } - - public RegistrationProcessor( - @Nullable RegistrationInput input) { - this(input, null); - } - - public RegistrationProcessor( - @Nullable RegistrationOutput output, - int permissions) { - this(null, output, permissions); - } - - public RegistrationProcessor( - @Nullable RegistrationOutput output) { - this(null, output); - } - - public Map getInput(@Nullable byte[] pinToken) { - Map registrationInput = input != null - ? input.prepareInput(pinToken) - : null; - return registrationInput != null - ? registrationInput - : Collections.emptyMap(); - } - - public ClientExtensionResultProvider getOutput( - AttestationObject attestationObject, - @Nullable byte[] pinToken) { - ClientExtensionResultProvider resultProvider = output != null - ? output.prepareOutput(attestationObject, pinToken) - : null; - return resultProvider != null - ? resultProvider - : serializationType -> Collections.emptyMap(); - } - - public int getPermissions() { - return permissions; - } - } - - public static class AuthenticationProcessor { - @Nullable private final AuthenticationInput input; - @Nullable private final AuthenticationOutput output; - private final int permissions; - - public AuthenticationProcessor( - @Nullable AuthenticationInput input, - @Nullable AuthenticationOutput output, - int permissions) { - this.input = input; - this.output = output; - this.permissions = permissions; - } - - public AuthenticationProcessor( - @Nullable AuthenticationInput input, - @Nullable AuthenticationOutput output) { - this(input, output, ClientPin.PIN_PERMISSION_NONE); - } - - public AuthenticationProcessor( - @Nullable AuthenticationInput input, int permissions) { - this(input, null, permissions); - } - - public AuthenticationProcessor( - @Nullable AuthenticationInput input) { - this(input, null); - } - - public AuthenticationProcessor( - @Nullable AuthenticationOutput output, - int permissions) { - this(null, output, permissions); - } - - public AuthenticationProcessor( - @Nullable AuthenticationOutput output) { - this(null, output); - } - - public Map getInput( - @Nullable PublicKeyCredentialDescriptor selected, - @Nullable byte[] pinToken) { - Map authenticatorInput = input != null - ? input.prepareInput(selected, pinToken) - : null; - return authenticatorInput != null - ? authenticatorInput - : Collections.emptyMap(); - } - - public ClientExtensionResultProvider getOutput( - Ctap2Session.AssertionData assertionData, - @Nullable byte[] pinToken) { - ClientExtensionResultProvider resultProvider = output != null - ? output.prepareOutput(assertionData, pinToken) - : null; - return resultProvider != null - ? resultProvider - : serializationType -> Collections.emptyMap(); - } - - public int getPermissions() { - return permissions; - } + public int getPermissions() { + return permissions; } + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/HmacSecretExtension.java b/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/HmacSecretExtension.java index 6b651882..babca42d 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/HmacSecretExtension.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/HmacSecretExtension.java @@ -33,9 +33,6 @@ import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialDescriptor; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialRequestOptions; import com.yubico.yubikit.fido.webauthn.SerializationType; - -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -48,344 +45,345 @@ import java.util.List; import java.util.Map; import java.util.Set; - import javax.annotation.Nullable; +import org.slf4j.LoggerFactory; /** * Implements the Pseudo-random function (prf) and the hmac-secret CTAP2 extensions. - *

- * The hmac-secret extension is not directly available to clients by default, instead - * the prf extension is used. + * + *

The hmac-secret extension is not directly available to clients by default, instead the prf + * extension is used. * * @see PRF extension - * @see HMAC secret extension + * @see HMAC + * secret extension */ public class HmacSecretExtension extends Extension { - private final boolean allowHmacSecret; - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(HmacSecretExtension.class); - private static final int SALT_LEN = 32; - - public HmacSecretExtension() { - this(false); + private final boolean allowHmacSecret; + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(HmacSecretExtension.class); + private static final int SALT_LEN = 32; + + public HmacSecretExtension() { + this(false); + } + + /** + * @param allowHmacSecret Set to True to allow hmac-secret, in addition to prf + */ + public HmacSecretExtension(boolean allowHmacSecret) { + super("hmac-secret"); + this.allowHmacSecret = allowHmacSecret; + } + + @Override + public RegistrationProcessor makeCredential( + Ctap2Session ctap2, + PublicKeyCredentialCreationOptions options, + PinUvAuthProtocol pinUvAuthProtocol) { + Extensions extensions = options.getExtensions(); + if (extensions == null) { + return null; } - - /** - * @param allowHmacSecret Set to True to allow hmac-secret, in addition to prf - */ - public HmacSecretExtension(boolean allowHmacSecret) { - super("hmac-secret"); - this.allowHmacSecret = allowHmacSecret; + if (allowHmacSecret && Boolean.TRUE.equals(extensions.get("hmacCreateSecret"))) { + return new RegistrationProcessor( + pinToken -> Collections.singletonMap(name, true), + (attestationObject, pinToken) -> + serializationType -> registrationOutput(attestationObject, false)); + } else if (extensions.has("prf")) { + return new RegistrationProcessor( + pinToken -> Collections.singletonMap(name, true), + (attestationObject, pinToken) -> + serializationType -> registrationOutput(attestationObject, true)); } - - @Override - public RegistrationProcessor makeCredential( - Ctap2Session ctap2, - PublicKeyCredentialCreationOptions options, - PinUvAuthProtocol pinUvAuthProtocol) { - Extensions extensions = options.getExtensions(); - if (extensions == null) { - return null; - } - if (allowHmacSecret && Boolean.TRUE.equals(extensions.get("hmacCreateSecret"))) { - return new RegistrationProcessor( - pinToken -> Collections.singletonMap(name, true), - (attestationObject, pinToken) -> - serializationType -> registrationOutput(attestationObject, false) - ); - } else if (extensions.has("prf")) { - return new RegistrationProcessor( - pinToken -> Collections.singletonMap(name, true), - (attestationObject, pinToken) -> - serializationType -> registrationOutput(attestationObject, true) - ); - } - return null; + return null; + } + + @Override + public AuthenticationProcessor getAssertion( + Ctap2Session ctap, + PublicKeyCredentialRequestOptions options, + PinUvAuthProtocol pinUvAuthProtocol) { + if (!isSupported(ctap)) { + return null; } - @Override - public AuthenticationProcessor getAssertion( - Ctap2Session ctap, - PublicKeyCredentialRequestOptions options, - PinUvAuthProtocol pinUvAuthProtocol) { - if (!isSupported(ctap)) { - return null; - } - - final ClientPin clientPin = new ClientPin(ctap, pinUvAuthProtocol); - Pair, byte[]> keyAgreement; - try { - keyAgreement = clientPin.getSharedSecret(); - } catch (IOException | CommandException e) { - Logger.error(logger, "Failed to get shared secret: ", e); - return null; - } - - final Inputs inputs = Inputs.fromExtensions(options.getExtensions()); - if (inputs == null) { - return null; - } - final AuthenticationInput prepareInput = (selected, pinToken) -> { - Salts salts; - if (inputs.prf != null) { - Map secrets = inputs.prf.eval; - Map evalByCredential = inputs.prf.evalByCredential; - - if (evalByCredential != null) { - List allowCredentials = options.getAllowCredentials(); - - if (allowCredentials.isEmpty()) { - throw new IllegalArgumentException("evalByCredential needs allow list"); - } - - Set ids = new HashSet<>(); - for (PublicKeyCredentialDescriptor descriptor : allowCredentials) { - ids.add(toUrlSafeString(descriptor.getId())); - } - - if (!ids.containsAll(evalByCredential.keySet())) { - throw new IllegalArgumentException("evalByCredentials contains invalid key"); - } - - if (selected != null) { - String key = toUrlSafeString(selected.getId()); - if (evalByCredential.containsKey(key)) { - secrets = inputs.evalByCredential(key); - } - } - } - - if (secrets == null) { - return null; - } - - Logger.debug(logger, "PRF inputs: {}, {}", secrets.get("first"), secrets.get("second")); + final ClientPin clientPin = new ClientPin(ctap, pinUvAuthProtocol); + Pair, byte[]> keyAgreement; + try { + keyAgreement = clientPin.getSharedSecret(); + } catch (IOException | CommandException e) { + Logger.error(logger, "Failed to get shared secret: ", e); + return null; + } - String firstInput = (String) secrets.get("first"); - if (firstInput == null) { - return null; + final Inputs inputs = Inputs.fromExtensions(options.getExtensions()); + if (inputs == null) { + return null; + } + final AuthenticationInput prepareInput = + (selected, pinToken) -> { + Salts salts; + if (inputs.prf != null) { + Map secrets = inputs.prf.eval; + Map evalByCredential = inputs.prf.evalByCredential; + + if (evalByCredential != null) { + List allowCredentials = options.getAllowCredentials(); + + if (allowCredentials.isEmpty()) { + throw new IllegalArgumentException("evalByCredential needs allow list"); + } + + Set ids = new HashSet<>(); + for (PublicKeyCredentialDescriptor descriptor : allowCredentials) { + ids.add(toUrlSafeString(descriptor.getId())); + } + + if (!ids.containsAll(evalByCredential.keySet())) { + throw new IllegalArgumentException("evalByCredentials contains invalid key"); + } + + if (selected != null) { + String key = toUrlSafeString(selected.getId()); + if (evalByCredential.containsKey(key)) { + secrets = inputs.evalByCredential(key); } + } + } - byte[] first = prfSalt(fromUrlSafeString(firstInput)); - byte[] second = secrets.containsKey("second") - ? prfSalt(fromUrlSafeString((String) secrets.get("second"))) - : null; - - salts = new Salts(first, second); - } else { - if (inputs.hmac == null) { - return null; - } + if (secrets == null) { + return null; + } - Logger.debug(logger, "hmacGetSecret inputs: {}, {}", - inputs.hmac.salt1 != null ? inputs.hmac.salt1 : "none", - inputs.hmac.salt2 != null ? inputs.hmac.salt2 : "none"); + Logger.debug(logger, "PRF inputs: {}, {}", secrets.get("first"), secrets.get("second")); - if (inputs.hmac.salt1 == null) { - return null; - } + String firstInput = (String) secrets.get("first"); + if (firstInput == null) { + return null; + } - byte[] salt1 = prfSalt(fromUrlSafeString(inputs.hmac.salt1)); - byte[] salt2 = inputs.hmac.salt2 != null - ? prfSalt(fromUrlSafeString(inputs.hmac.salt2)) - : null; + byte[] first = prfSalt(fromUrlSafeString(firstInput)); + byte[] second = + secrets.containsKey("second") + ? prfSalt(fromUrlSafeString((String) secrets.get("second"))) + : null; - salts = new Salts(salt1, salt2); + salts = new Salts(first, second); + } else { + if (inputs.hmac == null) { + return null; } - Logger.debug(logger, "Salts: {}, {}", - StringUtils.bytesToHex(salts.salt1), - StringUtils.bytesToHex(salts.salt2)); - if (!(salts.salt1.length == SALT_LEN && - (salts.salt2.length == 0 || salts.salt2.length == SALT_LEN))) { - throw new IllegalArgumentException("Invalid salt length"); + Logger.debug( + logger, + "hmacGetSecret inputs: {}, {}", + inputs.hmac.salt1 != null ? inputs.hmac.salt1 : "none", + inputs.hmac.salt2 != null ? inputs.hmac.salt2 : "none"); + + if (inputs.hmac.salt1 == null) { + return null; } - byte[] saltEnc = clientPin.getPinUvAuth().encrypt( - keyAgreement.second, - ByteBuffer - .allocate(salts.salt1.length + salts.salt2.length) - .put(salts.salt1) - .put(salts.salt2) - .array()); - - byte[] saltAuth = clientPin.getPinUvAuth().authenticate( - keyAgreement.second, - saltEnc); - - final Map hmacGetSecretInput = new HashMap<>(); - hmacGetSecretInput.put(1, keyAgreement.first); - hmacGetSecretInput.put(2, saltEnc); - hmacGetSecretInput.put(3, saltAuth); - hmacGetSecretInput.put(4, clientPin.getPinUvAuth().getVersion()); - return Collections.singletonMap(name, hmacGetSecretInput); + byte[] salt1 = prfSalt(fromUrlSafeString(inputs.hmac.salt1)); + byte[] salt2 = + inputs.hmac.salt2 != null ? prfSalt(fromUrlSafeString(inputs.hmac.salt2)) : null; + + salts = new Salts(salt1, salt2); + } + + Logger.debug( + logger, + "Salts: {}, {}", + StringUtils.bytesToHex(salts.salt1), + StringUtils.bytesToHex(salts.salt2)); + if (!(salts.salt1.length == SALT_LEN + && (salts.salt2.length == 0 || salts.salt2.length == SALT_LEN))) { + throw new IllegalArgumentException("Invalid salt length"); + } + + byte[] saltEnc = + clientPin + .getPinUvAuth() + .encrypt( + keyAgreement.second, + ByteBuffer.allocate(salts.salt1.length + salts.salt2.length) + .put(salts.salt1) + .put(salts.salt2) + .array()); + + byte[] saltAuth = clientPin.getPinUvAuth().authenticate(keyAgreement.second, saltEnc); + + final Map hmacGetSecretInput = new HashMap<>(); + hmacGetSecretInput.put(1, keyAgreement.first); + hmacGetSecretInput.put(2, saltEnc); + hmacGetSecretInput.put(3, saltAuth); + hmacGetSecretInput.put(4, clientPin.getPinUvAuth().getVersion()); + return Collections.singletonMap(name, hmacGetSecretInput); }; - final AuthenticationOutput prepareOutput = (assertionData, pinToken) -> { - AuthenticatorData authenticatorData = AuthenticatorData.parseFrom(ByteBuffer.wrap( - assertionData.getAuthenticatorData() - )); - - Map extensionOutputs = authenticatorData.getExtensions(); - if (extensionOutputs == null) { - return null; - } + final AuthenticationOutput prepareOutput = + (assertionData, pinToken) -> { + AuthenticatorData authenticatorData = + AuthenticatorData.parseFrom(ByteBuffer.wrap(assertionData.getAuthenticatorData())); - byte[] value = (byte[]) extensionOutputs.get(name); - if (value == null) { - return null; - } + Map extensionOutputs = authenticatorData.getExtensions(); + if (extensionOutputs == null) { + return null; + } - byte[] decrypted = clientPin.getPinUvAuth().decrypt(keyAgreement.second, value); - - byte[] output1 = Arrays.copyOf(decrypted, SALT_LEN); - byte[] output2 = decrypted.length > SALT_LEN - ? Arrays.copyOfRange(decrypted, SALT_LEN, 2 * SALT_LEN) - : new byte[0]; - - Logger.debug(logger, "Decrypted: {}, o1: {}, o2: {}", - StringUtils.bytesToHex(decrypted), - StringUtils.bytesToHex(output1), - StringUtils.bytesToHex(output2)); - - Map results = new HashMap<>(); - if (inputs.prf != null) { - return serializationType -> { - results.put("first", serializationType == SerializationType.JSON - ? toUrlSafeString(output1) - : output1); - if (output2.length > 0) { - results.put("second", serializationType == SerializationType.JSON - ? toUrlSafeString(output2) - : output2); - } - return Collections.singletonMap("prf", - Collections.singletonMap("results", results)); - }; - } else { - return serializationType -> { - results.put("output1", serializationType == SerializationType.JSON - ? toUrlSafeString(output1) - : output1); - if (output2.length > 0) { - results.put("output2", serializationType == SerializationType.JSON - ? toUrlSafeString(output2) - : output2); - } - return Collections.singletonMap("hmacGetSecret", results); - }; - } + byte[] value = (byte[]) extensionOutputs.get(name); + if (value == null) { + return null; + } + + byte[] decrypted = clientPin.getPinUvAuth().decrypt(keyAgreement.second, value); + + byte[] output1 = Arrays.copyOf(decrypted, SALT_LEN); + byte[] output2 = + decrypted.length > SALT_LEN + ? Arrays.copyOfRange(decrypted, SALT_LEN, 2 * SALT_LEN) + : new byte[0]; + + Logger.debug( + logger, + "Decrypted: {}, o1: {}, o2: {}", + StringUtils.bytesToHex(decrypted), + StringUtils.bytesToHex(output1), + StringUtils.bytesToHex(output2)); + + Map results = new HashMap<>(); + if (inputs.prf != null) { + return serializationType -> { + results.put( + "first", + serializationType == SerializationType.JSON ? toUrlSafeString(output1) : output1); + if (output2.length > 0) { + results.put( + "second", + serializationType == SerializationType.JSON + ? toUrlSafeString(output2) + : output2); + } + return Collections.singletonMap("prf", Collections.singletonMap("results", results)); + }; + } else { + return serializationType -> { + results.put( + "output1", + serializationType == SerializationType.JSON ? toUrlSafeString(output1) : output1); + if (output2.length > 0) { + results.put( + "output2", + serializationType == SerializationType.JSON + ? toUrlSafeString(output2) + : output2); + } + return Collections.singletonMap("hmacGetSecret", results); + }; + } }; - return new AuthenticationProcessor(prepareInput, prepareOutput); - } - - Map registrationOutput(AttestationObject attestationObject, boolean isPrf) { - Map extensions = attestationObject.getAuthenticatorData().getExtensions(); + return new AuthenticationProcessor(prepareInput, prepareOutput); + } - boolean enabled = extensions != null && Boolean.TRUE.equals(extensions.get(name)); - return isPrf - ? Collections.singletonMap("prf", Collections.singletonMap("enabled", enabled)) - : Collections.singletonMap("hmacCreateSecret", enabled); - } + Map registrationOutput(AttestationObject attestationObject, boolean isPrf) { + Map extensions = attestationObject.getAuthenticatorData().getExtensions(); + boolean enabled = extensions != null && Boolean.TRUE.equals(extensions.get(name)); + return isPrf + ? Collections.singletonMap("prf", Collections.singletonMap("enabled", enabled)) + : Collections.singletonMap("hmacCreateSecret", enabled); + } - private static class Salts { - byte[] salt1; - byte[] salt2; + private static class Salts { + byte[] salt1; + byte[] salt2; - Salts(byte[] salt1, @Nullable byte[] salt2) { - this.salt1 = salt1; - this.salt2 = salt2 != null ? salt2 : new byte[0]; - } + Salts(byte[] salt1, @Nullable byte[] salt2) { + this.salt1 = salt1; + this.salt2 = salt2 != null ? salt2 : new byte[0]; + } + } + + private byte[] prfSalt(byte[] secret) { + try { + return MessageDigest.getInstance("SHA-256") + .digest( + ByteBuffer.allocate(13 + secret.length) + .put("WebAuthn PRF".getBytes(StandardCharsets.US_ASCII)) + .put((byte) 0x00) + .put(secret) + .array()); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("SHA-256 missing", e); } + } + + private static class PrfInputs { + @Nullable final Map eval; + @Nullable final Map evalByCredential; - private byte[] prfSalt(byte[] secret) { - try { - return MessageDigest.getInstance("SHA-256").digest( - ByteBuffer - .allocate(13 + secret.length) - .put("WebAuthn PRF".getBytes(StandardCharsets.US_ASCII)) - .put((byte) 0x00) - .put(secret) - .array()); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("SHA-256 missing", e); - } + PrfInputs(@Nullable Map eval, @Nullable Map evalByCredential) { + this.eval = eval; + this.evalByCredential = evalByCredential; } - private static class PrfInputs { - @Nullable final Map eval; - @Nullable final Map evalByCredential; + @SuppressWarnings("unchecked") + @Nullable static PrfInputs fromMap(@Nullable Map map) { + if (map == null) { + return null; + } - PrfInputs(@Nullable Map eval, @Nullable Map evalByCredential) { - this.eval = eval; - this.evalByCredential = evalByCredential; - } + return new PrfInputs( + (Map) map.get("eval"), (Map) map.get("evalByCredential")); + } + } - @SuppressWarnings("unchecked") - @Nullable - static PrfInputs fromMap(@Nullable Map map) { - if (map == null) { - return null; - } + private static class HmacInputs { + @Nullable final String salt1; + @Nullable final String salt2; - return new PrfInputs( - (Map) map.get("eval"), - (Map) map.get("evalByCredential") - ); - } + HmacInputs(@Nullable String salt1, @Nullable String salt2) { + this.salt1 = salt1; + this.salt2 = salt2; } - private static class HmacInputs { - @Nullable final String salt1; - @Nullable final String salt2; + @Nullable static HmacInputs fromMap(@Nullable Map map) { + if (map == null) { + return null; + } - HmacInputs(@Nullable String salt1, @Nullable String salt2) { - this.salt1 = salt1; - this.salt2 = salt2; - } + return new HmacInputs((String) map.get("salt1"), (String) map.get("salt2")); + } + } - @Nullable - static HmacInputs fromMap(@Nullable Map map) { - if (map == null) { - return null; - } + @SuppressWarnings("unchecked") + private static class Inputs { + @Nullable final PrfInputs prf; + @Nullable final HmacInputs hmac; - return new HmacInputs( - (String) map.get("salt1"), - (String) map.get("salt2") - ); - } + Inputs(@Nullable Map prf, @Nullable Map hmac) { + this.prf = PrfInputs.fromMap(prf); + this.hmac = HmacInputs.fromMap(hmac); } - @SuppressWarnings("unchecked") - private static class Inputs { - @Nullable final PrfInputs prf; - @Nullable final HmacInputs hmac; - - Inputs(@Nullable Map prf, @Nullable Map hmac) { - this.prf = PrfInputs.fromMap(prf); - this.hmac = HmacInputs.fromMap(hmac); - } - - @Nullable - Map evalByCredential(String key) { - if (prf == null || prf.evalByCredential == null) { - return null; - } + @Nullable Map evalByCredential(String key) { + if (prf == null || prf.evalByCredential == null) { + return null; + } - return (Map) prf.evalByCredential.get(key); - } + return (Map) prf.evalByCredential.get(key); + } - @Nullable - public static Inputs fromExtensions(@Nullable Extensions extensions) { - if (extensions == null) { - return null; - } + @Nullable public static Inputs fromExtensions(@Nullable Extensions extensions) { + if (extensions == null) { + return null; + } - return new Inputs( - (Map) extensions.get("prf"), - (Map) extensions.get("hmacGetSecret")); - } + return new Inputs( + (Map) extensions.get("prf"), + (Map) extensions.get("hmacGetSecret")); } + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/LargeBlobExtension.java b/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/LargeBlobExtension.java index 403423d3..df339a6a 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/LargeBlobExtension.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/LargeBlobExtension.java @@ -29,178 +29,174 @@ import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialCreationOptions; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialRequestOptions; import com.yubico.yubikit.fido.webauthn.SerializationType; - -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.security.GeneralSecurityException; import java.util.Collections; import java.util.Map; - import javax.annotation.Nullable; +import org.slf4j.LoggerFactory; /** * Implements the Large Blob storage (largeBlob) WebAuthn extension. - * @see Large blob extension - * @see Large Blob Key (largeBlobKey) + * + * @see Large blob + * extension + * @see Large + * Blob Key (largeBlobKey) */ public class LargeBlobExtension extends Extension { - private static final String LARGE_BLOB_KEY = "largeBlobKey"; - private static final String LARGE_BLOB = "largeBlob"; - private static final String LARGE_BLOBS = "largeBlobs"; - private static final String ACTION_READ = "read"; - private static final String ACTION_WRITE = "write"; - private static final String WRITTEN = "written"; - private static final String SUPPORT = "support"; - private static final String SUPPORTED = "supported"; - private static final String REQUIRED = "required"; - private static final String BLOB = "blob"; - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(LargeBlobExtension.class); - - public LargeBlobExtension() { - super(LARGE_BLOB_KEY); + private static final String LARGE_BLOB_KEY = "largeBlobKey"; + private static final String LARGE_BLOB = "largeBlob"; + private static final String LARGE_BLOBS = "largeBlobs"; + private static final String ACTION_READ = "read"; + private static final String ACTION_WRITE = "write"; + private static final String WRITTEN = "written"; + private static final String SUPPORT = "support"; + private static final String SUPPORTED = "supported"; + private static final String REQUIRED = "required"; + private static final String BLOB = "blob"; + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(LargeBlobExtension.class); + + public LargeBlobExtension() { + super(LARGE_BLOB_KEY); + } + + @Override + protected boolean isSupported(Ctap2Session ctap) { + return super.isSupported(ctap) && ctap.getCachedInfo().getOptions().containsKey(LARGE_BLOBS); + } + + @Nullable @Override + public RegistrationProcessor makeCredential( + Ctap2Session ctap, + PublicKeyCredentialCreationOptions options, + PinUvAuthProtocol pinUvAuthProtocol) { + final Inputs inputs = Inputs.fromExtensions(options.getExtensions()); + if (inputs != null) { + if (inputs.read != null || inputs.write != null) { + throw new IllegalArgumentException("Invalid set of parameters"); + } + if (REQUIRED.equals(inputs.support) && !isSupported(ctap)) { + throw new IllegalArgumentException("Authenticator does not support large blob storage"); + } + return new RegistrationProcessor( + pinToken -> Collections.singletonMap(LARGE_BLOB, true), + (attestationObject, pinToken) -> + serializationType -> + Collections.singletonMap( + LARGE_BLOB, + Collections.singletonMap( + SUPPORTED, attestationObject.getLargeBlobKey() != null))); + } + return null; + } + + @Nullable @Override + public AuthenticationProcessor getAssertion( + Ctap2Session ctap, + PublicKeyCredentialRequestOptions options, + PinUvAuthProtocol pinUvAuthProtocol) { + + final Inputs inputs = Inputs.fromExtensions(options.getExtensions()); + if (inputs == null) { + return null; + } + if (Boolean.TRUE.equals(inputs.read)) { + return new AuthenticationProcessor( + (selected, pinToken) -> Collections.singletonMap(LARGE_BLOB, true), + (assertionData, pinToken) -> read(assertionData, ctap)); + } else if (inputs.write != null) { + return new AuthenticationProcessor( + (selected, pinToken) -> Collections.singletonMap(LARGE_BLOB, true), + (assertionData, pinToken) -> + write( + assertionData, + ctap, + fromUrlSafeString(inputs.write), + pinUvAuthProtocol, + pinToken), + ClientPin.PIN_PERMISSION_LBW); } + return null; + } - @Override - protected boolean isSupported(Ctap2Session ctap) { - return super.isSupported(ctap) && - ctap.getCachedInfo().getOptions().containsKey(LARGE_BLOBS); + @Nullable ClientExtensionResultProvider read(Ctap2Session.AssertionData assertionData, Ctap2Session ctap) { + + byte[] largeBlobKey = assertionData.getLargeBlobKey(); + if (largeBlobKey == null) { + return null; } - @Nullable - @Override - public RegistrationProcessor makeCredential( - Ctap2Session ctap, - PublicKeyCredentialCreationOptions options, - PinUvAuthProtocol pinUvAuthProtocol) { - final Inputs inputs = Inputs.fromExtensions(options.getExtensions()); - if (inputs != null) { - if (inputs.read != null || inputs.write != null) { - throw new IllegalArgumentException("Invalid set of parameters"); - } - if (REQUIRED.equals(inputs.support) && !isSupported(ctap)) { - throw new IllegalArgumentException("Authenticator does not support large blob storage"); - } - return new RegistrationProcessor( - pinToken -> Collections.singletonMap(LARGE_BLOB, true), - (attestationObject, pinToken) -> - serializationType -> Collections.singletonMap(LARGE_BLOB, - Collections.singletonMap(SUPPORTED, - attestationObject.getLargeBlobKey() != null)) - ); - } - return null; + try { + LargeBlobs largeBlobs = new LargeBlobs(ctap); + byte[] blob = largeBlobs.getBlob(largeBlobKey); + return serializationType -> + Collections.singletonMap( + LARGE_BLOB, + blob != null + ? Collections.singletonMap( + BLOB, + serializationType == SerializationType.JSON ? toUrlSafeString(blob) : blob) + : Collections.emptyMap()); + } catch (IOException | CommandException e) { + Logger.error(logger, "LargeBlob processing failed: ", e); } - @Nullable - @Override - public AuthenticationProcessor getAssertion( - Ctap2Session ctap, - PublicKeyCredentialRequestOptions options, - PinUvAuthProtocol pinUvAuthProtocol) { - - final Inputs inputs = Inputs.fromExtensions(options.getExtensions()); - if (inputs == null) { - return null; - } - if (Boolean.TRUE.equals(inputs.read)) { - return new AuthenticationProcessor( - (selected, pinToken) -> Collections.singletonMap(LARGE_BLOB, true), - (assertionData, pinToken) -> read(assertionData, ctap) - ); - } else if (inputs.write != null) { - return new AuthenticationProcessor( - (selected, pinToken) -> Collections.singletonMap(LARGE_BLOB, true), - (assertionData, pinToken) -> write(assertionData, ctap, - fromUrlSafeString(inputs.write), - pinUvAuthProtocol, pinToken), - ClientPin.PIN_PERMISSION_LBW); - } - return null; + return null; + } + + @Nullable ClientExtensionResultProvider write( + Ctap2Session.AssertionData assertionData, + Ctap2Session ctap, + byte[] bytes, + PinUvAuthProtocol pinUvAuthProtocol, + @Nullable byte[] pinToken) { + + byte[] largeBlobKey = assertionData.getLargeBlobKey(); + if (largeBlobKey == null) { + return null; } - @Nullable - ClientExtensionResultProvider read( - Ctap2Session.AssertionData assertionData, - Ctap2Session ctap) { - - byte[] largeBlobKey = assertionData.getLargeBlobKey(); - if (largeBlobKey == null) { - return null; - } - - try { - LargeBlobs largeBlobs = new LargeBlobs(ctap); - byte[] blob = largeBlobs.getBlob(largeBlobKey); - return serializationType -> Collections.singletonMap(LARGE_BLOB, blob != null - ? Collections.singletonMap(BLOB, serializationType == SerializationType.JSON - ? toUrlSafeString(blob) - : blob) - : Collections.emptyMap()); - } catch (IOException | CommandException e) { - Logger.error(logger, "LargeBlob processing failed: ", e); - } + try { + LargeBlobs largeBlobs = new LargeBlobs(ctap, pinUvAuthProtocol, pinToken); + largeBlobs.putBlob(largeBlobKey, bytes); - return null; + return serializationType -> + Collections.singletonMap(LARGE_BLOB, Collections.singletonMap(WRITTEN, true)); + + } catch (IOException | CommandException | GeneralSecurityException e) { + Logger.error(logger, "LargeBlob processing failed: ", e); } - @Nullable - ClientExtensionResultProvider write( - Ctap2Session.AssertionData assertionData, - Ctap2Session ctap, - byte[] bytes, - PinUvAuthProtocol pinUvAuthProtocol, - @Nullable byte[] pinToken) { - - byte[] largeBlobKey = assertionData.getLargeBlobKey(); - if (largeBlobKey == null) { - return null; - } - - try { - LargeBlobs largeBlobs = new LargeBlobs( - ctap, - pinUvAuthProtocol, - pinToken); - largeBlobs.putBlob(largeBlobKey, bytes); - - return serializationType -> - Collections.singletonMap(LARGE_BLOB, Collections.singletonMap(WRITTEN, true)); - - } catch (IOException | CommandException | GeneralSecurityException e) { - Logger.error(logger, "LargeBlob processing failed: ", e); - } + return null; + } - return null; + private static class Inputs { + @Nullable final Boolean read; + @Nullable final String write; + @Nullable final String support; + + private Inputs(@Nullable Boolean read, @Nullable String write, @Nullable String support) { + this.read = read; + this.write = write; + this.support = support; } + @SuppressWarnings("unchecked") + @Nullable static Inputs fromExtensions(@Nullable Extensions extensions) { + if (extensions == null) { + return null; + } - private static class Inputs { - @Nullable final Boolean read; - @Nullable final String write; - @Nullable final String support; - - private Inputs(@Nullable Boolean read, @Nullable String write, @Nullable String support) { - this.read = read; - this.write = write; - this.support = support; - } - - @SuppressWarnings("unchecked") - @Nullable - static Inputs fromExtensions(@Nullable Extensions extensions) { - if (extensions == null) { - return null; - } - - Map data = (Map) extensions.get(LARGE_BLOB); - if (data == null) { - return null; - } - return new Inputs( - (Boolean) data.get(ACTION_READ), - (String) data.get(ACTION_WRITE), - (String) data.get(SUPPORT)); - } + Map data = (Map) extensions.get(LARGE_BLOB); + if (data == null) { + return null; + } + return new Inputs( + (Boolean) data.get(ACTION_READ), + (String) data.get(ACTION_WRITE), + (String) data.get(SUPPORT)); } + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/LargeBlobs.java b/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/LargeBlobs.java index 2defbe15..c6b7591a 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/LargeBlobs.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/LargeBlobs.java @@ -21,7 +21,6 @@ import com.yubico.yubikit.fido.Cbor; import com.yubico.yubikit.fido.ctap.Ctap2Session; import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; - import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FilterInputStream; @@ -42,7 +41,6 @@ import java.util.zip.DeflaterInputStream; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; - import javax.annotation.Nullable; import javax.crypto.Cipher; import javax.crypto.spec.GCMParameterSpec; @@ -50,370 +48,353 @@ public class LargeBlobs { - private final Ctap2Session session; - private final int maxFragmentLen; + private final Ctap2Session session; + private final int maxFragmentLen; - @Nullable final PinUvAuthProtocol pinUvAuthProtocol; - @Nullable final byte[] pinUvAuthToken; + @Nullable final PinUvAuthProtocol pinUvAuthProtocol; + @Nullable final byte[] pinUvAuthToken; - LargeBlobs(Ctap2Session session) { - this(session, null, null); - } + LargeBlobs(Ctap2Session session) { + this(session, null, null); + } - LargeBlobs(Ctap2Session session, - @Nullable - PinUvAuthProtocol pinUvAuthProtocol, - @Nullable - byte[] pinUvAuthToken) { + LargeBlobs( + Ctap2Session session, + @Nullable PinUvAuthProtocol pinUvAuthProtocol, + @Nullable byte[] pinUvAuthToken) { - final Ctap2Session.InfoData info = session.getCachedInfo(); + final Ctap2Session.InfoData info = session.getCachedInfo(); - if (!isSupported(info)) { - throw new IllegalStateException("Authenticator does not support large blobs"); - } + if (!isSupported(info)) { + throw new IllegalStateException("Authenticator does not support large blobs"); + } - this.session = session; - this.maxFragmentLen = info.getMaxMsgSize() - 64; + this.session = session; + this.maxFragmentLen = info.getMaxMsgSize() - 64; - if (pinUvAuthToken != null && pinUvAuthProtocol != null) { - this.pinUvAuthProtocol = pinUvAuthProtocol; - this.pinUvAuthToken = pinUvAuthToken; - } else { - this.pinUvAuthProtocol = null; - this.pinUvAuthToken = null; - } + if (pinUvAuthToken != null && pinUvAuthProtocol != null) { + this.pinUvAuthProtocol = pinUvAuthProtocol; + this.pinUvAuthToken = pinUvAuthToken; + } else { + this.pinUvAuthProtocol = null; + this.pinUvAuthToken = null; } - - static boolean isSupported(Ctap2Session.InfoData info) { - return Boolean.TRUE.equals(info.getOptions().get("largeBlobs")); + } + + static boolean isSupported(Ctap2Session.InfoData info) { + return Boolean.TRUE.equals(info.getOptions().get("largeBlobs")); + } + + LargeBlobArray readBlobArray() throws IOException, CommandException { + int offset = 0; + ByteArrayOutputStream os = new ByteArrayOutputStream(); + while (true) { + Map map = session.largeBlobs(offset, maxFragmentLen, null, null, null, null); + if (!map.containsKey(1)) { + return LargeBlobArray.empty(); + } + + byte[] fragment = (byte[]) map.get(1); + os.write(fragment); + + if (fragment.length < maxFragmentLen) { + break; + } + offset += fragment.length; } - - LargeBlobArray readBlobArray() throws IOException, CommandException { - int offset = 0; - ByteArrayOutputStream os = new ByteArrayOutputStream(); - while (true) { - Map map = session.largeBlobs(offset, maxFragmentLen, null, null, null, null); - if (!map.containsKey(1)) { - return LargeBlobArray.empty(); - } - - byte[] fragment = (byte[]) map.get(1); - os.write(fragment); - - if (fragment.length < maxFragmentLen) { - break; - } - offset += fragment.length; - } - byte[] buffer = os.toByteArray(); - byte[] data = Arrays.copyOf(buffer, buffer.length - 16); - byte[] digest = hash(data); - if (!Arrays.equals( - Arrays.copyOfRange(buffer, buffer.length - 16, buffer.length), - Arrays.copyOf(digest, digest.length - 16))) { - return LargeBlobArray.empty(); - } - - return LargeBlobArray.fromBytes(data); + byte[] buffer = os.toByteArray(); + byte[] data = Arrays.copyOf(buffer, buffer.length - 16); + byte[] digest = hash(data); + if (!Arrays.equals( + Arrays.copyOfRange(buffer, buffer.length - 16, buffer.length), + Arrays.copyOf(digest, digest.length - 16))) { + return LargeBlobArray.empty(); } - void writeBlobArray(LargeBlobArray largeBlobArray) throws IOException, CommandException { - final byte[] data = largeBlobArray.toBytes(); - - byte[] dataWithHash = ByteBuffer - .allocate(data.length + 16) - .put(data) - .put(hash(data), 0, 16) - .array(); - - int offset = 0; - int size = dataWithHash.length; - while (offset < size) { - int ln = Math.min(size - offset, maxFragmentLen); - byte[] fragment = Arrays.copyOfRange(dataWithHash, offset, offset + ln); - - Integer pinUvAuthProtocolVersion = null; - byte[] pinUvAuthParam = null; - - if (pinUvAuthToken != null && pinUvAuthProtocol != null) { - byte[] msg = fragmentMessage(offset, fragment); - pinUvAuthProtocolVersion = this.pinUvAuthProtocol.getVersion(); - pinUvAuthParam = pinUvAuthProtocol.authenticate(pinUvAuthToken, msg); - } - - session.largeBlobs( - offset, - null, - fragment, - offset == 0 ? size : null, - pinUvAuthParam, - pinUvAuthProtocolVersion); - - offset += ln; - } - } + return LargeBlobArray.fromBytes(data); + } - private final static byte[] fragmentMessagePrefix = new byte[32]; - static { - Arrays.fill(fragmentMessagePrefix, (byte) 0xff); - } + void writeBlobArray(LargeBlobArray largeBlobArray) throws IOException, CommandException { + final byte[] data = largeBlobArray.toBytes(); - private byte[] fragmentMessage(int offset, byte[] fragment) { - return ByteBuffer - .allocate(70) - .put(fragmentMessagePrefix) - .put((byte) 0x0c) - .put((byte) 0x00) - .order(ByteOrder.LITTLE_ENDIAN) - .putInt(offset) - .put(hash(fragment)) - .array(); - } + byte[] dataWithHash = + ByteBuffer.allocate(data.length + 16).put(data).put(hash(data), 0, 16).array(); - @Nullable - byte[] getBlob(byte[] largeBlobKey) throws IOException, CommandException { - for (LargeBlobMap entry : readBlobArray()) { - try { - byte[] blob = CompressionUtils.decompress(unpack(largeBlobKey, entry)); - if (blob.length == entry.getOrigSize()) { - return blob; - } - } catch (GeneralSecurityException ignoredException) { - // ignoring this entry - } - } - return null; - } + int offset = 0; + int size = dataWithHash.length; + while (offset < size) { + int ln = Math.min(size - offset, maxFragmentLen); + byte[] fragment = Arrays.copyOfRange(dataWithHash, offset, offset + ln); - void putBlob(byte[] largeBlobKey, @Nullable byte[] data) throws IOException, CommandException, GeneralSecurityException { - boolean modified = data != null; - LargeBlobArray blobArray = readBlobArray(); - List entries = new ArrayList<>(); - for (LargeBlobMap largeBlobMap : blobArray) { - try { - unpack(largeBlobKey, largeBlobMap); - modified = true; - } catch (Exception e) { - entries.add(largeBlobMap); - } - } + Integer pinUvAuthProtocolVersion = null; + byte[] pinUvAuthParam = null; - if (data != null) { - entries.add(pack(largeBlobKey, data)); - } + if (pinUvAuthToken != null && pinUvAuthProtocol != null) { + byte[] msg = fragmentMessage(offset, fragment); + pinUvAuthProtocolVersion = this.pinUvAuthProtocol.getVersion(); + pinUvAuthParam = pinUvAuthProtocol.authenticate(pinUvAuthToken, msg); + } + + session.largeBlobs( + offset, + null, + fragment, + offset == 0 ? size : null, + pinUvAuthParam, + pinUvAuthProtocolVersion); - if (modified) { - writeBlobArray(new LargeBlobArray(entries)); + offset += ln; + } + } + + private static final byte[] fragmentMessagePrefix = new byte[32]; + + static { + Arrays.fill(fragmentMessagePrefix, (byte) 0xff); + } + + private byte[] fragmentMessage(int offset, byte[] fragment) { + return ByteBuffer.allocate(70) + .put(fragmentMessagePrefix) + .put((byte) 0x0c) + .put((byte) 0x00) + .order(ByteOrder.LITTLE_ENDIAN) + .putInt(offset) + .put(hash(fragment)) + .array(); + } + + @Nullable byte[] getBlob(byte[] largeBlobKey) throws IOException, CommandException { + for (LargeBlobMap entry : readBlobArray()) { + try { + byte[] blob = CompressionUtils.decompress(unpack(largeBlobKey, entry)); + if (blob.length == entry.getOrigSize()) { + return blob; } + } catch (GeneralSecurityException ignoredException) { + // ignoring this entry + } } - - private byte[] unpack(byte[] key, final LargeBlobMap largeBlobMap) throws GeneralSecurityException { - return AesGcm.decrypt( - key, - largeBlobMap.getNonce(), - largeBlobMap.getCiphertext(), - associatedData(largeBlobMap.getOrigSize()) - ); + return null; + } + + void putBlob(byte[] largeBlobKey, @Nullable byte[] data) + throws IOException, CommandException, GeneralSecurityException { + boolean modified = data != null; + LargeBlobArray blobArray = readBlobArray(); + List entries = new ArrayList<>(); + for (LargeBlobMap largeBlobMap : blobArray) { + try { + unpack(largeBlobKey, largeBlobMap); + modified = true; + } catch (Exception e) { + entries.add(largeBlobMap); + } } - private LargeBlobMap pack(byte[] key, byte[] data) throws IOException, GeneralSecurityException { - int origSize = data.length; - byte[] nonce = RandomUtils.getRandomBytes(12); - byte[] ciphertext = AesGcm.encrypt(key, nonce, CompressionUtils.compress(data), associatedData(origSize)); - - return new LargeBlobMap(ciphertext, nonce, origSize); + if (data != null) { + entries.add(pack(largeBlobKey, data)); } - private byte[] associatedData(int origSize) { - return ByteBuffer - .allocate(12) - .order(ByteOrder.BIG_ENDIAN) - .putInt(0x626c6f62) // blob - .order(ByteOrder.LITTLE_ENDIAN) - .putLong(origSize) - .array(); + if (modified) { + writeBlobArray(new LargeBlobArray(entries)); + } + } + + private byte[] unpack(byte[] key, final LargeBlobMap largeBlobMap) + throws GeneralSecurityException { + return AesGcm.decrypt( + key, + largeBlobMap.getNonce(), + largeBlobMap.getCiphertext(), + associatedData(largeBlobMap.getOrigSize())); + } + + private LargeBlobMap pack(byte[] key, byte[] data) throws IOException, GeneralSecurityException { + int origSize = data.length; + byte[] nonce = RandomUtils.getRandomBytes(12); + byte[] ciphertext = + AesGcm.encrypt(key, nonce, CompressionUtils.compress(data), associatedData(origSize)); + + return new LargeBlobMap(ciphertext, nonce, origSize); + } + + private byte[] associatedData(int origSize) { + return ByteBuffer.allocate(12) + .order(ByteOrder.BIG_ENDIAN) + .putInt(0x626c6f62) // blob + .order(ByteOrder.LITTLE_ENDIAN) + .putLong(origSize) + .array(); + } + + static class LargeBlobArray implements Iterable { + + @Nullable final List entries; + + private LargeBlobArray(@Nullable final List entries) { + this.entries = entries; } - static class LargeBlobArray implements Iterable { - - @Nullable final List entries; + static LargeBlobArray empty() { + return new LargeBlobArray(null); + } - private LargeBlobArray(@Nullable final List entries) { - this.entries = entries; + static LargeBlobArray fromBytes(byte[] cbor) { + try { + @SuppressWarnings("unchecked") + List> list = (List>) Cbor.decode(cbor); + if (list == null) { + return empty(); } - - static LargeBlobArray empty() { - return new LargeBlobArray(null); + final List entries = new ArrayList<>(); + for (Map entry : list) { + LargeBlobMap largeBlobMap = LargeBlobMap.fromMap(entry); + if (largeBlobMap != null) { + // only add conforming items + entries.add(largeBlobMap); + } } + return new LargeBlobArray(entries); + } catch (Exception e) { + return empty(); + } + } - static LargeBlobArray fromBytes(byte[] cbor) { - try { - @SuppressWarnings("unchecked") - List> list = (List>) Cbor.decode(cbor); - if (list == null) { - return empty(); - } - final List entries = new ArrayList<>(); - for (Map entry : list) { - LargeBlobMap largeBlobMap = LargeBlobMap.fromMap(entry); - if (largeBlobMap != null) { - // only add conforming items - entries.add(largeBlobMap); - } - } - return new LargeBlobArray(entries); - } catch (Exception e) { - return empty(); - } - } + byte[] toBytes() { + if (entries == null) { + return new byte[0]; + } + + List> largeBlobs = new ArrayList<>(); + for (LargeBlobMap map : entries) { + largeBlobs.add(map.toMap()); + } + + return Cbor.encode(largeBlobs); + } - byte[] toBytes() { - if (entries == null) { - return new byte[0]; - } + @Override + public Iterator iterator() { + return new Iterator() { - List> largeBlobs = new ArrayList<>(); - for (LargeBlobMap map : entries) { - largeBlobs.add(map.toMap()); - } + private int currentIndex = 0; - return Cbor.encode(largeBlobs); + @Override + public boolean hasNext() { + return entries != null && currentIndex < entries.size(); } @Override - public Iterator iterator() { - return new Iterator() { - - private int currentIndex = 0; - - @Override - public boolean hasNext() { - return entries != null && currentIndex < entries.size(); - } - - @Override - public LargeBlobMap next() { - if (entries == null || currentIndex >= entries.size()) { - throw new NoSuchElementException(); - } - return entries.get(currentIndex++); - } - }; + public LargeBlobMap next() { + if (entries == null || currentIndex >= entries.size()) { + throw new NoSuchElementException(); + } + return entries.get(currentIndex++); } + }; } + } - static class LargeBlobMap { - private static final int CIPHERTEXT = 1; - private static final int NONCE = 2; - private static final int ORIG_SIZE = 3; - - private final Map data; + static class LargeBlobMap { + private static final int CIPHERTEXT = 1; + private static final int NONCE = 2; + private static final int ORIG_SIZE = 3; - private LargeBlobMap(byte[] ciphertext, byte[] nonce, int origSize) { - data = new HashMap<>(); - data.put(CIPHERTEXT, ciphertext); - data.put(NONCE, nonce ); - data.put(ORIG_SIZE, origSize); - } + private final Map data; - Map toMap() { - return data; - } + private LargeBlobMap(byte[] ciphertext, byte[] nonce, int origSize) { + data = new HashMap<>(); + data.put(CIPHERTEXT, ciphertext); + data.put(NONCE, nonce); + data.put(ORIG_SIZE, origSize); + } - @Nullable - static LargeBlobMap fromMap(Map map) { - byte[] ciphertext = (byte[]) map.get(CIPHERTEXT); - byte[] nonce = (byte[]) map.get(NONCE); - Integer origSize = (Integer) map.get(ORIG_SIZE); + Map toMap() { + return data; + } - if (ciphertext == null || nonce == null || origSize == null) { - // does not conform large-blob map - return null; - } + @Nullable static LargeBlobMap fromMap(Map map) { + byte[] ciphertext = (byte[]) map.get(CIPHERTEXT); + byte[] nonce = (byte[]) map.get(NONCE); + Integer origSize = (Integer) map.get(ORIG_SIZE); - return new LargeBlobMap(ciphertext, nonce, origSize); - } + if (ciphertext == null || nonce == null || origSize == null) { + // does not conform large-blob map + return null; + } - byte[] getCiphertext() { - return (byte[]) data.get(CIPHERTEXT); - } + return new LargeBlobMap(ciphertext, nonce, origSize); + } - byte[] getNonce() { - return (byte[]) data.get(NONCE); - } + byte[] getCiphertext() { + return (byte[]) data.get(CIPHERTEXT); + } - int getOrigSize() { - return (int) data.get(ORIG_SIZE); - } + byte[] getNonce() { + return (byte[]) data.get(NONCE); } - static class CompressionUtils { - static byte[] decompress(byte[] data) throws IOException { - ByteArrayInputStream inputStream = new ByteArrayInputStream(data); - InflaterInputStream inflaterInputStream = - new InflaterInputStream(inputStream, new Inflater(true)); - return process(inflaterInputStream); - } + int getOrigSize() { + return (int) data.get(ORIG_SIZE); + } + } + + static class CompressionUtils { + static byte[] decompress(byte[] data) throws IOException { + ByteArrayInputStream inputStream = new ByteArrayInputStream(data); + InflaterInputStream inflaterInputStream = + new InflaterInputStream(inputStream, new Inflater(true)); + return process(inflaterInputStream); + } - static byte[] compress(byte[] input) throws IOException { - ByteArrayInputStream inputStream = new ByteArrayInputStream(input); - DeflaterInputStream deflaterInputStream = - new DeflaterInputStream(inputStream, new Deflater(-1, true)); - return process(deflaterInputStream); - } + static byte[] compress(byte[] input) throws IOException { + ByteArrayInputStream inputStream = new ByteArrayInputStream(input); + DeflaterInputStream deflaterInputStream = + new DeflaterInputStream(inputStream, new Deflater(-1, true)); + return process(deflaterInputStream); + } - static private byte[] process(FilterInputStream filterInputStream) throws IOException { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - byte[] buf = new byte[255]; - int len; - while ((len = filterInputStream.read(buf)) != -1) { - outputStream.write(buf, 0, len); - } - return outputStream.toByteArray(); - } + private static byte[] process(FilterInputStream filterInputStream) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] buf = new byte[255]; + int len; + while ((len = filterInputStream.read(buf)) != -1) { + outputStream.write(buf, 0, len); + } + return outputStream.toByteArray(); } + } - static class AesGcm { - static byte[] decrypt( - byte[] key, - byte[] nonce, - byte[] data, - byte[] associatedData - ) throws GeneralSecurityException { - return process(Cipher.DECRYPT_MODE, key, nonce, data, associatedData); - } + static class AesGcm { + static byte[] decrypt(byte[] key, byte[] nonce, byte[] data, byte[] associatedData) + throws GeneralSecurityException { + return process(Cipher.DECRYPT_MODE, key, nonce, data, associatedData); + } - static byte[] encrypt( - byte[] key, - byte[] nonce, - byte[] data, - byte[] associatedData - ) throws GeneralSecurityException { - return process(Cipher.ENCRYPT_MODE, key, nonce, data, associatedData); - } + static byte[] encrypt(byte[] key, byte[] nonce, byte[] data, byte[] associatedData) + throws GeneralSecurityException { + return process(Cipher.ENCRYPT_MODE, key, nonce, data, associatedData); + } - private static byte[] process( - int mode, - byte[] key, - byte[] nonce, - byte[] ciphertext, - byte[] associatedData - ) throws GeneralSecurityException { - Cipher c = Cipher.getInstance("AES/GCM/NoPadding"); - SecretKeySpec k = new SecretKeySpec(key, "AES"); - GCMParameterSpec p = new GCMParameterSpec(128, nonce); - c.init(mode, k, p); - c.updateAAD(associatedData); - c.update(ciphertext); - return c.doFinal(); - } + private static byte[] process( + int mode, byte[] key, byte[] nonce, byte[] ciphertext, byte[] associatedData) + throws GeneralSecurityException { + Cipher c = Cipher.getInstance("AES/GCM/NoPadding"); + SecretKeySpec k = new SecretKeySpec(key, "AES"); + GCMParameterSpec p = new GCMParameterSpec(128, nonce); + c.init(mode, k, p); + c.updateAAD(associatedData); + c.update(ciphertext); + return c.doFinal(); } + } - static byte[] hash(byte[] message) { - try { - return MessageDigest.getInstance("SHA-256").digest(message); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } + static byte[] hash(byte[] message) { + try { + return MessageDigest.getInstance("SHA-256").digest(message); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); } + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/MinPinLengthExtension.java b/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/MinPinLengthExtension.java index 58f168d5..f80bcd0b 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/MinPinLengthExtension.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/MinPinLengthExtension.java @@ -20,48 +20,48 @@ import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import com.yubico.yubikit.fido.webauthn.Extensions; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialCreationOptions; - import java.util.Collections; - import javax.annotation.Nullable; /** * Implements the Minimum PIN Length (minPinLength) CTAP2 extension. - * @see Minimum PIN Length Extension (minPinLength) + * + * @see Minimum + * PIN Length Extension (minPinLength) */ public class MinPinLengthExtension extends Extension { - public MinPinLengthExtension() { - super("minPinLength"); - } + public MinPinLengthExtension() { + super("minPinLength"); + } - @Override - protected boolean isSupported(Ctap2Session ctap) { - return super.isSupported(ctap) && - ctap.getCachedInfo().getOptions().containsKey("setMinPINLength"); - } + @Override + protected boolean isSupported(Ctap2Session ctap) { + return super.isSupported(ctap) + && ctap.getCachedInfo().getOptions().containsKey("setMinPINLength"); + } - @Nullable - @Override - public RegistrationProcessor makeCredential( - Ctap2Session ctap, - PublicKeyCredentialCreationOptions options, - PinUvAuthProtocol pinUvAuthProtocol) { + @Nullable @Override + public RegistrationProcessor makeCredential( + Ctap2Session ctap, + PublicKeyCredentialCreationOptions options, + PinUvAuthProtocol pinUvAuthProtocol) { - if (!isSupported(ctap)) { - return null; - } + if (!isSupported(ctap)) { + return null; + } - Extensions extensions = options.getExtensions(); - if (extensions == null) { - return null; - } + Extensions extensions = options.getExtensions(); + if (extensions == null) { + return null; + } - Boolean input = (Boolean) extensions.get(name); - if (input == null) { - return null; - } - return new RegistrationProcessor( - pinToken -> Collections.singletonMap(name, Boolean.TRUE.equals(input))); + Boolean input = (Boolean) extensions.get(name); + if (input == null) { + return null; } + return new RegistrationProcessor( + pinToken -> Collections.singletonMap(name, Boolean.TRUE.equals(input))); + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/package-info.java b/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/package-info.java index dff9d056..6bedd31d 100755 --- a/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/package-info.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/client/extensions/package-info.java @@ -14,10 +14,8 @@ * limitations under the License. */ -/** - * WebAuthn Extensions implementation. - */ +/** WebAuthn Extensions implementation. */ @PackageNonnullByDefault package com.yubico.yubikit.fido.client.extensions; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/fido/src/main/java/com/yubico/yubikit/fido/client/package-info.java b/fido/src/main/java/com/yubico/yubikit/fido/client/package-info.java index cc982f1b..bcdc8465 100755 --- a/fido/src/main/java/com/yubico/yubikit/fido/client/package-info.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/client/package-info.java @@ -16,10 +16,10 @@ /** * WebAuthn client implementation. - *

- * Contains classes for implementing a WebAuthn client which uses a YubiKeySession. + * + *

Contains classes for implementing a WebAuthn client which uses a YubiKeySession. */ @PackageNonnullByDefault package com.yubico.yubikit.fido.client; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/fido/src/main/java/com/yubico/yubikit/fido/ctap/BioEnrollment.java b/fido/src/main/java/com/yubico/yubikit/fido/ctap/BioEnrollment.java index 4f30e18e..e9521045 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/ctap/BioEnrollment.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/ctap/BioEnrollment.java @@ -17,7 +17,6 @@ package com.yubico.yubikit.fido.ctap; import com.yubico.yubikit.core.application.CommandException; - import java.io.IOException; import java.util.Map; import java.util.Objects; @@ -25,54 +24,52 @@ /** * Implements Bio enrollment commands. * - * @see authenticatorBioEnrollment + * @see authenticatorBioEnrollment */ public class BioEnrollment { - protected static final int RESULT_MODALITY = 0x01; - protected static final int MODALITY_FINGERPRINT = 0x01; + protected static final int RESULT_MODALITY = 0x01; + protected static final int MODALITY_FINGERPRINT = 0x01; - protected final Ctap2Session ctap; - protected final int modality; + protected final Ctap2Session ctap; + protected final int modality; - public BioEnrollment(Ctap2Session ctap, int modality) throws IOException, CommandException { - if (!isSupported(ctap.getCachedInfo())) { - throw new IllegalStateException("Bio enrollment not supported"); - } + public BioEnrollment(Ctap2Session ctap, int modality) throws IOException, CommandException { + if (!isSupported(ctap.getCachedInfo())) { + throw new IllegalStateException("Bio enrollment not supported"); + } - this.ctap = ctap; - this.modality = getModality(ctap); + this.ctap = ctap; + this.modality = getModality(ctap); - if (this.modality != modality) { - throw new IllegalStateException("Device does not support modality " + modality); - } + if (this.modality != modality) { + throw new IllegalStateException("Device does not support modality " + modality); } + } - public static boolean isSupported(Ctap2Session.InfoData info) { - final Map options = info.getOptions(); - if (options.containsKey("bioEnroll")) { - return true; - } else return info.getVersions().contains("FIDO_2_1_PRE") && - options.containsKey("userVerificationMgmtPreview"); - } + public static boolean isSupported(Ctap2Session.InfoData info) { + final Map options = info.getOptions(); + if (options.containsKey("bioEnroll")) { + return true; + } else + return info.getVersions().contains("FIDO_2_1_PRE") + && options.containsKey("userVerificationMgmtPreview"); + } - /** - * Get the type of modality the authenticator supports. - * - * @param ctap CTAP2 session - * @return The type of modality authenticator supports. For fingerprint, its value is 1. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @see Get bio modality - */ - public static int getModality(Ctap2Session ctap) throws IOException, CommandException { - final Map result = ctap.bioEnrollment( - null, - null, - null, - null, - null, - Boolean.TRUE, - null); - return Objects.requireNonNull((Integer) result.get(RESULT_MODALITY)); - } + /** + * Get the type of modality the authenticator supports. + * + * @param ctap CTAP2 session + * @return The type of modality authenticator supports. For fingerprint, its value is 1. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @see Get + * bio modality + */ + public static int getModality(Ctap2Session ctap) throws IOException, CommandException { + final Map result = + ctap.bioEnrollment(null, null, null, null, null, Boolean.TRUE, null); + return Objects.requireNonNull((Integer) result.get(RESULT_MODALITY)); + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/ctap/ClientPin.java b/fido/src/main/java/com/yubico/yubikit/fido/ctap/ClientPin.java index 4ba6bb02..6f6bdd9a 100755 --- a/fido/src/main/java/com/yubico/yubikit/fido/ctap/ClientPin.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/ctap/ClientPin.java @@ -20,9 +20,6 @@ import com.yubico.yubikit.core.application.CommandState; import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.util.Pair; - -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.nio.ByteBuffer; import java.nio.CharBuffer; @@ -32,384 +29,358 @@ import java.util.Arrays; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; +import org.slf4j.LoggerFactory; -/** - * Implements Client PIN commands. - */ +/** Implements Client PIN commands. */ public class ClientPin { - private static final byte CMD_GET_RETRIES = 0x01; - private static final byte CMD_GET_KEY_AGREEMENT = 0x02; - private static final byte CMD_SET_PIN = 0x03; - private static final byte CMD_CHANGE_PIN = 0x04; - private static final byte CMD_GET_PIN_TOKEN = 0x05; - private static final byte CMD_GET_PIN_TOKEN_USING_UV_WITH_PERMISSIONS = 0x06; - private static final byte CMD_GET_UV_RETRIES = 0x07; - private static final byte CMD_GET_PIN_TOKEN_USING_PIN_WITH_PERMISSIONS = 0x09; - - private static final int RESULT_KEY_AGREEMENT = 0x01; - private static final int RESULT_PIN_UV_TOKEN = 0x02; - private static final int RESULT_RETRIES = 0x03; - private static final int RESULT_POWER_CYCLE_STATE = 0x04; - private static final int RESULT_UV_RETRIES = 0x05; - - private static final int MIN_PIN_LEN = 4; - private static final int PIN_BUFFER_LEN = 64; - private static final int MAX_PIN_LEN = PIN_BUFFER_LEN - 1; - private static final int PIN_HASH_LEN = 16; - - public static final int PIN_PERMISSION_NONE = 0x00; - public static final int PIN_PERMISSION_MC = 0x01; - public static final int PIN_PERMISSION_GA = 0x02; - public static final int PIN_PERMISSION_CM = 0x04; - public static final int PIN_PERMISSION_BE = 0x08; - public static final int PIN_PERMISSION_LBW = 0x10; - public static final int PIN_PERMISSION_ACFG = 0x20; - - private final Ctap2Session ctap; - private final PinUvAuthProtocol pinUvAuth; - - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(ClientPin.class); - - public static class PinRetries { - final int count; - @Nullable - final Boolean powerCycleState; - - PinRetries(int count, @Nullable Boolean powerCycleState) { - this.count = count; - this.powerCycleState = powerCycleState; - } - - public int getCount() { - return count; - } - - @Nullable - public Boolean getPowerCycleState() { - return powerCycleState; - } + private static final byte CMD_GET_RETRIES = 0x01; + private static final byte CMD_GET_KEY_AGREEMENT = 0x02; + private static final byte CMD_SET_PIN = 0x03; + private static final byte CMD_CHANGE_PIN = 0x04; + private static final byte CMD_GET_PIN_TOKEN = 0x05; + private static final byte CMD_GET_PIN_TOKEN_USING_UV_WITH_PERMISSIONS = 0x06; + private static final byte CMD_GET_UV_RETRIES = 0x07; + private static final byte CMD_GET_PIN_TOKEN_USING_PIN_WITH_PERMISSIONS = 0x09; + + private static final int RESULT_KEY_AGREEMENT = 0x01; + private static final int RESULT_PIN_UV_TOKEN = 0x02; + private static final int RESULT_RETRIES = 0x03; + private static final int RESULT_POWER_CYCLE_STATE = 0x04; + private static final int RESULT_UV_RETRIES = 0x05; + + private static final int MIN_PIN_LEN = 4; + private static final int PIN_BUFFER_LEN = 64; + private static final int MAX_PIN_LEN = PIN_BUFFER_LEN - 1; + private static final int PIN_HASH_LEN = 16; + + public static final int PIN_PERMISSION_NONE = 0x00; + public static final int PIN_PERMISSION_MC = 0x01; + public static final int PIN_PERMISSION_GA = 0x02; + public static final int PIN_PERMISSION_CM = 0x04; + public static final int PIN_PERMISSION_BE = 0x08; + public static final int PIN_PERMISSION_LBW = 0x10; + public static final int PIN_PERMISSION_ACFG = 0x20; + + private final Ctap2Session ctap; + private final PinUvAuthProtocol pinUvAuth; + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(ClientPin.class); + + public static class PinRetries { + final int count; + @Nullable final Boolean powerCycleState; + + PinRetries(int count, @Nullable Boolean powerCycleState) { + this.count = count; + this.powerCycleState = powerCycleState; } - /** - * Construct a new ClientPin object using a specified PIN/UV Auth protocol. - * - * @param ctap an active CTAP2 connection - * @param pinUvAuth the PIN/UV Auth protocol to use - */ - public ClientPin(Ctap2Session ctap, PinUvAuthProtocol pinUvAuth) { - this.ctap = ctap; - this.pinUvAuth = pinUvAuth; + public int getCount() { + return count; } - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public static boolean isSupported(Ctap2Session.InfoData infoData) { - return infoData.getOptions().containsKey("clientPin"); + @Nullable public Boolean getPowerCycleState() { + return powerCycleState; } - - public static boolean isTokenSupported(Ctap2Session.InfoData infoData) { - return Boolean.TRUE.equals(infoData.getOptions().get("pinUvAuthToken")); + } + + /** + * Construct a new ClientPin object using a specified PIN/UV Auth protocol. + * + * @param ctap an active CTAP2 connection + * @param pinUvAuth the PIN/UV Auth protocol to use + */ + public ClientPin(Ctap2Session ctap, PinUvAuthProtocol pinUvAuth) { + this.ctap = ctap; + this.pinUvAuth = pinUvAuth; + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public static boolean isSupported(Ctap2Session.InfoData infoData) { + return infoData.getOptions().containsKey("clientPin"); + } + + public static boolean isTokenSupported(Ctap2Session.InfoData infoData) { + return Boolean.TRUE.equals(infoData.getOptions().get("pinUvAuthToken")); + } + + public Pair, byte[]> getSharedSecret() throws IOException, CommandException { + Logger.debug(logger, "Getting shared secret"); + Map result = + ctap.clientPin( + pinUvAuth.getVersion(), + CMD_GET_KEY_AGREEMENT, + null, + null, + null, + null, + null, + null, + null); + + @SuppressWarnings("unchecked") + Map peerCoseKey = + Objects.requireNonNull((Map) result.get(RESULT_KEY_AGREEMENT)); + return pinUvAuth.encapsulate(peerCoseKey); + } + + /** + * Get the underlying Pin/UV Auth protocol in use. + * + * @return the PinUvAuthProtocol in use + */ + public PinUvAuthProtocol getPinUvAuth() { + return pinUvAuth; + } + + /** + * Get a pinToken from the YubiKey which can be use to authenticate commands for the given + * session. + * + * @param pin The FIDO PIN set for the YubiKey. + * @param permissions requested permissions + * @param permissionsRpId rpId for token used in permission context + * @return A pinToken valid for the current CTAP2 session. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + */ + public byte[] getPinToken( + char[] pin, @Nullable Integer permissions, @Nullable String permissionsRpId) + throws IOException, CommandException { + + if (!isSupported(ctap.getCachedInfo())) { + throw new IllegalStateException("Not supported"); } - public Pair, byte[]> getSharedSecret() throws IOException, CommandException { - Logger.debug(logger, "Getting shared secret"); - Map result = ctap.clientPin( - pinUvAuth.getVersion(), - CMD_GET_KEY_AGREEMENT, - null, - null, - null, - null, - null, - null, - null - ); - - @SuppressWarnings("unchecked") - Map peerCoseKey = - Objects.requireNonNull((Map) result.get(RESULT_KEY_AGREEMENT)); - return pinUvAuth.encapsulate(peerCoseKey); + Pair, byte[]> pair = getSharedSecret(); + byte[] pinHash = null; + try { + pinHash = + Arrays.copyOf( + MessageDigest.getInstance("SHA-256").digest(preparePin(pin, false)), PIN_HASH_LEN); + byte[] pinHashEnc = pinUvAuth.encrypt(pair.second, pinHash); + + Logger.debug(logger, "Getting PIN token"); + + final boolean tokenSupported = isTokenSupported(ctap.getCachedInfo()); + byte subCommand = + tokenSupported ? CMD_GET_PIN_TOKEN_USING_PIN_WITH_PERMISSIONS : CMD_GET_PIN_TOKEN; + + Map result = + ctap.clientPin( + pinUvAuth.getVersion(), + subCommand, + pair.first, + null, + null, + pinHashEnc, + tokenSupported ? permissions : null, + tokenSupported ? permissionsRpId : null, + null); + + byte[] pinTokenEnc = (byte[]) result.get(RESULT_PIN_UV_TOKEN); + Logger.debug( + logger, + "Got PIN token for permissions: {}, permissions rpID: {}", + permissions != null ? permissions : "none", + permissionsRpId != null ? permissionsRpId : "none"); + return pinUvAuth.decrypt(pair.second, pinTokenEnc); + } catch (NoSuchAlgorithmException e) { + Logger.error(logger, "Failure getting PIN token: ", e); + throw new IllegalStateException(e); + } finally { + if (pinHash != null) { + Arrays.fill(pinHash, (byte) 0); + } } - - /** - * Get the underlying Pin/UV Auth protocol in use. - * - * @return the PinUvAuthProtocol in use - */ - public PinUvAuthProtocol getPinUvAuth() { - return pinUvAuth; + } + + /** + * Get a UV Token from the YubiKey which can be use to authenticate commands for the given + * session. + * + * @param permissions requested permissions + * @param permissionsRpId rpId for token used in permission context + * @param state If needed, the state to provide control over the ongoing operation + * @return A pinToken valid for the current CTAP2 session. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + */ + public byte[] getUvToken( + @Nullable Integer permissions, @Nullable String permissionsRpId, @Nullable CommandState state) + throws IOException, CommandException { + + if (!isTokenSupported(ctap.getCachedInfo())) { + throw new IllegalStateException("Not supported"); } - /** - * Get a pinToken from the YubiKey which can be use to authenticate commands for the given - * session. - * - * @param pin The FIDO PIN set for the YubiKey. - * @param permissions requested permissions - * @param permissionsRpId rpId for token used in permission context - * @return A pinToken valid for the current CTAP2 session. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - */ - public byte[] getPinToken(char[] pin, - @Nullable Integer permissions, - @Nullable String permissionsRpId) - throws IOException, CommandException { - - if (!isSupported(ctap.getCachedInfo())) { - throw new IllegalStateException("Not supported"); - } - - Pair, byte[]> pair = getSharedSecret(); - byte[] pinHash = null; - try { - pinHash = Arrays.copyOf( - MessageDigest.getInstance("SHA-256").digest(preparePin(pin, false)), - PIN_HASH_LEN); - byte[] pinHashEnc = pinUvAuth.encrypt(pair.second, pinHash); - - Logger.debug(logger, "Getting PIN token"); - - final boolean tokenSupported = isTokenSupported(ctap.getCachedInfo()); - byte subCommand = tokenSupported - ? CMD_GET_PIN_TOKEN_USING_PIN_WITH_PERMISSIONS - : CMD_GET_PIN_TOKEN; - - Map result = ctap.clientPin( - pinUvAuth.getVersion(), - subCommand, - pair.first, - null, - null, - pinHashEnc, - tokenSupported ? permissions : null, - tokenSupported ? permissionsRpId : null, - null - ); - - byte[] pinTokenEnc = (byte[]) result.get(RESULT_PIN_UV_TOKEN); - Logger.debug(logger, "Got PIN token for permissions: {}, permissions rpID: {}", - permissions != null ? permissions : "none", - permissionsRpId != null ? permissionsRpId : "none"); - return pinUvAuth.decrypt(pair.second, pinTokenEnc); - } catch (NoSuchAlgorithmException e) { - Logger.error(logger, "Failure getting PIN token: ", e); - throw new IllegalStateException(e); - } finally { - if (pinHash != null) { - Arrays.fill(pinHash, (byte) 0); - } - } - } + Pair, byte[]> pair = getSharedSecret(); - /** - * Get a UV Token from the YubiKey which can be use to authenticate commands for the given - * session. - * - * @param permissions requested permissions - * @param permissionsRpId rpId for token used in permission context - * @param state If needed, the state to provide control over the ongoing operation - * @return A pinToken valid for the current CTAP2 session. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - */ - public byte[] getUvToken(@Nullable Integer permissions, - @Nullable String permissionsRpId, - @Nullable CommandState state) - throws IOException, CommandException { - - if (!isTokenSupported(ctap.getCachedInfo())) { - throw new IllegalStateException("Not supported"); - } - - Pair, byte[]> pair = getSharedSecret(); - - Logger.debug(logger, "Getting UV token"); - - Map result = ctap.clientPin( - pinUvAuth.getVersion(), - CMD_GET_PIN_TOKEN_USING_UV_WITH_PERMISSIONS, - pair.first, - null, - null, - null, - permissions, - permissionsRpId, - state - ); - - byte[] pinTokenEnc = (byte[]) result.get(RESULT_PIN_UV_TOKEN); - - Logger.debug(logger, "Got UV token for permissions: {}, permissions rpID: {}", - permissions != null ? permissions : "none", - permissionsRpId != null ? permissionsRpId : "none"); - - return pinUvAuth.decrypt(pair.second, pinTokenEnc); - } + Logger.debug(logger, "Getting UV token"); - /** - * Get the number of invalid PIN attempts available before the PIN becomes blocked and the power - * cycle state, if available. - * - * @return A pair invalid PIN attempts available before the PIN becomes blocked and the power - * cycle state, if available. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - */ - public PinRetries getPinRetries() throws IOException, CommandException { - Logger.debug(logger, "Getting PIN retries"); - Map result = ctap.clientPin( - pinUvAuth.getVersion(), - CMD_GET_RETRIES, - null, - null, - null, - null, - null, - null, - null - ); - - return new PinRetries( - Objects.requireNonNull((Integer) result.get(RESULT_RETRIES)), - (Boolean) result.get(RESULT_POWER_CYCLE_STATE)); + Map result = + ctap.clientPin( + pinUvAuth.getVersion(), + CMD_GET_PIN_TOKEN_USING_UV_WITH_PERMISSIONS, + pair.first, + null, + null, + null, + permissions, + permissionsRpId, + state); + + byte[] pinTokenEnc = (byte[]) result.get(RESULT_PIN_UV_TOKEN); + + Logger.debug( + logger, + "Got UV token for permissions: {}, permissions rpID: {}", + permissions != null ? permissions : "none", + permissionsRpId != null ? permissionsRpId : "none"); + + return pinUvAuth.decrypt(pair.second, pinTokenEnc); + } + + /** + * Get the number of invalid PIN attempts available before the PIN becomes blocked and the power + * cycle state, if available. + * + * @return A pair invalid PIN attempts available before the PIN becomes blocked and the power + * cycle state, if available. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + */ + public PinRetries getPinRetries() throws IOException, CommandException { + Logger.debug(logger, "Getting PIN retries"); + Map result = + ctap.clientPin( + pinUvAuth.getVersion(), CMD_GET_RETRIES, null, null, null, null, null, null, null); + + return new PinRetries( + Objects.requireNonNull((Integer) result.get(RESULT_RETRIES)), + (Boolean) result.get(RESULT_POWER_CYCLE_STATE)); + } + + /** + * Get the number of UV retries remaining. + * + * @return The number of UV retries remaining. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + */ + public int getUvRetries() throws IOException, CommandException { + Logger.debug(logger, "Getting UV retries"); + Map result = + ctap.clientPin( + pinUvAuth.getVersion(), CMD_GET_UV_RETRIES, null, null, null, null, null, null, null); + + return Objects.requireNonNull((Integer) result.get(RESULT_UV_RETRIES)); + } + + /** + * Set the FIDO PIN on a YubiKey with no PIN currently set. + * + * @param pin The PIN to set + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + */ + public void setPin(char[] pin) throws IOException, CommandException { + + if (!isSupported(ctap.getCachedInfo())) { + throw new IllegalStateException("Not supported"); } - /** - * Get the number of UV retries remaining. - * - * @return The number of UV retries remaining. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - */ - public int getUvRetries() throws IOException, CommandException { - Logger.debug(logger, "Getting UV retries"); - Map result = ctap.clientPin( - pinUvAuth.getVersion(), - CMD_GET_UV_RETRIES, - null, - null, - null, - null, - null, - null, - null - ); - - return Objects.requireNonNull((Integer) result.get(RESULT_UV_RETRIES)); + Pair, byte[]> pair = getSharedSecret(); + + byte[] pinEnc = pinUvAuth.encrypt(pair.second, preparePin(pin, true)); + Logger.debug(logger, "Setting PIN"); + ctap.clientPin( + pinUvAuth.getVersion(), + CMD_SET_PIN, + pair.first, + pinUvAuth.authenticate(pair.second, pinEnc), + pinEnc, + null, + null, + null, + null); + Logger.info(logger, "PIN set"); + } + + /** + * Change the FIDO PIN on a YubiKey. + * + * @param currentPin The currently set PIN + * @param newPin The new PIN to set + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + */ + public void changePin(char[] currentPin, char[] newPin) throws IOException, CommandException { + + if (!isSupported(ctap.getCachedInfo())) { + throw new IllegalStateException("Not supported"); } - /** - * Set the FIDO PIN on a YubiKey with no PIN currently set. - * - * @param pin The PIN to set - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - */ - public void setPin(char[] pin) throws IOException, CommandException { - - if (!isSupported(ctap.getCachedInfo())) { - throw new IllegalStateException("Not supported"); - } - - Pair, byte[]> pair = getSharedSecret(); - - byte[] pinEnc = pinUvAuth.encrypt(pair.second, preparePin(pin, true)); - Logger.debug(logger, "Setting PIN"); - ctap.clientPin( - pinUvAuth.getVersion(), - CMD_SET_PIN, - pair.first, - pinUvAuth.authenticate(pair.second, pinEnc), - pinEnc, - null, - null, - null, - null - ); - Logger.info(logger, "PIN set"); + byte[] newPinBytes = preparePin(newPin, true); + Pair, byte[]> pair = getSharedSecret(); + + byte[] pinHash = null; + try { + pinHash = + Arrays.copyOf( + MessageDigest.getInstance("SHA-256").digest(preparePin(currentPin, false)), + PIN_HASH_LEN); + byte[] pinHashEnc = pinUvAuth.encrypt(pair.second, pinHash); + byte[] newPinEnc = pinUvAuth.encrypt(pair.second, newPinBytes); + + Logger.debug(logger, "Changing PIN"); + + byte[] pinUvAuthParam = + pinUvAuth.authenticate( + pair.second, + ByteBuffer.allocate(newPinEnc.length + pinHashEnc.length) + .put(newPinEnc) + .put(pinHashEnc) + .array()); + ctap.clientPin( + pinUvAuth.getVersion(), + CMD_CHANGE_PIN, + pair.first, + pinUvAuthParam, + newPinEnc, + pinHashEnc, + null, + null, + null); + Logger.info(logger, "PIN changed"); + } catch (NoSuchAlgorithmException e) { + Logger.error(logger, "Failure changing PIN: ", e); + throw new IllegalStateException(e); + } finally { + if (pinHash != null) { + Arrays.fill(pinHash, (byte) 0); + } } + } - /** - * Change the FIDO PIN on a YubiKey. - * - * @param currentPin The currently set PIN - * @param newPin The new PIN to set - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - */ - public void changePin(char[] currentPin, char[] newPin) - throws IOException, CommandException { - - if (!isSupported(ctap.getCachedInfo())) { - throw new IllegalStateException("Not supported"); - } - - byte[] newPinBytes = preparePin(newPin, true); - Pair, byte[]> pair = getSharedSecret(); - - byte[] pinHash = null; - try { - pinHash = Arrays.copyOf( - MessageDigest.getInstance("SHA-256").digest(preparePin(currentPin, false)), - PIN_HASH_LEN - ); - byte[] pinHashEnc = pinUvAuth.encrypt(pair.second, pinHash); - byte[] newPinEnc = pinUvAuth.encrypt(pair.second, newPinBytes); - - Logger.debug(logger, "Changing PIN"); - - byte[] pinUvAuthParam = pinUvAuth.authenticate( - pair.second, - ByteBuffer.allocate(newPinEnc.length + pinHashEnc.length) - .put(newPinEnc) - .put(pinHashEnc).array() - ); - ctap.clientPin( - pinUvAuth.getVersion(), - CMD_CHANGE_PIN, - pair.first, - pinUvAuthParam, - newPinEnc, - pinHashEnc, - null, - null, - null - ); - Logger.info(logger, "PIN changed"); - } catch (NoSuchAlgorithmException e) { - Logger.error(logger, "Failure changing PIN: ", e); - throw new IllegalStateException(e); - } finally { - if (pinHash != null) { - Arrays.fill(pinHash, (byte) 0); - } - } + /** Check PIN length, encode to bytes, and optionally pad. */ + static byte[] preparePin(char[] pin, boolean pad) { + if (pin.length < MIN_PIN_LEN) { + throw new IllegalArgumentException("PIN must be at least " + MIN_PIN_LEN + " characters"); } - - /** - * Check PIN length, encode to bytes, and optionally pad. - */ - static byte[] preparePin(char[] pin, boolean pad) { - if (pin.length < MIN_PIN_LEN) { - throw new IllegalArgumentException( - "PIN must be at least " + MIN_PIN_LEN + " characters"); - } - ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(pin)); - try { - int byteLen = byteBuffer.limit() - byteBuffer.position(); - if (byteLen > MAX_PIN_LEN) { - throw new IllegalArgumentException( - "PIN must be no more than " + MAX_PIN_LEN + " bytes"); - } - byte[] pinBytes = new byte[pad ? PIN_BUFFER_LEN : byteLen]; - System.arraycopy(byteBuffer.array(), byteBuffer.position(), pinBytes, 0, byteLen); - - return pinBytes; - } finally { - Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data - } + ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(pin)); + try { + int byteLen = byteBuffer.limit() - byteBuffer.position(); + if (byteLen > MAX_PIN_LEN) { + throw new IllegalArgumentException("PIN must be no more than " + MAX_PIN_LEN + " bytes"); + } + byte[] pinBytes = new byte[pad ? PIN_BUFFER_LEN : byteLen]; + System.arraycopy(byteBuffer.array(), byteBuffer.position(), pinBytes, 0, byteLen); + + return pinBytes; + } finally { + Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data } - + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/ctap/Config.java b/fido/src/main/java/com/yubico/yubikit/fido/ctap/Config.java index ea300d29..c1f5d581 100755 --- a/fido/src/main/java/com/yubico/yubikit/fido/ctap/Config.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/ctap/Config.java @@ -20,9 +20,6 @@ import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.util.Pair; import com.yubico.yubikit.fido.Cbor; - -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; @@ -30,177 +27,171 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - import javax.annotation.Nullable; +import org.slf4j.LoggerFactory; /** * Implements Config commands. * - * @see authenticatorConfig + * @see authenticatorConfig */ @SuppressWarnings("unused") public class Config { - private static final byte CMD_ENABLE_ENTERPRISE_ATT = 0x01; - private static final byte CMD_TOGGLE_ALWAYS_UV = 0x02; - private static final byte CMD_SET_MIN_PIN_LENGTH = 0x03; - private static final byte CMD_VENDOR_PROTOTYPE = (byte) 0xFF; - - private static final byte PARAM_NEW_MIN_PIN_LENGTH = 0x01; - private static final byte PARAM_MIN_PIN_LENGTH_RPIDS = 0x02; - private static final byte PARAM_FORCE_CHANGE_PIN = 0x03; - - private static final byte PARAM_VENDOR_CMD_ID = 0x01; - - private final Ctap2Session ctap; - @Nullable - private final Pair pinUv; - - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(Config.class); - - /** - * Construct a new Config object using a specified PIN/UV Auth protocol and token. - * - * @param ctap an active CTAP2 connection - * @param pinUvAuth the PIN/UV Auth protocol to use - * @param pinUvToken the PIN/UV token to use - */ - public Config( - Ctap2Session ctap, - @Nullable PinUvAuthProtocol pinUvAuth, - @Nullable byte[] pinUvToken - ) { - - if (!isSupported(ctap.getCachedInfo())) { - throw new IllegalStateException("Not supported"); - } - - this.ctap = ctap; - if (pinUvAuth != null && pinUvToken != null) { - this.pinUv = new Pair<>(pinUvAuth, pinUvToken); - } else { - this.pinUv = null; - } - } - - public static boolean isSupported(Ctap2Session.InfoData infoData) { - return Boolean.TRUE.equals(infoData.getOptions().get("authnrCfg")); + private static final byte CMD_ENABLE_ENTERPRISE_ATT = 0x01; + private static final byte CMD_TOGGLE_ALWAYS_UV = 0x02; + private static final byte CMD_SET_MIN_PIN_LENGTH = 0x03; + private static final byte CMD_VENDOR_PROTOTYPE = (byte) 0xFF; + + private static final byte PARAM_NEW_MIN_PIN_LENGTH = 0x01; + private static final byte PARAM_MIN_PIN_LENGTH_RPIDS = 0x02; + private static final byte PARAM_FORCE_CHANGE_PIN = 0x03; + + private static final byte PARAM_VENDOR_CMD_ID = 0x01; + + private final Ctap2Session ctap; + @Nullable private final Pair pinUv; + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(Config.class); + + /** + * Construct a new Config object using a specified PIN/UV Auth protocol and token. + * + * @param ctap an active CTAP2 connection + * @param pinUvAuth the PIN/UV Auth protocol to use + * @param pinUvToken the PIN/UV token to use + */ + public Config( + Ctap2Session ctap, @Nullable PinUvAuthProtocol pinUvAuth, @Nullable byte[] pinUvToken) { + + if (!isSupported(ctap.getCachedInfo())) { + throw new IllegalStateException("Not supported"); } - public static boolean supportsVendorPrototypeConfigCommands(Ctap2Session.InfoData infoData) { - return infoData.getVendorPrototypeConfigCommands() != null; + this.ctap = ctap; + if (pinUvAuth != null && pinUvToken != null) { + this.pinUv = new Pair<>(pinUvAuth, pinUvToken); + } else { + this.pinUv = null; } - - private Map call( - byte subCommand, - @Nullable Map subCommandParams - ) throws IOException, CommandException { - Integer pinUvProtocol = null; - byte[] pinUvAuthParam = null; - - final byte[] header = new byte[32]; - Arrays.fill(header, (byte) 0xff); - - if (pinUv != null) { - ByteBuffer msg; - if (subCommandParams != null) { - final byte[] enc = Cbor.encode(subCommandParams); - msg = ByteBuffer.allocate(34 + enc.length) - .put(header) - .put((byte) 0x0d) - .put(subCommand) - .put(enc); - } else { - msg = ByteBuffer.allocate(34) - .put(header) - .put((byte) 0x0d) - .put(subCommand); - } - - pinUvProtocol = pinUv.first.getVersion(); - pinUvAuthParam = pinUv.first.authenticate(pinUv.second, msg.array()); - } - - return ctap.config( - subCommand, - subCommandParams, - pinUvProtocol, - pinUvAuthParam - ); + } + + public static boolean isSupported(Ctap2Session.InfoData infoData) { + return Boolean.TRUE.equals(infoData.getOptions().get("authnrCfg")); + } + + public static boolean supportsVendorPrototypeConfigCommands(Ctap2Session.InfoData infoData) { + return infoData.getVendorPrototypeConfigCommands() != null; + } + + private Map call(byte subCommand, @Nullable Map subCommandParams) + throws IOException, CommandException { + Integer pinUvProtocol = null; + byte[] pinUvAuthParam = null; + + final byte[] header = new byte[32]; + Arrays.fill(header, (byte) 0xff); + + if (pinUv != null) { + ByteBuffer msg; + if (subCommandParams != null) { + final byte[] enc = Cbor.encode(subCommandParams); + msg = + ByteBuffer.allocate(34 + enc.length) + .put(header) + .put((byte) 0x0d) + .put(subCommand) + .put(enc); + } else { + msg = ByteBuffer.allocate(34).put(header).put((byte) 0x0d).put(subCommand); + } + + pinUvProtocol = pinUv.first.getVersion(); + pinUvAuthParam = pinUv.first.authenticate(pinUv.second, msg.array()); } - /** - * Enables Enterprise Attestation. - * If already enabled, this command is ignored. - * - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @see Enable Enterprise Attestation - */ - public void enableEnterpriseAttestation() throws IOException, CommandException { - Logger.debug(logger, "Enabling enterprise attestation"); - call(CMD_ENABLE_ENTERPRISE_ATT, null); - Logger.info(logger, "Enterprise attestation enabled"); - } - - /** - * Toggle the alwaysUV setting. - * When true, the Authenticator always requires UV for credential assertion. - * - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @see Toggle Always Require User Verification - */ - public void toggleAlwaysUv() throws IOException, CommandException { - Logger.debug(logger, "Toggling always UV"); - call(CMD_TOGGLE_ALWAYS_UV, null); - Logger.info(logger, "Always UV toggled"); - } - - /** - * Set the minimum PIN length allowed when setting/changing the PIN. - * When true, the Authenticator always requires UV for credential assertion. - * - * @param minPinLength The minimum PIN length the Authenticator should allow. - * @param rpIds A list of RP IDs which should be allowed to get the current - * minimum PIN length. - * @param forceChangePin True if the Authenticator should enforce changing the - * PIN before the next use. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @see Setting a minimum PIN Length - */ - public void setMinPinLength(@Nullable Integer minPinLength, - @Nullable List rpIds, - @Nullable Boolean forceChangePin) throws IOException, CommandException { - Logger.debug(logger, "Setting minimum PIN length"); - Map parameters = new HashMap<>(); - if (minPinLength != null) parameters.put(PARAM_NEW_MIN_PIN_LENGTH, minPinLength); - if (rpIds != null) parameters.put(PARAM_MIN_PIN_LENGTH_RPIDS, rpIds); - if (forceChangePin != null) parameters.put(PARAM_FORCE_CHANGE_PIN, forceChangePin); - - call(CMD_SET_MIN_PIN_LENGTH, parameters.isEmpty() ? null : parameters); - - if (minPinLength != null) Logger.info(logger, "Minimum PIN length set"); - if (rpIds != null) Logger.info(logger, "Minimum PIN length RP ID list set"); - if (forceChangePin != null) Logger.info(logger, "ForcePINChange set"); - } - - /** - * Allows vendors to test authenticator configuration features. - * - * @param vendorCommandId Vendor-assigned command ID. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @see Vendor Prototype Command - */ - public Map vendorPrototype(Integer vendorCommandId) - throws IOException, CommandException { - Logger.debug(logger, "Call vendor prototype command"); - final Map response = call( - CMD_VENDOR_PROTOTYPE, - Collections.singletonMap(PARAM_VENDOR_CMD_ID, vendorCommandId) - ); - Logger.info(logger, "Vendor prototype command executed"); - return response; - } + return ctap.config(subCommand, subCommandParams, pinUvProtocol, pinUvAuthParam); + } + + /** + * Enables Enterprise Attestation. If already enabled, this command is ignored. + * + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @see Enable + * Enterprise Attestation + */ + public void enableEnterpriseAttestation() throws IOException, CommandException { + Logger.debug(logger, "Enabling enterprise attestation"); + call(CMD_ENABLE_ENTERPRISE_ATT, null); + Logger.info(logger, "Enterprise attestation enabled"); + } + + /** + * Toggle the alwaysUV setting. When true, the Authenticator always requires UV for credential + * assertion. + * + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @see Toggle + * Always Require User Verification + */ + public void toggleAlwaysUv() throws IOException, CommandException { + Logger.debug(logger, "Toggling always UV"); + call(CMD_TOGGLE_ALWAYS_UV, null); + Logger.info(logger, "Always UV toggled"); + } + + /** + * Set the minimum PIN length allowed when setting/changing the PIN. When true, the Authenticator + * always requires UV for credential assertion. + * + * @param minPinLength The minimum PIN length the Authenticator should allow. + * @param rpIds A list of RP IDs which should be allowed to get the current minimum PIN length. + * @param forceChangePin True if the Authenticator should enforce changing the PIN before the next + * use. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @see Setting + * a minimum PIN Length + */ + public void setMinPinLength( + @Nullable Integer minPinLength, + @Nullable List rpIds, + @Nullable Boolean forceChangePin) + throws IOException, CommandException { + Logger.debug(logger, "Setting minimum PIN length"); + Map parameters = new HashMap<>(); + if (minPinLength != null) parameters.put(PARAM_NEW_MIN_PIN_LENGTH, minPinLength); + if (rpIds != null) parameters.put(PARAM_MIN_PIN_LENGTH_RPIDS, rpIds); + if (forceChangePin != null) parameters.put(PARAM_FORCE_CHANGE_PIN, forceChangePin); + + call(CMD_SET_MIN_PIN_LENGTH, parameters.isEmpty() ? null : parameters); + + if (minPinLength != null) Logger.info(logger, "Minimum PIN length set"); + if (rpIds != null) Logger.info(logger, "Minimum PIN length RP ID list set"); + if (forceChangePin != null) Logger.info(logger, "ForcePINChange set"); + } + + /** + * Allows vendors to test authenticator configuration features. + * + * @param vendorCommandId Vendor-assigned command ID. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @see Vendor + * Prototype Command + */ + public Map vendorPrototype(Integer vendorCommandId) + throws IOException, CommandException { + Logger.debug(logger, "Call vendor prototype command"); + final Map response = + call(CMD_VENDOR_PROTOTYPE, Collections.singletonMap(PARAM_VENDOR_CMD_ID, vendorCommandId)); + Logger.info(logger, "Vendor prototype command executed"); + return response; + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/ctap/CredentialManagement.java b/fido/src/main/java/com/yubico/yubikit/fido/ctap/CredentialManagement.java index e371f646..0abcabea 100755 --- a/fido/src/main/java/com/yubico/yubikit/fido/ctap/CredentialManagement.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/ctap/CredentialManagement.java @@ -19,7 +19,6 @@ import com.yubico.yubikit.core.application.CommandException; import com.yubico.yubikit.core.fido.CtapException; import com.yubico.yubikit.fido.Cbor; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; @@ -28,312 +27,302 @@ import java.util.List; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; -/** - * Provides Credential management on the CTAP level. - */ +/** Provides Credential management on the CTAP level. */ @SuppressWarnings("unused") public class CredentialManagement { - private static final byte CMD_GET_CREDS_METADATA = 0x01; - private static final byte CMD_ENUMERATE_RPS_BEGIN = 0x02; - private static final byte CMD_ENUMERATE_RPS_NEXT = 0x03; - private static final byte CMD_ENUMERATE_CREDS_BEGIN = 0x04; - private static final byte CMD_ENUMERATE_CREDS_NEXT = 0x05; - private static final byte CMD_DELETE_CREDENTIAL = 0x06; - private static final byte CMD_UPDATE_USER_INFORMATION = 0x07; - - private static final byte PARAM_RP_ID_HASH = 0x01; - private static final byte PARAM_CREDENTIAL_ID = 0x02; - private static final byte PARAM_USER = 0x03; - - private static final int RESULT_EXISTING_CRED_COUNT = 0x01; - private static final int RESULT_MAX_REMAINING_COUNT = 0x02; - private static final int RESULT_RP = 0x03; - private static final int RESULT_RP_ID_HASH = 0x04; - private static final int RESULT_TOTAL_RPS = 0x05; - private static final int RESULT_USER = 0x06; - private static final int RESULT_CREDENTIAL_ID = 0x07; - private static final int RESULT_PUBLIC_KEY = 0x08; - private static final int RESULT_TOTAL_CREDENTIALS = 0x09; - private static final int RESULT_CRED_PROTECT = 0x0A; - private static final int RESULT_LARGE_BLOB_KEY = 0x0B; - - private final Ctap2Session ctap; - private final PinUvAuthProtocol pinUvAuth; - private final byte[] pinUvToken; - - /** - * Construct a new CredentialManagement object. - * - * @param ctap an active CTAP2 connection. - * @param pinUvAuth the PIN/UV Auth protocol to use - * @param pinUvToken a pinUvToken to be used, which must match the protocol and have the proper permissions - */ - public CredentialManagement( - Ctap2Session ctap, - PinUvAuthProtocol pinUvAuth, - byte[] pinUvToken - ) { - if (!isSupported(ctap.getCachedInfo())) { - throw new IllegalStateException("Credential manager not supported"); - } - this.ctap = ctap; - this.pinUvAuth = pinUvAuth; - this.pinUvToken = pinUvToken; + private static final byte CMD_GET_CREDS_METADATA = 0x01; + private static final byte CMD_ENUMERATE_RPS_BEGIN = 0x02; + private static final byte CMD_ENUMERATE_RPS_NEXT = 0x03; + private static final byte CMD_ENUMERATE_CREDS_BEGIN = 0x04; + private static final byte CMD_ENUMERATE_CREDS_NEXT = 0x05; + private static final byte CMD_DELETE_CREDENTIAL = 0x06; + private static final byte CMD_UPDATE_USER_INFORMATION = 0x07; + + private static final byte PARAM_RP_ID_HASH = 0x01; + private static final byte PARAM_CREDENTIAL_ID = 0x02; + private static final byte PARAM_USER = 0x03; + + private static final int RESULT_EXISTING_CRED_COUNT = 0x01; + private static final int RESULT_MAX_REMAINING_COUNT = 0x02; + private static final int RESULT_RP = 0x03; + private static final int RESULT_RP_ID_HASH = 0x04; + private static final int RESULT_TOTAL_RPS = 0x05; + private static final int RESULT_USER = 0x06; + private static final int RESULT_CREDENTIAL_ID = 0x07; + private static final int RESULT_PUBLIC_KEY = 0x08; + private static final int RESULT_TOTAL_CREDENTIALS = 0x09; + private static final int RESULT_CRED_PROTECT = 0x0A; + private static final int RESULT_LARGE_BLOB_KEY = 0x0B; + + private final Ctap2Session ctap; + private final PinUvAuthProtocol pinUvAuth; + private final byte[] pinUvToken; + + /** + * Construct a new CredentialManagement object. + * + * @param ctap an active CTAP2 connection. + * @param pinUvAuth the PIN/UV Auth protocol to use + * @param pinUvToken a pinUvToken to be used, which must match the protocol and have the proper + * permissions + */ + public CredentialManagement(Ctap2Session ctap, PinUvAuthProtocol pinUvAuth, byte[] pinUvToken) { + if (!isSupported(ctap.getCachedInfo())) { + throw new IllegalStateException("Credential manager not supported"); } - - public static boolean isSupported(Ctap2Session.InfoData info) { - return supportsCredMgmt(info) || supportsCredentialMgmtPreview(info); + this.ctap = ctap; + this.pinUvAuth = pinUvAuth; + this.pinUvToken = pinUvToken; + } + + public static boolean isSupported(Ctap2Session.InfoData info) { + return supportsCredMgmt(info) || supportsCredentialMgmtPreview(info); + } + + private static boolean supportsCredMgmt(Ctap2Session.InfoData info) { + return Boolean.TRUE.equals(info.getOptions().get("credMgmt")); + } + + private static boolean supportsCredentialMgmtPreview(Ctap2Session.InfoData info) { + return info.getVersions().contains("FIDO_2_1_PRE") + && Boolean.TRUE.equals(info.getOptions().get("credentialMgmtPreview")); + } + + private Map call( + byte subCommand, @Nullable Map subCommandParams, boolean authenticate) + throws IOException, CommandException { + byte[] pinUvAuthParam = null; + if (authenticate) { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + output.write(subCommand); + if (subCommandParams != null) { + Cbor.encodeTo(output, subCommandParams); + } + pinUvAuthParam = pinUvAuth.authenticate(pinUvToken, output.toByteArray()); } - private static boolean supportsCredMgmt(Ctap2Session.InfoData info) { - return Boolean.TRUE.equals(info.getOptions().get("credMgmt")); + return ctap.credentialManagement( + subCommand, subCommandParams, pinUvAuth.getVersion(), pinUvAuthParam); + } + + /** + * Get the underlying Pin/UV Auth protocol in use. + * + * @return the PinUvAuthProtocol in use + */ + public PinUvAuthProtocol getPinUvAuth() { + return pinUvAuth; + } + + /** + * Read metadata about credential management from the YubiKey. + * + * @return Metadata from the YubiKey. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + */ + public Metadata getMetadata() throws IOException, CommandException { + Map data = call(CMD_GET_CREDS_METADATA, null, true); + return new Metadata( + Objects.requireNonNull((Integer) data.get(RESULT_EXISTING_CRED_COUNT)), + Objects.requireNonNull((Integer) data.get(RESULT_MAX_REMAINING_COUNT))); + } + + /** + * Enumerate which RPs this YubiKey has credentials stored for. + * + * @return A list of RPs. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + */ + public List enumerateRps() throws IOException, CommandException { + List list = new ArrayList<>(); + try { + Map first = call(CMD_ENUMERATE_RPS_BEGIN, null, true); + Integer nRps = (Integer) first.get(RESULT_TOTAL_RPS); + + if (nRps != null && nRps > 0) { + list.add(RpData.fromData(first)); + for (int i = nRps; i > 1; i--) { + list.add(RpData.fromData(call(CMD_ENUMERATE_RPS_NEXT, null, false))); + } + } + } catch (CtapException e) { + if (e.getCtapError() != CtapException.ERR_NO_CREDENTIALS) { + throw e; + } } - private static boolean supportsCredentialMgmtPreview(Ctap2Session.InfoData info) { - return info.getVersions().contains("FIDO_2_1_PRE") && - Boolean.TRUE.equals(info.getOptions().get("credentialMgmtPreview")); + return list; + } + + /** + * Enumerate credentials stored for a particular RP. + * + * @param rpIdHash The SHA-256 hash of an RP ID to enumerate for. + * @return A list of Credentials. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + */ + public List enumerateCredentials(byte[] rpIdHash) + throws IOException, CommandException { + List list = new ArrayList<>(); + try { + Map first = + call( + CMD_ENUMERATE_CREDS_BEGIN, + Collections.singletonMap(PARAM_RP_ID_HASH, rpIdHash), + true); + list.add(CredentialData.fromData(first)); + int nCreds = Objects.requireNonNull((Integer) first.get(RESULT_TOTAL_CREDENTIALS)); + for (int i = nCreds; i > 1; i--) { + list.add(CredentialData.fromData(call(CMD_ENUMERATE_CREDS_NEXT, null, false))); + } + } catch (CtapException e) { + if (e.getCtapError() != CtapException.ERR_NO_CREDENTIALS) { + throw e; + } + } + return list; + } + + /** + * Delete a stored credential. + * + * @param credentialId A Map representing a PublicKeyCredentialDescriptor identifying a credential + * to delete. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + */ + public void deleteCredential(Map credentialId) throws IOException, CommandException { + call(CMD_DELETE_CREDENTIAL, Collections.singletonMap(PARAM_CREDENTIAL_ID, credentialId), true); + } + + /** + * @return true if updating user information is supported + */ + public boolean isUpdateUserInformationSupported() { + return supportsCredMgmt(ctap.getCachedInfo()); + } + + /** + * Update user information associated to a credential. Only supported on authenticators with + * version FIDO_2_1 and greater. + * + * @param credentialId A Map representing a PublicKeyCredentialDescriptor identifying a credential + * to delete. + * @param userEntity A Map representing a PublicKeyCredentialUserEntity containing the updated + * information. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @throws UnsupportedOperationException In case the functionality is not supported. + */ + public void updateUserInformation(Map credentialId, Map userEntity) + throws IOException, CommandException { + + if (!isUpdateUserInformationSupported()) { + throw new UnsupportedOperationException("Update user information not supported"); } - private Map call( - byte subCommand, - @Nullable Map subCommandParams, - boolean authenticate - ) throws IOException, CommandException { - byte[] pinUvAuthParam = null; - if (authenticate) { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - output.write(subCommand); - if (subCommandParams != null) { - Cbor.encodeTo(output, subCommandParams); - } - pinUvAuthParam = pinUvAuth.authenticate(pinUvToken, output.toByteArray()); - } - - return ctap.credentialManagement( - subCommand, - subCommandParams, - pinUvAuth.getVersion(), - pinUvAuthParam - ); + Map parameters = new HashMap<>(); + parameters.put((int) PARAM_CREDENTIAL_ID, credentialId); + parameters.put((int) PARAM_USER, userEntity); + call(CMD_UPDATE_USER_INFORMATION, parameters, true); + } + + /** CTAP2 Credential Management Metadata object. */ + public static class Metadata { + private final int existingResidentCredentialsCount; + private final int maxPossibleRemainingResidentCredentialsCount; + + private Metadata( + int existingResidentCredentialsCount, int maxPossibleRemainingResidentCredentialsCount) { + this.existingResidentCredentialsCount = existingResidentCredentialsCount; + this.maxPossibleRemainingResidentCredentialsCount = + maxPossibleRemainingResidentCredentialsCount; } /** - * Get the underlying Pin/UV Auth protocol in use. + * The total number of resident credentials existing on the authenticator. * - * @return the PinUvAuthProtocol in use + * @return The number of existing resident credentials. */ - public PinUvAuthProtocol getPinUvAuth() { - return pinUvAuth; + public int getExistingResidentCredentialsCount() { + return existingResidentCredentialsCount; } /** - * Read metadata about credential management from the YubiKey. + * The maximum number of possible remaining credentials that can be created on the + * authenticator. Note that this number is an estimate as actual space consumed to create a + * credential depends on various conditions such as which algorithm is picked, user entity + * information etc. * - * @return Metadata from the YubiKey. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. + * @return The maximum number of possible remaining credentials that can be created. */ - public Metadata getMetadata() throws IOException, CommandException { - Map data = call(CMD_GET_CREDS_METADATA, null, true); - return new Metadata( - Objects.requireNonNull((Integer) data.get(RESULT_EXISTING_CRED_COUNT)), - Objects.requireNonNull((Integer) data.get(RESULT_MAX_REMAINING_COUNT)) - ); + public int getMaxPossibleRemainingResidentCredentialsCount() { + return maxPossibleRemainingResidentCredentialsCount; } + } - /** - * Enumerate which RPs this YubiKey has credentials stored for. - * - * @return A list of RPs. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - */ - public List enumerateRps() throws IOException, CommandException { - List list = new ArrayList<>(); - try { - Map first = call(CMD_ENUMERATE_RPS_BEGIN, null, true); - Integer nRps = (Integer) first.get(RESULT_TOTAL_RPS); - - if (nRps != null && nRps > 0) { - list.add(RpData.fromData(first)); - for (int i = nRps; i > 1; i--) { - list.add(RpData.fromData(call(CMD_ENUMERATE_RPS_NEXT, null, false))); - } - } - } catch (CtapException e) { - if (e.getCtapError() != CtapException.ERR_NO_CREDENTIALS) { - throw e; - } - } + /** A data class representing an RP for which one or more credentials may be stored. */ + public static class RpData { + private final Map rp; + private final byte[] rpIdHash; - return list; + private RpData(Map rp, byte[] rpIdHash) { + this.rp = rp; + this.rpIdHash = rpIdHash; } - /** - * Enumerate credentials stored for a particular RP. - * - * @param rpIdHash The SHA-256 hash of an RP ID to enumerate for. - * @return A list of Credentials. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - */ - public List enumerateCredentials(byte[] rpIdHash) throws IOException, CommandException { - List list = new ArrayList<>(); - try { - Map first = call(CMD_ENUMERATE_CREDS_BEGIN, Collections.singletonMap(PARAM_RP_ID_HASH, rpIdHash), true); - list.add(CredentialData.fromData(first)); - int nCreds = Objects.requireNonNull((Integer) first.get(RESULT_TOTAL_CREDENTIALS)); - for (int i = nCreds; i > 1; i--) { - list.add(CredentialData.fromData(call(CMD_ENUMERATE_CREDS_NEXT, null, false))); - } - } catch (CtapException e) { - if (e.getCtapError() != CtapException.ERR_NO_CREDENTIALS) { - throw e; - } - } - return list; + public Map getRp() { + return rp; } - /** - * Delete a stored credential. - * - * @param credentialId A Map representing a PublicKeyCredentialDescriptor identifying a credential to delete. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - */ - public void deleteCredential(Map credentialId) throws IOException, CommandException { - call(CMD_DELETE_CREDENTIAL, Collections.singletonMap(PARAM_CREDENTIAL_ID, credentialId), true); + public byte[] getRpIdHash() { + return rpIdHash; } - /** - * @return true if updating user information is supported - */ - public boolean isUpdateUserInformationSupported() { - return supportsCredMgmt(ctap.getCachedInfo()); + @SuppressWarnings("unchecked") + private static RpData fromData(Map data) { + return new RpData( + Objects.requireNonNull((Map) data.get(RESULT_RP)), + Objects.requireNonNull((byte[]) data.get(RESULT_RP_ID_HASH))); } - - /** - * Update user information associated to a credential. - * Only supported on authenticators with version FIDO_2_1 and greater. - * - * @param credentialId A Map representing a PublicKeyCredentialDescriptor identifying a credential to delete. - * @param userEntity A Map representing a PublicKeyCredentialUserEntity containing the updated information. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @throws UnsupportedOperationException In case the functionality is not supported. - */ - public void updateUserInformation(Map credentialId, Map userEntity) - throws IOException, CommandException { - - if (!isUpdateUserInformationSupported()) { - throw new UnsupportedOperationException("Update user information not supported"); - } - - Map parameters = new HashMap<>(); - parameters.put((int) PARAM_CREDENTIAL_ID, credentialId); - parameters.put((int) PARAM_USER, userEntity); - call(CMD_UPDATE_USER_INFORMATION, parameters, true); + } + + /** A data class representing a stored credential. */ + public static class CredentialData { + private final Map user; + private final Map credentialId; + private final Map publicKey; + + private CredentialData( + Map user, Map credentialId, Map publicKey) { + this.user = user; + this.credentialId = credentialId; + this.publicKey = publicKey; } - /** - * CTAP2 Credential Management Metadata object. - */ - public static class Metadata { - private final int existingResidentCredentialsCount; - private final int maxPossibleRemainingResidentCredentialsCount; - - private Metadata(int existingResidentCredentialsCount, int maxPossibleRemainingResidentCredentialsCount) { - this.existingResidentCredentialsCount = existingResidentCredentialsCount; - this.maxPossibleRemainingResidentCredentialsCount = maxPossibleRemainingResidentCredentialsCount; - } - - /** - * The total number of resident credentials existing on the authenticator. - * - * @return The number of existing resident credentials. - */ - public int getExistingResidentCredentialsCount() { - return existingResidentCredentialsCount; - } - - /** - * The maximum number of possible remaining credentials that can be created on the - * authenticator. Note that this number is an estimate as actual space consumed to create a - * credential depends on various conditions such as which algorithm is picked, user entity - * information etc. - * - * @return The maximum number of possible remaining credentials that can be created. - */ - public int getMaxPossibleRemainingResidentCredentialsCount() { - return maxPossibleRemainingResidentCredentialsCount; - } + public Map getUser() { + return user; } - /** - * A data class representing an RP for which one or more credentials may be stored. - */ - public static class RpData { - private final Map rp; - private final byte[] rpIdHash; - - private RpData(Map rp, byte[] rpIdHash) { - this.rp = rp; - this.rpIdHash = rpIdHash; - } - - public Map getRp() { - return rp; - } - - public byte[] getRpIdHash() { - return rpIdHash; - } - - @SuppressWarnings("unchecked") - private static RpData fromData(Map data) { - return new RpData( - Objects.requireNonNull((Map) data.get(RESULT_RP)), - Objects.requireNonNull((byte[]) data.get(RESULT_RP_ID_HASH)) - ); - } + public Map getCredentialId() { + return credentialId; } - /** - * A data class representing a stored credential. - */ - public static class CredentialData { - private final Map user; - private final Map credentialId; - private final Map publicKey; - - private CredentialData(Map user, Map credentialId, Map publicKey) { - this.user = user; - this.credentialId = credentialId; - this.publicKey = publicKey; - } - - public Map getUser() { - return user; - } - - public Map getCredentialId() { - return credentialId; - } - - public Map getPublicKey() { - return publicKey; - } + public Map getPublicKey() { + return publicKey; + } - @SuppressWarnings("unchecked") - private static CredentialData fromData(Map data) { - return new CredentialData( - Objects.requireNonNull((Map) data.get(RESULT_USER)), - Objects.requireNonNull((Map) data.get(RESULT_CREDENTIAL_ID)), - Objects.requireNonNull((Map) data.get(RESULT_PUBLIC_KEY)) - ); - } + @SuppressWarnings("unchecked") + private static CredentialData fromData(Map data) { + return new CredentialData( + Objects.requireNonNull((Map) data.get(RESULT_USER)), + Objects.requireNonNull((Map) data.get(RESULT_CREDENTIAL_ID)), + Objects.requireNonNull((Map) data.get(RESULT_PUBLIC_KEY))); } + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/ctap/Ctap2Session.java b/fido/src/main/java/com/yubico/yubikit/fido/ctap/Ctap2Session.java index 984dfec3..cb329be9 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/ctap/Ctap2Session.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/ctap/Ctap2Session.java @@ -37,9 +37,6 @@ import com.yubico.yubikit.fido.Cbor; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialDescriptor; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialParameters; - -import org.slf4j.LoggerFactory; - import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; @@ -49,1205 +46,1267 @@ import java.util.List; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; +import org.slf4j.LoggerFactory; /** * Implements CTAP 2.1 * - * @see Client to Authenticator Protocol (CTAP) + * @see Client + * to Authenticator Protocol (CTAP) */ public class Ctap2Session extends ApplicationSession { - private static final byte NFCCTAP_MSG = 0x10; - - private static final byte CMD_MAKE_CREDENTIAL = 0x01; - private static final byte CMD_GET_ASSERTION = 0x02; - private static final byte CMD_GET_INFO = 0x04; - private static final byte CMD_CLIENT_PIN = 0x06; - private static final byte CMD_RESET = 0x07; - private static final byte CMD_GET_NEXT_ASSERTION = 0x08; - private static final byte CMD_BIO_ENROLLMENT = 0x09; - private static final byte CMD_CREDENTIAL_MANAGEMENT = 0x0A; - private static final byte CMD_SELECTION = 0x0B; - private static final byte CMD_LARGE_BLOBS = 0x0C; - private static final byte CMD_CONFIG = 0x0D; - private static final byte CMD_BIO_ENROLLMENT_PRE = 0x40; - private static final byte CMD_CREDENTIAL_MANAGEMENT_PRE = 0x41; - - - private final Version version; - private final Backend backend; - private final InfoData info; - @Nullable - private final Byte credentialManagerCommand; - @Nullable - private final Byte bioEnrollmentCommand; - - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(Ctap2Session.class); + private static final byte NFCCTAP_MSG = 0x10; + + private static final byte CMD_MAKE_CREDENTIAL = 0x01; + private static final byte CMD_GET_ASSERTION = 0x02; + private static final byte CMD_GET_INFO = 0x04; + private static final byte CMD_CLIENT_PIN = 0x06; + private static final byte CMD_RESET = 0x07; + private static final byte CMD_GET_NEXT_ASSERTION = 0x08; + private static final byte CMD_BIO_ENROLLMENT = 0x09; + private static final byte CMD_CREDENTIAL_MANAGEMENT = 0x0A; + private static final byte CMD_SELECTION = 0x0B; + private static final byte CMD_LARGE_BLOBS = 0x0C; + private static final byte CMD_CONFIG = 0x0D; + private static final byte CMD_BIO_ENROLLMENT_PRE = 0x40; + private static final byte CMD_CREDENTIAL_MANAGEMENT_PRE = 0x41; + + private final Version version; + private final Backend backend; + private final InfoData info; + @Nullable private final Byte credentialManagerCommand; + @Nullable private final Byte bioEnrollmentCommand; + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(Ctap2Session.class); + + /** + * Construct a new Ctap2Session for a given YubiKey. + * + * @param device a YubiKeyDevice over NFC or USB + * @param callback a callback to invoke with the session + */ + public static void create( + YubiKeyDevice device, Callback> callback) { + if (device.supportsConnection(FidoConnection.class)) { + device.requestConnection( + FidoConnection.class, + value -> callback.invoke(Result.of(() -> new Ctap2Session(value.getValue())))); + } else if (device.supportsConnection(SmartCardConnection.class)) { + device.requestConnection( + SmartCardConnection.class, + value -> callback.invoke(Result.of(() -> new Ctap2Session(value.getValue())))); + } else { + callback.invoke( + Result.failure( + new ApplicationNotAvailableException( + "Session does not support any compatible connection type"))); + } + } + + public Ctap2Session(SmartCardConnection connection) throws IOException, CommandException { + this(connection, new Version(0, 0, 0)); + } + + public Ctap2Session(SmartCardConnection connection, Version version) + throws IOException, CommandException { + this(version, getSmartCardBackend(connection)); + Logger.debug( + logger, + "Ctap2Session session initialized for connection={}, version={}", + connection.getClass().getSimpleName(), + version); + } + + public Ctap2Session(FidoConnection connection) throws IOException, CommandException { + this(new FidoProtocol(connection)); + Logger.debug( + logger, + "Ctap2Session session initialized for connection={}, version={}", + connection.getClass().getSimpleName(), + version); + } + + private Ctap2Session(Version version, Backend backend) throws IOException, CommandException { + this.version = version; + this.backend = backend; + this.info = getInfo(); + + final Map options = info.getOptions(); + if (Boolean.TRUE.equals(options.get("credMgmt"))) { + this.credentialManagerCommand = CMD_CREDENTIAL_MANAGEMENT; + } else if (info.getVersions().contains("FIDO_2_1_PRE") + && Boolean.TRUE.equals(options.get("credentialMgmtPreview"))) { + this.credentialManagerCommand = CMD_CREDENTIAL_MANAGEMENT_PRE; + } else { + this.credentialManagerCommand = null; + } - /** - * Construct a new Ctap2Session for a given YubiKey. - * - * @param device a YubiKeyDevice over NFC or USB - * @param callback a callback to invoke with the session - */ - public static void create(YubiKeyDevice device, Callback> callback) { - if (device.supportsConnection(FidoConnection.class)) { - device.requestConnection(FidoConnection.class, value -> callback.invoke(Result.of(() -> new Ctap2Session(value.getValue())))); - } else if (device.supportsConnection(SmartCardConnection.class)) { - device.requestConnection(SmartCardConnection.class, value -> callback.invoke(Result.of(() -> new Ctap2Session(value.getValue())))); - } else { - callback.invoke(Result.failure(new ApplicationNotAvailableException("Session does not support any compatible connection type"))); - } + if (options.containsKey("bioEnroll")) { + this.bioEnrollmentCommand = CMD_BIO_ENROLLMENT; + } else if (info.getVersions().contains("FIDO_2_1_PRE") + && options.containsKey("userVerificationMgmtPreview")) { + this.bioEnrollmentCommand = CMD_BIO_ENROLLMENT_PRE; + } else { + this.bioEnrollmentCommand = null; + } + } + + private static Backend getSmartCardBackend(SmartCardConnection connection) + throws IOException, ApplicationNotAvailableException { + final SmartCardProtocol protocol = new SmartCardProtocol(connection); + protocol.select(AppId.FIDO); + return new Backend(protocol) { + byte[] sendCbor(byte[] data, @Nullable CommandState state) + throws IOException, CommandException { + // Cancellation is not implemented for NFC, and most likely not needed. + return delegate.sendAndReceive(new Apdu(0x80, NFCCTAP_MSG, 0x00, 0x00, data)); + } + }; + } + + private Ctap2Session(FidoProtocol protocol) throws IOException, CommandException { + this( + protocol.getVersion(), + new Backend(protocol) { + @Override + byte[] sendCbor(byte[] data, @Nullable CommandState state) throws IOException { + byte CTAPHID_CBOR = (byte) 0x80 | 0x10; + return delegate.sendAndReceive(CTAPHID_CBOR, data, state); + } + }); + } + + /** Packs a list of objects into a 1-indexed map, discarding any null values. */ + private static Map args(Object... params) { + Map argMap = new HashMap<>(); + for (int i = 0; i < params.length; i++) { + if (params[i] != null) { + argMap.put(i + 1, params[i]); + } + } + return argMap; + } + + private Map sendCbor( + byte command, @Nullable Object payload, @Nullable CommandState state) + throws IOException, CommandException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(command); + if (payload != null) { + Cbor.encodeTo(baos, payload); + } + byte[] response = backend.sendCbor(baos.toByteArray(), state); + byte status = response[0]; + if (status != 0x00) { + throw new CtapException(status); + } + if (response.length == 1) { + return Collections.emptyMap(); // Empty response } - public Ctap2Session(SmartCardConnection connection) - throws IOException, CommandException { - this(connection, new Version(0, 0, 0)); + try { + @SuppressWarnings("unchecked") + Map value = (Map) Cbor.decode(response, 1, response.length - 1); + return value != null ? value : Collections.emptyMap(); + } catch (ClassCastException e) { + throw new BadResponseException("Unexpected CBOR data in response"); + } + } + + /** + * This method is invoked by the host to request generation of a new credential in the + * authenticator. + * + * @param clientDataHash a SHA-256 hash of the clientDataJson + * @param rp a Map containing the RpEntity data + * @param user a Map containing the UserEntity data + * @param pubKeyCredParams a List of Maps containing the supported credential algorithms + * @param excludeList a List of Maps of already registered credentials + * @param extensions a Map of CTAP extension inputs + * @param options a Map of CTAP options + * @param pinUvAuthParam a byte array derived from a pinToken + * @param pinUvAuthProtocol the PIN protocol version used for the pinUvAuthParam + * @param enterpriseAttestation an enterprise attestation request + * @param state an optional state object to cancel a request and handle keepalive signals + * @return a new credential + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @see authenticatorMakeCredential + */ + public CredentialData makeCredential( + byte[] clientDataHash, + Map rp, + Map user, + List> pubKeyCredParams, + @Nullable List> excludeList, + @Nullable Map extensions, + @Nullable Map options, + @Nullable byte[] pinUvAuthParam, + @Nullable Integer pinUvAuthProtocol, + @Nullable Integer enterpriseAttestation, + @Nullable CommandState state) + throws IOException, CommandException { + Logger.debug( + logger, + "makeCredential for clientDataHash={},rp={},user={}," + + "pubKeyCredParams={},excludeList={},extensions={},options={}," + + "pinUvAuthParam={},pinUvAuthProtocol={},enterpriseAttestation={},state={}", + clientDataHash, + rp, + user, + pubKeyCredParams, + excludeList, + extensions, + options, + pinUvAuthParam, + pinUvAuthProtocol, + enterpriseAttestation, + state); + + final Map data = + sendCbor( + CMD_MAKE_CREDENTIAL, + args( + clientDataHash, + rp, + user, + pubKeyCredParams, + excludeList, + extensions, + options, + pinUvAuthParam, + pinUvAuthProtocol, + enterpriseAttestation), + state); + + CredentialData credentialData = CredentialData.fromData(data); + Logger.info(logger, "Credential created"); + return credentialData; + } + + /** + * This method is used by a host to request cryptographic proof of user authentication as well as + * user consent to a given transaction, using a previously generated credential that is bound to + * the authenticator and relying party identifier. + * + * @param rpId the RP ID for the request + * @param clientDataHash a SHA-256 hash of the clientDataJson + * @param allowList a List of Maps of already registered credentials + * @param extensions a Map of CTAP extension inputs + * @param options a Map of CTAP options + * @param pinUvAuthParam a byte array derived from a pinToken + * @param pinUvAuthProtocol the PIN protocol version used for the pinUvAuthParam + * @param state used to cancel a request and handle keepalive signals + * @return a List of available assertions + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @see authenticatorGetAssertion + * @see authenticatorGetNextAssertion + */ + public List getAssertions( + String rpId, + byte[] clientDataHash, + @Nullable List> allowList, + @Nullable Map extensions, + @Nullable Map options, + @Nullable byte[] pinUvAuthParam, + @Nullable Integer pinUvAuthProtocol, + @Nullable CommandState state) + throws IOException, CommandException { + Logger.debug( + logger, + "getAssertions for rpId={},clientDataHash={}," + + "allowList={},extensions={},options={},pinUvAuthParam={}," + + "pinUvAuthProtocol={},state={}", + rpId, + clientDataHash, + allowList, + extensions, + options, + pinUvAuthParam, + pinUvAuthProtocol); + + final Map assertion = + sendCbor( + CMD_GET_ASSERTION, + args( + rpId, + clientDataHash, + allowList, + extensions, + options, + pinUvAuthParam, + pinUvAuthProtocol), + state); + List assertions = new ArrayList<>(); + assertions.add(AssertionData.fromData(assertion)); + Integer nCreds = (Integer) assertion.get(AssertionData.RESULT_N_CREDS); + int credentialCount = nCreds != null ? nCreds : 1; + for (int i = credentialCount; i > 1; i--) { + assertions.add( + AssertionData.fromData( + Objects.requireNonNull(sendCbor(CMD_GET_NEXT_ASSERTION, null, null)))); + } + Logger.info(logger, "Authenticator returned {} assertions.", credentialCount); + return assertions; + } + + /** + * Using this method, platforms can request that the authenticator report a list of its supported + * protocol versions and extensions, its AAGUID, and other aspects of its overall capabilities. + * Platforms should use this information to tailor their command parameters choices. + * + * @return an InfoData object with information about the YubiKey + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @see authenticatorGetInfo + */ + public InfoData getInfo() throws IOException, CommandException { + final Map infoData = sendCbor(CMD_GET_INFO, null, null); + final InfoData info = InfoData.fromData(infoData); + Logger.debug(logger, "Ctap2.InfoData: {}", info); + return info; + } + + /** + * This command exists so that plaintext PINs are not sent to the authenticator. + * + * @param pinUvAuthProtocol PIN/UV protocol version chosen by the platform + * @param subCommand the specific action being requested + * @param keyAgreement the platform key-agreement key + * @param pinUvAuthParam the output of calling authenticate(key, message) → signature on some + * context specific to the subcommand + * @param newPinEnc an encrypted PIN + * @param pinHashEnc an encrypted proof-of-knowledge of a PIN + * @param permissions bitfield of permissions + * @param rpId the RP ID to assign as the permissions RP ID + * @return an InfoData object with information about the YubiKey + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @see authenticatorClientPIN + */ + Map clientPin( + @Nullable Integer pinUvAuthProtocol, + int subCommand, + @Nullable Map keyAgreement, + @Nullable byte[] pinUvAuthParam, + @Nullable byte[] newPinEnc, + @Nullable byte[] pinHashEnc, + @Nullable Integer permissions, + @Nullable String rpId, + @Nullable CommandState state) + throws IOException, CommandException { + Logger.debug( + logger, + "clientPin for pinUvAuthProtocol={},subCommand={}," + + "keyAgreement={},pinUvAuthParam={},newPinEnc={},pinHashEnc={}," + + "permissions={},rpId={}", + pinUvAuthProtocol, + subCommand, + keyAgreement, + pinUvAuthParam, + newPinEnc, + pinHashEnc, + permissions, + rpId); + return sendCbor( + CMD_CLIENT_PIN, + args( + pinUvAuthProtocol, + subCommand, + keyAgreement, + pinUvAuthParam, + newPinEnc, + pinHashEnc, + null, + null, + permissions, + rpId), + state); + } + + /** + * Issues a CTAP2 reset, which will delete/invalidate all FIDO credentials. + * + *

NOTE: Over USB this command must be sent within a few seconds of plugging the YubiKey in, + * and it requires touch confirmation. + * + * @param state if needed, the state to provide control over the ongoing operation + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @see authenticatorReset + */ + public void reset(@Nullable CommandState state) throws IOException, CommandException { + sendCbor(CMD_RESET, null, state); + } + + /** + * This command is used by the platform to provision/enumerate/delete bio enrollments in the + * authenticator. + * + * @param modality the user verification modality being requested + * @param subCommand the user verification sub command currently being requested + * @param subCommandParams a map of subCommands parameters + * @param pinUvAuthProtocol PIN/UV protocol version chosen by the platform + * @param pinUvAuthParam first 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken + * @param getModality get the user verification type modality + * @param state an optional state object to cancel a request and handle keepalive signals + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @see authenticatorBioEnrollment + */ + Map bioEnrollment( + @Nullable Integer modality, + @Nullable Integer subCommand, + @Nullable Map subCommandParams, + @Nullable Integer pinUvAuthProtocol, + @Nullable byte[] pinUvAuthParam, + @Nullable Boolean getModality, + @Nullable CommandState state) + throws IOException, CommandException { + if (bioEnrollmentCommand == null) { + throw new IllegalStateException("Bio enrollment not supported"); + } + return sendCbor( + bioEnrollmentCommand, + args( + modality, subCommand, subCommandParams, pinUvAuthProtocol, pinUvAuthParam, getModality), + state); + } + + /** + * This command is used by the platform to manage discoverable credentials on the authenticator. + * + * @param subCommand the subCommand currently being requested + * @param subCommandParams a map of subCommands parameters + * @param pinUvAuthProtocol PIN/UV protocol version chosen by the platform + * @param pinUvAuthParam first 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @see authenticatorCredentialManagement + */ + Map credentialManagement( + int subCommand, + @Nullable Map subCommandParams, + @Nullable Integer pinUvAuthProtocol, + @Nullable byte[] pinUvAuthParam) + throws IOException, CommandException { + if (credentialManagerCommand == null) { + throw new IllegalStateException("Credential manager not supported"); + } + return sendCbor( + credentialManagerCommand, + args(subCommand, subCommandParams, pinUvAuthProtocol, pinUvAuthParam), + null); + } + + /** + * This command allows the platform to let a user select a certain authenticator by asking for + * user presence. + * + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @see authenticatorSelection + */ + public void selection(@Nullable CommandState state) throws IOException, CommandException { + sendCbor(CMD_SELECTION, null, state); + } + + /** + * This command allows a platform to store a larger amount of information associated with a + * credential. + * + * @param offset the byte offset at which to read/write + * @param get the number of bytes requested to read, must not be present if set is present + * @param set a fragment to write, must not be present if get is present + * @param length the total length of a write operation, present if, and only if, set is present + * and offset is zero + * @param pinUvAuthParam first 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken + * @param pinUvAuthProtocol PIN/UV protocol version chosen by the platform + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @see authenticatorLargeBlobs + */ + public Map largeBlobs( + int offset, + @Nullable Integer get, + @Nullable byte[] set, + @Nullable Integer length, + @Nullable byte[] pinUvAuthParam, + @Nullable Integer pinUvAuthProtocol) + throws IOException, CommandException { + return sendCbor( + CMD_LARGE_BLOBS, + args( + get, + set, + offset, + length, + pinUvAuthParam, + pinUvAuthParam != null ? pinUvAuthProtocol : null), + null); + } + + /** + * This command is used to configure various authenticator features through the use of its + * subcommands. + * + *

Note: Platforms MUST NOT invoke this command unless the authnrCfg option ID is present and + * true in the response to an authenticatorGetInfo command. + * + * @param subCommand the subCommand currently being requested + * @param subCommandParams a map of subCommands parameters + * @param pinUvAuthProtocol PIN/UV protocol version chosen by the platform + * @param pinUvAuthParam first 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication in the protocol layer. + * @see authenticatorConfig + */ + public Map config( + byte subCommand, + @Nullable Map subCommandParams, + @Nullable Integer pinUvAuthProtocol, + @Nullable byte[] pinUvAuthParam) + throws IOException, CommandException { + return sendCbor( + CMD_CONFIG, + args( + subCommand, + subCommandParams, + pinUvAuthParam != null ? pinUvAuthProtocol : null, + pinUvAuthParam), + null); + } + + @Override + public void close() throws IOException { + backend.close(); + } + + @Override + public Version getVersion() { + return version; + } + + public InfoData getCachedInfo() { + return info; + } + + private abstract static class Backend implements Closeable { + protected final T delegate; + + private Backend(T delegate) { + this.delegate = delegate; } - public Ctap2Session(SmartCardConnection connection, Version version) - throws IOException, CommandException { - this(version, getSmartCardBackend(connection)); - Logger.debug(logger, "Ctap2Session session initialized for connection={}, version={}", - connection.getClass().getSimpleName(), - version); + @Override + public void close() throws IOException { + delegate.close(); } - public Ctap2Session(FidoConnection connection) throws IOException, CommandException { - this(new FidoProtocol(connection)); - Logger.debug(logger, "Ctap2Session session initialized for connection={}, version={}", - connection.getClass().getSimpleName(), - version); + abstract byte[] sendCbor(byte[] data, @Nullable CommandState state) + throws IOException, CommandException; + } + + /** + * Data object containing the information readable form a YubiKey using the getInfo command. + * + * @see authenticatorGetInfo + */ + public static class InfoData { + private static final int RESULT_VERSIONS = 0x01; + private static final int RESULT_EXTENSIONS = 0x02; + private static final int RESULT_AAGUID = 0x03; + private static final int RESULT_OPTIONS = 0x04; + private static final int RESULT_MAX_MSG_SIZE = 0x05; + private static final int RESULT_PIN_UV_AUTH_PROTOCOLS = 0x06; + private static final int RESULT_MAX_CREDS_IN_LIST = 0x07; + private static final int RESULT_MAX_CRED_ID_LENGTH = 0x08; + private static final int RESULT_TRANSPORTS = 0x09; + private static final int RESULT_ALGORITHMS = 0x0A; + private static final int RESULT_MAX_SERIALIZED_LARGE_BLOB_ARRAY = 0x0B; + private static final int RESULT_FORCE_PIN_CHANGE = 0x0C; + private static final int RESULT_MIN_PIN_LENGTH = 0x0D; + private static final int RESULT_FIRMWARE_VERSION = 0x0E; + private static final int RESULT_MAX_CRED_BLOB_LENGTH = 0x0F; + private static final int RESULT_MAX_RPID_FOR_SET_MIN_PIN_LENGTH = 0x10; + private static final int RESULT_PREFERRED_PLATFORM_UV_ATTEMPTS = 0x11; + private static final int RESULT_UV_MODALITY = 0x12; + private static final int RESULT_CERTIFICATIONS = 0x13; + private static final int RESULT_REMAINING_DISCOVERABLE_CREDENTIALS = 0x14; + private static final int RESULT_VENDOR_PROTOTYPE_CONFIG_COMMANDS = 0x15; + + private final List versions; + private final List extensions; + private final byte[] aaguid; + private final int maxMsgSize; + private final Map options; + private final List pinUvAuthProtocols; + @Nullable private final Integer maxCredentialCountInList; + @Nullable private final Integer maxCredentialIdLength; + private final List transports; + private final List algorithms; + private final int maxSerializedLargeBlobArray; + private final boolean forcePinChange; + private final int minPinLength; + @Nullable private final Integer firmwareVersion; + private final int maxCredBlobLength; + private final int maxRPIDsForSetMinPinLength; + @Nullable private final Integer preferredPlatformUvAttempts; + private final int uvModality; + private final Map certifications; + @Nullable private final Integer remainingDiscoverableCredentials; + @Nullable private final List vendorPrototypeConfigCommands; + + private InfoData( + List versions, + List extensions, + byte[] aaguid, + Map options, + int maxMsgSize, + List pinUvAuthProtocols, + @Nullable Integer maxCredentialCountInList, + @Nullable Integer maxCredentialIdLength, + List transports, + List algorithms, + int maxSerializedLargeBlobArray, + boolean forcePinChange, + int minPinLength, + @Nullable Integer firmwareVersion, + int maxCredBlobLength, + int maxRPIDsForSetMinPinLength, + @Nullable Integer preferredPlatformUvAttempts, + int uvModality, + Map certifications, + @Nullable Integer remainingDiscoverableCredentials, + @Nullable List vendorPrototypeConfigCommands) { + this.versions = versions; + this.extensions = extensions; + this.aaguid = aaguid; + this.options = options; + this.maxMsgSize = maxMsgSize; + this.pinUvAuthProtocols = pinUvAuthProtocols; + this.maxCredentialCountInList = maxCredentialCountInList; + this.maxCredentialIdLength = maxCredentialIdLength; + this.transports = transports; + this.algorithms = algorithms; + this.maxSerializedLargeBlobArray = maxSerializedLargeBlobArray; + this.forcePinChange = forcePinChange; + this.minPinLength = minPinLength; + this.firmwareVersion = firmwareVersion; + this.maxCredBlobLength = maxCredBlobLength; + this.maxRPIDsForSetMinPinLength = maxRPIDsForSetMinPinLength; + this.preferredPlatformUvAttempts = preferredPlatformUvAttempts; + this.uvModality = uvModality; + this.certifications = certifications; + this.remainingDiscoverableCredentials = remainingDiscoverableCredentials; + this.vendorPrototypeConfigCommands = vendorPrototypeConfigCommands; } - private Ctap2Session(Version version, Backend backend) - throws IOException, CommandException { - this.version = version; - this.backend = backend; - this.info = getInfo(); - - final Map options = info.getOptions(); - if (Boolean.TRUE.equals(options.get("credMgmt"))) { - this.credentialManagerCommand = CMD_CREDENTIAL_MANAGEMENT; - } else if (info.getVersions().contains("FIDO_2_1_PRE") && - Boolean.TRUE.equals(options.get("credentialMgmtPreview"))) { - this.credentialManagerCommand = CMD_CREDENTIAL_MANAGEMENT_PRE; - } else { - this.credentialManagerCommand = null; - } + @SuppressWarnings("unchecked") + private static InfoData fromData(Map data) { + return new InfoData( + (List) data.get(RESULT_VERSIONS), + data.containsKey(RESULT_EXTENSIONS) + ? (List) data.get(RESULT_EXTENSIONS) + : Collections.emptyList(), + (byte[]) data.get(RESULT_AAGUID), + data.containsKey(RESULT_OPTIONS) + ? (Map) data.get(RESULT_OPTIONS) + : Collections.emptyMap(), + data.containsKey(RESULT_MAX_MSG_SIZE) ? (Integer) data.get(RESULT_MAX_MSG_SIZE) : 1024, + data.containsKey(RESULT_PIN_UV_AUTH_PROTOCOLS) + ? (List) data.get(RESULT_PIN_UV_AUTH_PROTOCOLS) + : Collections.emptyList(), + (Integer) data.get(RESULT_MAX_CREDS_IN_LIST), + (Integer) data.get(RESULT_MAX_CRED_ID_LENGTH), + data.containsKey(RESULT_TRANSPORTS) + ? (List) data.get(RESULT_TRANSPORTS) + : Collections.emptyList(), + data.containsKey(RESULT_ALGORITHMS) + ? (List) data.get(RESULT_ALGORITHMS) + : Collections.emptyList(), + data.containsKey(RESULT_MAX_SERIALIZED_LARGE_BLOB_ARRAY) + ? (Integer) data.get(RESULT_MAX_SERIALIZED_LARGE_BLOB_ARRAY) + : 0, + data.containsKey(RESULT_FORCE_PIN_CHANGE) + ? (Boolean) data.get(RESULT_FORCE_PIN_CHANGE) + : false, + data.containsKey(RESULT_MIN_PIN_LENGTH) ? (Integer) data.get(RESULT_MIN_PIN_LENGTH) : 4, + (Integer) data.get(RESULT_FIRMWARE_VERSION), + data.containsKey(RESULT_MAX_CRED_BLOB_LENGTH) + ? (Integer) data.get(RESULT_MAX_CRED_BLOB_LENGTH) + : 0, + data.containsKey(RESULT_MAX_RPID_FOR_SET_MIN_PIN_LENGTH) + ? (Integer) data.get(RESULT_MAX_RPID_FOR_SET_MIN_PIN_LENGTH) + : 0, + (Integer) data.get(RESULT_PREFERRED_PLATFORM_UV_ATTEMPTS), + data.containsKey(RESULT_UV_MODALITY) + ? (Integer) data.get(RESULT_UV_MODALITY) + : UserVerify.NONE.value, + data.containsKey(RESULT_CERTIFICATIONS) + ? (Map) data.get(RESULT_CERTIFICATIONS) + : Collections.emptyMap(), + (Integer) data.get(RESULT_REMAINING_DISCOVERABLE_CREDENTIALS), + (List) data.get(RESULT_VENDOR_PROTOTYPE_CONFIG_COMMANDS)); + } - if (options.containsKey("bioEnroll")) { - this.bioEnrollmentCommand = CMD_BIO_ENROLLMENT; - } else if (info.getVersions().contains("FIDO_2_1_PRE") && - options.containsKey("userVerificationMgmtPreview")) { - this.bioEnrollmentCommand = CMD_BIO_ENROLLMENT_PRE; - } else { - this.bioEnrollmentCommand = null; - } + /** + * List of supported versions. + * + *

Supported versions are: {@code FIDO_2_0}, {@code FIDO_2_1_PRE}, and {@code FIDO_2_1} for + * CTAP2 / FIDO2 / Web Authentication authenticators and {@code U2F_V2} for CTAP1/U2F + * authenticators. + * + * @return list of supported versions + */ + public List getVersions() { + return versions; } - private static Backend getSmartCardBackend(SmartCardConnection connection) - throws IOException, ApplicationNotAvailableException { - final SmartCardProtocol protocol = new SmartCardProtocol(connection); - protocol.select(AppId.FIDO); - return new Backend(protocol) { - byte[] sendCbor(byte[] data, @Nullable CommandState state) - throws IOException, CommandException { - //Cancellation is not implemented for NFC, and most likely not needed. - return delegate.sendAndReceive(new Apdu(0x80, NFCCTAP_MSG, 0x00, 0x00, data)); - } - }; + /** + * List of supported extensions. + * + * @return list of supported extensions + */ + public List getExtensions() { + return extensions; } - private Ctap2Session(FidoProtocol protocol) throws IOException, CommandException { - this(protocol.getVersion(), new Backend(protocol) { - @Override - byte[] sendCbor(byte[] data, @Nullable CommandState state) throws IOException { - byte CTAPHID_CBOR = (byte) 0x80 | 0x10; - return delegate.sendAndReceive(CTAPHID_CBOR, data, state); - } - }); + /** + * Get the claimed AAGUID of the YubiKey. + * + * @return the AAGUID of the YubiKey + */ + public byte[] getAaguid() { + return aaguid; } /** - * Packs a list of objects into a 1-indexed map, discarding any null values. + * Get the options map, which defines which options are supported, and their configuration. + * + * @return a Map of supported options */ - private static Map args(Object... params) { - Map argMap = new HashMap<>(); - for (int i = 0; i < params.length; i++) { - if (params[i] != null) { - argMap.put(i + 1, params[i]); - } - } - return argMap; + public Map getOptions() { + return options; } - private Map sendCbor( - byte command, - @Nullable Object payload, - @Nullable CommandState state - ) throws IOException, CommandException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - baos.write(command); - if (payload != null) { - Cbor.encodeTo(baos, payload); - } - byte[] response = backend.sendCbor(baos.toByteArray(), state); - byte status = response[0]; - if (status != 0x00) { - throw new CtapException(status); - } - if (response.length == 1) { - return Collections.emptyMap(); // Empty response - } + /** + * Get maximum message size supported by the authenticator. + * + * @return maximum message size + */ + public int getMaxMsgSize() { + return maxMsgSize; + } - try { - @SuppressWarnings("unchecked") - Map value = (Map) Cbor.decode(response, 1, response.length - 1); - return value != null ? value : Collections.emptyMap(); - } catch (ClassCastException e) { - throw new BadResponseException("Unexpected CBOR data in response"); - } + /** + * Get a list of the supported PIN/UV Auth protocol versions in order of decreasing + * authenticator preference. + * + * @return a list of supported protocol versions + * @see pinUvAuthProtocols + */ + public List getPinUvAuthProtocols() { + return pinUvAuthProtocols; } /** - * This method is invoked by the host to request generation of a new credential in the + * Get the maximum number of credentials supported in credentialID list at a time by the * authenticator. * - * @param clientDataHash a SHA-256 hash of the clientDataJson - * @param rp a Map containing the RpEntity data - * @param user a Map containing the UserEntity data - * @param pubKeyCredParams a List of Maps containing the supported credential algorithms - * @param excludeList a List of Maps of already registered credentials - * @param extensions a Map of CTAP extension inputs - * @param options a Map of CTAP options - * @param pinUvAuthParam a byte array derived from a pinToken - * @param pinUvAuthProtocol the PIN protocol version used for the pinUvAuthParam - * @param enterpriseAttestation an enterprise attestation request - * @param state an optional state object to cancel a request and handle - * keepalive signals - * @return a new credential - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @see authenticatorMakeCredential + * @return maximum number of credentials */ - public CredentialData makeCredential( - byte[] clientDataHash, - Map rp, - Map user, - List> pubKeyCredParams, - @Nullable List> excludeList, - @Nullable Map extensions, - @Nullable Map options, - @Nullable byte[] pinUvAuthParam, - @Nullable Integer pinUvAuthProtocol, - @Nullable Integer enterpriseAttestation, - @Nullable CommandState state - ) throws IOException, CommandException { - Logger.debug(logger, "makeCredential for clientDataHash={},rp={},user={}," + - "pubKeyCredParams={},excludeList={},extensions={},options={}," + - "pinUvAuthParam={},pinUvAuthProtocol={},enterpriseAttestation={},state={}", - clientDataHash, rp, user, pubKeyCredParams, excludeList, extensions, options, - pinUvAuthParam, pinUvAuthProtocol, enterpriseAttestation, state); - - final Map data = sendCbor(CMD_MAKE_CREDENTIAL, args( - clientDataHash, - rp, - user, - pubKeyCredParams, - excludeList, - extensions, - options, - pinUvAuthParam, - pinUvAuthProtocol, - enterpriseAttestation), - state); - - CredentialData credentialData = CredentialData.fromData(data); - Logger.info(logger, "Credential created"); - return credentialData; + @Nullable public Integer getMaxCredentialCountInList() { + return maxCredentialCountInList; } /** - * This method is used by a host to request cryptographic proof of user authentication as well - * as user consent to a given transaction, using a previously generated credential that is bound - * to the authenticator and relying party identifier. + * Get the maximum Credential ID Length supported by the authenticator. * - * @param rpId the RP ID for the request - * @param clientDataHash a SHA-256 hash of the clientDataJson - * @param allowList a List of Maps of already registered credentials - * @param extensions a Map of CTAP extension inputs - * @param options a Map of CTAP options - * @param pinUvAuthParam a byte array derived from a pinToken - * @param pinUvAuthProtocol the PIN protocol version used for the pinUvAuthParam - * @param state used to cancel a request and handle keepalive signals - * @return a List of available assertions - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @see authenticatorGetAssertion - * @see authenticatorGetNextAssertion + * @return maximum Credential ID length */ - public List getAssertions( - String rpId, - byte[] clientDataHash, - @Nullable List> allowList, - @Nullable Map extensions, - @Nullable Map options, - @Nullable byte[] pinUvAuthParam, - @Nullable Integer pinUvAuthProtocol, - @Nullable CommandState state - ) throws IOException, CommandException { - Logger.debug(logger, "getAssertions for rpId={},clientDataHash={}," + - "allowList={},extensions={},options={},pinUvAuthParam={}," + - "pinUvAuthProtocol={},state={}", - rpId, clientDataHash, allowList, extensions, options, pinUvAuthParam, pinUvAuthProtocol); - - final Map assertion = sendCbor(CMD_GET_ASSERTION, args( - rpId, - clientDataHash, - allowList, - extensions, - options, - pinUvAuthParam, - pinUvAuthProtocol), - state); - List assertions = new ArrayList<>(); - assertions.add(AssertionData.fromData(assertion)); - Integer nCreds = (Integer) assertion.get(AssertionData.RESULT_N_CREDS); - int credentialCount = nCreds != null ? nCreds : 1; - for (int i = credentialCount; i > 1; i--) { - assertions.add(AssertionData.fromData(Objects.requireNonNull(sendCbor(CMD_GET_NEXT_ASSERTION, null, null)))); - } - Logger.info(logger, "Authenticator returned {} assertions.", credentialCount); - return assertions; + @Nullable public Integer getMaxCredentialIdLength() { + return maxCredentialIdLength; } /** - * Using this method, platforms can request that the authenticator report a list of its - * supported protocol versions and extensions, its AAGUID, and other aspects of its overall - * capabilities. Platforms should use this information to tailor their command parameters - * choices. + * Get a list of supported transports. Values are taken from the AuthenticatorTransport enum in + * WebAuthn. * - * @return an InfoData object with information about the YubiKey - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @see authenticatorGetInfo + * @return list of supported transports + * @see AuthenticatorTransport + * enum */ - public InfoData getInfo() throws IOException, CommandException { - final Map infoData = sendCbor(CMD_GET_INFO, null, null); - final InfoData info = InfoData.fromData(infoData); - Logger.debug(logger, "Ctap2.InfoData: {}", info); - return info; + public List getTransports() { + return transports; } /** - * This command exists so that plaintext PINs are not sent to the authenticator. + * Get a list of supported algorithms for credential generation, as specified in WebAuthn. + * + *

Empty return value indicates that the authenticator does not provide this information. * - * @param pinUvAuthProtocol PIN/UV protocol version chosen by the platform - * @param subCommand the specific action being requested - * @param keyAgreement the platform key-agreement key - * @param pinUvAuthParam the output of calling authenticate(key, message) → signature on some - * context specific to the subcommand - * @param newPinEnc an encrypted PIN - * @param pinHashEnc an encrypted proof-of-knowledge of a PIN - * @param permissions bitfield of permissions - * @param rpId the RP ID to assign as the permissions RP ID - * @return an InfoData object with information about the YubiKey - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @see authenticatorClientPIN + * @return list of supported algorithms + * @see COSE + * Algorithms */ - Map clientPin( - @Nullable Integer pinUvAuthProtocol, - int subCommand, - @Nullable Map keyAgreement, - @Nullable byte[] pinUvAuthParam, - @Nullable byte[] newPinEnc, - @Nullable byte[] pinHashEnc, - @Nullable Integer permissions, - @Nullable String rpId, - @Nullable CommandState state - ) throws IOException, CommandException { - Logger.debug(logger, "clientPin for pinUvAuthProtocol={},subCommand={}," + - "keyAgreement={},pinUvAuthParam={},newPinEnc={},pinHashEnc={}," + - "permissions={},rpId={}", pinUvAuthProtocol, subCommand, keyAgreement, - pinUvAuthParam, newPinEnc, pinHashEnc, permissions, rpId); - return sendCbor( - CMD_CLIENT_PIN, args( - pinUvAuthProtocol, - subCommand, - keyAgreement, - pinUvAuthParam, - newPinEnc, - pinHashEnc, - null, - null, - permissions, - rpId - ), state); + public List getAlgorithms() { + return algorithms; } /** - * Issues a CTAP2 reset, which will delete/invalidate all FIDO credentials. - *

- * NOTE: Over USB this command must be sent within a few seconds of plugging the YubiKey in, and - * it requires touch confirmation. + * Get the maximum size, in bytes, of the serialized large-blob array that this authenticator + * can store. * - * @param state if needed, the state to provide control over the ongoing operation - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @see authenticatorReset + * @return maximum size of serialized large-blob array the authenticator can store if {@code + * authenticatorLargeBlobs} command is supported by the authenticator, 0 otherwise + * @see authenticatorLargeBlobs */ - public void reset(@Nullable CommandState state) throws IOException, CommandException { - sendCbor(CMD_RESET, null, state); + public int getMaxSerializedLargeBlobArray() { + return maxSerializedLargeBlobArray; } /** - * This command is used by the platform to provision/enumerate/delete bio enrollments in the - * authenticator. + * Get the requirement whether the authenticator requires PIN Change before use. * - * @param modality the user verification modality being requested - * @param subCommand the user verification sub command currently being requested - * @param subCommandParams a map of subCommands parameters - * @param pinUvAuthProtocol PIN/UV protocol version chosen by the platform - * @param pinUvAuthParam first 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken - * @param getModality get the user verification type modality - * @param state an optional state object to cancel a request and handle - * keepalive signals - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @see authenticatorBioEnrollment + * @return force PIN Change requirement + * @see PIN + * Change */ - Map bioEnrollment( - @Nullable Integer modality, - @Nullable Integer subCommand, - @Nullable Map subCommandParams, - @Nullable Integer pinUvAuthProtocol, - @Nullable byte[] pinUvAuthParam, - @Nullable Boolean getModality, - @Nullable CommandState state - ) throws IOException, CommandException { - if (bioEnrollmentCommand == null) { - throw new IllegalStateException("Bio enrollment not supported"); - } - return sendCbor( - bioEnrollmentCommand, args( - modality, - subCommand, - subCommandParams, - pinUvAuthProtocol, - pinUvAuthParam, - getModality - ), state); + public boolean getForcePinChange() { + return forcePinChange; } /** - * This command is used by the platform to manage discoverable credentials on the - * authenticator. + * The current minimum PIN length, in Unicode code points, the authenticator enforces for + * ClientPIN. + * + *

Only valid if options contain {@code clientPin} meaning that the authenticator supports + * {@code authenticatorClientPin} command. * - * @param subCommand the subCommand currently being requested - * @param subCommandParams a map of subCommands parameters - * @param pinUvAuthProtocol PIN/UV protocol version chosen by the platform - * @param pinUvAuthParam first 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @see authenticatorCredentialManagement + * @return current minimum PIN length + * @see Minimum + * PIN length */ - Map credentialManagement( - int subCommand, - @Nullable Map subCommandParams, - @Nullable Integer pinUvAuthProtocol, - @Nullable byte[] pinUvAuthParam - ) throws IOException, CommandException { - if (credentialManagerCommand == null) { - throw new IllegalStateException("Credential manager not supported"); - } - return sendCbor( - credentialManagerCommand, - args( - subCommand, - subCommandParams, - pinUvAuthProtocol, - pinUvAuthParam), - null); + public int getMinPinLength() { + return minPinLength; } /** - * This command allows the platform to let a user select a certain authenticator by asking for - * user presence. + * Get the firmware version of the authenticator model identified by AAGUID. * - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @see authenticatorSelection + * @return the firmware version */ - public void selection(@Nullable CommandState state) throws IOException, CommandException { - sendCbor(CMD_SELECTION, null, state); + @Nullable Integer getFirmwareVersion() { + return firmwareVersion; } /** - * This command allows a platform to store a larger amount of information associated with a credential. + * Get maximum credBlob length in bytes supported by the authenticator. * - * @param offset the byte offset at which to read/write - * @param get the number of bytes requested to read, must not be present if set is present - * @param set a fragment to write, must not be present if get is present - * @param length the total length of a write operation, present if, and only if, set is present - * and offset is zero - * @param pinUvAuthParam first 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken - * @param pinUvAuthProtocol PIN/UV protocol version chosen by the platform - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @see authenticatorLargeBlobs + * @return maximum credBlob length if the authenticator supports {@code credBlob} extension, 0 + * otherwise + * @see Maximum + * credBlob lenght */ - public Map largeBlobs( - int offset, - @Nullable Integer get, - @Nullable byte[] set, - @Nullable Integer length, - @Nullable byte[] pinUvAuthParam, - @Nullable Integer pinUvAuthProtocol - ) throws IOException, CommandException { - return sendCbor( - CMD_LARGE_BLOBS, - args( - get, - set, - offset, - length, - pinUvAuthParam, - pinUvAuthParam != null ? pinUvAuthProtocol : null), - null); + public int getMaxCredBlobLength() { + return maxCredBlobLength; } /** - * This command is used to configure various authenticator features through the use of its - * subcommands. - *

- * Note: Platforms MUST NOT invoke this command unless the authnrCfg option ID is present and - * true in the response to an authenticatorGetInfo command. + * Get the maximum number of RP IDs that authenticator can set via setMinPINLength subcommand. * - * @param subCommand the subCommand currently being requested - * @param subCommandParams a map of subCommands parameters - * @param pinUvAuthProtocol PIN/UV protocol version chosen by the platform - * @param pinUvAuthParam first 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication in the protocol layer. - * @see authenticatorConfig + *

Only valid if {@code setMinPINLength} option ID is present. + * + * @return the maximum number of RP IDs + * @see Setting + * a minimum PIN Length */ - public Map config( - byte subCommand, - @Nullable Map subCommandParams, - @Nullable Integer pinUvAuthProtocol, - @Nullable byte[] pinUvAuthParam - ) throws IOException, CommandException { - return sendCbor(CMD_CONFIG, args( - subCommand, - subCommandParams, - pinUvAuthParam != null ? pinUvAuthProtocol : null, - pinUvAuthParam), null); + public int getMaxRPIDsForSetMinPinLength() { + return maxRPIDsForSetMinPinLength; } - @Override - public void close() throws IOException { - backend.close(); + /** + * The preferred number of invocations of the getPinUvAuthTokenUsingUvWithPermissions subCommand + * the platform may attempt before falling back to the getPinUvAuthTokenUsingPinWithPermissions + * subCommand or displaying an error. + * + * @return the preferred number of {@code getPinUvAuthTokenUsingUvWithPermissions} invocations + * @see Preferred + * platfrom UV attempts + */ + @Nullable public Integer getPreferredPlatformUvAttempts() { + return preferredPlatformUvAttempts; } - @Override - public Version getVersion() { - return version; + /** + * The user verification modality supported by the authenticator via authenticatorClientPIN's + * getPinUvAuthTokenUsingUvWithPermissions subcommand. + * + * @return the user verification modality + * @see User + * Verification Methods + */ + public int getUvModality() { + return uvModality; } - public InfoData getCachedInfo() { - return info; + /** + * Provides a hint to the platform with additional information about certifications that the + * authenticator has received. + * + * @return certifications in the form key-value pairs with string IDs and integer values + * @see Authenticator + * Certifications + */ + public final Map getCertifications() { + return certifications; } - private static abstract class Backend implements Closeable { - protected final T delegate; - - private Backend(T delegate) { - this.delegate = delegate; - } - - @Override - public void close() throws IOException { - delegate.close(); - } - - abstract byte[] sendCbor(byte[] data, @Nullable CommandState state) throws IOException, CommandException; + /** + * The estimated number of additional discoverable credentials that can be stored. + * + * @return the estimated number of credentials that can be stored + */ + @Nullable public Integer getRemainingDiscoverableCredentials() { + return remainingDiscoverableCredentials; } /** - * Data object containing the information readable form a YubiKey using the getInfo command. + * List of authenticatorConfig vendorCommandId values supported. * - * @see authenticatorGetInfo + * @return list of vendor command id's + * @see Vendor + * prototype config commands */ - public static class InfoData { - private final static int RESULT_VERSIONS = 0x01; - private final static int RESULT_EXTENSIONS = 0x02; - private final static int RESULT_AAGUID = 0x03; - private final static int RESULT_OPTIONS = 0x04; - private final static int RESULT_MAX_MSG_SIZE = 0x05; - private final static int RESULT_PIN_UV_AUTH_PROTOCOLS = 0x06; - private final static int RESULT_MAX_CREDS_IN_LIST = 0x07; - private final static int RESULT_MAX_CRED_ID_LENGTH = 0x08; - private final static int RESULT_TRANSPORTS = 0x09; - private final static int RESULT_ALGORITHMS = 0x0A; - private final static int RESULT_MAX_SERIALIZED_LARGE_BLOB_ARRAY = 0x0B; - private final static int RESULT_FORCE_PIN_CHANGE = 0x0C; - private final static int RESULT_MIN_PIN_LENGTH = 0x0D; - private final static int RESULT_FIRMWARE_VERSION = 0x0E; - private final static int RESULT_MAX_CRED_BLOB_LENGTH = 0x0F; - private final static int RESULT_MAX_RPID_FOR_SET_MIN_PIN_LENGTH = 0x10; - private final static int RESULT_PREFERRED_PLATFORM_UV_ATTEMPTS = 0x11; - private final static int RESULT_UV_MODALITY = 0x12; - private final static int RESULT_CERTIFICATIONS = 0x13; - private final static int RESULT_REMAINING_DISCOVERABLE_CREDENTIALS = 0x14; - private final static int RESULT_VENDOR_PROTOTYPE_CONFIG_COMMANDS = 0x15; - - private final List versions; - private final List extensions; - private final byte[] aaguid; - private final int maxMsgSize; - private final Map options; - private final List pinUvAuthProtocols; - @Nullable - private final Integer maxCredentialCountInList; - @Nullable - private final Integer maxCredentialIdLength; - private final List transports; - private final List algorithms; - private final int maxSerializedLargeBlobArray; - private final boolean forcePinChange; - private final int minPinLength; - @Nullable - private final Integer firmwareVersion; - private final int maxCredBlobLength; - private final int maxRPIDsForSetMinPinLength; - @Nullable - private final Integer preferredPlatformUvAttempts; - private final int uvModality; - private final Map certifications; - @Nullable - private final Integer remainingDiscoverableCredentials; - @Nullable - private final List vendorPrototypeConfigCommands; - - private InfoData( - List versions, - List extensions, - byte[] aaguid, - Map options, - int maxMsgSize, - List pinUvAuthProtocols, - @Nullable Integer maxCredentialCountInList, - @Nullable Integer maxCredentialIdLength, - List transports, - List algorithms, - int maxSerializedLargeBlobArray, - boolean forcePinChange, - int minPinLength, - @Nullable Integer firmwareVersion, - int maxCredBlobLength, - int maxRPIDsForSetMinPinLength, - @Nullable Integer preferredPlatformUvAttempts, - int uvModality, - Map certifications, - @Nullable Integer remainingDiscoverableCredentials, - @Nullable List vendorPrototypeConfigCommands) { - this.versions = versions; - this.extensions = extensions; - this.aaguid = aaguid; - this.options = options; - this.maxMsgSize = maxMsgSize; - this.pinUvAuthProtocols = pinUvAuthProtocols; - this.maxCredentialCountInList = maxCredentialCountInList; - this.maxCredentialIdLength = maxCredentialIdLength; - this.transports = transports; - this.algorithms = algorithms; - this.maxSerializedLargeBlobArray = maxSerializedLargeBlobArray; - this.forcePinChange = forcePinChange; - this.minPinLength = minPinLength; - this.firmwareVersion = firmwareVersion; - this.maxCredBlobLength = maxCredBlobLength; - this.maxRPIDsForSetMinPinLength = maxRPIDsForSetMinPinLength; - this.preferredPlatformUvAttempts = preferredPlatformUvAttempts; - this.uvModality = uvModality; - this.certifications = certifications; - this.remainingDiscoverableCredentials = remainingDiscoverableCredentials; - this.vendorPrototypeConfigCommands = vendorPrototypeConfigCommands; - - } - - @SuppressWarnings("unchecked") - private static InfoData fromData(Map data) { - return new InfoData( - (List) data.get(RESULT_VERSIONS), - data.containsKey(RESULT_EXTENSIONS) - ? (List) data.get(RESULT_EXTENSIONS) - : Collections.emptyList(), - (byte[]) data.get(RESULT_AAGUID), - data.containsKey(RESULT_OPTIONS) - ? (Map) data.get(RESULT_OPTIONS) - : Collections.emptyMap(), - data.containsKey(RESULT_MAX_MSG_SIZE) - ? (Integer) data.get(RESULT_MAX_MSG_SIZE) - : 1024, - data.containsKey(RESULT_PIN_UV_AUTH_PROTOCOLS) - ? (List) data.get(RESULT_PIN_UV_AUTH_PROTOCOLS) - : Collections.emptyList(), - (Integer) data.get(RESULT_MAX_CREDS_IN_LIST), - (Integer) data.get(RESULT_MAX_CRED_ID_LENGTH), - data.containsKey(RESULT_TRANSPORTS) - ? (List) data.get(RESULT_TRANSPORTS) - : Collections.emptyList(), - data.containsKey(RESULT_ALGORITHMS) - ? (List) data.get(RESULT_ALGORITHMS) - : Collections.emptyList(), - data.containsKey(RESULT_MAX_SERIALIZED_LARGE_BLOB_ARRAY) - ? (Integer) data.get(RESULT_MAX_SERIALIZED_LARGE_BLOB_ARRAY) - : 0, - data.containsKey(RESULT_FORCE_PIN_CHANGE) - ? (Boolean) data.get(RESULT_FORCE_PIN_CHANGE) - : false, - data.containsKey(RESULT_MIN_PIN_LENGTH) - ? (Integer) data.get(RESULT_MIN_PIN_LENGTH) - : 4, - (Integer) data.get(RESULT_FIRMWARE_VERSION), - data.containsKey(RESULT_MAX_CRED_BLOB_LENGTH) - ? (Integer) data.get(RESULT_MAX_CRED_BLOB_LENGTH) - : 0, - data.containsKey(RESULT_MAX_RPID_FOR_SET_MIN_PIN_LENGTH) - ? (Integer) data.get(RESULT_MAX_RPID_FOR_SET_MIN_PIN_LENGTH) - : 0, - (Integer) data.get(RESULT_PREFERRED_PLATFORM_UV_ATTEMPTS), - data.containsKey(RESULT_UV_MODALITY) - ? (Integer) data.get(RESULT_UV_MODALITY) - : UserVerify.NONE.value, - data.containsKey(RESULT_CERTIFICATIONS) - ? (Map) data.get(RESULT_CERTIFICATIONS) - : Collections.emptyMap(), - (Integer) data.get(RESULT_REMAINING_DISCOVERABLE_CREDENTIALS), - (List) data.get(RESULT_VENDOR_PROTOTYPE_CONFIG_COMMANDS) - ); - } - - /** - * List of supported versions. - *

- * Supported versions are: {@code FIDO_2_0}, {@code FIDO_2_1_PRE}, and {@code FIDO_2_1} - * for CTAP2 / FIDO2 / Web Authentication authenticators and {@code U2F_V2} for - * CTAP1/U2F authenticators. - * - * @return list of supported versions - */ - public List getVersions() { - return versions; - } - - /** - * List of supported extensions. - * - * @return list of supported extensions - */ - public List getExtensions() { - return extensions; - } - - /** - * Get the claimed AAGUID of the YubiKey. - * - * @return the AAGUID of the YubiKey - */ - public byte[] getAaguid() { - return aaguid; - } - - /** - * Get the options map, which defines which options are supported, and their configuration. - * - * @return a Map of supported options - */ - public Map getOptions() { - return options; - } - - /** - * Get maximum message size supported by the authenticator. - * - * @return maximum message size - */ - public int getMaxMsgSize() { - return maxMsgSize; - } - - /** - * Get a list of the supported PIN/UV Auth protocol versions in order of decreasing - * authenticator preference. - * - * @return a list of supported protocol versions - * @see pinUvAuthProtocols - */ - public List getPinUvAuthProtocols() { - return pinUvAuthProtocols; - } - - /** - * Get the maximum number of credentials supported in credentialID list - * at a time by the authenticator. - * - * @return maximum number of credentials - */ - @Nullable - public Integer getMaxCredentialCountInList() { - return maxCredentialCountInList; - } - - /** - * Get the maximum Credential ID Length supported by the authenticator. - * - * @return maximum Credential ID length - */ - @Nullable - public Integer getMaxCredentialIdLength() { - return maxCredentialIdLength; - } - - /** - * Get a list of supported transports. Values are taken from the AuthenticatorTransport - * enum in WebAuthn. - * - * @return list of supported transports - * @see AuthenticatorTransport enum - */ - public List getTransports() { - return transports; - } - - /** - * Get a list of supported algorithms for credential generation, as specified in WebAuthn. - *

Empty return value indicates that the authenticator does not provide this information. - * - * @return list of supported algorithms - * @see COSE Algorithms - */ - public List getAlgorithms() { - return algorithms; - } - - /** - * Get the maximum size, in bytes, of the serialized large-blob array that this - * authenticator can store. - * - * @return maximum size of serialized large-blob array the authenticator can store if - * {@code authenticatorLargeBlobs} command is supported by the authenticator, 0 otherwise - * @see authenticatorLargeBlobs - */ - public int getMaxSerializedLargeBlobArray() { - return maxSerializedLargeBlobArray; - } - - /** - * Get the requirement whether the authenticator requires PIN Change before use. - * - * @return force PIN Change requirement - * @see PIN Change - */ - public boolean getForcePinChange() { - return forcePinChange; - } - - /** - * The current minimum PIN length, in Unicode code points, the authenticator - * enforces for ClientPIN. - *

- * Only valid if options contain {@code clientPin} meaning that the authenticator supports - * {@code authenticatorClientPin} command. - * - * @return current minimum PIN length - * @see Minimum PIN length - */ - public int getMinPinLength() { - return minPinLength; - } - - /** - * Get the firmware version of the authenticator model identified by AAGUID. - * - * @return the firmware version - */ - @Nullable - Integer getFirmwareVersion() { - return firmwareVersion; - } - - /** - * Get maximum credBlob length in bytes supported by the authenticator. - * - * @return maximum credBlob length if the authenticator supports {@code credBlob} - * extension, 0 otherwise - * @see Maximum credBlob lenght - */ - public int getMaxCredBlobLength() { - return maxCredBlobLength; - } - - /** - * Get the maximum number of RP IDs that authenticator can set via setMinPINLength - * subcommand. - *

- * Only valid if {@code setMinPINLength} option ID is present. - * - * @return the maximum number of RP IDs - * @see Setting a minimum PIN Length - */ - public int getMaxRPIDsForSetMinPinLength() { - return maxRPIDsForSetMinPinLength; - } - - /** - * The preferred number of invocations of the getPinUvAuthTokenUsingUvWithPermissions - * subCommand the platform may attempt before falling back to the - * getPinUvAuthTokenUsingPinWithPermissions subCommand or displaying an error. - * - * @return the preferred number of {@code getPinUvAuthTokenUsingUvWithPermissions} - * invocations - * @see Preferred platfrom UV attempts - */ - @Nullable - public Integer getPreferredPlatformUvAttempts() { - return preferredPlatformUvAttempts; - } - - /** - * The user verification modality supported by the authenticator via - * authenticatorClientPIN's getPinUvAuthTokenUsingUvWithPermissions subcommand. - * - * @return the user verification modality - * @see User Verification Methods - */ - public int getUvModality() { - return uvModality; - } - - /** - * Provides a hint to the platform with additional information about certifications that - * the authenticator has received. - * - * @return certifications in the form key-value pairs with string IDs and integer values - * @see Authenticator Certifications - */ - public final Map getCertifications() { - return certifications; - } - - /** - * The estimated number of additional discoverable credentials that can be stored. - * - * @return the estimated number of credentials that can be stored - */ - @Nullable - public Integer getRemainingDiscoverableCredentials() { - return remainingDiscoverableCredentials; - } + @Nullable public List getVendorPrototypeConfigCommands() { + return vendorPrototypeConfigCommands; + } - /** - * List of authenticatorConfig vendorCommandId values supported. - * - * @return list of vendor command id's - * @see Vendor prototype config commands - */ - @Nullable - public List getVendorPrototypeConfigCommands() { - return vendorPrototypeConfigCommands; - } + @Override + public String toString() { + return "Ctap2Session.InfoData{" + + "versions=" + + versions + + ", extensions=" + + extensions + + ", aaguid=" + + StringUtils.bytesToHex(aaguid) + + ", options=" + + options + + ", maxMsgSize=" + + maxMsgSize + + ", pinUvAuthProtocols=" + + pinUvAuthProtocols + + ", maxCredentialCountInList=" + + maxCredentialCountInList + + ", maxCredentialIdLength=" + + maxCredentialIdLength + + ", transports=" + + transports + + ", algorithms=" + + algorithms + + ", maxSerializedLargeBlobArray=" + + maxSerializedLargeBlobArray + + ", forcePinChange=" + + forcePinChange + + ", minPinLength=" + + minPinLength + + ", firmwareVersion=" + + firmwareVersion + + ", maxCredBlobLength=" + + maxCredBlobLength + + ", maxRPIDsForSetMinPinLength=" + + maxRPIDsForSetMinPinLength + + ", preferredPlatformUvAttempts=" + + preferredPlatformUvAttempts + + ", uvModality=" + + uvModality + + ", certifications=" + + certifications + + ", remainingDiscoverableCredentials=" + + remainingDiscoverableCredentials + + ", vendorPrototypeConfigCommands=" + + vendorPrototypeConfigCommands + + '}'; + } + } + + /** Data class holding the result of makeCredential. */ + public static class CredentialData { + private static final int RESULT_FMT = 0x01; + private static final int RESULT_AUTH_DATA = 0x02; + private static final int RESULT_ATT_STMT = 0x03; + private static final int RESULT_EP_ATT = 0x04; + private static final int RESULT_LARGE_BLOB_KEY = 0x05; + + private final String format; + private final byte[] authenticatorData; + private final Map attestationStatement; + @Nullable private final Boolean enterpriseAttestation; + @Nullable private final byte[] largeBlobKey; + + private CredentialData( + String format, + byte[] authenticatorData, + Map attestationStatement, + @Nullable Boolean enterpriseAttestation, + @Nullable byte[] largeBlobKey) { + this.format = format; + this.authenticatorData = authenticatorData; + this.attestationStatement = attestationStatement; + this.enterpriseAttestation = enterpriseAttestation; + this.largeBlobKey = largeBlobKey; + } - @Override - public String toString() { - return "Ctap2Session.InfoData{" + - "versions=" + versions + - ", extensions=" + extensions + - ", aaguid=" + StringUtils.bytesToHex(aaguid) + - ", options=" + options + - ", maxMsgSize=" + maxMsgSize + - ", pinUvAuthProtocols=" + pinUvAuthProtocols + - ", maxCredentialCountInList=" + maxCredentialCountInList + - ", maxCredentialIdLength=" + maxCredentialIdLength + - ", transports=" + transports + - ", algorithms=" + algorithms + - ", maxSerializedLargeBlobArray=" + maxSerializedLargeBlobArray + - ", forcePinChange=" + forcePinChange + - ", minPinLength=" + minPinLength + - ", firmwareVersion=" + firmwareVersion + - ", maxCredBlobLength=" + maxCredBlobLength + - ", maxRPIDsForSetMinPinLength=" + maxRPIDsForSetMinPinLength + - ", preferredPlatformUvAttempts=" + preferredPlatformUvAttempts + - ", uvModality=" + uvModality + - ", certifications=" + certifications + - ", remainingDiscoverableCredentials=" + remainingDiscoverableCredentials + - ", vendorPrototypeConfigCommands=" + vendorPrototypeConfigCommands + - '}'; - } + @SuppressWarnings("unchecked") + private static CredentialData fromData(Map data) { + return new CredentialData( + Objects.requireNonNull((String) data.get(RESULT_FMT)), + Objects.requireNonNull((byte[]) data.get(RESULT_AUTH_DATA)), + Objects.requireNonNull((Map) data.get(RESULT_ATT_STMT)), + (Boolean) data.get(RESULT_EP_ATT), + (byte[]) data.get(RESULT_LARGE_BLOB_KEY)); } /** - * Data class holding the result of makeCredential. + * The AuthenticatorData object. + * + * @return the AuthenticatorData + * @see authenticator-data */ - public static class CredentialData { - private final static int RESULT_FMT = 0x01; - private final static int RESULT_AUTH_DATA = 0x02; - private final static int RESULT_ATT_STMT = 0x03; - private final static int RESULT_EP_ATT = 0x04; - private final static int RESULT_LARGE_BLOB_KEY = 0x05; - - private final String format; - private final byte[] authenticatorData; - private final Map attestationStatement; - @Nullable - private final Boolean enterpriseAttestation; - @Nullable - private final byte[] largeBlobKey; - - private CredentialData( - String format, - byte[] authenticatorData, - Map attestationStatement, - @Nullable Boolean enterpriseAttestation, - @Nullable byte[] largeBlobKey - ) { - this.format = format; - this.authenticatorData = authenticatorData; - this.attestationStatement = attestationStatement; - this.enterpriseAttestation = enterpriseAttestation; - this.largeBlobKey = largeBlobKey; - } - - @SuppressWarnings("unchecked") - private static CredentialData fromData(Map data) { - return new CredentialData( - Objects.requireNonNull((String) data.get(RESULT_FMT)), - Objects.requireNonNull((byte[]) data.get(RESULT_AUTH_DATA)), - Objects.requireNonNull((Map) data.get(RESULT_ATT_STMT)), - (Boolean) data.get(RESULT_EP_ATT), - (byte[]) data.get(RESULT_LARGE_BLOB_KEY) - ); - } - - /** - * The AuthenticatorData object. - * - * @return the AuthenticatorData - * @see authenticator-data - */ - public byte[] getAuthenticatorData() { - return authenticatorData; - } - - /** - * The attestation statement format identifier. - * - * @return the format of the attestation - */ - public String getFormat() { - return format; - } + public byte[] getAuthenticatorData() { + return authenticatorData; + } - /** - * The attestation statement, whose format is identified by the "fmt" object member. - * - * @return the attestation statement - */ - public Map getAttestationStatement() { - return attestationStatement; - } + /** + * The attestation statement format identifier. + * + * @return the format of the attestation + */ + public String getFormat() { + return format; + } - /** - * Indicates whether an enterprise attestation was returned for this credential. - * - * @return null or false if enterprise attestation was not returned, otherwise true - */ - @Nullable - public Boolean getEnterpriseAttestation() { - return enterpriseAttestation; - } + /** + * The attestation statement, whose format is identified by the "fmt" object member. + * + * @return the attestation statement + */ + public Map getAttestationStatement() { + return attestationStatement; + } - /** - * The largeBlobKey for the credential, if requested with the largeBlobKey extension. - * - * @return the largeBlobKey for the credential - */ - @Nullable - public byte[] getLargeBlobKey() { - return largeBlobKey; - } + /** + * Indicates whether an enterprise attestation was returned for this credential. + * + * @return null or false if enterprise attestation was not returned, otherwise true + */ + @Nullable public Boolean getEnterpriseAttestation() { + return enterpriseAttestation; } /** - * Data class holding the result of getAssertion. + * The largeBlobKey for the credential, if requested with the largeBlobKey extension. * - * @see authenticatorGetAssertion response structure. + * @return the largeBlobKey for the credential */ - public static class AssertionData { - private final static int RESULT_CREDENTIAL = 1; - private final static int RESULT_AUTH_DATA = 2; - private final static int RESULT_SIGNATURE = 3; - private final static int RESULT_USER = 4; - private final static int RESULT_N_CREDS = 5; - private final static int RESULT_USER_SELECTED = 6; - private final static int RESULT_LARGE_BLOB_KEY = 7; - - @Nullable - private final Map credential; - private final byte[] authenticatorData; - private final byte[] signature; - @Nullable - private final Map user; - @Nullable - private final Integer numberOfCredentials; - @Nullable - private final Boolean userSelected; - @Nullable - private final byte[] largeBlobKey; - - private AssertionData( - @Nullable Map credential, - byte[] authenticatorData, - byte[] signature, - @Nullable Map user, - @Nullable Integer numberOfCredentials, - @Nullable Boolean userSelected, - @Nullable byte[] largeBlobKey) { - this.credential = credential; - this.user = user; - this.signature = signature; - this.authenticatorData = authenticatorData; - this.numberOfCredentials = numberOfCredentials; - this.userSelected = userSelected; - this.largeBlobKey = largeBlobKey; - } + @Nullable public byte[] getLargeBlobKey() { + return largeBlobKey; + } + } + + /** + * Data class holding the result of getAssertion. + * + * @see authenticatorGetAssertion + * response structure. + */ + public static class AssertionData { + private static final int RESULT_CREDENTIAL = 1; + private static final int RESULT_AUTH_DATA = 2; + private static final int RESULT_SIGNATURE = 3; + private static final int RESULT_USER = 4; + private static final int RESULT_N_CREDS = 5; + private static final int RESULT_USER_SELECTED = 6; + private static final int RESULT_LARGE_BLOB_KEY = 7; + + @Nullable private final Map credential; + private final byte[] authenticatorData; + private final byte[] signature; + @Nullable private final Map user; + @Nullable private final Integer numberOfCredentials; + @Nullable private final Boolean userSelected; + @Nullable private final byte[] largeBlobKey; + + private AssertionData( + @Nullable Map credential, + byte[] authenticatorData, + byte[] signature, + @Nullable Map user, + @Nullable Integer numberOfCredentials, + @Nullable Boolean userSelected, + @Nullable byte[] largeBlobKey) { + this.credential = credential; + this.user = user; + this.signature = signature; + this.authenticatorData = authenticatorData; + this.numberOfCredentials = numberOfCredentials; + this.userSelected = userSelected; + this.largeBlobKey = largeBlobKey; + } - @SuppressWarnings("unchecked") - private static AssertionData fromData(Map data) { - return new AssertionData( - (Map) data.get(RESULT_CREDENTIAL), - Objects.requireNonNull((byte[]) data.get(RESULT_AUTH_DATA)), - Objects.requireNonNull((byte[]) data.get(RESULT_SIGNATURE)), - (Map) data.get(RESULT_USER), - (Integer) data.get(RESULT_N_CREDS), - (Boolean) data.get(RESULT_USER_SELECTED), - (byte[]) data.get(RESULT_LARGE_BLOB_KEY) - ); - } + @SuppressWarnings("unchecked") + private static AssertionData fromData(Map data) { + return new AssertionData( + (Map) data.get(RESULT_CREDENTIAL), + Objects.requireNonNull((byte[]) data.get(RESULT_AUTH_DATA)), + Objects.requireNonNull((byte[]) data.get(RESULT_SIGNATURE)), + (Map) data.get(RESULT_USER), + (Integer) data.get(RESULT_N_CREDS), + (Boolean) data.get(RESULT_USER_SELECTED), + (byte[]) data.get(RESULT_LARGE_BLOB_KEY)); + } - /** - * The user structure containing account information. - * - * @return the user structure for the assertion - */ - @Nullable - public Map getUser() { - return user; - } + /** + * The user structure containing account information. + * + * @return the user structure for the assertion + */ + @Nullable public Map getUser() { + return user; + } - /** - * The credential identifier whose private key was used to generate the assertion. - * - * @return the credential descriptor for the assertion - */ - @Nullable - public Map getCredential() { - return credential; - } + /** + * The credential identifier whose private key was used to generate the assertion. + * + * @return the credential descriptor for the assertion + */ + @Nullable public Map getCredential() { + return credential; + } - /** - * The assertion signature produced by the authenticator - * - * @return the signature for the assertion - */ - public byte[] getSignature() { - return signature; - } + /** + * The assertion signature produced by the authenticator + * + * @return the signature for the assertion + */ + public byte[] getSignature() { + return signature; + } - /** - * The AuthenticatorData object. - * - * @return the AuthenticatorData - * @see authenticator-data - */ - public byte[] getAuthenticatorData() { - return authenticatorData; - } + /** + * The AuthenticatorData object. + * + * @return the AuthenticatorData + * @see authenticator-data + */ + public byte[] getAuthenticatorData() { + return authenticatorData; + } - /** - * Total number of account credentials for the RP. Optional; defaults to one. - * This member is required when more than one credential is found for an RP, and - * the authenticator does not have a display or the UV/UP flags are false. - *

- * Omitted when returned for the authenticatorGetNextAssertion method. - * - * @return Total number of account credentials for the RP. - * @see authenticatorGetAssertion response structure. - */ - @Nullable - public Integer getNumberOfCredentials() { - return numberOfCredentials; - } + /** + * Total number of account credentials for the RP. Optional; defaults to one. This member is + * required when more than one credential is found for an RP, and the authenticator does not + * have a display or the UV/UP flags are false. + * + *

Omitted when returned for the authenticatorGetNextAssertion method. + * + * @return Total number of account credentials for the RP. + * @see authenticatorGetAssertion + * response structure. + */ + @Nullable public Integer getNumberOfCredentials() { + return numberOfCredentials; + } - /** - * Indicates that a credential was selected by the user via interaction directly with - * the authenticator, and thus the platform does not need to confirm the credential. - *

- * Optional; defaults to false. - *

- * MUST NOT be present in response to a request where an allowList was given, - * where numberOfCredentials is greater than one, nor in response to - * an authenticatorGetNextAssertion request. - * - * @return True if the credential was selected by the user via interaction directly with - * the authenticator. - * @see authenticatorGetAssertion response structure. - */ - @Nullable - public Boolean getUserSelected() { - return userSelected; - } + /** + * Indicates that a credential was selected by the user via interaction directly with the + * authenticator, and thus the platform does not need to confirm the credential. + * + *

Optional; defaults to false. + * + *

MUST NOT be present in response to a request where an allowList was given, where + * numberOfCredentials is greater than one, nor in response to an authenticatorGetNextAssertion + * request. + * + * @return True if the credential was selected by the user via interaction directly with the + * authenticator. + * @see authenticatorGetAssertion + * response structure. + */ + @Nullable public Boolean getUserSelected() { + return userSelected; + } - /** - * The contents of the associated largeBlobKey if present for the asserted credential, - * and if largeBlobKey was true in the extensions input. - * - * @return The contents of the associated largeBlobKey. - * @see authenticatorGetAssertion response structure. - * @see Large Blob Key Extension. - */ - @Nullable - public byte[] getLargeBlobKey() { - return largeBlobKey; - } + /** + * The contents of the associated largeBlobKey if present for the asserted credential, and if + * largeBlobKey was true in the extensions input. + * + * @return The contents of the associated largeBlobKey. + * @see authenticatorGetAssertion + * response structure. + * @see Large + * Blob Key Extension. + */ + @Nullable public byte[] getLargeBlobKey() { + return largeBlobKey; + } - /** - * Helper function for obtaining credential id for AssertionData with help of allowCredentials. - * - * @param allowCredentials list of allowed credentials which might help to get correct - * credential id - * @return credentialId for assertion - * @throws RuntimeException if credential id could not be computed - */ - public byte[] getCredentialId( - @Nullable List allowCredentials - ) { - byte[] credentialId; - Map credentialMap = getCredential(); - if (credentialMap != null) { - credentialId = Objects.requireNonNull((byte[]) credentialMap.get(PublicKeyCredentialDescriptor.ID)); - } else { - // Credential is optional if allowList contains exactly one credential. - if (allowCredentials == null || allowCredentials.size() != 1) { - throw new RuntimeException("Expecting exactly one valid credential in allowCredentials"); - } - credentialId = allowCredentials.get(0).getId(); - } - return credentialId; + /** + * Helper function for obtaining credential id for AssertionData with help of allowCredentials. + * + * @param allowCredentials list of allowed credentials which might help to get correct + * credential id + * @return credentialId for assertion + * @throws RuntimeException if credential id could not be computed + */ + public byte[] getCredentialId(@Nullable List allowCredentials) { + byte[] credentialId; + Map credentialMap = getCredential(); + if (credentialMap != null) { + credentialId = + Objects.requireNonNull((byte[]) credentialMap.get(PublicKeyCredentialDescriptor.ID)); + } else { + // Credential is optional if allowList contains exactly one credential. + if (allowCredentials == null || allowCredentials.size() != 1) { + throw new RuntimeException("Expecting exactly one valid credential in allowCredentials"); } + credentialId = allowCredentials.get(0).getId(); + } + return credentialId; } -} \ No newline at end of file + } +} diff --git a/fido/src/main/java/com/yubico/yubikit/fido/ctap/FingerprintBioEnrollment.java b/fido/src/main/java/com/yubico/yubikit/fido/ctap/FingerprintBioEnrollment.java index 9e20339f..48823fb4 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/ctap/FingerprintBioEnrollment.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/ctap/FingerprintBioEnrollment.java @@ -22,9 +22,6 @@ import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.internal.codec.Base64; import com.yubico.yubikit.fido.Cbor; - -import org.slf4j.LoggerFactory; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Collections; @@ -32,456 +29,449 @@ import java.util.List; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; +import org.slf4j.LoggerFactory; /** * Implements Fingerprint Bio Enrollment commands. * - * @see authenticatorConfig + * @see authenticatorConfig */ public class FingerprintBioEnrollment extends BioEnrollment { - private static final int CMD_ENROLL_BEGIN = 0x01; - private static final int CMD_ENROLL_CAPTURE_NEXT = 0x02; - private static final int CMD_ENROLL_CANCEL = 0x03; - private static final int CMD_ENUMERATE_ENROLLMENTS = 0x04; - private static final int CMD_SET_NAME = 0x05; - private static final int CMD_REMOVE_ENROLLMENT = 0x06; - private static final int CMD_GET_SENSOR_INFO = 0x07; - - private static final int RESULT_FINGERPRINT_KIND = 0x02; - private static final int RESULT_MAX_SAMPLES_REQUIRED = 0x03; - private static final int RESULT_TEMPLATE_ID = 0x04; - private static final int RESULT_LAST_SAMPLE_STATUS = 0x05; - private static final int RESULT_REMAINING_SAMPLES = 0x06; - private static final int RESULT_TEMPLATE_INFOS = 0x07; - private static final int RESULT_MAX_TEMPLATE_FRIENDLY_NAME = 0x08; - - protected static final int TEMPLATE_INFO_ID = 0x01; - protected static final int TEMPLATE_INFO_FRIENDLY_NAME = 0x02; - - private static final int PARAM_TEMPLATE_ID = 0x01; - private static final int PARAM_TEMPLATE_FRIENDLY_NAME = 0x02; - private static final int PARAM_TIMEOUT_MS = 0x03; - - public static final int FEEDBACK_FP_GOOD = 0x00; - public static final int FEEDBACK_FP_TOO_HIGH = 0x01; - public static final int FEEDBACK_FP_TOO_LOW = 0x02; - public static final int FEEDBACK_FP_TOO_LEFT = 0x03; - public static final int FEEDBACK_FP_TOO_RIGHT = 0x04; - public static final int FEEDBACK_FP_TOO_FAST = 0x05; - public static final int FEEDBACK_FP_TOO_SLOW = 0x06; - public static final int FEEDBACK_FP_POOR_QUALITY = 0x07; - public static final int FEEDBACK_FP_TOO_SKEWED = 0x08; - public static final int FEEDBACK_FP_TOO_SHORT = 0x09; - public static final int FEEDBACK_FP_MERGE_FAILURE = 0x0A; - public static final int FEEDBACK_FP_EXISTS = 0x0B; - // 0x0C not used - public static final int FEEDBACK_NO_USER_ACTIVITY = 0x0D; - public static final int FEEDBACK_NO_UP_TRANSITION = 0x0E; - - private final PinUvAuthProtocol pinUvAuth; - private final byte[] pinUvToken; - - private final org.slf4j.Logger logger = LoggerFactory.getLogger(FingerprintBioEnrollment.class); - - public static class SensorInfo { - public final int fingerprintKind; - public final int maxCaptureSamplesRequiredForEnroll; - public final int maxTemplateFriendlyName; - - public SensorInfo(int fingerprintKind, int maxCaptureSamplesRequiredForEnroll, int maxTemplateFriendlyName) { - this.fingerprintKind = fingerprintKind; - this.maxCaptureSamplesRequiredForEnroll = maxCaptureSamplesRequiredForEnroll; - this.maxTemplateFriendlyName = maxTemplateFriendlyName; - } - - /** - * Indicates type of fingerprint sensor. - * - * @return For touch type fingerprints returns 1, for swipe type fingerprints returns 2. - */ - public int getFingerprintKind() { - return fingerprintKind; - } - - /** - * Indicates the maximum good samples required for enrollment. - * - * @return Maximum good samples required for enrollment. - */ - public int getMaxCaptureSamplesRequiredForEnroll() { - return maxCaptureSamplesRequiredForEnroll; - } - - /** - * Indicates the maximum number of bytes the authenticator will accept as a templateFriendlyName. - * - * @return Maximum number of bytes the authenticator will accept as a templateFriendlyName. - */ - public int getMaxTemplateFriendlyName() { - return maxTemplateFriendlyName; - } + private static final int CMD_ENROLL_BEGIN = 0x01; + private static final int CMD_ENROLL_CAPTURE_NEXT = 0x02; + private static final int CMD_ENROLL_CANCEL = 0x03; + private static final int CMD_ENUMERATE_ENROLLMENTS = 0x04; + private static final int CMD_SET_NAME = 0x05; + private static final int CMD_REMOVE_ENROLLMENT = 0x06; + private static final int CMD_GET_SENSOR_INFO = 0x07; + + private static final int RESULT_FINGERPRINT_KIND = 0x02; + private static final int RESULT_MAX_SAMPLES_REQUIRED = 0x03; + private static final int RESULT_TEMPLATE_ID = 0x04; + private static final int RESULT_LAST_SAMPLE_STATUS = 0x05; + private static final int RESULT_REMAINING_SAMPLES = 0x06; + private static final int RESULT_TEMPLATE_INFOS = 0x07; + private static final int RESULT_MAX_TEMPLATE_FRIENDLY_NAME = 0x08; + + protected static final int TEMPLATE_INFO_ID = 0x01; + protected static final int TEMPLATE_INFO_FRIENDLY_NAME = 0x02; + + private static final int PARAM_TEMPLATE_ID = 0x01; + private static final int PARAM_TEMPLATE_FRIENDLY_NAME = 0x02; + private static final int PARAM_TIMEOUT_MS = 0x03; + + public static final int FEEDBACK_FP_GOOD = 0x00; + public static final int FEEDBACK_FP_TOO_HIGH = 0x01; + public static final int FEEDBACK_FP_TOO_LOW = 0x02; + public static final int FEEDBACK_FP_TOO_LEFT = 0x03; + public static final int FEEDBACK_FP_TOO_RIGHT = 0x04; + public static final int FEEDBACK_FP_TOO_FAST = 0x05; + public static final int FEEDBACK_FP_TOO_SLOW = 0x06; + public static final int FEEDBACK_FP_POOR_QUALITY = 0x07; + public static final int FEEDBACK_FP_TOO_SKEWED = 0x08; + public static final int FEEDBACK_FP_TOO_SHORT = 0x09; + public static final int FEEDBACK_FP_MERGE_FAILURE = 0x0A; + public static final int FEEDBACK_FP_EXISTS = 0x0B; + // 0x0C not used + public static final int FEEDBACK_NO_USER_ACTIVITY = 0x0D; + public static final int FEEDBACK_NO_UP_TRANSITION = 0x0E; + + private final PinUvAuthProtocol pinUvAuth; + private final byte[] pinUvToken; + + private final org.slf4j.Logger logger = LoggerFactory.getLogger(FingerprintBioEnrollment.class); + + public static class SensorInfo { + public final int fingerprintKind; + public final int maxCaptureSamplesRequiredForEnroll; + public final int maxTemplateFriendlyName; + + public SensorInfo( + int fingerprintKind, int maxCaptureSamplesRequiredForEnroll, int maxTemplateFriendlyName) { + this.fingerprintKind = fingerprintKind; + this.maxCaptureSamplesRequiredForEnroll = maxCaptureSamplesRequiredForEnroll; + this.maxTemplateFriendlyName = maxTemplateFriendlyName; } - public static class CaptureError extends Exception { - private final int code; - - public CaptureError(int code) { - super("Fingerprint capture error: " + code); - this.code = code; - } - - public int getCode() { - return code; - } + /** + * Indicates type of fingerprint sensor. + * + * @return For touch type fingerprints returns 1, for swipe type fingerprints returns 2. + */ + public int getFingerprintKind() { + return fingerprintKind; } - public static class CaptureStatus { - private final int sampleStatus; - private final int remaining; - - public CaptureStatus(int sampleStatus, int remaining) { - this.sampleStatus = sampleStatus; - this.remaining = remaining; - } - - public int getSampleStatus() { - return sampleStatus; - } - - public int getRemaining() { - return remaining; - } + /** + * Indicates the maximum good samples required for enrollment. + * + * @return Maximum good samples required for enrollment. + */ + public int getMaxCaptureSamplesRequiredForEnroll() { + return maxCaptureSamplesRequiredForEnroll; } - public static class EnrollBeginStatus extends CaptureStatus { - private final byte[] templateId; + /** + * Indicates the maximum number of bytes the authenticator will accept as a + * templateFriendlyName. + * + * @return Maximum number of bytes the authenticator will accept as a templateFriendlyName. + */ + public int getMaxTemplateFriendlyName() { + return maxTemplateFriendlyName; + } + } - public EnrollBeginStatus(byte[] templateId, int sampleStatus, int remaining) { - super(sampleStatus, remaining); - this.templateId = templateId; - } + public static class CaptureError extends Exception { + private final int code; - public byte[] getTemplateId() { - return templateId; - } + public CaptureError(int code) { + super("Fingerprint capture error: " + code); + this.code = code; } - /** - * Convenience class for handling one fingerprint enrollment - */ - public static class Context { - private final FingerprintBioEnrollment bioEnrollment; - @Nullable - private final Integer timeout; - @Nullable - private byte[] templateId; - @Nullable - private Integer remaining; - - public Context( - FingerprintBioEnrollment bioEnrollment, - @Nullable Integer timeout, - @Nullable byte[] templateId, - @Nullable Integer remaining) { - this.bioEnrollment = bioEnrollment; - this.timeout = timeout; - this.templateId = templateId; - this.remaining = remaining; - } - - /** - * Capture a fingerprint sample. - *

- * This call will block for up to timeout milliseconds (or indefinitely, if - * timeout not specified) waiting for the user to scan their fingerprint to - * collect one sample. - * - * @param state If needed, the state to provide control over the ongoing operation. - * @return None, if more samples are needed, or the template ID if enrollment is - * completed. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication error in the protocol layer. - * @throws CaptureError An error during fingerprint capture. - */ - @Nullable - public byte[] capture(@Nullable CommandState state) - throws IOException, CommandException, CaptureError { - int sampleStatus; - if (templateId == null) { - final EnrollBeginStatus status = bioEnrollment.enrollBegin(timeout, state); - templateId = status.getTemplateId(); - remaining = status.getRemaining(); - sampleStatus = status.getSampleStatus(); - } else { - final CaptureStatus status = bioEnrollment.enrollCaptureNext( - templateId, - timeout, - state); - remaining = status.getRemaining(); - sampleStatus = status.getSampleStatus(); - } - - if (sampleStatus != FEEDBACK_FP_GOOD) { - throw new CaptureError(sampleStatus); - } - - if (remaining == 0) { - return templateId; - } - return null; - } + public int getCode() { + return code; + } + } - /** - * Cancels ongoing enrollment. - */ - public void cancel() throws IOException, CommandException { - bioEnrollment.enrollCancel(); - templateId = null; - } + public static class CaptureStatus { + private final int sampleStatus; + private final int remaining; - /** - * @return number of remaining captures for successful enrollment - */ - @Nullable - public Integer getRemaining() { - return remaining; - } + public CaptureStatus(int sampleStatus, int remaining) { + this.sampleStatus = sampleStatus; + this.remaining = remaining; } - public FingerprintBioEnrollment( - Ctap2Session ctap, - PinUvAuthProtocol pinUvAuthProtocol, - byte[] pinUvToken) throws IOException, CommandException { - super(ctap, BioEnrollment.MODALITY_FINGERPRINT); - this.pinUvAuth = pinUvAuthProtocol; - this.pinUvToken = pinUvToken; + public int getSampleStatus() { + return sampleStatus; } - private Map call( - Integer subCommand, - @Nullable Map subCommandParams, - @Nullable CommandState state) throws IOException, CommandException { - return call(subCommand, subCommandParams, state, true); + public int getRemaining() { + return remaining; } + } - private Map call( - Integer subCommand, - @Nullable Map subCommandParams, - @Nullable CommandState state, - boolean authenticate) throws IOException, CommandException { - byte[] pinUvAuthParam = null; - if (authenticate) { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - output.write(MODALITY_FINGERPRINT); - output.write(subCommand); - if (subCommandParams != null) { - Cbor.encodeTo(output, subCommandParams); - } - pinUvAuthParam = pinUvAuth.authenticate(pinUvToken, output.toByteArray()); - } + public static class EnrollBeginStatus extends CaptureStatus { + private final byte[] templateId; - return ctap.bioEnrollment( - modality, - subCommand, - subCommandParams, - pinUvAuth.getVersion(), - pinUvAuthParam, - null, - state); + public EnrollBeginStatus(byte[] templateId, int sampleStatus, int remaining) { + super(sampleStatus, remaining); + this.templateId = templateId; } - /** - * Get fingerprint sensor info. - * - * @return A dict containing FINGERPRINT_KIND, MAX_SAMPLES_REQUIRES and - * MAX_TEMPLATE_FRIENDLY_NAME. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication error in the protocol layer. - * @see Get fingerprint sensor info - */ - public SensorInfo getSensorInfo() throws IOException, CommandException { - - final Map result = ctap.bioEnrollment( - MODALITY_FINGERPRINT, - CMD_GET_SENSOR_INFO, - null, - null, - null, - null, - null); - - return new SensorInfo( - Objects.requireNonNull((Integer) result.get(RESULT_FINGERPRINT_KIND)), - Objects.requireNonNull((Integer) result.get(RESULT_MAX_SAMPLES_REQUIRED)), - Objects.requireNonNull((Integer) result.get(RESULT_MAX_TEMPLATE_FRIENDLY_NAME)) - ); + public byte[] getTemplateId() { + return templateId; } - - /** - * Start fingerprint enrollment. - *

- * Starts the process of enrolling a new fingerprint, and will wait for the user - * to scan their fingerprint once to provide an initial sample. - * - * @param timeout Optional timeout in milliseconds. - * @param state If needed, the state to provide control over the ongoing operation. - * @return A status object containing the new template ID, the sample status, - * and the number of samples remaining to complete the enrollment. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication error in the protocol layer. - * @see Enrolling fingerprint - */ - public EnrollBeginStatus enrollBegin(@Nullable Integer timeout, @Nullable CommandState state) - throws IOException, CommandException { - Logger.debug(logger, "Starting fingerprint enrollment"); - - Map parameters = new HashMap<>(); - if (timeout != null) parameters.put(PARAM_TIMEOUT_MS, timeout); - - final Map result = call(CMD_ENROLL_BEGIN, parameters, state); - Logger.debug(logger, "Sample capture result: {}", result); - return new EnrollBeginStatus( - Objects.requireNonNull((byte[]) result.get(RESULT_TEMPLATE_ID)), - Objects.requireNonNull((Integer) result.get(RESULT_LAST_SAMPLE_STATUS)), - Objects.requireNonNull((Integer) result.get(RESULT_REMAINING_SAMPLES))); + } + + /** Convenience class for handling one fingerprint enrollment */ + public static class Context { + private final FingerprintBioEnrollment bioEnrollment; + @Nullable private final Integer timeout; + @Nullable private byte[] templateId; + @Nullable private Integer remaining; + + public Context( + FingerprintBioEnrollment bioEnrollment, + @Nullable Integer timeout, + @Nullable byte[] templateId, + @Nullable Integer remaining) { + this.bioEnrollment = bioEnrollment; + this.timeout = timeout; + this.templateId = templateId; + this.remaining = remaining; } /** - * Continue fingerprint enrollment. - *

- * Continues enrolling a new fingerprint and will wait for the user to scan their - * fingerprint once to provide a new sample. - * Once the number of samples remaining is 0, the enrollment is completed. + * Capture a fingerprint sample. * - * @param templateId The template ID returned by a call to - * {@link #enrollBegin(Integer timeout, CommandState state)}. - * @param timeout Optional timeout in milliseconds. - * @param state If needed, the state to provide control over the ongoing operation. - * @return A status object containing the sample status, and the number of samples - * remaining to complete the enrollment. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication error in the protocol layer. - * @see Enrolling fingerprint - */ - public CaptureStatus enrollCaptureNext( - byte[] templateId, - @Nullable Integer timeout, - @Nullable CommandState state) throws IOException, CommandException { - Logger.debug(logger, "Capturing next sample with (timeout={})", - timeout != null - ? timeout - : "none specified"); - - Map parameters = new HashMap<>(); - parameters.put(PARAM_TEMPLATE_ID, templateId); - if (timeout != null) parameters.put(PARAM_TIMEOUT_MS, timeout); - - final Map result = call(CMD_ENROLL_CAPTURE_NEXT, parameters, state); - Logger.debug(logger, "Sample capture result: {}", result); - return new CaptureStatus( - Objects.requireNonNull((Integer) result.get(RESULT_LAST_SAMPLE_STATUS)), - Objects.requireNonNull((Integer) result.get(RESULT_REMAINING_SAMPLES))); - } - - /** - * Cancel any ongoing fingerprint enrollment. + *

This call will block for up to timeout milliseconds (or indefinitely, if timeout not + * specified) waiting for the user to scan their fingerprint to collect one sample. * - * @throws IOException A communication error in the transport layer. + * @param state If needed, the state to provide control over the ongoing operation. + * @return None, if more samples are needed, or the template ID if enrollment is completed. + * @throws IOException A communication error in the transport layer. * @throws CommandException A communication error in the protocol layer. - * @see Cancel current enrollment + * @throws CaptureError An error during fingerprint capture. */ - public void enrollCancel() throws IOException, CommandException { - Logger.debug(logger, "Cancelling fingerprint enrollment."); - call(CMD_ENROLL_CANCEL, null, null, false); + @Nullable public byte[] capture(@Nullable CommandState state) + throws IOException, CommandException, CaptureError { + int sampleStatus; + if (templateId == null) { + final EnrollBeginStatus status = bioEnrollment.enrollBegin(timeout, state); + templateId = status.getTemplateId(); + remaining = status.getRemaining(); + sampleStatus = status.getSampleStatus(); + } else { + final CaptureStatus status = bioEnrollment.enrollCaptureNext(templateId, timeout, state); + remaining = status.getRemaining(); + sampleStatus = status.getSampleStatus(); + } + + if (sampleStatus != FEEDBACK_FP_GOOD) { + throw new CaptureError(sampleStatus); + } + + if (remaining == 0) { + return templateId; + } + return null; } - /** - * Convenience wrapper for doing fingerprint enrollment. - * - * @param timeout Optional timeout in milliseconds. - * @return An initialized FingerprintEnrollment.Context. - * @see FingerprintBioEnrollment.Context - */ - public Context enroll(@Nullable Integer timeout) { - return new Context(this, timeout, null, null); + /** Cancels ongoing enrollment. */ + public void cancel() throws IOException, CommandException { + bioEnrollment.enrollCancel(); + templateId = null; } /** - * Get a dict of enrolled fingerprint templates which maps template ID's to - * their friendly names. - * - * @return A Map of enrolled templateId -> name pairs. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication error in the protocol layer. - * @see Enumerate enrollments + * @return number of remaining captures for successful enrollment */ - public Map enumerateEnrollments() throws IOException, CommandException { - try { - final Map result = call(CMD_ENUMERATE_ENROLLMENTS, null, null); - - @SuppressWarnings("unchecked") - final List> infos = (List>) result.get(RESULT_TEMPLATE_INFOS); - final Map enrollments = new HashMap<>(); - for (Map info : infos) { - final byte[] id = Objects.requireNonNull((byte[]) info.get(TEMPLATE_INFO_ID)); - @Nullable - String friendlyName = (String) info.get(TEMPLATE_INFO_FRIENDLY_NAME); - // treat empty strings as null values - if (friendlyName != null) { - friendlyName = friendlyName.trim(); - if (friendlyName.isEmpty()) { - friendlyName = null; - } - } - enrollments.put(id, friendlyName); - } - - return enrollments; - } catch (CtapException e) { - if (e.getCtapError() == CtapException.ERR_INVALID_OPTION) { - return Collections.emptyMap(); - } - throw e; - } + @Nullable public Integer getRemaining() { + return remaining; } - - /** - * Set/Change the friendly name of a previously enrolled fingerprint template. - * - * @param templateId The ID of the template to change. - * @param name A friendly name to give the template. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication error in the protocol layer. - * @see Rename/Set FriendlyName - */ - public void setName(byte[] templateId, String name) throws IOException, CommandException { - Logger.debug(logger, "Changing name of template: {} {}", Base64.toUrlSafeString(templateId), name); - - Map parameters = new HashMap<>(); - parameters.put(PARAM_TEMPLATE_ID, templateId); - parameters.put(PARAM_TEMPLATE_FRIENDLY_NAME, name); - - call(CMD_SET_NAME, parameters, null); - Logger.info(logger, "Fingerprint template renamed"); + } + + public FingerprintBioEnrollment( + Ctap2Session ctap, PinUvAuthProtocol pinUvAuthProtocol, byte[] pinUvToken) + throws IOException, CommandException { + super(ctap, BioEnrollment.MODALITY_FINGERPRINT); + this.pinUvAuth = pinUvAuthProtocol; + this.pinUvToken = pinUvToken; + } + + private Map call( + Integer subCommand, @Nullable Map subCommandParams, @Nullable CommandState state) + throws IOException, CommandException { + return call(subCommand, subCommandParams, state, true); + } + + private Map call( + Integer subCommand, + @Nullable Map subCommandParams, + @Nullable CommandState state, + boolean authenticate) + throws IOException, CommandException { + byte[] pinUvAuthParam = null; + if (authenticate) { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + output.write(MODALITY_FINGERPRINT); + output.write(subCommand); + if (subCommandParams != null) { + Cbor.encodeTo(output, subCommandParams); + } + pinUvAuthParam = pinUvAuth.authenticate(pinUvToken, output.toByteArray()); } - /** - * Remove a previously enrolled fingerprint template. - * - * @param templateId The Id of the template to remove. - * @throws IOException A communication error in the transport layer. - * @throws CommandException A communication error in the protocol layer. - * @see Remove enrollment - */ - public void removeEnrollment(byte[] templateId) throws IOException, CommandException { - Logger.debug(logger, "Deleting template: {}", Base64.toUrlSafeString(templateId)); - - Map parameters = new HashMap<>(); - parameters.put(PARAM_TEMPLATE_ID, templateId); - - call(CMD_REMOVE_ENROLLMENT, parameters, null); - Logger.info(logger, "Fingerprint template deleted"); + return ctap.bioEnrollment( + modality, + subCommand, + subCommandParams, + pinUvAuth.getVersion(), + pinUvAuthParam, + null, + state); + } + + /** + * Get fingerprint sensor info. + * + * @return A dict containing FINGERPRINT_KIND, MAX_SAMPLES_REQUIRES and + * MAX_TEMPLATE_FRIENDLY_NAME. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication error in the protocol layer. + * @see Get + * fingerprint sensor info + */ + public SensorInfo getSensorInfo() throws IOException, CommandException { + + final Map result = + ctap.bioEnrollment(MODALITY_FINGERPRINT, CMD_GET_SENSOR_INFO, null, null, null, null, null); + + return new SensorInfo( + Objects.requireNonNull((Integer) result.get(RESULT_FINGERPRINT_KIND)), + Objects.requireNonNull((Integer) result.get(RESULT_MAX_SAMPLES_REQUIRED)), + Objects.requireNonNull((Integer) result.get(RESULT_MAX_TEMPLATE_FRIENDLY_NAME))); + } + + /** + * Start fingerprint enrollment. + * + *

Starts the process of enrolling a new fingerprint, and will wait for the user to scan their + * fingerprint once to provide an initial sample. + * + * @param timeout Optional timeout in milliseconds. + * @param state If needed, the state to provide control over the ongoing operation. + * @return A status object containing the new template ID, the sample status, and the number of + * samples remaining to complete the enrollment. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication error in the protocol layer. + * @see Enrolling + * fingerprint + */ + public EnrollBeginStatus enrollBegin(@Nullable Integer timeout, @Nullable CommandState state) + throws IOException, CommandException { + Logger.debug(logger, "Starting fingerprint enrollment"); + + Map parameters = new HashMap<>(); + if (timeout != null) parameters.put(PARAM_TIMEOUT_MS, timeout); + + final Map result = call(CMD_ENROLL_BEGIN, parameters, state); + Logger.debug(logger, "Sample capture result: {}", result); + return new EnrollBeginStatus( + Objects.requireNonNull((byte[]) result.get(RESULT_TEMPLATE_ID)), + Objects.requireNonNull((Integer) result.get(RESULT_LAST_SAMPLE_STATUS)), + Objects.requireNonNull((Integer) result.get(RESULT_REMAINING_SAMPLES))); + } + + /** + * Continue fingerprint enrollment. + * + *

Continues enrolling a new fingerprint and will wait for the user to scan their fingerprint + * once to provide a new sample. Once the number of samples remaining is 0, the enrollment is + * completed. + * + * @param templateId The template ID returned by a call to {@link #enrollBegin(Integer timeout, + * CommandState state)}. + * @param timeout Optional timeout in milliseconds. + * @param state If needed, the state to provide control over the ongoing operation. + * @return A status object containing the sample status, and the number of samples remaining to + * complete the enrollment. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication error in the protocol layer. + * @see Enrolling + * fingerprint + */ + public CaptureStatus enrollCaptureNext( + byte[] templateId, @Nullable Integer timeout, @Nullable CommandState state) + throws IOException, CommandException { + Logger.debug( + logger, + "Capturing next sample with (timeout={})", + timeout != null ? timeout : "none specified"); + + Map parameters = new HashMap<>(); + parameters.put(PARAM_TEMPLATE_ID, templateId); + if (timeout != null) parameters.put(PARAM_TIMEOUT_MS, timeout); + + final Map result = call(CMD_ENROLL_CAPTURE_NEXT, parameters, state); + Logger.debug(logger, "Sample capture result: {}", result); + return new CaptureStatus( + Objects.requireNonNull((Integer) result.get(RESULT_LAST_SAMPLE_STATUS)), + Objects.requireNonNull((Integer) result.get(RESULT_REMAINING_SAMPLES))); + } + + /** + * Cancel any ongoing fingerprint enrollment. + * + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication error in the protocol layer. + * @see Cancel + * current enrollment + */ + public void enrollCancel() throws IOException, CommandException { + Logger.debug(logger, "Cancelling fingerprint enrollment."); + call(CMD_ENROLL_CANCEL, null, null, false); + } + + /** + * Convenience wrapper for doing fingerprint enrollment. + * + * @param timeout Optional timeout in milliseconds. + * @return An initialized FingerprintEnrollment.Context. + * @see FingerprintBioEnrollment.Context + */ + public Context enroll(@Nullable Integer timeout) { + return new Context(this, timeout, null, null); + } + + /** + * Get a dict of enrolled fingerprint templates which maps template ID's to their friendly names. + * + * @return A Map of enrolled templateId -> name pairs. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication error in the protocol layer. + * @see Enumerate + * enrollments + */ + public Map enumerateEnrollments() throws IOException, CommandException { + try { + final Map result = call(CMD_ENUMERATE_ENROLLMENTS, null, null); + + @SuppressWarnings("unchecked") + final List> infos = (List>) result.get(RESULT_TEMPLATE_INFOS); + final Map enrollments = new HashMap<>(); + for (Map info : infos) { + final byte[] id = Objects.requireNonNull((byte[]) info.get(TEMPLATE_INFO_ID)); + @Nullable String friendlyName = (String) info.get(TEMPLATE_INFO_FRIENDLY_NAME); + // treat empty strings as null values + if (friendlyName != null) { + friendlyName = friendlyName.trim(); + if (friendlyName.isEmpty()) { + friendlyName = null; + } + } + enrollments.put(id, friendlyName); + } + + return enrollments; + } catch (CtapException e) { + if (e.getCtapError() == CtapException.ERR_INVALID_OPTION) { + return Collections.emptyMap(); + } + throw e; } + } + + /** + * Set/Change the friendly name of a previously enrolled fingerprint template. + * + * @param templateId The ID of the template to change. + * @param name A friendly name to give the template. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication error in the protocol layer. + * @see Rename/Set + * FriendlyName + */ + public void setName(byte[] templateId, String name) throws IOException, CommandException { + Logger.debug( + logger, "Changing name of template: {} {}", Base64.toUrlSafeString(templateId), name); + + Map parameters = new HashMap<>(); + parameters.put(PARAM_TEMPLATE_ID, templateId); + parameters.put(PARAM_TEMPLATE_FRIENDLY_NAME, name); + + call(CMD_SET_NAME, parameters, null); + Logger.info(logger, "Fingerprint template renamed"); + } + + /** + * Remove a previously enrolled fingerprint template. + * + * @param templateId The Id of the template to remove. + * @throws IOException A communication error in the transport layer. + * @throws CommandException A communication error in the protocol layer. + * @see Remove + * enrollment + */ + public void removeEnrollment(byte[] templateId) throws IOException, CommandException { + Logger.debug(logger, "Deleting template: {}", Base64.toUrlSafeString(templateId)); + + Map parameters = new HashMap<>(); + parameters.put(PARAM_TEMPLATE_ID, templateId); + + call(CMD_REMOVE_ENROLLMENT, parameters, null); + Logger.info(logger, "Fingerprint template deleted"); + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/ctap/Hkdf.java b/fido/src/main/java/com/yubico/yubikit/fido/ctap/Hkdf.java index 9c15434b..12d024c4 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/ctap/Hkdf.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/ctap/Hkdf.java @@ -20,7 +20,6 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; - import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; @@ -31,58 +30,47 @@ */ class Hkdf { - private final Mac mac; + private final Mac mac; - Hkdf(String algo) throws NoSuchAlgorithmException { - this.mac = Mac.getInstance(algo); - } + Hkdf(String algo) throws NoSuchAlgorithmException { + this.mac = Mac.getInstance(algo); + } - byte[] hmacDigest(byte[] key, byte[] data) throws InvalidKeyException { - mac.init(new SecretKeySpec(key, mac.getAlgorithm())); - return mac.doFinal(data); - } + byte[] hmacDigest(byte[] key, byte[] data) throws InvalidKeyException { + mac.init(new SecretKeySpec(key, mac.getAlgorithm())); + return mac.doFinal(data); + } - byte[] extract(byte[] salt, byte[] ikm) throws InvalidKeyException { - return hmacDigest( - salt.length != 0 - ? salt - : new byte[mac.getMacLength()], - ikm); - } + byte[] extract(byte[] salt, byte[] ikm) throws InvalidKeyException { + return hmacDigest(salt.length != 0 ? salt : new byte[mac.getMacLength()], ikm); + } - byte[] expand(byte[] prk, byte[] info, int length) throws InvalidKeyException { - byte[] t = new byte[0]; - byte[] okm = new byte[0]; - byte i = 0; - while (okm.length < length) { - i++; - byte[] data = ByteBuffer.allocate(t.length + info.length + 1) - .put(t) - .put(info) - .put(i) - .array(); - Arrays.fill(t, (byte) 0); - byte[] digest = hmacDigest(prk, data); + byte[] expand(byte[] prk, byte[] info, int length) throws InvalidKeyException { + byte[] t = new byte[0]; + byte[] okm = new byte[0]; + byte i = 0; + while (okm.length < length) { + i++; + byte[] data = ByteBuffer.allocate(t.length + info.length + 1).put(t).put(info).put(i).array(); + Arrays.fill(t, (byte) 0); + byte[] digest = hmacDigest(prk, data); - byte[] result = ByteBuffer.allocate(okm.length + digest.length) - .put(okm) - .put(digest) - .array(); - Arrays.fill(okm, (byte) 0); - Arrays.fill(data, (byte) 0); - okm = result; - t = digest; - } - - byte[] result = Arrays.copyOf(okm, length); - Arrays.fill(okm, (byte) 0); - return result; + byte[] result = ByteBuffer.allocate(okm.length + digest.length).put(okm).put(digest).array(); + Arrays.fill(okm, (byte) 0); + Arrays.fill(data, (byte) 0); + okm = result; + t = digest; } - byte[] digest(byte[] ikm, byte[] salt, byte[] info, int length) throws InvalidKeyException { - byte[] prk = extract(salt, ikm); - byte[] result = expand(prk, info, length); - Arrays.fill(prk, (byte) 0); - return result; - } + byte[] result = Arrays.copyOf(okm, length); + Arrays.fill(okm, (byte) 0); + return result; + } + + byte[] digest(byte[] ikm, byte[] salt, byte[] info, int length) throws InvalidKeyException { + byte[] prk = extract(salt, ikm); + byte[] result = expand(prk, info, length); + Arrays.fill(prk, (byte) 0); + return result; + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/ctap/PinUvAuthDummyProtocol.java b/fido/src/main/java/com/yubico/yubikit/fido/ctap/PinUvAuthDummyProtocol.java index fd53c69b..4aeb5371 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/ctap/PinUvAuthDummyProtocol.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/ctap/PinUvAuthDummyProtocol.java @@ -17,41 +17,43 @@ package com.yubico.yubikit.fido.ctap; import com.yubico.yubikit.core.util.Pair; - import java.util.Map; /** * Implements a dummy PIN/UV Auth Protocol - * @see PIN/UV Auth Protocol Abstract Definition. + * + * @see PIN/UV + * Auth Protocol Abstract Definition. */ public class PinUvAuthDummyProtocol implements PinUvAuthProtocol { - @Override - public int getVersion() { - throw new UnsupportedPinUvAuthProtocolError(); - } - - @Override - public Pair, byte[]> encapsulate(Map peerCoseKey) { - throw new UnsupportedPinUvAuthProtocolError(); - } - - @Override - public byte[] encrypt(byte[] key, byte[] demPlaintext) { - throw new UnsupportedPinUvAuthProtocolError(); - } - - @Override - public byte[] decrypt(byte[] key, byte[] demCiphertext) { - throw new UnsupportedPinUvAuthProtocolError(); - } - - @Override - public byte[] authenticate(byte[] key, byte[] message) { - throw new UnsupportedPinUvAuthProtocolError(); - } - - @Override - public byte[] kdf(byte[] z) { - throw new UnsupportedPinUvAuthProtocolError(); - } -} \ No newline at end of file + @Override + public int getVersion() { + throw new UnsupportedPinUvAuthProtocolError(); + } + + @Override + public Pair, byte[]> encapsulate(Map peerCoseKey) { + throw new UnsupportedPinUvAuthProtocolError(); + } + + @Override + public byte[] encrypt(byte[] key, byte[] demPlaintext) { + throw new UnsupportedPinUvAuthProtocolError(); + } + + @Override + public byte[] decrypt(byte[] key, byte[] demCiphertext) { + throw new UnsupportedPinUvAuthProtocolError(); + } + + @Override + public byte[] authenticate(byte[] key, byte[] message) { + throw new UnsupportedPinUvAuthProtocolError(); + } + + @Override + public byte[] kdf(byte[] z) { + throw new UnsupportedPinUvAuthProtocolError(); + } +} diff --git a/fido/src/main/java/com/yubico/yubikit/fido/ctap/PinUvAuthProtocol.java b/fido/src/main/java/com/yubico/yubikit/fido/ctap/PinUvAuthProtocol.java index 19718b51..c52d3e96 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/ctap/PinUvAuthProtocol.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/ctap/PinUvAuthProtocol.java @@ -17,7 +17,6 @@ package com.yubico.yubikit.fido.ctap; import com.yubico.yubikit.core.util.Pair; - import java.util.Map; /** @@ -26,50 +25,50 @@ * commands. */ public interface PinUvAuthProtocol { - /** - * Returns the version number of the PIN/UV Auth protocol. - * - * @return the version of the protocol - */ - int getVersion(); + /** + * Returns the version number of the PIN/UV Auth protocol. + * + * @return the version of the protocol + */ + int getVersion(); - /** - * Generates an encapsulation for the authenticator’s public key and returns the message to transmit and the shared secret. - * - * @param peerCoseKey a public key returned by the YubiKey - * @return a Pair containing a keyAgreement to transmit, and the shared secret. - */ - Pair, byte[]> encapsulate(Map peerCoseKey); + /** + * Generates an encapsulation for the authenticator’s public key and returns the message to + * transmit and the shared secret. + * + * @param peerCoseKey a public key returned by the YubiKey + * @return a Pair containing a keyAgreement to transmit, and the shared secret. + */ + Pair, byte[]> encapsulate(Map peerCoseKey); - /** - * Computes shared secret - */ - byte[] kdf(byte[] z); + /** Computes shared secret */ + byte[] kdf(byte[] z); - /** - * Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext. The plaintext is restricted to being a multiple of the AES block size (16 bytes) in length. - * - * @param key the secret key to use - * @param demPlaintext the value to encrypt - * @return the encrypted value - */ - byte[] encrypt(byte[] key, byte[] demPlaintext); + /** + * Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext. The + * plaintext is restricted to being a multiple of the AES block size (16 bytes) in length. + * + * @param key the secret key to use + * @param demPlaintext the value to encrypt + * @return the encrypted value + */ + byte[] encrypt(byte[] key, byte[] demPlaintext); - /** - * Decrypts a ciphertext and returns the plaintext. - * - * @param key the secret key to use - * @param demCiphertext the value to decrypt - * @return the decrypted value - */ - byte[] decrypt(byte[] key, byte[] demCiphertext); + /** + * Decrypts a ciphertext and returns the plaintext. + * + * @param key the secret key to use + * @param demCiphertext the value to decrypt + * @return the decrypted value + */ + byte[] decrypt(byte[] key, byte[] demCiphertext); - /** - * Computes a MAC of the given message. - * - * @param key the secret key to use - * @param message the message to sign - * @return a signature - */ - byte[] authenticate(byte[] key, byte[] message); + /** + * Computes a MAC of the given message. + * + * @param key the secret key to use + * @param message the message to sign + * @return a signature + */ + byte[] authenticate(byte[] key, byte[] message); } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/ctap/PinUvAuthProtocolV1.java b/fido/src/main/java/com/yubico/yubikit/fido/ctap/PinUvAuthProtocolV1.java index bbe87c30..be6084c3 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/ctap/PinUvAuthProtocolV1.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/ctap/PinUvAuthProtocolV1.java @@ -17,7 +17,7 @@ package com.yubico.yubikit.fido.ctap; import com.yubico.yubikit.core.util.Pair; - +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.math.BigInteger; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; @@ -33,7 +33,6 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; - import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; @@ -43,137 +42,146 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - /** * Implements PIN/UV Auth Protocol 1 * - * @see authenticatorClientPIN. - * @see PIN/UV Auth Protocol One. + * @see authenticatorClientPIN. + * @see PIN/UV + * Auth Protocol One. */ public class PinUvAuthProtocolV1 implements PinUvAuthProtocol { - public static final int VERSION = 1; - - private static final String HASH_ALG = "SHA-256"; - private static final String MAC_ALG = "HmacSHA256"; - private static final String CIPHER_ALG = "AES"; - private static final String CIPHER_TRANSFORMATION = "AES/CBC/NoPadding"; - private static final String KEY_AGREEMENT_ALG = "ECDH"; - private static final String KEY_AGREEMENT_KEY_ALG = "EC"; - - private static final byte[] IV = new byte[16]; // All zero IV - - private static final int COORDINATE_SIZE = 32; - private static final int AUTHENTICATE_HASH_LEN = 16; - - private static final int KEY_SHAREDSECRET_POINT_X = -2; - private static final int KEY_SHAREDSECRET_POINT_Y = -3; - - @Override - public int getVersion() { - return VERSION; + public static final int VERSION = 1; + + private static final String HASH_ALG = "SHA-256"; + private static final String MAC_ALG = "HmacSHA256"; + private static final String CIPHER_ALG = "AES"; + private static final String CIPHER_TRANSFORMATION = "AES/CBC/NoPadding"; + private static final String KEY_AGREEMENT_ALG = "ECDH"; + private static final String KEY_AGREEMENT_KEY_ALG = "EC"; + + private static final byte[] IV = new byte[16]; // All zero IV + + private static final int COORDINATE_SIZE = 32; + private static final int AUTHENTICATE_HASH_LEN = 16; + + private static final int KEY_SHAREDSECRET_POINT_X = -2; + private static final int KEY_SHAREDSECRET_POINT_Y = -3; + + @Override + public int getVersion() { + return VERSION; + } + + @Override + public Pair, byte[]> encapsulate(Map peerCoseKey) { + try { + KeyPairGenerator kpg = KeyPairGenerator.getInstance(KEY_AGREEMENT_KEY_ALG); + kpg.initialize(256); // SECP256R1 + KeyPair kp = kpg.generateKeyPair(); + ECPoint point = ((ECPublicKey) kp.getPublic()).getW(); + Map keyAgreement = new HashMap<>(); + keyAgreement.put(1, 2); + keyAgreement.put(3, -25); + keyAgreement.put(-1, 1); + keyAgreement.put(KEY_SHAREDSECRET_POINT_X, encodeCoordinate(point.getAffineX())); + keyAgreement.put(KEY_SHAREDSECRET_POINT_Y, encodeCoordinate(point.getAffineY())); + + ECPoint w = + new ECPoint( + new BigInteger(1, ((byte[]) peerCoseKey.get(KEY_SHAREDSECRET_POINT_X))), + new BigInteger(1, ((byte[]) peerCoseKey.get(KEY_SHAREDSECRET_POINT_Y)))); + ECPublicKeySpec otherKeySpec = + new ECPublicKeySpec(w, ((ECPublicKey) kp.getPublic()).getParams()); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_AGREEMENT_KEY_ALG); + ECPublicKey otherKey = (ECPublicKey) keyFactory.generatePublic(otherKeySpec); + + KeyAgreement ecdh = KeyAgreement.getInstance(KEY_AGREEMENT_ALG); + ecdh.init(kp.getPrivate()); + ecdh.doPhase(otherKey, true); + byte[] sharedSecret = kdf(ecdh.generateSecret()); + return new Pair<>(keyAgreement, sharedSecret); + } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException e) { + throw new IllegalStateException(e); } - - @Override - public Pair, byte[]> encapsulate(Map peerCoseKey) { - try { - KeyPairGenerator kpg = KeyPairGenerator.getInstance(KEY_AGREEMENT_KEY_ALG); - kpg.initialize(256); // SECP256R1 - KeyPair kp = kpg.generateKeyPair(); - ECPoint point = ((ECPublicKey) kp.getPublic()).getW(); - Map keyAgreement = new HashMap<>(); - keyAgreement.put(1, 2); - keyAgreement.put(3, -25); - keyAgreement.put(-1, 1); - keyAgreement.put(KEY_SHAREDSECRET_POINT_X, encodeCoordinate(point.getAffineX())); - keyAgreement.put(KEY_SHAREDSECRET_POINT_Y, encodeCoordinate(point.getAffineY())); - - ECPoint w = new ECPoint( - new BigInteger(1, ((byte[]) peerCoseKey.get(KEY_SHAREDSECRET_POINT_X))), - new BigInteger(1, ((byte[]) peerCoseKey.get(KEY_SHAREDSECRET_POINT_Y))) - ); - ECPublicKeySpec otherKeySpec = new ECPublicKeySpec(w, ((ECPublicKey) kp.getPublic()).getParams()); - KeyFactory keyFactory = KeyFactory.getInstance(KEY_AGREEMENT_KEY_ALG); - ECPublicKey otherKey = (ECPublicKey) keyFactory.generatePublic(otherKeySpec); - - KeyAgreement ecdh = KeyAgreement.getInstance(KEY_AGREEMENT_ALG); - ecdh.init(kp.getPrivate()); - ecdh.doPhase(otherKey, true); - byte[] sharedSecret = kdf(ecdh.generateSecret()); - return new Pair<>(keyAgreement, sharedSecret); - } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException e) { - throw new IllegalStateException(e); - } + } + + @Override + public byte[] kdf(byte[] z) { + try { + return MessageDigest.getInstance(HASH_ALG).digest(z); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); } - - @Override - public byte[] kdf(byte[] z) { - try { - return MessageDigest.getInstance(HASH_ALG).digest(z); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } + } + + @SuppressFBWarnings( + value = {"CIPHER_INTEGRITY", "STATIC_IV"}, + justification = + "No padding is performed as the size of demPlaintext is required " + + "to be a multiple of the AES block length. The specification for " + + "PIN/UV Auth Protocol One expects all null IV") + @Override + public byte[] encrypt(byte[] key, byte[] demPlaintext) { + try { + Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, CIPHER_ALG), new IvParameterSpec(IV)); + return cipher.doFinal(demPlaintext); + } catch (NoSuchAlgorithmException + | InvalidKeyException + | InvalidAlgorithmParameterException + | NoSuchPaddingException + | BadPaddingException + | IllegalBlockSizeException e) { + throw new IllegalStateException(e); } - - @SuppressFBWarnings(value = {"CIPHER_INTEGRITY", "STATIC_IV"}, - justification = "No padding is performed as the size of demPlaintext is required " + - "to be a multiple of the AES block length. The specification for " + - "PIN/UV Auth Protocol One expects all null IV") - @Override - public byte[] encrypt(byte[] key, byte[] demPlaintext) { - try { - Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION); - cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, CIPHER_ALG), new IvParameterSpec(IV)); - return cipher.doFinal(demPlaintext); - } catch (NoSuchAlgorithmException | InvalidKeyException | - InvalidAlgorithmParameterException | NoSuchPaddingException | BadPaddingException | - IllegalBlockSizeException e) { - throw new IllegalStateException(e); - } + } + + @SuppressFBWarnings( + value = "CIPHER_INTEGRITY", + justification = + "No padding is performed as the size of demPlaintext is required " + + "to be a multiple of the AES block length.") + @Override + public byte[] decrypt(byte[] key, byte[] demCiphertext) { + try { + Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION); + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, CIPHER_ALG), new IvParameterSpec(IV)); + return cipher.doFinal(demCiphertext); + } catch (NoSuchAlgorithmException + | InvalidKeyException + | InvalidAlgorithmParameterException + | NoSuchPaddingException + | BadPaddingException + | IllegalBlockSizeException e) { + throw new IllegalStateException(e); } - - - @SuppressFBWarnings(value = "CIPHER_INTEGRITY", - justification = "No padding is performed as the size of demPlaintext is required " + - "to be a multiple of the AES block length.") - @Override - public byte[] decrypt(byte[] key, byte[] demCiphertext) { - try { - Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION); - cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, CIPHER_ALG), new IvParameterSpec(IV)); - return cipher.doFinal(demCiphertext); - } catch (NoSuchAlgorithmException | InvalidKeyException | - InvalidAlgorithmParameterException | NoSuchPaddingException | BadPaddingException | - IllegalBlockSizeException e) { - throw new IllegalStateException(e); - } - } - - @Override - public byte[] authenticate(byte[] key, byte[] message) { - Mac mac; - try { - mac = Mac.getInstance(MAC_ALG); - mac.init(new SecretKeySpec(key, MAC_ALG)); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - throw new RuntimeException(e); - } - return Arrays.copyOf(mac.doFinal(message), AUTHENTICATE_HASH_LEN); + } + + @Override + public byte[] authenticate(byte[] key, byte[] message) { + Mac mac; + try { + mac = Mac.getInstance(MAC_ALG); + mac.init(new SecretKeySpec(key, MAC_ALG)); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new RuntimeException(e); } - - /** - * Encode a BigInteger as a 32 byte array. - */ - static byte[] encodeCoordinate(BigInteger value) { - byte[] valueBytes = value.toByteArray(); - byte[] result = new byte[COORDINATE_SIZE]; - if (valueBytes.length < COORDINATE_SIZE) { // Left pad with zeroes - System.arraycopy(valueBytes, 0, result, result.length - valueBytes.length, valueBytes.length); - } else if (valueBytes.length > COORDINATE_SIZE) { // Truncate from left - System.arraycopy(valueBytes, valueBytes.length - COORDINATE_SIZE, result, 0, COORDINATE_SIZE); - } else { - result = valueBytes; - } - return result; + return Arrays.copyOf(mac.doFinal(message), AUTHENTICATE_HASH_LEN); + } + + /** Encode a BigInteger as a 32 byte array. */ + static byte[] encodeCoordinate(BigInteger value) { + byte[] valueBytes = value.toByteArray(); + byte[] result = new byte[COORDINATE_SIZE]; + if (valueBytes.length < COORDINATE_SIZE) { // Left pad with zeroes + System.arraycopy(valueBytes, 0, result, result.length - valueBytes.length, valueBytes.length); + } else if (valueBytes.length > COORDINATE_SIZE) { // Truncate from left + System.arraycopy(valueBytes, valueBytes.length - COORDINATE_SIZE, result, 0, COORDINATE_SIZE); + } else { + result = valueBytes; } -} \ No newline at end of file + return result; + } +} diff --git a/fido/src/main/java/com/yubico/yubikit/fido/ctap/PinUvAuthProtocolV2.java b/fido/src/main/java/com/yubico/yubikit/fido/ctap/PinUvAuthProtocolV2.java index 0c7ff0cf..e865cf71 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/ctap/PinUvAuthProtocolV2.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/ctap/PinUvAuthProtocolV2.java @@ -17,14 +17,13 @@ package com.yubico.yubikit.fido.ctap; import com.yubico.yubikit.core.util.RandomUtils; - +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; - import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; @@ -33,133 +32,116 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - /** * Implements PIN/UV Auth Protocol 2 * - * @see authenticatorClientPIN. - * @see PIN/UV Auth Protocol Two. + * @see authenticatorClientPIN. + * @see PIN/UV + * Auth Protocol Two. */ - public class PinUvAuthProtocolV2 extends PinUvAuthProtocolV1 { - public static final int VERSION = 2; - - private static final String HKDF_ALG = "HmacSHA256"; - private static final byte[] HKDF_SALT = new byte[32]; - private static final byte[] HKDF_INFO_HMAC = "CTAP2 HMAC key".getBytes(StandardCharsets.UTF_8); - private static final byte[] HKDF_INFO_AES = "CTAP2 AES key".getBytes(StandardCharsets.UTF_8); - private static final int HKDF_LENGTH = 32; - - @Override - public int getVersion() { - return VERSION; + public static final int VERSION = 2; + + private static final String HKDF_ALG = "HmacSHA256"; + private static final byte[] HKDF_SALT = new byte[32]; + private static final byte[] HKDF_INFO_HMAC = "CTAP2 HMAC key".getBytes(StandardCharsets.UTF_8); + private static final byte[] HKDF_INFO_AES = "CTAP2 AES key".getBytes(StandardCharsets.UTF_8); + private static final int HKDF_LENGTH = 32; + + @Override + public int getVersion() { + return VERSION; + } + + @Override + public byte[] kdf(byte[] z) { + byte[] hmacKey = null; + byte[] aesKey = null; + try { + hmacKey = new Hkdf(HKDF_ALG).digest(z, HKDF_SALT, HKDF_INFO_HMAC, HKDF_LENGTH); + + aesKey = new Hkdf(HKDF_ALG).digest(z, HKDF_SALT, HKDF_INFO_AES, HKDF_LENGTH); + + return ByteBuffer.allocate(hmacKey.length + aesKey.length).put(hmacKey).put(aesKey).array(); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new IllegalStateException(e); + } finally { + if (hmacKey != null) { + Arrays.fill(hmacKey, (byte) 0); + } + if (aesKey != null) { + Arrays.fill(aesKey, (byte) 0); + } } - - @Override - public byte[] kdf(byte[] z) { - byte[] hmacKey = null; - byte[] aesKey = null; - try { - hmacKey = new Hkdf(HKDF_ALG).digest( - z, - HKDF_SALT, - HKDF_INFO_HMAC, - HKDF_LENGTH); - - aesKey = new Hkdf(HKDF_ALG).digest( - z, - HKDF_SALT, - HKDF_INFO_AES, - HKDF_LENGTH); - - return ByteBuffer.allocate(hmacKey.length + aesKey.length) - .put(hmacKey) - .put(aesKey) - .array(); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - throw new IllegalStateException(e); - } finally { - if (hmacKey != null) { - Arrays.fill(hmacKey, (byte) 0); - } - if (aesKey != null) { - Arrays.fill(aesKey, (byte) 0); - } - } - } - - @Override - public byte[] encrypt(byte[] key, byte[] plaintext) { - byte[] aesKey = null; - try { - aesKey = Arrays.copyOfRange(key, 32, key.length); - byte[] iv = RandomUtils.getRandomBytes(16); - - final byte[] ciphertext = - getCipher(Cipher.ENCRYPT_MODE, aesKey, iv) - .doFinal(plaintext); - return ByteBuffer.allocate(iv.length + ciphertext.length) - .put(iv) - .put(ciphertext) - .array(); - } catch (IllegalBlockSizeException | BadPaddingException e) { - throw new IllegalStateException(e); - } finally { - if (aesKey != null) { - Arrays.fill(aesKey, (byte) 0); - } - } + } + + @Override + public byte[] encrypt(byte[] key, byte[] plaintext) { + byte[] aesKey = null; + try { + aesKey = Arrays.copyOfRange(key, 32, key.length); + byte[] iv = RandomUtils.getRandomBytes(16); + + final byte[] ciphertext = getCipher(Cipher.ENCRYPT_MODE, aesKey, iv).doFinal(plaintext); + return ByteBuffer.allocate(iv.length + ciphertext.length).put(iv).put(ciphertext).array(); + } catch (IllegalBlockSizeException | BadPaddingException e) { + throw new IllegalStateException(e); + } finally { + if (aesKey != null) { + Arrays.fill(aesKey, (byte) 0); + } } - - @Override - public byte[] decrypt(byte[] key, byte[] ciphertext) { - byte[] aesKey = null; - try { - aesKey = Arrays.copyOfRange(key, 32, key.length); - byte[] iv = Arrays.copyOf(ciphertext, 16); - byte[] ct = Arrays.copyOfRange(ciphertext, 16, ciphertext.length); - return getCipher(Cipher.DECRYPT_MODE, aesKey, iv).doFinal(ct); - } catch (BadPaddingException | IllegalBlockSizeException e) { - throw new IllegalStateException(e); - } finally { - if (aesKey != null) { - Arrays.fill(aesKey, (byte) 0); - } - } + } + + @Override + public byte[] decrypt(byte[] key, byte[] ciphertext) { + byte[] aesKey = null; + try { + aesKey = Arrays.copyOfRange(key, 32, key.length); + byte[] iv = Arrays.copyOf(ciphertext, 16); + byte[] ct = Arrays.copyOfRange(ciphertext, 16, ciphertext.length); + return getCipher(Cipher.DECRYPT_MODE, aesKey, iv).doFinal(ct); + } catch (BadPaddingException | IllegalBlockSizeException e) { + throw new IllegalStateException(e); + } finally { + if (aesKey != null) { + Arrays.fill(aesKey, (byte) 0); + } } - - @Override - public byte[] authenticate(byte[] key, byte[] message) { - final String MAC_ALG = "HmacSHA256"; - byte[] hmacKey = Arrays.copyOf(key, 32); - Mac mac; - try { - mac = Mac.getInstance(MAC_ALG); - mac.init(new SecretKeySpec(hmacKey, MAC_ALG)); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - throw new RuntimeException(e); - } - return mac.doFinal(message); + } + + @Override + public byte[] authenticate(byte[] key, byte[] message) { + final String MAC_ALG = "HmacSHA256"; + byte[] hmacKey = Arrays.copyOf(key, 32); + Mac mac; + try { + mac = Mac.getInstance(MAC_ALG); + mac.init(new SecretKeySpec(hmacKey, MAC_ALG)); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new RuntimeException(e); } - - @SuppressFBWarnings(value = {"CIPHER_INTEGRITY", "STATIC_IV"}, - justification = "No padding is performed as the size of demPlaintext is required " + - "to be a multiple of the AES block length. The IV is randomly generated " + - "for every encrypt operation") - private Cipher getCipher(int mode, byte[] secret, byte[] iv) { - try { - Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); - cipher.init( - mode, - new SecretKeySpec(secret, "AES"), - new IvParameterSpec(iv)); - return cipher; - } catch (NoSuchPaddingException | - NoSuchAlgorithmException | - InvalidAlgorithmParameterException | - InvalidKeyException e) { - throw new IllegalStateException(e); - } + return mac.doFinal(message); + } + + @SuppressFBWarnings( + value = {"CIPHER_INTEGRITY", "STATIC_IV"}, + justification = + "No padding is performed as the size of demPlaintext is required " + + "to be a multiple of the AES block length. The IV is randomly generated " + + "for every encrypt operation") + private Cipher getCipher(int mode, byte[] secret, byte[] iv) { + try { + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + cipher.init(mode, new SecretKeySpec(secret, "AES"), new IvParameterSpec(iv)); + return cipher; + } catch (NoSuchPaddingException + | NoSuchAlgorithmException + | InvalidAlgorithmParameterException + | InvalidKeyException e) { + throw new IllegalStateException(e); } + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/ctap/UnsupportedPinUvAuthProtocolError.java b/fido/src/main/java/com/yubico/yubikit/fido/ctap/UnsupportedPinUvAuthProtocolError.java index 54be71d9..e1ae57d9 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/ctap/UnsupportedPinUvAuthProtocolError.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/ctap/UnsupportedPinUvAuthProtocolError.java @@ -17,7 +17,7 @@ package com.yubico.yubikit.fido.ctap; public class UnsupportedPinUvAuthProtocolError extends RuntimeException { - public UnsupportedPinUvAuthProtocolError() { - super("No supported PIN/UV Auth Protocol"); - } + public UnsupportedPinUvAuthProtocolError() { + super("No supported PIN/UV Auth Protocol"); + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/ctap/UserVerify.java b/fido/src/main/java/com/yubico/yubikit/fido/ctap/UserVerify.java index 8f6fcd21..c370570c 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/ctap/UserVerify.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/ctap/UserVerify.java @@ -19,32 +19,34 @@ import java.util.Locale; /** - * The USER_VERIFY constants are flags in a bitfield represented as a 32 bit long integer. - * They describe the methods and capabilities of a FIDO authenticator for locally verifying a user. + * The USER_VERIFY constants are flags in a bitfield represented as a 32 bit long integer. They + * describe the methods and capabilities of a FIDO authenticator for locally verifying a user. * - * @see User Verification Methods + * @see User + * Verification Methods */ public enum UserVerify { - UNDEFINED(0x0), - PRESENCE_INTERNAL(0x00000001), - FINGERPRINT_INTERNAL(0x00000002), - PASSCODE_INTERNAL(0x00000004), - VOICEPRINT_INTERNAL(0x00000008), - FACEPRINT_INTERNAL(0x00000010), - LOCATION_INTERNAL(0x00000020), - EYEPRINT_INTERNAL(0x00000040), - PATTERN_INTERNAL(0x00000080), - HANDPRINT_INTERNAL(0x00000100), - PASSCODE_EXTERNAL(0x00000800), - PATTERN_EXTERNAL(0x00001000), - NONE(0x00000200), - ALL(0x00000400); + UNDEFINED(0x0), + PRESENCE_INTERNAL(0x00000001), + FINGERPRINT_INTERNAL(0x00000002), + PASSCODE_INTERNAL(0x00000004), + VOICEPRINT_INTERNAL(0x00000008), + FACEPRINT_INTERNAL(0x00000010), + LOCATION_INTERNAL(0x00000020), + EYEPRINT_INTERNAL(0x00000040), + PATTERN_INTERNAL(0x00000080), + HANDPRINT_INTERNAL(0x00000100), + PASSCODE_EXTERNAL(0x00000800), + PATTERN_EXTERNAL(0x00001000), + NONE(0x00000200), + ALL(0x00000400); - public final int value; - public final String name; + public final int value; + public final String name; - UserVerify(int value) { - this.value = value; - this.name = this.name().toLowerCase(Locale.ROOT); - } + UserVerify(int value) { + this.value = value; + this.name = this.name().toLowerCase(Locale.ROOT); + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/ctap/package-info.java b/fido/src/main/java/com/yubico/yubikit/fido/ctap/package-info.java index 1252ff92..1b9c0b1a 100755 --- a/fido/src/main/java/com/yubico/yubikit/fido/ctap/package-info.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/ctap/package-info.java @@ -16,12 +16,12 @@ /** * CTAP2 client implementation. - *

- * Contains classes implementing low level CTAP2 communication. Client implementations should prefer - * to use classes contained in the com.yubico.yubikit.fido.client package, which offer a higher - * level of abstraction. + * + *

Contains classes implementing low level CTAP2 communication. Client implementations should + * prefer to use classes contained in the com.yubico.yubikit.fido.client package, which offer a + * higher level of abstraction. */ @PackageNonnullByDefault package com.yubico.yubikit.fido.ctap; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/fido/src/main/java/com/yubico/yubikit/fido/package-info.java b/fido/src/main/java/com/yubico/yubikit/fido/package-info.java index 3398771f..98f74657 100755 --- a/fido/src/main/java/com/yubico/yubikit/fido/package-info.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/package-info.java @@ -16,10 +16,10 @@ /** * WebAuthn client implementation. - *

- * Contains classes for implementing a WebAuthn client which uses a YubiKeySession. + * + *

Contains classes for implementing a WebAuthn client which uses a YubiKeySession. */ @PackageNonnullByDefault package com.yubico.yubikit.fido; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AttestationConveyancePreference.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AttestationConveyancePreference.java index c7e8b2cc..1104530c 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AttestationConveyancePreference.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AttestationConveyancePreference.java @@ -18,8 +18,8 @@ @SuppressWarnings("unused") public class AttestationConveyancePreference { - public static final String NONE = "none"; - public static final String INDIRECT = "indirect"; - public static final String DIRECT = "direct"; - public static final String ENTERPRISE = "enterprise"; + public static final String NONE = "none"; + public static final String INDIRECT = "indirect"; + public static final String DIRECT = "direct"; + public static final String ENTERPRISE = "enterprise"; } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AttestationObject.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AttestationObject.java index ae244cf3..e9713c1e 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AttestationObject.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AttestationObject.java @@ -18,122 +18,113 @@ import com.yubico.yubikit.fido.Cbor; import com.yubico.yubikit.fido.ctap.Ctap2Session; - import java.nio.ByteBuffer; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; /** * Webauthn AttestationObject which exposes attestation authenticator data. * - * @see WebAuthn Attestation + * @see WebAuthn + * Attestation */ public class AttestationObject { - public static final String KEY_FORMAT = "fmt"; - public static final String KEY_AUTHENTICATOR_DATA = "authData"; - public static final String KEY_ATTESTATION_STATEMENT = "attStmt"; - public static final String KEY_EP_ATT = "epAtt"; - public static final String KEY_LARGE_BLOB_KEY = "largeBlobKey"; - - private final String format; - private final AuthenticatorData authenticatorData; - private final Map attestationStatement; - @Nullable - private final Boolean enterpriseAttestation; - @Nullable - private final byte[] largeBlobKey; - - public AttestationObject( - String format, - AuthenticatorData authenticatorData, - Map attestationStatement, - @Nullable Boolean enterpriseAttestation, - @Nullable byte[] largeBlobKey - ) { - this.format = format; - this.authenticatorData = authenticatorData; - this.attestationStatement = attestationStatement; - this.enterpriseAttestation = enterpriseAttestation; - this.largeBlobKey = largeBlobKey; - } - - static public AttestationObject fromCredential(Ctap2Session.CredentialData credential) { - return new AttestationObject( - credential.getFormat(), - AuthenticatorData.parseFrom(ByteBuffer.wrap(credential.getAuthenticatorData())), - credential.getAttestationStatement(), - credential.getEnterpriseAttestation(), - credential.getLargeBlobKey() - ); - } - - @SuppressWarnings("unused") - public String getFormat() { - return format; + public static final String KEY_FORMAT = "fmt"; + public static final String KEY_AUTHENTICATOR_DATA = "authData"; + public static final String KEY_ATTESTATION_STATEMENT = "attStmt"; + public static final String KEY_EP_ATT = "epAtt"; + public static final String KEY_LARGE_BLOB_KEY = "largeBlobKey"; + + private final String format; + private final AuthenticatorData authenticatorData; + private final Map attestationStatement; + @Nullable private final Boolean enterpriseAttestation; + @Nullable private final byte[] largeBlobKey; + + public AttestationObject( + String format, + AuthenticatorData authenticatorData, + Map attestationStatement, + @Nullable Boolean enterpriseAttestation, + @Nullable byte[] largeBlobKey) { + this.format = format; + this.authenticatorData = authenticatorData; + this.attestationStatement = attestationStatement; + this.enterpriseAttestation = enterpriseAttestation; + this.largeBlobKey = largeBlobKey; + } + + public static AttestationObject fromCredential(Ctap2Session.CredentialData credential) { + return new AttestationObject( + credential.getFormat(), + AuthenticatorData.parseFrom(ByteBuffer.wrap(credential.getAuthenticatorData())), + credential.getAttestationStatement(), + credential.getEnterpriseAttestation(), + credential.getLargeBlobKey()); + } + + @SuppressWarnings("unused") + public String getFormat() { + return format; + } + + public AuthenticatorData getAuthenticatorData() { + return authenticatorData; + } + + @SuppressWarnings("unused") + public Map getAttestationStatement() { + return attestationStatement; + } + + @SuppressWarnings("unused") + @Nullable public Boolean getEnterpriseAttestation() { + return enterpriseAttestation; + } + + @SuppressWarnings("unused") + @Nullable public byte[] getLargeBlobKey() { + return largeBlobKey; + } + + public byte[] toBytes() { + Map attestationObject = new HashMap<>(); + attestationObject.put(AttestationObject.KEY_FORMAT, format); + attestationObject.put(AttestationObject.KEY_AUTHENTICATOR_DATA, authenticatorData.getBytes()); + attestationObject.put(AttestationObject.KEY_ATTESTATION_STATEMENT, attestationStatement); + if (enterpriseAttestation != null) { + attestationObject.put(AttestationObject.KEY_EP_ATT, enterpriseAttestation); } - - public AuthenticatorData getAuthenticatorData() { - return authenticatorData; - } - - @SuppressWarnings("unused") - public Map getAttestationStatement() { - return attestationStatement; - } - - @SuppressWarnings("unused") - @Nullable - public Boolean getEnterpriseAttestation() { - return enterpriseAttestation; - } - - @SuppressWarnings("unused") - @Nullable - public byte[] getLargeBlobKey() { - return largeBlobKey; - } - - public byte[] toBytes() { - Map attestationObject = new HashMap<>(); - attestationObject.put(AttestationObject.KEY_FORMAT, format); - attestationObject.put(AttestationObject.KEY_AUTHENTICATOR_DATA, authenticatorData.getBytes()); - attestationObject.put(AttestationObject.KEY_ATTESTATION_STATEMENT, attestationStatement); - if (enterpriseAttestation != null) { - attestationObject.put(AttestationObject.KEY_EP_ATT, enterpriseAttestation); - } - if (largeBlobKey != null) { - attestationObject.put(AttestationObject.KEY_LARGE_BLOB_KEY, largeBlobKey); - } - return Cbor.encode(attestationObject); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - AttestationObject that = (AttestationObject) o; - - if (!format.equals(that.format)) return false; - if (!authenticatorData.equals(that.authenticatorData)) return false; - if (!Objects.equals(enterpriseAttestation, that.enterpriseAttestation)) return false; - if (!Arrays.equals(largeBlobKey, that.largeBlobKey)) return false; - return Arrays.equals( - Cbor.encode(attestationStatement), - Cbor.encode(that.attestationStatement)); - } - - @Override - public int hashCode() { - int result = format.hashCode(); - result = 31 * result + authenticatorData.hashCode(); - result = 31 * result + Arrays.hashCode(Cbor.encode(attestationStatement)); - result = 31 * result + (enterpriseAttestation != null ? enterpriseAttestation.hashCode() : 0); - result = 31 * result + Arrays.hashCode(largeBlobKey); - return result; + if (largeBlobKey != null) { + attestationObject.put(AttestationObject.KEY_LARGE_BLOB_KEY, largeBlobKey); } + return Cbor.encode(attestationObject); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AttestationObject that = (AttestationObject) o; + + if (!format.equals(that.format)) return false; + if (!authenticatorData.equals(that.authenticatorData)) return false; + if (!Objects.equals(enterpriseAttestation, that.enterpriseAttestation)) return false; + if (!Arrays.equals(largeBlobKey, that.largeBlobKey)) return false; + return Arrays.equals(Cbor.encode(attestationStatement), Cbor.encode(that.attestationStatement)); + } + + @Override + public int hashCode() { + int result = format.hashCode(); + result = 31 * result + authenticatorData.hashCode(); + result = 31 * result + Arrays.hashCode(Cbor.encode(attestationStatement)); + result = 31 * result + (enterpriseAttestation != null ? enterpriseAttestation.hashCode() : 0); + result = 31 * result + Arrays.hashCode(largeBlobKey); + return result; + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AttestedCredentialData.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AttestedCredentialData.java index 03401d2e..1d4470a6 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AttestedCredentialData.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AttestedCredentialData.java @@ -17,7 +17,6 @@ package com.yubico.yubikit.fido.webauthn; import com.yubico.yubikit.fido.Cbor; - import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Map; @@ -25,73 +24,67 @@ /** * Webauthn AttestedCredentialData structure * - * @see WebAuthn Attested Credential Data + * @see WebAuthn + * Attested Credential Data */ public class AttestedCredentialData { - private final byte[] aaguid; - private final byte[] credentialId; - private final Map cosePublicKey; - - public AttestedCredentialData( - byte[] aaguid, - byte[] credentialId, - Map cosePublicKey - ) { - this.aaguid = aaguid; - this.credentialId = credentialId; - this.cosePublicKey = cosePublicKey; - } + private final byte[] aaguid; + private final byte[] credentialId; + private final Map cosePublicKey; - @SuppressWarnings("unchecked") - public static AttestedCredentialData parseFrom(ByteBuffer buffer) { - final byte[] aaguid = new byte[16]; - buffer.get(aaguid); - final byte[] credentialId = new byte[buffer.getShort()]; - buffer.get(credentialId); - Map cosePublicKey = (Map) Cbor.decodeFrom(buffer); - if (cosePublicKey == null) { - throw new IllegalArgumentException("Invalid public key data"); - } + public AttestedCredentialData(byte[] aaguid, byte[] credentialId, Map cosePublicKey) { + this.aaguid = aaguid; + this.credentialId = credentialId; + this.cosePublicKey = cosePublicKey; + } - return new AttestedCredentialData( - aaguid, - credentialId, - cosePublicKey - ); + @SuppressWarnings("unchecked") + public static AttestedCredentialData parseFrom(ByteBuffer buffer) { + final byte[] aaguid = new byte[16]; + buffer.get(aaguid); + final byte[] credentialId = new byte[buffer.getShort()]; + buffer.get(credentialId); + Map cosePublicKey = (Map) Cbor.decodeFrom(buffer); + if (cosePublicKey == null) { + throw new IllegalArgumentException("Invalid public key data"); } - @SuppressWarnings("unused") - public byte[] getAaguid() { - return aaguid; - } + return new AttestedCredentialData(aaguid, credentialId, cosePublicKey); + } - @SuppressWarnings("unused") - public byte[] getCredentialId() { - return credentialId; - } + @SuppressWarnings("unused") + public byte[] getAaguid() { + return aaguid; + } - @SuppressWarnings("unused") - public Map getCosePublicKey() { - return cosePublicKey; - } + @SuppressWarnings("unused") + public byte[] getCredentialId() { + return credentialId; + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unused") + public Map getCosePublicKey() { + return cosePublicKey; + } - AttestedCredentialData that = (AttestedCredentialData) o; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; - if (!Arrays.equals(aaguid, that.aaguid)) return false; - if (!Arrays.equals(credentialId, that.credentialId)) return false; - return Arrays.equals(Cbor.encode(cosePublicKey), Cbor.encode(that.cosePublicKey)); - } + AttestedCredentialData that = (AttestedCredentialData) o; - @Override - public int hashCode() { - int result = Arrays.hashCode(aaguid); - result = 31 * result + Arrays.hashCode(credentialId); - result = 31 * result + Arrays.hashCode(Cbor.encode(cosePublicKey)); - return result; - } + if (!Arrays.equals(aaguid, that.aaguid)) return false; + if (!Arrays.equals(credentialId, that.credentialId)) return false; + return Arrays.equals(Cbor.encode(cosePublicKey), Cbor.encode(that.cosePublicKey)); + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(aaguid); + result = 31 * result + Arrays.hashCode(credentialId); + result = 31 * result + Arrays.hashCode(Cbor.encode(cosePublicKey)); + return result; + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorAssertionResponse.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorAssertionResponse.java index 320a979f..7dfa14ef 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorAssertionResponse.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorAssertionResponse.java @@ -23,84 +23,78 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; public class AuthenticatorAssertionResponse extends AuthenticatorResponse { - public static final String AUTHENTICATOR_DATA = "authenticatorData"; - public static final String SIGNATURE = "signature"; - public static final String USER_HANDLE = "userHandle"; - - private final byte[] authenticatorData; - private final byte[] signature; - @Nullable - private final byte[] userHandle; - - public AuthenticatorAssertionResponse( - byte[] clientDataJson, - byte[] authenticatorData, - byte[] signature, - @Nullable byte[] userHandle - ) { - super(clientDataJson); - this.authenticatorData = authenticatorData; - this.signature = signature; - this.userHandle = userHandle; - } - - public byte[] getAuthenticatorData() { - return authenticatorData; - } - - public byte[] getSignature() { - return signature; - } - - @Nullable - public byte[] getUserHandle() { - return userHandle; - } - - @Override - public Map toMap(SerializationType serializationType) { - Map map = new HashMap<>(); - map.put(CLIENT_DATA_JSON, serializeBytes(getClientDataJson(), serializationType)); - map.put(AUTHENTICATOR_DATA, serializeBytes(authenticatorData, serializationType)); - map.put(SIGNATURE, serializeBytes(signature, serializationType)); - if (userHandle != null) { - map.put(USER_HANDLE, serializeBytes(userHandle, serializationType)); - } - return map; - } - - public static AuthenticatorAssertionResponse fromMap( - Map map, - SerializationType serializationType - ) { - return new AuthenticatorAssertionResponse( - deserializeBytes(Objects.requireNonNull(map.get(CLIENT_DATA_JSON)), serializationType), - deserializeBytes(Objects.requireNonNull(map.get(AUTHENTICATOR_DATA)), serializationType), - deserializeBytes(Objects.requireNonNull(map.get(SIGNATURE)), serializationType), - deserializeBytes(map.get(USER_HANDLE), serializationType)); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - AuthenticatorAssertionResponse that = (AuthenticatorAssertionResponse) o; - - if (!Arrays.equals(authenticatorData, that.authenticatorData)) return false; - if (!Arrays.equals(signature, that.signature)) return false; - return Arrays.equals(userHandle, that.userHandle); - } - - @Override - public int hashCode() { - int result = Arrays.hashCode(authenticatorData); - result = 31 * result + Arrays.hashCode(signature); - result = 31 * result + Arrays.hashCode(userHandle); - return result; + public static final String AUTHENTICATOR_DATA = "authenticatorData"; + public static final String SIGNATURE = "signature"; + public static final String USER_HANDLE = "userHandle"; + + private final byte[] authenticatorData; + private final byte[] signature; + @Nullable private final byte[] userHandle; + + public AuthenticatorAssertionResponse( + byte[] clientDataJson, + byte[] authenticatorData, + byte[] signature, + @Nullable byte[] userHandle) { + super(clientDataJson); + this.authenticatorData = authenticatorData; + this.signature = signature; + this.userHandle = userHandle; + } + + public byte[] getAuthenticatorData() { + return authenticatorData; + } + + public byte[] getSignature() { + return signature; + } + + @Nullable public byte[] getUserHandle() { + return userHandle; + } + + @Override + public Map toMap(SerializationType serializationType) { + Map map = new HashMap<>(); + map.put(CLIENT_DATA_JSON, serializeBytes(getClientDataJson(), serializationType)); + map.put(AUTHENTICATOR_DATA, serializeBytes(authenticatorData, serializationType)); + map.put(SIGNATURE, serializeBytes(signature, serializationType)); + if (userHandle != null) { + map.put(USER_HANDLE, serializeBytes(userHandle, serializationType)); } + return map; + } + + public static AuthenticatorAssertionResponse fromMap( + Map map, SerializationType serializationType) { + return new AuthenticatorAssertionResponse( + deserializeBytes(Objects.requireNonNull(map.get(CLIENT_DATA_JSON)), serializationType), + deserializeBytes(Objects.requireNonNull(map.get(AUTHENTICATOR_DATA)), serializationType), + deserializeBytes(Objects.requireNonNull(map.get(SIGNATURE)), serializationType), + deserializeBytes(map.get(USER_HANDLE), serializationType)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AuthenticatorAssertionResponse that = (AuthenticatorAssertionResponse) o; + + if (!Arrays.equals(authenticatorData, that.authenticatorData)) return false; + if (!Arrays.equals(signature, that.signature)) return false; + return Arrays.equals(userHandle, that.userHandle); + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(authenticatorData); + result = 31 * result + Arrays.hashCode(signature); + result = 31 * result + Arrays.hashCode(userHandle); + return result; + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorAttachment.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorAttachment.java index 6483d933..80d8960e 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorAttachment.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorAttachment.java @@ -18,6 +18,6 @@ @SuppressWarnings("unused") public class AuthenticatorAttachment { - public static final String PLATFORM = "platform"; - public static final String CROSS_PLATFORM = "cross-platform"; + public static final String PLATFORM = "platform"; + public static final String CROSS_PLATFORM = "cross-platform"; } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorAttestationResponse.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorAttestationResponse.java index 04365797..71aca55c 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorAttestationResponse.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorAttestationResponse.java @@ -21,9 +21,6 @@ import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.fido.Cose; - -import org.slf4j.LoggerFactory; - import java.nio.ByteBuffer; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; @@ -33,152 +30,144 @@ import java.util.List; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; +import org.slf4j.LoggerFactory; public class AuthenticatorAttestationResponse extends AuthenticatorResponse { - public static final String ATTESTATION_OBJECT = "attestationObject"; - public static final String TRANSPORTS = "transports"; - public static final String AUTHENTICATOR_DATA = "authenticatorData"; - public static final String PUBLIC_KEY = "publicKey"; - public static final String PUBLIC_KEY_ALGORITHM = "publicKeyAlgorithm"; - - private final AuthenticatorData authenticatorData; - private final List transports; - @Nullable - private final byte[] publicKey; - private final Integer publicKeyAlgorithm; - private final byte[] attestationObject; - - private static final org.slf4j.Logger logger = LoggerFactory.getLogger( - AuthenticatorAttestationResponse.class - ); - - public AuthenticatorAttestationResponse( - byte[] clientDataJson, - AuthenticatorData authenticatorData, - List transports, - @Nullable byte[] publicKey, - int publicKeyAlgorithm, - byte[] attestationObject - ) { - super(clientDataJson); - this.transports = transports; - this.attestationObject = attestationObject; - - this.authenticatorData = authenticatorData; - this.publicKey = publicKey; - this.publicKeyAlgorithm = publicKeyAlgorithm; - } - - public AuthenticatorAttestationResponse( - byte[] clientDataJson, - List transports, - AttestationObject attestationObject - ) { - super(clientDataJson); - this.authenticatorData = attestationObject.getAuthenticatorData(); - this.transports = transports; - this.attestationObject = attestationObject.toBytes(); - - if (!authenticatorData.isAt()) { - throw new IllegalArgumentException("Invalid attestation for makeCredential"); - } - - AttestedCredentialData attestedCredentialData = - Objects.requireNonNull(authenticatorData.getAttestedCredentialData()); - - // compute public key information - Map cosePublicKey = attestedCredentialData.getCosePublicKey(); - this.publicKeyAlgorithm = Cose.getAlgorithm(cosePublicKey); - byte[] resultPublicKey = null; - try { - PublicKey publicKey = Cose.getPublicKey(cosePublicKey); - resultPublicKey = publicKey == null - ? null - : publicKey.getEncoded(); - } catch (InvalidKeySpecException | NoSuchAlgorithmException exception) { - // library does not support this public key format - Logger.info(logger, "Platform does not support binary serialization of the given key" + - " type, the 'publicKey' field will be null."); - } - this.publicKey = resultPublicKey; - } - - @SuppressWarnings("unused") - public AuthenticatorData getAuthenticatorData() { - return authenticatorData; - } - - public List getTransports() { - return transports; - } - - @Nullable - @SuppressWarnings("unused") - public byte[] getPublicKey() { - return publicKey; - } - - @SuppressWarnings("unused") - public Integer getPublicKeyAlgorithm() { - return publicKeyAlgorithm; - } - - public byte[] getAttestationObject() { - return attestationObject; - } - - @Override - public Map toMap(SerializationType serializationType) { - Map map = new HashMap<>(); - map.put(CLIENT_DATA_JSON, serializeBytes(getClientDataJson(), serializationType)); - map.put(AUTHENTICATOR_DATA, serializeBytes(authenticatorData.getBytes(), serializationType)); - map.put(TRANSPORTS, transports); - if (publicKey != null) { - map.put(PUBLIC_KEY, serializeBytes(publicKey, serializationType)); - } - map.put(PUBLIC_KEY_ALGORITHM, publicKeyAlgorithm); - map.put(ATTESTATION_OBJECT, serializeBytes(attestationObject, serializationType)); - return map; - } - - @SuppressWarnings("unchecked") - public static AuthenticatorAttestationResponse fromMap(Map map, SerializationType serializationType) { - Object publicKey = map.get(PUBLIC_KEY); - return new AuthenticatorAttestationResponse( - deserializeBytes(Objects.requireNonNull(map.get(CLIENT_DATA_JSON)), serializationType), - AuthenticatorData.parseFrom( - ByteBuffer.wrap( - deserializeBytes(map.get(AUTHENTICATOR_DATA), serializationType))), - (List) Objects.requireNonNull(map.get(TRANSPORTS)), - publicKey == null ? null : deserializeBytes(publicKey, serializationType), - (Integer) map.get(PUBLIC_KEY_ALGORITHM), - deserializeBytes(Objects.requireNonNull(map.get(ATTESTATION_OBJECT)), serializationType) - ); + public static final String ATTESTATION_OBJECT = "attestationObject"; + public static final String TRANSPORTS = "transports"; + public static final String AUTHENTICATOR_DATA = "authenticatorData"; + public static final String PUBLIC_KEY = "publicKey"; + public static final String PUBLIC_KEY_ALGORITHM = "publicKeyAlgorithm"; + + private final AuthenticatorData authenticatorData; + private final List transports; + @Nullable private final byte[] publicKey; + private final Integer publicKeyAlgorithm; + private final byte[] attestationObject; + + private static final org.slf4j.Logger logger = + LoggerFactory.getLogger(AuthenticatorAttestationResponse.class); + + public AuthenticatorAttestationResponse( + byte[] clientDataJson, + AuthenticatorData authenticatorData, + List transports, + @Nullable byte[] publicKey, + int publicKeyAlgorithm, + byte[] attestationObject) { + super(clientDataJson); + this.transports = transports; + this.attestationObject = attestationObject; + + this.authenticatorData = authenticatorData; + this.publicKey = publicKey; + this.publicKeyAlgorithm = publicKeyAlgorithm; + } + + public AuthenticatorAttestationResponse( + byte[] clientDataJson, List transports, AttestationObject attestationObject) { + super(clientDataJson); + this.authenticatorData = attestationObject.getAuthenticatorData(); + this.transports = transports; + this.attestationObject = attestationObject.toBytes(); + + if (!authenticatorData.isAt()) { + throw new IllegalArgumentException("Invalid attestation for makeCredential"); } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - AuthenticatorAttestationResponse that = (AuthenticatorAttestationResponse) o; - - if (!authenticatorData.equals(that.authenticatorData)) return false; - if (!transports.equals(that.transports)) return false; - if (!Arrays.equals(publicKey, that.publicKey)) return false; - if (!publicKeyAlgorithm.equals(that.publicKeyAlgorithm)) return false; - return Arrays.equals(attestationObject, that.attestationObject); + AttestedCredentialData attestedCredentialData = + Objects.requireNonNull(authenticatorData.getAttestedCredentialData()); + + // compute public key information + Map cosePublicKey = attestedCredentialData.getCosePublicKey(); + this.publicKeyAlgorithm = Cose.getAlgorithm(cosePublicKey); + byte[] resultPublicKey = null; + try { + PublicKey publicKey = Cose.getPublicKey(cosePublicKey); + resultPublicKey = publicKey == null ? null : publicKey.getEncoded(); + } catch (InvalidKeySpecException | NoSuchAlgorithmException exception) { + // library does not support this public key format + Logger.info( + logger, + "Platform does not support binary serialization of the given key" + + " type, the 'publicKey' field will be null."); } - - @Override - public int hashCode() { - int result = authenticatorData.hashCode(); - result = 31 * result + transports.hashCode(); - result = 31 * result + Arrays.hashCode(publicKey); - result = 31 * result + publicKeyAlgorithm.hashCode(); - result = 31 * result + Arrays.hashCode(attestationObject); - return result; + this.publicKey = resultPublicKey; + } + + @SuppressWarnings("unused") + public AuthenticatorData getAuthenticatorData() { + return authenticatorData; + } + + public List getTransports() { + return transports; + } + + @Nullable @SuppressWarnings("unused") + public byte[] getPublicKey() { + return publicKey; + } + + @SuppressWarnings("unused") + public Integer getPublicKeyAlgorithm() { + return publicKeyAlgorithm; + } + + public byte[] getAttestationObject() { + return attestationObject; + } + + @Override + public Map toMap(SerializationType serializationType) { + Map map = new HashMap<>(); + map.put(CLIENT_DATA_JSON, serializeBytes(getClientDataJson(), serializationType)); + map.put(AUTHENTICATOR_DATA, serializeBytes(authenticatorData.getBytes(), serializationType)); + map.put(TRANSPORTS, transports); + if (publicKey != null) { + map.put(PUBLIC_KEY, serializeBytes(publicKey, serializationType)); } + map.put(PUBLIC_KEY_ALGORITHM, publicKeyAlgorithm); + map.put(ATTESTATION_OBJECT, serializeBytes(attestationObject, serializationType)); + return map; + } + + @SuppressWarnings("unchecked") + public static AuthenticatorAttestationResponse fromMap( + Map map, SerializationType serializationType) { + Object publicKey = map.get(PUBLIC_KEY); + return new AuthenticatorAttestationResponse( + deserializeBytes(Objects.requireNonNull(map.get(CLIENT_DATA_JSON)), serializationType), + AuthenticatorData.parseFrom( + ByteBuffer.wrap(deserializeBytes(map.get(AUTHENTICATOR_DATA), serializationType))), + (List) Objects.requireNonNull(map.get(TRANSPORTS)), + publicKey == null ? null : deserializeBytes(publicKey, serializationType), + (Integer) map.get(PUBLIC_KEY_ALGORITHM), + deserializeBytes(Objects.requireNonNull(map.get(ATTESTATION_OBJECT)), serializationType)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AuthenticatorAttestationResponse that = (AuthenticatorAttestationResponse) o; + + if (!authenticatorData.equals(that.authenticatorData)) return false; + if (!transports.equals(that.transports)) return false; + if (!Arrays.equals(publicKey, that.publicKey)) return false; + if (!publicKeyAlgorithm.equals(that.publicKeyAlgorithm)) return false; + return Arrays.equals(attestationObject, that.attestationObject); + } + + @Override + public int hashCode() { + int result = authenticatorData.hashCode(); + result = 31 * result + transports.hashCode(); + result = 31 * result + Arrays.hashCode(publicKey); + result = 31 * result + publicKeyAlgorithm.hashCode(); + result = 31 * result + Arrays.hashCode(attestationObject); + return result; + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorData.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorData.java index 7b3ffc4c..6b89caa7 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorData.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorData.java @@ -17,191 +17,178 @@ package com.yubico.yubikit.fido.webauthn; import com.yubico.yubikit.fido.Cbor; - import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; /** * Webauthn AuthenticatorData class * - * @see WebAuthn Authenticator Data + * @see WebAuthn + * Authenticator Data */ public class AuthenticatorData { - @SuppressWarnings("unused") - public static final int FLAG_UP = 0x00; - @SuppressWarnings("unused") - public static final int FLAG_UV = 0x02; - public static final int FLAG_AT = 0x06; - public static final int FLAG_ED = 0x07; - - private final byte[] rpIdHash; - private final byte flags; - private final int signCount; - - @Nullable - private final AttestedCredentialData attestedCredentialData; - @Nullable - private final Map extensions; - - private final byte[] rawData; - - private static boolean getFlag(byte flags, int bitIndex) { - return (flags >> bitIndex & 1) == 1; - } - - public AuthenticatorData( - byte[] rpIdHash, - byte flags, - int signCount, - @Nullable AttestedCredentialData attestedCredentialData, - @Nullable Map extensions, - byte[] rawData - ) { - this.rpIdHash = rpIdHash; - this.flags = flags; - this.signCount = signCount; - this.attestedCredentialData = attestedCredentialData; - this.extensions = extensions; - this.rawData = rawData; - } - - @SuppressWarnings("unchecked") - public static AuthenticatorData parseFrom(ByteBuffer buffer) { - int startPos = buffer.position(); - final byte[] rpIdHash = new byte[32]; - buffer.get(rpIdHash); - final byte flags = buffer.get(); - final int signCount = buffer.order(ByteOrder.BIG_ENDIAN).getInt(); - - boolean flagAT = getFlag(flags, FLAG_AT); - boolean flagED = getFlag(flags, FLAG_ED); - - AttestedCredentialData attestedCredentialData = flagAT - ? AttestedCredentialData.parseFrom(buffer) - : null; - - if (!flagED && buffer.hasRemaining()) { - throw new IllegalArgumentException("Unexpected extensions data"); - } - - if (flagED && !buffer.hasRemaining()) { - throw new IllegalArgumentException("Missing extensions data"); - } - - Map extensions = flagED - ? (Map) Cbor.decodeFrom(buffer) - : null; - - // there should not be anything more in the buffer at this point - if (buffer.hasRemaining()) { - throw new IllegalArgumentException("Unexpected data in authenticatorData"); - } - - byte[] originalData = new byte[buffer.position() - startPos]; - buffer.position(startPos); - buffer.get(originalData); - - return new AuthenticatorData( - rpIdHash, - flags, - signCount, - attestedCredentialData, - extensions, - originalData - ); + @SuppressWarnings("unused") + public static final int FLAG_UP = 0x00; + + @SuppressWarnings("unused") + public static final int FLAG_UV = 0x02; + + public static final int FLAG_AT = 0x06; + public static final int FLAG_ED = 0x07; + + private final byte[] rpIdHash; + private final byte flags; + private final int signCount; + + @Nullable private final AttestedCredentialData attestedCredentialData; + @Nullable private final Map extensions; + + private final byte[] rawData; + + private static boolean getFlag(byte flags, int bitIndex) { + return (flags >> bitIndex & 1) == 1; + } + + public AuthenticatorData( + byte[] rpIdHash, + byte flags, + int signCount, + @Nullable AttestedCredentialData attestedCredentialData, + @Nullable Map extensions, + byte[] rawData) { + this.rpIdHash = rpIdHash; + this.flags = flags; + this.signCount = signCount; + this.attestedCredentialData = attestedCredentialData; + this.extensions = extensions; + this.rawData = rawData; + } + + @SuppressWarnings("unchecked") + public static AuthenticatorData parseFrom(ByteBuffer buffer) { + int startPos = buffer.position(); + final byte[] rpIdHash = new byte[32]; + buffer.get(rpIdHash); + final byte flags = buffer.get(); + final int signCount = buffer.order(ByteOrder.BIG_ENDIAN).getInt(); + + boolean flagAT = getFlag(flags, FLAG_AT); + boolean flagED = getFlag(flags, FLAG_ED); + + AttestedCredentialData attestedCredentialData = + flagAT ? AttestedCredentialData.parseFrom(buffer) : null; + + if (!flagED && buffer.hasRemaining()) { + throw new IllegalArgumentException("Unexpected extensions data"); } - @SuppressWarnings("unused") - public byte[] getRpIdHash() { - return rpIdHash; + if (flagED && !buffer.hasRemaining()) { + throw new IllegalArgumentException("Missing extensions data"); } - @SuppressWarnings("unused") - public byte getFlags() { - return flags; - } + Map extensions = flagED ? (Map) Cbor.decodeFrom(buffer) : null; - @SuppressWarnings("unused") - public int getSignCount() { - return signCount; + // there should not be anything more in the buffer at this point + if (buffer.hasRemaining()) { + throw new IllegalArgumentException("Unexpected data in authenticatorData"); } - @Nullable - @SuppressWarnings("unused") - public AttestedCredentialData getAttestedCredentialData() { - return attestedCredentialData; + byte[] originalData = new byte[buffer.position() - startPos]; + buffer.position(startPos); + buffer.get(originalData); + + return new AuthenticatorData( + rpIdHash, flags, signCount, attestedCredentialData, extensions, originalData); + } + + @SuppressWarnings("unused") + public byte[] getRpIdHash() { + return rpIdHash; + } + + @SuppressWarnings("unused") + public byte getFlags() { + return flags; + } + + @SuppressWarnings("unused") + public int getSignCount() { + return signCount; + } + + @Nullable @SuppressWarnings("unused") + public AttestedCredentialData getAttestedCredentialData() { + return attestedCredentialData; + } + + @Nullable @SuppressWarnings("unused") + public Map getExtensions() { + return extensions; + } + + @SuppressWarnings("unused") + public boolean isUp() { + return getFlag(flags, FLAG_UP); + } + + @SuppressWarnings("unused") + public boolean isUv() { + return getFlag(flags, FLAG_UV); + } + + @SuppressWarnings("unused") + public boolean isAt() { + return getFlag(flags, FLAG_AT); + } + + @SuppressWarnings("unused") + public boolean isEd() { + return getFlag(flags, FLAG_ED); + } + + @SuppressWarnings("unused") + public byte[] getBytes() { + return rawData; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AuthenticatorData that = (AuthenticatorData) o; + + if (flags != that.flags) return false; + if (signCount != that.signCount) return false; + if (!Arrays.equals(rpIdHash, that.rpIdHash)) return false; + if (!Objects.equals(attestedCredentialData, that.attestedCredentialData)) { + return false; } - - @Nullable - @SuppressWarnings("unused") - public Map getExtensions() { - return extensions; - } - - @SuppressWarnings("unused") - public boolean isUp() { - return getFlag(flags, FLAG_UP); + if (extensions != null && that.extensions != null) { + if (!Arrays.equals(Cbor.encode(extensions), Cbor.encode(that.extensions))) return false; } - @SuppressWarnings("unused") - public boolean isUv() { - return getFlag(flags, FLAG_UV); + if ((extensions != null && that.extensions == null) + || (extensions == null && that.extensions != null)) { + return false; } - @SuppressWarnings("unused") - public boolean isAt() { - return getFlag(flags, FLAG_AT); - } - - @SuppressWarnings("unused") - public boolean isEd() { - return getFlag(flags, FLAG_ED); - } - - @SuppressWarnings("unused") - public byte[] getBytes() { - return rawData; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - AuthenticatorData that = (AuthenticatorData) o; - - if (flags != that.flags) return false; - if (signCount != that.signCount) return false; - if (!Arrays.equals(rpIdHash, that.rpIdHash)) return false; - if (!Objects.equals(attestedCredentialData, that.attestedCredentialData)) { - return false; - } - if (extensions != null && that.extensions != null) { - if (!Arrays.equals(Cbor.encode(extensions), Cbor.encode(that.extensions))) - return false; - } - - if ((extensions != null && that.extensions == null) || - (extensions == null && that.extensions != null)) { - return false; - } - - return Arrays.equals(rawData, that.rawData); - } - - @Override - public int hashCode() { - int result = Arrays.hashCode(rpIdHash); - result = 31 * result + (int) flags; - result = 31 * result + signCount; - result = 31 * result + (attestedCredentialData != null ? attestedCredentialData.hashCode() : 0); - result = 31 * result + (extensions != null ? Arrays.hashCode(Cbor.encode(extensions)) : 0); - result = 31 * result + Arrays.hashCode(rawData); - return result; - } -} \ No newline at end of file + return Arrays.equals(rawData, that.rawData); + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(rpIdHash); + result = 31 * result + (int) flags; + result = 31 * result + signCount; + result = 31 * result + (attestedCredentialData != null ? attestedCredentialData.hashCode() : 0); + result = 31 * result + (extensions != null ? Arrays.hashCode(Cbor.encode(extensions)) : 0); + result = 31 * result + Arrays.hashCode(rawData); + return result; + } +} diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorResponse.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorResponse.java index e0bed284..d6d64484 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorResponse.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorResponse.java @@ -19,17 +19,17 @@ import java.util.Map; public abstract class AuthenticatorResponse { - static final String CLIENT_DATA_JSON = "clientDataJSON"; + static final String CLIENT_DATA_JSON = "clientDataJSON"; - private final byte[] clientDataJson; + private final byte[] clientDataJson; - AuthenticatorResponse(byte[] clientDataJson) { - this.clientDataJson = clientDataJson; - } + AuthenticatorResponse(byte[] clientDataJson) { + this.clientDataJson = clientDataJson; + } - public byte[] getClientDataJson() { - return clientDataJson; - } + public byte[] getClientDataJson() { + return clientDataJson; + } - public abstract Map toMap(SerializationType serializationType); + public abstract Map toMap(SerializationType serializationType); } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorSelectionCriteria.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorSelectionCriteria.java index 911a40da..4f400504 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorSelectionCriteria.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/AuthenticatorSelectionCriteria.java @@ -16,103 +16,94 @@ package com.yubico.yubikit.fido.webauthn; -import javax.annotation.Nullable; - import java.util.HashMap; import java.util.Map; import java.util.Objects; +import javax.annotation.Nullable; public class AuthenticatorSelectionCriteria { - private static final String AUTHENTICATOR_ATTACHMENT = "authenticatorAttachment"; - private static final String RESIDENT_KEY = "residentKey"; - private static final String REQUIRE_RESIDENT_KEY = "requireResidentKey"; - private static final String USER_VERIFICATION = "userVerification"; + private static final String AUTHENTICATOR_ATTACHMENT = "authenticatorAttachment"; + private static final String RESIDENT_KEY = "residentKey"; + private static final String REQUIRE_RESIDENT_KEY = "requireResidentKey"; + private static final String USER_VERIFICATION = "userVerification"; - @Nullable - private final String authenticatorAttachment; - @Nullable - private final String residentKey; - private final boolean requireResidentKey; - private final String userVerification; + @Nullable private final String authenticatorAttachment; + @Nullable private final String residentKey; + private final boolean requireResidentKey; + private final String userVerification; - public AuthenticatorSelectionCriteria( - @Nullable String authenticatorAttachment, - @Nullable String residentKey, - @Nullable String userVerification - ) { - this.authenticatorAttachment = authenticatorAttachment; - this.residentKey = residentKey; - this.requireResidentKey = ResidentKeyRequirement.REQUIRED.equals(residentKey); - this.userVerification = userVerification != null ? userVerification : UserVerificationRequirement.PREFERRED; - } + public AuthenticatorSelectionCriteria( + @Nullable String authenticatorAttachment, + @Nullable String residentKey, + @Nullable String userVerification) { + this.authenticatorAttachment = authenticatorAttachment; + this.residentKey = residentKey; + this.requireResidentKey = ResidentKeyRequirement.REQUIRED.equals(residentKey); + this.userVerification = + userVerification != null ? userVerification : UserVerificationRequirement.PREFERRED; + } - @Nullable - public String getAuthenticatorAttachment() { - return authenticatorAttachment; - } + @Nullable public String getAuthenticatorAttachment() { + return authenticatorAttachment; + } - @Nullable - public String getResidentKey() { - return residentKey; - } + @Nullable public String getResidentKey() { + return residentKey; + } - public String getUserVerification() { - return userVerification; - } + public String getUserVerification() { + return userVerification; + } - public Map toMap(@SuppressWarnings("unused") SerializationType serializationType) { - Map map = new HashMap<>(); - if (authenticatorAttachment != null) { - map.put(AUTHENTICATOR_ATTACHMENT, authenticatorAttachment); - } - if (residentKey != null) { - map.put(RESIDENT_KEY, residentKey); - } - map.put(REQUIRE_RESIDENT_KEY, requireResidentKey); - map.put(USER_VERIFICATION, userVerification); - return map; + public Map toMap(@SuppressWarnings("unused") SerializationType serializationType) { + Map map = new HashMap<>(); + if (authenticatorAttachment != null) { + map.put(AUTHENTICATOR_ATTACHMENT, authenticatorAttachment); + } + if (residentKey != null) { + map.put(RESIDENT_KEY, residentKey); } + map.put(REQUIRE_RESIDENT_KEY, requireResidentKey); + map.put(USER_VERIFICATION, userVerification); + return map; + } - public static AuthenticatorSelectionCriteria fromMap( - Map map, - @SuppressWarnings("unused") SerializationType serializationType) { - String residentKeyRequirement = (String) map.get(RESIDENT_KEY); - if (residentKeyRequirement == null) { - // Backwards compatibility with WebAuthn level 1 - if(Boolean.TRUE.equals(map.get(REQUIRE_RESIDENT_KEY))) { - residentKeyRequirement = ResidentKeyRequirement.REQUIRED; - } else { - residentKeyRequirement = ResidentKeyRequirement.DISCOURAGED; - } - } - return new AuthenticatorSelectionCriteria( - (String) map.get(AUTHENTICATOR_ATTACHMENT), - residentKeyRequirement, - (String) map.get(USER_VERIFICATION) - ); + public static AuthenticatorSelectionCriteria fromMap( + Map map, @SuppressWarnings("unused") SerializationType serializationType) { + String residentKeyRequirement = (String) map.get(RESIDENT_KEY); + if (residentKeyRequirement == null) { + // Backwards compatibility with WebAuthn level 1 + if (Boolean.TRUE.equals(map.get(REQUIRE_RESIDENT_KEY))) { + residentKeyRequirement = ResidentKeyRequirement.REQUIRED; + } else { + residentKeyRequirement = ResidentKeyRequirement.DISCOURAGED; + } } + return new AuthenticatorSelectionCriteria( + (String) map.get(AUTHENTICATOR_ATTACHMENT), + residentKeyRequirement, + (String) map.get(USER_VERIFICATION)); + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; - AuthenticatorSelectionCriteria that = (AuthenticatorSelectionCriteria) o; + AuthenticatorSelectionCriteria that = (AuthenticatorSelectionCriteria) o; - if (requireResidentKey != that.requireResidentKey) return false; - if (!Objects.equals(authenticatorAttachment, that.authenticatorAttachment)) - return false; - if (!Objects.equals(residentKey, that.residentKey)) - return false; - return userVerification.equals(that.userVerification); - } + if (requireResidentKey != that.requireResidentKey) return false; + if (!Objects.equals(authenticatorAttachment, that.authenticatorAttachment)) return false; + if (!Objects.equals(residentKey, that.residentKey)) return false; + return userVerification.equals(that.userVerification); + } - @Override - public int hashCode() { - int result = authenticatorAttachment != null ? authenticatorAttachment.hashCode() : 0; - result = 31 * result + (residentKey != null ? residentKey.hashCode() : 0); - result = 31 * result + (requireResidentKey ? 1 : 0); - result = 31 * result + userVerification.hashCode(); - return result; - } + @Override + public int hashCode() { + int result = authenticatorAttachment != null ? authenticatorAttachment.hashCode() : 0; + result = 31 * result + (residentKey != null ? residentKey.hashCode() : 0); + result = 31 * result + (requireResidentKey ? 1 : 0); + result = 31 * result + userVerification.hashCode(); + return result; + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/ClientExtensionResultProvider.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/ClientExtensionResultProvider.java index c83a072b..854af53c 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/ClientExtensionResultProvider.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/ClientExtensionResultProvider.java @@ -19,5 +19,5 @@ import java.util.Map; public interface ClientExtensionResultProvider { - Map getClientExtensionResult(SerializationType serializationType); + Map getClientExtensionResult(SerializationType serializationType); } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/ClientExtensionResults.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/ClientExtensionResults.java index c72b3a80..e32fed7a 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/ClientExtensionResults.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/ClientExtensionResults.java @@ -23,17 +23,17 @@ public class ClientExtensionResults { - final private List resultProviders = new ArrayList<>(); + private final List resultProviders = new ArrayList<>(); - public void add(ClientExtensionResultProvider resultProvider) { - resultProviders.add(resultProvider); - } + public void add(ClientExtensionResultProvider resultProvider) { + resultProviders.add(resultProvider); + } - public Map toMap(SerializationType serializationType) { - Map map = new HashMap<>(); - for (ClientExtensionResultProvider resultProvider : resultProviders) { - map.putAll(resultProvider.getClientExtensionResult(serializationType)); - } - return map; + public Map toMap(SerializationType serializationType) { + Map map = new HashMap<>(); + for (ClientExtensionResultProvider resultProvider : resultProviders) { + map.putAll(resultProvider.getClientExtensionResult(serializationType)); } + return map; + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/Credential.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/Credential.java index f78a0cff..67a25f20 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/Credential.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/Credential.java @@ -17,29 +17,29 @@ package com.yubico.yubikit.fido.webauthn; public class Credential { - public static final String ID = "id"; - public static final String TYPE = "type"; + public static final String ID = "id"; + public static final String TYPE = "type"; - private final String id; - private final String type; + private final String id; + private final String type; - /** - * Webauthn Credential interface - * - * @param id The credential’s identifier. The requirements for the identifier are distinct - * for each type of credential. - * @param type Specifies the credential type represented by this object - */ - public Credential(String id, String type) { - this.id = id; - this.type = type; - } + /** + * Webauthn Credential interface + * + * @param id The credential’s identifier. The requirements for the identifier are distinct for + * each type of credential. + * @param type Specifies the credential type represented by this object + */ + public Credential(String id, String type) { + this.id = id; + this.type = type; + } - public String getId() { - return id; - } + public String getId() { + return id; + } - public String getType() { - return type; - } + public String getType() { + return type; + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/Extensions.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/Extensions.java index bcaf6dc2..74a131b6 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/Extensions.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/Extensions.java @@ -16,47 +16,40 @@ package com.yubico.yubikit.fido.webauthn; -import java.util.Collections; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; public class Extensions { - @Nullable - public static Extensions fromMap(@Nullable Map input) { - return input != null ? new Extensions(input) : null; - } - - @Nullable - private final Map extensions; - - private Extensions(@Nullable Map extensions) { - this.extensions = extensions; - } - - @Nullable - public Object get(String extension) { - return extensions != null - ? extensions.get(extension) - : null; - } - - public boolean has(String extension) { - return extensions != null && extensions.containsKey(extension); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Extensions that = (Extensions) o; - return Objects.equals(extensions, that.extensions); - } - - @Override - public int hashCode() { - return Objects.hashCode(extensions); - } + @Nullable public static Extensions fromMap(@Nullable Map input) { + return input != null ? new Extensions(input) : null; + } + + @Nullable private final Map extensions; + + private Extensions(@Nullable Map extensions) { + this.extensions = extensions; + } + + @Nullable public Object get(String extension) { + return extensions != null ? extensions.get(extension) : null; + } + + public boolean has(String extension) { + return extensions != null && extensions.containsKey(extension); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Extensions that = (Extensions) o; + return Objects.equals(extensions, that.extensions); + } + + @Override + public int hashCode() { + return Objects.hashCode(extensions); + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredential.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredential.java index e1aaa396..6fbfc8b5 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredential.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredential.java @@ -20,211 +20,200 @@ import com.yubico.yubikit.core.internal.codec.Base64; import com.yubico.yubikit.fido.ctap.Ctap2Session; - import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; public class PublicKeyCredential extends Credential { - public static final String RAW_ID = "rawId"; - public static final String RESPONSE = "response"; - public static final String AUTHENTICATOR_ATTACHMENT = "authenticatorAttachment"; - public static final String CLIENT_EXTENSION_RESULTS = "clientExtensionResults"; - - public static final String PUBLIC_KEY_CREDENTIAL_TYPE = "public-key"; - - private final byte[] rawId; - private final AuthenticatorResponse response; - @Nullable - private final ClientExtensionResults clientExtensionResults; - - /** - * Constructs a new Webauthn PublicKeyCredential object - * - * @param id Credential id in base64 url safe encoding. - * @param response Operation response. - * @see AuthenticatorAttestationResponse - * @see AuthenticatorAssertionResponse - */ - public PublicKeyCredential(String id, AuthenticatorResponse response) { - this(id, response, null); - } - - /** - * Constructs a new Webauthn PublicKeyCredential object - * - * @param id Credential id in base64 url safe encoding. - * @param response Operation response. - * @param clientExtensionResults Extension results. - * @see AuthenticatorAttestationResponse - * @see AuthenticatorAssertionResponse - */ - public PublicKeyCredential( - String id, - AuthenticatorResponse response, - @Nullable ClientExtensionResults clientExtensionResults) { - super(id, PUBLIC_KEY_CREDENTIAL_TYPE); - this.rawId = Base64.fromUrlSafeString(id); - this.response = response; - this.clientExtensionResults = clientExtensionResults; - } - - /** - * Constructs a new Webauthn PublicKeyCredential object - * - * @param id Credential id in binary form. - * @param response Operation response. - * @see AuthenticatorAttestationResponse - * @see AuthenticatorAssertionResponse - */ - public PublicKeyCredential(byte[] id, AuthenticatorResponse response) { - this(id, response, null); - } - - /** - * Constructs a new Webauthn PublicKeyCredential object - * - * @param id Credential id in binary form. - * @param response Operation response. - * @param clientExtensionResults Extension results. - * @see AuthenticatorAttestationResponse - * @see AuthenticatorAssertionResponse - */ - public PublicKeyCredential( - byte[] id, - AuthenticatorResponse response, - @Nullable ClientExtensionResults clientExtensionResults - ) { - super(Base64.toUrlSafeString(id), PUBLIC_KEY_CREDENTIAL_TYPE); - this.rawId = id; - this.response = response; - this.clientExtensionResults = clientExtensionResults; - } - - public byte[] getRawId() { - return Arrays.copyOf(rawId, rawId.length); - } - - public AuthenticatorResponse getResponse() { - return response; - } - - @Nullable - public ClientExtensionResults getClientExtensionResults() { - return clientExtensionResults; - } - - public Map toMap(SerializationType serializationType) { - Map map = new HashMap<>(); - map.put(ID, getId()); - map.put(TYPE, getType()); - map.put(RAW_ID, serializeBytes(getRawId(), serializationType)); - map.put(AUTHENTICATOR_ATTACHMENT, AuthenticatorAttachment.CROSS_PLATFORM); - map.put(RESPONSE, getResponse().toMap(serializationType)); - if (getClientExtensionResults() != null) { - map.put(CLIENT_EXTENSION_RESULTS, getClientExtensionResults().toMap(serializationType)); - } - return map; + public static final String RAW_ID = "rawId"; + public static final String RESPONSE = "response"; + public static final String AUTHENTICATOR_ATTACHMENT = "authenticatorAttachment"; + public static final String CLIENT_EXTENSION_RESULTS = "clientExtensionResults"; + + public static final String PUBLIC_KEY_CREDENTIAL_TYPE = "public-key"; + + private final byte[] rawId; + private final AuthenticatorResponse response; + @Nullable private final ClientExtensionResults clientExtensionResults; + + /** + * Constructs a new Webauthn PublicKeyCredential object + * + * @param id Credential id in base64 url safe encoding. + * @param response Operation response. + * @see AuthenticatorAttestationResponse + * @see AuthenticatorAssertionResponse + */ + public PublicKeyCredential(String id, AuthenticatorResponse response) { + this(id, response, null); + } + + /** + * Constructs a new Webauthn PublicKeyCredential object + * + * @param id Credential id in base64 url safe encoding. + * @param response Operation response. + * @param clientExtensionResults Extension results. + * @see AuthenticatorAttestationResponse + * @see AuthenticatorAssertionResponse + */ + public PublicKeyCredential( + String id, + AuthenticatorResponse response, + @Nullable ClientExtensionResults clientExtensionResults) { + super(id, PUBLIC_KEY_CREDENTIAL_TYPE); + this.rawId = Base64.fromUrlSafeString(id); + this.response = response; + this.clientExtensionResults = clientExtensionResults; + } + + /** + * Constructs a new Webauthn PublicKeyCredential object + * + * @param id Credential id in binary form. + * @param response Operation response. + * @see AuthenticatorAttestationResponse + * @see AuthenticatorAssertionResponse + */ + public PublicKeyCredential(byte[] id, AuthenticatorResponse response) { + this(id, response, null); + } + + /** + * Constructs a new Webauthn PublicKeyCredential object + * + * @param id Credential id in binary form. + * @param response Operation response. + * @param clientExtensionResults Extension results. + * @see AuthenticatorAttestationResponse + * @see AuthenticatorAssertionResponse + */ + public PublicKeyCredential( + byte[] id, + AuthenticatorResponse response, + @Nullable ClientExtensionResults clientExtensionResults) { + super(Base64.toUrlSafeString(id), PUBLIC_KEY_CREDENTIAL_TYPE); + this.rawId = id; + this.response = response; + this.clientExtensionResults = clientExtensionResults; + } + + public byte[] getRawId() { + return Arrays.copyOf(rawId, rawId.length); + } + + public AuthenticatorResponse getResponse() { + return response; + } + + @Nullable public ClientExtensionResults getClientExtensionResults() { + return clientExtensionResults; + } + + public Map toMap(SerializationType serializationType) { + Map map = new HashMap<>(); + map.put(ID, getId()); + map.put(TYPE, getType()); + map.put(RAW_ID, serializeBytes(getRawId(), serializationType)); + map.put(AUTHENTICATOR_ATTACHMENT, AuthenticatorAttachment.CROSS_PLATFORM); + map.put(RESPONSE, getResponse().toMap(serializationType)); + if (getClientExtensionResults() != null) { + map.put(CLIENT_EXTENSION_RESULTS, getClientExtensionResults().toMap(serializationType)); } - - public Map toMap() { - return toMap(SerializationType.DEFAULT); + return map; + } + + public Map toMap() { + return toMap(SerializationType.DEFAULT); + } + + @SuppressWarnings("unchecked") + public static PublicKeyCredential fromMap( + Map map, SerializationType serializationType) { + if (!PUBLIC_KEY_CREDENTIAL_TYPE.equals(Objects.requireNonNull((String) map.get(TYPE)))) { + throw new IllegalArgumentException("Expecting type=" + PUBLIC_KEY_CREDENTIAL_TYPE); } - @SuppressWarnings("unchecked") - public static PublicKeyCredential fromMap(Map map, SerializationType serializationType) { - if (!PUBLIC_KEY_CREDENTIAL_TYPE.equals(Objects.requireNonNull((String) map.get(TYPE)))) { - throw new IllegalArgumentException("Expecting type=" + PUBLIC_KEY_CREDENTIAL_TYPE); - } - - Map responseMap = Objects.requireNonNull((Map) map.get(RESPONSE)); - AuthenticatorResponse response; - try { - if (responseMap.containsKey(AuthenticatorAttestationResponse.ATTESTATION_OBJECT)) { - response = AuthenticatorAttestationResponse.fromMap(responseMap, serializationType); - } else { - response = AuthenticatorAssertionResponse.fromMap(responseMap, serializationType); - } - } catch (Exception e) { - throw new IllegalArgumentException("Unknown AuthenticatorResponse format", e); - } - - return new PublicKeyCredential( - Objects.requireNonNull((String) map.get(ID)), - response - ); + Map responseMap = Objects.requireNonNull((Map) map.get(RESPONSE)); + AuthenticatorResponse response; + try { + if (responseMap.containsKey(AuthenticatorAttestationResponse.ATTESTATION_OBJECT)) { + response = AuthenticatorAttestationResponse.fromMap(responseMap, serializationType); + } else { + response = AuthenticatorAssertionResponse.fromMap(responseMap, serializationType); + } + } catch (Exception e) { + throw new IllegalArgumentException("Unknown AuthenticatorResponse format", e); } - public static PublicKeyCredential fromMap(Map map) { - return fromMap(map, SerializationType.DEFAULT); + return new PublicKeyCredential(Objects.requireNonNull((String) map.get(ID)), response); + } + + public static PublicKeyCredential fromMap(Map map) { + return fromMap(map, SerializationType.DEFAULT); + } + + /** + * Constructs new PublicKeyCredential from AssertionData + * + * @param assertion Data base for the new credential. + * @param clientDataJson Response client data. + * @param allowCredentials Used for querying credential id for incomplete assertion objects + * @return new PublicKeyCredential object. + */ + public static PublicKeyCredential fromAssertion( + Ctap2Session.AssertionData assertion, + byte[] clientDataJson, + @Nullable List allowCredentials) { + return fromAssertion(assertion, clientDataJson, allowCredentials, null); + } + + /** + * Constructs new PublicKeyCredential from AssertionData + * + * @param assertion Data base for the new credential. + * @param clientDataJson Response client data. + * @param allowCredentials Used for querying credential id for incomplete assertion objects. + * @param clientExtensionResults Extension results. + * @return new PublicKeyCredential object + */ + public static PublicKeyCredential fromAssertion( + Ctap2Session.AssertionData assertion, + byte[] clientDataJson, + @Nullable List allowCredentials, + @Nullable ClientExtensionResults clientExtensionResults) { + byte[] userId = null; + Map userMap = assertion.getUser(); + if (userMap != null) { + // This is not a complete UserEntity object, it may contain only "id". + userId = Objects.requireNonNull((byte[]) userMap.get(PublicKeyCredentialUserEntity.ID)); } - /** - * Constructs new PublicKeyCredential from AssertionData - * - * @param assertion Data base for the new credential. - * @param clientDataJson Response client data. - * @param allowCredentials Used for querying credential id for incomplete assertion objects - * @return new PublicKeyCredential object. - */ - public static PublicKeyCredential fromAssertion( - Ctap2Session.AssertionData assertion, - byte[] clientDataJson, - @Nullable List allowCredentials) { - return fromAssertion(assertion, clientDataJson, allowCredentials, null); - } - - /** - * Constructs new PublicKeyCredential from AssertionData - * - * @param assertion Data base for the new credential. - * @param clientDataJson Response client data. - * @param allowCredentials Used for querying credential id for incomplete assertion objects. - * @param clientExtensionResults Extension results. - * @return new PublicKeyCredential object - */ - public static PublicKeyCredential fromAssertion( - Ctap2Session.AssertionData assertion, - byte[] clientDataJson, - @Nullable List allowCredentials, - @Nullable ClientExtensionResults clientExtensionResults) { - byte[] userId = null; - Map userMap = assertion.getUser(); - if (userMap != null) { - // This is not a complete UserEntity object, it may contain only "id". - userId = Objects.requireNonNull((byte[]) userMap.get(PublicKeyCredentialUserEntity.ID)); - } - - return new PublicKeyCredential( - assertion.getCredentialId(allowCredentials), - new AuthenticatorAssertionResponse( - clientDataJson, - assertion.getAuthenticatorData(), - assertion.getSignature(), - userId - ), - clientExtensionResults); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - PublicKeyCredential that = (PublicKeyCredential) o; - - if (!Arrays.equals(rawId, that.rawId)) return false; - return response.equals(that.response); - } - - @Override - public int hashCode() { - int result = Arrays.hashCode(rawId); - result = 31 * result + response.hashCode(); - return result; - } + return new PublicKeyCredential( + assertion.getCredentialId(allowCredentials), + new AuthenticatorAssertionResponse( + clientDataJson, assertion.getAuthenticatorData(), assertion.getSignature(), userId), + clientExtensionResults); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PublicKeyCredential that = (PublicKeyCredential) o; + + if (!Arrays.equals(rawId, that.rawId)) return false; + return response.equals(that.response); + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(rawId); + result = 31 * result + response.hashCode(); + return result; + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialCreationOptions.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialCreationOptions.java index f7c146b4..dc397a25 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialCreationOptions.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialCreationOptions.java @@ -25,165 +25,155 @@ import java.util.List; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; public class PublicKeyCredentialCreationOptions { - private final static String RP = "rp"; - private final static String USER = "user"; - private final static String CHALLENGE = "challenge"; - private final static String PUB_KEY_CRED_PARAMS = "pubKeyCredParams"; - private final static String TIMEOUT = "timeout"; - private final static String EXCLUDE_CREDENTIALS = "excludeCredentials"; - private final static String AUTHENTICATOR_SELECTION = "authenticatorSelection"; - private final static String ATTESTATION = "attestation"; - private final static String EXTENSIONS = "extensions"; - - private final PublicKeyCredentialRpEntity rp; - private final PublicKeyCredentialUserEntity user; - private final byte[] challenge; - private final List pubKeyCredParams; - @Nullable - private final Long timeout; - private final List excludeCredentials; - @Nullable - private final AuthenticatorSelectionCriteria authenticatorSelection; - private final String attestation; - @Nullable - private final Extensions extensions; - - public PublicKeyCredentialCreationOptions( - PublicKeyCredentialRpEntity rp, - PublicKeyCredentialUserEntity user, - byte[] challenge, - List pubKeyCredParams, - @Nullable Long timeout, - @Nullable List excludeCredentials, - @Nullable AuthenticatorSelectionCriteria authenticatorSelection, - @Nullable String attestation, - @Nullable Extensions extensions - ) { - this.rp = rp; - this.user = user; - this.challenge = challenge; - this.pubKeyCredParams = pubKeyCredParams; - this.timeout = timeout; - this.excludeCredentials = excludeCredentials != null ? excludeCredentials : Collections.emptyList(); - this.authenticatorSelection = authenticatorSelection; - this.attestation = attestation != null ? attestation : AttestationConveyancePreference.NONE; - this.extensions = extensions; - } - - public PublicKeyCredentialRpEntity getRp() { - return rp; - } - - public PublicKeyCredentialUserEntity getUser() { - return user; - } - - public byte[] getChallenge() { - return challenge; - } - - public List getPubKeyCredParams() { - return pubKeyCredParams; + private static final String RP = "rp"; + private static final String USER = "user"; + private static final String CHALLENGE = "challenge"; + private static final String PUB_KEY_CRED_PARAMS = "pubKeyCredParams"; + private static final String TIMEOUT = "timeout"; + private static final String EXCLUDE_CREDENTIALS = "excludeCredentials"; + private static final String AUTHENTICATOR_SELECTION = "authenticatorSelection"; + private static final String ATTESTATION = "attestation"; + private static final String EXTENSIONS = "extensions"; + + private final PublicKeyCredentialRpEntity rp; + private final PublicKeyCredentialUserEntity user; + private final byte[] challenge; + private final List pubKeyCredParams; + @Nullable private final Long timeout; + private final List excludeCredentials; + @Nullable private final AuthenticatorSelectionCriteria authenticatorSelection; + private final String attestation; + @Nullable private final Extensions extensions; + + public PublicKeyCredentialCreationOptions( + PublicKeyCredentialRpEntity rp, + PublicKeyCredentialUserEntity user, + byte[] challenge, + List pubKeyCredParams, + @Nullable Long timeout, + @Nullable List excludeCredentials, + @Nullable AuthenticatorSelectionCriteria authenticatorSelection, + @Nullable String attestation, + @Nullable Extensions extensions) { + this.rp = rp; + this.user = user; + this.challenge = challenge; + this.pubKeyCredParams = pubKeyCredParams; + this.timeout = timeout; + this.excludeCredentials = + excludeCredentials != null ? excludeCredentials : Collections.emptyList(); + this.authenticatorSelection = authenticatorSelection; + this.attestation = attestation != null ? attestation : AttestationConveyancePreference.NONE; + this.extensions = extensions; + } + + public PublicKeyCredentialRpEntity getRp() { + return rp; + } + + public PublicKeyCredentialUserEntity getUser() { + return user; + } + + public byte[] getChallenge() { + return challenge; + } + + public List getPubKeyCredParams() { + return pubKeyCredParams; + } + + @Nullable public Long getTimeout() { + return timeout; + } + + public List getExcludeCredentials() { + return excludeCredentials; + } + + @Nullable public AuthenticatorSelectionCriteria getAuthenticatorSelection() { + return authenticatorSelection; + } + + public String getAttestation() { + return attestation; + } + + @Nullable public Extensions getExtensions() { + return extensions; + } + + public Map toMap(SerializationType serializationType) { + Map map = new HashMap<>(); + map.put(RP, rp.toMap(serializationType)); + map.put(USER, user.toMap(serializationType)); + map.put(CHALLENGE, serializeBytes(challenge, serializationType)); + List> paramsList = new ArrayList<>(); + for (PublicKeyCredentialParameters params : pubKeyCredParams) { + paramsList.add(params.toMap(serializationType)); } - - @Nullable - public Long getTimeout() { - return timeout; + map.put(PUB_KEY_CRED_PARAMS, paramsList); + if (timeout != null) { + map.put(TIMEOUT, timeout); } - - public List getExcludeCredentials() { - return excludeCredentials; + if (!excludeCredentials.isEmpty()) { + List> excludeCredentialsList = new ArrayList<>(); + for (PublicKeyCredentialDescriptor cred : excludeCredentials) { + excludeCredentialsList.add(cred.toMap(serializationType)); + } + map.put(EXCLUDE_CREDENTIALS, excludeCredentialsList); } - - @Nullable - public AuthenticatorSelectionCriteria getAuthenticatorSelection() { - return authenticatorSelection; + if (authenticatorSelection != null) { + map.put(AUTHENTICATOR_SELECTION, authenticatorSelection.toMap(serializationType)); } - - public String getAttestation() { - return attestation; + map.put(ATTESTATION, attestation); + if (extensions != null) { + map.put(EXTENSIONS, extensions); } - - @Nullable - public Extensions getExtensions() { - return extensions; + return map; + } + + @SuppressWarnings("unchecked") + public static PublicKeyCredentialCreationOptions fromMap( + Map map, SerializationType serializationType) { + List pubKeyCredParams = new ArrayList<>(); + for (Map params : + Objects.requireNonNull((List>) map.get(PUB_KEY_CRED_PARAMS))) { + pubKeyCredParams.add(PublicKeyCredentialParameters.fromMap(params, serializationType)); } - - public Map toMap(SerializationType serializationType) { - Map map = new HashMap<>(); - map.put(RP, rp.toMap(serializationType)); - map.put(USER, user.toMap(serializationType)); - map.put(CHALLENGE, serializeBytes(challenge, serializationType)); - List> paramsList = new ArrayList<>(); - for (PublicKeyCredentialParameters params : pubKeyCredParams) { - paramsList.add(params.toMap(serializationType)); - } - map.put(PUB_KEY_CRED_PARAMS, paramsList); - if (timeout != null) { - map.put(TIMEOUT, timeout); - } - if (!excludeCredentials.isEmpty()) { - List> excludeCredentialsList = new ArrayList<>(); - for (PublicKeyCredentialDescriptor cred : excludeCredentials) { - excludeCredentialsList.add(cred.toMap(serializationType)); - } - map.put(EXCLUDE_CREDENTIALS, excludeCredentialsList); - } - if (authenticatorSelection != null) { - map.put(AUTHENTICATOR_SELECTION, authenticatorSelection.toMap(serializationType)); - } - map.put(ATTESTATION, attestation); - if (extensions != null) { - map.put(EXTENSIONS, extensions); - } - return map; - } - - @SuppressWarnings("unchecked") - public static PublicKeyCredentialCreationOptions fromMap( - Map map, - SerializationType serializationType) { - List pubKeyCredParams = new ArrayList<>(); - for (Map params : - Objects.requireNonNull((List>) map.get(PUB_KEY_CRED_PARAMS))) { - pubKeyCredParams.add(PublicKeyCredentialParameters.fromMap(params, serializationType)); - } - List excludeCredentials = null; - List> excludeCredentialsList = (List>) map.get(EXCLUDE_CREDENTIALS); - if (excludeCredentialsList != null) { - excludeCredentials = new ArrayList<>(); - for (Map cred : excludeCredentialsList) { - excludeCredentials.add(PublicKeyCredentialDescriptor.fromMap(cred, serializationType)); - } - } - - Map authenticatorSelection = (Map) map.get(AUTHENTICATOR_SELECTION); - Number timeout = (Number) map.get(TIMEOUT); - - return new PublicKeyCredentialCreationOptions( - PublicKeyCredentialRpEntity.fromMap( - Objects.requireNonNull((Map) map.get(RP)), - serializationType), - PublicKeyCredentialUserEntity.fromMap( - Objects.requireNonNull((Map) map.get(USER)), - serializationType), - deserializeBytes(Objects.requireNonNull(map.get(CHALLENGE)), serializationType), - pubKeyCredParams, - timeout == null ? null : timeout.longValue(), - excludeCredentials, - authenticatorSelection == null ? null : AuthenticatorSelectionCriteria.fromMap( - authenticatorSelection, - serializationType), - (String) map.get(ATTESTATION), - Extensions.fromMap((Map) map.get(EXTENSIONS)) - ); + List excludeCredentials = null; + List> excludeCredentialsList = + (List>) map.get(EXCLUDE_CREDENTIALS); + if (excludeCredentialsList != null) { + excludeCredentials = new ArrayList<>(); + for (Map cred : excludeCredentialsList) { + excludeCredentials.add(PublicKeyCredentialDescriptor.fromMap(cred, serializationType)); + } } - public static PublicKeyCredentialCreationOptions fromMap(Map map) { - return fromMap(map, SerializationType.DEFAULT); - } -} \ No newline at end of file + Map authenticatorSelection = (Map) map.get(AUTHENTICATOR_SELECTION); + Number timeout = (Number) map.get(TIMEOUT); + + return new PublicKeyCredentialCreationOptions( + PublicKeyCredentialRpEntity.fromMap( + Objects.requireNonNull((Map) map.get(RP)), serializationType), + PublicKeyCredentialUserEntity.fromMap( + Objects.requireNonNull((Map) map.get(USER)), serializationType), + deserializeBytes(Objects.requireNonNull(map.get(CHALLENGE)), serializationType), + pubKeyCredParams, + timeout == null ? null : timeout.longValue(), + excludeCredentials, + authenticatorSelection == null + ? null + : AuthenticatorSelectionCriteria.fromMap(authenticatorSelection, serializationType), + (String) map.get(ATTESTATION), + Extensions.fromMap((Map) map.get(EXTENSIONS))); + } + + public static PublicKeyCredentialCreationOptions fromMap(Map map) { + return fromMap(map, SerializationType.DEFAULT); + } +} diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialDescriptor.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialDescriptor.java index 0d92f068..6c73445d 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialDescriptor.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialDescriptor.java @@ -24,82 +24,77 @@ import java.util.List; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; public class PublicKeyCredentialDescriptor { - public static final String TYPE = "type"; - public static final String ID = "id"; - public static final String TRANSPORTS = "transports"; - - private final String type; - private final byte[] id; - @Nullable - private final List transports; - - public PublicKeyCredentialDescriptor(String type, byte[] id) { - this.type = type; - this.id = id; - this.transports = null; - } - - public PublicKeyCredentialDescriptor(String type, byte[] id, @Nullable List transports) { - this.type = type; - this.id = id; - this.transports = transports; - } - - public String getType() { - return type; - } - - public byte[] getId() { - return id; - } - - @Nullable - public List getTransports() { - return transports; - } - - public Map toMap(SerializationType serializationType) { - Map map = new HashMap<>(); - map.put(TYPE, type); - map.put(ID, serializeBytes(id, serializationType)); - if (transports != null) { - map.put(TRANSPORTS, transports); - } - return map; - } - - @SuppressWarnings("unchecked") - public static PublicKeyCredentialDescriptor fromMap( - Map map, - SerializationType serializationType - ) { - return new PublicKeyCredentialDescriptor( - Objects.requireNonNull((String) map.get(TYPE)), - deserializeBytes(Objects.requireNonNull(map.get(ID)), serializationType), - (List) map.get(TRANSPORTS)); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - PublicKeyCredentialDescriptor that = (PublicKeyCredentialDescriptor) o; - - if (!type.equals(that.type)) return false; - if (!Arrays.equals(id, that.id)) return false; - return Objects.equals(transports, that.transports); - } - - @Override - public int hashCode() { - int result = type.hashCode(); - result = 31 * result + Arrays.hashCode(id); - result = 31 * result + (transports != null ? transports.hashCode() : 0); - return result; + public static final String TYPE = "type"; + public static final String ID = "id"; + public static final String TRANSPORTS = "transports"; + + private final String type; + private final byte[] id; + @Nullable private final List transports; + + public PublicKeyCredentialDescriptor(String type, byte[] id) { + this.type = type; + this.id = id; + this.transports = null; + } + + public PublicKeyCredentialDescriptor(String type, byte[] id, @Nullable List transports) { + this.type = type; + this.id = id; + this.transports = transports; + } + + public String getType() { + return type; + } + + public byte[] getId() { + return id; + } + + @Nullable public List getTransports() { + return transports; + } + + public Map toMap(SerializationType serializationType) { + Map map = new HashMap<>(); + map.put(TYPE, type); + map.put(ID, serializeBytes(id, serializationType)); + if (transports != null) { + map.put(TRANSPORTS, transports); } + return map; + } + + @SuppressWarnings("unchecked") + public static PublicKeyCredentialDescriptor fromMap( + Map map, SerializationType serializationType) { + return new PublicKeyCredentialDescriptor( + Objects.requireNonNull((String) map.get(TYPE)), + deserializeBytes(Objects.requireNonNull(map.get(ID)), serializationType), + (List) map.get(TRANSPORTS)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PublicKeyCredentialDescriptor that = (PublicKeyCredentialDescriptor) o; + + if (!type.equals(that.type)) return false; + if (!Arrays.equals(id, that.id)) return false; + return Objects.equals(transports, that.transports); + } + + @Override + public int hashCode() { + int result = type.hashCode(); + result = 31 * result + Arrays.hashCode(id); + result = 31 * result + (transports != null ? transports.hashCode() : 0); + return result; + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialEntity.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialEntity.java index 3f1b04c0..f4f030b2 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialEntity.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialEntity.java @@ -17,15 +17,15 @@ package com.yubico.yubikit.fido.webauthn; public class PublicKeyCredentialEntity { - public static final String NAME = "name"; + public static final String NAME = "name"; - private final String name; + private final String name; - public PublicKeyCredentialEntity(String name) { - this.name = name; - } + public PublicKeyCredentialEntity(String name) { + this.name = name; + } - public String getName() { - return name; - } + public String getName() { + return name; + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialParameters.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialParameters.java index 2c1702e2..4155ff04 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialParameters.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialParameters.java @@ -21,56 +21,54 @@ import java.util.Objects; public class PublicKeyCredentialParameters { - private static final String TYPE = "type"; - private static final String ALG = "alg"; + private static final String TYPE = "type"; + private static final String ALG = "alg"; - private final String type; - private final int alg; + private final String type; + private final int alg; - public PublicKeyCredentialParameters(String type, int alg) { - this.type = type; - this.alg = alg; - } + public PublicKeyCredentialParameters(String type, int alg) { + this.type = type; + this.alg = alg; + } - public String getType() { - return type; - } + public String getType() { + return type; + } - public int getAlg() { - return alg; - } + public int getAlg() { + return alg; + } - public Map toMap(@SuppressWarnings("unused") SerializationType serializationType) { - Map map = new HashMap<>(); - map.put(TYPE, type); - map.put(ALG, alg); - return map; - } + public Map toMap(@SuppressWarnings("unused") SerializationType serializationType) { + Map map = new HashMap<>(); + map.put(TYPE, type); + map.put(ALG, alg); + return map; + } - public static PublicKeyCredentialParameters fromMap( - Map map, - @SuppressWarnings("unused") SerializationType serializationType) { - return new PublicKeyCredentialParameters( - Objects.requireNonNull((String) map.get(TYPE)), - Objects.requireNonNull((Integer) map.get(ALG)) - ); - } + public static PublicKeyCredentialParameters fromMap( + Map map, @SuppressWarnings("unused") SerializationType serializationType) { + return new PublicKeyCredentialParameters( + Objects.requireNonNull((String) map.get(TYPE)), + Objects.requireNonNull((Integer) map.get(ALG))); + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; - PublicKeyCredentialParameters that = (PublicKeyCredentialParameters) o; + PublicKeyCredentialParameters that = (PublicKeyCredentialParameters) o; - if (alg != that.alg) return false; - return type.equals(that.type); - } + if (alg != that.alg) return false; + return type.equals(that.type); + } - @Override - public int hashCode() { - int result = type.hashCode(); - result = 31 * result + alg; - return result; - } + @Override + public int hashCode() { + int result = type.hashCode(); + result = 31 * result + alg; + return result; + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialRequestOptions.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialRequestOptions.java index 21716ea2..f6919ff9 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialRequestOptions.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialRequestOptions.java @@ -26,140 +26,134 @@ import java.util.List; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; public class PublicKeyCredentialRequestOptions { - private final static String CHALLENGE = "challenge"; - private final static String TIMEOUT = "timeout"; - private final static String RP_ID = "rpId"; - private final static String ALLOW_CREDENTIALS = "allowCredentials"; - private final static String USER_VERIFICATION = "userVerification"; - private final static String EXTENSIONS = "extensions"; - - private final byte[] challenge; - @Nullable - private final Long timeout; - @Nullable - private final String rpId; - private final List allowCredentials; - private final String userVerification; - @Nullable - private final Extensions extensions; - - public PublicKeyCredentialRequestOptions( - byte[] challenge, - @Nullable Long timeout, - @Nullable String rpId, - @Nullable List allowCredentials, - @Nullable String userVerification, - @Nullable Extensions extensions - ) { - this.challenge = challenge; - this.timeout = timeout; - this.rpId = rpId; - this.allowCredentials = allowCredentials != null ? allowCredentials : Collections.emptyList(); - this.userVerification = userVerification != null ? userVerification : UserVerificationRequirement.PREFERRED; - this.extensions = extensions; - } - - public byte[] getChallenge() { - return challenge; - } - - public @Nullable Long getTimeout() { - return timeout; - } - - @Nullable - public String getRpId() { - return rpId; - } - - public List getAllowCredentials() { - return allowCredentials; - } - - public String getUserVerification() { - return userVerification; + private static final String CHALLENGE = "challenge"; + private static final String TIMEOUT = "timeout"; + private static final String RP_ID = "rpId"; + private static final String ALLOW_CREDENTIALS = "allowCredentials"; + private static final String USER_VERIFICATION = "userVerification"; + private static final String EXTENSIONS = "extensions"; + + private final byte[] challenge; + @Nullable private final Long timeout; + @Nullable private final String rpId; + private final List allowCredentials; + private final String userVerification; + @Nullable private final Extensions extensions; + + public PublicKeyCredentialRequestOptions( + byte[] challenge, + @Nullable Long timeout, + @Nullable String rpId, + @Nullable List allowCredentials, + @Nullable String userVerification, + @Nullable Extensions extensions) { + this.challenge = challenge; + this.timeout = timeout; + this.rpId = rpId; + this.allowCredentials = allowCredentials != null ? allowCredentials : Collections.emptyList(); + this.userVerification = + userVerification != null ? userVerification : UserVerificationRequirement.PREFERRED; + this.extensions = extensions; + } + + public byte[] getChallenge() { + return challenge; + } + + public @Nullable Long getTimeout() { + return timeout; + } + + @Nullable public String getRpId() { + return rpId; + } + + public List getAllowCredentials() { + return allowCredentials; + } + + public String getUserVerification() { + return userVerification; + } + + @Nullable public Extensions getExtensions() { + return extensions; + } + + public Map toMap(SerializationType serializationType) { + Map map = new HashMap<>(); + map.put(CHALLENGE, serializeBytes(challenge, serializationType)); + if (timeout != null) { + map.put(TIMEOUT, timeout); } - - @Nullable - public Extensions getExtensions() { - return extensions; + if (rpId != null) { + map.put(RP_ID, rpId); } - - public Map toMap(SerializationType serializationType) { - Map map = new HashMap<>(); - map.put(CHALLENGE, serializeBytes(challenge, serializationType)); - if (timeout != null) { - map.put(TIMEOUT, timeout); - } - if (rpId != null) { - map.put(RP_ID, rpId); - } - List> allowCredentialsList = new ArrayList<>(); - for (PublicKeyCredentialDescriptor cred : allowCredentials) { - allowCredentialsList.add(cred.toMap(serializationType)); - } - map.put(ALLOW_CREDENTIALS, allowCredentialsList); - map.put(USER_VERIFICATION, userVerification); - if (extensions != null) { - map.put(EXTENSIONS, extensions); - } - return map; - } - - @SuppressWarnings("unchecked") - public static PublicKeyCredentialRequestOptions fromMap(Map map, SerializationType serializationType) { - List allowCredentials = null; - List> allowCredentialsList = (List>) map.get(ALLOW_CREDENTIALS); - if (allowCredentialsList != null) { - allowCredentials = new ArrayList<>(); - for (Map cred : allowCredentialsList) { - allowCredentials.add(PublicKeyCredentialDescriptor.fromMap(cred, serializationType)); - } - } - - Number timeout = ((Number) map.get(TIMEOUT)); - - return new PublicKeyCredentialRequestOptions( - deserializeBytes(Objects.requireNonNull(map.get(CHALLENGE)), serializationType), - timeout == null ? null : timeout.longValue(), - (String) map.get(RP_ID), - allowCredentials, - (String) map.get(USER_VERIFICATION), - Extensions.fromMap((Map) map.get(EXTENSIONS)) - ); + List> allowCredentialsList = new ArrayList<>(); + for (PublicKeyCredentialDescriptor cred : allowCredentials) { + allowCredentialsList.add(cred.toMap(serializationType)); } - - public static PublicKeyCredentialRequestOptions fromMap(Map map) { - return fromMap(map, SerializationType.DEFAULT); + map.put(ALLOW_CREDENTIALS, allowCredentialsList); + map.put(USER_VERIFICATION, userVerification); + if (extensions != null) { + map.put(EXTENSIONS, extensions); } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - PublicKeyCredentialRequestOptions that = (PublicKeyCredentialRequestOptions) o; - - if (!Arrays.equals(challenge, that.challenge)) return false; - if (!Objects.equals(timeout, that.timeout)) return false; - if (!Objects.equals(rpId, that.rpId)) return false; - if (!allowCredentials.equals(that.allowCredentials)) return false; - if (!userVerification.equals(that.userVerification)) return false; - return Objects.equals(extensions, that.extensions); + return map; + } + + @SuppressWarnings("unchecked") + public static PublicKeyCredentialRequestOptions fromMap( + Map map, SerializationType serializationType) { + List allowCredentials = null; + List> allowCredentialsList = (List>) map.get(ALLOW_CREDENTIALS); + if (allowCredentialsList != null) { + allowCredentials = new ArrayList<>(); + for (Map cred : allowCredentialsList) { + allowCredentials.add(PublicKeyCredentialDescriptor.fromMap(cred, serializationType)); + } } - @Override - public int hashCode() { - int result = Arrays.hashCode(challenge); - result = 31 * result + (timeout != null ? timeout.hashCode() : 0); - result = 31 * result + (rpId != null ? rpId.hashCode() : 0); - result = 31 * result + allowCredentials.hashCode(); - result = 31 * result + userVerification.hashCode(); - result = 31 * result + (extensions != null ? extensions.hashCode() : 0); - return result; - } + Number timeout = ((Number) map.get(TIMEOUT)); + + return new PublicKeyCredentialRequestOptions( + deserializeBytes(Objects.requireNonNull(map.get(CHALLENGE)), serializationType), + timeout == null ? null : timeout.longValue(), + (String) map.get(RP_ID), + allowCredentials, + (String) map.get(USER_VERIFICATION), + Extensions.fromMap((Map) map.get(EXTENSIONS))); + } + + public static PublicKeyCredentialRequestOptions fromMap(Map map) { + return fromMap(map, SerializationType.DEFAULT); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PublicKeyCredentialRequestOptions that = (PublicKeyCredentialRequestOptions) o; + + if (!Arrays.equals(challenge, that.challenge)) return false; + if (!Objects.equals(timeout, that.timeout)) return false; + if (!Objects.equals(rpId, that.rpId)) return false; + if (!allowCredentials.equals(that.allowCredentials)) return false; + if (!userVerification.equals(that.userVerification)) return false; + return Objects.equals(extensions, that.extensions); + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(challenge); + result = 31 * result + (timeout != null ? timeout.hashCode() : 0); + result = 31 * result + (rpId != null ? rpId.hashCode() : 0); + result = 31 * result + allowCredentials.hashCode(); + result = 31 * result + userVerification.hashCode(); + result = 31 * result + (extensions != null ? extensions.hashCode() : 0); + return result; + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialRpEntity.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialRpEntity.java index cc6dbafd..a05f6452 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialRpEntity.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialRpEntity.java @@ -19,55 +19,48 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; public class PublicKeyCredentialRpEntity extends PublicKeyCredentialEntity { - private static final String ID = "id"; - @Nullable - private final String id; + private static final String ID = "id"; + @Nullable private final String id; - public PublicKeyCredentialRpEntity(String name, @Nullable String id) { - super(name); - this.id = id; - } + public PublicKeyCredentialRpEntity(String name, @Nullable String id) { + super(name); + this.id = id; + } - @Nullable - public String getId() { - return id; - } + @Nullable public String getId() { + return id; + } - public Map toMap(@SuppressWarnings("unused") SerializationType serializationType) { - Map map = new HashMap<>(); - map.put(NAME, getName()); - if (id != null) { - map.put(ID, id); - } - return map; + public Map toMap(@SuppressWarnings("unused") SerializationType serializationType) { + Map map = new HashMap<>(); + map.put(NAME, getName()); + if (id != null) { + map.put(ID, id); } + return map; + } - public static PublicKeyCredentialRpEntity fromMap( - Map map, - @SuppressWarnings("unused") SerializationType serializationType - ) { - return new PublicKeyCredentialRpEntity( - Objects.requireNonNull((String) map.get(NAME)), - (String) map.get(ID) - ); - } + public static PublicKeyCredentialRpEntity fromMap( + Map map, @SuppressWarnings("unused") SerializationType serializationType) { + return new PublicKeyCredentialRpEntity( + Objects.requireNonNull((String) map.get(NAME)), (String) map.get(ID)); + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; - PublicKeyCredentialRpEntity that = (PublicKeyCredentialRpEntity) o; + PublicKeyCredentialRpEntity that = (PublicKeyCredentialRpEntity) o; - return Objects.equals(id, that.id); - } + return Objects.equals(id, that.id); + } - @Override - public int hashCode() { - return id != null ? id.hashCode() : 0; - } + @Override + public int hashCode() { + return id != null ? id.hashCode() : 0; + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialType.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialType.java index b1028031..ff0f8624 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialType.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialType.java @@ -17,5 +17,5 @@ package com.yubico.yubikit.fido.webauthn; public class PublicKeyCredentialType { - public static final String PUBLIC_KEY = "public-key"; + public static final String PUBLIC_KEY = "public-key"; } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialUserEntity.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialUserEntity.java index a7383129..0fc92920 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialUserEntity.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/PublicKeyCredentialUserEntity.java @@ -25,57 +25,57 @@ import java.util.Objects; public class PublicKeyCredentialUserEntity extends PublicKeyCredentialEntity { - public static final String ID = "id"; - public static final String DISPLAY_NAME = "displayName"; + public static final String ID = "id"; + public static final String DISPLAY_NAME = "displayName"; - private final byte[] id; - private final String displayName; + private final byte[] id; + private final String displayName; - public PublicKeyCredentialUserEntity(String name, byte[] id, String displayName) { - super(name); - this.id = id; - this.displayName = displayName; - } + public PublicKeyCredentialUserEntity(String name, byte[] id, String displayName) { + super(name); + this.id = id; + this.displayName = displayName; + } - public byte[] getId() { - return id; - } + public byte[] getId() { + return id; + } - public String getDisplayName() { - return displayName; - } + public String getDisplayName() { + return displayName; + } - public Map toMap(SerializationType serializationType) { - Map map = new HashMap<>(); - map.put(NAME, getName()); - map.put(ID, serializeBytes(id, serializationType)); - map.put(DISPLAY_NAME, displayName); - return map; - } + public Map toMap(SerializationType serializationType) { + Map map = new HashMap<>(); + map.put(NAME, getName()); + map.put(ID, serializeBytes(id, serializationType)); + map.put(DISPLAY_NAME, displayName); + return map; + } - public static PublicKeyCredentialUserEntity fromMap(Map map, SerializationType serializationType) { - return new PublicKeyCredentialUserEntity( - Objects.requireNonNull((String) map.get(NAME)), - deserializeBytes(Objects.requireNonNull(map.get(ID)), serializationType), - Objects.requireNonNull((String) map.get(DISPLAY_NAME)) - ); - } + public static PublicKeyCredentialUserEntity fromMap( + Map map, SerializationType serializationType) { + return new PublicKeyCredentialUserEntity( + Objects.requireNonNull((String) map.get(NAME)), + deserializeBytes(Objects.requireNonNull(map.get(ID)), serializationType), + Objects.requireNonNull((String) map.get(DISPLAY_NAME))); + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; - PublicKeyCredentialUserEntity that = (PublicKeyCredentialUserEntity) o; + PublicKeyCredentialUserEntity that = (PublicKeyCredentialUserEntity) o; - if (!Arrays.equals(id, that.id)) return false; - return displayName.equals(that.displayName); - } + if (!Arrays.equals(id, that.id)) return false; + return displayName.equals(that.displayName); + } - @Override - public int hashCode() { - int result = Arrays.hashCode(id); - result = 31 * result + displayName.hashCode(); - return result; - } + @Override + public int hashCode() { + int result = Arrays.hashCode(id); + result = 31 * result + displayName.hashCode(); + return result; + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/ResidentKeyRequirement.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/ResidentKeyRequirement.java index c09efe27..1d3d0d21 100755 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/ResidentKeyRequirement.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/ResidentKeyRequirement.java @@ -18,7 +18,7 @@ @SuppressWarnings("unused") public class ResidentKeyRequirement { - public static final String REQUIRED = "required"; - public static final String PREFERRED = "preferred"; - public static final String DISCOURAGED = "discouraged"; + public static final String REQUIRED = "required"; + public static final String PREFERRED = "preferred"; + public static final String DISCOURAGED = "discouraged"; } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/SerializationType.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/SerializationType.java index 7a027b3e..d207146f 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/SerializationType.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/SerializationType.java @@ -17,8 +17,8 @@ package com.yubico.yubikit.fido.webauthn; public enum SerializationType { - CBOR, - JSON; + CBOR, + JSON; - public static final SerializationType DEFAULT = SerializationType.JSON; + public static final SerializationType DEFAULT = SerializationType.JSON; } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/SerializationUtils.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/SerializationUtils.java index 2e17e296..8e0ac9b1 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/SerializationUtils.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/SerializationUtils.java @@ -19,31 +19,35 @@ import com.yubico.yubikit.core.internal.codec.Base64; class SerializationUtils { - static Object serializeBytes(byte[] value, SerializationType serializationType) { - switch (serializationType) { - case JSON: { - return Base64.toUrlSafeString(value); - } - - case CBOR: { - return value; - } + static Object serializeBytes(byte[] value, SerializationType serializationType) { + switch (serializationType) { + case JSON: + { + return Base64.toUrlSafeString(value); } - throw new IllegalArgumentException("Invalid serialization type"); + case CBOR: + { + return value; + } } - static byte[] deserializeBytes(Object value, SerializationType serializationType) { - switch (serializationType) { - case JSON: { - return Base64.fromUrlSafeString((String) value); - } + throw new IllegalArgumentException("Invalid serialization type"); + } - case CBOR: { - return (byte[]) value; - } + static byte[] deserializeBytes(Object value, SerializationType serializationType) { + switch (serializationType) { + case JSON: + { + return Base64.fromUrlSafeString((String) value); } - throw new IllegalArgumentException("Invalid serialization type"); + case CBOR: + { + return (byte[]) value; + } } + + throw new IllegalArgumentException("Invalid serialization type"); + } } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/UserVerificationRequirement.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/UserVerificationRequirement.java index 81646ab2..504d2fe9 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/UserVerificationRequirement.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/UserVerificationRequirement.java @@ -17,7 +17,7 @@ package com.yubico.yubikit.fido.webauthn; public class UserVerificationRequirement { - public static final String REQUIRED = "required"; - public static final String PREFERRED = "preferred"; - public static final String DISCOURAGED = "discouraged"; + public static final String REQUIRED = "required"; + public static final String PREFERRED = "preferred"; + public static final String DISCOURAGED = "discouraged"; } diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/package-info.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/package-info.java index 389f8a1b..8ddf7e6d 100755 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/package-info.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/package-info.java @@ -16,11 +16,11 @@ /** * Data classes used with the WebAuthn API. - *

- * These data classes are derived from the types defined by the W3C WebAuthn Level 1 specification. - * For details on their usage, please see: https://www.w3.org/TR/webauthn/ + * + *

These data classes are derived from the types defined by the W3C WebAuthn Level 1 + * specification. For details on their usage, please see: https://www.w3.org/TR/webauthn/ */ @PackageNonnullByDefault package com.yubico.yubikit.fido.webauthn; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/fido/src/test/java/com/yubico/yubikit/fido/CborTest.java b/fido/src/test/java/com/yubico/yubikit/fido/CborTest.java index 620334b4..69f253cd 100644 --- a/fido/src/test/java/com/yubico/yubikit/fido/CborTest.java +++ b/fido/src/test/java/com/yubico/yubikit/fido/CborTest.java @@ -19,6 +19,12 @@ import static com.yubico.yubikit.fido.TestUtils.decodeHex; import static com.yubico.yubikit.fido.TestUtils.encodeHex; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.junit.Assert; import org.junit.Test; import org.junit.experimental.runners.Enclosed; @@ -27,281 +33,261 @@ import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Unit tests for cbor functionality implemented in {@code Cbor} class. - */ +/** Unit tests for cbor functionality implemented in {@code Cbor} class. */ @RunWith(Enclosed.class) public class CborTest { - /** - * Superclass for cbor parametrized tests. - *

- * Subclasses need to be annotated with {@code @RunWith(Parameterized.class)} and implement one - * or more {@code @Test} methods and return test data through {@code @Parameters data()} - */ - private static class ParametrizedCborTest { - - @SuppressWarnings("NotNullFieldNotInitialized") - @Parameter - public String cborHex; + /** + * Superclass for cbor parametrized tests. + * + *

Subclasses need to be annotated with {@code @RunWith(Parameterized.class)} and implement one + * or more {@code @Test} methods and return test data through {@code @Parameters data()} + */ + private static class ParametrizedCborTest { - @SuppressWarnings("NotNullFieldNotInitialized") - @Parameter(1) - public Object value; - } + @SuppressWarnings("NotNullFieldNotInitialized") + @Parameter + public String cborHex; - /** - * Tests encoding and decoding of integer types - */ - @RunWith(Parameterized.class) - public static class IntegerTest extends ParametrizedCborTest { - @Parameters - public static Collection data() { - return Arrays.asList(new Object[][]{ - {"00", 0}, - {"01", 1}, - {"0a", 10}, - {"17", 23}, - {"1818", 24}, - {"1819", 25}, - {"1864", 100}, - {"1903e8", 1000}, - {"1a000f4240", 1000000}, - {"19ffff", 65535}, - {"1a00010000", 65536}, - {"1a7fffffff", Integer.MAX_VALUE}, - {"20", -1}, - {"29", -10}, - {"37", -24}, - {"3818", -25}, - {"3863", -100}, - {"3903e7", -1000}, - {"3a7fffffff", Integer.MIN_VALUE}, - }); - } + @SuppressWarnings("NotNullFieldNotInitialized") + @Parameter(1) + public Object value; + } - @Test - public void testInteger() { - assertCborEncodeAndDecode(cborHex, value); - } + /** Tests encoding and decoding of integer types */ + @RunWith(Parameterized.class) + public static class IntegerTest extends ParametrizedCborTest { + @Parameters + public static Collection data() { + return Arrays.asList( + new Object[][] { + {"00", 0}, + {"01", 1}, + {"0a", 10}, + {"17", 23}, + {"1818", 24}, + {"1819", 25}, + {"1864", 100}, + {"1903e8", 1000}, + {"1a000f4240", 1000000}, + {"19ffff", 65535}, + {"1a00010000", 65536}, + {"1a7fffffff", Integer.MAX_VALUE}, + {"20", -1}, + {"29", -10}, + {"37", -24}, + {"3818", -25}, + {"3863", -100}, + {"3903e7", -1000}, + {"3a7fffffff", Integer.MIN_VALUE}, + }); } - /** - * Tests encoding and decoding of simple boolean values - */ - @RunWith(Parameterized.class) - public static class SimpleBooleanTest extends ParametrizedCborTest { - @Parameters - public static Collection data() { - return Arrays.asList(new Object[][]{ - {"f4", false}, - {"f5", true}, - }); - } - - @Test - public void testSimpleBoolean() { - assertCborEncodeAndDecode(cborHex, value); - } + @Test + public void testInteger() { + assertCborEncodeAndDecode(cborHex, value); } + } - /** - * Tests encoding and decoding of byte array values - */ - @RunWith(Parameterized.class) - public static class ByteArrayTest extends ParametrizedCborTest { - @Parameters - public static Collection data() { - return Arrays.asList(new Object[][]{ - {"40", new byte[0]}, - {"4401020304", new byte[]{1, 2, 3, 4}}, - }); - } + /** Tests encoding and decoding of simple boolean values */ + @RunWith(Parameterized.class) + public static class SimpleBooleanTest extends ParametrizedCborTest { + @Parameters + public static Collection data() { + return Arrays.asList( + new Object[][] { + {"f4", false}, + {"f5", true}, + }); + } - @Test - public void testByteArray() { - assertCborEncodeAndDecode(cborHex, value); - } + @Test + public void testSimpleBoolean() { + assertCborEncodeAndDecode(cborHex, value); } + } - /** - * Tests encoding and decoding of String values - */ - @RunWith(Parameterized.class) - public static class StringTest extends ParametrizedCborTest { - @Parameters - public static Collection data() { - return Arrays.asList(new Object[][]{ - {"40", new byte[0]}, - {"4401020304", new byte[]{1, 2, 3, 4}}, - {"60", ""}, - {"6161", "a"}, - {"6449455446", "IETF"}, - {"62225c", "\"\\"}, - {"62c3bc", "ü"}, - {"63e6b0b4", "水"}, - {"64f0908591", "\ud800\udd51"}, - }); - } + /** Tests encoding and decoding of byte array values */ + @RunWith(Parameterized.class) + public static class ByteArrayTest extends ParametrizedCborTest { + @Parameters + public static Collection data() { + return Arrays.asList( + new Object[][] { + {"40", new byte[0]}, + {"4401020304", new byte[] {1, 2, 3, 4}}, + }); + } - @Test - public void testString() { - assertCborEncodeAndDecode(cborHex, value); - } + @Test + public void testByteArray() { + assertCborEncodeAndDecode(cborHex, value); } + } - /** - * Tests encoding and decoding of Lists - */ - @RunWith(Parameterized.class) - public static class ListTest extends ParametrizedCborTest { - @Parameters - public static Collection data() { - return Arrays.asList(new Object[][]{ - {"80", listOf()}, - {"83010203", listOf(1, 2, 3)}, - {"8301820203820405", listOf(1, listOf(2, 3), listOf(4, 5))}, - {"98190102030405060708090a0b0c0d0e0f101112131415161718181819", listOf( - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, - 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25) - }, - }); - } + /** Tests encoding and decoding of String values */ + @RunWith(Parameterized.class) + public static class StringTest extends ParametrizedCborTest { + @Parameters + public static Collection data() { + return Arrays.asList( + new Object[][] { + {"40", new byte[0]}, + {"4401020304", new byte[] {1, 2, 3, 4}}, + {"60", ""}, + {"6161", "a"}, + {"6449455446", "IETF"}, + {"62225c", "\"\\"}, + {"62c3bc", "ü"}, + {"63e6b0b4", "水"}, + {"64f0908591", "\ud800\udd51"}, + }); + } - @Test - public void testList() { - assertCborEncodeAndDecode(cborHex, value); - } + @Test + public void testString() { + assertCborEncodeAndDecode(cborHex, value); } + } - /** - * Tests encoding and decoding of Maps - */ - @RunWith(Parameterized.class) - public static class MapTest extends ParametrizedCborTest { - @Parameters - public static Collection data() { - return Arrays.asList(new Object[][]{ - {"a0", mapOf()}, - {"a201020304", mapOf(1, 2, 3, 4)}, - {"a26161016162820203", mapOf("a", 1, "b", listOf(2, 3))}, - {"826161a161626163", listOf("a", mapOf("b", "c"))}, - {"a56161614161626142616361436164614461656145", - mapOf("c", "C", "d", "D", "a", "A", "b", "B", "e", "E") - }, - }); - } + /** Tests encoding and decoding of Lists */ + @RunWith(Parameterized.class) + public static class ListTest extends ParametrizedCborTest { + @Parameters + public static Collection data() { + return Arrays.asList( + new Object[][] { + {"80", listOf()}, + {"83010203", listOf(1, 2, 3)}, + {"8301820203820405", listOf(1, listOf(2, 3), listOf(4, 5))}, + { + "98190102030405060708090a0b0c0d0e0f101112131415161718181819", + listOf( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25) + }, + }); + } - @Test - public void testMap() { - assertCborEncodeAndDecode(cborHex, value); - } + @Test + public void testList() { + assertCborEncodeAndDecode(cborHex, value); } + } - /** - * Tests order of keys - */ - @RunWith(Parameterized.class) - public static class KeyOrderTest extends ParametrizedCborTest { - @Parameters - public static Collection data() { - return Arrays.asList(new Object[][]{ - {"a30100413200613300", - mapOf("3", 0, "2".getBytes(), 0, 1, 0)}, - {"a3190100004000613300", - mapOf("3", 0, "".getBytes(), 0, 256, 0)}, - {"a4000018ff00190100001a7fffffff00", - mapOf(Integer.MAX_VALUE, 0, 255, 0, 256, 0, 0, 0)}, - {"a3413300423232004331313100", - mapOf("22".getBytes(), 0, "3".getBytes(), 0, "111".getBytes(), 0)}, - {"a3433030310043303032004330303300", - mapOf("001".getBytes(), 0, "003".getBytes(), 0, "002".getBytes(), 0)}, - {"a2f400f500", - mapOf(true, 0, false, 0)}, - {"a3613100623130006331303000", - mapOf("1", 0, "100", 0, "10", 0)} - }); - } + /** Tests encoding and decoding of Maps */ + @RunWith(Parameterized.class) + public static class MapTest extends ParametrizedCborTest { + @Parameters + public static Collection data() { + return Arrays.asList( + new Object[][] { + {"a0", mapOf()}, + {"a201020304", mapOf(1, 2, 3, 4)}, + {"a26161016162820203", mapOf("a", 1, "b", listOf(2, 3))}, + {"826161a161626163", listOf("a", mapOf("b", "c"))}, + { + "a56161614161626142616361436164614461656145", + mapOf("c", "C", "d", "D", "a", "A", "b", "B", "e", "E") + }, + }); + } - @Test - public void testKeyOrder() { - assertCborEncode(cborHex, value); - } + @Test + public void testMap() { + assertCborEncodeAndDecode(cborHex, value); } + } - public static class OtherTests { - @Test(expected = IllegalArgumentException.class) - public void testDecodeIntOutOfRange() { - Cbor.decode(decodeHex("1a80000000")); - } + /** Tests order of keys */ + @RunWith(Parameterized.class) + public static class KeyOrderTest extends ParametrizedCborTest { + @Parameters + public static Collection data() { + return Arrays.asList( + new Object[][] { + {"a30100413200613300", mapOf("3", 0, "2".getBytes(), 0, 1, 0)}, + {"a3190100004000613300", mapOf("3", 0, "".getBytes(), 0, 256, 0)}, + {"a4000018ff00190100001a7fffffff00", mapOf(Integer.MAX_VALUE, 0, 255, 0, 256, 0, 0, 0)}, + { + "a3413300423232004331313100", + mapOf("22".getBytes(), 0, "3".getBytes(), 0, "111".getBytes(), 0) + }, + { + "a3433030310043303032004330303300", + mapOf("001".getBytes(), 0, "003".getBytes(), 0, "002".getBytes(), 0) + }, + {"a2f400f500", mapOf(true, 0, false, 0)}, + {"a3613100623130006331303000", mapOf("1", 0, "100", 0, "10", 0)} + }); } - // helper methods - private static Object wrapInt(Object value) { - if (value instanceof Number) { - return ((Number) value).intValue(); - } - return value; + @Test + public void testKeyOrder() { + assertCborEncode(cborHex, value); } + } - private static Map mapOf(Object... items) { - Map map = new HashMap<>(); - for (int i = 0; i < items.length; i += 2) { - map.put(wrapInt(items[i]), wrapInt(items[i + 1])); - } - return map; + public static class OtherTests { + @Test(expected = IllegalArgumentException.class) + public void testDecodeIntOutOfRange() { + Cbor.decode(decodeHex("1a80000000")); } + } - private static List listOf(Object... items) { - List list = new ArrayList<>(); - for (Object item : items) { - list.add(wrapInt(item)); - } - return list; + // helper methods + private static Object wrapInt(Object value) { + if (value instanceof Number) { + return ((Number) value).intValue(); } + return value; + } - private static void assertCborEncode(String expectedHex, Object value) { - byte[] encoded = Cbor.encode(value); - Assert.assertArrayEquals( - String.format("Expected to encode to %s, but got %s", - expectedHex, - encodeHex(encoded)), - decodeHex(expectedHex), - encoded - ); + private static Map mapOf(Object... items) { + Map map = new HashMap<>(); + for (int i = 0; i < items.length; i += 2) { + map.put(wrapInt(items[i]), wrapInt(items[i + 1])); } + return map; + } - private static void assertCborDecode(Object expected, String cborHex) { - Object actual = Cbor.decode(decodeHex(cborHex)); - if (expected instanceof byte[]) { - byte[] expectBytes = (byte[]) expected; - byte[] actualBytes = (byte[]) actual; - Assert.assertNotNull(actualBytes); - Assert.assertArrayEquals( - String.format("Expected to decode into %s, but got %s", - encodeHex(expectBytes), - encodeHex(actualBytes) - ), - expectBytes, - actualBytes - ); - } else { - Assert.assertEquals( - String.format("Expected to decode into %s, but got %s", expected, actual), - wrapInt(expected), - actual - ); - } + private static List listOf(Object... items) { + List list = new ArrayList<>(); + for (Object item : items) { + list.add(wrapInt(item)); } + return list; + } + + private static void assertCborEncode(String expectedHex, Object value) { + byte[] encoded = Cbor.encode(value); + Assert.assertArrayEquals( + String.format("Expected to encode to %s, but got %s", expectedHex, encodeHex(encoded)), + decodeHex(expectedHex), + encoded); + } - private static void assertCborEncodeAndDecode(String expectedHex, Object value) { - assertCborEncode(expectedHex, value); - assertCborDecode(value, expectedHex); + private static void assertCborDecode(Object expected, String cborHex) { + Object actual = Cbor.decode(decodeHex(cborHex)); + if (expected instanceof byte[]) { + byte[] expectBytes = (byte[]) expected; + byte[] actualBytes = (byte[]) actual; + Assert.assertNotNull(actualBytes); + Assert.assertArrayEquals( + String.format( + "Expected to decode into %s, but got %s", + encodeHex(expectBytes), encodeHex(actualBytes)), + expectBytes, + actualBytes); + } else { + Assert.assertEquals( + String.format("Expected to decode into %s, but got %s", expected, actual), + wrapInt(expected), + actual); } -} \ No newline at end of file + } + + private static void assertCborEncodeAndDecode(String expectedHex, Object value) { + assertCborEncode(expectedHex, value); + assertCborDecode(value, expectedHex); + } +} diff --git a/fido/src/test/java/com/yubico/yubikit/fido/CoseTest.java b/fido/src/test/java/com/yubico/yubikit/fido/CoseTest.java index 5994f6a1..4a44a464 100644 --- a/fido/src/test/java/com/yubico/yubikit/fido/CoseTest.java +++ b/fido/src/test/java/com/yubico/yubikit/fido/CoseTest.java @@ -17,180 +17,180 @@ package com.yubico.yubikit.fido; import com.yubico.yubikit.core.internal.codec.Base64; - -import org.junit.Assert; -import org.junit.Test; - import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.spec.InvalidKeySpecException; import java.util.HashMap; import java.util.Map; +import org.junit.Assert; +import org.junit.Test; public class CoseTest { - private static final Map EMPTY_COSE = new HashMap<>(); + private static final Map EMPTY_COSE = new HashMap<>(); - private static final Map RS256 = new HashMap<>(); - @SuppressWarnings("SpellCheckingInspection") - private static final String RS256_PUB = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0KeO-wu" + - "DQK18v9WwN5hFe6G_1TM4Ra8alOFa8cyN9xfqaLK1TvYVQHZfOcVvgM5XztCEOPNcQ5AWMJmTOESwvjuHkj" + - "5ulGt2jCVJUWKxPX-KYq0UFlb5jr305D66p5vRKb7zBterpDJSOxwLKr7g9jVhgpM2mgVjrRnQPMUAfvt8q" + - "9QMUWy1eIgIxnABi9b28cZ6WBDi42LMYiHz8mfUWi_ga9TASAwTqYZmGFUr7Z71ZuPKxuOxsgTxUksqKEmJ" + - "w8iWcCgTC6-O8sMe-aZ3gqcwDEk9kRKZQJKlxtyYuArn2zDKfaAHJ1A2wLwjtq8m_TsiOEdW3289Fe_F4gS" + - "A_wIDAQAB"; - - @SuppressWarnings("SpellCheckingInspection") - private static final String RS256_N = "0KeO-wuDQK18v9WwN5hFe6G_1TM4Ra8alOFa8cyN9xfqaLK1TvYVQ" + - "HZfOcVvgM5XztCEOPNcQ5AWMJmTOESwvjuHkj5ulGt2jCVJUWKxPX-KYq0UFlb5jr305D66p5vRKb7zBter" + - "pDJSOxwLKr7g9jVhgpM2mgVjrRnQPMUAfvt8q9QMUWy1eIgIxnABi9b28cZ6WBDi42LMYiHz8mfUWi_ga9T" + - "ASAwTqYZmGFUr7Z71ZuPKxuOxsgTxUksqKEmJw8iWcCgTC6-O8sMe-aZ3gqcwDEk9kRKZQJKlxtyYuArn2z" + - "DKfaAHJ1A2wLwjtq8m_TsiOEdW3289Fe_F4gSA_w"; - - @SuppressWarnings("SpellCheckingInspection") - private static final String RS256_E = "AAEAAQ"; - - private static final Map ES256 = new HashMap<>(); - - @SuppressWarnings("SpellCheckingInspection") - private static final String ES256_PUB = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwYXQNcHYEQHhLWs" + - "sYM3Wxh59Glcd27iQRAbH7g73zEfw3nbfNHww9DdUZVXRCbWETV_QEQb3PiZAhIdamjpdfA"; - - @SuppressWarnings("SpellCheckingInspection") - private static final String ES256_X = "wYXQNcHYEQHhLWssYM3Wxh59Glcd27iQRAbH7g73zEc"; - - @SuppressWarnings("SpellCheckingInspection") - private static final String ES256_Y = "8N523zR8MPQ3VGVV0Qm1hE1f0BEG9z4mQISHWpo6XXw"; - - private static final Map ES384 = new HashMap<>(); - - @SuppressWarnings("SpellCheckingInspection") - private static final String ES384_PUB = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEetBCP2oYwt-gkaDtb4e" + - "Ry_QwdcywdSYvTtzpXMNxwfby4npVyJJ1yktnFhgi9ftU1VpkK0DSb8XIv-k7cJiU5eT1m8YYu8nlV7hKCz" + - "5_YzDtsprXCaHMhv37XGiENkLp"; - - @SuppressWarnings("SpellCheckingInspection") - private static final String ES384_X = "etBCP2oYwt-gkaDtb4eRy_QwdcywdSYvTtzpXMNxwfby4npVyJJ1y" + - "ktnFhgi9ftU"; - - @SuppressWarnings("SpellCheckingInspection") - private static final String ES384_Y = "1VpkK0DSb8XIv-k7cJiU5eT1m8YYu8nlV7hKCz5_YzDtsprXCaHMh" + - "v37XGiENkLp"; - - - private static final Map ES512 = new HashMap<>(); - - @SuppressWarnings("SpellCheckingInspection") - private static final String ES512_PUB = "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBt0sGA8_brqo0_oR" + - "LGGxxndU6NZAsPLE0Bi6IS6MCoNILVI6uonHWOTMsfM2hoD5AM2Jm1VM8tQwC41jGxzQD6Q4BBMI0mQapyT" + - "TeA5SpUnZDN7fnbkRHAU2BzOLTvzWA2y6R8f87ghp-2pRtn_mKEqjzJLjOW0-ECHjmMiLGH8Qww88"; - - @SuppressWarnings("SpellCheckingInspection") - private static final String ES512_X = "AbdLBgPP266qNP6ESxhscZ3VOjWQLDyxNAYuiEujAqDSC1SOrqJx1" + - "jkzLHzNoaA-QDNiZtVTPLUMAuNYxsc0A-kO"; - - @SuppressWarnings("SpellCheckingInspection") - private static final String ES512_Y = "AQTCNJkGqck03gOUqVJ2Qze3525ERwFNgczi0781gNsukfH_O4Iaf" + - "tqUbZ_5ihKo8yS4zltPhAh45jIixh_EMMPP"; - - private static final Map EDDSA = new HashMap<>(); - - @SuppressWarnings("SpellCheckingInspection") - private static final String EDDSA_PUB = "MCowBQYDK2VwAyEA3wIKsJK63Ctb-nLkcwG8fJOp2vZxz8lmhv3" + - "BcFI-ves"; - - @SuppressWarnings("SpellCheckingInspection") - private static final String EDDSA_RAW_KEY = "3wIKsJK63Ctb-nLkcwG8fJOp2vZxz8lmhv3BcFI-ves"; - - static { - RS256.put(1, 3); // kty - RS256.put(3, -257); // alg - RS256.put(-1, decode(RS256_N)); // n - RS256.put(-2, decode(RS256_E)); // e - - ES256.put(1, 2); // kty - ES256.put(3, -7); // alg - ES256.put(-1, 1); // crv - ES256.put(-2, decode(ES256_X)); // x - ES256.put(-3, decode(ES256_Y)); // y - - ES384.put(1, 2); // kty - ES384.put(3, -7); // alg - ES384.put(-1, 2); // crv - ES384.put(-2, decode(ES384_X)); // x - ES384.put(-3, decode(ES384_Y)); // y - - ES512.put(1, 2); // kty - ES512.put(3, -7); // alg - ES512.put(-1, 3); // crv - ES512.put(-2, decode(ES512_X)); // x - ES512.put(-3, decode(ES512_Y)); // y - - EDDSA.put(1, 1); // kty - EDDSA.put(3, -8); // alg - EDDSA.put(-1, 6); // crv - EDDSA.put(-2, decode(EDDSA_RAW_KEY)); // raw key - } - - @Test(expected = NullPointerException.class) - public void getAlgorithmOnInvalidData() { - Assert.assertNull(Cose.getAlgorithm(EMPTY_COSE)); - } - - @Test - public void getAlgorithm() { - Assert.assertEquals(Integer.valueOf(-257), Cose.getAlgorithm(RS256)); - Assert.assertEquals(Integer.valueOf(-7), Cose.getAlgorithm(ES256)); - Assert.assertEquals(Integer.valueOf(-7), Cose.getAlgorithm(ES384)); - Assert.assertEquals(Integer.valueOf(-7), Cose.getAlgorithm(ES512)); - Assert.assertEquals(Integer.valueOf(-8), Cose.getAlgorithm(EDDSA)); - } - - @Test - public void getPublicKeyRS256() - throws InvalidKeySpecException, NoSuchAlgorithmException, NullPointerException { - PublicKey publicKey = Cose.getPublicKey(RS256); - Assert.assertNotNull(publicKey); - Assert.assertEquals(RS256_PUB, encode(publicKey.getEncoded())); - } - - @Test - public void getPublicKeyES256() - throws InvalidKeySpecException, NoSuchAlgorithmException, NullPointerException { - PublicKey publicKey = Cose.getPublicKey(ES256); - Assert.assertNotNull(publicKey); - Assert.assertEquals(ES256_PUB, encode(publicKey.getEncoded())); - } - - @Test - public void getPublicKeyES384() - throws InvalidKeySpecException, NoSuchAlgorithmException, NullPointerException { - PublicKey publicKey = Cose.getPublicKey(ES384); - Assert.assertNotNull(publicKey); - Assert.assertEquals(ES384_PUB, encode(publicKey.getEncoded())); - } - - @Test - public void getPublicKeyES512() - throws InvalidKeySpecException, NoSuchAlgorithmException, NullPointerException { - PublicKey publicKey = Cose.getPublicKey(ES512); - Assert.assertNotNull(publicKey); - Assert.assertEquals(ES512_PUB, encode(publicKey.getEncoded())); - } - - @Test - public void getPublicKeyEDDSA() - throws InvalidKeySpecException, NoSuchAlgorithmException, NullPointerException { - PublicKey publicKey = Cose.getPublicKey(EDDSA); - Assert.assertNotNull(publicKey); - Assert.assertEquals(EDDSA_PUB, encode(publicKey.getEncoded())); - } - - private static byte[] decode(String urlSafeBase64) { - return Base64.fromUrlSafeString(urlSafeBase64); - } - - private static String encode(byte[] data) { - return Base64.toUrlSafeString(data); - } -} \ No newline at end of file + private static final Map RS256 = new HashMap<>(); + + @SuppressWarnings("SpellCheckingInspection") + private static final String RS256_PUB = + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0KeO-wuDQK18v9WwN5hFe6G_1TM4Ra8alOFa8cyN9xfqaLK" + + "1TvYVQHZfOcVvgM5XztCEOPNcQ5AWMJmTOESwvjuHkj5ulGt2jCVJUWKxPX-KYq0UFlb5jr305D66p5vRKb7z" + + "BterpDJSOxwLKr7g9jVhgpM2mgVjrRnQPMUAfvt8q9QMUWy1eIgIxnABi9b28cZ6WBDi42LMYiHz8mfUWi_ga" + + "9TASAwTqYZmGFUr7Z71ZuPKxuOxsgTxUksqKEmJw8iWcCgTC6-O8sMe-aZ3gqcwDEk9kRKZQJKlxtyYuArn2z" + + "DKfaAHJ1A2wLwjtq8m_TsiOEdW3289Fe_F4gSA_wIDAQAB"; + + @SuppressWarnings("SpellCheckingInspection") + private static final String RS256_N = + "0KeO-wuDQK18v9WwN5hFe6G_1TM4Ra8alOFa8cyN9xfqaLK1TvYVQHZfOcVvgM5XztCEOPNcQ5AWMJmTOESwvjuHkj5" + + "ulGt2jCVJUWKxPX-KYq0UFlb5jr305D66p5vRKb7zBterpDJSOxwLKr7g9jVhgpM2mgVjrRnQPMUAfvt8q9QM" + + "UWy1eIgIxnABi9b28cZ6WBDi42LMYiHz8mfUWi_ga9TASAwTqYZmGFUr7Z71ZuPKxuOxsgTxUksqKEmJw8iWc" + + "CgTC6-O8sMe-aZ3gqcwDEk9kRKZQJKlxtyYuArn2zDKfaAHJ1A2wLwjtq8m_TsiOEdW3289Fe_F4gSA_w"; + + @SuppressWarnings("SpellCheckingInspection") + private static final String RS256_E = "AAEAAQ"; + + private static final Map ES256 = new HashMap<>(); + + @SuppressWarnings("SpellCheckingInspection") + private static final String ES256_PUB = + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwYXQNcHYEQHhLWssYM3Wxh59Glcd27iQRAbH7g73zEfw3nbfNHww9Dd" + + "UZVXRCbWETV_QEQb3PiZAhIdamjpdfA"; + + @SuppressWarnings("SpellCheckingInspection") + private static final String ES256_X = "wYXQNcHYEQHhLWssYM3Wxh59Glcd27iQRAbH7g73zEc"; + + @SuppressWarnings("SpellCheckingInspection") + private static final String ES256_Y = "8N523zR8MPQ3VGVV0Qm1hE1f0BEG9z4mQISHWpo6XXw"; + + private static final Map ES384 = new HashMap<>(); + + @SuppressWarnings("SpellCheckingInspection") + private static final String ES384_PUB = + "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEetBCP2oYwt-gkaDtb4eRy_QwdcywdSYvTtzpXMNxwfby4npVyJJ1yktnFhg" + + "i9ftU1VpkK0DSb8XIv-k7cJiU5eT1m8YYu8nlV7hKCz5_YzDtsprXCaHMhv37XGiENkLp"; + + @SuppressWarnings("SpellCheckingInspection") + private static final String ES384_X = + "etBCP2oYwt-gkaDtb4eRy_QwdcywdSYvTtzpXMNxwfby4npVyJJ1yktnFhgi9ftU"; + + @SuppressWarnings("SpellCheckingInspection") + private static final String ES384_Y = + "1VpkK0DSb8XIv-k7cJiU5eT1m8YYu8nlV7hKCz5_YzDtsprXCaHMhv37XGiENkLp"; + + private static final Map ES512 = new HashMap<>(); + + @SuppressWarnings("SpellCheckingInspection") + private static final String ES512_PUB = + "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBt0sGA8_brqo0_oRLGGxxndU6NZAsPLE0Bi6IS6MCoNILVI6uonHWOTM" + + "sfM2hoD5AM2Jm1VM8tQwC41jGxzQD6Q4BBMI0mQapyTTeA5SpUnZDN7fnbkRHAU2BzOLTvzWA2y6R8f87ghp-" + + "2pRtn_mKEqjzJLjOW0-ECHjmMiLGH8Qww88"; + + @SuppressWarnings("SpellCheckingInspection") + private static final String ES512_X = + "AbdLBgPP266qNP6ESxhscZ3VOjWQLDyxNAYuiEujAqDSC1SOrqJx1jkzLHzNoaA-QDNiZtVTPLUMAuNYxsc0A-kO"; + + @SuppressWarnings("SpellCheckingInspection") + private static final String ES512_Y = + "AQTCNJkGqck03gOUqVJ2Qze3525ERwFNgczi0781gNsukfH_O4IaftqUbZ_5ihKo8yS4zltPhAh45jIixh_EMMPP"; + + private static final Map EDDSA = new HashMap<>(); + + @SuppressWarnings("SpellCheckingInspection") + private static final String EDDSA_PUB = + "MCowBQYDK2VwAyEA3wIKsJK63Ctb-nLkcwG8fJOp2vZxz8lmhv3BcFI-ves"; + + @SuppressWarnings("SpellCheckingInspection") + private static final String EDDSA_RAW_KEY = "3wIKsJK63Ctb-nLkcwG8fJOp2vZxz8lmhv3BcFI-ves"; + + static { + RS256.put(1, 3); // kty + RS256.put(3, -257); // alg + RS256.put(-1, decode(RS256_N)); // n + RS256.put(-2, decode(RS256_E)); // e + + ES256.put(1, 2); // kty + ES256.put(3, -7); // alg + ES256.put(-1, 1); // crv + ES256.put(-2, decode(ES256_X)); // x + ES256.put(-3, decode(ES256_Y)); // y + + ES384.put(1, 2); // kty + ES384.put(3, -7); // alg + ES384.put(-1, 2); // crv + ES384.put(-2, decode(ES384_X)); // x + ES384.put(-3, decode(ES384_Y)); // y + + ES512.put(1, 2); // kty + ES512.put(3, -7); // alg + ES512.put(-1, 3); // crv + ES512.put(-2, decode(ES512_X)); // x + ES512.put(-3, decode(ES512_Y)); // y + + EDDSA.put(1, 1); // kty + EDDSA.put(3, -8); // alg + EDDSA.put(-1, 6); // crv + EDDSA.put(-2, decode(EDDSA_RAW_KEY)); // raw key + } + + private static byte[] decode(String urlSafeBase64) { + return Base64.fromUrlSafeString(urlSafeBase64); + } + + private static String encode(byte[] data) { + return Base64.toUrlSafeString(data); + } + + @Test(expected = NullPointerException.class) + public void getAlgorithmOnInvalidData() { + Assert.assertNull(Cose.getAlgorithm(EMPTY_COSE)); + } + + @Test + public void getAlgorithm() { + Assert.assertEquals(Integer.valueOf(-257), Cose.getAlgorithm(RS256)); + Assert.assertEquals(Integer.valueOf(-7), Cose.getAlgorithm(ES256)); + Assert.assertEquals(Integer.valueOf(-7), Cose.getAlgorithm(ES384)); + Assert.assertEquals(Integer.valueOf(-7), Cose.getAlgorithm(ES512)); + Assert.assertEquals(Integer.valueOf(-8), Cose.getAlgorithm(EDDSA)); + } + + @Test + public void getPublicKeyRS256() + throws InvalidKeySpecException, NoSuchAlgorithmException, NullPointerException { + PublicKey publicKey = Cose.getPublicKey(RS256); + Assert.assertNotNull(publicKey); + Assert.assertEquals(RS256_PUB, encode(publicKey.getEncoded())); + } + + @Test + public void getPublicKeyES256() + throws InvalidKeySpecException, NoSuchAlgorithmException, NullPointerException { + PublicKey publicKey = Cose.getPublicKey(ES256); + Assert.assertNotNull(publicKey); + Assert.assertEquals(ES256_PUB, encode(publicKey.getEncoded())); + } + + @Test + public void getPublicKeyES384() + throws InvalidKeySpecException, NoSuchAlgorithmException, NullPointerException { + PublicKey publicKey = Cose.getPublicKey(ES384); + Assert.assertNotNull(publicKey); + Assert.assertEquals(ES384_PUB, encode(publicKey.getEncoded())); + } + + @Test + public void getPublicKeyES512() + throws InvalidKeySpecException, NoSuchAlgorithmException, NullPointerException { + PublicKey publicKey = Cose.getPublicKey(ES512); + Assert.assertNotNull(publicKey); + Assert.assertEquals(ES512_PUB, encode(publicKey.getEncoded())); + } + + @Test + public void getPublicKeyEDDSA() + throws InvalidKeySpecException, NoSuchAlgorithmException, NullPointerException { + PublicKey publicKey = Cose.getPublicKey(EDDSA); + Assert.assertNotNull(publicKey); + Assert.assertEquals(EDDSA_PUB, encode(publicKey.getEncoded())); + } +} diff --git a/fido/src/test/java/com/yubico/yubikit/fido/TestUtils.java b/fido/src/test/java/com/yubico/yubikit/fido/TestUtils.java index b45469a1..e4d2dcf6 100644 --- a/fido/src/test/java/com/yubico/yubikit/fido/TestUtils.java +++ b/fido/src/test/java/com/yubico/yubikit/fido/TestUtils.java @@ -19,17 +19,17 @@ import com.yubico.yubikit.core.util.StringUtils; public class TestUtils { - public static byte[] decodeHex(String s) { - int len = s.length(); - byte[] data = new byte[len / 2]; - for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) - + Character.digit(s.charAt(i + 1), 16)); - } - return data; + public static byte[] decodeHex(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = + (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16)); } + return data; + } - public static String encodeHex(byte[] data) { - return StringUtils.bytesToHex(data).replace(" ", ""); - } + public static String encodeHex(byte[] data) { + return StringUtils.bytesToHex(data).replace(" ", ""); + } } diff --git a/fido/src/test/java/com/yubico/yubikit/fido/client/BasicWebAuthnClientUtilsTest.java b/fido/src/test/java/com/yubico/yubikit/fido/client/BasicWebAuthnClientUtilsTest.java index 3377ba5f..439c14a1 100644 --- a/fido/src/test/java/com/yubico/yubikit/fido/client/BasicWebAuthnClientUtilsTest.java +++ b/fido/src/test/java/com/yubico/yubikit/fido/client/BasicWebAuthnClientUtilsTest.java @@ -39,9 +39,6 @@ import com.yubico.yubikit.fido.ctap.Ctap2Session; import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialDescriptor; - -import org.junit.Test; - import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; @@ -49,384 +46,353 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - import javax.annotation.Nullable; +import org.junit.Test; public class BasicWebAuthnClientUtilsTest { - static final String RP_EXAMPLE = "example.com"; - - @Test - public void testFilterCredsOnEmptyList() throws Throwable { - assertNull(BasicWebAuthnClient.Utils.filterCreds( - new CtapMockBuilder().build(), - RP_EXAMPLE, - Collections.emptyList(), - RP_EXAMPLE, - null, - null)); - - assertNull(BasicWebAuthnClient.Utils.filterCreds( - new CtapMockBuilder() - .credentialsForRpId(RP_EXAMPLE, credId(16)) - .build(), - RP_EXAMPLE, - Collections.emptyList(), - RP_EXAMPLE, - null, - null)); - } - - @Test - public void testFilterCredsByRpId() throws Throwable { - - byte[] cred1 = credId("CRED1"); - byte[] cred2 = credId("CRED2"); - - List descriptors = Arrays.asList( - new PublicKeyCredentialDescriptor(PUBLIC_KEY, cred1), - new PublicKeyCredentialDescriptor(PUBLIC_KEY, credId(16)) - ); - - // cred1 will not be found in test.com and is not present for RP_EXAMPLE - assertNull(BasicWebAuthnClient.Utils.filterCreds( - new CtapMockBuilder() - .credentialsForRpId("test.com", - credId(16), - cred1, - credId(16)) - .credentialsForRpId(RP_EXAMPLE, - cred2) - .build(), - RP_EXAMPLE, - descriptors, - RP_EXAMPLE, - null, - null)); - - // cred1 will be found in test.com - PublicKeyCredentialDescriptor desc = BasicWebAuthnClient.Utils.filterCreds( - new CtapMockBuilder() - .credentialsForRpId("test.com", - credId(16), - cred1, - credId(16)) - .credentialsForRpId(RP_EXAMPLE, - cred2) - .build(), - "test.com", - descriptors, - "test.com", - null, - null); - assertNotNull(desc); - assertArrayEquals(cred1, desc.getId()); - } - - @Test - public void testFilterCredsPinUvParams() throws Throwable { - - byte[] cred1 = credId("CRED1"); - - Ctap2Session ctap = new CtapMockBuilder() - .credentialsForRpId(RP_EXAMPLE, - credId(16), - cred1, - credId(16)) - .build(); - - List descriptors = Arrays.asList( - new PublicKeyCredentialDescriptor(PUBLIC_KEY, cred1), - new PublicKeyCredentialDescriptor(PUBLIC_KEY, credId(16)) - ); - - PinUvAuthProtocol mockPinUvAuthProtocol = mock(PinUvAuthProtocol.class); - doReturn(new byte[32]).when(mockPinUvAuthProtocol).authenticate(any(), any()); - byte[] pinUvAuthToken = new byte[32]; - - assertNotNull(BasicWebAuthnClient.Utils.filterCreds( - ctap, - RP_EXAMPLE, - descriptors, - RP_EXAMPLE, - mockPinUvAuthProtocol, - null)); - - // no authenticate is called - verify(mockPinUvAuthProtocol, never()).authenticate(any(), any()); - // check that null pinUv params are passed to getAssertions - verify(ctap, atLeastOnce()).getAssertions( - anyString(), any(), any(), any(), any(), - isNull(), isNull(), any() - ); - - assertNotNull(BasicWebAuthnClient.Utils.filterCreds( - ctap, - RP_EXAMPLE, - descriptors, - RP_EXAMPLE, - null, - pinUvAuthToken)); - - // check that null pinUv params are passed to getAssertions - verify(ctap, atLeastOnce()).getAssertions( - anyString(), any(), any(), any(), any(), - isNull(), isNull(), any() - ); - - assertNotNull(BasicWebAuthnClient.Utils.filterCreds( - ctap, - RP_EXAMPLE, - descriptors, - RP_EXAMPLE, - mockPinUvAuthProtocol, - pinUvAuthToken)); - - // authenticate is called - verify(mockPinUvAuthProtocol, times(1)).authenticate(any(), any()); - // check that non-null pinUv params are passed to getAssertions - verify(ctap, atLeastOnce()).getAssertions( - anyString(), any(), any(), any(), any(), - isNotNull(), isNotNull(), any() - ); + static final String RP_EXAMPLE = "example.com"; + + @Test + public void testFilterCredsOnEmptyList() throws Throwable { + assertNull( + BasicWebAuthnClient.Utils.filterCreds( + new CtapMockBuilder().build(), + RP_EXAMPLE, + Collections.emptyList(), + RP_EXAMPLE, + null, + null)); + + assertNull( + BasicWebAuthnClient.Utils.filterCreds( + new CtapMockBuilder().credentialsForRpId(RP_EXAMPLE, credId(16)).build(), + RP_EXAMPLE, + Collections.emptyList(), + RP_EXAMPLE, + null, + null)); + } + + @Test + public void testFilterCredsByRpId() throws Throwable { + + byte[] cred1 = credId("CRED1"); + byte[] cred2 = credId("CRED2"); + + List descriptors = + Arrays.asList( + new PublicKeyCredentialDescriptor(PUBLIC_KEY, cred1), + new PublicKeyCredentialDescriptor(PUBLIC_KEY, credId(16))); + + // cred1 will not be found in test.com and is not present for RP_EXAMPLE + assertNull( + BasicWebAuthnClient.Utils.filterCreds( + new CtapMockBuilder() + .credentialsForRpId("test.com", credId(16), cred1, credId(16)) + .credentialsForRpId(RP_EXAMPLE, cred2) + .build(), + RP_EXAMPLE, + descriptors, + RP_EXAMPLE, + null, + null)); + + // cred1 will be found in test.com + PublicKeyCredentialDescriptor desc = + BasicWebAuthnClient.Utils.filterCreds( + new CtapMockBuilder() + .credentialsForRpId("test.com", credId(16), cred1, credId(16)) + .credentialsForRpId(RP_EXAMPLE, cred2) + .build(), + "test.com", + descriptors, + "test.com", + null, + null); + assertNotNull(desc); + assertArrayEquals(cred1, desc.getId()); + } + + @Test + public void testFilterCredsPinUvParams() throws Throwable { + + byte[] cred1 = credId("CRED1"); + + Ctap2Session ctap = + new CtapMockBuilder().credentialsForRpId(RP_EXAMPLE, credId(16), cred1, credId(16)).build(); + + List descriptors = + Arrays.asList( + new PublicKeyCredentialDescriptor(PUBLIC_KEY, cred1), + new PublicKeyCredentialDescriptor(PUBLIC_KEY, credId(16))); + + PinUvAuthProtocol mockPinUvAuthProtocol = mock(PinUvAuthProtocol.class); + doReturn(new byte[32]).when(mockPinUvAuthProtocol).authenticate(any(), any()); + byte[] pinUvAuthToken = new byte[32]; + + assertNotNull( + BasicWebAuthnClient.Utils.filterCreds( + ctap, RP_EXAMPLE, descriptors, RP_EXAMPLE, mockPinUvAuthProtocol, null)); + + // no authenticate is called + verify(mockPinUvAuthProtocol, never()).authenticate(any(), any()); + // check that null pinUv params are passed to getAssertions + verify(ctap, atLeastOnce()) + .getAssertions(anyString(), any(), any(), any(), any(), isNull(), isNull(), any()); + + assertNotNull( + BasicWebAuthnClient.Utils.filterCreds( + ctap, RP_EXAMPLE, descriptors, RP_EXAMPLE, null, pinUvAuthToken)); + + // check that null pinUv params are passed to getAssertions + verify(ctap, atLeastOnce()) + .getAssertions(anyString(), any(), any(), any(), any(), isNull(), isNull(), any()); + + assertNotNull( + BasicWebAuthnClient.Utils.filterCreds( + ctap, RP_EXAMPLE, descriptors, RP_EXAMPLE, mockPinUvAuthProtocol, pinUvAuthToken)); + + // authenticate is called + verify(mockPinUvAuthProtocol, times(1)).authenticate(any(), any()); + // check that non-null pinUv params are passed to getAssertions + verify(ctap, atLeastOnce()) + .getAssertions(anyString(), any(), any(), any(), any(), isNotNull(), isNotNull(), any()); + } + + @Test + public void testFilterCredsOfLongCredentialIds() throws Throwable { + byte[] longIdCred = credId(256); + byte[] target = credId(32); + + Ctap2Session ctap = + new CtapMockBuilder() + .maxCredentialIdLength(128) + .credentialsForRpId( + "rp.com", + target, + longIdCred, // longer than max, will be filtered out + credId(64), + credId(64)) + .build(); + + assertNull( + BasicWebAuthnClient.Utils.filterCreds( + ctap, + null, + Arrays.asList( + new PublicKeyCredentialDescriptor(PUBLIC_KEY, credId(16)), + new PublicKeyCredentialDescriptor(PUBLIC_KEY, longIdCred)), + "rp.com", + null, + null)); + + PublicKeyCredentialDescriptor cred = + BasicWebAuthnClient.Utils.filterCreds( + ctap, + null, + Arrays.asList( + new PublicKeyCredentialDescriptor(PUBLIC_KEY, credId(16)), + new PublicKeyCredentialDescriptor(PUBLIC_KEY, target)), + "rp.com", + null, + null); + assertNotNull(cred); + assertArrayEquals(target, cred.getId()); + } + + @Test + public void testFilterCredsChunking() throws Throwable { + byte[] target = credId(32); + + CtapMockBuilder ctapBuilder = + new CtapMockBuilder().maxCredentialIdLength(64).maxCredentialCountInList(8); + + PublicKeyCredentialDescriptor dummy = new PublicKeyCredentialDescriptor(PUBLIC_KEY, credId(48)); + + List descriptors = + Arrays.asList( + dummy, + dummy, + dummy, + dummy, + dummy, + dummy, + dummy, + dummy, + dummy, + dummy, + dummy, + dummy, + dummy, + dummy, + dummy, + dummy, + // target credential is the last in the last chunk + dummy, + dummy, + dummy, + dummy, + dummy, + dummy, + dummy, + new PublicKeyCredentialDescriptor(PUBLIC_KEY, target)); + + PublicKeyCredentialDescriptor cred = + BasicWebAuthnClient.Utils.filterCreds( + ctapBuilder.credentialsForRpId(RP_EXAMPLE, credId(64), target).build(), + null, + descriptors, + RP_EXAMPLE, + null, + null); + assertNotNull(cred); + assertArrayEquals(target, cred.getId()); + + cred = + BasicWebAuthnClient.Utils.filterCreds( + ctapBuilder.credentialsForRpId(RP_EXAMPLE, target, credId(64)).build(), + null, + descriptors, + RP_EXAMPLE, + null, + null); + assertNotNull(cred); + assertArrayEquals(target, cred.getId()); + + assertNull( + BasicWebAuthnClient.Utils.filterCreds( + ctapBuilder.credentialsForRpId(RP_EXAMPLE, credId(64)).build(), + null, + descriptors, + RP_EXAMPLE, + null, + null)); + + // try smaller chunks + cred = + BasicWebAuthnClient.Utils.filterCreds( + ctapBuilder + .credentialsForRpId(RP_EXAMPLE, credId(64), target) + .maxCredentialCountInList(3) + .build(), + null, + descriptors, + RP_EXAMPLE, + null, + null); + assertNotNull(cred); + assertArrayEquals(target, cred.getId()); + } + + @Test + public void testFilterCredsExceptionHandling() throws Throwable { + try { + BasicWebAuthnClient.Utils.filterCreds( + new CtapMockBuilder().throwCtapError(true).build(), + RP_EXAMPLE, + Collections.singletonList(new PublicKeyCredentialDescriptor(PUBLIC_KEY, credId("TEST"))), + RP_EXAMPLE, + null, + null); + } catch (Exception e) { + assertTrue(e instanceof CtapException); } - @Test - public void testFilterCredsOfLongCredentialIds() throws Throwable { - byte[] longIdCred = credId(256); - byte[] target = credId(32); - - Ctap2Session ctap = new CtapMockBuilder() - .maxCredentialIdLength(128) - .credentialsForRpId("rp.com", - target, - longIdCred, // longer than max, will be filtered out - credId(64), - credId(64)) - .build(); - - assertNull(BasicWebAuthnClient.Utils.filterCreds( - ctap, - null, - Arrays.asList( - new PublicKeyCredentialDescriptor(PUBLIC_KEY, credId(16)), - new PublicKeyCredentialDescriptor(PUBLIC_KEY, longIdCred) - ), - "rp.com", - null, - null)); - - PublicKeyCredentialDescriptor cred = BasicWebAuthnClient.Utils.filterCreds( - ctap, - null, - Arrays.asList( - new PublicKeyCredentialDescriptor(PUBLIC_KEY, credId(16)), - new PublicKeyCredentialDescriptor(PUBLIC_KEY, target) - ), - "rp.com", - null, - null); - assertNotNull(cred); - assertArrayEquals(target, cred.getId()); + try { + BasicWebAuthnClient.Utils.filterCreds( + new CtapMockBuilder().throwCtapError(true).build(), + "wrong.com", + Collections.emptyList(), + RP_EXAMPLE, + null, + null); + } catch (Exception e) { + assertTrue(e instanceof ClientError); } - @Test - public void testFilterCredsChunking() throws Throwable { - byte[] target = credId(32); - - CtapMockBuilder ctapBuilder = new CtapMockBuilder() - .maxCredentialIdLength(64) - .maxCredentialCountInList(8); - - PublicKeyCredentialDescriptor dummy = - new PublicKeyCredentialDescriptor( - PUBLIC_KEY, - credId(48)); - - List descriptors = Arrays.asList( - dummy, dummy, dummy, dummy, dummy, dummy, dummy, dummy, - dummy, dummy, dummy, dummy, dummy, dummy, dummy, dummy, - // target credential is the last in the last chunk - dummy, dummy, dummy, dummy, dummy, dummy, dummy, - new PublicKeyCredentialDescriptor(PUBLIC_KEY, target) - ); - - PublicKeyCredentialDescriptor cred = BasicWebAuthnClient.Utils.filterCreds( - ctapBuilder - .credentialsForRpId(RP_EXAMPLE, credId(64), target) - .build(), - null, - descriptors, - RP_EXAMPLE, - null, - null); - assertNotNull(cred); - assertArrayEquals(target, cred.getId()); - - cred = BasicWebAuthnClient.Utils.filterCreds( - ctapBuilder - .credentialsForRpId(RP_EXAMPLE, target, credId(64)) - .build(), - null, - descriptors, - RP_EXAMPLE, - null, - null); - assertNotNull(cred); - assertArrayEquals(target, cred.getId()); - - assertNull(BasicWebAuthnClient.Utils.filterCreds( - ctapBuilder - .credentialsForRpId(RP_EXAMPLE, credId(64)) - .build(), - null, - descriptors, - RP_EXAMPLE, - null, - null)); - - // try smaller chunks - cred = BasicWebAuthnClient.Utils.filterCreds( - ctapBuilder - .credentialsForRpId(RP_EXAMPLE, credId(64), target) - .maxCredentialCountInList(3) - .build(), - null, - descriptors, - RP_EXAMPLE, - null, - null); - assertNotNull(cred); - assertArrayEquals(target, cred.getId()); + try { + BasicWebAuthnClient.Utils.filterCreds( + new CtapMockBuilder().throwCtapError(true).build(), + RP_EXAMPLE, + Collections.emptyList(), + "." + RP_EXAMPLE, + null, + null); + } catch (Exception e) { + assertTrue(e instanceof ClientError); } + } - @Test - public void testFilterCredsExceptionHandling() throws Throwable { - try { - BasicWebAuthnClient.Utils.filterCreds( - new CtapMockBuilder() - .throwCtapError(true) - .build(), - RP_EXAMPLE, - Collections.singletonList( - new PublicKeyCredentialDescriptor(PUBLIC_KEY, credId("TEST"))), - RP_EXAMPLE, - null, - null); - } catch (Exception e) { - assertTrue(e instanceof CtapException); - } + private byte[] credId(int length) { + return RandomUtils.getRandomBytes(length); + } - try { - BasicWebAuthnClient.Utils.filterCreds( - new CtapMockBuilder() - .throwCtapError(true) - .build(), - "wrong.com", - Collections.emptyList(), - RP_EXAMPLE, - null, - null); - } catch (Exception e) { - assertTrue(e instanceof ClientError); - } + private byte[] credId(String string) { + return string.getBytes(StandardCharsets.UTF_8); + } - try { - BasicWebAuthnClient.Utils.filterCreds( - new CtapMockBuilder() - .throwCtapError(true) - .build(), - RP_EXAMPLE, - Collections.emptyList(), - "." + RP_EXAMPLE, - null, - null); - } catch (Exception e) { - assertTrue(e instanceof ClientError); - } + static class CtapMockBuilder { + @Nullable Integer maxCredentialIdLength = null; + @Nullable Integer maxCredentialCountInList = null; + @Nullable Map> credentialsForRpId = null; + boolean throwCtapError = false; + CtapMockBuilder maxCredentialIdLength(@Nullable Integer maxCredentialIdLength) { + this.maxCredentialIdLength = maxCredentialIdLength; + return this; } - private byte[] credId(int length) { - return RandomUtils.getRandomBytes(length); + CtapMockBuilder maxCredentialCountInList(@Nullable Integer maxCredentialCountInList) { + this.maxCredentialCountInList = maxCredentialCountInList; + return this; } - private byte[] credId(String string) { - return string.getBytes(StandardCharsets.UTF_8); - } - - static class CtapMockBuilder { - @Nullable - Integer maxCredentialIdLength = null; - @Nullable - Integer maxCredentialCountInList = null; - @Nullable - Map> credentialsForRpId = null; - boolean throwCtapError = false; - - CtapMockBuilder maxCredentialIdLength(@Nullable Integer maxCredentialIdLength) { - this.maxCredentialIdLength = maxCredentialIdLength; - return this; - } - - CtapMockBuilder maxCredentialCountInList(@Nullable Integer maxCredentialCountInList) { - this.maxCredentialCountInList = maxCredentialCountInList; - return this; - } - - CtapMockBuilder credentialsForRpId(@Nullable Map> credentialsForRpId) { - if (credentialsForRpId == null) { - this.credentialsForRpId = credentialsForRpId; - } else { - if (this.credentialsForRpId == null) { - this.credentialsForRpId = new HashMap<>(); - } - this.credentialsForRpId.putAll(credentialsForRpId); - } - return this; - } - - CtapMockBuilder credentialsForRpId(String rpId, byte[]... credentialIds) { - if (this.credentialsForRpId == null) { - this.credentialsForRpId = new HashMap<>(); - } - this.credentialsForRpId.put(rpId, Arrays.asList(credentialIds)); - return this; + CtapMockBuilder credentialsForRpId(@Nullable Map> credentialsForRpId) { + if (credentialsForRpId == null) { + this.credentialsForRpId = credentialsForRpId; + } else { + if (this.credentialsForRpId == null) { + this.credentialsForRpId = new HashMap<>(); } + this.credentialsForRpId.putAll(credentialsForRpId); + } + return this; + } - @SuppressWarnings("SameParameterValue") - CtapMockBuilder throwCtapError(boolean throwCtapError) { - this.throwCtapError = throwCtapError; - return this; - } + CtapMockBuilder credentialsForRpId(String rpId, byte[]... credentialIds) { + if (this.credentialsForRpId == null) { + this.credentialsForRpId = new HashMap<>(); + } + this.credentialsForRpId.put(rpId, Arrays.asList(credentialIds)); + return this; + } - Ctap2Session build() throws Throwable { + @SuppressWarnings("SameParameterValue") + CtapMockBuilder throwCtapError(boolean throwCtapError) { + this.throwCtapError = throwCtapError; + return this; + } - Ctap2Session.InfoData mockInfoData = mock(Ctap2Session.InfoData.class); - Ctap2Session ctapMock = mock(Ctap2Session.class); + Ctap2Session build() throws Throwable { - doReturn(maxCredentialIdLength) - .when(mockInfoData).getMaxCredentialIdLength(); - doReturn(maxCredentialCountInList) - .when(mockInfoData).getMaxCredentialCountInList(); - doReturn(mockInfoData) - .when(ctapMock).getCachedInfo(); + Ctap2Session.InfoData mockInfoData = mock(Ctap2Session.InfoData.class); + Ctap2Session ctapMock = mock(Ctap2Session.class); - when(ctapMock.getAssertions( - anyString(), - any(), - any(), - isNull(), - anyMap(), - isNull(), - isNull(), - isNull())).then(invocation -> { + doReturn(maxCredentialIdLength).when(mockInfoData).getMaxCredentialIdLength(); + doReturn(maxCredentialCountInList).when(mockInfoData).getMaxCredentialCountInList(); + doReturn(mockInfoData).when(ctapMock).getCachedInfo(); + when(ctapMock.getAssertions( + anyString(), any(), any(), isNull(), anyMap(), isNull(), isNull(), isNull())) + .then( + invocation -> { if (throwCtapError) { - throw new CtapException(CtapException.ERR_INVALID_PARAMETER); + throw new CtapException(CtapException.ERR_INVALID_PARAMETER); } - List idsForRp = credentialsForRpId != null + List idsForRp = + credentialsForRpId != null ? credentialsForRpId.get((String) invocation.getArgument(0)) : Collections.emptyList(); @@ -434,34 +400,33 @@ Ctap2Session build() throws Throwable { List ids; if (idsForRp != null) { - ids = new ArrayList<>(); - for (byte[] id : idsForRp) { - for (Map desc : allowList) { - byte[] descId = (byte[]) desc.get(PublicKeyCredentialDescriptor.ID); - if (Arrays.equals(id, descId)) { - ids.add(id); - break; - } - } + ids = new ArrayList<>(); + for (byte[] id : idsForRp) { + for (Map desc : allowList) { + byte[] descId = (byte[]) desc.get(PublicKeyCredentialDescriptor.ID); + if (Arrays.equals(id, descId)) { + ids.add(id); + break; + } } + } } else { - ids = Collections.emptyList(); + ids = Collections.emptyList(); } if (ids.isEmpty()) { - throw new CtapException(CtapException.ERR_NO_CREDENTIALS); + throw new CtapException(CtapException.ERR_NO_CREDENTIALS); } List list = new ArrayList<>(); for (byte[] bytes : ids) { - Ctap2Session.AssertionData assertionData = - mock(Ctap2Session.AssertionData.class); - when(assertionData.getCredentialId(isNull())).thenReturn(bytes); - list.add(assertionData); + Ctap2Session.AssertionData assertionData = mock(Ctap2Session.AssertionData.class); + when(assertionData.getCredentialId(isNull())).thenReturn(bytes); + list.add(assertionData); } return list; - }); + }); - return ctapMock; - } + return ctapMock; } + } } diff --git a/fido/src/test/java/com/yubico/yubikit/fido/ctap/ClientPinTest.java b/fido/src/test/java/com/yubico/yubikit/fido/ctap/ClientPinTest.java index ad274f12..b050d451 100755 --- a/fido/src/test/java/com/yubico/yubikit/fido/ctap/ClientPinTest.java +++ b/fido/src/test/java/com/yubico/yubikit/fido/ctap/ClientPinTest.java @@ -16,41 +16,57 @@ package com.yubico.yubikit.fido.ctap; +import static com.yubico.yubikit.fido.TestUtils.decodeHex; + +import java.util.Arrays; import org.junit.Assert; import org.junit.Test; -import java.util.Arrays; +public class ClientPinTest { + @Test + public void testPadPin() { + Assert.assertArrayEquals( + decodeHex("31323334"), ClientPin.preparePin("1234".toCharArray(), false)); + Assert.assertArrayEquals( + Arrays.copyOf(decodeHex("31323334"), 64), ClientPin.preparePin("1234".toCharArray(), true)); + Assert.assertArrayEquals( + decodeHex("666f6f626172"), ClientPin.preparePin("foobar".toCharArray(), false)); + Assert.assertArrayEquals( + Arrays.copyOf(decodeHex("666f6f626172"), 64), + ClientPin.preparePin("foobar".toCharArray(), true)); + Assert.assertEquals( + 64, + ClientPin.preparePin( + "123456789012345678901234567890123456789012345678901234567890123".toCharArray(), + true) + .length); + Assert.assertEquals( + 63, + ClientPin.preparePin( + "123456789012345678901234567890123456789012345678901234567890123".toCharArray(), + false) + .length); + } -import static com.yubico.yubikit.fido.TestUtils.decodeHex; + @Test(expected = IllegalArgumentException.class) + public void testTooShortPin() { + ClientPin.preparePin("123".toCharArray(), false); + } -public class ClientPinTest { - @Test - public void testPadPin() { - Assert.assertArrayEquals(decodeHex("31323334"), ClientPin.preparePin("1234".toCharArray(), false)); - Assert.assertArrayEquals(Arrays.copyOf(decodeHex("31323334"), 64), ClientPin.preparePin("1234".toCharArray(), true)); - Assert.assertArrayEquals(decodeHex("666f6f626172"), ClientPin.preparePin("foobar".toCharArray(), false)); - Assert.assertArrayEquals(Arrays.copyOf(decodeHex("666f6f626172"), 64), ClientPin.preparePin("foobar".toCharArray(), true)); - Assert.assertEquals(64, ClientPin.preparePin("123456789012345678901234567890123456789012345678901234567890123".toCharArray(), true).length); - Assert.assertEquals(63, ClientPin.preparePin("123456789012345678901234567890123456789012345678901234567890123".toCharArray(), false).length); - } - - @Test(expected = IllegalArgumentException.class) - public void testTooShortPin() { - ClientPin.preparePin("123".toCharArray(), false); - } - - @Test(expected = IllegalArgumentException.class) - public void testTooShortPinWithPad() { - ClientPin.preparePin("123".toCharArray(), true); - } - - @Test(expected = IllegalArgumentException.class) - public void testTooLongPin() { - ClientPin.preparePin("1234567890123456789012345678901234567890123456789012345678901234".toCharArray(), false); - } - - @Test(expected = IllegalArgumentException.class) - public void testTooLongPinWithPad() { - ClientPin.preparePin("1234567890123456789012345678901234567890123456789012345678901234".toCharArray(), true); - } + @Test(expected = IllegalArgumentException.class) + public void testTooShortPinWithPad() { + ClientPin.preparePin("123".toCharArray(), true); + } + + @Test(expected = IllegalArgumentException.class) + public void testTooLongPin() { + ClientPin.preparePin( + "1234567890123456789012345678901234567890123456789012345678901234".toCharArray(), false); + } + + @Test(expected = IllegalArgumentException.class) + public void testTooLongPinWithPad() { + ClientPin.preparePin( + "1234567890123456789012345678901234567890123456789012345678901234".toCharArray(), true); + } } diff --git a/fido/src/test/java/com/yubico/yubikit/fido/ctap/HkdfTest.java b/fido/src/test/java/com/yubico/yubikit/fido/ctap/HkdfTest.java index e8f9854c..72a0afd7 100644 --- a/fido/src/test/java/com/yubico/yubikit/fido/ctap/HkdfTest.java +++ b/fido/src/test/java/com/yubico/yubikit/fido/ctap/HkdfTest.java @@ -17,138 +17,141 @@ package com.yubico.yubikit.fido.ctap; import com.yubico.yubikit.testing.Codec; - -import org.junit.Assert; -import org.junit.Test; - import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import org.junit.Assert; +import org.junit.Test; @SuppressWarnings("SpellCheckingInspection") public class HkdfTest { - @Test - public void testCase1() throws NoSuchAlgorithmException, InvalidKeyException { - byte[] okm = new Hkdf( - "HmacSHA256" - ).digest(Codec.fromHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"), + @Test + public void testCase1() throws NoSuchAlgorithmException, InvalidKeyException { + byte[] okm = + new Hkdf("HmacSHA256") + .digest( + Codec.fromHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"), Codec.fromHex("000102030405060708090a0b0c"), Codec.fromHex("f0f1f2f3f4f5f6f7f8f9"), 42); - Assert.assertArrayEquals( - Codec.fromHex("3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf" + - "34007208d5b887185865"), - okm - ); - } - - @Test - public void testCase2() throws NoSuchAlgorithmException, InvalidKeyException { - byte[] okm = new Hkdf( - "HmacSHA256" - ).digest( - Codec.fromHex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + - "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142" + - "434445464748494a4b4c4d4e4f"), - Codec.fromHex("606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f" + - "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2" + - "a3a4a5a6a7a8a9aaabacadaeaf"), - Codec.fromHex("b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf" + - "d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2" + - "f3f4f5f6f7f8f9fafbfcfdfeff"), + Assert.assertArrayEquals( + Codec.fromHex( + "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"), + okm); + } + + @Test + public void testCase2() throws NoSuchAlgorithmException, InvalidKeyException { + byte[] okm = + new Hkdf("HmacSHA256") + .digest( + Codec.fromHex( + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021222324252" + + "62728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40414243444546474849" + + "4a4b4c4d4e4f"), + Codec.fromHex( + "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f8081828384858" + + "68788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9" + + "aaabacadaeaf"), + Codec.fromHex( + "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d" + + "6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9" + + "fafbfcfdfeff"), 82); - Assert.assertArrayEquals( - Codec.fromHex("b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c" + - "59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c5" + - "8179ec3e87c14c01d5c1f3434f1d87"), - okm - ); - } - - @Test - public void testCase3() throws NoSuchAlgorithmException, InvalidKeyException { - byte[] okm = new Hkdf( - "HmacSHA256" - ).digest(Codec.fromHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"), + Assert.assertArrayEquals( + Codec.fromHex( + "b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb4" + + "1c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87"), + okm); + } + + @Test + public void testCase3() throws NoSuchAlgorithmException, InvalidKeyException { + byte[] okm = + new Hkdf("HmacSHA256") + .digest( + Codec.fromHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"), new byte[0], new byte[0], 42); - Assert.assertArrayEquals( - Codec.fromHex("8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d" + - "9d201395faa4b61a96c8"), - okm - ); - } - - @Test - public void testCase4() throws NoSuchAlgorithmException, InvalidKeyException { - byte[] okm = new Hkdf( - "HmacSHA1" - ).digest(Codec.fromHex("0b0b0b0b0b0b0b0b0b0b0b"), + Assert.assertArrayEquals( + Codec.fromHex( + "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8"), + okm); + } + + @Test + public void testCase4() throws NoSuchAlgorithmException, InvalidKeyException { + byte[] okm = + new Hkdf("HmacSHA1") + .digest( + Codec.fromHex("0b0b0b0b0b0b0b0b0b0b0b"), Codec.fromHex("000102030405060708090a0b0c"), Codec.fromHex("f0f1f2f3f4f5f6f7f8f9"), 42); - Assert.assertArrayEquals( - Codec.fromHex("085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2" + - "c22e422478d305f3f896"), - okm - ); - } - - @Test - public void testCase5() throws NoSuchAlgorithmException, InvalidKeyException { - byte[] okm = new Hkdf( - "HmacSHA1" - ).digest(Codec.fromHex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1" + - "f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40414" + - "2434445464748494a4b4c4d4e4f"), - Codec.fromHex("606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f" + - "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2" + - "a3a4a5a6a7a8a9aaabacadaeaf"), - Codec.fromHex("b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf" + - "d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2" + - "f3f4f5f6f7f8f9fafbfcfdfeff"), + Assert.assertArrayEquals( + Codec.fromHex( + "085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2c22e422478d305f3f896"), + okm); + } + + @Test + public void testCase5() throws NoSuchAlgorithmException, InvalidKeyException { + byte[] okm = + new Hkdf("HmacSHA1") + .digest( + Codec.fromHex( + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021222324252" + + "62728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40414243444546474849" + + "4a4b4c4d4e4f"), + Codec.fromHex( + "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f8081828384858" + + "68788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9" + + "aaabacadaeaf"), + Codec.fromHex( + "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d" + + "6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9" + + "fafbfcfdfeff"), 82); - Assert.assertArrayEquals( - Codec.fromHex("0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ffe" + - "8fa3f1a4e5ad79f3f334b3b202b2173c486ea37ce3d397ed034c7f9dfeb15c5e927336" + - "d0441f4c4300e2cff0d0900b52d3b4"), - okm - ); - } - - @Test - public void testCase6() throws NoSuchAlgorithmException, InvalidKeyException { - byte[] okm = new Hkdf( - "HmacSHA1" - ).digest(Codec.fromHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"), + Assert.assertArrayEquals( + Codec.fromHex( + "0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ffe8fa3f1a4e5ad79f3f334b" + + "3b202b2173c486ea37ce3d397ed034c7f9dfeb15c5e927336d0441f4c4300e2cff0d0900b52d3b4"), + okm); + } + + @Test + public void testCase6() throws NoSuchAlgorithmException, InvalidKeyException { + byte[] okm = + new Hkdf("HmacSHA1") + .digest( + Codec.fromHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"), new byte[0], new byte[0], 42); - Assert.assertArrayEquals( - Codec.fromHex("0ac1af7002b3d761d1e55298da9d0506b9ae52057220a306e07b6b87e8df21d0" + - "ea00033de03984d34918"), - okm - ); - } - - @Test - public void testCase7() throws NoSuchAlgorithmException, InvalidKeyException { - byte[] okm = new Hkdf( - "HmacSHA1" - ).digest(Codec.fromHex("0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c"), + Assert.assertArrayEquals( + Codec.fromHex( + "0ac1af7002b3d761d1e55298da9d0506b9ae52057220a306e07b6b87e8df21d0ea00033de03984d34918"), + okm); + } + + @Test + public void testCase7() throws NoSuchAlgorithmException, InvalidKeyException { + byte[] okm = + new Hkdf("HmacSHA1") + .digest( + Codec.fromHex("0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c"), new byte[0], new byte[0], 42); - Assert.assertArrayEquals( - Codec.fromHex("2c91117204d745f3500d636a62f64f0ab3bae548aa53d423b0d1f27ebba6f5e5" + - "673a081d70cce7acfc48"), - okm - ); - } + Assert.assertArrayEquals( + Codec.fromHex( + "2c91117204d745f3500d636a62f64f0ab3bae548aa53d423b0d1f27ebba6f5e5673a081d70cce7acfc48"), + okm); + } } diff --git a/fido/src/test/java/com/yubico/yubikit/fido/ctap/PinUvAuthProtocolV1Test.java b/fido/src/test/java/com/yubico/yubikit/fido/ctap/PinUvAuthProtocolV1Test.java index 04bc65be..5fedbb3d 100644 --- a/fido/src/test/java/com/yubico/yubikit/fido/ctap/PinUvAuthProtocolV1Test.java +++ b/fido/src/test/java/com/yubico/yubikit/fido/ctap/PinUvAuthProtocolV1Test.java @@ -17,90 +17,86 @@ package com.yubico.yubikit.fido.ctap; import com.yubico.yubikit.fido.TestUtils; - +import java.math.BigInteger; import org.junit.Assert; import org.junit.Test; -import java.math.BigInteger; - public class PinUvAuthProtocolV1Test { - private final PinUvAuthProtocolV1 protocol = new PinUvAuthProtocolV1(); - - @Test - public void testEncrypt() { - byte[] pinToken = TestUtils.decodeHex("000102030405060708090a0b0c0d0e0f"); - - Assert.assertArrayEquals( - TestUtils.decodeHex("0a940bb5416ef045f1c39458c653ea5a"), - protocol.encrypt(pinToken, TestUtils.decodeHex("000102030405060708090a0b0c0d0e0f")) - ); - - Assert.assertArrayEquals( - TestUtils.decodeHex("c6a13b37878f5b826f4f8162a1c8d879"), - protocol.encrypt(pinToken, TestUtils.decodeHex("00000000000000000000000000000000")) - ); - } - - @Test - public void testDecrypt() { - byte[] pinToken = TestUtils.decodeHex("000102030405060708090a0b0c0d0e0f"); - - Assert.assertArrayEquals( - TestUtils.decodeHex("000102030405060708090a0b0c0d0e0f"), - protocol.decrypt(pinToken, TestUtils.decodeHex("0a940bb5416ef045f1c39458c653ea5a")) - ); - - Assert.assertArrayEquals( - TestUtils.decodeHex("00000000000000000000000000000000"), - protocol.decrypt(pinToken, TestUtils.decodeHex("c6a13b37878f5b826f4f8162a1c8d879")) - ); - } - - @Test - public void testAuthenticate() { - byte[] pinToken = TestUtils.decodeHex("000102030405060708090a0b0c0d0e0f"); - - Assert.assertArrayEquals( - TestUtils.decodeHex("9f3aa28826b37485ca05014d7142b3ea"), - protocol.authenticate(pinToken, TestUtils.decodeHex("000102030405060708090a0b0c0d0e0f")) - ); - - Assert.assertArrayEquals( - TestUtils.decodeHex("fe6e8016f7f241c07565b467688e20c7"), - protocol.authenticate(pinToken, TestUtils.decodeHex("00000000000000000000000000000000")) - ); - } - - @Test - public void testEncodeCoordinate() { - Assert.assertArrayEquals( - TestUtils.decodeHex("0000000000000000000000000000000000000000000000000000000000000001"), - PinUvAuthProtocolV1.encodeCoordinate(new BigInteger("1")) - ); - - Assert.assertArrayEquals( - TestUtils.decodeHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), - PinUvAuthProtocolV1.encodeCoordinate(new BigInteger("115792089237316195423570985008687907853269984665640564039457584007913129639935")) - ); - - Assert.assertArrayEquals( - TestUtils.decodeHex("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), - PinUvAuthProtocolV1.encodeCoordinate(new BigInteger("57896044618658097711785492504343953926634992332820282019728792003956564819967")) - ); - - Assert.assertArrayEquals( - TestUtils.decodeHex("01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), - PinUvAuthProtocolV1.encodeCoordinate(new BigInteger("904625697166532776746648320380374280103671755200316906558262375061821325311")) - ); - - Assert.assertArrayEquals( - TestUtils.decodeHex("00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), - PinUvAuthProtocolV1.encodeCoordinate(new BigInteger("452312848583266388373324160190187140051835877600158453279131187530910662655")) - ); - - Assert.assertArrayEquals( - TestUtils.decodeHex("007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), - PinUvAuthProtocolV1.encodeCoordinate(new BigInteger("226156424291633194186662080095093570025917938800079226639565593765455331327")) - ); - } + private final PinUvAuthProtocolV1 protocol = new PinUvAuthProtocolV1(); + + @Test + public void testEncrypt() { + byte[] pinToken = TestUtils.decodeHex("000102030405060708090a0b0c0d0e0f"); + + Assert.assertArrayEquals( + TestUtils.decodeHex("0a940bb5416ef045f1c39458c653ea5a"), + protocol.encrypt(pinToken, TestUtils.decodeHex("000102030405060708090a0b0c0d0e0f"))); + + Assert.assertArrayEquals( + TestUtils.decodeHex("c6a13b37878f5b826f4f8162a1c8d879"), + protocol.encrypt(pinToken, TestUtils.decodeHex("00000000000000000000000000000000"))); + } + + @Test + public void testDecrypt() { + byte[] pinToken = TestUtils.decodeHex("000102030405060708090a0b0c0d0e0f"); + + Assert.assertArrayEquals( + TestUtils.decodeHex("000102030405060708090a0b0c0d0e0f"), + protocol.decrypt(pinToken, TestUtils.decodeHex("0a940bb5416ef045f1c39458c653ea5a"))); + + Assert.assertArrayEquals( + TestUtils.decodeHex("00000000000000000000000000000000"), + protocol.decrypt(pinToken, TestUtils.decodeHex("c6a13b37878f5b826f4f8162a1c8d879"))); + } + + @Test + public void testAuthenticate() { + byte[] pinToken = TestUtils.decodeHex("000102030405060708090a0b0c0d0e0f"); + + Assert.assertArrayEquals( + TestUtils.decodeHex("9f3aa28826b37485ca05014d7142b3ea"), + protocol.authenticate(pinToken, TestUtils.decodeHex("000102030405060708090a0b0c0d0e0f"))); + + Assert.assertArrayEquals( + TestUtils.decodeHex("fe6e8016f7f241c07565b467688e20c7"), + protocol.authenticate(pinToken, TestUtils.decodeHex("00000000000000000000000000000000"))); + } + + @Test + public void testEncodeCoordinate() { + Assert.assertArrayEquals( + TestUtils.decodeHex("0000000000000000000000000000000000000000000000000000000000000001"), + PinUvAuthProtocolV1.encodeCoordinate(new BigInteger("1"))); + + Assert.assertArrayEquals( + TestUtils.decodeHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + PinUvAuthProtocolV1.encodeCoordinate( + new BigInteger( + "115792089237316195423570985008687907853269984665640564039457584007913129639935"))); + + Assert.assertArrayEquals( + TestUtils.decodeHex("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + PinUvAuthProtocolV1.encodeCoordinate( + new BigInteger( + "57896044618658097711785492504343953926634992332820282019728792003956564819967"))); + + Assert.assertArrayEquals( + TestUtils.decodeHex("01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + PinUvAuthProtocolV1.encodeCoordinate( + new BigInteger( + "904625697166532776746648320380374280103671755200316906558262375061821325311"))); + + Assert.assertArrayEquals( + TestUtils.decodeHex("00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + PinUvAuthProtocolV1.encodeCoordinate( + new BigInteger( + "452312848583266388373324160190187140051835877600158453279131187530910662655"))); + + Assert.assertArrayEquals( + TestUtils.decodeHex("007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + PinUvAuthProtocolV1.encodeCoordinate( + new BigInteger( + "226156424291633194186662080095093570025917938800079226639565593765455331327"))); + } } diff --git a/fido/src/test/java/com/yubico/yubikit/fido/ctap/PinUvAuthProtocolV2Test.java b/fido/src/test/java/com/yubico/yubikit/fido/ctap/PinUvAuthProtocolV2Test.java index fbf146fe..a6e52f5b 100644 --- a/fido/src/test/java/com/yubico/yubikit/fido/ctap/PinUvAuthProtocolV2Test.java +++ b/fido/src/test/java/com/yubico/yubikit/fido/ctap/PinUvAuthProtocolV2Test.java @@ -17,116 +17,89 @@ package com.yubico.yubikit.fido.ctap; import com.yubico.yubikit.fido.TestUtils; - -import org.junit.Assert; -import org.junit.Test; - import java.math.BigInteger; import java.nio.ByteBuffer; +import org.junit.Assert; +import org.junit.Test; public class PinUvAuthProtocolV2Test { - private final PinUvAuthProtocolV2 protocol = new PinUvAuthProtocolV2(); - - @Test - public void testEncryptDecrypt() { - byte[] pinToken = ByteBuffer.allocate(64) - .position(32) - .put(TestUtils.decodeHex( - "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f")) - .array(); - - byte[] ciphertext = protocol.encrypt( - pinToken, - TestUtils.decodeHex("000102030405060708090a0b0c0d0e0f") - ); - - Assert.assertEquals(32, ciphertext.length); - - byte[] plaintext = protocol.decrypt(pinToken, ciphertext); - Assert.assertArrayEquals( - TestUtils.decodeHex("000102030405060708090a0b0c0d0e0f"), - plaintext); - - ciphertext = protocol.encrypt( - pinToken, - TestUtils.decodeHex("00000000000000000000000000000000") - ); - - Assert.assertEquals(32, ciphertext.length); - - plaintext = protocol.decrypt(pinToken, ciphertext); - Assert.assertArrayEquals( - TestUtils.decodeHex("00000000000000000000000000000000"), - plaintext); - } - - @Test - public void testAuthenticate() { - byte[] pinToken = TestUtils.decodeHex( - "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f"); - - Assert.assertArrayEquals( - TestUtils.decodeHex( - "495d46aa392d51132edb93bc49e60ecaaeb7802f3ae529779d5883f9330af561"), - protocol.authenticate( - pinToken, - TestUtils.decodeHex("000102030405060708090a0b0c0d0e0f")) - ); - - Assert.assertArrayEquals( - TestUtils.decodeHex( - "7f0ea2b80504890f3c6d42a77e31c833e881f741d2125569ac6427aa0c466aad"), - protocol.authenticate( - pinToken, - TestUtils.decodeHex("00000000000000000000000000000000")) - ); - } - - @Test - public void testEncodeCoordinate() { - Assert.assertArrayEquals( - TestUtils.decodeHex( - "0000000000000000000000000000000000000000000000000000000000000001"), - PinUvAuthProtocolV2.encodeCoordinate(new BigInteger("1")) - ); - - Assert.assertArrayEquals( - TestUtils.decodeHex( - "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), - PinUvAuthProtocolV2.encodeCoordinate( - new BigInteger("11579208923731619542357098500868790785326998466564056" + - "4039457584007913129639935")) - ); - - Assert.assertArrayEquals( - TestUtils.decodeHex( - "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), - PinUvAuthProtocolV2.encodeCoordinate( - new BigInteger("57896044618658097711785492504343953926634992332820282" + - "019728792003956564819967")) - ); - - Assert.assertArrayEquals( - TestUtils.decodeHex( - "01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), - PinUvAuthProtocolV2.encodeCoordinate( - new BigInteger("90462569716653277674664832038037428010367175520031690" + - "6558262375061821325311")) - ); - - Assert.assertArrayEquals( - TestUtils.decodeHex( - "00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), - PinUvAuthProtocolV2.encodeCoordinate( - new BigInteger("45231284858326638837332416019018714005183587760015845" + - "3279131187530910662655")) - ); - - Assert.assertArrayEquals( + private final PinUvAuthProtocolV2 protocol = new PinUvAuthProtocolV2(); + + @Test + public void testEncryptDecrypt() { + byte[] pinToken = + ByteBuffer.allocate(64) + .position(32) + .put( TestUtils.decodeHex( - "007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), - PinUvAuthProtocolV2.encodeCoordinate(new BigInteger("226156424291633194186662" + - "080095093570025917938800079226639565593765455331327")) - ); - } + "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f")) + .array(); + + byte[] ciphertext = + protocol.encrypt(pinToken, TestUtils.decodeHex("000102030405060708090a0b0c0d0e0f")); + + Assert.assertEquals(32, ciphertext.length); + + byte[] plaintext = protocol.decrypt(pinToken, ciphertext); + Assert.assertArrayEquals(TestUtils.decodeHex("000102030405060708090a0b0c0d0e0f"), plaintext); + + ciphertext = + protocol.encrypt(pinToken, TestUtils.decodeHex("00000000000000000000000000000000")); + + Assert.assertEquals(32, ciphertext.length); + + plaintext = protocol.decrypt(pinToken, ciphertext); + Assert.assertArrayEquals(TestUtils.decodeHex("00000000000000000000000000000000"), plaintext); + } + + @Test + public void testAuthenticate() { + byte[] pinToken = + TestUtils.decodeHex("000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f"); + + Assert.assertArrayEquals( + TestUtils.decodeHex("495d46aa392d51132edb93bc49e60ecaaeb7802f3ae529779d5883f9330af561"), + protocol.authenticate(pinToken, TestUtils.decodeHex("000102030405060708090a0b0c0d0e0f"))); + + Assert.assertArrayEquals( + TestUtils.decodeHex("7f0ea2b80504890f3c6d42a77e31c833e881f741d2125569ac6427aa0c466aad"), + protocol.authenticate(pinToken, TestUtils.decodeHex("00000000000000000000000000000000"))); + } + + @Test + public void testEncodeCoordinate() { + Assert.assertArrayEquals( + TestUtils.decodeHex("0000000000000000000000000000000000000000000000000000000000000001"), + PinUvAuthProtocolV2.encodeCoordinate(new BigInteger("1"))); + + Assert.assertArrayEquals( + TestUtils.decodeHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + PinUvAuthProtocolV2.encodeCoordinate( + new BigInteger( + "115792089237316195423570985008687907853269984665640564039457584007913129639935"))); + + Assert.assertArrayEquals( + TestUtils.decodeHex("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + PinUvAuthProtocolV2.encodeCoordinate( + new BigInteger( + "57896044618658097711785492504343953926634992332820282019728792003956564819967"))); + + Assert.assertArrayEquals( + TestUtils.decodeHex("01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + PinUvAuthProtocolV2.encodeCoordinate( + new BigInteger( + "904625697166532776746648320380374280103671755200316906558262375061821325311"))); + + Assert.assertArrayEquals( + TestUtils.decodeHex("00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + PinUvAuthProtocolV2.encodeCoordinate( + new BigInteger( + "452312848583266388373324160190187140051835877600158453279131187530910662655"))); + + Assert.assertArrayEquals( + TestUtils.decodeHex("007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + PinUvAuthProtocolV2.encodeCoordinate( + new BigInteger( + "226156424291633194186662080095093570025917938800079226639565593765455331327"))); + } } diff --git a/fido/src/test/java/com/yubico/yubikit/fido/webauthn/SerializationTest.java b/fido/src/test/java/com/yubico/yubikit/fido/webauthn/SerializationTest.java index 5c3c41e0..5c0e5589 100755 --- a/fido/src/test/java/com/yubico/yubikit/fido/webauthn/SerializationTest.java +++ b/fido/src/test/java/com/yubico/yubikit/fido/webauthn/SerializationTest.java @@ -17,401 +17,363 @@ package com.yubico.yubikit.fido.webauthn; import com.yubico.yubikit.core.internal.codec.Base64; - -import org.junit.Assert; -import org.junit.Test; - import java.nio.ByteBuffer; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; - import javax.annotation.Nullable; +import org.junit.Assert; +import org.junit.Test; /** - * Test serialization and deserialization of WebAuthn data objects using toMap/fromMap as well as toBytes/fromBytes where applicable. - * Also tests that each object can successfully serialize to and from CBOR. + * Test serialization and deserialization of WebAuthn data objects using toMap/fromMap as well as + * toBytes/fromBytes where applicable. Also tests that each object can successfully serialize to and + * from CBOR. */ public class SerializationTest { - private final SecureRandom random = new SecureRandom(); - - @Test - public void testRpEntity() { - PublicKeyCredentialRpEntity rp = new PublicKeyCredentialRpEntity( - "An Example Company", "example.com" - ); - - Map map = rp.toMap(SerializationType.CBOR); - - Assert.assertEquals(rp.getId(), map.get("id")); - Assert.assertEquals(rp.getName(), map.get("name")); - - Assert.assertEquals(rp, PublicKeyCredentialRpEntity.fromMap(map, SerializationType.CBOR)); - } - - @Test - public void testUserEntity() { - byte[] userId = new byte[4 + random.nextInt(29)]; - random.nextBytes(userId); - - PublicKeyCredentialUserEntity user = new PublicKeyCredentialUserEntity( - "user@example.com", userId, - "A. User" - ); - - Map cborMap = user.toMap(SerializationType.CBOR); - Assert.assertEquals(user.getId(), cborMap.get("id")); - Assert.assertEquals(user.getName(), cborMap.get("name")); - Assert.assertEquals(user.getDisplayName(), cborMap.get("displayName")); - Assert.assertEquals(user, PublicKeyCredentialUserEntity.fromMap(cborMap, SerializationType.CBOR)); - - Map jsonMap = user.toMap(SerializationType.JSON); - Assert.assertEquals(Base64.toUrlSafeString(user.getId()), jsonMap.get("id")); - Assert.assertEquals(user.getName(), jsonMap.get("name")); - Assert.assertEquals(user.getDisplayName(), jsonMap.get("displayName")); - Assert.assertEquals(user, PublicKeyCredentialUserEntity.fromMap(jsonMap, SerializationType.JSON)); - } - - private void compareParametersLists(List a, List b) { - Assert.assertEquals(a.size(), b.size()); - for (int i = 0; i < a.size(); i++) { - Assert.assertEquals(a.get(i), b.get(i)); - } - } - - @Test - public void testParameters() { - PublicKeyCredentialParameters param = new PublicKeyCredentialParameters( - PublicKeyCredentialType.PUBLIC_KEY, - -7 - ); - - Map map = param.toMap(SerializationType.CBOR); - - Assert.assertEquals(param.getType(), map.get("type")); - Assert.assertEquals(param.getAlg(), map.get("alg")); - - Assert.assertEquals(param, PublicKeyCredentialParameters.fromMap(map, SerializationType.CBOR)); - } - - private void compareDescriptorLists(List a, List b) { - Assert.assertEquals(a.size(), b.size()); - for (int i = 0; i < a.size(); i++) { - Assert.assertEquals(a.get(i), b.get(i)); - } - } - - @Test - public void testDescriptor() { - byte[] credentialId = new byte[4 + random.nextInt(29)]; - random.nextBytes(credentialId); - - PublicKeyCredentialDescriptor descriptor = new PublicKeyCredentialDescriptor( - PublicKeyCredentialType.PUBLIC_KEY, - credentialId, - Arrays.asList("USB", "NFC") - ); - - Map cborMap = descriptor.toMap(SerializationType.CBOR); - Assert.assertEquals(descriptor.getType(), cborMap.get("type")); - Assert.assertArrayEquals(descriptor.getId(), (byte[]) cborMap.get("id")); - Assert.assertEquals(descriptor, PublicKeyCredentialDescriptor.fromMap(cborMap, SerializationType.CBOR)); - - Map jsonMap = descriptor.toMap(SerializationType.JSON); - Assert.assertEquals(descriptor.getType(), jsonMap.get("type")); - Assert.assertEquals(Base64.toUrlSafeString(descriptor.getId()), jsonMap.get("id")); - Assert.assertEquals(descriptor, PublicKeyCredentialDescriptor.fromMap(jsonMap, SerializationType.JSON)); + private final SecureRandom random = new SecureRandom(); + + @Test + public void testRpEntity() { + PublicKeyCredentialRpEntity rp = + new PublicKeyCredentialRpEntity("An Example Company", "example.com"); + + Map map = rp.toMap(SerializationType.CBOR); + + Assert.assertEquals(rp.getId(), map.get("id")); + Assert.assertEquals(rp.getName(), map.get("name")); + + Assert.assertEquals(rp, PublicKeyCredentialRpEntity.fromMap(map, SerializationType.CBOR)); + } + + @Test + public void testUserEntity() { + byte[] userId = new byte[4 + random.nextInt(29)]; + random.nextBytes(userId); + + PublicKeyCredentialUserEntity user = + new PublicKeyCredentialUserEntity("user@example.com", userId, "A. User"); + + Map cborMap = user.toMap(SerializationType.CBOR); + Assert.assertEquals(user.getId(), cborMap.get("id")); + Assert.assertEquals(user.getName(), cborMap.get("name")); + Assert.assertEquals(user.getDisplayName(), cborMap.get("displayName")); + Assert.assertEquals( + user, PublicKeyCredentialUserEntity.fromMap(cborMap, SerializationType.CBOR)); + + Map jsonMap = user.toMap(SerializationType.JSON); + Assert.assertEquals(Base64.toUrlSafeString(user.getId()), jsonMap.get("id")); + Assert.assertEquals(user.getName(), jsonMap.get("name")); + Assert.assertEquals(user.getDisplayName(), jsonMap.get("displayName")); + Assert.assertEquals( + user, PublicKeyCredentialUserEntity.fromMap(jsonMap, SerializationType.JSON)); + } + + private void compareParametersLists( + List a, List b) { + Assert.assertEquals(a.size(), b.size()); + for (int i = 0; i < a.size(); i++) { + Assert.assertEquals(a.get(i), b.get(i)); } + } - @Test - public void testSelectionCriteria() { - AuthenticatorSelectionCriteria criteria = new AuthenticatorSelectionCriteria( - AuthenticatorAttachment.PLATFORM, - ResidentKeyRequirement.REQUIRED, - UserVerificationRequirement.PREFERRED - ); + @Test + public void testParameters() { + PublicKeyCredentialParameters param = + new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, -7); - Map map = criteria.toMap(SerializationType.CBOR); + Map map = param.toMap(SerializationType.CBOR); - Assert.assertNotNull(criteria.getAuthenticatorAttachment()); - Assert.assertNotNull(criteria.getResidentKey()); - Assert.assertEquals(criteria.getAuthenticatorAttachment(), map.get("authenticatorAttachment")); - Assert.assertEquals(criteria.getUserVerification(), map.get("userVerification")); - Assert.assertEquals(criteria.getResidentKey(), map.get("residentKey")); + Assert.assertEquals(param.getType(), map.get("type")); + Assert.assertEquals(param.getAlg(), map.get("alg")); - Assert.assertEquals(criteria, AuthenticatorSelectionCriteria.fromMap(map, SerializationType.CBOR)); - } + Assert.assertEquals(param, PublicKeyCredentialParameters.fromMap(map, SerializationType.CBOR)); + } - private void compareCreationOptions(PublicKeyCredentialCreationOptions a, PublicKeyCredentialCreationOptions b) { - Assert.assertEquals(a.getRp(), b.getRp()); - Assert.assertEquals(a.getUser(), b.getUser()); - Assert.assertArrayEquals(a.getChallenge(), b.getChallenge()); - compareParametersLists(a.getPubKeyCredParams(), b.getPubKeyCredParams()); - Assert.assertEquals(a.getTimeout(), b.getTimeout()); - compareDescriptorLists(a.getExcludeCredentials(), b.getExcludeCredentials()); - Assert.assertEquals(a.getAuthenticatorSelection(), b.getAuthenticatorSelection()); - Assert.assertEquals(a.getAttestation(), b.getAttestation()); - Assert.assertEquals(a.getExtensions(), b.getExtensions()); + private void compareDescriptorLists( + List a, List b) { + Assert.assertEquals(a.size(), b.size()); + for (int i = 0; i < a.size(); i++) { + Assert.assertEquals(a.get(i), b.get(i)); } - - void testCreationOptions(@Nullable Long timeout) { - byte[] userId = new byte[4 + random.nextInt(29)]; - byte[] challenge = new byte[32]; - random.nextBytes(userId); - random.nextBytes(challenge); - - List pubKeyCredParams = new ArrayList<>( + } + + @Test + public void testDescriptor() { + byte[] credentialId = new byte[4 + random.nextInt(29)]; + random.nextBytes(credentialId); + + PublicKeyCredentialDescriptor descriptor = + new PublicKeyCredentialDescriptor( + PublicKeyCredentialType.PUBLIC_KEY, credentialId, Arrays.asList("USB", "NFC")); + + Map cborMap = descriptor.toMap(SerializationType.CBOR); + Assert.assertEquals(descriptor.getType(), cborMap.get("type")); + Assert.assertArrayEquals(descriptor.getId(), (byte[]) cborMap.get("id")); + Assert.assertEquals( + descriptor, PublicKeyCredentialDescriptor.fromMap(cborMap, SerializationType.CBOR)); + + Map jsonMap = descriptor.toMap(SerializationType.JSON); + Assert.assertEquals(descriptor.getType(), jsonMap.get("type")); + Assert.assertEquals(Base64.toUrlSafeString(descriptor.getId()), jsonMap.get("id")); + Assert.assertEquals( + descriptor, PublicKeyCredentialDescriptor.fromMap(jsonMap, SerializationType.JSON)); + } + + @Test + public void testSelectionCriteria() { + AuthenticatorSelectionCriteria criteria = + new AuthenticatorSelectionCriteria( + AuthenticatorAttachment.PLATFORM, + ResidentKeyRequirement.REQUIRED, + UserVerificationRequirement.PREFERRED); + + Map map = criteria.toMap(SerializationType.CBOR); + + Assert.assertNotNull(criteria.getAuthenticatorAttachment()); + Assert.assertNotNull(criteria.getResidentKey()); + Assert.assertEquals(criteria.getAuthenticatorAttachment(), map.get("authenticatorAttachment")); + Assert.assertEquals(criteria.getUserVerification(), map.get("userVerification")); + Assert.assertEquals(criteria.getResidentKey(), map.get("residentKey")); + + Assert.assertEquals( + criteria, AuthenticatorSelectionCriteria.fromMap(map, SerializationType.CBOR)); + } + + private void compareCreationOptions( + PublicKeyCredentialCreationOptions a, PublicKeyCredentialCreationOptions b) { + Assert.assertEquals(a.getRp(), b.getRp()); + Assert.assertEquals(a.getUser(), b.getUser()); + Assert.assertArrayEquals(a.getChallenge(), b.getChallenge()); + compareParametersLists(a.getPubKeyCredParams(), b.getPubKeyCredParams()); + Assert.assertEquals(a.getTimeout(), b.getTimeout()); + compareDescriptorLists(a.getExcludeCredentials(), b.getExcludeCredentials()); + Assert.assertEquals(a.getAuthenticatorSelection(), b.getAuthenticatorSelection()); + Assert.assertEquals(a.getAttestation(), b.getAttestation()); + Assert.assertEquals(a.getExtensions(), b.getExtensions()); + } + + void testCreationOptions(@Nullable Long timeout) { + byte[] userId = new byte[4 + random.nextInt(29)]; + byte[] challenge = new byte[32]; + random.nextBytes(userId); + random.nextBytes(challenge); + + List pubKeyCredParams = + new ArrayList<>( + Arrays.asList( + new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, -7), + new PublicKeyCredentialParameters("unknown public key type", -7))); + + List excludeCredentials = + new ArrayList<>( + Arrays.asList( + new PublicKeyCredentialDescriptor(PublicKeyCredentialType.PUBLIC_KEY, userId, null), + new PublicKeyCredentialDescriptor("unknown public key type", userId, null))); + + PublicKeyCredentialCreationOptions options = + new PublicKeyCredentialCreationOptions( + new PublicKeyCredentialRpEntity("Example", "example.com"), + new PublicKeyCredentialUserEntity("user", userId, "A User Name"), + challenge, + pubKeyCredParams, + timeout, + excludeCredentials, + new AuthenticatorSelectionCriteria(null, ResidentKeyRequirement.REQUIRED, null), + AttestationConveyancePreference.INDIRECT, + null); + + compareCreationOptions( + options, + PublicKeyCredentialCreationOptions.fromMap( + options.toMap(SerializationType.CBOR), SerializationType.CBOR)); + + compareCreationOptions( + options, + PublicKeyCredentialCreationOptions.fromMap( + options.toMap(SerializationType.JSON), SerializationType.JSON)); + } + + @Test + public void testCreationOptions() { + testCreationOptions((long) random.nextInt(Integer.MAX_VALUE)); + testCreationOptions(null); + } + + public void testRequestOptions(@Nullable Long timeout) { + byte[] challenge = new byte[32]; + byte[] credentialId = new byte[1 + random.nextInt(128)]; + random.nextBytes(challenge); + random.nextBytes(credentialId); + + PublicKeyCredentialRequestOptions options = + new PublicKeyCredentialRequestOptions( + challenge, + timeout, + "example.com", + new ArrayList<>( Arrays.asList( - new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, -7), - new PublicKeyCredentialParameters("unknown public key type", -7) - ) - ); - - List excludeCredentials = new ArrayList<>( - Arrays.asList( - new PublicKeyCredentialDescriptor(PublicKeyCredentialType.PUBLIC_KEY, userId, null), - new PublicKeyCredentialDescriptor("unknown public key type", userId, null) - ) - ); - - PublicKeyCredentialCreationOptions options = new PublicKeyCredentialCreationOptions( - new PublicKeyCredentialRpEntity("Example", "example.com"), - new PublicKeyCredentialUserEntity("user", userId, "A User Name"), - challenge, - pubKeyCredParams, - timeout, - excludeCredentials, - new AuthenticatorSelectionCriteria(null, ResidentKeyRequirement.REQUIRED, null), - AttestationConveyancePreference.INDIRECT, - null - ); - - compareCreationOptions(options, PublicKeyCredentialCreationOptions.fromMap( - options.toMap(SerializationType.CBOR), - SerializationType.CBOR) - ); - - compareCreationOptions(options, PublicKeyCredentialCreationOptions.fromMap( - options.toMap(SerializationType.JSON), - SerializationType.JSON) - ); - } - - @Test - public void testCreationOptions() { - testCreationOptions((long) random.nextInt(Integer.MAX_VALUE)); - testCreationOptions(null); - } - - public void testRequestOptions(@Nullable Long timeout) { - byte[] challenge = new byte[32]; - byte[] credentialId = new byte[1 + random.nextInt(128)]; - random.nextBytes(challenge); - random.nextBytes(credentialId); - - PublicKeyCredentialRequestOptions options = new PublicKeyCredentialRequestOptions( - challenge, - timeout, - "example.com", - new ArrayList<>( - Arrays.asList( - new PublicKeyCredentialDescriptor(PublicKeyCredentialType.PUBLIC_KEY, credentialId, null), - new PublicKeyCredentialDescriptor("unknown public key type", credentialId, null)) - ), - UserVerificationRequirement.REQUIRED, - null - ); - - Assert.assertEquals(options, - PublicKeyCredentialRequestOptions.fromMap( - options.toMap(SerializationType.JSON), - SerializationType.JSON) - ); - - Assert.assertEquals(options, - PublicKeyCredentialRequestOptions.fromMap( - options.toMap(SerializationType.CBOR), - SerializationType.CBOR) - ); - } - - @Test - public void testRequestOptions() { - testRequestOptions((long) random.nextInt(Integer.MAX_VALUE)); - testRequestOptions(null); - } - - private AuthenticatorAssertionResponse randomAuthenticatorAssertionResponse() { - byte[] authData = new byte[128]; - random.nextBytes(authData); - byte[] credentialId = new byte[1 + random.nextInt(64)]; - random.nextBytes(credentialId); - byte[] signature = new byte[70]; - random.nextBytes(signature); - byte[] userId = new byte[1 + random.nextInt(64)]; - random.nextBytes(userId); - byte[] clientDataJson = new byte[64 + random.nextInt(64)]; - random.nextBytes(clientDataJson); - - return new AuthenticatorAssertionResponse( - clientDataJson, - authData, - signature, - userId - ); - } - - @Test - public void testAssertionResponse() { - AuthenticatorAssertionResponse response = randomAuthenticatorAssertionResponse(); - - Assert.assertEquals(response, - AuthenticatorAssertionResponse.fromMap( - response.toMap(SerializationType.CBOR), - SerializationType.CBOR - ) - ); - - Assert.assertEquals(response, - AuthenticatorAssertionResponse.fromMap( - response.toMap(SerializationType.JSON), - SerializationType.JSON - ) - ); - } - - AuthenticatorAttestationResponse randomAuthenticatorAttestationResponse() { - byte[] attestationObject = new byte[128 + random.nextInt(128)]; - random.nextBytes(attestationObject); - byte[] clientDataJson = new byte[64 + random.nextInt(64)]; - random.nextBytes(clientDataJson); - List transports = Arrays.asList("nfc", "usb"); - - @SuppressWarnings("SpellCheckingInspection") - AuthenticatorData authenticatorData = AuthenticatorData.parseFrom( - ByteBuffer.wrap(Base64.fromUrlSafeString("5Yaf4EYzO6ALp_K7s-p-BQLPSCYVYcKLZptoXw" + - "xqQztFAAAAAhSaICGO9kEzlriB-NW38fUAMA5hR7Wj16h_z28qvtukB63QcIhzJ_sUkkJPf" + - "sU-KzdCFeaF2mZ80gSROEtELSHniKUBAgMmIAEhWCAOYUe1o9eof89vKr7bLZhH7nLY4wjK" + - "x5oxa66Kv0JjXiJYIKyPUlRxXHJjLrACafd_1stM7DyX120jDO7BlwqYsJyJ") - )); - - return new AuthenticatorAttestationResponse( - clientDataJson, - authenticatorData, - transports, - null, - 0, - attestationObject - ); - } - - @Test - public void testAttestationResponse() { - AuthenticatorAttestationResponse response = randomAuthenticatorAttestationResponse(); - Assert.assertEquals( - response, - AuthenticatorAttestationResponse.fromMap( - response.toMap(SerializationType.CBOR), - SerializationType.CBOR) - ); - - Assert.assertEquals( - response, - AuthenticatorAttestationResponse.fromMap( - response.toMap(SerializationType.JSON), - SerializationType.JSON) - ); - } - - @Test - public void testPublicKeyCredentialCreation() { - byte[] credentialId = new byte[1 + random.nextInt(64)]; - random.nextBytes(credentialId); - String credentialIdB64UrlEncoded = Base64.toUrlSafeString(credentialId); - - AuthenticatorAttestationResponse response = randomAuthenticatorAttestationResponse(); - - // credentialId as String - PublicKeyCredential credential = new PublicKeyCredential( - credentialIdB64UrlEncoded, - response - ); - - Assert.assertEquals(credentialIdB64UrlEncoded, credential.getId()); - Assert.assertArrayEquals(credentialId, credential.getRawId()); - Assert.assertEquals(PublicKeyCredential.PUBLIC_KEY_CREDENTIAL_TYPE, credential.getType()); - - // credentialId as byte[] - PublicKeyCredential credential2 = new PublicKeyCredential( - credentialId, - response); - - Assert.assertEquals(credentialIdB64UrlEncoded, credential2.getId()); - Assert.assertArrayEquals(credentialId, credential2.getRawId()); - Assert.assertEquals(PublicKeyCredential.PUBLIC_KEY_CREDENTIAL_TYPE, credential2.getType()); - } - - @Test - public void testPublicKeyCredentialWithAssertion() { - byte[] credentialId = new byte[1 + random.nextInt(64)]; - random.nextBytes(credentialId); - String credentialIdB64UrlEncoded = Base64.toUrlSafeString(credentialId); - - AuthenticatorAssertionResponse response = randomAuthenticatorAssertionResponse(); - - PublicKeyCredential credential = new PublicKeyCredential( - credentialIdB64UrlEncoded, - response - ); - - Assert.assertEquals(credentialIdB64UrlEncoded, credential.getId()); - Assert.assertArrayEquals(credentialId, credential.getRawId()); - Assert.assertEquals(PublicKeyCredential.PUBLIC_KEY_CREDENTIAL_TYPE, credential.getType()); - - Assert.assertEquals(credential, - PublicKeyCredential.fromMap( - credential.toMap(SerializationType.CBOR), - SerializationType.CBOR - ) - ); - - Assert.assertEquals(credential, - PublicKeyCredential.fromMap( - credential.toMap(SerializationType.JSON), - SerializationType.JSON - ) - ); - } - - @Test - public void testPublicKeyCredentialWithAttestation() { - byte[] credentialId = new byte[1 + random.nextInt(64)]; - random.nextBytes(credentialId); - String credentialIdB64UrlEncoded = Base64.toUrlSafeString(credentialId); - - AuthenticatorAttestationResponse response = randomAuthenticatorAttestationResponse(); - - PublicKeyCredential credential = new PublicKeyCredential( - credentialIdB64UrlEncoded, - response - ); - - Assert.assertEquals(credentialIdB64UrlEncoded, credential.getId()); - Assert.assertArrayEquals(credentialId, credential.getRawId()); - Assert.assertEquals(PublicKeyCredential.PUBLIC_KEY_CREDENTIAL_TYPE, credential.getType()); - - Assert.assertEquals(credential, - PublicKeyCredential.fromMap( - credential.toMap(SerializationType.CBOR), - SerializationType.CBOR) - ); - - Assert.assertEquals(credential, - PublicKeyCredential.fromMap( - credential.toMap(SerializationType.JSON), - SerializationType.JSON) - ); - } + new PublicKeyCredentialDescriptor( + PublicKeyCredentialType.PUBLIC_KEY, credentialId, null), + new PublicKeyCredentialDescriptor( + "unknown public key type", credentialId, null))), + UserVerificationRequirement.REQUIRED, + null); + + Assert.assertEquals( + options, + PublicKeyCredentialRequestOptions.fromMap( + options.toMap(SerializationType.JSON), SerializationType.JSON)); + + Assert.assertEquals( + options, + PublicKeyCredentialRequestOptions.fromMap( + options.toMap(SerializationType.CBOR), SerializationType.CBOR)); + } + + @Test + public void testRequestOptions() { + testRequestOptions((long) random.nextInt(Integer.MAX_VALUE)); + testRequestOptions(null); + } + + private AuthenticatorAssertionResponse randomAuthenticatorAssertionResponse() { + byte[] authData = new byte[128]; + random.nextBytes(authData); + byte[] credentialId = new byte[1 + random.nextInt(64)]; + random.nextBytes(credentialId); + byte[] signature = new byte[70]; + random.nextBytes(signature); + byte[] userId = new byte[1 + random.nextInt(64)]; + random.nextBytes(userId); + byte[] clientDataJson = new byte[64 + random.nextInt(64)]; + random.nextBytes(clientDataJson); + + return new AuthenticatorAssertionResponse(clientDataJson, authData, signature, userId); + } + + @Test + public void testAssertionResponse() { + AuthenticatorAssertionResponse response = randomAuthenticatorAssertionResponse(); + + Assert.assertEquals( + response, + AuthenticatorAssertionResponse.fromMap( + response.toMap(SerializationType.CBOR), SerializationType.CBOR)); + + Assert.assertEquals( + response, + AuthenticatorAssertionResponse.fromMap( + response.toMap(SerializationType.JSON), SerializationType.JSON)); + } + + AuthenticatorAttestationResponse randomAuthenticatorAttestationResponse() { + byte[] attestationObject = new byte[128 + random.nextInt(128)]; + random.nextBytes(attestationObject); + byte[] clientDataJson = new byte[64 + random.nextInt(64)]; + random.nextBytes(clientDataJson); + List transports = Arrays.asList("nfc", "usb"); + + @SuppressWarnings("SpellCheckingInspection") + AuthenticatorData authenticatorData = + AuthenticatorData.parseFrom( + ByteBuffer.wrap( + Base64.fromUrlSafeString( + "5Yaf4EYzO6ALp_K7s-p-BQLPSCYVYcKLZptoXwxqQztFAAAAAhSaICGO9kEzlriB-NW38fUAMA5hR" + + "7Wj16h_z28qvtukB63QcIhzJ_sUkkJPfsU-KzdCFeaF2mZ80gSROEtELSHniKUBAgMmIAEh" + + "WCAOYUe1o9eof89vKr7bLZhH7nLY4wjKx5oxa66Kv0JjXiJYIKyPUlRxXHJjLrACafd_1st" + + "M7DyX120jDO7BlwqYsJyJ"))); + + return new AuthenticatorAttestationResponse( + clientDataJson, authenticatorData, transports, null, 0, attestationObject); + } + + @Test + public void testAttestationResponse() { + AuthenticatorAttestationResponse response = randomAuthenticatorAttestationResponse(); + Assert.assertEquals( + response, + AuthenticatorAttestationResponse.fromMap( + response.toMap(SerializationType.CBOR), SerializationType.CBOR)); + + Assert.assertEquals( + response, + AuthenticatorAttestationResponse.fromMap( + response.toMap(SerializationType.JSON), SerializationType.JSON)); + } + + @Test + public void testPublicKeyCredentialCreation() { + byte[] credentialId = new byte[1 + random.nextInt(64)]; + random.nextBytes(credentialId); + String credentialIdB64UrlEncoded = Base64.toUrlSafeString(credentialId); + + AuthenticatorAttestationResponse response = randomAuthenticatorAttestationResponse(); + + // credentialId as String + PublicKeyCredential credential = new PublicKeyCredential(credentialIdB64UrlEncoded, response); + + Assert.assertEquals(credentialIdB64UrlEncoded, credential.getId()); + Assert.assertArrayEquals(credentialId, credential.getRawId()); + Assert.assertEquals(PublicKeyCredential.PUBLIC_KEY_CREDENTIAL_TYPE, credential.getType()); + + // credentialId as byte[] + PublicKeyCredential credential2 = new PublicKeyCredential(credentialId, response); + + Assert.assertEquals(credentialIdB64UrlEncoded, credential2.getId()); + Assert.assertArrayEquals(credentialId, credential2.getRawId()); + Assert.assertEquals(PublicKeyCredential.PUBLIC_KEY_CREDENTIAL_TYPE, credential2.getType()); + } + + @Test + public void testPublicKeyCredentialWithAssertion() { + byte[] credentialId = new byte[1 + random.nextInt(64)]; + random.nextBytes(credentialId); + String credentialIdB64UrlEncoded = Base64.toUrlSafeString(credentialId); + + AuthenticatorAssertionResponse response = randomAuthenticatorAssertionResponse(); + + PublicKeyCredential credential = new PublicKeyCredential(credentialIdB64UrlEncoded, response); + + Assert.assertEquals(credentialIdB64UrlEncoded, credential.getId()); + Assert.assertArrayEquals(credentialId, credential.getRawId()); + Assert.assertEquals(PublicKeyCredential.PUBLIC_KEY_CREDENTIAL_TYPE, credential.getType()); + + Assert.assertEquals( + credential, + PublicKeyCredential.fromMap( + credential.toMap(SerializationType.CBOR), SerializationType.CBOR)); + + Assert.assertEquals( + credential, + PublicKeyCredential.fromMap( + credential.toMap(SerializationType.JSON), SerializationType.JSON)); + } + + @Test + public void testPublicKeyCredentialWithAttestation() { + byte[] credentialId = new byte[1 + random.nextInt(64)]; + random.nextBytes(credentialId); + String credentialIdB64UrlEncoded = Base64.toUrlSafeString(credentialId); + + AuthenticatorAttestationResponse response = randomAuthenticatorAttestationResponse(); + + PublicKeyCredential credential = new PublicKeyCredential(credentialIdB64UrlEncoded, response); + + Assert.assertEquals(credentialIdB64UrlEncoded, credential.getId()); + Assert.assertArrayEquals(credentialId, credential.getRawId()); + Assert.assertEquals(PublicKeyCredential.PUBLIC_KEY_CREDENTIAL_TYPE, credential.getType()); + + Assert.assertEquals( + credential, + PublicKeyCredential.fromMap( + credential.toMap(SerializationType.CBOR), SerializationType.CBOR)); + + Assert.assertEquals( + credential, + PublicKeyCredential.fromMap( + credential.toMap(SerializationType.JSON), SerializationType.JSON)); + } } diff --git a/management/src/main/java/com/yubico/yubikit/management/Capability.java b/management/src/main/java/com/yubico/yubikit/management/Capability.java index b5372c1d..8af59d1d 100755 --- a/management/src/main/java/com/yubico/yubikit/management/Capability.java +++ b/management/src/main/java/com/yubico/yubikit/management/Capability.java @@ -16,41 +16,28 @@ package com.yubico.yubikit.management; /** - * Identifies a feature (typically an application) on a YubiKey which may or may not be supported, and which can be enabled or disabled. + * Identifies a feature (typically an application) on a YubiKey which may or may not be supported, + * and which can be enabled or disabled. */ public enum Capability { - /** - * Identifies the YubiOTP application. - */ - OTP(0x0001), - /** - * Identifies the U2F (CTAP1) portion of the FIDO application. - */ - U2F(0x0002), - /** - * Identifies the OpenPGP application, implementing the OpenPGP Card protocol. - */ - OPENPGP(0x0008), - /** - * Identifies the PIV application, implementing the PIV protocol. - */ - PIV(0x0010), - /** - * Identifies the OATH application, implementing the YKOATH protocol. - */ - OATH(0x0020), - /** - * Identifies the HSMAUTH application. - */ - HSMAUTH(0x0100), - /** - * Identifies the FIDO2 (CTAP2) portion of the FIDO application. - */ - FIDO2(0x0200); + /** Identifies the YubiOTP application. */ + OTP(0x0001), + /** Identifies the U2F (CTAP1) portion of the FIDO application. */ + U2F(0x0002), + /** Identifies the OpenPGP application, implementing the OpenPGP Card protocol. */ + OPENPGP(0x0008), + /** Identifies the PIV application, implementing the PIV protocol. */ + PIV(0x0010), + /** Identifies the OATH application, implementing the YKOATH protocol. */ + OATH(0x0020), + /** Identifies the HSMAUTH application. */ + HSMAUTH(0x0100), + /** Identifies the FIDO2 (CTAP2) portion of the FIDO application. */ + FIDO2(0x0200); - public final int bit; + public final int bit; - Capability(int bit) { - this.bit = bit; - } + Capability(int bit) { + this.bit = bit; + } } diff --git a/management/src/main/java/com/yubico/yubikit/management/DeviceConfig.java b/management/src/main/java/com/yubico/yubikit/management/DeviceConfig.java index f5e17ea9..3bf61828 100755 --- a/management/src/main/java/com/yubico/yubikit/management/DeviceConfig.java +++ b/management/src/main/java/com/yubico/yubikit/management/DeviceConfig.java @@ -17,248 +17,233 @@ import com.yubico.yubikit.core.Transport; import com.yubico.yubikit.core.util.Tlvs; - import java.nio.ByteBuffer; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; -/** - * Describes the configuration of a YubiKey which can be altered via the Management application. - */ +/** Describes the configuration of a YubiKey which can be altered via the Management application. */ public class DeviceConfig { - /** - * In pure CCID mode eject/inject the card with the button. - */ - public static final int FLAG_EJECT = 0x80; - /** - * Enables remote wakeup. - */ - public static final int FLAG_REMOTE_WAKEUP = 0x40; - - private static final int TAG_USB_ENABLED = 0x03; - private static final int TAG_AUTO_EJECT_TIMEOUT = 0x06; - private static final int TAG_CHALLENGE_RESPONSE_TIMEOUT = 0x07; - private static final int TAG_DEVICE_FLAGS = 0x08; - private static final int TAG_NFC_ENABLED = 0x0e; - private static final int TAG_CONFIGURATION_LOCK = 0x0a; - private static final int TAG_UNLOCK = 0x0b; - private static final int TAG_REBOOT = 0x0c; - private static final int TAG_NFC_RESTRICTED = 0x17; - - private final Map enabledCapabilities; - @Nullable - private final Short autoEjectTimeout; - @Nullable - private final Byte challengeResponseTimeout; - @Nullable - private final Integer deviceFlags; - @Nullable - private final Boolean nfcRestricted; - - @Deprecated - DeviceConfig( - Map enabledCapabilities, - @Nullable Short autoEjectTimeout, - @Nullable Byte challengeResponseTimeout, - @Nullable Integer deviceFlags) { - this.enabledCapabilities = enabledCapabilities; - this.autoEjectTimeout = autoEjectTimeout; - this.challengeResponseTimeout = challengeResponseTimeout; - this.deviceFlags = deviceFlags; - this.nfcRestricted = null; + /** In pure CCID mode eject/inject the card with the button. */ + public static final int FLAG_EJECT = 0x80; + + /** Enables remote wakeup. */ + public static final int FLAG_REMOTE_WAKEUP = 0x40; + + private static final int TAG_USB_ENABLED = 0x03; + private static final int TAG_AUTO_EJECT_TIMEOUT = 0x06; + private static final int TAG_CHALLENGE_RESPONSE_TIMEOUT = 0x07; + private static final int TAG_DEVICE_FLAGS = 0x08; + private static final int TAG_NFC_ENABLED = 0x0e; + private static final int TAG_CONFIGURATION_LOCK = 0x0a; + private static final int TAG_UNLOCK = 0x0b; + private static final int TAG_REBOOT = 0x0c; + private static final int TAG_NFC_RESTRICTED = 0x17; + + private final Map enabledCapabilities; + @Nullable private final Short autoEjectTimeout; + @Nullable private final Byte challengeResponseTimeout; + @Nullable private final Integer deviceFlags; + @Nullable private final Boolean nfcRestricted; + + @Deprecated + DeviceConfig( + Map enabledCapabilities, + @Nullable Short autoEjectTimeout, + @Nullable Byte challengeResponseTimeout, + @Nullable Integer deviceFlags) { + this.enabledCapabilities = enabledCapabilities; + this.autoEjectTimeout = autoEjectTimeout; + this.challengeResponseTimeout = challengeResponseTimeout; + this.deviceFlags = deviceFlags; + this.nfcRestricted = null; + } + + DeviceConfig(Builder builder) { + this.enabledCapabilities = builder.enabledCapabilities; + this.autoEjectTimeout = builder.autoEjectTimeout; + this.challengeResponseTimeout = builder.challengeResponseTimeout; + this.deviceFlags = builder.deviceFlags; + this.nfcRestricted = builder.nfcRestricted; + } + + /** + * Get the currently enabled capabilities for a given Interface. + * + *

NOTE: This method will return null if the Interface is not supported by the YubiKey, OR if + * the enabled capabilities state isn't readable. The YubiKey 4 series, for example, does not + * return enabled-status for USB applications. + * + * @param transport the physical transport to get enabled capabilities for + * @return the enabled capabilities, represented as {@link Capability} bits being set (1) or not + * (0) + */ + @Nullable public Integer getEnabledCapabilities(Transport transport) { + return enabledCapabilities.get(transport); + } + + /** Returns the timeout used when in CCID-only mode with {@link #FLAG_EJECT} enabled. */ + @Nullable public Short getAutoEjectTimeout() { + return autoEjectTimeout; + } + + /** + * Returns the timeout value used by the YubiOTP application when waiting for a user presence + * check (physical touch). + */ + @Nullable public Byte getChallengeResponseTimeout() { + return challengeResponseTimeout; + } + + /** Returns the NFC restricted flag. */ + @Nullable public Boolean getNfcRestricted() { + return nfcRestricted; + } + + /** Returns the device flags that are set. */ + @Nullable public Integer getDeviceFlags() { + return deviceFlags; + } + + byte[] getBytes(boolean reboot, @Nullable byte[] currentLockCode, @Nullable byte[] newLockCode) { + Map values = new LinkedHashMap<>(); + if (reboot) { + values.put(TAG_REBOOT, null); } - - DeviceConfig(Builder builder) { - this.enabledCapabilities = builder.enabledCapabilities; - this.autoEjectTimeout = builder.autoEjectTimeout; - this.challengeResponseTimeout = builder.challengeResponseTimeout; - this.deviceFlags = builder.deviceFlags; - this.nfcRestricted = builder.nfcRestricted; + if (currentLockCode != null) { + values.put(TAG_UNLOCK, currentLockCode); } - - /** - * Get the currently enabled capabilities for a given Interface. - * NOTE: This method will return null if the Interface is not supported by the YubiKey, OR if the enabled - * capabilities state isn't readable. The YubiKey 4 series, for example, does not return enabled-status for USB - * applications. - * - * @param transport the physical transport to get enabled capabilities for - * @return the enabled capabilities, represented as {@link Capability} bits being set (1) or not (0) - */ - @Nullable - public Integer getEnabledCapabilities(Transport transport) { - return enabledCapabilities.get(transport); + Integer usbEnabled = enabledCapabilities.get(Transport.USB); + if (usbEnabled != null) { + values.put(TAG_USB_ENABLED, new byte[] {(byte) (usbEnabled >> 8), usbEnabled.byteValue()}); + } + Integer nfcEnabled = enabledCapabilities.get(Transport.NFC); + if (nfcEnabled != null) { + values.put(TAG_NFC_ENABLED, new byte[] {(byte) (nfcEnabled >> 8), nfcEnabled.byteValue()}); + } + if (autoEjectTimeout != null) { + values.put( + TAG_AUTO_EJECT_TIMEOUT, + new byte[] {(byte) (autoEjectTimeout >> 8), autoEjectTimeout.byteValue()}); + } + if (challengeResponseTimeout != null) { + values.put(TAG_CHALLENGE_RESPONSE_TIMEOUT, new byte[] {challengeResponseTimeout}); + } + if (deviceFlags != null) { + values.put(TAG_DEVICE_FLAGS, new byte[] {deviceFlags.byteValue()}); + } + if (newLockCode != null) { + values.put(TAG_CONFIGURATION_LOCK, newLockCode); } + if (nfcRestricted != null) { + values.put(TAG_NFC_RESTRICTED, new byte[] {nfcRestricted ? 0x01 : (byte) 0x00}); + } + byte[] data = Tlvs.encodeMap(values); - /** - * Returns the timeout used when in CCID-only mode with {@link #FLAG_EJECT} enabled. - */ - @Nullable - public Short getAutoEjectTimeout() { - return autoEjectTimeout; + if (data.length > 0xff) { + throw new IllegalStateException("DeviceConfiguration too large"); } + return ByteBuffer.allocate(data.length + 1).put((byte) data.length).put(data).array(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DeviceConfig that = (DeviceConfig) o; + return Objects.equals(enabledCapabilities, that.enabledCapabilities) + && Objects.equals(autoEjectTimeout, that.autoEjectTimeout) + && Objects.equals(challengeResponseTimeout, that.challengeResponseTimeout) + && Objects.equals(deviceFlags, that.deviceFlags) + && Objects.equals(nfcRestricted, that.nfcRestricted); + } + + @Override + public int hashCode() { + return Objects.hash( + enabledCapabilities, + autoEjectTimeout, + challengeResponseTimeout, + deviceFlags, + nfcRestricted); + } + + /** + * Builder class for use with {@link ManagementSession#updateDeviceConfig} when altering the + * device configuration. + */ + public static class Builder { + private final Map enabledCapabilities = new HashMap<>(); + @Nullable private Short autoEjectTimeout; + @Nullable private Byte challengeResponseTimeout; + @Nullable private Integer deviceFlags; + @Nullable private Boolean nfcRestricted; /** - * Returns the timeout value used by the YubiOTP application when waiting for a user presence check (physical touch). + * Sets the enabled capabilities for the given transport. + * + * @param transport the transport to change capabilities for + * @param capabilities the capabilities to set */ - @Nullable - public Byte getChallengeResponseTimeout() { - return challengeResponseTimeout; + public Builder enabledCapabilities(Transport transport, int capabilities) { + enabledCapabilities.put(transport, capabilities); + return this; } /** - * Returns the NFC restricted flag. + * Sets the timeout used when the YubiKey is in CCID-only mode with {@link #FLAG_EJECT} set. + * + * @param autoEjectTimeout the timeout, in seconds */ - @Nullable - public Boolean getNfcRestricted() { - return nfcRestricted; + public Builder autoEjectTimeout(short autoEjectTimeout) { + this.autoEjectTimeout = autoEjectTimeout; + return this; } /** - * Returns the device flags that are set. + * Sets the timeout used by the YubiOTP application, when waiting for a user presence check + * (physical touch). + * + * @param challengeResponseTimeout the timeout, in seconds */ - @Nullable - public Integer getDeviceFlags() { - return deviceFlags; + public Builder challengeResponseTimeout(byte challengeResponseTimeout) { + this.challengeResponseTimeout = challengeResponseTimeout; + return this; } - byte[] getBytes(boolean reboot, @Nullable byte[] currentLockCode, @Nullable byte[] newLockCode) { - Map values = new LinkedHashMap<>(); - if (reboot) { - values.put(TAG_REBOOT, null); - } - if (currentLockCode != null) { - values.put(TAG_UNLOCK, currentLockCode); - } - Integer usbEnabled = enabledCapabilities.get(Transport.USB); - if (usbEnabled != null) { - values.put(TAG_USB_ENABLED, new byte[]{(byte) (usbEnabled >> 8), usbEnabled.byteValue()}); - } - Integer nfcEnabled = enabledCapabilities.get(Transport.NFC); - if (nfcEnabled != null) { - values.put(TAG_NFC_ENABLED, new byte[]{(byte) (nfcEnabled >> 8), nfcEnabled.byteValue()}); - } - if (autoEjectTimeout != null) { - values.put(TAG_AUTO_EJECT_TIMEOUT, new byte[]{(byte) (autoEjectTimeout >> 8), autoEjectTimeout.byteValue()}); - } - if (challengeResponseTimeout != null) { - values.put(TAG_CHALLENGE_RESPONSE_TIMEOUT, new byte[]{challengeResponseTimeout}); - } - if (deviceFlags != null) { - values.put(TAG_DEVICE_FLAGS, new byte[]{deviceFlags.byteValue()}); - } - if (newLockCode != null) { - values.put(TAG_CONFIGURATION_LOCK, newLockCode); - } - if (nfcRestricted != null) { - values.put(TAG_NFC_RESTRICTED, new byte[]{nfcRestricted ? 0x01 : (byte) 0x00}); - } - byte[] data = Tlvs.encodeMap(values); - - if (data.length > 0xff) { - throw new IllegalStateException("DeviceConfiguration too large"); - } - return ByteBuffer.allocate(data.length + 1).put((byte) data.length).put(data).array(); + /** Sets the Device flags of the YubiKey. */ + public Builder deviceFlags(int deviceFlags) { + this.deviceFlags = deviceFlags; + return this; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - DeviceConfig that = (DeviceConfig) o; - return Objects.equals(enabledCapabilities, that.enabledCapabilities) && - Objects.equals(autoEjectTimeout, that.autoEjectTimeout) && - Objects.equals(challengeResponseTimeout, that.challengeResponseTimeout) && - Objects.equals(deviceFlags, that.deviceFlags) && - Objects.equals(nfcRestricted, that.nfcRestricted); - } - - @Override - public int hashCode() { - return Objects.hash(enabledCapabilities, autoEjectTimeout, challengeResponseTimeout, deviceFlags, nfcRestricted); - } - - /** - * Builder class for use with {@link ManagementSession#updateDeviceConfig} when altering the device configuration. - */ - public static class Builder { - private final Map enabledCapabilities = new HashMap<>(); - @Nullable - private Short autoEjectTimeout; - @Nullable - private Byte challengeResponseTimeout; - @Nullable - private Integer deviceFlags; - @Nullable - private Boolean nfcRestricted; - - /** - * Sets the enabled capabilities for the given transport. - * - * @param transport the transport to change capabilities for - * @param capabilities the capabilities to set - */ - public Builder enabledCapabilities(Transport transport, int capabilities) { - enabledCapabilities.put(transport, capabilities); - return this; - } - - /** - * Sets the timeout used when the YubiKey is in CCID-only mode with {@link #FLAG_EJECT} set. - * - * @param autoEjectTimeout the timeout, in seconds - */ - public Builder autoEjectTimeout(short autoEjectTimeout) { - this.autoEjectTimeout = autoEjectTimeout; - return this; - } - - /** - * Sets the timeout used by the YubiOTP application, when waiting for a user presence check (physical touch). - * - * @param challengeResponseTimeout the timeout, in seconds - */ - public Builder challengeResponseTimeout(byte challengeResponseTimeout) { - this.challengeResponseTimeout = challengeResponseTimeout; - return this; - } - - /** - * Sets the Device flags of the YubiKey. - */ - public Builder deviceFlags(int deviceFlags) { - this.deviceFlags = deviceFlags; - return this; - } - - /** - * Sets the NFC restricted flag of the YubiKey. - */ - public Builder nfcRestricted(@Nullable Boolean nfcRestricted) { - this.nfcRestricted = nfcRestricted; - return this; - } - - /** - * Constructs a DeviceConfig using the current configuration. - */ - public DeviceConfig build() { - return new DeviceConfig(this); - } + /** Sets the NFC restricted flag of the YubiKey. */ + public Builder nfcRestricted(@Nullable Boolean nfcRestricted) { + this.nfcRestricted = nfcRestricted; + return this; } - @Override - public String toString() { - return "DeviceConfig{" + - "enabledCapabilities=" + enabledCapabilities + - ", autoEjectTimeout=" + autoEjectTimeout + - ", challengeResponseTimeout=" + challengeResponseTimeout + - ", deviceFlags=" + deviceFlags + - ", nfcRestricted=" + nfcRestricted + - '}'; + /** Constructs a DeviceConfig using the current configuration. */ + public DeviceConfig build() { + return new DeviceConfig(this); } + } + + @Override + public String toString() { + return "DeviceConfig{" + + "enabledCapabilities=" + + enabledCapabilities + + ", autoEjectTimeout=" + + autoEjectTimeout + + ", challengeResponseTimeout=" + + challengeResponseTimeout + + ", deviceFlags=" + + deviceFlags + + ", nfcRestricted=" + + nfcRestricted + + '}'; + } } diff --git a/management/src/main/java/com/yubico/yubikit/management/DeviceInfo.java b/management/src/main/java/com/yubico/yubikit/management/DeviceInfo.java index 2fef83a6..c90ee035 100755 --- a/management/src/main/java/com/yubico/yubikit/management/DeviceInfo.java +++ b/management/src/main/java/com/yubico/yubikit/management/DeviceInfo.java @@ -17,531 +17,517 @@ import com.yubico.yubikit.core.Transport; import com.yubico.yubikit.core.Version; - import java.nio.ByteBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; -/** - * Contains metadata, including Device Configuration, of a YubiKey. - */ +/** Contains metadata, including Device Configuration, of a YubiKey. */ public class DeviceInfo { - private static final int TAG_USB_SUPPORTED = 0x01; - private static final int TAG_SERIAL_NUMBER = 0x02; - private static final int TAG_USB_ENABLED = 0x03; - private static final int TAG_FORMFACTOR = 0x04; - private static final int TAG_FIRMWARE_VERSION = 0x05; - private static final int TAG_AUTO_EJECT_TIMEOUT = 0x06; - private static final int TAG_CHALLENGE_RESPONSE_TIMEOUT = 0x07; - private static final int TAG_DEVICE_FLAGS = 0x08; - private static final int TAG_NFC_SUPPORTED = 0x0d; - private static final int TAG_NFC_ENABLED = 0x0e; - private static final int TAG_CONFIG_LOCKED = 0x0a; - private static final int TAG_PART_NUMBER = 0x13; - private static final int TAG_FIPS_CAPABLE = 0x14; - private static final int TAG_FIPS_APPROVED = 0x15; - private static final int TAG_PIN_COMPLEXITY = 0x16; - private static final int TAG_NFC_RESTRICTED = 0x17; - private static final int TAG_RESET_BLOCKED = 0x18; - private static final int TAG_FPS_VERSION = 0x20; - private static final int TAG_STM_VERSION = 0x21; - - private final DeviceConfig config; - @Nullable - private final Integer serialNumber; - private final Version version; - private final FormFactor formFactor; - private final Map supportedCapabilities; - private final boolean isLocked; - private final boolean isFips; - private final boolean isSky; - @Nullable - private final String partNumber; - private final int fipsCapable; - private final int fipsApproved; - private final boolean pinComplexity; - private final int resetBlocked; - @Nullable - private final Version fpsVersion; - @Nullable - private final Version stmVersion; - - private DeviceInfo(Builder builder) { - this.config = builder.config; - this.serialNumber = builder.serialNumber; - this.version = builder.version; - this.formFactor = builder.formFactor; - this.supportedCapabilities = builder.supportedCapabilities; - this.isLocked = builder.isLocked; - this.isFips = builder.isFips; - this.isSky = builder.isSky; - this.partNumber = builder.partNumber; - this.fipsCapable = builder.fipsCapable; - this.fipsApproved = builder.fipsApproved; - this.pinComplexity = builder.pinComplexity; - this.resetBlocked = builder.resetBlocked; - this.fpsVersion = builder.fpsVersion; - this.stmVersion = builder.stmVersion; + private static final int TAG_USB_SUPPORTED = 0x01; + private static final int TAG_SERIAL_NUMBER = 0x02; + private static final int TAG_USB_ENABLED = 0x03; + private static final int TAG_FORMFACTOR = 0x04; + private static final int TAG_FIRMWARE_VERSION = 0x05; + private static final int TAG_AUTO_EJECT_TIMEOUT = 0x06; + private static final int TAG_CHALLENGE_RESPONSE_TIMEOUT = 0x07; + private static final int TAG_DEVICE_FLAGS = 0x08; + private static final int TAG_NFC_SUPPORTED = 0x0d; + private static final int TAG_NFC_ENABLED = 0x0e; + private static final int TAG_CONFIG_LOCKED = 0x0a; + private static final int TAG_PART_NUMBER = 0x13; + private static final int TAG_FIPS_CAPABLE = 0x14; + private static final int TAG_FIPS_APPROVED = 0x15; + private static final int TAG_PIN_COMPLEXITY = 0x16; + private static final int TAG_NFC_RESTRICTED = 0x17; + private static final int TAG_RESET_BLOCKED = 0x18; + private static final int TAG_FPS_VERSION = 0x20; + private static final int TAG_STM_VERSION = 0x21; + + private final DeviceConfig config; + @Nullable private final Integer serialNumber; + private final Version version; + private final FormFactor formFactor; + private final Map supportedCapabilities; + private final boolean isLocked; + private final boolean isFips; + private final boolean isSky; + @Nullable private final String partNumber; + private final int fipsCapable; + private final int fipsApproved; + private final boolean pinComplexity; + private final int resetBlocked; + @Nullable private final Version fpsVersion; + @Nullable private final Version stmVersion; + + private DeviceInfo(Builder builder) { + this.config = builder.config; + this.serialNumber = builder.serialNumber; + this.version = builder.version; + this.formFactor = builder.formFactor; + this.supportedCapabilities = builder.supportedCapabilities; + this.isLocked = builder.isLocked; + this.isFips = builder.isFips; + this.isSky = builder.isSky; + this.partNumber = builder.partNumber; + this.fipsCapable = builder.fipsCapable; + this.fipsApproved = builder.fipsApproved; + this.pinComplexity = builder.pinComplexity; + this.resetBlocked = builder.resetBlocked; + this.fpsVersion = builder.fpsVersion; + this.stmVersion = builder.stmVersion; + } + + /** + * Constructs a new DeviceInfo. + * + * @param config the mutable configuration of the YubiKey + * @param serialNumber the YubiKeys serial number + * @param version the firmware version of the YubiKey + * @param formFactor the YubiKeys physical form factor + * @param supportedCapabilities the capabilities supported by the YubiKey + * @param isLocked whether or not the configuration is protected by a lock code + * @param isFips whether or not the YubiKey is a FIPS model + * @param isSky whether or not the YubiKey is a Security Key by Yubico model + * @deprecated Replaced with {@link Builder#build()}. + */ + @Deprecated + public DeviceInfo( + DeviceConfig config, + @Nullable Integer serialNumber, + Version version, + FormFactor formFactor, + Map supportedCapabilities, + boolean isLocked, + boolean isFips, + boolean isSky) { + this( + new Builder() + .config(config) + .serialNumber(serialNumber) + .version(version) + .formFactor(formFactor) + .supportedCapabilities(supportedCapabilities) + .isLocked(isLocked) + .isFips(isFips) + .isSky(isSky)); + } + + /** Legacy constructor, retained for backwards compatibility until 3.0.0. */ + @Deprecated + public DeviceInfo( + DeviceConfig config, + @Nullable Integer serialNumber, + Version version, + FormFactor formFactor, + Map supportedCapabilities, + boolean isLocked) { + this(config, serialNumber, version, formFactor, supportedCapabilities, isLocked, false, false); + } + + /** Returns the current Device configuration of the YubiKey. */ + public DeviceConfig getConfig() { + return config; + } + + /** + * Returns the serial number of the YubiKey, if available. + * + *

The serial number can be read if the YubiKey has a serial number, and one of the YubiOTP + * slots is configured with the SERIAL_API_VISIBLE flag. + */ + @Nullable public Integer getSerialNumber() { + return serialNumber; + } + + /** Returns the version number of the YubiKey firmware. */ + public Version getVersion() { + return version; + } + + /** Returns the form factor of the YubiKey. */ + public FormFactor getFormFactor() { + return formFactor; + } + + /** Returns whether or not a specific transport is available on this YubiKey. */ + public boolean hasTransport(Transport transport) { + return supportedCapabilities.containsKey(transport); + } + + /** Returns the supported (not necessarily enabled) capabilities for a given transport. */ + public int getSupportedCapabilities(Transport transport) { + Integer capabilities = supportedCapabilities.get(transport); + return capabilities == null ? 0 : capabilities; + } + + /** + * Returns whether or not a Configuration Lock is set for the Management application on the + * YubiKey. + */ + public boolean isLocked() { + return isLocked; + } + + /** Returns whether or not this is a FIPS compliant device */ + public boolean isFips() { + return isFips; + } + + /** Returns whether or not this is a Security key */ + public boolean isSky() { + return isSky; + } + + /** Returns part number */ + @Nullable public String getPartNumber() { + return partNumber; + } + + /** Returns FIPS capable flags */ + public int getFipsCapable() { + return fipsCapable; + } + + /** Returns FIPS approved flags */ + public int getFipsApproved() { + return fipsApproved; + } + + /** Returns value of PIN complexity */ + public boolean getPinComplexity() { + return pinComplexity; + } + + /** Returns reset blocked flags */ + public int getResetBlocked() { + return resetBlocked; + } + + /** Returns FPS version */ + @Nullable public Version getFpsVersion() { + return fpsVersion; + } + + /** Returns STM version */ + @Nullable public Version getStmVersion() { + return stmVersion; + } + + static DeviceInfo parseTlvs(Map data, Version defaultVersion) { + boolean isLocked = readInt(data.get(TAG_CONFIG_LOCKED)) == 1; + int serialNumber = readInt(data.get(TAG_SERIAL_NUMBER)); + int formFactorTagData = readInt(data.get(TAG_FORMFACTOR)); + boolean isFips = (formFactorTagData & 0x80) != 0; + boolean isSky = (formFactorTagData & 0x40) != 0; + @Nullable String partNumber = null; + int fipsCapable = fromFips(readInt(data.get(TAG_FIPS_CAPABLE))); + int fipsApproved = fromFips(readInt(data.get(TAG_FIPS_APPROVED))); + boolean pinComplexity = readInt(data.get(TAG_PIN_COMPLEXITY)) == 1; + int resetBlocked = readInt(data.get(TAG_RESET_BLOCKED)); + FormFactor formFactor = FormFactor.valueOf(formFactorTagData); + + Version version = + data.containsKey(TAG_FIRMWARE_VERSION) + ? Version.fromBytes(data.get(TAG_FIRMWARE_VERSION)) + : defaultVersion; + + final Version versionZero = new Version(0, 0, 0); + + Version fpsVersion = null; + if (data.containsKey(TAG_FPS_VERSION)) { + Version tempVersion = Version.fromBytes(data.get(TAG_FPS_VERSION)); + if (!tempVersion.equals(versionZero)) { + fpsVersion = tempVersion; + } } - /** - * Constructs a new DeviceInfo. - * - * @param config the mutable configuration of the YubiKey - * @param serialNumber the YubiKeys serial number - * @param version the firmware version of the YubiKey - * @param formFactor the YubiKeys physical form factor - * @param supportedCapabilities the capabilities supported by the YubiKey - * @param isLocked whether or not the configuration is protected by a lock code - * @param isFips whether or not the YubiKey is a FIPS model - * @param isSky whether or not the YubiKey is a Security Key by Yubico model - * @deprecated Replaced with {@link Builder#build()}. - */ - @Deprecated - public DeviceInfo( - DeviceConfig config, - @Nullable Integer serialNumber, - Version version, - FormFactor formFactor, - Map supportedCapabilities, - boolean isLocked, - boolean isFips, - boolean isSky) { - this(new Builder() - .config(config) - .serialNumber(serialNumber) - .version(version) - .formFactor(formFactor) - .supportedCapabilities(supportedCapabilities) - .isLocked(isLocked) - .isFips(isFips) - .isSky(isSky)); + Version stmVersion = null; + if (data.containsKey(TAG_STM_VERSION)) { + Version tempVersion = Version.fromBytes(data.get(TAG_STM_VERSION)); + if (!tempVersion.equals(versionZero)) { + stmVersion = tempVersion; + } } - /** - * Legacy constructor, retained for backwards compatibility until 3.0.0. - */ - @Deprecated - public DeviceInfo(DeviceConfig config, @Nullable Integer serialNumber, Version version, FormFactor formFactor, Map supportedCapabilities, boolean isLocked) { - this(config, serialNumber, version, formFactor, supportedCapabilities, isLocked, false, false); - } + short autoEjectTimeout = (short) readInt(data.get(TAG_AUTO_EJECT_TIMEOUT)); + byte challengeResponseTimeout = (byte) readInt(data.get(TAG_CHALLENGE_RESPONSE_TIMEOUT)); + int deviceFlags = readInt(data.get(TAG_DEVICE_FLAGS)); + Boolean nfcRestricted = readInt(data.get(TAG_NFC_RESTRICTED)) == 1; - /** - * Returns the current Device configuration of the YubiKey. - */ - public DeviceConfig getConfig() { - return config; - } + Map supportedCapabilities = new HashMap<>(); + Map enabledCapabilities = new HashMap<>(); - /** - * Returns the serial number of the YubiKey, if available. - *

- * The serial number can be read if the YubiKey has a serial number, and one of the YubiOTP slots - * is configured with the SERIAL_API_VISIBLE flag. - */ - @Nullable - public Integer getSerialNumber() { - return serialNumber; + if (version.major == 4 && version.minor == 2 && version.micro == 4) { + // 4.2.4 doesn't report supported capabilities correctly, but they are always 0x3f. + supportedCapabilities.put(Transport.USB, 0x3f); + } else { + supportedCapabilities.put(Transport.USB, readInt(data.get(TAG_USB_SUPPORTED))); + } + if (data.containsKey(TAG_USB_ENABLED) && version.major != 4) { + // YK4 reports this incorrectly, instead use supportedCapabilities and USB mode. + enabledCapabilities.put(Transport.USB, readInt(data.get(TAG_USB_ENABLED))); } - /** - * Returns the version number of the YubiKey firmware. - */ - public Version getVersion() { - return version; + if (data.containsKey(TAG_NFC_SUPPORTED)) { + supportedCapabilities.put(Transport.NFC, readInt(data.get(TAG_NFC_SUPPORTED))); + enabledCapabilities.put(Transport.NFC, readInt(data.get(TAG_NFC_ENABLED))); } - /** - * Returns the form factor of the YubiKey. - */ - public FormFactor getFormFactor() { - return formFactor; + if (data.containsKey(TAG_PART_NUMBER)) { + try { + partNumber = + StandardCharsets.UTF_8 + .newDecoder() + .decode(ByteBuffer.wrap(data.get(TAG_PART_NUMBER))) + .toString(); + } catch (IllegalStateException | CharacterCodingException e) { + // ignored + } } - /** - * Returns whether or not a specific transport is available on this YubiKey. - */ - public boolean hasTransport(Transport transport) { - return supportedCapabilities.containsKey(transport); + DeviceConfig.Builder deviceConfigBuilder = + new DeviceConfig.Builder() + .autoEjectTimeout(autoEjectTimeout) + .challengeResponseTimeout(challengeResponseTimeout) + .deviceFlags(deviceFlags) + .nfcRestricted(nfcRestricted); + + for (Transport transport : Transport.values()) { + if (enabledCapabilities.containsKey(transport)) { + deviceConfigBuilder.enabledCapabilities(transport, enabledCapabilities.get(transport)); + } } - /** - * Returns the supported (not necessarily enabled) capabilities for a given transport. - */ - public int getSupportedCapabilities(Transport transport) { - Integer capabilities = supportedCapabilities.get(transport); - return capabilities == null ? 0 : capabilities; + return new Builder() + .config(deviceConfigBuilder.build()) + .serialNumber(serialNumber == 0 ? null : serialNumber) + .version(version) + .formFactor(formFactor) + .supportedCapabilities(supportedCapabilities) + .isLocked(isLocked) + .isFips(isFips) + .isSky(isSky) + .partNumber(partNumber) + .fipsCapable(fipsCapable) + .fipsApproved(fipsApproved) + .pinComplexity(pinComplexity) + .resetBlocked(resetBlocked) + .fpsVersion(fpsVersion) + .stmVersion(stmVersion) + .build(); + } + + public static class Builder { + private DeviceConfig config = new DeviceConfig.Builder().build(); + @Nullable private Integer serialNumber = null; + private Version version = new Version(0, 0, 0); + private FormFactor formFactor = FormFactor.UNKNOWN; + private Map supportedCapabilities = new HashMap<>(); + private boolean isLocked = false; + private boolean isFips = false; + private boolean isSky = false; + @Nullable private String partNumber = ""; + private int fipsCapable = 0; + private int fipsApproved = 0; + private boolean pinComplexity = false; + private int resetBlocked = 0; + @Nullable private Version fpsVersion = null; + @Nullable private Version stmVersion = null; + + public DeviceInfo build() { + return new DeviceInfo(this); } - /** - * Returns whether or not a Configuration Lock is set for the Management application on the YubiKey. - */ - public boolean isLocked() { - return isLocked; + public Builder config(DeviceConfig deviceConfig) { + this.config = deviceConfig; + return this; } - /** - * Returns whether or not this is a FIPS compliant device - */ - public boolean isFips() { - return isFips; + public Builder serialNumber(@Nullable Integer serialNumber) { + this.serialNumber = serialNumber; + return this; } - /** - * Returns whether or not this is a Security key - */ - public boolean isSky() { - return isSky; + public Builder version(Version version) { + this.version = version; + return this; } - /** - * Returns part number - */ - @Nullable - public String getPartNumber() { - return partNumber; + public Builder formFactor(FormFactor formFactor) { + this.formFactor = formFactor; + return this; } - /** - * Returns FIPS capable flags - */ - public int getFipsCapable() { - return fipsCapable; + public Builder supportedCapabilities(Map supportedCapabilities) { + this.supportedCapabilities = supportedCapabilities; + return this; } - /** - * Returns FIPS approved flags - */ - public int getFipsApproved() { - return fipsApproved; + public Builder isLocked(boolean locked) { + this.isLocked = locked; + return this; } - /** - * Returns value of PIN complexity - */ - public boolean getPinComplexity() { - return pinComplexity; + public Builder isFips(boolean fips) { + this.isFips = fips; + return this; } - /** - * Returns reset blocked flags - */ - public int getResetBlocked() { - return resetBlocked; + public Builder isSky(boolean sky) { + this.isSky = sky; + return this; } - /** - * Returns FPS version - */ - @Nullable - public Version getFpsVersion() { - return fpsVersion; + public Builder partNumber(@Nullable String partNumber) { + this.partNumber = partNumber; + return this; } - /** - * Returns STM version - */ - @Nullable - public Version getStmVersion() { - return stmVersion; + public Builder fipsCapable(int fipsCapable) { + this.fipsCapable = fipsCapable; + return this; } - static DeviceInfo parseTlvs(Map data, Version defaultVersion) { - boolean isLocked = readInt(data.get(TAG_CONFIG_LOCKED)) == 1; - int serialNumber = readInt(data.get(TAG_SERIAL_NUMBER)); - int formFactorTagData = readInt(data.get(TAG_FORMFACTOR)); - boolean isFips = (formFactorTagData & 0x80) != 0; - boolean isSky = (formFactorTagData & 0x40) != 0; - @Nullable - String partNumber = null; - int fipsCapable = fromFips(readInt(data.get(TAG_FIPS_CAPABLE))); - int fipsApproved = fromFips(readInt(data.get(TAG_FIPS_APPROVED))); - boolean pinComplexity = readInt(data.get(TAG_PIN_COMPLEXITY)) == 1; - int resetBlocked = readInt(data.get(TAG_RESET_BLOCKED)); - FormFactor formFactor = FormFactor.valueOf(formFactorTagData); - - Version version = data.containsKey(TAG_FIRMWARE_VERSION) - ? Version.fromBytes(data.get(TAG_FIRMWARE_VERSION)) - : defaultVersion; - - final Version versionZero = new Version(0, 0, 0); - - Version fpsVersion = null; - if (data.containsKey(TAG_FPS_VERSION)) { - Version tempVersion = Version.fromBytes(data.get(TAG_FPS_VERSION)); - if (!tempVersion.equals(versionZero)) { - fpsVersion = tempVersion; - } - } - - Version stmVersion = null; - if (data.containsKey(TAG_STM_VERSION)) { - Version tempVersion = Version.fromBytes(data.get(TAG_STM_VERSION)); - if (!tempVersion.equals(versionZero)) { - stmVersion = tempVersion; - } - } - - short autoEjectTimeout = (short) readInt(data.get(TAG_AUTO_EJECT_TIMEOUT)); - byte challengeResponseTimeout = (byte) readInt(data.get(TAG_CHALLENGE_RESPONSE_TIMEOUT)); - int deviceFlags = readInt(data.get(TAG_DEVICE_FLAGS)); - Boolean nfcRestricted = readInt(data.get(TAG_NFC_RESTRICTED)) == 1; - - Map supportedCapabilities = new HashMap<>(); - Map enabledCapabilities = new HashMap<>(); - - if (version.major == 4 && version.minor == 2 && version.micro == 4) { - // 4.2.4 doesn't report supported capabilities correctly, but they are always 0x3f. - supportedCapabilities.put(Transport.USB, 0x3f); - } else { - supportedCapabilities.put(Transport.USB, readInt(data.get(TAG_USB_SUPPORTED))); - } - if (data.containsKey(TAG_USB_ENABLED) && version.major != 4) { - // YK4 reports this incorrectly, instead use supportedCapabilities and USB mode. - enabledCapabilities.put(Transport.USB, readInt(data.get(TAG_USB_ENABLED))); - } - - if (data.containsKey(TAG_NFC_SUPPORTED)) { - supportedCapabilities.put(Transport.NFC, readInt(data.get(TAG_NFC_SUPPORTED))); - enabledCapabilities.put(Transport.NFC, readInt(data.get(TAG_NFC_ENABLED))); - } - - if (data.containsKey(TAG_PART_NUMBER)) { - try { - partNumber = StandardCharsets.UTF_8.newDecoder() - .decode(ByteBuffer.wrap(data.get(TAG_PART_NUMBER))) - .toString(); - } catch (IllegalStateException | CharacterCodingException e) { - // ignored - } - } - - DeviceConfig.Builder deviceConfigBuilder = new DeviceConfig.Builder() - .autoEjectTimeout(autoEjectTimeout) - .challengeResponseTimeout(challengeResponseTimeout) - .deviceFlags(deviceFlags) - .nfcRestricted(nfcRestricted); - - for (Transport transport : Transport.values()) { - if (enabledCapabilities.containsKey(transport)) { - deviceConfigBuilder.enabledCapabilities( - transport, - enabledCapabilities.get(transport) - ); - } - } - - return new Builder() - .config(deviceConfigBuilder.build()) - .serialNumber(serialNumber == 0 ? null : serialNumber) - .version(version) - .formFactor(formFactor) - .supportedCapabilities(supportedCapabilities) - .isLocked(isLocked) - .isFips(isFips) - .isSky(isSky) - .partNumber(partNumber) - .fipsCapable(fipsCapable) - .fipsApproved(fipsApproved) - .pinComplexity(pinComplexity) - .resetBlocked(resetBlocked) - .fpsVersion(fpsVersion) - .stmVersion(stmVersion) - .build(); + public Builder fipsApproved(int fipsApproved) { + this.fipsApproved = fipsApproved; + return this; } - public static class Builder { - private DeviceConfig config = new DeviceConfig.Builder().build(); - @Nullable - private Integer serialNumber = null; - private Version version = new Version(0, 0, 0); - private FormFactor formFactor = FormFactor.UNKNOWN; - private Map supportedCapabilities = new HashMap<>(); - private boolean isLocked = false; - private boolean isFips = false; - private boolean isSky = false; - @Nullable - private String partNumber = ""; - private int fipsCapable = 0; - private int fipsApproved = 0; - private boolean pinComplexity = false; - private int resetBlocked = 0; - @Nullable - private Version fpsVersion = null; - @Nullable - private Version stmVersion = null; - - public DeviceInfo build() { - return new DeviceInfo(this); - } - - public Builder config(DeviceConfig deviceConfig) { - this.config = deviceConfig; - return this; - } - - public Builder serialNumber(@Nullable Integer serialNumber) { - this.serialNumber = serialNumber; - return this; - } - - public Builder version(Version version) { - this.version = version; - return this; - } - - public Builder formFactor(FormFactor formFactor) { - this.formFactor = formFactor; - return this; - } - - public Builder supportedCapabilities(Map supportedCapabilities) { - this.supportedCapabilities = supportedCapabilities; - return this; - } - - public Builder isLocked(boolean locked) { - this.isLocked = locked; - return this; - } - - public Builder isFips(boolean fips) { - this.isFips = fips; - return this; - } - - public Builder isSky(boolean sky) { - this.isSky = sky; - return this; - } - - public Builder partNumber(@Nullable String partNumber) { - this.partNumber = partNumber; - return this; - } - - public Builder fipsCapable(int fipsCapable) { - this.fipsCapable = fipsCapable; - return this; - } - - public Builder fipsApproved(int fipsApproved) { - this.fipsApproved = fipsApproved; - return this; - } - - public Builder pinComplexity(boolean pinComplexity) { - this.pinComplexity = pinComplexity; - return this; - } - - public Builder resetBlocked(int resetBlocked) { - this.resetBlocked = resetBlocked; - return this; - } - - public Builder fpsVersion(@Nullable Version fpsVersion) { - this.fpsVersion = fpsVersion; - return this; - } - - public Builder stmVersion(@Nullable Version stmVersion) { - this.stmVersion = stmVersion; - return this; - } + public Builder pinComplexity(boolean pinComplexity) { + this.pinComplexity = pinComplexity; + return this; } - /** - * Convert value to use bits of the {@link Capability} enum - */ - private static int fromFips(int fips) { - int capabilities = 0; - if ((fips & 0b00000001) != 0) { - capabilities |= Capability.FIDO2.bit; - } - if ((fips & 0b00000010) != 0) { - capabilities |= Capability.PIV.bit; - } - if ((fips & 0b00000100) != 0) { - capabilities |= Capability.OPENPGP.bit; - } - if ((fips & 0b00001000) != 0) { - capabilities |= Capability.OATH.bit; - } - if ((fips & 0b00010000) != 0) { - capabilities |= Capability.HSMAUTH.bit; - } - - return capabilities; + public Builder resetBlocked(int resetBlocked) { + this.resetBlocked = resetBlocked; + return this; } - /** - * Reads an int from a variable length byte array. - */ - private static int readInt(@Nullable byte[] data) { - if (data == null || data.length == 0) { - return 0; - } - int value = 0; - for (byte b : data) { - value <<= 8; - value += (0xff & b); - } - return value; + public Builder fpsVersion(@Nullable Version fpsVersion) { + this.fpsVersion = fpsVersion; + return this; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - DeviceInfo that = (DeviceInfo) o; - return isLocked == that.isLocked && - isFips == that.isFips && - isSky == that.isSky && - fipsCapable == that.fipsCapable && - fipsApproved == that.fipsApproved && - pinComplexity == that.pinComplexity && - resetBlocked == that.resetBlocked && - Objects.equals(config, that.config) && - Objects.equals(serialNumber, that.serialNumber) && - Objects.equals(version, that.version) && - formFactor == that.formFactor && - Objects.equals(supportedCapabilities, that.supportedCapabilities) && - Objects.equals(partNumber, that.partNumber) && - Objects.equals(fpsVersion, that.fpsVersion) && - Objects.equals(stmVersion, that.stmVersion); + public Builder stmVersion(@Nullable Version stmVersion) { + this.stmVersion = stmVersion; + return this; } + } - @Override - public int hashCode() { - return Objects.hash(config, serialNumber, version, formFactor, supportedCapabilities, - isLocked, isFips, isSky, partNumber, fipsCapable, fipsApproved, pinComplexity, - resetBlocked, fpsVersion, stmVersion); + /** Convert value to use bits of the {@link Capability} enum */ + private static int fromFips(int fips) { + int capabilities = 0; + if ((fips & 0b00000001) != 0) { + capabilities |= Capability.FIDO2.bit; } + if ((fips & 0b00000010) != 0) { + capabilities |= Capability.PIV.bit; + } + if ((fips & 0b00000100) != 0) { + capabilities |= Capability.OPENPGP.bit; + } + if ((fips & 0b00001000) != 0) { + capabilities |= Capability.OATH.bit; + } + if ((fips & 0b00010000) != 0) { + capabilities |= Capability.HSMAUTH.bit; + } + + return capabilities; + } - @Override - public String toString() { - return "DeviceInfo{" + - "config=" + config + - ", serialNumber=" + serialNumber + - ", version=" + version + - ", formFactor=" + formFactor + - ", supportedCapabilities=" + supportedCapabilities + - ", isLocked=" + isLocked + - ", isFips=" + isFips + - ", isSky=" + isSky + - ", partNumber=" + partNumber + - ", fipsCapable=" + fipsCapable + - ", fipsApproved=" + fipsApproved + - ", pinComplexity=" + pinComplexity + - ", resetBlocked=" + resetBlocked + - ", fpsVersion=" + fpsVersion + - ", stmVersion=" + stmVersion + - '}'; + /** Reads an int from a variable length byte array. */ + private static int readInt(@Nullable byte[] data) { + if (data == null || data.length == 0) { + return 0; + } + int value = 0; + for (byte b : data) { + value <<= 8; + value += (0xff & b); } + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DeviceInfo that = (DeviceInfo) o; + return isLocked == that.isLocked + && isFips == that.isFips + && isSky == that.isSky + && fipsCapable == that.fipsCapable + && fipsApproved == that.fipsApproved + && pinComplexity == that.pinComplexity + && resetBlocked == that.resetBlocked + && Objects.equals(config, that.config) + && Objects.equals(serialNumber, that.serialNumber) + && Objects.equals(version, that.version) + && formFactor == that.formFactor + && Objects.equals(supportedCapabilities, that.supportedCapabilities) + && Objects.equals(partNumber, that.partNumber) + && Objects.equals(fpsVersion, that.fpsVersion) + && Objects.equals(stmVersion, that.stmVersion); + } + + @Override + public int hashCode() { + return Objects.hash( + config, + serialNumber, + version, + formFactor, + supportedCapabilities, + isLocked, + isFips, + isSky, + partNumber, + fipsCapable, + fipsApproved, + pinComplexity, + resetBlocked, + fpsVersion, + stmVersion); + } + + @Override + public String toString() { + return "DeviceInfo{" + + "config=" + + config + + ", serialNumber=" + + serialNumber + + ", version=" + + version + + ", formFactor=" + + formFactor + + ", supportedCapabilities=" + + supportedCapabilities + + ", isLocked=" + + isLocked + + ", isFips=" + + isFips + + ", isSky=" + + isSky + + ", partNumber=" + + partNumber + + ", fipsCapable=" + + fipsCapable + + ", fipsApproved=" + + fipsApproved + + ", pinComplexity=" + + pinComplexity + + ", resetBlocked=" + + resetBlocked + + ", fpsVersion=" + + fpsVersion + + ", stmVersion=" + + stmVersion + + '}'; + } } diff --git a/management/src/main/java/com/yubico/yubikit/management/FormFactor.java b/management/src/main/java/com/yubico/yubikit/management/FormFactor.java index 7d80da62..de531d75 100755 --- a/management/src/main/java/com/yubico/yubikit/management/FormFactor.java +++ b/management/src/main/java/com/yubico/yubikit/management/FormFactor.java @@ -16,57 +16,40 @@ package com.yubico.yubikit.management; -/** - * The physical form factor of a YubiKey. - */ +/** The physical form factor of a YubiKey. */ public enum FormFactor { - /** - * Used when information about the YubiKey's form factor isn't available. - */ - UNKNOWN(0x00), - /** - * A keychain-sized YubiKey with a USB-A connector. - */ - USB_A_KEYCHAIN(0x01), - /** - * A nano-sized YubiKey with a USB-A connector. - */ - USB_A_NANO(0x02), - /** - * A keychain-sized YubiKey with a USB-C connector. - */ - USB_C_KEYCHAIN(0x03), - /** - * A nano-sized YubiKey with a USB-C connector. - */ - USB_C_NANO(0x04), - /** - * A keychain-sized YubiKey with both USB-C and Lightning connectors. - */ - USB_C_LIGHTNING(0x05), - /** - * A keychain-sized YubiKey with fingerprint sensor and USB-A connector. - */ - USB_A_BIO(0x06), - /** - * A keychain-sized YubiKey with fingerprint sensor and USB-C connector. - */ - USB_C_BIO(0x07); + /** Used when information about the YubiKey's form factor isn't available. */ + UNKNOWN(0x00), + /** A keychain-sized YubiKey with a USB-A connector. */ + USB_A_KEYCHAIN(0x01), + /** A nano-sized YubiKey with a USB-A connector. */ + USB_A_NANO(0x02), + /** A keychain-sized YubiKey with a USB-C connector. */ + USB_C_KEYCHAIN(0x03), + /** A nano-sized YubiKey with a USB-C connector. */ + USB_C_NANO(0x04), + /** A keychain-sized YubiKey with both USB-C and Lightning connectors. */ + USB_C_LIGHTNING(0x05), + /** A keychain-sized YubiKey with fingerprint sensor and USB-A connector. */ + USB_A_BIO(0x06), + /** A keychain-sized YubiKey with fingerprint sensor and USB-C connector. */ + USB_C_BIO(0x07); - public final int value; + public final int value; - FormFactor(int value) { - this.value = value; - } + FormFactor(int value) { + this.value = value; + } - /** - * Returns the form factor corresponding to the given Management application form factor constant, or UNKNOWN if the value is unknown. - */ - public static FormFactor valueOf(int value) { - value &= 0xf; - if (value < FormFactor.values().length) { - return FormFactor.values()[value]; - } - return UNKNOWN; + /** + * Returns the form factor corresponding to the given Management application form factor constant, + * or UNKNOWN if the value is unknown. + */ + public static FormFactor valueOf(int value) { + value &= 0xf; + if (value < FormFactor.values().length) { + return FormFactor.values()[value]; } + return UNKNOWN; + } } diff --git a/management/src/main/java/com/yubico/yubikit/management/ManagementSession.java b/management/src/main/java/com/yubico/yubikit/management/ManagementSession.java index 0764b4e6..9afc9028 100755 --- a/management/src/main/java/com/yubico/yubikit/management/ManagementSession.java +++ b/management/src/main/java/com/yubico/yubikit/management/ManagementSession.java @@ -41,9 +41,6 @@ import com.yubico.yubikit.core.util.Callback; import com.yubico.yubikit.core.util.Result; import com.yubico.yubikit.core.util.Tlvs; - -import org.slf4j.LoggerFactory; - import java.io.Closeable; import java.io.IOException; import java.nio.ByteBuffer; @@ -51,406 +48,455 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; - import javax.annotation.Nullable; +import org.slf4j.LoggerFactory; /** * Application to get information about and configure a YubiKey via the Management Application. - * https://developers.yubico.com/yubikey-manager/Config_Reference.html + * + * @see + * https://developers.yubico.com/yubikey-manager/Config_Reference.html */ public class ManagementSession extends ApplicationSession { - // Features - /** - * Support the SET_MODE command to change the USB mode of the YubiKey. - */ - public static final Feature FEATURE_MODE = new Feature("Mode") { + // Features + /** Support the SET_MODE command to change the USB mode of the YubiKey. */ + public static final Feature FEATURE_MODE = + new Feature("Mode") { @Override public boolean isSupportedBy(Version version) { - return version.isAtLeast(3, 0, 0) && version.isLessThan(5, 0, 0); - } - }; - /** - * Support for reading the DeviceInfo data from the YubiKey. - */ - public static final Feature FEATURE_DEVICE_INFO = new Feature.Versioned<>("Device Info", 4, 1, 0); - /** - * Support for writing DeviceConfig data to the YubiKey. - */ - public static final Feature FEATURE_DEVICE_CONFIG = new Feature.Versioned<>("Device Config", 5, 0, 0); - /** - * Support for device-wide reset. - */ - public static final Feature FEATURE_DEVICE_RESET = new Feature.Versioned<>("Device Reset", 5, 6, 0); - - // Smart card command constants - private static final byte OTP_INS_CONFIG = 0x01; - private static final byte INS_READ_CONFIG = 0x1d; - private static final byte INS_WRITE_CONFIG = 0x1c; - private static final byte INS_SET_MODE = 0x16; - private static final byte INS_DEVICE_RESET = 0x1f; - private static final byte P1_DEVICE_CONFIG = 0x11; - - // OTP command constants - private static final byte CMD_DEVICE_CONFIG = 0x11; - private static final byte CMD_YK4_CAPABILITIES = 0x13; - private static final byte CMD_YK4_SET_DEVICE_INFO = 0x15; - - // FIDO command constants - private static final byte CTAP_TYPE_INIT = (byte) 0x80; - private static final byte CTAP_VENDOR_FIRST = 0x40; - private static final byte CTAP_YUBIKEY_DEVICE_CONFIG = CTAP_TYPE_INIT | CTAP_VENDOR_FIRST; - private static final byte CTAP_READ_CONFIG = CTAP_TYPE_INIT | CTAP_VENDOR_FIRST + 2; - private static final byte CTAP_WRITE_CONFIG = CTAP_TYPE_INIT | CTAP_VENDOR_FIRST + 3; - - private final Backend backend; - private final Version version; - - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(ManagementSession.class); - - /** - * Establishes a new session with a YubiKeys Management application, over a {@link SmartCardConnection}. - * - * @param connection connection with YubiKey - * @throws IOException in case of connection error - * @throws ApplicationNotAvailableException in case the application is missing/disabled - */ - public ManagementSession(SmartCardConnection connection) throws IOException, ApplicationNotAvailableException { - this(connection, null); - } - - /** - * Establishes a new session with a YubiKeys Management application, over a {@link SmartCardConnection}. - * - * @param connection connection with YubiKey - * @param scpKeyParams SCP key parameters to establish a secure connection - * @throws IOException in case of connection error - * @throws ApplicationNotAvailableException in case the application is missing/disabled - */ - public ManagementSession(SmartCardConnection connection, @Nullable ScpKeyParams scpKeyParams) throws IOException, ApplicationNotAvailableException { - SmartCardProtocol protocol = new SmartCardProtocol(connection); - Version version; - try { - version = Version.parse(new String(protocol.select(AppId.MANAGEMENT), StandardCharsets.UTF_8)); - if (scpKeyParams != null) { - protocol.initScp(scpKeyParams); - } else if (version.major == 3) { - // Workaround to "de-select" on NEO - connection.sendAndReceive(new byte[]{(byte) 0xa4, 0x04, 0x00, 0x08}); - protocol.select(AppId.OTP); - } - } catch (ApplicationNotAvailableException e) { - if (connection.getTransport() == Transport.NFC) { - // NEO doesn't support the Management Application over NFC, but can use the OTP application. - version = Version.fromBytes(protocol.select(AppId.OTP)); - } else { - throw e; - } - } catch (BadResponseException | ApduException e) { - throw new IOException("Failed setting up SCP session", e); + return version.isAtLeast(3, 0, 0) && version.isLessThan(5, 0, 0); } - this.version = version; - - if (version.major == 3) { // NEO, using the OTP application - backend = new Backend(protocol) { - @Override - byte[] readConfig(int page) { - throw new UnsupportedOperationException("readConfig not supported on YubiKey NEO"); - } - - @Override - void writeConfig(byte[] config) { - throw new UnsupportedOperationException("writeConfig not supported on YubiKey NEO"); - } - - @Override - void setMode(byte[] data) throws IOException, CommandException { - delegate.sendAndReceive(new Apdu(0, OTP_INS_CONFIG, CMD_DEVICE_CONFIG, 0, data)); - } - - @Override - void deviceReset() { - throw new UnsupportedOperationException("deviceReset not supported on YubiKey NEO"); - } - }; - } else { - backend = new Backend(protocol) { - @Override - byte[] readConfig(int page) throws IOException, CommandException { - Logger.debug(logger, "Reading config page {}...", page); - return delegate.sendAndReceive(new Apdu(0, INS_READ_CONFIG, page, 0, null)); - } - - @Override - void writeConfig(byte[] config) throws IOException, CommandException { - delegate.sendAndReceive(new Apdu(0, INS_WRITE_CONFIG, 0, 0, config)); - } - - @Override - void setMode(byte[] data) throws IOException, CommandException { - delegate.sendAndReceive(new Apdu(0, INS_SET_MODE, P1_DEVICE_CONFIG, 0, data)); - } - - @Override - void deviceReset() throws IOException, CommandException { - delegate.sendAndReceive(new Apdu(0, INS_DEVICE_RESET, 0, 0, null)); - } - }; - } - logCtor(connection); + }; + + /** Support for reading the DeviceInfo data from the YubiKey. */ + public static final Feature FEATURE_DEVICE_INFO = + new Feature.Versioned<>("Device Info", 4, 1, 0); + + /** Support for writing DeviceConfig data to the YubiKey. */ + public static final Feature FEATURE_DEVICE_CONFIG = + new Feature.Versioned<>("Device Config", 5, 0, 0); + + /** Support for device-wide reset. */ + public static final Feature FEATURE_DEVICE_RESET = + new Feature.Versioned<>("Device Reset", 5, 6, 0); + + // Smart card command constants + private static final byte OTP_INS_CONFIG = 0x01; + private static final byte INS_READ_CONFIG = 0x1d; + private static final byte INS_WRITE_CONFIG = 0x1c; + private static final byte INS_SET_MODE = 0x16; + private static final byte INS_DEVICE_RESET = 0x1f; + private static final byte P1_DEVICE_CONFIG = 0x11; + + // OTP command constants + private static final byte CMD_DEVICE_CONFIG = 0x11; + private static final byte CMD_YK4_CAPABILITIES = 0x13; + private static final byte CMD_YK4_SET_DEVICE_INFO = 0x15; + + // FIDO command constants + private static final byte CTAP_TYPE_INIT = (byte) 0x80; + private static final byte CTAP_VENDOR_FIRST = 0x40; + private static final byte CTAP_YUBIKEY_DEVICE_CONFIG = CTAP_TYPE_INIT | CTAP_VENDOR_FIRST; + private static final byte CTAP_READ_CONFIG = CTAP_TYPE_INIT | CTAP_VENDOR_FIRST + 2; + private static final byte CTAP_WRITE_CONFIG = CTAP_TYPE_INIT | CTAP_VENDOR_FIRST + 3; + + private final Backend backend; + private final Version version; + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(ManagementSession.class); + + /** + * Establishes a new session with a YubiKeys Management application, over a {@link + * SmartCardConnection}. + * + * @param connection connection with YubiKey + * @throws IOException in case of connection error + * @throws ApplicationNotAvailableException in case the application is missing/disabled + */ + public ManagementSession(SmartCardConnection connection) + throws IOException, ApplicationNotAvailableException { + this(connection, null); + } + + /** + * Establishes a new session with a YubiKeys Management application, over a {@link + * SmartCardConnection}. + * + * @param connection connection with YubiKey + * @param scpKeyParams SCP key parameters to establish a secure connection + * @throws IOException in case of connection error + * @throws ApplicationNotAvailableException in case the application is missing/disabled + */ + public ManagementSession(SmartCardConnection connection, @Nullable ScpKeyParams scpKeyParams) + throws IOException, ApplicationNotAvailableException { + SmartCardProtocol protocol = new SmartCardProtocol(connection); + Version version; + try { + version = + Version.parse(new String(protocol.select(AppId.MANAGEMENT), StandardCharsets.UTF_8)); + if (scpKeyParams != null) { + protocol.initScp(scpKeyParams); + } else if (version.major == 3) { + // Workaround to "de-select" on NEO + connection.sendAndReceive(new byte[] {(byte) 0xa4, 0x04, 0x00, 0x08}); + protocol.select(AppId.OTP); + } + } catch (ApplicationNotAvailableException e) { + if (connection.getTransport() == Transport.NFC) { + // NEO doesn't support the Management Application over NFC, but can use the OTP application. + version = Version.fromBytes(protocol.select(AppId.OTP)); + } else { + throw e; + } + } catch (BadResponseException | ApduException e) { + throw new IOException("Failed setting up SCP session", e); } + this.version = version; - /** - * Establishes a new session with a YubiKeys Management application, over an {@link OtpConnection}. - * - * @param connection connection with YubiKey - * @throws IOException in case of connection error - * @throws ApplicationNotAvailableException in case the application is missing/disabled - */ - public ManagementSession(OtpConnection connection) throws IOException, ApplicationNotAvailableException { - OtpProtocol protocol = new OtpProtocol(connection); - version = Version.fromBytes(protocol.readStatus()); - if (version.isLessThan(3, 0, 0) && version.major != 0) { - throw new ApplicationNotAvailableException("Management Application requires YubiKey 3 or later"); - } - backend = new Backend(protocol) { + if (version.major == 3) { // NEO, using the OTP application + backend = + new Backend(protocol) { @Override - byte[] readConfig(int page) throws IOException, CommandException { - Logger.debug(logger, "Reading config page {}...", page); - byte[] response = delegate.sendAndReceive(CMD_YK4_CAPABILITIES, pagePayload(page), null); - if (ChecksumUtils.checkCrc(response, response[0] + 1 + 2)) { - return Arrays.copyOf(response, response[0] + 1); - } - throw new IOException("Invalid CRC"); + byte[] readConfig(int page) { + throw new UnsupportedOperationException("readConfig not supported on YubiKey NEO"); } @Override - void writeConfig(byte[] config) throws IOException, CommandException { - delegate.sendAndReceive(CMD_YK4_SET_DEVICE_INFO, config, null); + void writeConfig(byte[] config) { + throw new UnsupportedOperationException("writeConfig not supported on YubiKey NEO"); } @Override void setMode(byte[] data) throws IOException, CommandException { - delegate.sendAndReceive(CMD_DEVICE_CONFIG, data, null); + delegate.sendAndReceive(new Apdu(0, OTP_INS_CONFIG, CMD_DEVICE_CONFIG, 0, data)); } @Override void deviceReset() { - throw new UnsupportedOperationException("deviceReset is only available over CCID"); + throw new UnsupportedOperationException("deviceReset not supported on YubiKey NEO"); } - }; - logCtor(connection); - } - - /** - * Establishes a new session with a YubiKeys Management application, over a {@link FidoConnection}. - * - * @param connection a connection over the FIDO USB transport with a YubiKey - * @throws IOException in case of connection error - */ - public ManagementSession(FidoConnection connection) throws IOException { - FidoProtocol protocol = new FidoProtocol(connection); - version = protocol.getVersion(); - backend = new Backend(protocol) { + }; + } else { + backend = + new Backend(protocol) { @Override - byte[] readConfig(int page) throws IOException { - Logger.debug(logger, "Reading config page {}...", page); - return delegate.sendAndReceive(CTAP_READ_CONFIG, pagePayload(page), null); + byte[] readConfig(int page) throws IOException, CommandException { + Logger.debug(logger, "Reading config page {}...", page); + return delegate.sendAndReceive(new Apdu(0, INS_READ_CONFIG, page, 0, null)); } @Override - void writeConfig(byte[] config) throws IOException { - delegate.sendAndReceive(CTAP_WRITE_CONFIG, config, null); + void writeConfig(byte[] config) throws IOException, CommandException { + delegate.sendAndReceive(new Apdu(0, INS_WRITE_CONFIG, 0, 0, config)); } @Override - void setMode(byte[] data) throws IOException { - delegate.sendAndReceive(CTAP_YUBIKEY_DEVICE_CONFIG, data, null); + void setMode(byte[] data) throws IOException, CommandException { + delegate.sendAndReceive(new Apdu(0, INS_SET_MODE, P1_DEVICE_CONFIG, 0, data)); } @Override - void deviceReset() { - throw new UnsupportedOperationException("deviceReset is only available over CCID"); + void deviceReset() throws IOException, CommandException { + delegate.sendAndReceive(new Apdu(0, INS_DEVICE_RESET, 0, 0, null)); } - }; - logCtor(connection); - } - - /** - * Connects to a YubiKeyDevice and establishes a new session with a YubiKeys Management application. - *

- * This method will use whichever connection type is available. - * - * @param device A YubiKey device to use - */ - public static void create(YubiKeyDevice device, Callback> callback) { - if (device.supportsConnection(SmartCardConnection.class)) { - device.requestConnection(SmartCardConnection.class, value -> callback.invoke(Result.of(() -> new ManagementSession(value.getValue())))); - } else if (device.supportsConnection(OtpConnection.class)) { - device.requestConnection(OtpConnection.class, value -> callback.invoke(Result.of(() -> new ManagementSession(value.getValue())))); - } else if (device.supportsConnection(FidoConnection.class)) { - device.requestConnection(FidoConnection.class, value -> callback.invoke(Result.of(() -> new ManagementSession(value.getValue())))); - } else { - callback.invoke(Result.failure(new ApplicationNotAvailableException("Session does not support any compatible connection type"))); - } - } - - @Override - public void close() throws IOException { - backend.close(); + }; } - - @Override - public Version getVersion() { - return version; + logCtor(connection); + } + + /** + * Establishes a new session with a YubiKeys Management application, over an {@link + * OtpConnection}. + * + * @param connection connection with YubiKey + * @throws IOException in case of connection error + * @throws ApplicationNotAvailableException in case the application is missing/disabled + */ + public ManagementSession(OtpConnection connection) + throws IOException, ApplicationNotAvailableException { + OtpProtocol protocol = new OtpProtocol(connection); + version = Version.fromBytes(protocol.readStatus()); + if (version.isLessThan(3, 0, 0) && version.major != 0) { + throw new ApplicationNotAvailableException( + "Management Application requires YubiKey 3 or later"); } - - /** - * Get device information from the YubiKey. - *

- * This functionality requires support for {@link #FEATURE_DEVICE_INFO}, available on YubiKey 4.1 or later. - * - * @return a DeviceInfo object - * @throws IOException in case of connection error - * @throws CommandException in case of error response - */ - public DeviceInfo getDeviceInfo() throws IOException, CommandException { - require(FEATURE_DEVICE_INFO); - - final Map tlvs = new HashMap<>(); - boolean hasMoreData = true; - int page = 0; - - while (hasMoreData) { - final byte[] encoded = backend.readConfig(page); - if (encoded.length - 1 != encoded[0]) { - throw new BadResponseException("Invalid length"); + backend = + new Backend(protocol) { + @Override + byte[] readConfig(int page) throws IOException, CommandException { + Logger.debug(logger, "Reading config page {}...", page); + byte[] response = + delegate.sendAndReceive(CMD_YK4_CAPABILITIES, pagePayload(page), null); + if (ChecksumUtils.checkCrc(response, response[0] + 1 + 2)) { + return Arrays.copyOf(response, response[0] + 1); } - Map decoded = - Tlvs.decodeMap(Arrays.copyOfRange(encoded, 1, encoded.length)); - byte[] moreData = decoded.get(0x10); // YK_MORE_DEVICE_INFO - hasMoreData = moreData != null && - moreData.length == 1 && - moreData[0] == (byte) 1; - tlvs.putAll(decoded); - page++; - } - return DeviceInfo.parseTlvs(tlvs, version); + throw new IOException("Invalid CRC"); + } + + @Override + void writeConfig(byte[] config) throws IOException, CommandException { + delegate.sendAndReceive(CMD_YK4_SET_DEVICE_INFO, config, null); + } + + @Override + void setMode(byte[] data) throws IOException, CommandException { + delegate.sendAndReceive(CMD_DEVICE_CONFIG, data, null); + } + + @Override + void deviceReset() { + throw new UnsupportedOperationException("deviceReset is only available over CCID"); + } + }; + logCtor(connection); + } + + /** + * Establishes a new session with a YubiKeys Management application, over a {@link + * FidoConnection}. + * + * @param connection a connection over the FIDO USB transport with a YubiKey + * @throws IOException in case of connection error + */ + public ManagementSession(FidoConnection connection) throws IOException { + FidoProtocol protocol = new FidoProtocol(connection); + version = protocol.getVersion(); + backend = + new Backend(protocol) { + @Override + byte[] readConfig(int page) throws IOException { + Logger.debug(logger, "Reading config page {}...", page); + return delegate.sendAndReceive(CTAP_READ_CONFIG, pagePayload(page), null); + } + + @Override + void writeConfig(byte[] config) throws IOException { + delegate.sendAndReceive(CTAP_WRITE_CONFIG, config, null); + } + + @Override + void setMode(byte[] data) throws IOException { + delegate.sendAndReceive(CTAP_YUBIKEY_DEVICE_CONFIG, data, null); + } + + @Override + void deviceReset() { + throw new UnsupportedOperationException("deviceReset is only available over CCID"); + } + }; + logCtor(connection); + } + + /** + * Connects to a YubiKeyDevice and establishes a new session with a YubiKeys Management + * application. + * + *

This method will use whichever connection type is available. + * + * @param device A YubiKey device to use + */ + public static void create( + YubiKeyDevice device, Callback> callback) { + if (device.supportsConnection(SmartCardConnection.class)) { + device.requestConnection( + SmartCardConnection.class, + value -> callback.invoke(Result.of(() -> new ManagementSession(value.getValue())))); + } else if (device.supportsConnection(OtpConnection.class)) { + device.requestConnection( + OtpConnection.class, + value -> callback.invoke(Result.of(() -> new ManagementSession(value.getValue())))); + } else if (device.supportsConnection(FidoConnection.class)) { + device.requestConnection( + FidoConnection.class, + value -> callback.invoke(Result.of(() -> new ManagementSession(value.getValue())))); + } else { + callback.invoke( + Result.failure( + new ApplicationNotAvailableException( + "Session does not support any compatible connection type"))); } - - /** - * Write device configuration to a YubiKey 5 or later. - *

- * This functionality requires support for {@link #FEATURE_DEVICE_CONFIG}, available on YubiKey 5 or later. - * - * @param config the device configuration to write - * @param reboot if true cause the YubiKey to immediately reboot, applying the new configuration - * @param currentLockCode required if a configuration lock code is set - * @param newLockCode changes or removes (if 16 byte all-zero) the configuration lock code - * @throws IOException in case of connection error - * @throws CommandException in case of error response - */ - public void updateDeviceConfig(DeviceConfig config, boolean reboot, @Nullable byte[] currentLockCode, @Nullable byte[] newLockCode) throws IOException, CommandException { - require(FEATURE_DEVICE_CONFIG); - byte[] data = config.getBytes(reboot, currentLockCode, newLockCode); - Logger.debug(logger, "Writing device config: {}, reboot: {}, " - + "current lock code: {}, new lock code: {}", - config, reboot, currentLockCode != null, newLockCode != null); - backend.writeConfig(data); - Logger.info(logger, "Device config written"); + } + + @Override + public void close() throws IOException { + backend.close(); + } + + @Override + public Version getVersion() { + return version; + } + + /** + * Get device information from the YubiKey. + * + *

This functionality requires support for {@link #FEATURE_DEVICE_INFO}, available on YubiKey + * 4.1 or later. + * + * @return a DeviceInfo object + * @throws IOException in case of connection error + * @throws CommandException in case of error response + */ + public DeviceInfo getDeviceInfo() throws IOException, CommandException { + require(FEATURE_DEVICE_INFO); + + final Map tlvs = new HashMap<>(); + boolean hasMoreData = true; + int page = 0; + + while (hasMoreData) { + final byte[] encoded = backend.readConfig(page); + if (encoded.length - 1 != encoded[0]) { + throw new BadResponseException("Invalid length"); + } + Map decoded = Tlvs.decodeMap(Arrays.copyOfRange(encoded, 1, encoded.length)); + byte[] moreData = decoded.get(0x10); // YK_MORE_DEVICE_INFO + hasMoreData = moreData != null && moreData.length == 1 && moreData[0] == (byte) 1; + tlvs.putAll(decoded); + page++; } - - /** - * Write device configuration for YubiKey NEO and YubiKey 4. - *

- * This functionality requires support for {@link #FEATURE_MODE}, available on YubiKey 3 and 4. - * If {@link #FEATURE_DEVICE_CONFIG} is supported, the mode will be translated into a DeviceConfig and {@link #updateDeviceConfig} will instead be used. - * - * @param mode USB transport mode to set - * @param chalrespTimeout timeout (seconds) for challenge-response requiring touch. - * @param autoejectTimeout timeout (10x seconds) for auto-eject (only used for CCID-only mode). - * @throws IOException in case of connection error - * @throws ApduException in case of communication or not supported operation error - */ - public void setMode(UsbInterface.Mode mode, byte chalrespTimeout, short autoejectTimeout) throws IOException, CommandException { - Logger.debug(logger, "Set mode: {}, chalresp_timeout: {}, auto_eject_timeout: {}", - mode, chalrespTimeout, autoejectTimeout); - if (supports(FEATURE_DEVICE_CONFIG)) { - //Translate into DeviceConfig and set using writeDeviceConfig - int usbEnabled = 0; - if ((mode.interfaces & UsbInterface.OTP) != 0) { - usbEnabled |= Capability.OTP.bit; - } - if ((mode.interfaces & UsbInterface.CCID) != 0) { - usbEnabled |= Capability.OATH.bit | Capability.PIV.bit | Capability.OPENPGP.bit; - } - if ((mode.interfaces & UsbInterface.FIDO) != 0) { - usbEnabled |= Capability.U2F.bit | Capability.FIDO2.bit; - } - Logger.debug(logger, "Delegating to DeviceConfig with usb_enabled: {}", usbEnabled); - updateDeviceConfig( - new DeviceConfig.Builder() - .enabledCapabilities(Transport.USB, usbEnabled) - .challengeResponseTimeout(chalrespTimeout) - .autoEjectTimeout(autoejectTimeout) - .build(), - false, null, null); - } else { - require(FEATURE_MODE); - byte[] data = ByteBuffer.allocate(4).put(mode.value).put(chalrespTimeout).putShort(autoejectTimeout).array(); - backend.setMode(data); - Logger.info(logger, "Mode configuration written"); - } + return DeviceInfo.parseTlvs(tlvs, version); + } + + /** + * Write device configuration to a YubiKey 5 or later. + * + *

This functionality requires support for {@link #FEATURE_DEVICE_CONFIG}, available on YubiKey + * 5 or later. + * + * @param config the device configuration to write + * @param reboot if true cause the YubiKey to immediately reboot, applying the new configuration + * @param currentLockCode required if a configuration lock code is set + * @param newLockCode changes or removes (if 16 byte all-zero) the configuration lock code + * @throws IOException in case of connection error + * @throws CommandException in case of error response + */ + public void updateDeviceConfig( + DeviceConfig config, + boolean reboot, + @Nullable byte[] currentLockCode, + @Nullable byte[] newLockCode) + throws IOException, CommandException { + require(FEATURE_DEVICE_CONFIG); + byte[] data = config.getBytes(reboot, currentLockCode, newLockCode); + Logger.debug( + logger, + "Writing device config: {}, reboot: {}, " + "current lock code: {}, new lock code: {}", + config, + reboot, + currentLockCode != null, + newLockCode != null); + backend.writeConfig(data); + Logger.info(logger, "Device config written"); + } + + /** + * Write device configuration for YubiKey NEO and YubiKey 4. + * + *

This functionality requires support for {@link #FEATURE_MODE}, available on YubiKey 3 and 4. + * If {@link #FEATURE_DEVICE_CONFIG} is supported, the mode will be translated into a DeviceConfig + * and {@link #updateDeviceConfig} will instead be used. + * + * @param mode USB transport mode to set + * @param chalrespTimeout timeout (seconds) for challenge-response requiring touch. + * @param autoejectTimeout timeout (10x seconds) for auto-eject (only used for CCID-only mode). + * @throws IOException in case of connection error + * @throws ApduException in case of communication or not supported operation error + */ + public void setMode(UsbInterface.Mode mode, byte chalrespTimeout, short autoejectTimeout) + throws IOException, CommandException { + Logger.debug( + logger, + "Set mode: {}, chalresp_timeout: {}, auto_eject_timeout: {}", + mode, + chalrespTimeout, + autoejectTimeout); + if (supports(FEATURE_DEVICE_CONFIG)) { + // Translate into DeviceConfig and set using writeDeviceConfig + int usbEnabled = 0; + if ((mode.interfaces & UsbInterface.OTP) != 0) { + usbEnabled |= Capability.OTP.bit; + } + if ((mode.interfaces & UsbInterface.CCID) != 0) { + usbEnabled |= Capability.OATH.bit | Capability.PIV.bit | Capability.OPENPGP.bit; + } + if ((mode.interfaces & UsbInterface.FIDO) != 0) { + usbEnabled |= Capability.U2F.bit | Capability.FIDO2.bit; + } + Logger.debug(logger, "Delegating to DeviceConfig with usb_enabled: {}", usbEnabled); + updateDeviceConfig( + new DeviceConfig.Builder() + .enabledCapabilities(Transport.USB, usbEnabled) + .challengeResponseTimeout(chalrespTimeout) + .autoEjectTimeout(autoejectTimeout) + .build(), + false, + null, + null); + } else { + require(FEATURE_MODE); + byte[] data = + ByteBuffer.allocate(4) + .put(mode.value) + .put(chalrespTimeout) + .putShort(autoejectTimeout) + .array(); + backend.setMode(data); + Logger.info(logger, "Mode configuration written"); } - - /** - * Perform a device-wide reset in Bio Multi-protocol Edition devices. - *

- * This functionality requires support for {@link #FEATURE_DEVICE_RESET}. - * - * @throws IOException in case of connection error - * @throws ApduException in case of communication or not supported operation error - */ - public void deviceReset() throws IOException, CommandException { - require(FEATURE_DEVICE_RESET); - backend.deviceReset(); - Logger.info(logger, "Device reset"); + } + + /** + * Perform a device-wide reset in Bio Multi-protocol Edition devices. + * + *

This functionality requires support for {@link #FEATURE_DEVICE_RESET}. + * + * @throws IOException in case of connection error + * @throws ApduException in case of communication or not supported operation error + */ + public void deviceReset() throws IOException, CommandException { + require(FEATURE_DEVICE_RESET); + backend.deviceReset(); + Logger.info(logger, "Device reset"); + } + + private abstract static class Backend implements Closeable { + protected final T delegate; + + private Backend(T delegate) { + this.delegate = delegate; } - private static abstract class Backend implements Closeable { - protected final T delegate; - - private Backend(T delegate) { - this.delegate = delegate; - } - - byte[] readConfig() throws IOException, CommandException { - return readConfig(0); - } - - abstract byte[] readConfig(int page) throws IOException, CommandException; + byte[] readConfig() throws IOException, CommandException { + return readConfig(0); + } - abstract void writeConfig(byte[] config) throws IOException, CommandException; + abstract byte[] readConfig(int page) throws IOException, CommandException; - abstract void setMode(byte[] data) throws IOException, CommandException; + abstract void writeConfig(byte[] config) throws IOException, CommandException; - abstract void deviceReset() throws IOException, CommandException; + abstract void setMode(byte[] data) throws IOException, CommandException; - @Override - public void close() throws IOException { - delegate.close(); - } - } + abstract void deviceReset() throws IOException, CommandException; - private void logCtor(YubiKeyConnection connection) { - Logger.debug(logger, "Management session initialized for connection={}, version={}", - connection.getClass().getSimpleName(), - getVersion()); + @Override + public void close() throws IOException { + delegate.close(); } - - private static byte[] pagePayload(int page) { - if (page > 255 || page < 0) { - throw new IllegalArgumentException("Invalid page value " + page); - } - return new byte[]{(byte) page}; + } + + private void logCtor(YubiKeyConnection connection) { + Logger.debug( + logger, + "Management session initialized for connection={}, version={}", + connection.getClass().getSimpleName(), + getVersion()); + } + + private static byte[] pagePayload(int page) { + if (page > 255 || page < 0) { + throw new IllegalArgumentException("Invalid page value " + page); } + return new byte[] {(byte) page}; + } } diff --git a/management/src/main/java/com/yubico/yubikit/management/UsbInterface.java b/management/src/main/java/com/yubico/yubikit/management/UsbInterface.java index 6611a73b..171f571b 100755 --- a/management/src/main/java/com/yubico/yubikit/management/UsbInterface.java +++ b/management/src/main/java/com/yubico/yubikit/management/UsbInterface.java @@ -16,57 +16,52 @@ package com.yubico.yubikit.management; /** - * Provides constants for the different YubiKey USB interfaces, and the Mode enum for combinations of enabled interfaces. - * @deprecated This class has been moved to the core module, and will remain here only until YubiKit 3.0 is released. + * Provides constants for the different YubiKey USB interfaces, and the Mode enum for combinations + * of enabled interfaces. + * + * @deprecated This class has been moved to the core module, and will remain here only until YubiKit + * 3.0 is released. */ @Deprecated public final class UsbInterface { - @Deprecated - public static final int OTP = 0x01; - @Deprecated - public static final int FIDO = 0x02; - @Deprecated - public static final int CCID = 0x04; + @Deprecated public static final int OTP = 0x01; + @Deprecated public static final int FIDO = 0x02; + @Deprecated public static final int CCID = 0x04; - private UsbInterface() { - } + private UsbInterface() {} - /** - * Used for configuring USB Mode for YubiKey 3 and 4. - *

- * This is replaced by DeviceConfig starting with YubiKey 5. - */ - @Deprecated - public enum Mode { - OTP((byte) 0x00, UsbInterface.OTP), - CCID((byte) 0x01, UsbInterface.CCID), - OTP_CCID((byte) 0x02, UsbInterface.OTP | UsbInterface.CCID), - FIDO((byte) 0x03, UsbInterface.FIDO), - OTP_FIDO((byte) 0x04, UsbInterface.OTP | UsbInterface.FIDO), - FIDO_CCID((byte) 0x05, UsbInterface.FIDO | UsbInterface.CCID), - OTP_FIDO_CCID((byte) 0x06, UsbInterface.OTP | UsbInterface.FIDO | UsbInterface.CCID); + /** + * Used for configuring USB Mode for YubiKey 3 and 4. + * + *

This is replaced by DeviceConfig starting with YubiKey 5. + */ + @Deprecated + public enum Mode { + OTP((byte) 0x00, UsbInterface.OTP), + CCID((byte) 0x01, UsbInterface.CCID), + OTP_CCID((byte) 0x02, UsbInterface.OTP | UsbInterface.CCID), + FIDO((byte) 0x03, UsbInterface.FIDO), + OTP_FIDO((byte) 0x04, UsbInterface.OTP | UsbInterface.FIDO), + FIDO_CCID((byte) 0x05, UsbInterface.FIDO | UsbInterface.CCID), + OTP_FIDO_CCID((byte) 0x06, UsbInterface.OTP | UsbInterface.FIDO | UsbInterface.CCID); - @Deprecated - public final byte value; - @Deprecated - public final int interfaces; + @Deprecated public final byte value; + @Deprecated public final int interfaces; - Mode(byte value, int interfaces) { - this.value = value; - this.interfaces = interfaces; - } + Mode(byte value, int interfaces) { + this.value = value; + this.interfaces = interfaces; + } - /** - * Returns the USB Mode given the enabled USB interfaces it has. - */ - @Deprecated - public static Mode getMode(int interfaces) { - for (Mode mode : Mode.values()) { - if (mode.interfaces == interfaces) { - return mode; - } - } - throw new IllegalArgumentException("Invalid interfaces for Mode"); + /** Returns the USB Mode given the enabled USB interfaces it has. */ + @Deprecated + public static Mode getMode(int interfaces) { + for (Mode mode : Mode.values()) { + if (mode.interfaces == interfaces) { + return mode; } + } + throw new IllegalArgumentException("Invalid interfaces for Mode"); } + } } diff --git a/management/src/main/java/com/yubico/yubikit/management/package-info.java b/management/src/main/java/com/yubico/yubikit/management/package-info.java index 5f571339..ea5880dc 100755 --- a/management/src/main/java/com/yubico/yubikit/management/package-info.java +++ b/management/src/main/java/com/yubico/yubikit/management/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.management; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/management/src/test/java/com/yubico/yubikit/management/DeviceConfigBuilderTest.java b/management/src/test/java/com/yubico/yubikit/management/DeviceConfigBuilderTest.java index f7db8244..7ff5d73d 100644 --- a/management/src/test/java/com/yubico/yubikit/management/DeviceConfigBuilderTest.java +++ b/management/src/test/java/com/yubico/yubikit/management/DeviceConfigBuilderTest.java @@ -24,37 +24,37 @@ import static org.junit.Assert.assertNull; import com.yubico.yubikit.core.Transport; - import org.junit.Test; public class DeviceConfigBuilderTest { - @Test - public void testDefaults() { - DeviceConfig defaultConfig = new DeviceConfig.Builder().build(); - assertNull(defaultConfig.getEnabledCapabilities(Transport.USB)); - assertNull(defaultConfig.getEnabledCapabilities(Transport.NFC)); - assertNull(defaultConfig.getAutoEjectTimeout()); - assertNull(defaultConfig.getChallengeResponseTimeout()); - assertNull(defaultConfig.getDeviceFlags()); - assertNull(defaultConfig.getNfcRestricted()); - } + @Test + public void testDefaults() { + DeviceConfig defaultConfig = new DeviceConfig.Builder().build(); + assertNull(defaultConfig.getEnabledCapabilities(Transport.USB)); + assertNull(defaultConfig.getEnabledCapabilities(Transport.NFC)); + assertNull(defaultConfig.getAutoEjectTimeout()); + assertNull(defaultConfig.getChallengeResponseTimeout()); + assertNull(defaultConfig.getDeviceFlags()); + assertNull(defaultConfig.getNfcRestricted()); + } - @Test - public void testBuild() { - DeviceConfig config = new DeviceConfig.Builder() - .enabledCapabilities(Transport.USB, 12345) - .enabledCapabilities(Transport.NFC, 67890) - .autoEjectTimeout((short) 128) - .challengeResponseTimeout((byte) 55) - .deviceFlags(98765) - .nfcRestricted(true) - .build(); - assertIntegerEquals(12345, config.getEnabledCapabilities(Transport.USB)); - assertIntegerEquals(67890, config.getEnabledCapabilities(Transport.NFC)); - assertShortEquals(128, config.getAutoEjectTimeout()); - assertByteEquals(55, config.getChallengeResponseTimeout()); - assertIntegerEquals(98765, config.getDeviceFlags()); - assertNotNull(config.getNfcRestricted()); - assertIsTrue(config.getNfcRestricted()); - } + @Test + public void testBuild() { + DeviceConfig config = + new DeviceConfig.Builder() + .enabledCapabilities(Transport.USB, 12345) + .enabledCapabilities(Transport.NFC, 67890) + .autoEjectTimeout((short) 128) + .challengeResponseTimeout((byte) 55) + .deviceFlags(98765) + .nfcRestricted(true) + .build(); + assertIntegerEquals(12345, config.getEnabledCapabilities(Transport.USB)); + assertIntegerEquals(67890, config.getEnabledCapabilities(Transport.NFC)); + assertShortEquals(128, config.getAutoEjectTimeout()); + assertByteEquals(55, config.getChallengeResponseTimeout()); + assertIntegerEquals(98765, config.getDeviceFlags()); + assertNotNull(config.getNfcRestricted()); + assertIsTrue(config.getNfcRestricted()); + } } diff --git a/management/src/test/java/com/yubico/yubikit/management/DeviceConfigTest.java b/management/src/test/java/com/yubico/yubikit/management/DeviceConfigTest.java index 71a226c4..6da66202 100644 --- a/management/src/test/java/com/yubico/yubikit/management/DeviceConfigTest.java +++ b/management/src/test/java/com/yubico/yubikit/management/DeviceConfigTest.java @@ -29,39 +29,39 @@ public class DeviceConfigTest { - @Test - public void testParseNfcRestricted() { - assertIsFalse(defaultConfig().getNfcRestricted()); - assertIsFalse(configOf(0x17, new byte[]{0x00}).getNfcRestricted()); - assertIsFalse(configOf(0x17, new byte[]{0x02}).getNfcRestricted()); - assertIsTrue(configOf(0x17, new byte[]{0x01}).getNfcRestricted()); - } + @Test + public void testParseNfcRestricted() { + assertIsFalse(defaultConfig().getNfcRestricted()); + assertIsFalse(configOf(0x17, new byte[] {0x00}).getNfcRestricted()); + assertIsFalse(configOf(0x17, new byte[] {0x02}).getNfcRestricted()); + assertIsTrue(configOf(0x17, new byte[] {0x01}).getNfcRestricted()); + } - @Test - public void testParseAutoEjectTimeout() { - assertShortEquals(0, defaultConfig().getAutoEjectTimeout()); - assertShortEquals(16384, configOf(0x06, new byte[]{0x40, 0x00}).getAutoEjectTimeout()); - assertShortEquals(-32768, configOf(0x06, new byte[]{(byte) 0x80, 0x00}).getAutoEjectTimeout()); - } + @Test + public void testParseAutoEjectTimeout() { + assertShortEquals(0, defaultConfig().getAutoEjectTimeout()); + assertShortEquals(16384, configOf(0x06, new byte[] {0x40, 0x00}).getAutoEjectTimeout()); + assertShortEquals(-32768, configOf(0x06, new byte[] {(byte) 0x80, 0x00}).getAutoEjectTimeout()); + } - @Test - public void testParseChallengeResponseTimeout() { - assertByteEquals(0, defaultConfig().getChallengeResponseTimeout()); - assertByteEquals(50, configOf(0x07, new byte[]{0x32}).getChallengeResponseTimeout()); - assertByteEquals(-128, configOf(0x07, new byte[]{(byte) 0x80}).getChallengeResponseTimeout()); - } + @Test + public void testParseChallengeResponseTimeout() { + assertByteEquals(0, defaultConfig().getChallengeResponseTimeout()); + assertByteEquals(50, configOf(0x07, new byte[] {0x32}).getChallengeResponseTimeout()); + assertByteEquals(-128, configOf(0x07, new byte[] {(byte) 0x80}).getChallengeResponseTimeout()); + } - @Test - public void testParseDeviceFlags() { - assertIntegerEquals(0, defaultConfig().getDeviceFlags()); - assertIntegerEquals(987654, configOf(0x08, new byte[]{0x0F, 0x12, 0x06}).getDeviceFlags()); - } + @Test + public void testParseDeviceFlags() { + assertIntegerEquals(0, defaultConfig().getDeviceFlags()); + assertIntegerEquals(987654, configOf(0x08, new byte[] {0x0F, 0x12, 0x06}).getDeviceFlags()); + } - private DeviceConfig defaultConfig() { - return DeviceInfo.parseTlvs(emptyTlvs(), defaultVersion).getConfig(); - } + private DeviceConfig defaultConfig() { + return DeviceInfo.parseTlvs(emptyTlvs(), defaultVersion).getConfig(); + } - private DeviceConfig configOf(int tag, byte[] data) { - return DeviceInfo.parseTlvs(tlvs(tag, data), defaultVersion).getConfig(); - } + private DeviceConfig configOf(int tag, byte[] data) { + return DeviceInfo.parseTlvs(tlvs(tag, data), defaultVersion).getConfig(); + } } diff --git a/management/src/test/java/com/yubico/yubikit/management/DeviceInfoBuilderTest.java b/management/src/test/java/com/yubico/yubikit/management/DeviceInfoBuilderTest.java index d3b9964c..70561df2 100755 --- a/management/src/test/java/com/yubico/yubikit/management/DeviceInfoBuilderTest.java +++ b/management/src/test/java/com/yubico/yubikit/management/DeviceInfoBuilderTest.java @@ -23,108 +23,108 @@ import com.yubico.yubikit.core.Transport; import com.yubico.yubikit.core.Version; - -import org.junit.Test; - import java.util.HashMap; import java.util.Map; +import org.junit.Test; public class DeviceInfoBuilderTest { - @Test - public void testDefaults() { - assertEquals(defaultConfig(), defaultInfo().getConfig()); - assertNull(defaultInfo().getSerialNumber()); - assertEquals(new Version(0, 0, 0), defaultInfo().getVersion()); - assertEquals(FormFactor.UNKNOWN, defaultInfo().getFormFactor()); - assertEquals(0, defaultInfo().getSupportedCapabilities(Transport.USB)); - assertEquals(0, defaultInfo().getSupportedCapabilities(Transport.NFC)); - assertFalse(defaultInfo().isLocked()); - assertFalse(defaultInfo().isFips()); - assertFalse(defaultInfo().isSky()); - assertFalse(defaultInfo().getPinComplexity()); - assertFalse(defaultInfo().hasTransport(Transport.USB)); - assertFalse(defaultInfo().hasTransport(Transport.NFC)); - } - - @Test - public void testConstruction() { - Map supportedCapabilities = new HashMap<>(); - supportedCapabilities.put(Transport.USB, 123); - supportedCapabilities.put(Transport.NFC, 456); - DeviceInfo deviceInfo = new DeviceInfo.Builder() - .config(defaultConfig()) - .serialNumber(987654321) - .version(new Version(3, 1, 1)) - .formFactor(FormFactor.USB_A_KEYCHAIN) - .supportedCapabilities(supportedCapabilities) - .isLocked(true) - .isFips(true) - .isSky(true) - .pinComplexity(true) - .build(); - assertEquals(defaultConfig(), deviceInfo.getConfig()); - assertEquals(Integer.valueOf(987654321), deviceInfo.getSerialNumber()); - assertEquals(new Version(3, 1, 1), deviceInfo.getVersion()); - assertEquals(FormFactor.USB_A_KEYCHAIN, deviceInfo.getFormFactor()); - assertEquals(123, deviceInfo.getSupportedCapabilities(Transport.USB)); - assertEquals(456, deviceInfo.getSupportedCapabilities(Transport.NFC)); - assertTrue(deviceInfo.isLocked()); - assertTrue(deviceInfo.isFips()); - assertTrue(deviceInfo.isSky()); - assertTrue(deviceInfo.getPinComplexity()); - assertTrue(deviceInfo.hasTransport(Transport.USB)); - assertTrue(deviceInfo.hasTransport(Transport.NFC)); - } - - @Test - public void testPartNumber() { - assertEquals("", defaultInfo().getPartNumber()); - assertEquals("", new DeviceInfo.Builder().partNumber("").build().getPartNumber()); - assertEquals("0123456789ABCDEF", new DeviceInfo.Builder() - .partNumber("0123456789ABCDEF").build().getPartNumber()); - } - - @Test - public void testFipsCapable() { - assertEquals(0, defaultInfo().getFipsCapable()); - DeviceInfo deviceInfo = new DeviceInfo.Builder().fipsCapable(145).build(); - assertEquals(145, deviceInfo.getFipsCapable()); - } - - @Test - public void testFipsApproved() { - assertEquals(0, defaultInfo().getFipsApproved()); - DeviceInfo deviceInfo = new DeviceInfo.Builder().fipsApproved(43445).build(); - assertEquals(43445, deviceInfo.getFipsApproved()); - } - - @Test - public void testResetBlocked() { - assertEquals(0, defaultInfo().getResetBlocked()); - DeviceInfo deviceInfo = new DeviceInfo.Builder().resetBlocked(874344).build(); - assertEquals(874344, deviceInfo.getResetBlocked()); - } - - @Test - public void testFpsVersion() { - assertNull(defaultInfo().getFpsVersion()); - DeviceInfo deviceInfo = new DeviceInfo.Builder().fpsVersion(new Version(5, 4, 3)).build(); - assertEquals(new Version(5, 4, 3), deviceInfo.getFpsVersion()); - } - - @Test - public void testStmVersion() { - assertNull(defaultInfo().getStmVersion()); - DeviceInfo deviceInfo = new DeviceInfo.Builder().stmVersion(new Version(5, 6, 2)).build(); - assertEquals(new Version(5, 6, 2), deviceInfo.getStmVersion()); - } - - private DeviceInfo defaultInfo() { - return new DeviceInfo.Builder().build(); - } - - private DeviceConfig defaultConfig() { - return new DeviceConfig.Builder().build(); - } + @Test + public void testDefaults() { + assertEquals(defaultConfig(), defaultInfo().getConfig()); + assertNull(defaultInfo().getSerialNumber()); + assertEquals(new Version(0, 0, 0), defaultInfo().getVersion()); + assertEquals(FormFactor.UNKNOWN, defaultInfo().getFormFactor()); + assertEquals(0, defaultInfo().getSupportedCapabilities(Transport.USB)); + assertEquals(0, defaultInfo().getSupportedCapabilities(Transport.NFC)); + assertFalse(defaultInfo().isLocked()); + assertFalse(defaultInfo().isFips()); + assertFalse(defaultInfo().isSky()); + assertFalse(defaultInfo().getPinComplexity()); + assertFalse(defaultInfo().hasTransport(Transport.USB)); + assertFalse(defaultInfo().hasTransport(Transport.NFC)); + } + + @Test + public void testConstruction() { + Map supportedCapabilities = new HashMap<>(); + supportedCapabilities.put(Transport.USB, 123); + supportedCapabilities.put(Transport.NFC, 456); + DeviceInfo deviceInfo = + new DeviceInfo.Builder() + .config(defaultConfig()) + .serialNumber(987654321) + .version(new Version(3, 1, 1)) + .formFactor(FormFactor.USB_A_KEYCHAIN) + .supportedCapabilities(supportedCapabilities) + .isLocked(true) + .isFips(true) + .isSky(true) + .pinComplexity(true) + .build(); + assertEquals(defaultConfig(), deviceInfo.getConfig()); + assertEquals(Integer.valueOf(987654321), deviceInfo.getSerialNumber()); + assertEquals(new Version(3, 1, 1), deviceInfo.getVersion()); + assertEquals(FormFactor.USB_A_KEYCHAIN, deviceInfo.getFormFactor()); + assertEquals(123, deviceInfo.getSupportedCapabilities(Transport.USB)); + assertEquals(456, deviceInfo.getSupportedCapabilities(Transport.NFC)); + assertTrue(deviceInfo.isLocked()); + assertTrue(deviceInfo.isFips()); + assertTrue(deviceInfo.isSky()); + assertTrue(deviceInfo.getPinComplexity()); + assertTrue(deviceInfo.hasTransport(Transport.USB)); + assertTrue(deviceInfo.hasTransport(Transport.NFC)); + } + + @Test + public void testPartNumber() { + assertEquals("", defaultInfo().getPartNumber()); + assertEquals("", new DeviceInfo.Builder().partNumber("").build().getPartNumber()); + assertEquals( + "0123456789ABCDEF", + new DeviceInfo.Builder().partNumber("0123456789ABCDEF").build().getPartNumber()); + } + + @Test + public void testFipsCapable() { + assertEquals(0, defaultInfo().getFipsCapable()); + DeviceInfo deviceInfo = new DeviceInfo.Builder().fipsCapable(145).build(); + assertEquals(145, deviceInfo.getFipsCapable()); + } + + @Test + public void testFipsApproved() { + assertEquals(0, defaultInfo().getFipsApproved()); + DeviceInfo deviceInfo = new DeviceInfo.Builder().fipsApproved(43445).build(); + assertEquals(43445, deviceInfo.getFipsApproved()); + } + + @Test + public void testResetBlocked() { + assertEquals(0, defaultInfo().getResetBlocked()); + DeviceInfo deviceInfo = new DeviceInfo.Builder().resetBlocked(874344).build(); + assertEquals(874344, deviceInfo.getResetBlocked()); + } + + @Test + public void testFpsVersion() { + assertNull(defaultInfo().getFpsVersion()); + DeviceInfo deviceInfo = new DeviceInfo.Builder().fpsVersion(new Version(5, 4, 3)).build(); + assertEquals(new Version(5, 4, 3), deviceInfo.getFpsVersion()); + } + + @Test + public void testStmVersion() { + assertNull(defaultInfo().getStmVersion()); + DeviceInfo deviceInfo = new DeviceInfo.Builder().stmVersion(new Version(5, 6, 2)).build(); + assertEquals(new Version(5, 6, 2), deviceInfo.getStmVersion()); + } + + private DeviceInfo defaultInfo() { + return new DeviceInfo.Builder().build(); + } + + private DeviceConfig defaultConfig() { + return new DeviceConfig.Builder().build(); + } } diff --git a/management/src/test/java/com/yubico/yubikit/management/DeviceInfoTest.java b/management/src/test/java/com/yubico/yubikit/management/DeviceInfoTest.java index 2377cb3b..191681c2 100644 --- a/management/src/test/java/com/yubico/yubikit/management/DeviceInfoTest.java +++ b/management/src/test/java/com/yubico/yubikit/management/DeviceInfoTest.java @@ -31,150 +31,159 @@ import static org.junit.Assert.assertTrue; import com.yubico.yubikit.core.Version; - import org.junit.Test; public class DeviceInfoTest { - @Test - public void testParseSerialNumber() { - assertNull(defaultInfo().getSerialNumber()); - assertEquals(Integer.valueOf(123456789), infoOf(0x02, fromHex("075BCD15")).getSerialNumber()); - } - - @Test - public void testParseVersion() { - assertEquals(new Version(5, 3, 4), infoOf(0x05, fromHex("050304")).getVersion()); - } - - @Test - public void testUseDefaultVersion() { - assertEquals(defaultVersion, defaultInfo().getVersion()); - } - - @Test - public void testParseFormFactor() { - assertEquals(FormFactor.UNKNOWN, defaultInfo().getFormFactor()); - assertEquals(FormFactor.USB_A_KEYCHAIN, infoOf(0x04, fromHex("01")).getFormFactor()); - assertEquals(FormFactor.USB_A_NANO, infoOf(0x04, fromHex("02")).getFormFactor()); - assertEquals(FormFactor.USB_C_KEYCHAIN, infoOf(0x04, fromHex("03")).getFormFactor()); - assertEquals(FormFactor.USB_C_NANO, infoOf(0x04, fromHex("04")).getFormFactor()); - assertEquals(FormFactor.USB_C_LIGHTNING, infoOf(0x04, fromHex("05")).getFormFactor()); - assertEquals(FormFactor.USB_A_BIO, infoOf(0x04, fromHex("06")).getFormFactor()); - assertEquals(FormFactor.USB_C_BIO, infoOf(0x04, fromHex("07")).getFormFactor()); - // the form factor byte contains fips (0x80) and sky (0x40) flags - assertEquals(FormFactor.USB_A_BIO, infoOf(0x04, fromHex("46")).getFormFactor()); - assertEquals(FormFactor.USB_C_NANO, infoOf(0x04, fromHex("84")).getFormFactor()); - } - - @Test - public void testParseLocked() { - assertFalse(defaultInfo().isLocked()); - assertTrue(infoOf(0x0a, fromHex("01")).isLocked()); - assertFalse(infoOf(0x0a, fromHex("00")).isLocked()); - } - - @Test - public void testParseFips() { - assertFalse(defaultInfo().isFips()); - assertTrue(infoOf(0x04, fromHex("80")).isFips()); - assertTrue(infoOf(0x04, fromHex("C0")).isFips()); - assertFalse(infoOf(0x04, fromHex("40")).isFips()); - } - - @Test - public void testParseSky() { - assertFalse(defaultInfo().isSky()); - assertTrue(infoOf(0x04, fromHex("40")).isSky()); - assertTrue(infoOf(0x04, fromHex("C0")).isSky()); - assertFalse(infoOf(0x04, fromHex("80")).isSky()); - } - - @Test - public void testParsePartNumber() { - // valid UTF-8 - assertNull(defaultInfo().getPartNumber()); - assertEquals("", infoOf(0x13, new byte[0]).getPartNumber()); - assertEquals("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_=+-", - infoOf(0x13, fromHex("6162636465666768696A6B6C6D6E6F707172737475767778797A41" + - "42434445464748494A4B4C4D4E4F505152535455565758595A303132333435363738395" + - "F3D2B2D")).getPartNumber()); - assertEquals("ÖÄÅöäåěščřžýáíúůĚŠČŘŽÝÁÍÚŮ", - infoOf(0x13, fromHex("C396C384C385C3B6C3A4C3A5C49BC5A1C48DC599C5BEC3BDC3A1C3" + - "ADC3BAC5AFC49AC5A0C48CC598C5BDC39DC381C38DC39AC5AE")).getPartNumber()); - assertEquals("😀", infoOf(0x13, fromHex("F09F9880")).getPartNumber()); - assertEquals("0123456789ABCDEF", - infoOf(0x13, fromHex("30313233343536373839414243444546")).getPartNumber()); - - // invalid UTF-8 - assertNull(infoOf(0x13, fromHex("c328")).getPartNumber()); - assertNull(infoOf(0x13, fromHex("a0a1")).getPartNumber()); - assertNull(infoOf(0x13, fromHex("e228a1")).getPartNumber()); - assertNull(infoOf(0x13, fromHex("e28228")).getPartNumber()); - assertNull(infoOf(0x13, fromHex("f0288cbc")).getPartNumber()); - assertNull(infoOf(0x13, fromHex("f09028bc")).getPartNumber()); - assertNull(infoOf(0x13, fromHex("f0288c28")).getPartNumber()); - } - - @Test - public void testParseFipsCapable() { - assertEquals(0, defaultInfo().getFipsCapable()); - assertEquals(FIDO2.bit, infoOf(0x14, fromHex("0001")).getFipsCapable()); - assertEquals(PIV.bit, infoOf(0x14, fromHex("0002")).getFipsCapable()); - assertEquals(OPENPGP.bit, infoOf(0x14, fromHex("0004")).getFipsCapable()); - assertEquals(OATH.bit, infoOf(0x14, fromHex("0008")).getFipsCapable()); - assertEquals(HSMAUTH.bit, infoOf(0x14, fromHex("0010")).getFipsCapable()); - assertEquals(PIV.bit | OATH.bit, infoOf(0x14, fromHex("000A")).getFipsCapable()); - } - - @Test - public void testParseFipsApproved() { - assertEquals(0, defaultInfo().getFipsApproved()); - assertEquals(FIDO2.bit, infoOf(0x15, fromHex("0001")).getFipsApproved()); - assertEquals(PIV.bit, infoOf(0x15, fromHex("0002")).getFipsApproved()); - assertEquals(OPENPGP.bit, infoOf(0x15, fromHex("0004")).getFipsApproved()); - assertEquals(OATH.bit, infoOf(0x15, fromHex("0008")).getFipsApproved()); - assertEquals(HSMAUTH.bit, infoOf(0x15, fromHex("0010")).getFipsApproved()); - assertEquals(PIV.bit | OATH.bit, infoOf(0x15, fromHex("000A")).getFipsApproved()); - } - - @Test - public void testParsePinComplexity() { - assertFalse(defaultInfo().getPinComplexity()); - assertFalse(infoOf(0x16, fromHex("00")).getPinComplexity()); - assertTrue(infoOf(0x16, fromHex("01")).getPinComplexity()); - } - - @Test - public void testParseResetBlocked() { - assertEquals(0, defaultInfo().getResetBlocked()); - assertEquals(1056, infoOf(0x18, fromHex("0420")).getResetBlocked()); - } - - @Test - public void testParseFpsVersion() { - assertNull(defaultInfo().getFpsVersion()); - assertNull(infoOf(0x20, fromHex("000000")).getFpsVersion()); - assertNull(infoOf(0x20, fromHex("000000000000000000")).getFpsVersion()); - assertEquals(new Version(0,0,1), infoOf(0x20, fromHex("000001")).getFpsVersion()); - assertEquals(new Version(5, 6, 6), infoOf(0x20, fromHex("050606")).getFpsVersion()); - } - - @Test - public void testParseStmVersion() { - assertNull(defaultInfo().getStmVersion()); - assertNull(infoOf(0x21, fromHex("000000")).getStmVersion()); - assertNull(infoOf(0x21, fromHex("000000000000000000")).getStmVersion()); - assertEquals(new Version(0,0,1), infoOf(0x21, fromHex("000001")).getStmVersion()); - assertEquals(new Version(7, 0, 5), infoOf(0x21, fromHex("070005")).getStmVersion()); - } - - private DeviceInfo defaultInfo() { - return DeviceInfo.parseTlvs(emptyTlvs(), defaultVersion); - } - - private DeviceInfo infoOf(int tag, byte[] data) { - return DeviceInfo.parseTlvs(tlvs(tag, data), defaultVersion); - } + @Test + public void testParseSerialNumber() { + assertNull(defaultInfo().getSerialNumber()); + assertEquals(Integer.valueOf(123456789), infoOf(0x02, fromHex("075BCD15")).getSerialNumber()); + } + + @Test + public void testParseVersion() { + assertEquals(new Version(5, 3, 4), infoOf(0x05, fromHex("050304")).getVersion()); + } + + @Test + public void testUseDefaultVersion() { + assertEquals(defaultVersion, defaultInfo().getVersion()); + } + + @Test + public void testParseFormFactor() { + assertEquals(FormFactor.UNKNOWN, defaultInfo().getFormFactor()); + assertEquals(FormFactor.USB_A_KEYCHAIN, infoOf(0x04, fromHex("01")).getFormFactor()); + assertEquals(FormFactor.USB_A_NANO, infoOf(0x04, fromHex("02")).getFormFactor()); + assertEquals(FormFactor.USB_C_KEYCHAIN, infoOf(0x04, fromHex("03")).getFormFactor()); + assertEquals(FormFactor.USB_C_NANO, infoOf(0x04, fromHex("04")).getFormFactor()); + assertEquals(FormFactor.USB_C_LIGHTNING, infoOf(0x04, fromHex("05")).getFormFactor()); + assertEquals(FormFactor.USB_A_BIO, infoOf(0x04, fromHex("06")).getFormFactor()); + assertEquals(FormFactor.USB_C_BIO, infoOf(0x04, fromHex("07")).getFormFactor()); + // the form factor byte contains fips (0x80) and sky (0x40) flags + assertEquals(FormFactor.USB_A_BIO, infoOf(0x04, fromHex("46")).getFormFactor()); + assertEquals(FormFactor.USB_C_NANO, infoOf(0x04, fromHex("84")).getFormFactor()); + } + + @Test + public void testParseLocked() { + assertFalse(defaultInfo().isLocked()); + assertTrue(infoOf(0x0a, fromHex("01")).isLocked()); + assertFalse(infoOf(0x0a, fromHex("00")).isLocked()); + } + + @Test + public void testParseFips() { + assertFalse(defaultInfo().isFips()); + assertTrue(infoOf(0x04, fromHex("80")).isFips()); + assertTrue(infoOf(0x04, fromHex("C0")).isFips()); + assertFalse(infoOf(0x04, fromHex("40")).isFips()); + } + + @Test + public void testParseSky() { + assertFalse(defaultInfo().isSky()); + assertTrue(infoOf(0x04, fromHex("40")).isSky()); + assertTrue(infoOf(0x04, fromHex("C0")).isSky()); + assertFalse(infoOf(0x04, fromHex("80")).isSky()); + } + + @Test + public void testParsePartNumber() { + // valid UTF-8 + assertNull(defaultInfo().getPartNumber()); + assertEquals("", infoOf(0x13, new byte[0]).getPartNumber()); + assertEquals( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_=+-", + infoOf( + 0x13, + fromHex( + "6162636465666768696A6B6C6D6E6F707172737475767778797A41" + + "42434445464748494A4B4C4D4E4F505152535455565758595A303132333435363738395" + + "F3D2B2D")) + .getPartNumber()); + assertEquals( + "ÖÄÅöäåěščřžýáíúůĚŠČŘŽÝÁÍÚŮ", + infoOf( + 0x13, + fromHex( + "C396C384C385C3B6C3A4C3A5C49BC5A1C48DC599C5BEC3BDC3A1C3" + + "ADC3BAC5AFC49AC5A0C48CC598C5BDC39DC381C38DC39AC5AE")) + .getPartNumber()); + assertEquals("😀", infoOf(0x13, fromHex("F09F9880")).getPartNumber()); + assertEquals( + "0123456789ABCDEF", + infoOf(0x13, fromHex("30313233343536373839414243444546")).getPartNumber()); + + // invalid UTF-8 + assertNull(infoOf(0x13, fromHex("c328")).getPartNumber()); + assertNull(infoOf(0x13, fromHex("a0a1")).getPartNumber()); + assertNull(infoOf(0x13, fromHex("e228a1")).getPartNumber()); + assertNull(infoOf(0x13, fromHex("e28228")).getPartNumber()); + assertNull(infoOf(0x13, fromHex("f0288cbc")).getPartNumber()); + assertNull(infoOf(0x13, fromHex("f09028bc")).getPartNumber()); + assertNull(infoOf(0x13, fromHex("f0288c28")).getPartNumber()); + } + + @Test + public void testParseFipsCapable() { + assertEquals(0, defaultInfo().getFipsCapable()); + assertEquals(FIDO2.bit, infoOf(0x14, fromHex("0001")).getFipsCapable()); + assertEquals(PIV.bit, infoOf(0x14, fromHex("0002")).getFipsCapable()); + assertEquals(OPENPGP.bit, infoOf(0x14, fromHex("0004")).getFipsCapable()); + assertEquals(OATH.bit, infoOf(0x14, fromHex("0008")).getFipsCapable()); + assertEquals(HSMAUTH.bit, infoOf(0x14, fromHex("0010")).getFipsCapable()); + assertEquals(PIV.bit | OATH.bit, infoOf(0x14, fromHex("000A")).getFipsCapable()); + } + + @Test + public void testParseFipsApproved() { + assertEquals(0, defaultInfo().getFipsApproved()); + assertEquals(FIDO2.bit, infoOf(0x15, fromHex("0001")).getFipsApproved()); + assertEquals(PIV.bit, infoOf(0x15, fromHex("0002")).getFipsApproved()); + assertEquals(OPENPGP.bit, infoOf(0x15, fromHex("0004")).getFipsApproved()); + assertEquals(OATH.bit, infoOf(0x15, fromHex("0008")).getFipsApproved()); + assertEquals(HSMAUTH.bit, infoOf(0x15, fromHex("0010")).getFipsApproved()); + assertEquals(PIV.bit | OATH.bit, infoOf(0x15, fromHex("000A")).getFipsApproved()); + } + + @Test + public void testParsePinComplexity() { + assertFalse(defaultInfo().getPinComplexity()); + assertFalse(infoOf(0x16, fromHex("00")).getPinComplexity()); + assertTrue(infoOf(0x16, fromHex("01")).getPinComplexity()); + } + + @Test + public void testParseResetBlocked() { + assertEquals(0, defaultInfo().getResetBlocked()); + assertEquals(1056, infoOf(0x18, fromHex("0420")).getResetBlocked()); + } + + @Test + public void testParseFpsVersion() { + assertNull(defaultInfo().getFpsVersion()); + assertNull(infoOf(0x20, fromHex("000000")).getFpsVersion()); + assertNull(infoOf(0x20, fromHex("000000000000000000")).getFpsVersion()); + assertEquals(new Version(0, 0, 1), infoOf(0x20, fromHex("000001")).getFpsVersion()); + assertEquals(new Version(5, 6, 6), infoOf(0x20, fromHex("050606")).getFpsVersion()); + } + + @Test + public void testParseStmVersion() { + assertNull(defaultInfo().getStmVersion()); + assertNull(infoOf(0x21, fromHex("000000")).getStmVersion()); + assertNull(infoOf(0x21, fromHex("000000000000000000")).getStmVersion()); + assertEquals(new Version(0, 0, 1), infoOf(0x21, fromHex("000001")).getStmVersion()); + assertEquals(new Version(7, 0, 5), infoOf(0x21, fromHex("070005")).getStmVersion()); + } + + private DeviceInfo defaultInfo() { + return DeviceInfo.parseTlvs(emptyTlvs(), defaultVersion); + } + + private DeviceInfo infoOf(int tag, byte[] data) { + return DeviceInfo.parseTlvs(tlvs(tag, data), defaultVersion); + } } - diff --git a/management/src/test/java/com/yubico/yubikit/management/TestUtil.java b/management/src/test/java/com/yubico/yubikit/management/TestUtil.java index 4046d2cb..5ab0399f 100644 --- a/management/src/test/java/com/yubico/yubikit/management/TestUtil.java +++ b/management/src/test/java/com/yubico/yubikit/management/TestUtil.java @@ -16,48 +16,46 @@ package com.yubico.yubikit.management; -import static org.junit.Assert.assertEquals; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; +import static org.junit.Assert.assertEquals; import com.yubico.yubikit.core.Version; - import java.util.HashMap; import java.util.Map; - import javax.annotation.Nullable; class TestUtil { - static final Version defaultVersion = new Version(2, 2, 2); + static final Version defaultVersion = new Version(2, 2, 2); - static Map tlvs(Integer tag, @Nullable byte[] data) { - Map tlvs = new HashMap<>(); - tlvs.put(tag, data); - return tlvs; - } + static Map tlvs(Integer tag, @Nullable byte[] data) { + Map tlvs = new HashMap<>(); + tlvs.put(tag, data); + return tlvs; + } - static Map emptyTlvs() { - return new HashMap<>(); - } + static Map emptyTlvs() { + return new HashMap<>(); + } - static void assertShortEquals(int expected, @Nullable Short value) { - assertEquals(Short.valueOf((short) expected), value); - } + static void assertShortEquals(int expected, @Nullable Short value) { + assertEquals(Short.valueOf((short) expected), value); + } - static void assertByteEquals(int expected, @Nullable Byte value) { - assertEquals(Byte.valueOf((byte) expected), value); - } + static void assertByteEquals(int expected, @Nullable Byte value) { + assertEquals(Byte.valueOf((byte) expected), value); + } - static void assertIntegerEquals(int expected, @Nullable Integer value) { - assertEquals(Integer.valueOf(expected), value); - } + static void assertIntegerEquals(int expected, @Nullable Integer value) { + assertEquals(Integer.valueOf(expected), value); + } - static void assertIsFalse(@Nullable Boolean value) { - assertEquals(FALSE, value); - } + static void assertIsFalse(@Nullable Boolean value) { + assertEquals(FALSE, value); + } - static void assertIsTrue(@Nullable Boolean value) { - assertEquals(TRUE, value); - } + static void assertIsTrue(@Nullable Boolean value) { + assertEquals(TRUE, value); + } } diff --git a/oath/src/main/java/com/yubico/yubikit/oath/AccessKey.java b/oath/src/main/java/com/yubico/yubikit/oath/AccessKey.java index de111a4b..1e546d2a 100755 --- a/oath/src/main/java/com/yubico/yubikit/oath/AccessKey.java +++ b/oath/src/main/java/com/yubico/yubikit/oath/AccessKey.java @@ -18,16 +18,16 @@ /** * Allows the implementation of custom backends to unlock an OathSession. - *

- * The AccessKey gives the OathSession the ability to unlock a session without providing the actual - * key material, which allows it to be stored in the Android KeyStore or similar. + * + *

The AccessKey gives the OathSession the ability to unlock a session without providing the + * actual key material, which allows it to be stored in the Android KeyStore or similar. */ public interface AccessKey { - /** - * Create a HMAC-SHA1 signature over the given challenge, using an OATH Access Key. - * - * @param challenge a challenge to sign - * @return a signature over the given challenge - */ - byte[] calculateResponse(byte[] challenge); -} \ No newline at end of file + /** + * Create a HMAC-SHA1 signature over the given challenge, using an OATH Access Key. + * + * @param challenge a challenge to sign + * @return a signature over the given challenge + */ + byte[] calculateResponse(byte[] challenge); +} diff --git a/oath/src/main/java/com/yubico/yubikit/oath/Base32.java b/oath/src/main/java/com/yubico/yubikit/oath/Base32.java index d2a2c4aa..5a25d107 100644 --- a/oath/src/main/java/com/yubico/yubikit/oath/Base32.java +++ b/oath/src/main/java/com/yubico/yubikit/oath/Base32.java @@ -26,129 +26,133 @@ */ @SuppressWarnings("SpellCheckingInspection") public class Base32 { - private static final char[] ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".toCharArray(); - - private static final char PADDING = '='; - - private static final String[] ENCODE_PADDING = new String[]{ - "", "======", "====", "===", "=" - }; - - public static String encode(byte[] data) { - StringBuilder buf = new StringBuilder(); - - int len = data.length; - for (int i = 0; i < len; i += 5) { - int b0 = Byte.toUnsignedInt(data[i]); - int b1 = len > i + 1 ? Byte.toUnsignedInt(data[i + 1]) : 0; - int b2 = len > i + 2 ? Byte.toUnsignedInt(data[i + 2]) : 0; - int b3 = len > i + 3 ? Byte.toUnsignedInt(data[i + 3]) : 0; - int b4 = len > i + 4 ? Byte.toUnsignedInt(data[i + 4]) : 0; - - buf.append(ALPHABET[b0 >> 3]); - buf.append(ALPHABET[b0 << 2 & 0x1c | b1 >> 6]); - if (len <= i + 1) break; - buf.append(ALPHABET[b1 >> 1 & 0x1f]); - buf.append(ALPHABET[b1 << 4 & 0x10 | b2 >> 4]); - if (len <= i + 2) break; - buf.append(ALPHABET[b2 << 1 & 0x1e | b3 >> 7]); - if (len <= i + 3) break; - buf.append(ALPHABET[b3 >> 2 & 0x1f]); - buf.append(ALPHABET[b3 << 3 & 0x1c | b4 >> 5]); - if (len <= i + 4) break; - buf.append(ALPHABET[b4 & 0x1f]); - } - - buf.append(ENCODE_PADDING[len % 5]); - return buf.toString(); + private static final char[] ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".toCharArray(); + + private static final char PADDING = '='; + + private static final String[] ENCODE_PADDING = new String[] {"", "======", "====", "===", "="}; + + public static String encode(byte[] data) { + StringBuilder buf = new StringBuilder(); + + int len = data.length; + for (int i = 0; i < len; i += 5) { + int b0 = Byte.toUnsignedInt(data[i]); + int b1 = len > i + 1 ? Byte.toUnsignedInt(data[i + 1]) : 0; + int b2 = len > i + 2 ? Byte.toUnsignedInt(data[i + 2]) : 0; + int b3 = len > i + 3 ? Byte.toUnsignedInt(data[i + 3]) : 0; + int b4 = len > i + 4 ? Byte.toUnsignedInt(data[i + 4]) : 0; + + buf.append(ALPHABET[b0 >> 3]); + buf.append(ALPHABET[b0 << 2 & 0x1c | b1 >> 6]); + if (len <= i + 1) break; + buf.append(ALPHABET[b1 >> 1 & 0x1f]); + buf.append(ALPHABET[b1 << 4 & 0x10 | b2 >> 4]); + if (len <= i + 2) break; + buf.append(ALPHABET[b2 << 1 & 0x1e | b3 >> 7]); + if (len <= i + 3) break; + buf.append(ALPHABET[b3 >> 2 & 0x1f]); + buf.append(ALPHABET[b3 << 3 & 0x1c | b4 >> 5]); + if (len <= i + 4) break; + buf.append(ALPHABET[b4 & 0x1f]); } - public static boolean isValid(String encoded) { - if (!encoded.matches("^$|^[A-Z2-7]{2,}(=*)$")) { - return false; - } - - int finalUnitLength = encoded.length() % 8; - int paddingIndex = encoded.indexOf("="); - if (paddingIndex == -1) { - // no padding; input is valid only if a correct padding can be appended - return finalUnitLength == 0 || finalUnitLength == 7 || finalUnitLength == 5 || - finalUnitLength == 4 || finalUnitLength == 2; - } - - if (finalUnitLength != 0) { - // wrong input length - return false; - } - - int paddingLength = encoded.length() - paddingIndex; - // padding can only be 1, 3, 4 or 6 characters long, otherwise this is not a valid input - return paddingLength == 1 || paddingLength == 3 || paddingLength == 4 || paddingLength == 6; + buf.append(ENCODE_PADDING[len % 5]); + return buf.toString(); + } + + public static boolean isValid(String encoded) { + if (!encoded.matches("^$|^[A-Z2-7]{2,}(=*)$")) { + return false; } - public static byte[] decode(String encoded) { - - if (!isValid(encoded)) { - throw new IllegalArgumentException("Invalid base32"); - } - - char[] padding = new char[(8 - encoded.length() % 8) % 8]; - Arrays.fill(padding, PADDING); - String b32 = encoded.concat(new String(padding)); - - int len = b32.length(); - int maxBufLength = len / 8 * 5; - byte[] buffer = new byte[maxBufLength]; - - ByteBuffer bb = ByteBuffer.wrap(buffer); - - for (int i = 0; i < b32.length(); i += 8) { - final char[] chars = b32.toCharArray(); - - // input characters - char c0 = chars[i]; - char c1 = chars[i + 1]; - char c2 = chars[i + 2]; - char c3 = chars[i + 3]; - char c4 = chars[i + 4]; - char c5 = chars[i + 5]; - char c6 = chars[i + 6]; - char c7 = chars[i + 7]; - - // input values - byte v0 = getValue(c0); - byte v1 = getValue(c1); - byte v2 = getValue(c2); - byte v3 = getValue(c3); - byte v4 = getValue(c4); - byte v5 = getValue(c5); - byte v6 = getValue(c6); - byte v7 = getValue(c7); - - // build result until padding is found - bb.put((byte) (v0 << 3 | v1 >> 2)); - if (c2 == PADDING) break; - bb.put((byte) (v1 << 6 | v2 << 1 | v3 >> 4)); - if (c3 == PADDING) break; - bb.put((byte) (v3 << 4 | v4 >> 1)); - if (c4 == PADDING) break; - bb.put((byte) (v4 << 7 | v5 << 2 | v6 >> 3)); - if (c6 == PADDING) break; - bb.put((byte) (v6 << 5 | v7)); - } - - // update result length - int resultLength = bb.position(); - byte[] result = Arrays.copyOf(buffer, (resultLength > 0 && buffer[resultLength - 1] == 0) - ? resultLength - 1 // strip last byte if 0 - : resultLength); - Arrays.fill(buffer, (byte) 0); - return result; + int finalUnitLength = encoded.length() % 8; + int paddingIndex = encoded.indexOf("="); + if (paddingIndex == -1) { + // no padding; input is valid only if a correct padding can be appended + return finalUnitLength == 0 + || finalUnitLength == 7 + || finalUnitLength == 5 + || finalUnitLength == 4 + || finalUnitLength == 2; + } + + if (finalUnitLength != 0) { + // wrong input length + return false; + } + + int paddingLength = encoded.length() - paddingIndex; + // padding can only be 1, 3, 4 or 6 characters long, otherwise this is not a valid input + return paddingLength == 1 || paddingLength == 3 || paddingLength == 4 || paddingLength == 6; + } + + public static byte[] decode(String encoded) { + + if (!isValid(encoded)) { + throw new IllegalArgumentException("Invalid base32"); } - private static byte getValue(char c) { - // compute the value - // if c is the padding character, use 0 to simplify bit operations - return c == PADDING ? 0 : (byte) ((c < 'A' ? c - '2' + 26 : c - 'A') & 0x1f); + char[] padding = new char[(8 - encoded.length() % 8) % 8]; + Arrays.fill(padding, PADDING); + String b32 = encoded.concat(new String(padding)); + + int len = b32.length(); + int maxBufLength = len / 8 * 5; + byte[] buffer = new byte[maxBufLength]; + + ByteBuffer bb = ByteBuffer.wrap(buffer); + + for (int i = 0; i < b32.length(); i += 8) { + final char[] chars = b32.toCharArray(); + + // input characters + char c0 = chars[i]; + char c1 = chars[i + 1]; + char c2 = chars[i + 2]; + char c3 = chars[i + 3]; + char c4 = chars[i + 4]; + char c5 = chars[i + 5]; + char c6 = chars[i + 6]; + char c7 = chars[i + 7]; + + // input values + byte v0 = getValue(c0); + byte v1 = getValue(c1); + byte v2 = getValue(c2); + byte v3 = getValue(c3); + byte v4 = getValue(c4); + byte v5 = getValue(c5); + byte v6 = getValue(c6); + byte v7 = getValue(c7); + + // build result until padding is found + bb.put((byte) (v0 << 3 | v1 >> 2)); + if (c2 == PADDING) break; + bb.put((byte) (v1 << 6 | v2 << 1 | v3 >> 4)); + if (c3 == PADDING) break; + bb.put((byte) (v3 << 4 | v4 >> 1)); + if (c4 == PADDING) break; + bb.put((byte) (v4 << 7 | v5 << 2 | v6 >> 3)); + if (c6 == PADDING) break; + bb.put((byte) (v6 << 5 | v7)); } + + // update result length + int resultLength = bb.position(); + byte[] result = + Arrays.copyOf( + buffer, + (resultLength > 0 && buffer[resultLength - 1] == 0) + ? resultLength - 1 // strip last byte if 0 + : resultLength); + Arrays.fill(buffer, (byte) 0); + return result; + } + + private static byte getValue(char c) { + // compute the value + // if c is the padding character, use 0 to simplify bit operations + return c == PADDING ? 0 : (byte) ((c < 'A' ? c - '2' + 26 : c - 'A') & 0x1f); + } } diff --git a/oath/src/main/java/com/yubico/yubikit/oath/Code.java b/oath/src/main/java/com/yubico/yubikit/oath/Code.java index 74dc134a..4adadf5d 100755 --- a/oath/src/main/java/com/yubico/yubikit/oath/Code.java +++ b/oath/src/main/java/com/yubico/yubikit/oath/Code.java @@ -16,39 +16,30 @@ package com.yubico.yubikit.oath; - -/** - * A one-time OATH code, calculated from a Credential stored in a YubiKey. - */ +/** A one-time OATH code, calculated from a Credential stored in a YubiKey. */ public class Code { - private final String value; - private final long validFrom; - private final long validUntil; + private final String value; + private final long validFrom; + private final long validUntil; - public Code(String value, long validFrom, long validUntil) { - this.value = value; - this.validFrom = validFrom; - this.validUntil = validUntil; - } + public Code(String value, long validFrom, long validUntil) { + this.value = value; + this.validFrom = validFrom; + this.validUntil = validUntil; + } - /** - * Returns the String value, typically a 6-8 digit code. - */ - public final String getValue() { - return this.value; - } + /** Returns the String value, typically a 6-8 digit code. */ + public final String getValue() { + return this.value; + } - /** - * Returns a UNIX timestamp in ms for when the validity period starts. - */ - public final long getValidFrom() { - return this.validFrom; - } + /** Returns a UNIX timestamp in ms for when the validity period starts. */ + public final long getValidFrom() { + return this.validFrom; + } - /** - * Returns a UNIX timestamp in ms for when the validity period ends. - */ - public final long getValidUntil() { - return this.validUntil; - } + /** Returns a UNIX timestamp in ms for when the validity period ends. */ + public final long getValidUntil() { + return this.validUntil; + } } diff --git a/oath/src/main/java/com/yubico/yubikit/oath/Credential.java b/oath/src/main/java/com/yubico/yubikit/oath/Credential.java index e8b9a93f..c94b910b 100755 --- a/oath/src/main/java/com/yubico/yubikit/oath/Credential.java +++ b/oath/src/main/java/com/yubico/yubikit/oath/Credential.java @@ -19,145 +19,130 @@ import java.io.Serializable; import java.util.Arrays; import java.util.Objects; - import javax.annotation.Nullable; -/** - * A reference to an OATH Credential stored on a YubiKey. - */ +/** A reference to an OATH Credential stored on a YubiKey. */ public class Credential implements Serializable { - final String deviceId; - - private final byte[] id; - private final OathType oathType; - private final int period; - @Nullable - private final String issuer; - private final String accountName; - - private boolean touchRequired = false; - - /* - * Variation of code types: - * 0x75 - TOTP full response - * 0x76 - TOTP truncated response - * 0x77 - HOTP - * 0x7c - TOTP requires touch - */ - private static final byte TYPE_HOTP = 0x77; - private static final byte TYPE_TOUCH = 0x7c; - - /** - * Construct a Credential using response data from a LIST call. - * - * @param deviceId the Device ID of the YubiKey - * @param response the parsed response from the YubiKey - */ - Credential(String deviceId, OathSession.ListResponse response) { - this.deviceId = deviceId; - id = response.id; - oathType = response.oathType; - - CredentialIdUtils.CredentialIdData idData = CredentialIdUtils.parseId(id, oathType); - issuer = idData.issuer; - accountName = idData.accountName; - period = idData.period; - } - - /** - * Construct a Credential using response data from a CALCULATE/CALCULATE_ALL call. - * - * @param deviceId the Device ID of the YubiKey - * @param id the ID of the Credential - * @param response the parsed response from the YubiKey for the Credential. - */ - Credential(String deviceId, byte[] id, OathSession.CalculateResponse response) { - this.deviceId = deviceId; - this.id = id; - oathType = response.responseType == TYPE_HOTP ? OathType.HOTP : OathType.TOTP; - touchRequired = response.responseType == TYPE_TOUCH; - - CredentialIdUtils.CredentialIdData idData = CredentialIdUtils.parseId(id, oathType); - issuer = idData.issuer; - accountName = idData.accountName; - period = idData.period; - } - - /** - * Creates an instance of {@link Credential} from CredentialData successfully added to a YubiKey - * - * @param deviceId the Device ID of the YubiKey - * @param credentialId the ID of the Credential - * @param oathType the OATH type of the credential - * @param touchRequired whether or not the Credential requires touch - */ - Credential(String deviceId, byte[] credentialId, OathType oathType, boolean touchRequired) { - this.deviceId = deviceId; - this.id = credentialId; - CredentialIdUtils.CredentialIdData idData = CredentialIdUtils.parseId(credentialId, oathType); - this.issuer = idData.issuer; - this.accountName = idData.accountName; - this.period = idData.period; - this.oathType = oathType; - this.touchRequired = touchRequired; - } - - /** - * Returns the ID of a Credential which is used to identify it to the YubiKey. - */ - public byte[] getId() { - return Arrays.copyOf(id, id.length); - } - - /** - * Returns the OATH type (HOTP or TOTP) of the Credential. - */ - public OathType getOathType() { - return oathType; - } - - /** - * Returns the name of the Credential issuer (e.g. Google, Amazon, Facebook, etc.) - */ - @Nullable - public String getIssuer() { - return issuer; - } - - /** - * Returns the name of the account (typically a username or email address). - */ - public String getAccountName() { - return accountName; - } - - /** - * Returns the validity time period in seconds for a Code generated from this Credential. - */ - public int getPeriod() { - return period; - } - - /** - * Returns whether or not a user presence check (a physical touch on the sensor of the YubiKey) is required for calculating a Code from this Credential. - */ - public boolean isTouchRequired() { - return touchRequired; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Credential that = (Credential) o; - return deviceId.equals(that.deviceId) && - Arrays.equals(id, that.id); - } - - @Override - public int hashCode() { - int result = Objects.hash(deviceId); - result = 31 * result + Arrays.hashCode(id); - return result; - } + final String deviceId; + + private final byte[] id; + private final OathType oathType; + private final int period; + @Nullable private final String issuer; + private final String accountName; + + private boolean touchRequired = false; + + /* + * Variation of code types: + * 0x75 - TOTP full response + * 0x76 - TOTP truncated response + * 0x77 - HOTP + * 0x7c - TOTP requires touch + */ + private static final byte TYPE_HOTP = 0x77; + private static final byte TYPE_TOUCH = 0x7c; + + /** + * Construct a Credential using response data from a LIST call. + * + * @param deviceId the Device ID of the YubiKey + * @param response the parsed response from the YubiKey + */ + Credential(String deviceId, OathSession.ListResponse response) { + this.deviceId = deviceId; + id = response.id; + oathType = response.oathType; + + CredentialIdUtils.CredentialIdData idData = CredentialIdUtils.parseId(id, oathType); + issuer = idData.issuer; + accountName = idData.accountName; + period = idData.period; + } + + /** + * Construct a Credential using response data from a CALCULATE/CALCULATE_ALL call. + * + * @param deviceId the Device ID of the YubiKey + * @param id the ID of the Credential + * @param response the parsed response from the YubiKey for the Credential. + */ + Credential(String deviceId, byte[] id, OathSession.CalculateResponse response) { + this.deviceId = deviceId; + this.id = id; + oathType = response.responseType == TYPE_HOTP ? OathType.HOTP : OathType.TOTP; + touchRequired = response.responseType == TYPE_TOUCH; + + CredentialIdUtils.CredentialIdData idData = CredentialIdUtils.parseId(id, oathType); + issuer = idData.issuer; + accountName = idData.accountName; + period = idData.period; + } + + /** + * Creates an instance of {@link Credential} from CredentialData successfully added to a YubiKey + * + * @param deviceId the Device ID of the YubiKey + * @param credentialId the ID of the Credential + * @param oathType the OATH type of the credential + * @param touchRequired whether or not the Credential requires touch + */ + Credential(String deviceId, byte[] credentialId, OathType oathType, boolean touchRequired) { + this.deviceId = deviceId; + this.id = credentialId; + CredentialIdUtils.CredentialIdData idData = CredentialIdUtils.parseId(credentialId, oathType); + this.issuer = idData.issuer; + this.accountName = idData.accountName; + this.period = idData.period; + this.oathType = oathType; + this.touchRequired = touchRequired; + } + + /** Returns the ID of a Credential which is used to identify it to the YubiKey. */ + public byte[] getId() { + return Arrays.copyOf(id, id.length); + } + + /** Returns the OATH type (HOTP or TOTP) of the Credential. */ + public OathType getOathType() { + return oathType; + } + + /** Returns the name of the Credential issuer (e.g. Google, Amazon, Facebook, etc.) */ + @Nullable public String getIssuer() { + return issuer; + } + + /** Returns the name of the account (typically a username or email address). */ + public String getAccountName() { + return accountName; + } + + /** Returns the validity time period in seconds for a Code generated from this Credential. */ + public int getPeriod() { + return period; + } + + /** + * Returns whether or not a user presence check (a physical touch on the sensor of the YubiKey) is + * required for calculating a Code from this Credential. + */ + public boolean isTouchRequired() { + return touchRequired; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Credential that = (Credential) o; + return deviceId.equals(that.deviceId) && Arrays.equals(id, that.id); + } + + @Override + public int hashCode() { + int result = Objects.hash(deviceId); + result = 31 * result + Arrays.hashCode(id); + return result; + } } diff --git a/oath/src/main/java/com/yubico/yubikit/oath/CredentialData.java b/oath/src/main/java/com/yubico/yubikit/oath/CredentialData.java index e2b6756a..13b4f4d8 100755 --- a/oath/src/main/java/com/yubico/yubikit/oath/CredentialData.java +++ b/oath/src/main/java/com/yubico/yubikit/oath/CredentialData.java @@ -17,282 +17,281 @@ package com.yubico.yubikit.oath; import com.yubico.yubikit.core.util.Pair; - import java.io.Serializable; import java.net.URI; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; -/** - * Data object holding all required information to add a new {@link Credential} to a YubiKey. - */ +/** Data object holding all required information to add a new {@link Credential} to a YubiKey. */ public class CredentialData implements Serializable { - /** - * The default time period for TOTP Credentials. - */ - public static final int DEFAULT_TOTP_PERIOD = 30; - /** - * The default number of digits for calculated {@link Code}s. - */ - public static final int DEFAULT_DIGITS = 6; - - private static final int DEFAULT_HOTP_COUNTER = 0; - - private final int period; - private final OathType oathType; - private final HashAlgorithm hashAlgorithm; - private final byte[] secret; - private final int counter; - private final int digits; - - // User-modifiable fields - @Nullable - private final String issuer; - private final String accountName; - - /** - * Parses an otpauth:// URI. - *

- * Example URI:

otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
- * - * @param uri the otpauth:// URI to parse - * @throws ParseUriException if the URI format is invalid - */ - @SuppressWarnings("SpellCheckingInspection") - public static CredentialData parseUri(URI uri) throws ParseUriException { - if (!"otpauth".equals(uri.getScheme())) { - throw new ParseUriException("Uri scheme must be otpauth://"); - } - - String path = uri.getPath(); - if (path.isEmpty()) { - throw new ParseUriException("Path must contain name"); - } - - Map params = new HashMap<>(); - for (String line : uri.getQuery().split("&")) { - String[] parts = line.split("=", 2); - params.put(parts[0], parts[1]); - } - - Pair nameAndIssuer = parseNameAndIssuer(path, params.get("issuer")); - - OathType oathType; - try { - oathType = OathType.fromString(uri.getHost()); - } catch (IllegalArgumentException e) { - throw new ParseUriException("Invalid or missing OATH algorithm"); - } - - HashAlgorithm hashAlgorithm; - String algorithmName = params.get("algorithm"); - if (algorithmName == null) { - hashAlgorithm = HashAlgorithm.SHA1; - } else { - try { - hashAlgorithm = HashAlgorithm.fromString(algorithmName); - } catch (IllegalArgumentException e) { - throw new ParseUriException("Invalid HMAC algorithm"); - } - } - - - byte[] secret = decodeSecret(params.get("secret")); - - int digits = getIntParam(params, "digits", DEFAULT_DIGITS); - if (digits < 6 || digits > 8) { - throw new ParseUriException("digits must be in range 6-8"); - } - - int period = getIntParam(params, "period", DEFAULT_TOTP_PERIOD); - int counter = getIntParam(params, "counter", DEFAULT_HOTP_COUNTER); - - return new CredentialData(nameAndIssuer.first, oathType, hashAlgorithm, secret, digits, period, counter, nameAndIssuer.second); - } - - /** - * Constructs a new instance from the given parameters. - * - * @param accountName the name/label of the account, typically a username or email address - * @param oathType the OATH type of the credential (TOTP or HOTP) - * @param hashAlgorithm the hash algorithm used by the credential (SHA1, SHA265 or SHA 512) - * @param secret the secret key of the credential, in raw bytes (not Base32 encoded) - * @param digits the number of digits to display for generated {@link Code}s - * @param period the validity period of generated {@link Code}s, in seconds, for a TOTP credential - * @param counter the initial counter value (initial moving factor) for a HOTP credential (typically this should be 0) - * @param issuer the name of the credential issuer (e.g. Google, Amazon, Facebook, etc.) - */ - public CredentialData(String accountName, OathType oathType, HashAlgorithm hashAlgorithm, byte[] secret, int digits, int period, int counter, @Nullable String issuer) { - this.accountName = accountName; - this.oathType = oathType; - this.hashAlgorithm = hashAlgorithm; - this.secret = Arrays.copyOf(secret, secret.length); - this.digits = digits; - this.period = period; - this.counter = counter; - this.issuer = issuer; - } - - /** - * Returns the credentials ID, as used to identify it on a YubiKey. - *

- * The Credential ID is calculated based on the combination of the issuer, the name, and (for - * TOTP credentials) the validity period. - */ - public byte[] getId() { - return CredentialIdUtils.formatId(issuer, accountName, oathType, period); + /** The default time period for TOTP Credentials. */ + public static final int DEFAULT_TOTP_PERIOD = 30; + + /** The default number of digits for calculated {@link Code}s. */ + public static final int DEFAULT_DIGITS = 6; + + private static final int DEFAULT_HOTP_COUNTER = 0; + + private final int period; + private final OathType oathType; + private final HashAlgorithm hashAlgorithm; + private final byte[] secret; + private final int counter; + private final int digits; + + // User-modifiable fields + @Nullable private final String issuer; + private final String accountName; + + /** + * Parses an otpauth:// URI. + * + *

Example URI: + * + *

otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
+ * + * @param uri the otpauth:// URI to parse + * @throws ParseUriException if the URI format is invalid + */ + @SuppressWarnings("SpellCheckingInspection") + public static CredentialData parseUri(URI uri) throws ParseUriException { + if (!"otpauth".equals(uri.getScheme())) { + throw new ParseUriException("Uri scheme must be otpauth://"); } - /** - * Returns the name of the credential. - */ - public String getAccountName() { - return accountName; + String path = uri.getPath(); + if (path.isEmpty()) { + throw new ParseUriException("Path must contain name"); } - /** - * Returns the OATH type (HOTP or TOTP) of the credential. - */ - public OathType getOathType() { - return oathType; + Map params = new HashMap<>(); + for (String line : uri.getQuery().split("&")) { + String[] parts = line.split("=", 2); + params.put(parts[0], parts[1]); } - /** - * Returns the hash algorithm used by the credential. - */ - public HashAlgorithm getHashAlgorithm() { - return hashAlgorithm; - } + Pair nameAndIssuer = parseNameAndIssuer(path, params.get("issuer")); - /** - * Returns the credential secret. - */ - public byte[] getSecret() { - return Arrays.copyOf(secret, secret.length); + OathType oathType; + try { + oathType = OathType.fromString(uri.getHost()); + } catch (IllegalArgumentException e) { + throw new ParseUriException("Invalid or missing OATH algorithm"); } - /** - * Returns the name of the credential issuer. - */ - @Nullable - public String getIssuer() { - return issuer; + HashAlgorithm hashAlgorithm; + String algorithmName = params.get("algorithm"); + if (algorithmName == null) { + hashAlgorithm = HashAlgorithm.SHA1; + } else { + try { + hashAlgorithm = HashAlgorithm.fromString(algorithmName); + } catch (IllegalArgumentException e) { + throw new ParseUriException("Invalid HMAC algorithm"); + } } - /** - * Returns the number of digits in {@link Code}s calculated from the credential. - * - * @return number of digits in code - */ - public int getDigits() { - return digits; - } + byte[] secret = decodeSecret(params.get("secret")); - /** - * Returns the validity time period in seconds for a {@link Code} generated from this credential. - */ - public int getPeriod() { - return period; + int digits = getIntParam(params, "digits", DEFAULT_DIGITS); + if (digits < 6 || digits > 8) { + throw new ParseUriException("digits must be in range 6-8"); } - /** - * Returns the initial counter value for a HOTP credential. - */ - public int getCounter() { - return counter; + int period = getIntParam(params, "period", DEFAULT_TOTP_PERIOD); + int counter = getIntParam(params, "counter", DEFAULT_HOTP_COUNTER); + + return new CredentialData( + nameAndIssuer.first, + oathType, + hashAlgorithm, + secret, + digits, + period, + counter, + nameAndIssuer.second); + } + + /** + * Constructs a new instance from the given parameters. + * + * @param accountName the name/label of the account, typically a username or email address + * @param oathType the OATH type of the credential (TOTP or HOTP) + * @param hashAlgorithm the hash algorithm used by the credential (SHA1, SHA265 or SHA 512) + * @param secret the secret key of the credential, in raw bytes (not Base32 encoded) + * @param digits the number of digits to display for generated {@link Code}s + * @param period the validity period of generated {@link Code}s, in seconds, for a TOTP credential + * @param counter the initial counter value (initial moving factor) for a HOTP credential + * (typically this should be 0) + * @param issuer the name of the credential issuer (e.g. Google, Amazon, Facebook, etc.) + */ + public CredentialData( + String accountName, + OathType oathType, + HashAlgorithm hashAlgorithm, + byte[] secret, + int digits, + int period, + int counter, + @Nullable String issuer) { + this.accountName = accountName; + this.oathType = oathType; + this.hashAlgorithm = hashAlgorithm; + this.secret = Arrays.copyOf(secret, secret.length); + this.digits = digits; + this.period = period; + this.counter = counter; + this.issuer = issuer; + } + + /** + * Returns the credentials ID, as used to identify it on a YubiKey. + * + *

The Credential ID is calculated based on the combination of the issuer, the name, and (for + * TOTP credentials) the validity period. + */ + public byte[] getId() { + return CredentialIdUtils.formatId(issuer, accountName, oathType, period); + } + + /** Returns the name of the credential. */ + public String getAccountName() { + return accountName; + } + + /** Returns the OATH type (HOTP or TOTP) of the credential. */ + public OathType getOathType() { + return oathType; + } + + /** Returns the hash algorithm used by the credential. */ + public HashAlgorithm getHashAlgorithm() { + return hashAlgorithm; + } + + /** Returns the credential secret. */ + public byte[] getSecret() { + return Arrays.copyOf(secret, secret.length); + } + + /** Returns the name of the credential issuer. */ + @Nullable public String getIssuer() { + return issuer; + } + + /** + * Returns the number of digits in {@link Code}s calculated from the credential. + * + * @return number of digits in code + */ + public int getDigits() { + return digits; + } + + /** + * Returns the validity time period in seconds for a {@link Code} generated from this credential. + */ + public int getPeriod() { + return period; + } + + /** Returns the initial counter value for a HOTP credential. */ + public int getCounter() { + return counter; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CredentialData that = (CredentialData) o; + return period == that.period + && counter == that.counter + && digits == that.digits + && Objects.equals(issuer, that.issuer) + && accountName.equals(that.accountName) + && oathType == that.oathType + && hashAlgorithm == that.hashAlgorithm + && Arrays.equals(secret, that.secret); + } + + @Override + public int hashCode() { + int result = + Objects.hash(issuer, accountName, period, oathType, hashAlgorithm, counter, digits); + result = 31 * result + Arrays.hashCode(secret); + return result; + } + + /** + * Parses name and issuer from string value from an otpauth:// URI. + * + * @param string UTF-8 string from key or qr code + * @param defaultIssuer issuer name that could be obtained from another source + * @return pair of name and issuer + */ + private static Pair parseNameAndIssuer(String string, String defaultIssuer) { + if (string.startsWith("/")) { + string = string.substring(1); } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CredentialData that = (CredentialData) o; - return period == that.period && - counter == that.counter && - digits == that.digits && - Objects.equals(issuer, that.issuer) && - accountName.equals(that.accountName) && - oathType == that.oathType && - hashAlgorithm == that.hashAlgorithm && - Arrays.equals(secret, that.secret); + if (string.length() > 64) { + string = string.substring(0, 64); } - @Override - public int hashCode() { - int result = Objects.hash(issuer, accountName, period, oathType, hashAlgorithm, counter, digits); - result = 31 * result + Arrays.hashCode(secret); - return result; + String issuer; + if (string.contains(":")) { + String[] parts = string.split(":", 2); + string = parts[1]; + issuer = parts[0]; + } else { + issuer = defaultIssuer; } - - /** - * Parses name and issuer from string value from an otpauth:// URI. - * - * @param string UTF-8 string from key or qr code - * @param defaultIssuer issuer name that could be obtained from another source - * @return pair of name and issuer - */ - private static Pair parseNameAndIssuer(String string, String defaultIssuer) { - if (string.startsWith("/")) { - string = string.substring(1); - } - if (string.length() > 64) { - string = string.substring(0, 64); - } - - String issuer; - if (string.contains(":")) { - String[] parts = string.split(":", 2); - string = parts[1]; - issuer = parts[0]; - } else { - issuer = defaultIssuer; - } - - String name = string; - return new Pair<>(name, issuer); + String name = string; + return new Pair<>(name, issuer); + } + + /** + * Parse an int from a Uri query parameter. + * + * @param params Query parameter map. + * @param name query parameter name. + * @param defaultValue default value in case query parameter is omitted. + * @return the parsed value, or the default value, if missing. + * @throws ParseUriException if the value exists and is malformed. + */ + private static int getIntParam(Map params, String name, int defaultValue) + throws ParseUriException { + String value = params.get(name); + int result = defaultValue; + if (!(value == null || value.isEmpty())) { + value = value.replaceAll("\\+", ""); + try { + result = Integer.parseInt(value); + } catch (NumberFormatException ignore) { + throw new ParseUriException(name + " is not a valid integer"); + } } - - /** - * Parse an int from a Uri query parameter. - * - * @param params Query parameter map. - * @param name query parameter name. - * @param defaultValue default value in case query parameter is omitted. - * @return the parsed value, or the default value, if missing. - * @throws ParseUriException if the value exists and is malformed. - */ - private static int getIntParam(Map params, String name, int defaultValue) throws ParseUriException { - String value = params.get(name); - int result = defaultValue; - if (!(value == null || value.isEmpty())) { - value = value.replaceAll("\\+", ""); - try { - result = Integer.parseInt(value); - } catch (NumberFormatException ignore) { - throw new ParseUriException(name + " is not a valid integer"); - } - } - return result; - } - - /** - * Makes sure that secret is Base32 encoded and decodes it - * - * @param secret string that contains Base32 encoded secret - * @return decoded secret in byte array - * @throws ParseUriException in case of not proper format - */ - private static byte[] decodeSecret(String secret) throws ParseUriException { - secret = secret.toUpperCase(); - try { - return Base32.decode(secret); - } catch (IllegalArgumentException illegalArgumentException) { - throw new ParseUriException("secret must be base32 encoded"); - } + return result; + } + + /** + * Makes sure that secret is Base32 encoded and decodes it + * + * @param secret string that contains Base32 encoded secret + * @return decoded secret in byte array + * @throws ParseUriException in case of not proper format + */ + private static byte[] decodeSecret(String secret) throws ParseUriException { + secret = secret.toUpperCase(); + try { + return Base32.decode(secret); + } catch (IllegalArgumentException illegalArgumentException) { + throw new ParseUriException("secret must be base32 encoded"); } + } } diff --git a/oath/src/main/java/com/yubico/yubikit/oath/CredentialIdUtils.java b/oath/src/main/java/com/yubico/yubikit/oath/CredentialIdUtils.java index aca8bb74..0299f040 100755 --- a/oath/src/main/java/com/yubico/yubikit/oath/CredentialIdUtils.java +++ b/oath/src/main/java/com/yubico/yubikit/oath/CredentialIdUtils.java @@ -20,86 +20,81 @@ import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; - import javax.annotation.Nullable; -/** - * Internal utility class for dealing with parameters stored in YKOATH credential names. - */ +/** Internal utility class for dealing with parameters stored in YKOATH credential names. */ class CredentialIdUtils { - private static final Pattern TOTP_ID_PATTERN = Pattern.compile("^((\\d+)/)?(([^:]+):)?(.+)$"); - private static final int DEFAULT_PERIOD = CredentialData.DEFAULT_TOTP_PERIOD; + private static final Pattern TOTP_ID_PATTERN = Pattern.compile("^((\\d+)/)?(([^:]+):)?(.+)$"); + private static final int DEFAULT_PERIOD = CredentialData.DEFAULT_TOTP_PERIOD; - /** - * Format the YKOATH Credential ID to use for a credential given its parameters. - * - * @param issuer an optional issuer name for the credential - * @param name the name of the credential - * @param oathType the type of the credential - * @param period the time period of a TOTP credential (ignored for HOTP) - * @return A byte string to use with YKOATH. - */ - static byte[] formatId(@Nullable String issuer, String name, OathType oathType, int period) { - String longName = ""; - if (oathType == OathType.TOTP && period != DEFAULT_PERIOD) { - //TODO: Add period even if default if TOTP and remainder of ID starts with \d+/ - longName += String.format(Locale.ROOT, "%d/", period); - } + /** + * Format the YKOATH Credential ID to use for a credential given its parameters. + * + * @param issuer an optional issuer name for the credential + * @param name the name of the credential + * @param oathType the type of the credential + * @param period the time period of a TOTP credential (ignored for HOTP) + * @return A byte string to use with YKOATH. + */ + static byte[] formatId(@Nullable String issuer, String name, OathType oathType, int period) { + String longName = ""; + if (oathType == OathType.TOTP && period != DEFAULT_PERIOD) { + // TODO: Add period even if default if TOTP and remainder of ID starts with \d+/ + longName += String.format(Locale.ROOT, "%d/", period); + } - if (issuer != null) { - longName += String.format(Locale.ROOT, "%s:", issuer); - } - longName += name; - return longName.getBytes(StandardCharsets.UTF_8); + if (issuer != null) { + longName += String.format(Locale.ROOT, "%s:", issuer); } + longName += name; + return longName.getBytes(StandardCharsets.UTF_8); + } - /** - * Parse credential parameters from a YKOATH credential name. - * - * @param credentialId as retrieved from a YubiKey. - * @param oathType the type of the credential - * @return parsed data stored in the credential ID. - */ - static CredentialIdData parseId(byte[] credentialId, OathType oathType) { - String data = new String(credentialId, StandardCharsets.UTF_8); + /** + * Parse credential parameters from a YKOATH credential name. + * + * @param credentialId as retrieved from a YubiKey. + * @param oathType the type of the credential + * @return parsed data stored in the credential ID. + */ + static CredentialIdData parseId(byte[] credentialId, OathType oathType) { + String data = new String(credentialId, StandardCharsets.UTF_8); - if (oathType == OathType.TOTP) { - Matcher m = TOTP_ID_PATTERN.matcher(data); - if (m.matches()) { - String periodString = m.group(2); - return new CredentialIdData( - m.group(4), - m.group(5), - periodString == null ? DEFAULT_PERIOD : Integer.parseInt(periodString) - ); - } else { //Invalid id, use it directly as name. - return new CredentialIdData(null, data, DEFAULT_PERIOD); - } - } else { - String issuer; - String name; - if (data.contains(":")) { - String[] parts = data.split(":", 2); - issuer = parts[0]; - name = parts[1]; - } else { - issuer = null; - name = data; - } - return new CredentialIdData(issuer, name, 0); - } + if (oathType == OathType.TOTP) { + Matcher m = TOTP_ID_PATTERN.matcher(data); + if (m.matches()) { + String periodString = m.group(2); + return new CredentialIdData( + m.group(4), + m.group(5), + periodString == null ? DEFAULT_PERIOD : Integer.parseInt(periodString)); + } else { // Invalid id, use it directly as name. + return new CredentialIdData(null, data, DEFAULT_PERIOD); + } + } else { + String issuer; + String name; + if (data.contains(":")) { + String[] parts = data.split(":", 2); + issuer = parts[0]; + name = parts[1]; + } else { + issuer = null; + name = data; + } + return new CredentialIdData(issuer, name, 0); } + } - static class CredentialIdData { - @Nullable - final String issuer; - final String accountName; - final int period; + static class CredentialIdData { + @Nullable final String issuer; + final String accountName; + final int period; - CredentialIdData(@Nullable String issuer, String accountName, int period) { - this.issuer = issuer; - this.accountName = accountName; - this.period = period; - } + CredentialIdData(@Nullable String issuer, String accountName, int period) { + this.issuer = issuer; + this.accountName = accountName; + this.period = period; } + } } diff --git a/oath/src/main/java/com/yubico/yubikit/oath/HashAlgorithm.java b/oath/src/main/java/com/yubico/yubikit/oath/HashAlgorithm.java index dfe39419..f42ed960 100755 --- a/oath/src/main/java/com/yubico/yubikit/oath/HashAlgorithm.java +++ b/oath/src/main/java/com/yubico/yubikit/oath/HashAlgorithm.java @@ -20,60 +20,54 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -/** - * Supported hash algorithms for use with the OATH YubiKey application. - */ +/** Supported hash algorithms for use with the OATH YubiKey application. */ public enum HashAlgorithm { - SHA1((byte) 1, 64), - SHA256((byte) 2, 64), - SHA512((byte) 3, 128); + SHA1((byte) 1, 64), + SHA256((byte) 2, 64), + SHA512((byte) 3, 128); - // Pad the key to at least 14 bytes, as required by the YubiKey. - private static final int MIN_KEY_SIZE = 14; + // Pad the key to at least 14 bytes, as required by the YubiKey. + private static final int MIN_KEY_SIZE = 14; - public final byte value; - public final int blockSize; + public final byte value; + public final int blockSize; - HashAlgorithm(byte value, int blockSize) { - this.value = value; - this.blockSize = blockSize; - } + HashAlgorithm(byte value, int blockSize) { + this.value = value; + this.blockSize = blockSize; + } - byte[] prepareKey(byte[] key) { - if (key.length < MIN_KEY_SIZE) { - return ByteBuffer.allocate(MIN_KEY_SIZE).put(key).array(); - } else if (key.length > blockSize) { - try { - return MessageDigest.getInstance(name()).digest(key); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } else { - return key; - } + byte[] prepareKey(byte[] key) { + if (key.length < MIN_KEY_SIZE) { + return ByteBuffer.allocate(MIN_KEY_SIZE).put(key).array(); + } else if (key.length > blockSize) { + try { + return MessageDigest.getInstance(name()).digest(key); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } else { + return key; } + } - /** - * Returns the algorithm corresponding to the given YKOATH ALGORITHM constant. - */ - public static HashAlgorithm fromValue(byte value) { - for (HashAlgorithm type : HashAlgorithm.values()) { - if (type.value == value) { - return type; - } - } - throw new IllegalArgumentException("Not a valid HashAlgorithm"); + /** Returns the algorithm corresponding to the given YKOATH ALGORITHM constant. */ + public static HashAlgorithm fromValue(byte value) { + for (HashAlgorithm type : HashAlgorithm.values()) { + if (type.value == value) { + return type; + } } + throw new IllegalArgumentException("Not a valid HashAlgorithm"); + } - /** - * Returns the algorithm corresponding to the given name, as used in otpauth:// URIs. - */ - public static HashAlgorithm fromString(String value) { - for (HashAlgorithm type : HashAlgorithm.values()) { - if (type.name().equalsIgnoreCase(value)) { - return type; - } - } - throw new IllegalArgumentException("Not a valid HashAlgorithm"); + /** Returns the algorithm corresponding to the given name, as used in otpauth:// URIs. */ + public static HashAlgorithm fromString(String value) { + for (HashAlgorithm type : HashAlgorithm.values()) { + if (type.name().equalsIgnoreCase(value)) { + return type; + } } + throw new IllegalArgumentException("Not a valid HashAlgorithm"); + } } diff --git a/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java b/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java index 8cdc8c4f..3d033cd5 100755 --- a/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java +++ b/oath/src/main/java/com/yubico/yubikit/oath/OathSession.java @@ -16,16 +16,16 @@ package com.yubico.yubikit.oath; -import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.Version; -import com.yubico.yubikit.core.smartcard.AppId; import com.yubico.yubikit.core.application.ApplicationNotAvailableException; import com.yubico.yubikit.core.application.ApplicationSession; import com.yubico.yubikit.core.application.BadResponseException; import com.yubico.yubikit.core.application.Feature; +import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.internal.codec.Base64; import com.yubico.yubikit.core.smartcard.Apdu; import com.yubico.yubikit.core.smartcard.ApduException; +import com.yubico.yubikit.core.smartcard.AppId; import com.yubico.yubikit.core.smartcard.SW; import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.core.smartcard.SmartCardProtocol; @@ -33,9 +33,6 @@ import com.yubico.yubikit.core.util.RandomUtils; import com.yubico.yubikit.core.util.Tlv; import com.yubico.yubikit.core.util.Tlvs; - -import org.slf4j.LoggerFactory; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; @@ -50,746 +47,779 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; - import javax.annotation.Nullable; import javax.crypto.Mac; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; +import org.slf4j.LoggerFactory; /** * Communicates with the OATH application on a YubiKey. - *

- * Protocol specification. + * + *

Protocol specification. * This application may optionally have an Access Key set, in which case most commands will be * locked until {@link #unlock} has been invoked. Note that {@link #reset()} can always be called, * regardless of if an Access Key is set or not. */ public class OathSession extends ApplicationSession { - // Features - /** - * Support for credentials that require touch to use. - */ - public static final Feature FEATURE_TOUCH = new Feature.Versioned<>("Touch", 4, 2, 0); - /** - * Support for credentials using the SHA-512 hash algorithm. - */ - public static final Feature FEATURE_SHA512 = new Feature.Versioned<>("SHA-512", 4, 3, 1); - /** - * Support for renaming a stored credential. - */ - public static final Feature FEATURE_RENAME = new Feature.Versioned<>("Rename Credential", 5, 3, 0); - - /** - * Support for secure messaging. - */ - public static final Feature FEATURE_SCP = new Feature.Versioned<>("SCP", 5, 6, 3); - - // Tlv tags YKOATH data - private static final int TAG_NAME = 0x71; - private static final int TAG_KEY = 0x73; - private static final int TAG_RESPONSE = 0x75; - private static final int TAG_PROPERTY = 0x78; - private static final int TAG_IMF = 0x7a; - private static final int TAG_CHALLENGE = 0x74; - private static final int TAG_VERSION = 0x79; - - // Instruction bytes for APDU commands - private static final byte INS_LIST = (byte) 0xa1; - private static final byte INS_PUT = 0x01; - private static final byte INS_DELETE = 0x02; - private static final byte INS_SET_CODE = 0x03; - private static final byte INS_RESET = 0x04; - private static final byte INS_RENAME = 0x05; - private static final byte INS_CALCULATE = (byte) 0xa2; - private static final byte INS_VALIDATE = (byte) 0xa3; - private static final byte INS_CALCULATE_ALL = (byte) 0xa4; - private static final byte INS_SEND_REMAINING = (byte) 0xa5; - - private static final byte PROPERTY_REQUIRE_TOUCH = (byte) 0x02; - - private static final long MILLS_IN_SECOND = 1000; - private static final int DEFAULT_TOTP_PERIOD = 30; - private static final int CHALLENGE_LEN = 8; - private static final int ACCESS_KEY_LEN = 16; - - private final SmartCardProtocol protocol; - private final Version version; - @Nullable - private final ScpKeyParams scpKeyParams; - - private String deviceId; - private byte[] salt; - @Nullable - private byte[] challenge; - private boolean isAccessKeySet; - - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(OathSession.class); - - /** - * Establishes a new session with a YubiKeys OATH application. - * - * @param connection to the YubiKey - * @throws IOException in case of connection error - * @throws ApplicationNotAvailableException if the application is missing or disabled - */ - public OathSession(SmartCardConnection connection) throws IOException, ApplicationNotAvailableException { - this(connection, null); - } - - /** - * Establishes a new session with a YubiKeys OATH application. - * - * @param connection to the YubiKey - * @param scpKeyParams SCP key parameters to establish a secure connection - * @throws IOException in case of connection error - * @throws ApplicationNotAvailableException if the application is missing or disabled - */ - public OathSession(SmartCardConnection connection, @Nullable ScpKeyParams scpKeyParams) throws IOException, ApplicationNotAvailableException { - protocol = new SmartCardProtocol(connection, INS_SEND_REMAINING); - SelectResponse selectResponse = new SelectResponse(protocol.select(AppId.OATH)); - this.scpKeyParams = scpKeyParams; - version = selectResponse.version; - deviceId = selectResponse.getDeviceId(); - salt = selectResponse.salt; - challenge = selectResponse.challenge; - isAccessKeySet = challenge != null && challenge.length != 0; - protocol.configure(version); - if (scpKeyParams != null) { - require(FEATURE_SCP); - try { - protocol.initScp(scpKeyParams); - } catch (ApduException | BadResponseException e) { - throw new IOException("Failed setting up SCP session", e); - } - } - Logger.debug(logger, "OATH session initialized (version={}, isAccessKeySet={})", version, isAccessKeySet); - } - - @Override - public void close() throws IOException { - protocol.close(); - } - - @Override - public Version getVersion() { - return version; - } - - /** - * Returns a unique ID which can be used to identify a particular YubiKey. - *

- * This ID is randomly generated upon invocation of {@link #reset()}. - */ - public String getDeviceId() { - return deviceId; - } - - /** - * Resets the application, deleting all credentials and removing any lock code. - * - * @throws IOException in case of connection error - * @throws ApduException in case of communication error - */ - public void reset() throws IOException, ApduException { - protocol.sendAndReceive(new Apdu(0, INS_RESET, 0xde, 0xad, null)); + // Features + /** Support for credentials that require touch to use. */ + public static final Feature FEATURE_TOUCH = + new Feature.Versioned<>("Touch", 4, 2, 0); + + /** Support for credentials using the SHA-512 hash algorithm. */ + public static final Feature FEATURE_SHA512 = + new Feature.Versioned<>("SHA-512", 4, 3, 1); + + /** Support for renaming a stored credential. */ + public static final Feature FEATURE_RENAME = + new Feature.Versioned<>("Rename Credential", 5, 3, 0); + + /** Support for secure messaging. */ + public static final Feature FEATURE_SCP = new Feature.Versioned<>("SCP", 5, 6, 3); + + // Tlv tags YKOATH data + private static final int TAG_NAME = 0x71; + private static final int TAG_KEY = 0x73; + private static final int TAG_RESPONSE = 0x75; + private static final int TAG_PROPERTY = 0x78; + private static final int TAG_IMF = 0x7a; + private static final int TAG_CHALLENGE = 0x74; + private static final int TAG_VERSION = 0x79; + + // Instruction bytes for APDU commands + private static final byte INS_LIST = (byte) 0xa1; + private static final byte INS_PUT = 0x01; + private static final byte INS_DELETE = 0x02; + private static final byte INS_SET_CODE = 0x03; + private static final byte INS_RESET = 0x04; + private static final byte INS_RENAME = 0x05; + private static final byte INS_CALCULATE = (byte) 0xa2; + private static final byte INS_VALIDATE = (byte) 0xa3; + private static final byte INS_CALCULATE_ALL = (byte) 0xa4; + private static final byte INS_SEND_REMAINING = (byte) 0xa5; + + private static final byte PROPERTY_REQUIRE_TOUCH = (byte) 0x02; + + private static final long MILLS_IN_SECOND = 1000; + private static final int DEFAULT_TOTP_PERIOD = 30; + private static final int CHALLENGE_LEN = 8; + private static final int ACCESS_KEY_LEN = 16; + + private final SmartCardProtocol protocol; + private final Version version; + @Nullable private final ScpKeyParams scpKeyParams; + + private String deviceId; + private byte[] salt; + @Nullable private byte[] challenge; + private boolean isAccessKeySet; + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(OathSession.class); + + /** + * Establishes a new session with a YubiKeys OATH application. + * + * @param connection to the YubiKey + * @throws IOException in case of connection error + * @throws ApplicationNotAvailableException if the application is missing or disabled + */ + public OathSession(SmartCardConnection connection) + throws IOException, ApplicationNotAvailableException { + this(connection, null); + } + + /** + * Establishes a new session with a YubiKeys OATH application. + * + * @param connection to the YubiKey + * @param scpKeyParams SCP key parameters to establish a secure connection + * @throws IOException in case of connection error + * @throws ApplicationNotAvailableException if the application is missing or disabled + */ + public OathSession(SmartCardConnection connection, @Nullable ScpKeyParams scpKeyParams) + throws IOException, ApplicationNotAvailableException { + protocol = new SmartCardProtocol(connection, INS_SEND_REMAINING); + SelectResponse selectResponse = new SelectResponse(protocol.select(AppId.OATH)); + this.scpKeyParams = scpKeyParams; + version = selectResponse.version; + deviceId = selectResponse.getDeviceId(); + salt = selectResponse.salt; + challenge = selectResponse.challenge; + isAccessKeySet = challenge != null && challenge.length != 0; + protocol.configure(version); + if (scpKeyParams != null) { + require(FEATURE_SCP); + try { + protocol.initScp(scpKeyParams); + } catch (ApduException | BadResponseException e) { + throw new IOException("Failed setting up SCP session", e); + } + } + Logger.debug( + logger, + "OATH session initialized (version={}, isAccessKeySet={})", + version, + isAccessKeySet); + } + + @Override + public void close() throws IOException { + protocol.close(); + } + + @Override + public Version getVersion() { + return version; + } + + /** + * Returns a unique ID which can be used to identify a particular YubiKey. + * + *

This ID is randomly generated upon invocation of {@link #reset()}. + */ + public String getDeviceId() { + return deviceId; + } + + /** + * Resets the application, deleting all credentials and removing any lock code. + * + * @throws IOException in case of connection error + * @throws ApduException in case of communication error + */ + public void reset() throws IOException, ApduException { + protocol.sendAndReceive(new Apdu(0, INS_RESET, 0xde, 0xad, null)); + try { + // Re-select since the device ID has changed + SelectResponse selectResponse = new SelectResponse(protocol.select(AppId.OATH)); + deviceId = selectResponse.getDeviceId(); + salt = selectResponse.salt; + challenge = null; + isAccessKeySet = false; + if (scpKeyParams != null) { try { - // Re-select since the device ID has changed - SelectResponse selectResponse = new SelectResponse(protocol.select(AppId.OATH)); - deviceId = selectResponse.getDeviceId(); - salt = selectResponse.salt; - challenge = null; - isAccessKeySet = false; - if (scpKeyParams != null) { - try { - protocol.initScp(scpKeyParams); - } catch (BadResponseException e) { - throw new IOException("Failed setting up SCP session", e); - } - } - Logger.info(logger, "OATH application data reset performed"); - } catch (ApplicationNotAvailableException e) { - throw new IllegalStateException(e); // This shouldn't happen + protocol.initScp(scpKeyParams); + } catch (BadResponseException e) { + throw new IOException("Failed setting up SCP session", e); } - } - - /** - * Returns true if an Access Key is currently set. - * - * @deprecated Use {@link #isAccessKeySet()} instead. - */ - @Deprecated - public boolean hasAccessKey() { - return isAccessKeySet; - } - - /** - * Returns true if an Access Key is currently set. - */ - public boolean isAccessKeySet() { - return isAccessKeySet; - } - - - /** - * Returns true if the session is locked. - */ - public boolean isLocked() { - return challenge != null && challenge.length != 0; - } - - /** - * Unlocks other commands when an Access Key is set, using a password to derive the Access Key. - *

- * Once unlocked, the application will remain unlocked for the duration of the session. - * See the YKOATH protocol specification for further details. - * - * @param password user-supplied password - * @return true if password valid - * @throws IOException in case of connection error - * @throws ApduException in case of communication error - */ - public boolean unlock(char[] password) throws IOException, ApduException { - if (!isLocked()) { - return true; - } - - if (password.length == 0) { - return false; - } - - byte[] secret = deriveAccessKey(password); - try { - return unlock(challenge -> doHmacSha1(secret, challenge)); - } finally { - Arrays.fill(secret, (byte) 0); - } - } - - /** - * Unlocks other commands when an Access Key is set. - *

- * Once unlocked, the application will remain unlocked for the duration of the session. - * See the YKOATH protocol specification for further details. - * - * @param validator to provide a correct response to a challenge, using the Access Key. - * @return if the command was successful or not - * @throws IOException in case of connection error - * @throws ApduException in case of communication error - */ - public boolean unlock(AccessKey validator) throws IOException, ApduException { - // if no validation/authentication required we consider that validation was successful - if (challenge == null) { - return true; - } - - Logger.debug(logger, "Unlocking session"); - - try { - Map request = new LinkedHashMap<>(); - request.put(TAG_RESPONSE, validator.calculateResponse(challenge)); - - byte[] clientChallenge = RandomUtils.getRandomBytes(CHALLENGE_LEN); - request.put(TAG_CHALLENGE, clientChallenge); - - byte[] data = protocol.sendAndReceive(new Apdu(0, INS_VALIDATE, 0, 0, Tlvs.encodeMap(request))); - Map map = Tlvs.decodeMap(data); - // return false if response from validation does not match verification - boolean responsesEqual = MessageDigest.isEqual(validator.calculateResponse(clientChallenge), map.get(TAG_RESPONSE)); - if (responsesEqual) { - challenge = null; - } - return responsesEqual; - } catch (ApduException e) { - if (e.getSw() == SW.INCORRECT_PARAMETERS) { - // key didn't recognize secret - return false; - } - throw e; - } - } - - /** - * Sets an Access Key derived from a password. Once a key is set, any usage of the credentials stored will - * require the application to be unlocked via one of the validate methods. Also see {@link #setAccessKey(byte[])}. - * - * @param password user-supplied password to set, encoded as UTF-8 bytes - * @throws IOException in case of connection error - * @throws ApduException in case of communication error - */ - public void setPassword(char[] password) throws IOException, ApduException { - setAccessKey(deriveAccessKey(password)); - } - - /** - * Sets an access key. Once an access key is set, any usage of the credentials stored will require the application - * to be unlocked via one of the validate methods, which requires knowledge of the access key. Typically this key is - * derived from a password (see {@link #deriveAccessKey}) and is set by instead using the - * {@link #setPassword} method. This method sets the raw 16 byte key. - * - * @param key the shared secret key used to unlock access to the application - * @throws IOException in case of connection error - * @throws ApduException in case of communication error - */ - public void setAccessKey(byte[] key) throws IOException, ApduException { - if (key.length != ACCESS_KEY_LEN) { - throw new IllegalArgumentException("Secret should be 16 bytes"); - } - - Map request = new LinkedHashMap<>(); - request.put(TAG_KEY, ByteBuffer.allocate(1 + key.length) - .put((byte) (OathType.TOTP.value | HashAlgorithm.SHA1.value)) - .put(key) - .array()); - - byte[] challenge = RandomUtils.getRandomBytes(CHALLENGE_LEN); - request.put(TAG_CHALLENGE, challenge); - request.put(TAG_RESPONSE, doHmacSha1(key, challenge)); - - protocol.sendAndReceive(new Apdu(0, INS_SET_CODE, 0, 0, Tlvs.encodeMap(request))); - isAccessKeySet = true; - Logger.info(logger, "New access key set"); - } - - /** - * Removes the access key, if one is set. - * - * @throws IOException in case of connection error - * @throws ApduException in case of communication error - */ - public void deleteAccessKey() throws IOException, ApduException { - protocol.sendAndReceive(new Apdu(0, INS_SET_CODE, 0, 0, new Tlv(TAG_KEY, null).getBytes())); - isAccessKeySet = false; - Logger.info(logger, "Access key removed"); - } - - /** - * Get a list of all Credentials stored on the YubiKey. - * - * @return list of credentials on device - * @throws IOException in case of connection error - * @throws ApduException in case of communication error - */ - public List getCredentials() throws IOException, ApduException { - byte[] response = protocol.sendAndReceive(new Apdu(0, INS_LIST, 0, 0, null)); - List list = Tlvs.decodeList(response); - List result = new ArrayList<>(); - for (Tlv tlv : list) { - result.add(new Credential(deviceId, new ListResponse(tlv))); - } - return result; - } - - /** - * Get a map of all Credentials stored on the YubiKey, together with a Code for each of them. - *

- * Credentials which use HOTP, or which require touch, will not be calculated. - * They will still be present in the result, but with a null value. - * The current system time will be used for TOTP calculation. - * - * @return a Map mapping Credentials to Code - * @throws IOException in case of connection error - * @throws ApduException in case of communication error - * @throws BadResponseException in case of incorrect YubiKey response - */ - public Map calculateCodes() throws IOException, ApduException, BadResponseException { - return calculateCodes(System.currentTimeMillis()); - } - - /** - * Get a map of all Credentials stored on the YubiKey, together with a Code for each of them. - *

- * Credentials which use HOTP, or which require touch, will not be calculated. - * They will still be present in the result, but with a null value. - * - * @param timestamp the timestamp which is used as start point for TOTP - * @return a Map mapping Credentials to Code - * @throws IOException in case of connection error - * @throws ApduException in case of communication error - * @throws BadResponseException in case of incorrect YubiKey response - */ - public Map calculateCodes(long timestamp) throws IOException, ApduException, BadResponseException { - // CALCULATE_ALL uses a single time step, so we run it with the most common one (period=30) - // and then recalculate any codes where period != 30. - long timeStep = (timestamp / MILLS_IN_SECOND / DEFAULT_TOTP_PERIOD); - byte[] challenge = ByteBuffer.allocate(CHALLENGE_LEN).putLong(timeStep).array(); - long validFrom = validFrom(timestamp, DEFAULT_TOTP_PERIOD); - long validUntil = validFrom + DEFAULT_TOTP_PERIOD * MILLS_IN_SECOND; - - Logger.info(logger, "Calculating all codes for time={}", timestamp); - - byte[] data = protocol.sendAndReceive(new Apdu(0, INS_CALCULATE_ALL, 0, 1, new Tlv(TAG_CHALLENGE, challenge).getBytes())); - Iterator responseTlvs = Tlvs.decodeList(data).iterator(); - Map map = new HashMap<>(); - while (responseTlvs.hasNext()) { - Tlv nameTlv = responseTlvs.next(); - if (nameTlv.getTag() != TAG_NAME) { - throw new BadResponseException(String.format("Unexpected tag: %02x", nameTlv.getTag())); - } - byte[] credentialId = nameTlv.getValue(); - CalculateResponse response = new CalculateResponse(responseTlvs.next()); - - // parse credential properties - Credential credential = new Credential(deviceId, credentialId, response); - - // Non-empty responses are for TOTP credentials which do not require touch. - if (response.response.length == 4) { - int period = credential.getPeriod(); - if (period != DEFAULT_TOTP_PERIOD) { - // Recalculate TOTP for correct period. - Logger.debug(logger, "Recalculating code for period={}", period); - map.put(credential, calculateCode(credential, timestamp)); - } else { - map.put(credential, new Code(formatTruncated(response), validFrom, validUntil)); - } - } else { - // HOTP, or TOTP that requires touch, no code. - map.put(credential, null); - } - } - - return map; - } - - /** - * Calculate a full (non-truncated) HMAC signature using a Credential. - *

- * Using this command a Credential can be used as an HMAC key to calculate a result for an - * arbitrary challenge. The hash algorithm specified for the Credential is used. - * - * @param credentialId the ID of a stored Credential - * @param challenge the input to the HMAC operation - * @return the calculated response - * @throws IOException in case of connection error - * @throws ApduException in case of communication error - * @throws BadResponseException in case an unexpected response was sent from the YubiKey - */ - public byte[] calculateResponse(byte[] credentialId, byte[] challenge) throws IOException, ApduException, BadResponseException { - Map request = new LinkedHashMap<>(); - request.put(TAG_NAME, credentialId); - request.put(TAG_CHALLENGE, challenge); - byte[] data = protocol.sendAndReceive(new Apdu(0, INS_CALCULATE, 0, 0, Tlvs.encodeMap(request))); - byte[] response = Tlvs.unpackValue(TAG_RESPONSE, data); - return Arrays.copyOfRange(response, 1, response.length); - } - - /** - * Returns a new Code for a stored Credential. - * The current system time will be used for TOTP calculation. - * - * @param credential credential that will get new code - * @return calculated code - * @throws IOException in case of connection error - * @throws ApduException in case of communication error - */ - public Code calculateCode(Credential credential) throws IOException, ApduException { - return calculateCode(credential, System.currentTimeMillis()); - } - - /** - * Returns a new Code for a stored Credential. - * - * @param credential credential that will get new code - * @param timestamp the timestamp which is used as start point for TOTP, this is ignored for HOTP - * @return a new code - * @throws IOException in case of connection error - * @throws ApduException in case of communication error - */ - public Code calculateCode(Credential credential, @Nullable Long timestamp) throws IOException, ApduException { - if (!credential.deviceId.equals(deviceId)) { - throw new IllegalArgumentException("The given credential belongs to a different device!"); - } - byte[] challenge = new byte[CHALLENGE_LEN]; - if (timestamp != null && credential.getPeriod() != 0) { - long timeStep = (timestamp / MILLS_IN_SECOND / credential.getPeriod()); - ByteBuffer.wrap(challenge).putLong(timeStep); - } - - if (credential.getOathType() == OathType.TOTP) { - Logger.debug(logger, "Calculating TOTP code for time={}, period={}", - timestamp, credential.getPeriod()); + } + Logger.info(logger, "OATH application data reset performed"); + } catch (ApplicationNotAvailableException e) { + throw new IllegalStateException(e); // This shouldn't happen + } + } + + /** + * Returns true if an Access Key is currently set. + * + * @deprecated Use {@link #isAccessKeySet()} instead. + */ + @Deprecated + public boolean hasAccessKey() { + return isAccessKeySet; + } + + /** Returns true if an Access Key is currently set. */ + public boolean isAccessKeySet() { + return isAccessKeySet; + } + + /** Returns true if the session is locked. */ + public boolean isLocked() { + return challenge != null && challenge.length != 0; + } + + /** + * Unlocks other commands when an Access Key is set, using a password to derive the Access Key. + * + *

Once unlocked, the application will remain unlocked for the duration of the session. See the + * YKOATH protocol specification for further details. + * + * @param password user-supplied password + * @return true if password valid + * @throws IOException in case of connection error + * @throws ApduException in case of communication error + */ + public boolean unlock(char[] password) throws IOException, ApduException { + if (!isLocked()) { + return true; + } + + if (password.length == 0) { + return false; + } + + byte[] secret = deriveAccessKey(password); + try { + return unlock(challenge -> doHmacSha1(secret, challenge)); + } finally { + Arrays.fill(secret, (byte) 0); + } + } + + /** + * Unlocks other commands when an Access Key is set. + * + *

Once unlocked, the application will remain unlocked for the duration of the session. See the + * YKOATH protocol specification for further details. + * + * @param validator to provide a correct response to a challenge, using the Access Key. + * @return if the command was successful or not + * @throws IOException in case of connection error + * @throws ApduException in case of communication error + */ + public boolean unlock(AccessKey validator) throws IOException, ApduException { + // if no validation/authentication required we consider that validation was successful + if (challenge == null) { + return true; + } + + Logger.debug(logger, "Unlocking session"); + + try { + Map request = new LinkedHashMap<>(); + request.put(TAG_RESPONSE, validator.calculateResponse(challenge)); + + byte[] clientChallenge = RandomUtils.getRandomBytes(CHALLENGE_LEN); + request.put(TAG_CHALLENGE, clientChallenge); + + byte[] data = + protocol.sendAndReceive(new Apdu(0, INS_VALIDATE, 0, 0, Tlvs.encodeMap(request))); + Map map = Tlvs.decodeMap(data); + // return false if response from validation does not match verification + boolean responsesEqual = + MessageDigest.isEqual( + validator.calculateResponse(clientChallenge), map.get(TAG_RESPONSE)); + if (responsesEqual) { + challenge = null; + } + return responsesEqual; + } catch (ApduException e) { + if (e.getSw() == SW.INCORRECT_PARAMETERS) { + // key didn't recognize secret + return false; + } + throw e; + } + } + + /** + * Sets an Access Key derived from a password. Once a key is set, any usage of the credentials + * stored will require the application to be unlocked via one of the validate methods. Also see + * {@link #setAccessKey(byte[])}. + * + * @param password user-supplied password to set, encoded as UTF-8 bytes + * @throws IOException in case of connection error + * @throws ApduException in case of communication error + */ + public void setPassword(char[] password) throws IOException, ApduException { + setAccessKey(deriveAccessKey(password)); + } + + /** + * Sets an access key. Once an access key is set, any usage of the credentials stored will require + * the application to be unlocked via one of the validate methods, which requires knowledge of the + * access key. Typically this key is derived from a password (see {@link #deriveAccessKey}) and is + * set by instead using the {@link #setPassword} method. This method sets the raw 16 byte key. + * + * @param key the shared secret key used to unlock access to the application + * @throws IOException in case of connection error + * @throws ApduException in case of communication error + */ + public void setAccessKey(byte[] key) throws IOException, ApduException { + if (key.length != ACCESS_KEY_LEN) { + throw new IllegalArgumentException("Secret should be 16 bytes"); + } + + Map request = new LinkedHashMap<>(); + request.put( + TAG_KEY, + ByteBuffer.allocate(1 + key.length) + .put((byte) (OathType.TOTP.value | HashAlgorithm.SHA1.value)) + .put(key) + .array()); + + byte[] challenge = RandomUtils.getRandomBytes(CHALLENGE_LEN); + request.put(TAG_CHALLENGE, challenge); + request.put(TAG_RESPONSE, doHmacSha1(key, challenge)); + + protocol.sendAndReceive(new Apdu(0, INS_SET_CODE, 0, 0, Tlvs.encodeMap(request))); + isAccessKeySet = true; + Logger.info(logger, "New access key set"); + } + + /** + * Removes the access key, if one is set. + * + * @throws IOException in case of connection error + * @throws ApduException in case of communication error + */ + public void deleteAccessKey() throws IOException, ApduException { + protocol.sendAndReceive(new Apdu(0, INS_SET_CODE, 0, 0, new Tlv(TAG_KEY, null).getBytes())); + isAccessKeySet = false; + Logger.info(logger, "Access key removed"); + } + + /** + * Get a list of all Credentials stored on the YubiKey. + * + * @return list of credentials on device + * @throws IOException in case of connection error + * @throws ApduException in case of communication error + */ + public List getCredentials() throws IOException, ApduException { + byte[] response = protocol.sendAndReceive(new Apdu(0, INS_LIST, 0, 0, null)); + List list = Tlvs.decodeList(response); + List result = new ArrayList<>(); + for (Tlv tlv : list) { + result.add(new Credential(deviceId, new ListResponse(tlv))); + } + return result; + } + + /** + * Get a map of all Credentials stored on the YubiKey, together with a Code for each of them. + * + *

Credentials which use HOTP, or which require touch, will not be calculated. They will still + * be present in the result, but with a null value. The current system time will be used for TOTP + * calculation. + * + * @return a Map mapping Credentials to Code + * @throws IOException in case of connection error + * @throws ApduException in case of communication error + * @throws BadResponseException in case of incorrect YubiKey response + */ + public Map calculateCodes() + throws IOException, ApduException, BadResponseException { + return calculateCodes(System.currentTimeMillis()); + } + + /** + * Get a map of all Credentials stored on the YubiKey, together with a Code for each of them. + * + *

Credentials which use HOTP, or which require touch, will not be calculated. They will still + * be present in the result, but with a null value. + * + * @param timestamp the timestamp which is used as start point for TOTP + * @return a Map mapping Credentials to Code + * @throws IOException in case of connection error + * @throws ApduException in case of communication error + * @throws BadResponseException in case of incorrect YubiKey response + */ + public Map calculateCodes(long timestamp) + throws IOException, ApduException, BadResponseException { + // CALCULATE_ALL uses a single time step, so we run it with the most common one (period=30) + // and then recalculate any codes where period != 30. + long timeStep = (timestamp / MILLS_IN_SECOND / DEFAULT_TOTP_PERIOD); + byte[] challenge = ByteBuffer.allocate(CHALLENGE_LEN).putLong(timeStep).array(); + long validFrom = validFrom(timestamp, DEFAULT_TOTP_PERIOD); + long validUntil = validFrom + DEFAULT_TOTP_PERIOD * MILLS_IN_SECOND; + + Logger.info(logger, "Calculating all codes for time={}", timestamp); + + byte[] data = + protocol.sendAndReceive( + new Apdu(0, INS_CALCULATE_ALL, 0, 1, new Tlv(TAG_CHALLENGE, challenge).getBytes())); + Iterator responseTlvs = Tlvs.decodeList(data).iterator(); + Map map = new HashMap<>(); + while (responseTlvs.hasNext()) { + Tlv nameTlv = responseTlvs.next(); + if (nameTlv.getTag() != TAG_NAME) { + throw new BadResponseException(String.format("Unexpected tag: %02x", nameTlv.getTag())); + } + byte[] credentialId = nameTlv.getValue(); + CalculateResponse response = new CalculateResponse(responseTlvs.next()); + + // parse credential properties + Credential credential = new Credential(deviceId, credentialId, response); + + // Non-empty responses are for TOTP credentials which do not require touch. + if (response.response.length == 4) { + int period = credential.getPeriod(); + if (period != DEFAULT_TOTP_PERIOD) { + // Recalculate TOTP for correct period. + Logger.debug(logger, "Recalculating code for period={}", period); + map.put(credential, calculateCode(credential, timestamp)); } else { - Logger.debug(logger, "Calculating HOTP code"); - } - - Map requestTlv = new LinkedHashMap<>(); - requestTlv.put(TAG_NAME, credential.getId()); - requestTlv.put(TAG_CHALLENGE, challenge); - byte[] data = protocol.sendAndReceive(new Apdu(0, INS_CALCULATE, 0, 1, Tlvs.encodeMap(requestTlv))); - String value = formatTruncated(new CalculateResponse(Tlv.parse(data))); - - switch (credential.getOathType()) { - case TOTP: - long validFrom = validFrom(timestamp, credential.getPeriod()); - return new Code(value, validFrom, validFrom + credential.getPeriod() * MILLS_IN_SECOND); - case HOTP: - default: - return new Code(value, System.currentTimeMillis(), Long.MAX_VALUE); - } - } - - /** - * Adds a new Credential to the YubiKey. - *

- * The Credential ID (see {@link CredentialData#getId()}) must be unique to the YubiKey, or the - * existing Credential with the same ID will be overwritten. - *

- * Setting requireTouch requires support for {@link #FEATURE_TOUCH}, available on YubiKey 4.2 or later. - * Using SHA-512 requires support for {@link #FEATURE_SHA512}, available on YubiKey 4.3.1 or later. - * - * @param credentialData credential data to add - * @param requireTouch true if the credential should require touch to be used - * @return the newly added Credential - * @throws IOException in case of connection error - * @throws ApduException in case of communication error - */ - public Credential putCredential(CredentialData credentialData, boolean requireTouch) throws IOException, ApduException { - if (credentialData.getHashAlgorithm() == HashAlgorithm.SHA512) { - require(FEATURE_SHA512); - } - - byte[] key = credentialData.getHashAlgorithm().prepareKey(credentialData.getSecret()); - Map requestTlvs = new LinkedHashMap<>(); - requestTlvs.put(TAG_NAME, credentialData.getId()); - - requestTlvs.put(TAG_KEY, ByteBuffer.allocate(2 + key.length) - .put((byte) (credentialData.getOathType().value | credentialData.getHashAlgorithm().value)) - .put((byte) credentialData.getDigits()) - .put(key) - .array()); - - ByteArrayOutputStream output = new ByteArrayOutputStream(); - output.write(Tlvs.encodeMap(requestTlvs)); - - if (requireTouch) { - require(FEATURE_TOUCH); - output.write(TAG_PROPERTY); - output.write(PROPERTY_REQUIRE_TOUCH); - } - - if (credentialData.getOathType() == OathType.HOTP && credentialData.getCounter() > 0) { - output.write(TAG_IMF); - output.write(4); - output.write(ByteBuffer.allocate(4).putInt(credentialData.getCounter()).array()); - } - - Logger.debug(logger, "Importing credential (type={}, hash={}, digits={}, " - + "period={}, imf={}, touch_required={})", - credentialData.getOathType(), credentialData.getHashAlgorithm(), - credentialData.getDigits(), credentialData.getPeriod(), - credentialData.getCounter(), requireTouch); - - protocol.sendAndReceive(new Apdu(0x00, INS_PUT, 0, 0, output.toByteArray())); - Logger.info(logger, "Credential imported"); - return new Credential(deviceId, credentialData.getId(), credentialData.getOathType(), requireTouch); - } - - /** - * Deletes an existing Credential from the YubiKey. - * - * @param credentialId the ID of the credential to remove - * @throws IOException in case of connection error - * @throws ApduException in case of communication error - */ - public void deleteCredential(byte[] credentialId) throws IOException, ApduException { - protocol.sendAndReceive(new Apdu(0x00, INS_DELETE, 0, 0, new Tlv(TAG_NAME, credentialId).getBytes())); - Logger.info(logger, "Credential deleted"); - } - - /** - * Deletes an existing Credential from the YubiKey. - * - * @param credential the Credential to remove - * @throws IOException in case of connection error - * @throws ApduException in case of communication error - */ - public void deleteCredential(Credential credential) throws IOException, ApduException { - if (!credential.deviceId.equals(deviceId)) { - throw new IllegalArgumentException("The given credential belongs to a different device!"); - } - deleteCredential(credential.getId()); - } - - /** - * Change the issuer and name of a Credential already stored on the YubiKey. - *

- * This functionality requires support for {@link #FEATURE_RENAME}, available on YubiKey 5.3 or later. - * - * @param credentialId the ID of the credential to rename - * @param newCredentialId the new ID to use - * @throws IOException in case of connection error - * @throws ApduException in case of communication error - */ - public void renameCredential(byte[] credentialId, byte[] newCredentialId) throws IOException, ApduException { - require(FEATURE_RENAME); - protocol.sendAndReceive(new Apdu(0x00, INS_RENAME, 0, 0, Tlvs.encodeList(Arrays.asList( - new Tlv(TAG_NAME, credentialId), - new Tlv(TAG_NAME, newCredentialId) - )))); - Logger.info(logger, "Credential renamed"); - } - - /** - * Change the issuer and name of a Credential already stored on the YubiKey. - *

- * This functionality requires support for {@link #FEATURE_RENAME}, available on YubiKey 5.3 or later. - * - * @param credential the Credential to rename - * @param accountName the new name of the credential - * @param issuer the new issuer of the credential - * @return the updated Credential - * @throws IOException in case of connection error - * @throws ApduException in case of communication error - */ - public Credential renameCredential(Credential credential, String accountName, @Nullable String issuer) throws IOException, ApduException { - if (!credential.deviceId.equals(deviceId)) { - throw new IllegalArgumentException("The given credential belongs to a different device!"); + map.put(credential, new Code(formatTruncated(response), validFrom, validUntil)); } - byte[] newId = CredentialIdUtils.formatId(issuer, accountName, credential.getOathType(), credential.getPeriod()); - renameCredential(credential.getId(), newId); - return new Credential( - credential.deviceId, - newId, - credential.getOathType(), - credential.isTouchRequired() - ); - } - - /** - * Derives an access key from a password and the device-specific salt. - * The key is derived by running 1000 rounds of PBKDF2 using the password and salt as inputs, with a 16 byte output. - * - * @param password a user-supplied password, encoded as UTF-8 bytes. - * @return an access key for unlocking the session - */ - public byte[] deriveAccessKey(char[] password) { - PBEKeySpec keyspec = new PBEKeySpec(password, salt, 1000, ACCESS_KEY_LEN * 8); - try { - SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); - return factory.generateSecret(keyspec).getEncoded(); - } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } finally { - keyspec.clearPassword(); - } - } - - /** - * Calculates an HMAC-SHA1 response - * - * @param secret the secret - * @param message data in bytes - * @return the MAC result - */ - private static byte[] doHmacSha1(byte[] secret, byte[] message) { - try { - Mac mac = Mac.getInstance("HmacSHA1"); - mac.init(new SecretKeySpec(secret, mac.getAlgorithm())); - return mac.doFinal(message); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - throw new RuntimeException(e); - } - } - - /** - * Calculate the time when the valid period of code starts - * - * @param timestamp current timestamp - * @param period period in seconds for how long code is valid from its calculation/generation time - * @return the start of period when code is valid - */ - private static long validFrom(@Nullable Long timestamp, int period) { - if (timestamp == null || period == 0) { - // handling case to avoid ArithmeticException - // we can't divide by zero, we can set that it valid from now - // but its valid period ends the very same time - return System.currentTimeMillis(); - } - - // Note: codes are valid in 'period' second slices - // so the valid sliced period actually starts before the calculation happens - // and potentially might happen even way before - // (so that code is valid only 1 second after calculation) - return timestamp - timestamp % (period * MILLS_IN_SECOND); - } + } else { + // HOTP, or TOTP that requires touch, no code. + map.put(credential, null); + } + } + + return map; + } + + /** + * Calculate a full (non-truncated) HMAC signature using a Credential. + * + *

Using this command a Credential can be used as an HMAC key to calculate a result for an + * arbitrary challenge. The hash algorithm specified for the Credential is used. + * + * @param credentialId the ID of a stored Credential + * @param challenge the input to the HMAC operation + * @return the calculated response + * @throws IOException in case of connection error + * @throws ApduException in case of communication error + * @throws BadResponseException in case an unexpected response was sent from the YubiKey + */ + public byte[] calculateResponse(byte[] credentialId, byte[] challenge) + throws IOException, ApduException, BadResponseException { + Map request = new LinkedHashMap<>(); + request.put(TAG_NAME, credentialId); + request.put(TAG_CHALLENGE, challenge); + byte[] data = + protocol.sendAndReceive(new Apdu(0, INS_CALCULATE, 0, 0, Tlvs.encodeMap(request))); + byte[] response = Tlvs.unpackValue(TAG_RESPONSE, data); + return Arrays.copyOfRange(response, 1, response.length); + } + + /** + * Returns a new Code for a stored Credential. The current system time will be used for TOTP + * calculation. + * + * @param credential credential that will get new code + * @return calculated code + * @throws IOException in case of connection error + * @throws ApduException in case of communication error + */ + public Code calculateCode(Credential credential) throws IOException, ApduException { + return calculateCode(credential, System.currentTimeMillis()); + } + + /** + * Returns a new Code for a stored Credential. + * + * @param credential credential that will get new code + * @param timestamp the timestamp which is used as start point for TOTP, this is ignored for HOTP + * @return a new code + * @throws IOException in case of connection error + * @throws ApduException in case of communication error + */ + public Code calculateCode(Credential credential, @Nullable Long timestamp) + throws IOException, ApduException { + if (!credential.deviceId.equals(deviceId)) { + throw new IllegalArgumentException("The given credential belongs to a different device!"); + } + byte[] challenge = new byte[CHALLENGE_LEN]; + if (timestamp != null && credential.getPeriod() != 0) { + long timeStep = (timestamp / MILLS_IN_SECOND / credential.getPeriod()); + ByteBuffer.wrap(challenge).putLong(timeStep); + } + + if (credential.getOathType() == OathType.TOTP) { + Logger.debug( + logger, + "Calculating TOTP code for time={}, period={}", + timestamp, + credential.getPeriod()); + } else { + Logger.debug(logger, "Calculating HOTP code"); + } + + Map requestTlv = new LinkedHashMap<>(); + requestTlv.put(TAG_NAME, credential.getId()); + requestTlv.put(TAG_CHALLENGE, challenge); + byte[] data = + protocol.sendAndReceive(new Apdu(0, INS_CALCULATE, 0, 1, Tlvs.encodeMap(requestTlv))); + String value = formatTruncated(new CalculateResponse(Tlv.parse(data))); + + switch (credential.getOathType()) { + case TOTP: + long validFrom = validFrom(timestamp, credential.getPeriod()); + return new Code(value, validFrom, validFrom + credential.getPeriod() * MILLS_IN_SECOND); + case HOTP: + default: + return new Code(value, System.currentTimeMillis(), Long.MAX_VALUE); + } + } + + /** + * Adds a new Credential to the YubiKey. + * + *

The Credential ID (see {@link CredentialData#getId()}) must be unique to the YubiKey, or the + * existing Credential with the same ID will be overwritten. + * + *

Setting requireTouch requires support for {@link #FEATURE_TOUCH}, available on YubiKey 4.2 + * or later. Using SHA-512 requires support for {@link #FEATURE_SHA512}, available on YubiKey + * 4.3.1 or later. + * + * @param credentialData credential data to add + * @param requireTouch true if the credential should require touch to be used + * @return the newly added Credential + * @throws IOException in case of connection error + * @throws ApduException in case of communication error + */ + public Credential putCredential(CredentialData credentialData, boolean requireTouch) + throws IOException, ApduException { + if (credentialData.getHashAlgorithm() == HashAlgorithm.SHA512) { + require(FEATURE_SHA512); + } + + byte[] key = credentialData.getHashAlgorithm().prepareKey(credentialData.getSecret()); + Map requestTlvs = new LinkedHashMap<>(); + requestTlvs.put(TAG_NAME, credentialData.getId()); + + requestTlvs.put( + TAG_KEY, + ByteBuffer.allocate(2 + key.length) + .put( + (byte) + (credentialData.getOathType().value | credentialData.getHashAlgorithm().value)) + .put((byte) credentialData.getDigits()) + .put(key) + .array()); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + output.write(Tlvs.encodeMap(requestTlvs)); + + if (requireTouch) { + require(FEATURE_TOUCH); + output.write(TAG_PROPERTY); + output.write(PROPERTY_REQUIRE_TOUCH); + } + + if (credentialData.getOathType() == OathType.HOTP && credentialData.getCounter() > 0) { + output.write(TAG_IMF); + output.write(4); + output.write(ByteBuffer.allocate(4).putInt(credentialData.getCounter()).array()); + } + + Logger.debug( + logger, + "Importing credential (type={}, hash={}, digits={}, " + + "period={}, imf={}, touch_required={})", + credentialData.getOathType(), + credentialData.getHashAlgorithm(), + credentialData.getDigits(), + credentialData.getPeriod(), + credentialData.getCounter(), + requireTouch); + + protocol.sendAndReceive(new Apdu(0x00, INS_PUT, 0, 0, output.toByteArray())); + Logger.info(logger, "Credential imported"); + return new Credential( + deviceId, credentialData.getId(), credentialData.getOathType(), requireTouch); + } + + /** + * Deletes an existing Credential from the YubiKey. + * + * @param credentialId the ID of the credential to remove + * @throws IOException in case of connection error + * @throws ApduException in case of communication error + */ + public void deleteCredential(byte[] credentialId) throws IOException, ApduException { + protocol.sendAndReceive( + new Apdu(0x00, INS_DELETE, 0, 0, new Tlv(TAG_NAME, credentialId).getBytes())); + Logger.info(logger, "Credential deleted"); + } + + /** + * Deletes an existing Credential from the YubiKey. + * + * @param credential the Credential to remove + * @throws IOException in case of connection error + * @throws ApduException in case of communication error + */ + public void deleteCredential(Credential credential) throws IOException, ApduException { + if (!credential.deviceId.equals(deviceId)) { + throw new IllegalArgumentException("The given credential belongs to a different device!"); + } + deleteCredential(credential.getId()); + } + + /** + * Change the issuer and name of a Credential already stored on the YubiKey. + * + *

This functionality requires support for {@link #FEATURE_RENAME}, available on YubiKey 5.3 or + * later. + * + * @param credentialId the ID of the credential to rename + * @param newCredentialId the new ID to use + * @throws IOException in case of connection error + * @throws ApduException in case of communication error + */ + public void renameCredential(byte[] credentialId, byte[] newCredentialId) + throws IOException, ApduException { + require(FEATURE_RENAME); + protocol.sendAndReceive( + new Apdu( + 0x00, + INS_RENAME, + 0, + 0, + Tlvs.encodeList( + Arrays.asList( + new Tlv(TAG_NAME, credentialId), new Tlv(TAG_NAME, newCredentialId))))); + Logger.info(logger, "Credential renamed"); + } + + /** + * Change the issuer and name of a Credential already stored on the YubiKey. + * + *

This functionality requires support for {@link #FEATURE_RENAME}, available on YubiKey 5.3 or + * later. + * + * @param credential the Credential to rename + * @param accountName the new name of the credential + * @param issuer the new issuer of the credential + * @return the updated Credential + * @throws IOException in case of connection error + * @throws ApduException in case of communication error + */ + public Credential renameCredential( + Credential credential, String accountName, @Nullable String issuer) + throws IOException, ApduException { + if (!credential.deviceId.equals(deviceId)) { + throw new IllegalArgumentException("The given credential belongs to a different device!"); + } + byte[] newId = + CredentialIdUtils.formatId( + issuer, accountName, credential.getOathType(), credential.getPeriod()); + renameCredential(credential.getId(), newId); + return new Credential( + credential.deviceId, newId, credential.getOathType(), credential.isTouchRequired()); + } + + /** + * Derives an access key from a password and the device-specific salt. The key is derived by + * running 1000 rounds of PBKDF2 using the password and salt as inputs, with a 16 byte output. + * + * @param password a user-supplied password, encoded as UTF-8 bytes. + * @return an access key for unlocking the session + */ + public byte[] deriveAccessKey(char[] password) { + PBEKeySpec keyspec = new PBEKeySpec(password, salt, 1000, ACCESS_KEY_LEN * 8); + try { + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + return factory.generateSecret(keyspec).getEncoded(); + } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } finally { + keyspec.clearPassword(); + } + } + + /** + * Calculates an HMAC-SHA1 response + * + * @param secret the secret + * @param message data in bytes + * @return the MAC result + */ + private static byte[] doHmacSha1(byte[] secret, byte[] message) { + try { + Mac mac = Mac.getInstance("HmacSHA1"); + mac.init(new SecretKeySpec(secret, mac.getAlgorithm())); + return mac.doFinal(message); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new RuntimeException(e); + } + } + + /** + * Calculate the time when the valid period of code starts + * + * @param timestamp current timestamp + * @param period period in seconds for how long code is valid from its calculation/generation time + * @return the start of period when code is valid + */ + private static long validFrom(@Nullable Long timestamp, int period) { + if (timestamp == null || period == 0) { + // handling case to avoid ArithmeticException + // we can't divide by zero, we can set that it valid from now + // but its valid period ends the very same time + return System.currentTimeMillis(); + } + + // Note: codes are valid in 'period' second slices + // so the valid sliced period actually starts before the calculation happens + // and potentially might happen even way before + // (so that code is valid only 1 second after calculation) + return timestamp - timestamp % (period * MILLS_IN_SECOND); + } + + /** + * Truncate result from key to have digits number of code + * + * @param data the code received within calculate or calculate all response + * @return truncated code + */ + private String formatTruncated(CalculateResponse data) { + String result = Integer.toString(ByteBuffer.wrap(data.response).getInt()); + String value; + // truncate result length (align it with value of digits) + if (result.length() > data.digits) { + // take last digits + value = result.substring(result.length() - data.digits); + } else if (result.length() < data.digits) { + // or append 0 at the beginning of string + value = String.format("%" + data.digits + "s", result).replace(' ', '0'); + } else { + value = result; + } + return value; + } + + static class ListResponse { + final byte[] id; + final OathType oathType; + final HashAlgorithm hashAlgorithm; + + private ListResponse(Tlv tlv) { + byte[] value = tlv.getValue(); + id = Arrays.copyOfRange(value, 1, value.length); + oathType = OathType.fromValue((byte) (0xf0 & value[0])); + hashAlgorithm = HashAlgorithm.fromValue((byte) (0x0f & value[0])); + } + } + + static class CalculateResponse { + final byte responseType; + final int digits; + final byte[] response; + + private CalculateResponse(Tlv tlv) { + responseType = (byte) tlv.getTag(); + byte[] value = tlv.getValue(); + digits = value[0]; + response = Arrays.copyOfRange(value, 1, value.length); + } + } + + private static class SelectResponse { + private final Version version; + private final byte[] salt; + @Nullable private final byte[] challenge; /** - * Truncate result from key to have digits number of code + * Creates an instance of OATH application info from SELECT response * - * @param data the code received within calculate or calculate all response - * @return truncated code + * @param response the response from OATH SELECT command */ - private String formatTruncated(CalculateResponse data) { - String result = Integer.toString(ByteBuffer.wrap(data.response).getInt()); - String value; - // truncate result length (align it with value of digits) - if (result.length() > data.digits) { - // take last digits - value = result.substring(result.length() - data.digits); - } else if (result.length() < data.digits) { - // or append 0 at the beginning of string - value = String.format("%" + data.digits + "s", result).replace(' ', '0'); - } else { - value = result; - } - return value; - } - - static class ListResponse { - final byte[] id; - final OathType oathType; - final HashAlgorithm hashAlgorithm; - - private ListResponse(Tlv tlv) { - byte[] value = tlv.getValue(); - id = Arrays.copyOfRange(value, 1, value.length); - oathType = OathType.fromValue((byte) (0xf0 & value[0])); - hashAlgorithm = HashAlgorithm.fromValue((byte) (0x0f & value[0])); - } - } - - static class CalculateResponse { - final byte responseType; - final int digits; - final byte[] response; - - private CalculateResponse(Tlv tlv) { - responseType = (byte) tlv.getTag(); - byte[] value = tlv.getValue(); - digits = value[0]; - response = Arrays.copyOfRange(value, 1, value.length); - } - } - - private static class SelectResponse { - private final Version version; - private final byte[] salt; - @Nullable - private final byte[] challenge; - - /** - * Creates an instance of OATH application info from SELECT response - * - * @param response the response from OATH SELECT command - */ - SelectResponse(byte[] response) { - Map map = Tlvs.decodeMap(response); - version = Version.fromBytes(map.get(TAG_VERSION)); - salt = map.get(TAG_NAME); - challenge = map.get(TAG_CHALLENGE); - } - - private String getDeviceId() { - MessageDigest messageDigest; - try { - messageDigest = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - // Shouldn't happen. - throw new IllegalStateException(e); - } - messageDigest.update(salt); - byte[] digest = messageDigest.digest(); - return Base64.toUrlSafeString(Arrays.copyOfRange(digest, 0, 16)); - } - } + SelectResponse(byte[] response) { + Map map = Tlvs.decodeMap(response); + version = Version.fromBytes(map.get(TAG_VERSION)); + salt = map.get(TAG_NAME); + challenge = map.get(TAG_CHALLENGE); + } + + private String getDeviceId() { + MessageDigest messageDigest; + try { + messageDigest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + // Shouldn't happen. + throw new IllegalStateException(e); + } + messageDigest.update(salt); + byte[] digest = messageDigest.digest(); + return Base64.toUrlSafeString(Arrays.copyOfRange(digest, 0, 16)); + } + } } diff --git a/oath/src/main/java/com/yubico/yubikit/oath/OathType.java b/oath/src/main/java/com/yubico/yubikit/oath/OathType.java index da669a8c..a70a9640 100755 --- a/oath/src/main/java/com/yubico/yubikit/oath/OathType.java +++ b/oath/src/main/java/com/yubico/yubikit/oath/OathType.java @@ -16,46 +16,36 @@ package com.yubico.yubikit.oath; -/** - * Supported OATH variants for use with the OATH YubiKey application. - */ +/** Supported OATH variants for use with the OATH YubiKey application. */ public enum OathType { - /** - * OATH HOTP (event based), as defined in RFC 4226 - */ - HOTP((byte) 0x10), - /** - * OATH TOTP (time based), as defined in RFC 6238 - */ - TOTP((byte) 0x20); + /** OATH HOTP (event based), as defined in RFC 4226 */ + HOTP((byte) 0x10), + /** OATH TOTP (time based), as defined in RFC 6238 */ + TOTP((byte) 0x20); - public final byte value; + public final byte value; - OathType(byte value) { - this.value = value; - } + OathType(byte value) { + this.value = value; + } - /** - * Returns the OATH type corresponding to the given YKOATH TYPE constant. - */ - public static OathType fromValue(byte value) { - for (OathType type : OathType.values()) { - if (type.value == value) { - return type; - } - } - throw new IllegalArgumentException("Not a valid OathType: " + value); + /** Returns the OATH type corresponding to the given YKOATH TYPE constant. */ + public static OathType fromValue(byte value) { + for (OathType type : OathType.values()) { + if (type.value == value) { + return type; + } } + throw new IllegalArgumentException("Not a valid OathType: " + value); + } - /** - * Returns the OATH type corresponding to the given name, as used in otpauth:// URIs. - */ - public static OathType fromString(String value) { - if ("hotp".equalsIgnoreCase(value)) { - return HOTP; - } else if ("totp".equalsIgnoreCase(value)) { - return TOTP; - } - throw new IllegalArgumentException("Not a valid OathType: " + value); + /** Returns the OATH type corresponding to the given name, as used in otpauth:// URIs. */ + public static OathType fromString(String value) { + if ("hotp".equalsIgnoreCase(value)) { + return HOTP; + } else if ("totp".equalsIgnoreCase(value)) { + return TOTP; } + throw new IllegalArgumentException("Not a valid OathType: " + value); + } } diff --git a/oath/src/main/java/com/yubico/yubikit/oath/ParseUriException.java b/oath/src/main/java/com/yubico/yubikit/oath/ParseUriException.java index 6face223..414db440 100755 --- a/oath/src/main/java/com/yubico/yubikit/oath/ParseUriException.java +++ b/oath/src/main/java/com/yubico/yubikit/oath/ParseUriException.java @@ -16,13 +16,11 @@ package com.yubico.yubikit.oath; -/** - * Thrown when an OATH otpauth:// URI couldn't be parsed. - */ +/** Thrown when an OATH otpauth:// URI couldn't be parsed. */ public class ParseUriException extends Exception { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - public ParseUriException(String message) { - super(message); - } + public ParseUriException(String message) { + super(message); + } } diff --git a/oath/src/main/java/com/yubico/yubikit/oath/package-info.java b/oath/src/main/java/com/yubico/yubikit/oath/package-info.java index 663de981..05bda8b4 100755 --- a/oath/src/main/java/com/yubico/yubikit/oath/package-info.java +++ b/oath/src/main/java/com/yubico/yubikit/oath/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.oath; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/oath/src/test/java/com/yubico/yubikit/oath/Base32Test.java b/oath/src/test/java/com/yubico/yubikit/oath/Base32Test.java index 1640f5a5..61c90d79 100644 --- a/oath/src/test/java/com/yubico/yubikit/oath/Base32Test.java +++ b/oath/src/test/java/com/yubico/yubikit/oath/Base32Test.java @@ -23,140 +23,153 @@ import static org.junit.Assert.fail; import com.yubico.yubikit.testing.Codec; - import org.junit.Test; @SuppressWarnings("SpellCheckingInspection") public class Base32Test { - @Test - public void testValidInput() { - assertTrue(Base32.isValid("")); - assertTrue(Base32.isValid("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567")); - assertTrue(Base32.isValid("AA======")); - assertTrue(Base32.isValid("MZXQ====")); - assertTrue(Base32.isValid("AA")); - assertTrue(Base32.isValid("AAAA")); - assertTrue(Base32.isValid("AAAAA")); - assertTrue(Base32.isValid("AAAAAAA")); + @Test + public void testValidInput() { + assertTrue(Base32.isValid("")); + assertTrue(Base32.isValid("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567")); + assertTrue(Base32.isValid("AA======")); + assertTrue(Base32.isValid("MZXQ====")); + assertTrue(Base32.isValid("AA")); + assertTrue(Base32.isValid("AAAA")); + assertTrue(Base32.isValid("AAAAA")); + assertTrue(Base32.isValid("AAAAAAA")); + } + + @Test + public void testInvalidInput() { + assertFalse(Base32.isValid("0189")); + assertFalse(Base32.isValid(";.*")); + assertFalse(Base32.isValid("😀")); + assertFalse(Base32.isValid("abcdefghijklmnopqrstuvwxyz234567")); + assertFalse(Base32.isValid("AA=")); + assertFalse(Base32.isValid("AA==")); + assertFalse(Base32.isValid("AA===")); + assertFalse(Base32.isValid("AA====")); + assertFalse(Base32.isValid("AA=====")); + assertFalse(Base32.isValid("AA=======")); + assertFalse(Base32.isValid("A")); + assertFalse(Base32.isValid("AAA")); + assertFalse(Base32.isValid("AAAAAA")); + assertFalse(Base32.isValid("=")); + assertFalse(Base32.isValid("==")); + assertFalse(Base32.isValid("===")); + assertFalse(Base32.isValid("AAAAAAA=A")); + assertFalse(Base32.isValid("MZ=XW6YTB")); + assertFalse(Base32.isValid("MZXQ==")); + } + + @Test + public void testEncode() { + assertEquals("", Base32.encode("".getBytes())); + assertEquals("MY======", Base32.encode("f".getBytes())); + assertEquals("MZXQ====", Base32.encode("fo".getBytes())); + assertEquals("MZXW6===", Base32.encode("foo".getBytes())); + assertEquals("MZXW6YQ=", Base32.encode("foob".getBytes())); + assertEquals("MZXW6YTB", Base32.encode("fooba".getBytes())); + assertEquals("MZXW6YTBOI======", Base32.encode("foobar".getBytes())); + assertEquals( + "PF2WE2LLNF2C243ENMQDELSYFYYCAIBAEE======", + Base32.encode("yubikit-sdk 2.X.0 !".getBytes())); + assertEquals( + "KRUGKIDROVUWG2ZAMJZG653OEBTG66BANJ2W24DTEBXXMZLSEB2GQZJANRQXU6JAMRXWOLQ=", + Base32.encode("The quick brown fox jumps over the lazy dog.".getBytes())); + assertEquals("WZEBZC7I6IQXTNMK", Base32.encode(Codec.fromHex("b6481c8be8f22179b58a"))); + assertEquals( + "NG3EQHELVORLMDUPEILZWWGNKY======", + Base32.encode(Codec.fromHex("69b6481c8baba2b60e8f22179b58cd56"))); + } + + @Test + public void testDecode() { + assertArrayEquals("f".getBytes(), Base32.decode("MY======")); + assertArrayEquals("fo".getBytes(), Base32.decode("MZXQ====")); + assertArrayEquals("foo".getBytes(), Base32.decode("MZXW6===")); + assertArrayEquals("foob".getBytes(), Base32.decode("MZXW6YQ=")); + assertArrayEquals("fooba".getBytes(), Base32.decode("MZXW6YTB")); + assertArrayEquals("foobar".getBytes(), Base32.decode("MZXW6YTBOI======")); + assertArrayEquals( + "yubikit-sdk 2.X.0 !".getBytes(), + Base32.decode("PF2WE2LLNF2C243ENMQDELSYFYYCAIBAEE======")); + assertArrayEquals( + "The quick brown fox jumps over the lazy dog.".getBytes(), + Base32.decode("KRUGKIDROVUWG2ZAMJZG653OEBTG66BANJ2W24DTEBXXMZLSEB2GQZJANRQXU6JAMRXWOLQ=")); + assertArrayEquals( + Codec.fromHex("69b6481c8baba2b60e8f22179b58cd56"), + Base32.decode("NG3EQHELVORLMDUPEILZWWGNKY======")); + } + + @Test + public void testDecodeWithoutPadding() { + assertArrayEquals("".getBytes(), Base32.decode("")); + assertArrayEquals("f".getBytes(), Base32.decode("MY")); + assertArrayEquals("fo".getBytes(), Base32.decode("MZXQ")); + assertArrayEquals("foo".getBytes(), Base32.decode("MZXW6")); + assertArrayEquals("foob".getBytes(), Base32.decode("MZXW6YQ")); + assertArrayEquals("fooba".getBytes(), Base32.decode("MZXW6YTB")); + assertArrayEquals("foobar".getBytes(), Base32.decode("MZXW6YTBOI")); + assertArrayEquals( + "yubikit-sdk 2.X.0 !".getBytes(), Base32.decode("PF2WE2LLNF2C243ENMQDELSYFYYCAIBAEE")); + assertArrayEquals( + "The quick brown fox jumps over the lazy dog.".getBytes(), + Base32.decode("KRUGKIDROVUWG2ZAMJZG653OEBTG66BANJ2W24DTEBXXMZLSEB2GQZJANRQXU6JAMRXWOLQ")); + assertArrayEquals(Codec.fromHex("b6481c8be8f22179b58a"), Base32.decode("WZEBZC7I6IQXTNMK")); + } + + @Test + public void testDecodeThrows() { + byte[] invalid = new byte[] {0}; + + try { + assertArrayEquals(invalid, Base32.decode("invalidinput")); + fail(); + } catch (IllegalArgumentException ignored) { + } + + try { + assertArrayEquals(invalid, Base32.decode("M=")); + fail(); + } catch (IllegalArgumentException ignored) { + } + try { + assertArrayEquals(invalid, Base32.decode("A=A")); + fail(); + } catch (IllegalArgumentException ignored) { } - @Test - public void testInvalidInput() { - assertFalse(Base32.isValid("0189")); - assertFalse(Base32.isValid(";.*")); - assertFalse(Base32.isValid("😀")); - assertFalse(Base32.isValid("abcdefghijklmnopqrstuvwxyz234567")); - assertFalse(Base32.isValid("AA=")); - assertFalse(Base32.isValid("AA==")); - assertFalse(Base32.isValid("AA===")); - assertFalse(Base32.isValid("AA====")); - assertFalse(Base32.isValid("AA=====")); - assertFalse(Base32.isValid("AA=======")); - assertFalse(Base32.isValid("A")); - assertFalse(Base32.isValid("AAA")); - assertFalse(Base32.isValid("AAAAAA")); - assertFalse(Base32.isValid("=")); - assertFalse(Base32.isValid("==")); - assertFalse(Base32.isValid("===")); - assertFalse(Base32.isValid("AAAAAAA=A")); - assertFalse(Base32.isValid("MZ=XW6YTB")); - assertFalse(Base32.isValid("MZXQ==")); + try { + assertArrayEquals(invalid, Base32.decode("=")); + fail(); + } catch (IllegalArgumentException ignored) { } - @Test - public void testEncode() { - assertEquals("", Base32.encode("".getBytes())); - assertEquals("MY======", Base32.encode("f".getBytes())); - assertEquals("MZXQ====", Base32.encode("fo".getBytes())); - assertEquals("MZXW6===", Base32.encode("foo".getBytes())); - assertEquals("MZXW6YQ=", Base32.encode("foob".getBytes())); - assertEquals("MZXW6YTB", Base32.encode("fooba".getBytes())); - assertEquals("MZXW6YTBOI======", Base32.encode("foobar".getBytes())); - assertEquals("PF2WE2LLNF2C243ENMQDELSYFYYCAIBAEE======", Base32.encode("yubikit-sdk 2.X.0 !".getBytes())); - assertEquals("KRUGKIDROVUWG2ZAMJZG653OEBTG66BANJ2W24DTEBXXMZLSEB2GQZJANRQXU6JAMRXWOLQ=", Base32.encode("The quick brown fox jumps over the lazy dog.".getBytes())); - assertEquals("WZEBZC7I6IQXTNMK", Base32.encode(Codec.fromHex("b6481c8be8f22179b58a"))); - assertEquals("NG3EQHELVORLMDUPEILZWWGNKY======", Base32.encode(Codec.fromHex("69b6481c8baba2b60e8f22179b58cd56"))); + try { + assertArrayEquals(invalid, Base32.decode("MZXQ==")); + fail(); + } catch (IllegalArgumentException ignored) { } - @Test - public void testDecode() { - assertArrayEquals("f".getBytes(), Base32.decode("MY======")); - assertArrayEquals("fo".getBytes(), Base32.decode("MZXQ====")); - assertArrayEquals("foo".getBytes(), Base32.decode("MZXW6===")); - assertArrayEquals("foob".getBytes(), Base32.decode("MZXW6YQ=")); - assertArrayEquals("fooba".getBytes(), Base32.decode("MZXW6YTB")); - assertArrayEquals("foobar".getBytes(), Base32.decode("MZXW6YTBOI======")); - assertArrayEquals("yubikit-sdk 2.X.0 !".getBytes(), Base32.decode("PF2WE2LLNF2C243ENMQDELSYFYYCAIBAEE======")); - assertArrayEquals("The quick brown fox jumps over the lazy dog.".getBytes(), Base32.decode("KRUGKIDROVUWG2ZAMJZG653OEBTG66BANJ2W24DTEBXXMZLSEB2GQZJANRQXU6JAMRXWOLQ=")); - assertArrayEquals(Codec.fromHex("69b6481c8baba2b60e8f22179b58cd56"), Base32.decode("NG3EQHELVORLMDUPEILZWWGNKY======")); + try { + assertArrayEquals(invalid, Base32.decode("A")); + fail(); + } catch (IllegalArgumentException ignored) { } - @Test - public void testDecodeWithoutPadding() { - assertArrayEquals("".getBytes(), Base32.decode("")); - assertArrayEquals("f".getBytes(), Base32.decode("MY")); - assertArrayEquals("fo".getBytes(), Base32.decode("MZXQ")); - assertArrayEquals("foo".getBytes(), Base32.decode("MZXW6")); - assertArrayEquals("foob".getBytes(), Base32.decode("MZXW6YQ")); - assertArrayEquals("fooba".getBytes(), Base32.decode("MZXW6YTB")); - assertArrayEquals("foobar".getBytes(), Base32.decode("MZXW6YTBOI")); - assertArrayEquals("yubikit-sdk 2.X.0 !".getBytes(), Base32.decode("PF2WE2LLNF2C243ENMQDELSYFYYCAIBAEE")); - assertArrayEquals("The quick brown fox jumps over the lazy dog.".getBytes(), Base32.decode("KRUGKIDROVUWG2ZAMJZG653OEBTG66BANJ2W24DTEBXXMZLSEB2GQZJANRQXU6JAMRXWOLQ")); - assertArrayEquals(Codec.fromHex("b6481c8be8f22179b58a"), Base32.decode("WZEBZC7I6IQXTNMK")); + try { + assertArrayEquals(invalid, Base32.decode("AAA")); + fail(); + } catch (IllegalArgumentException ignored) { } - @Test - public void testDecodeThrows() { - byte[] invalid = new byte[]{0}; - - try { - assertArrayEquals(invalid, Base32.decode("invalidinput")); - fail(); - } catch (IllegalArgumentException ignored) { - } - - try { - assertArrayEquals(invalid, Base32.decode("M=")); - fail(); - } catch (IllegalArgumentException ignored) { - } - - try { - assertArrayEquals(invalid, Base32.decode("A=A")); - fail(); - } catch (IllegalArgumentException ignored) { - } - - try { - assertArrayEquals(invalid, Base32.decode("=")); - fail(); - } catch (IllegalArgumentException ignored) { - } - - try { - assertArrayEquals(invalid, Base32.decode("MZXQ==")); - fail(); - } catch (IllegalArgumentException ignored) { - } - - try { - assertArrayEquals(invalid, Base32.decode("A")); - fail(); - } catch (IllegalArgumentException ignored) { - } - - try { - assertArrayEquals(invalid, Base32.decode("AAA")); - fail(); - } catch (IllegalArgumentException ignored) { - } - - try { - assertArrayEquals(invalid, Base32.decode("AAAAAA")); - fail(); - } catch (IllegalArgumentException ignored) { - } + try { + assertArrayEquals(invalid, Base32.decode("AAAAAA")); + fail(); + } catch (IllegalArgumentException ignored) { } -} \ No newline at end of file + } +} diff --git a/oath/src/test/java/com/yubico/yubikit/oath/CredentialDataTest.java b/oath/src/test/java/com/yubico/yubikit/oath/CredentialDataTest.java index 60e41fa2..3f3b0ff7 100755 --- a/oath/src/test/java/com/yubico/yubikit/oath/CredentialDataTest.java +++ b/oath/src/test/java/com/yubico/yubikit/oath/CredentialDataTest.java @@ -16,65 +16,74 @@ package com.yubico.yubikit.oath; -import org.junit.Assert; -import org.junit.Test; - import java.net.URI; import java.net.URISyntaxException; +import org.junit.Assert; +import org.junit.Test; @SuppressWarnings("SpellCheckingInspection") public class CredentialDataTest { - @Test - public void testParseUriGood() throws ParseUriException, URISyntaxException { - Assert.assertArrayEquals( - new byte[]{0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21, (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef}, - CredentialData.parseUri(new URI("otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example")).getSecret() - ); - Assert.assertArrayEquals( - new byte[]{0x0a, (byte) 0xc0, 0x77, 0x34, (byte) 0xc0}, - CredentialData.parseUri(new URI("otpauth://hotp/foobar:bob@example.com?secret=blahonga")).getSecret() - ); - Assert.assertArrayEquals( - new byte[]{0x00, 0x42}, - CredentialData.parseUri(new URI("otpauth://totp/foobar:bob@example.com?secret=abba")).getSecret() - ); - } + @Test + public void testParseUriGood() throws ParseUriException, URISyntaxException { + Assert.assertArrayEquals( + new byte[] { + 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21, (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef + }, + CredentialData.parseUri( + new URI( + "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example")) + .getSecret()); + Assert.assertArrayEquals( + new byte[] {0x0a, (byte) 0xc0, 0x77, 0x34, (byte) 0xc0}, + CredentialData.parseUri(new URI("otpauth://hotp/foobar:bob@example.com?secret=blahonga")) + .getSecret()); + Assert.assertArrayEquals( + new byte[] {0x00, 0x42}, + CredentialData.parseUri(new URI("otpauth://totp/foobar:bob@example.com?secret=abba")) + .getSecret()); + } - @Test - public void testParseIssuer() throws ParseUriException, URISyntaxException { - CredentialData noIssuer = CredentialData.parseUri(new URI("otpauth://totp/account?secret=abba")); - Assert.assertNull(noIssuer.getIssuer()); - CredentialData usingParam = CredentialData.parseUri(new URI("otpauth://totp/account?secret=abba&issuer=Issuer")); - Assert.assertEquals(usingParam.getIssuer(), "Issuer"); - CredentialData usingSeparator = CredentialData.parseUri(new URI("otpauth://totp/Issuer:account?secret=abba")); - Assert.assertEquals(usingSeparator.getIssuer(), "Issuer"); - CredentialData usingBoth = CredentialData.parseUri(new URI("otpauth://totp/IssuerA:account?secret=abba&issuer=IssuerB")); - Assert.assertEquals(usingBoth.getIssuer(), "IssuerA"); - } + @Test + public void testParseIssuer() throws ParseUriException, URISyntaxException { + CredentialData noIssuer = + CredentialData.parseUri(new URI("otpauth://totp/account?secret=abba")); + Assert.assertNull(noIssuer.getIssuer()); + CredentialData usingParam = + CredentialData.parseUri(new URI("otpauth://totp/account?secret=abba&issuer=Issuer")); + Assert.assertEquals(usingParam.getIssuer(), "Issuer"); + CredentialData usingSeparator = + CredentialData.parseUri(new URI("otpauth://totp/Issuer:account?secret=abba")); + Assert.assertEquals(usingSeparator.getIssuer(), "Issuer"); + CredentialData usingBoth = + CredentialData.parseUri( + new URI("otpauth://totp/IssuerA:account?secret=abba&issuer=IssuerB")); + Assert.assertEquals(usingBoth.getIssuer(), "IssuerA"); + } - @Test(expected = ParseUriException.class) - public void testParseHttpUri() throws ParseUriException, URISyntaxException { - CredentialData.parseUri(new URI("http://example.com/")); - } + @Test(expected = ParseUriException.class) + public void testParseHttpUri() throws ParseUriException, URISyntaxException { + CredentialData.parseUri(new URI("http://example.com/")); + } - @Test(expected = ParseUriException.class) - public void testParseWrongPath() throws ParseUriException, URISyntaxException { - CredentialData.parseUri(new URI("otpauth://foobar?secret=kaka")); - } + @Test(expected = ParseUriException.class) + public void testParseWrongPath() throws ParseUriException, URISyntaxException { + CredentialData.parseUri(new URI("otpauth://foobar?secret=kaka")); + } - @Test(expected = ParseUriException.class) - public void testParseNonUri() throws ParseUriException, URISyntaxException { - CredentialData.parseUri(new URI("foobar")); - } + @Test(expected = ParseUriException.class) + public void testParseNonUri() throws ParseUriException, URISyntaxException { + CredentialData.parseUri(new URI("foobar")); + } - @Test(expected = ParseUriException.class) - public void testParseSecretNotBase32() throws ParseUriException, URISyntaxException { - CredentialData.parseUri(new URI("otpauth://totp/Example:alice@google.com?secret=balhonga1&issuer=Example")); - } + @Test(expected = ParseUriException.class) + public void testParseSecretNotBase32() throws ParseUriException, URISyntaxException { + CredentialData.parseUri( + new URI("otpauth://totp/Example:alice@google.com?secret=balhonga1&issuer=Example")); + } - @Test(expected = ParseUriException.class) - public void testParseMissingAlgorithm() throws ParseUriException, URISyntaxException { - CredentialData.parseUri(new URI("otpauth:///foo:mallory@example.com?secret=kaka")); - } + @Test(expected = ParseUriException.class) + public void testParseMissingAlgorithm() throws ParseUriException, URISyntaxException { + CredentialData.parseUri(new URI("otpauth:///foo:mallory@example.com?secret=kaka")); + } } diff --git a/oath/src/test/java/com/yubico/yubikit/oath/CredentialIdUtilsTest.java b/oath/src/test/java/com/yubico/yubikit/oath/CredentialIdUtilsTest.java index 8c961d9c..039b0d50 100755 --- a/oath/src/test/java/com/yubico/yubikit/oath/CredentialIdUtilsTest.java +++ b/oath/src/test/java/com/yubico/yubikit/oath/CredentialIdUtilsTest.java @@ -16,56 +16,71 @@ package com.yubico.yubikit.oath; +import java.nio.charset.StandardCharsets; import org.junit.Assert; import org.junit.Test; -import java.nio.charset.StandardCharsets; - public class CredentialIdUtilsTest { - @Test - public void testParseData() { - final String issuer = "issuer"; - String accountId = "20/issuer:account"; - CredentialIdUtils.CredentialIdData idData = CredentialIdUtils.parseId(accountId.getBytes(StandardCharsets.UTF_8), OathType.TOTP); - Assert.assertEquals(issuer, idData.issuer); - Assert.assertEquals(20, idData.period); - byte[] credentialId = CredentialIdUtils.formatId(idData.issuer, idData.accountName, OathType.TOTP, idData.period); - Assert.assertEquals(accountId, new String(credentialId, StandardCharsets.UTF_8)); - - String accountWithSlash = "this/account"; - idData = CredentialIdUtils.parseId(accountWithSlash.getBytes(StandardCharsets.UTF_8), OathType.TOTP); - Assert.assertNull(idData.issuer); - Assert.assertEquals(accountWithSlash, idData.accountName); - credentialId = CredentialIdUtils.formatId(idData.issuer, idData.accountName, OathType.TOTP, idData.period); - Assert.assertEquals(accountWithSlash, new String(credentialId, StandardCharsets.UTF_8)); + @Test + public void testParseData() { + final String issuer = "issuer"; + String accountId = "20/issuer:account"; + CredentialIdUtils.CredentialIdData idData = + CredentialIdUtils.parseId(accountId.getBytes(StandardCharsets.UTF_8), OathType.TOTP); + Assert.assertEquals(issuer, idData.issuer); + Assert.assertEquals(20, idData.period); + byte[] credentialId = + CredentialIdUtils.formatId(idData.issuer, idData.accountName, OathType.TOTP, idData.period); + Assert.assertEquals(accountId, new String(credentialId, StandardCharsets.UTF_8)); - String accountWithSlashAndIssuer = "issuer:this/account"; - idData = CredentialIdUtils.parseId(accountWithSlashAndIssuer.getBytes(StandardCharsets.UTF_8), OathType.TOTP); - Assert.assertEquals(issuer, idData.issuer); - Assert.assertEquals(accountWithSlash, idData.accountName); - credentialId = CredentialIdUtils.formatId(idData.issuer, idData.accountName, OathType.TOTP, idData.period); - Assert.assertEquals(accountWithSlashAndIssuer, new String(credentialId, StandardCharsets.UTF_8)); + String accountWithSlash = "this/account"; + idData = + CredentialIdUtils.parseId(accountWithSlash.getBytes(StandardCharsets.UTF_8), OathType.TOTP); + Assert.assertNull(idData.issuer); + Assert.assertEquals(accountWithSlash, idData.accountName); + credentialId = + CredentialIdUtils.formatId(idData.issuer, idData.accountName, OathType.TOTP, idData.period); + Assert.assertEquals(accountWithSlash, new String(credentialId, StandardCharsets.UTF_8)); - String issuerAndAccountWithSlash = "this/issuer:this/account"; - idData = CredentialIdUtils.parseId(issuerAndAccountWithSlash.getBytes(StandardCharsets.UTF_8), OathType.TOTP); - Assert.assertEquals("this/issuer", idData.issuer); - Assert.assertEquals(accountWithSlash, idData.accountName); - credentialId = CredentialIdUtils.formatId(idData.issuer, idData.accountName, OathType.TOTP, idData.period); - Assert.assertEquals(issuerAndAccountWithSlash, new String(credentialId, StandardCharsets.UTF_8)); + String accountWithSlashAndIssuer = "issuer:this/account"; + idData = + CredentialIdUtils.parseId( + accountWithSlashAndIssuer.getBytes(StandardCharsets.UTF_8), OathType.TOTP); + Assert.assertEquals(issuer, idData.issuer); + Assert.assertEquals(accountWithSlash, idData.accountName); + credentialId = + CredentialIdUtils.formatId(idData.issuer, idData.accountName, OathType.TOTP, idData.period); + Assert.assertEquals( + accountWithSlashAndIssuer, new String(credentialId, StandardCharsets.UTF_8)); + String issuerAndAccountWithSlash = "this/issuer:this/account"; + idData = + CredentialIdUtils.parseId( + issuerAndAccountWithSlash.getBytes(StandardCharsets.UTF_8), OathType.TOTP); + Assert.assertEquals("this/issuer", idData.issuer); + Assert.assertEquals(accountWithSlash, idData.accountName); + credentialId = + CredentialIdUtils.formatId(idData.issuer, idData.accountName, OathType.TOTP, idData.period); + Assert.assertEquals( + issuerAndAccountWithSlash, new String(credentialId, StandardCharsets.UTF_8)); - String accountWithSlashAndColon = "issuer:this:account"; - idData = CredentialIdUtils.parseId(accountWithSlashAndColon.getBytes(StandardCharsets.UTF_8), OathType.TOTP); - Assert.assertEquals(issuer, idData.issuer); - Assert.assertEquals("this:account", idData.accountName); - credentialId = CredentialIdUtils.formatId(idData.issuer, idData.accountName, OathType.TOTP, idData.period); - Assert.assertEquals(accountWithSlashAndColon, new String(credentialId, StandardCharsets.UTF_8)); + String accountWithSlashAndColon = "issuer:this:account"; + idData = + CredentialIdUtils.parseId( + accountWithSlashAndColon.getBytes(StandardCharsets.UTF_8), OathType.TOTP); + Assert.assertEquals(issuer, idData.issuer); + Assert.assertEquals("this:account", idData.accountName); + credentialId = + CredentialIdUtils.formatId(idData.issuer, idData.accountName, OathType.TOTP, idData.period); + Assert.assertEquals(accountWithSlashAndColon, new String(credentialId, StandardCharsets.UTF_8)); - String hotpWithPeriod = "20/HOTP:example"; - idData = CredentialIdUtils.parseId(hotpWithPeriod.getBytes(StandardCharsets.UTF_8), OathType.HOTP); - Assert.assertEquals("20/HOTP", idData.issuer); - Assert.assertEquals("example", idData.accountName); - credentialId = CredentialIdUtils.formatId(idData.issuer, idData.accountName, OathType.HOTP, idData.period); - Assert.assertEquals(hotpWithPeriod, new String(credentialId, StandardCharsets.UTF_8)); - } + String hotpWithPeriod = "20/HOTP:example"; + idData = + CredentialIdUtils.parseId(hotpWithPeriod.getBytes(StandardCharsets.UTF_8), OathType.HOTP); + Assert.assertEquals("20/HOTP", idData.issuer); + Assert.assertEquals("example", idData.accountName); + credentialId = + CredentialIdUtils.formatId(idData.issuer, idData.accountName, OathType.HOTP, idData.period); + Assert.assertEquals(hotpWithPeriod, new String(credentialId, StandardCharsets.UTF_8)); + } } diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/AlgorithmAttributes.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/AlgorithmAttributes.java index 5d9c21d4..368fa831 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/AlgorithmAttributes.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/AlgorithmAttributes.java @@ -21,220 +21,222 @@ import java.util.Objects; public abstract class AlgorithmAttributes { - private final byte algorithmId; - - AlgorithmAttributes(byte algorithmId) { - this.algorithmId = algorithmId; + private final byte algorithmId; + + AlgorithmAttributes(byte algorithmId) { + this.algorithmId = algorithmId; + } + + byte getAlgorithmId() { + return algorithmId; + } + + abstract byte[] getBytes(); + + static AlgorithmAttributes parse(byte[] encoded) { + ByteBuffer buf = ByteBuffer.wrap(encoded); + byte algorithmId = buf.get(); + switch (algorithmId) { + case 1: + return Rsa.parse(algorithmId, buf); + case 0x12: + case 0x13: + case 0x16: + return Ec.parse(algorithmId, buf); + default: + throw new IllegalArgumentException("Unsupported algorithm ID"); } - - byte getAlgorithmId() { - return algorithmId; + } + + static class Rsa extends AlgorithmAttributes { + enum ImportFormat { + STANDARD((byte) 0), + STANDARD_W_MOD((byte) 1), + CRT((byte) 2), + CRT_W_MOD((byte) 3); + public final byte value; + + ImportFormat(byte value) { + this.value = value; + } + + static ImportFormat fromValue(int value) { + for (ImportFormat type : ImportFormat.values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Not a valid ImportFormat:" + value); + } } - abstract byte[] getBytes(); + private final int nLen; + private final int eLen; + private final ImportFormat importFormat; - static AlgorithmAttributes parse(byte[] encoded) { - ByteBuffer buf = ByteBuffer.wrap(encoded); - byte algorithmId = buf.get(); - switch (algorithmId) { - case 1: - return Rsa.parse(algorithmId, buf); - case 0x12: - case 0x13: - case 0x16: - return Ec.parse(algorithmId, buf); - default: - throw new IllegalArgumentException("Unsupported algorithm ID"); - } + Rsa(byte algorithmId, int nLen, int eLen, ImportFormat importFormat) { + super(algorithmId); + this.nLen = nLen; + this.eLen = eLen; + this.importFormat = importFormat; } - static class Rsa extends AlgorithmAttributes { - enum ImportFormat { - STANDARD((byte) 0), - STANDARD_W_MOD((byte) 1), - CRT((byte) 2), - CRT_W_MOD((byte) 3); - public final byte value; - - ImportFormat(byte value) { - this.value = value; - } - - static ImportFormat fromValue(int value) { - for (ImportFormat type : ImportFormat.values()) { - if (type.value == value) { - return type; - } - } - throw new IllegalArgumentException("Not a valid ImportFormat:" + value); - } - } - - private final int nLen; - private final int eLen; - private final ImportFormat importFormat; - - Rsa(byte algorithmId, int nLen, int eLen, ImportFormat importFormat) { - super(algorithmId); - this.nLen = nLen; - this.eLen = eLen; - this.importFormat = importFormat; - } - - int getNLen() { - return nLen; - } - - int getELen() { - return eLen; - } + int getNLen() { + return nLen; + } - @Override - byte[] getBytes() { - return ByteBuffer.allocate(6) - .put(getAlgorithmId()) - .putShort((short) nLen) - .putShort((short) eLen) - .put(importFormat.value) - .array(); - } + int getELen() { + return eLen; + } - static Rsa parse(byte algorithmId, ByteBuffer buf) { - return new Rsa( - algorithmId, - 0xffff & buf.getShort(), - 0xffff & buf.getShort(), - ImportFormat.fromValue(buf.get()) - ); - } + @Override + byte[] getBytes() { + return ByteBuffer.allocate(6) + .put(getAlgorithmId()) + .putShort((short) nLen) + .putShort((short) eLen) + .put(importFormat.value) + .array(); + } - static Rsa create(int nLen, ImportFormat importFormat) { - return new Rsa((byte) 1, nLen, 17, importFormat); - } + static Rsa parse(byte algorithmId, ByteBuffer buf) { + return new Rsa( + algorithmId, + 0xffff & buf.getShort(), + 0xffff & buf.getShort(), + ImportFormat.fromValue(buf.get())); + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Rsa that = (Rsa) o; - return nLen == that.nLen && eLen == that.eLen && importFormat == that.importFormat; - } + static Rsa create(int nLen, ImportFormat importFormat) { + return new Rsa((byte) 1, nLen, 17, importFormat); + } - @Override - public int hashCode() { - return Objects.hash(nLen, eLen, importFormat); - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Rsa that = (Rsa) o; + return nLen == that.nLen && eLen == that.eLen && importFormat == that.importFormat; + } - @Override - public String toString() { - return "Rsa{" + - "algorithmId=" + getAlgorithmId() + - ", nLen=" + nLen + - ", eLen=" + eLen + - ", importFormat=" + importFormat + - '}'; - } + @Override + public int hashCode() { + return Objects.hash(nLen, eLen, importFormat); } - static class Ec extends AlgorithmAttributes { - enum ImportFormat { - STANDARD((byte) 0), - STANDARD_W_PUBKEY((byte) 0xff); - public final byte value; - - ImportFormat(byte value) { - this.value = value; - } - - static ImportFormat fromValue(int value) { - for (ImportFormat type : ImportFormat.values()) { - if (type.value == value) { - return type; - } - } - throw new IllegalArgumentException("Not a valid ImportFormat:" + value); - } - } + @Override + public String toString() { + return "Rsa{" + + "algorithmId=" + + getAlgorithmId() + + ", nLen=" + + nLen + + ", eLen=" + + eLen + + ", importFormat=" + + importFormat + + '}'; + } + } + + static class Ec extends AlgorithmAttributes { + enum ImportFormat { + STANDARD((byte) 0), + STANDARD_W_PUBKEY((byte) 0xff); + public final byte value; + + ImportFormat(byte value) { + this.value = value; + } + + static ImportFormat fromValue(int value) { + for (ImportFormat type : ImportFormat.values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Not a valid ImportFormat:" + value); + } + } - private final OpenPgpCurve curve; - private final ImportFormat importFormat; + private final OpenPgpCurve curve; + private final ImportFormat importFormat; - Ec(byte algorithmId, OpenPgpCurve curve, ImportFormat importFormat) { - super(algorithmId); - this.curve = curve; - this.importFormat = importFormat; - } + Ec(byte algorithmId, OpenPgpCurve curve, ImportFormat importFormat) { + super(algorithmId); + this.curve = curve; + this.importFormat = importFormat; + } - OpenPgpCurve getCurve() { - return curve; - } + OpenPgpCurve getCurve() { + return curve; + } - ImportFormat getImportFormat() { - return importFormat; - } + ImportFormat getImportFormat() { + return importFormat; + } - @Override - byte[] getBytes() { - byte[] oidBytes = curve.getOid(); - byte[] bytes = ByteBuffer.allocate(1 + oidBytes.length) - .put(getAlgorithmId()) - .put(oidBytes) - .array(); - if (importFormat == ImportFormat.STANDARD_W_PUBKEY) { - bytes = Arrays.copyOf(bytes, bytes.length + 1); - bytes[bytes.length - 1] = importFormat.value; - } - return bytes; - } + @Override + byte[] getBytes() { + byte[] oidBytes = curve.getOid(); + byte[] bytes = + ByteBuffer.allocate(1 + oidBytes.length).put(getAlgorithmId()).put(oidBytes).array(); + if (importFormat == ImportFormat.STANDARD_W_PUBKEY) { + bytes = Arrays.copyOf(bytes, bytes.length + 1); + bytes[bytes.length - 1] = importFormat.value; + } + return bytes; + } - static Ec parse(byte algorithmId, ByteBuffer buf) { - if (buf.get(buf.remaining()) == ImportFormat.STANDARD_W_PUBKEY.value) { - return new Ec( - algorithmId, - OpenPgpCurve.fromOid(Arrays.copyOfRange(buf.array(), buf.position(), buf.limit() - 1)), - ImportFormat.STANDARD_W_PUBKEY - ); - } - // Standard is defined as "format byte not present" - return new Ec( - algorithmId, - OpenPgpCurve.fromOid(Arrays.copyOfRange(buf.array(), buf.position(), buf.limit())), - ImportFormat.STANDARD - ); - } + static Ec parse(byte algorithmId, ByteBuffer buf) { + if (buf.get(buf.remaining()) == ImportFormat.STANDARD_W_PUBKEY.value) { + return new Ec( + algorithmId, + OpenPgpCurve.fromOid(Arrays.copyOfRange(buf.array(), buf.position(), buf.limit() - 1)), + ImportFormat.STANDARD_W_PUBKEY); + } + // Standard is defined as "format byte not present" + return new Ec( + algorithmId, + OpenPgpCurve.fromOid(Arrays.copyOfRange(buf.array(), buf.position(), buf.limit())), + ImportFormat.STANDARD); + } - static Ec create(KeyRef keyRef, OpenPgpCurve curve) { - byte algId; - if (curve == OpenPgpCurve.Ed25519) { - algId = 0x16; // EdDSA - } else if (keyRef == KeyRef.DEC) { - algId = 0x12; // ECDH - } else { - algId = 0x13; // ECDSA - } - return new Ec(algId, curve, ImportFormat.STANDARD); - } + static Ec create(KeyRef keyRef, OpenPgpCurve curve) { + byte algId; + if (curve == OpenPgpCurve.Ed25519) { + algId = 0x16; // EdDSA + } else if (keyRef == KeyRef.DEC) { + algId = 0x12; // ECDH + } else { + algId = 0x13; // ECDSA + } + return new Ec(algId, curve, ImportFormat.STANDARD); + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Ec that = (Ec) o; - return curve == that.curve && importFormat == that.importFormat; - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Ec that = (Ec) o; + return curve == that.curve && importFormat == that.importFormat; + } - @Override - public int hashCode() { - return Objects.hash(curve, importFormat); - } + @Override + public int hashCode() { + return Objects.hash(curve, importFormat); + } - @Override - public String toString() { - return "Ec{" + - "algorithmId=" + getAlgorithmId() + - ", curve=" + curve + - ", importFormat=" + importFormat + - '}'; - } + @Override + public String toString() { + return "Ec{" + + "algorithmId=" + + getAlgorithmId() + + ", curve=" + + curve + + ", importFormat=" + + importFormat + + '}'; } + } } diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/ApplicationRelatedData.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/ApplicationRelatedData.java index 9e09f1f8..3fba070b 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/ApplicationRelatedData.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/ApplicationRelatedData.java @@ -18,91 +18,82 @@ import com.yubico.yubikit.core.application.BadResponseException; import com.yubico.yubikit.core.util.Tlvs; - import java.util.Arrays; import java.util.EnumSet; import java.util.HashSet; import java.util.Map; import java.util.Set; - import javax.annotation.Nullable; public class ApplicationRelatedData { - static private final int TAG_DISCRETIONARY = 0x73; - private final OpenPgpAid aid; - private final byte[] historical; - @Nullable - private final ExtendedLengthInfo extendedLengthInfo; - @Nullable - private final EnumSet generalFeatureManagement; - private final DiscretionaryDataObjects discretionary; + private static final int TAG_DISCRETIONARY = 0x73; + private final OpenPgpAid aid; + private final byte[] historical; + @Nullable private final ExtendedLengthInfo extendedLengthInfo; + @Nullable private final EnumSet generalFeatureManagement; + private final DiscretionaryDataObjects discretionary; - public ApplicationRelatedData( - OpenPgpAid aid, - byte[] historical, - @Nullable ExtendedLengthInfo extendedLengthInfo, - @Nullable EnumSet generalFeatureManagement, - DiscretionaryDataObjects discretionary - ) { - this.aid = aid; - this.historical = historical; - this.extendedLengthInfo = extendedLengthInfo; - this.generalFeatureManagement = generalFeatureManagement; - this.discretionary = discretionary; - } + public ApplicationRelatedData( + OpenPgpAid aid, + byte[] historical, + @Nullable ExtendedLengthInfo extendedLengthInfo, + @Nullable EnumSet generalFeatureManagement, + DiscretionaryDataObjects discretionary) { + this.aid = aid; + this.historical = historical; + this.extendedLengthInfo = extendedLengthInfo; + this.generalFeatureManagement = generalFeatureManagement; + this.discretionary = discretionary; + } - public OpenPgpAid getAid() { - return aid; - } + public OpenPgpAid getAid() { + return aid; + } - public byte[] getHistorical() { - return Arrays.copyOf(historical, historical.length); - } + public byte[] getHistorical() { + return Arrays.copyOf(historical, historical.length); + } - @Nullable - public ExtendedLengthInfo getExtendedLengthInfo() { - return extendedLengthInfo; - } + @Nullable public ExtendedLengthInfo getExtendedLengthInfo() { + return extendedLengthInfo; + } - @Nullable - public EnumSet getGeneralFeatureManagement() { - return generalFeatureManagement; - } + @Nullable public EnumSet getGeneralFeatureManagement() { + return generalFeatureManagement; + } - public DiscretionaryDataObjects getDiscretionary() { - return discretionary; - } + public DiscretionaryDataObjects getDiscretionary() { + return discretionary; + } - static ApplicationRelatedData parse(byte[] encoded) { - try { - byte[] outer = Tlvs.unpackValue(Do.APPLICATION_RELATED_DATA, encoded); - Map data = Tlvs.decodeMap(outer); - EnumSet generalFeatureManagement = null; - if (data.containsKey(Do.GENERAL_FEATURE_MANAGEMENT)) { - byte flags = Tlvs.unpackValue(0x81, data.get(Do.GENERAL_FEATURE_MANAGEMENT))[0]; - Set flagSet = new HashSet<>(); - for (GeneralFeatureManagement flag : GeneralFeatureManagement.values()) { - if ((flag.value & flags) != 0) { - flagSet.add(flag); - } - } - generalFeatureManagement = EnumSet.copyOf(flagSet); - } - byte[] discretionary = data.get(TAG_DISCRETIONARY); - return new ApplicationRelatedData( - new OpenPgpAid(data.get(Do.AID)), - data.get(Do.HISTORICAL_BYTES), - data.containsKey(Do.EXTENDED_LENGTH_INFO) - ? ExtendedLengthInfo.parse(data.get(Do.EXTENDED_LENGTH_INFO)) - : null, - generalFeatureManagement, - DiscretionaryDataObjects.parse( - // Older keys have data in outer dict - discretionary.length > 0 ? discretionary : outer - ) - ); - } catch (BadResponseException e) { - throw new IllegalArgumentException(e); + static ApplicationRelatedData parse(byte[] encoded) { + try { + byte[] outer = Tlvs.unpackValue(Do.APPLICATION_RELATED_DATA, encoded); + Map data = Tlvs.decodeMap(outer); + EnumSet generalFeatureManagement = null; + if (data.containsKey(Do.GENERAL_FEATURE_MANAGEMENT)) { + byte flags = Tlvs.unpackValue(0x81, data.get(Do.GENERAL_FEATURE_MANAGEMENT))[0]; + Set flagSet = new HashSet<>(); + for (GeneralFeatureManagement flag : GeneralFeatureManagement.values()) { + if ((flag.value & flags) != 0) { + flagSet.add(flag); + } } + generalFeatureManagement = EnumSet.copyOf(flagSet); + } + byte[] discretionary = data.get(TAG_DISCRETIONARY); + return new ApplicationRelatedData( + new OpenPgpAid(data.get(Do.AID)), + data.get(Do.HISTORICAL_BYTES), + data.containsKey(Do.EXTENDED_LENGTH_INFO) + ? ExtendedLengthInfo.parse(data.get(Do.EXTENDED_LENGTH_INFO)) + : null, + generalFeatureManagement, + DiscretionaryDataObjects.parse( + // Older keys have data in outer dict + discretionary.length > 0 ? discretionary : outer)); + } catch (BadResponseException e) { + throw new IllegalArgumentException(e); } + } } diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/CardholderRelatedData.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/CardholderRelatedData.java index 6f30c85d..0f124dcd 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/CardholderRelatedData.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/CardholderRelatedData.java @@ -17,39 +17,35 @@ package com.yubico.yubikit.openpgp; import com.yubico.yubikit.core.util.Tlvs; - import java.util.Arrays; import java.util.Map; public class CardholderRelatedData { - private final byte[] name; - private final byte[] language; - private final int sex; - - public byte[] getName() { - return Arrays.copyOf(name, name.length); - } - - public byte[] getLanguage() { - return Arrays.copyOf(language, language.length); - } - - public int getSex() { - return sex; - } - - CardholderRelatedData(byte[] name, byte[] language, int sex) { - this.name = name; - this.language = language; - this.sex = sex; - } - - static CardholderRelatedData parse(byte[] encoded) { - Map data = Tlvs.decodeMap(encoded); - return new CardholderRelatedData( - data.get(Do.NAME), - data.get(Do.LANGUAGE), - 0xff & data.get(Do.SEX)[0] - ); - } + private final byte[] name; + private final byte[] language; + private final int sex; + + public byte[] getName() { + return Arrays.copyOf(name, name.length); + } + + public byte[] getLanguage() { + return Arrays.copyOf(language, language.length); + } + + public int getSex() { + return sex; + } + + CardholderRelatedData(byte[] name, byte[] language, int sex) { + this.name = name; + this.language = language; + this.sex = sex; + } + + static CardholderRelatedData parse(byte[] encoded) { + Map data = Tlvs.decodeMap(encoded); + return new CardholderRelatedData( + data.get(Do.NAME), data.get(Do.LANGUAGE), 0xff & data.get(Do.SEX)[0]); + } } diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/Crt.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/Crt.java index c204c84e..8f32a79b 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/Crt.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/Crt.java @@ -19,8 +19,9 @@ import com.yubico.yubikit.core.util.Tlv; class Crt { - static final byte[] SIG = new Tlv(0xb6, null).getBytes(); - static final byte[] DEC = new Tlv(0xb8, null).getBytes(); - static final byte[] AUT = new Tlv(0xa4, null).getBytes(); - static final byte[] ATT = new Tlv(0xb6, new Tlv(0x84, new byte[]{(byte) 0x81}).getBytes()).getBytes(); + static final byte[] SIG = new Tlv(0xb6, null).getBytes(); + static final byte[] DEC = new Tlv(0xb8, null).getBytes(); + static final byte[] AUT = new Tlv(0xa4, null).getBytes(); + static final byte[] ATT = + new Tlv(0xb6, new Tlv(0x84, new byte[] {(byte) 0x81}).getBytes()).getBytes(); } diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/DiscretionaryDataObjects.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/DiscretionaryDataObjects.java index d3f4ea1e..0f1f440f 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/DiscretionaryDataObjects.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/DiscretionaryDataObjects.java @@ -17,197 +17,182 @@ package com.yubico.yubikit.openpgp; import com.yubico.yubikit.core.util.Tlvs; - import java.nio.ByteBuffer; import java.util.Arrays; import java.util.HashMap; import java.util.Map; - import javax.annotation.Nullable; public class DiscretionaryDataObjects { - static private final int TAG_EXTENDED_CAPABILITIES = 0xC0; - static private final int TAG_FINGERPRINTS = 0xC5; - static private final int TAG_CA_FINGERPRINTS = 0xC6; - static private final int TAG_GENERATION_TIMES = 0xCD; - static private final int TAG_KEY_INFORMATION = 0xDE; - private final ExtendedCapabilities extendedCapabilities; - - private final AlgorithmAttributes attributesSig; - private final AlgorithmAttributes attributesDec; - private final AlgorithmAttributes attributesAut; - @Nullable - private final AlgorithmAttributes attributesAtt; - private final PwStatus pwStatus; - private final Map fingerprints; - private final Map caFingerprints; - private final Map generationTimes; - private final Map keyInformation; - @Nullable - private final Uif uifSig; - @Nullable - private final Uif uifDec; - @Nullable - private final Uif uifAut; - @Nullable - private final Uif uifAtt; - - public DiscretionaryDataObjects( - ExtendedCapabilities extendedCapabilities, - AlgorithmAttributes attributesSig, - AlgorithmAttributes attributesDec, - AlgorithmAttributes attributesAut, - @Nullable AlgorithmAttributes attributesAtt, - PwStatus pwStatus, - Map fingerprints, - Map caFingerprints, - Map generationTimes, - Map keyInformation, - @Nullable Uif uifSig, - @Nullable Uif uifDec, - @Nullable Uif uifAut, - @Nullable Uif uifAtt - ) { - this.extendedCapabilities = extendedCapabilities; - this.attributesSig = attributesSig; - this.attributesDec = attributesDec; - this.attributesAut = attributesAut; - this.attributesAtt = attributesAtt; - this.pwStatus = pwStatus; - this.fingerprints = fingerprints; - this.caFingerprints = caFingerprints; - this.generationTimes = generationTimes; - this.keyInformation = keyInformation; - this.uifSig = uifSig; - this.uifDec = uifDec; - this.uifAut = uifAut; - this.uifAtt = uifAtt; - } - - public ExtendedCapabilities getExtendedCapabilities() { - return extendedCapabilities; - } - - public PwStatus getPwStatus() { - return pwStatus; - } - - @Nullable - public AlgorithmAttributes getAlgorithmAttributes(KeyRef keyRef) { - switch (keyRef) { - case SIG: - return attributesSig; - case DEC: - return attributesDec; - case AUT: - return attributesAut; - case ATT: - return attributesAtt; - default: - throw new IllegalStateException(); - } + private static final int TAG_EXTENDED_CAPABILITIES = 0xC0; + private static final int TAG_FINGERPRINTS = 0xC5; + private static final int TAG_CA_FINGERPRINTS = 0xC6; + private static final int TAG_GENERATION_TIMES = 0xCD; + private static final int TAG_KEY_INFORMATION = 0xDE; + private final ExtendedCapabilities extendedCapabilities; + + private final AlgorithmAttributes attributesSig; + private final AlgorithmAttributes attributesDec; + private final AlgorithmAttributes attributesAut; + @Nullable private final AlgorithmAttributes attributesAtt; + private final PwStatus pwStatus; + private final Map fingerprints; + private final Map caFingerprints; + private final Map generationTimes; + private final Map keyInformation; + @Nullable private final Uif uifSig; + @Nullable private final Uif uifDec; + @Nullable private final Uif uifAut; + @Nullable private final Uif uifAtt; + + public DiscretionaryDataObjects( + ExtendedCapabilities extendedCapabilities, + AlgorithmAttributes attributesSig, + AlgorithmAttributes attributesDec, + AlgorithmAttributes attributesAut, + @Nullable AlgorithmAttributes attributesAtt, + PwStatus pwStatus, + Map fingerprints, + Map caFingerprints, + Map generationTimes, + Map keyInformation, + @Nullable Uif uifSig, + @Nullable Uif uifDec, + @Nullable Uif uifAut, + @Nullable Uif uifAtt) { + this.extendedCapabilities = extendedCapabilities; + this.attributesSig = attributesSig; + this.attributesDec = attributesDec; + this.attributesAut = attributesAut; + this.attributesAtt = attributesAtt; + this.pwStatus = pwStatus; + this.fingerprints = fingerprints; + this.caFingerprints = caFingerprints; + this.generationTimes = generationTimes; + this.keyInformation = keyInformation; + this.uifSig = uifSig; + this.uifDec = uifDec; + this.uifAut = uifAut; + this.uifAtt = uifAtt; + } + + public ExtendedCapabilities getExtendedCapabilities() { + return extendedCapabilities; + } + + public PwStatus getPwStatus() { + return pwStatus; + } + + @Nullable public AlgorithmAttributes getAlgorithmAttributes(KeyRef keyRef) { + switch (keyRef) { + case SIG: + return attributesSig; + case DEC: + return attributesDec; + case AUT: + return attributesAut; + case ATT: + return attributesAtt; + default: + throw new IllegalStateException(); } + } - @Nullable - public byte[] getFingerprint(KeyRef keyRef) { - byte[] fingerprint = fingerprints.get(keyRef); - if (fingerprint != null) { - return Arrays.copyOf(fingerprint, fingerprint.length); - } - return null; + @Nullable public byte[] getFingerprint(KeyRef keyRef) { + byte[] fingerprint = fingerprints.get(keyRef); + if (fingerprint != null) { + return Arrays.copyOf(fingerprint, fingerprint.length); } + return null; + } - @Nullable - public byte[] getCaFingerprint(KeyRef keyRef) { - byte[] fingerprint = caFingerprints.get(keyRef); - if (fingerprint != null) { - return Arrays.copyOf(fingerprint, fingerprint.length); - } - return null; + @Nullable public byte[] getCaFingerprint(KeyRef keyRef) { + byte[] fingerprint = caFingerprints.get(keyRef); + if (fingerprint != null) { + return Arrays.copyOf(fingerprint, fingerprint.length); } + return null; + } - public int getGenerationTime(KeyRef keyRef) { - Integer time = generationTimes.get(keyRef); - if (time != null) { - return time; - } - return -1; + public int getGenerationTime(KeyRef keyRef) { + Integer time = generationTimes.get(keyRef); + if (time != null) { + return time; } - - @Nullable - public KeyStatus getKeyStatus(KeyRef keyRef) { - return keyInformation.get(keyRef); - } - - @Nullable - public Uif getUif(KeyRef keyRef) { - switch (keyRef) { - case SIG: - return uifSig; - case DEC: - return uifDec; - case AUT: - return uifAut; - case ATT: - return uifAtt; - default: - throw new IllegalStateException(); - } - } - - private static Map parseFingerprints(byte[] encoded) { - KeyRef[] refs = KeyRef.values(); - Map fingerprints = new HashMap<>(); - ByteBuffer buf = ByteBuffer.wrap(encoded); - byte[] fingerprint = new byte[20]; - for (int i = 0; buf.remaining() > 0; i++) { - buf.get(fingerprint); - fingerprints.put(refs[i], fingerprint); - } - return fingerprints; + return -1; + } + + @Nullable public KeyStatus getKeyStatus(KeyRef keyRef) { + return keyInformation.get(keyRef); + } + + @Nullable public Uif getUif(KeyRef keyRef) { + switch (keyRef) { + case SIG: + return uifSig; + case DEC: + return uifDec; + case AUT: + return uifAut; + case ATT: + return uifAtt; + default: + throw new IllegalStateException(); } - - private static Map parseTimestamps(byte[] encoded) { - KeyRef[] refs = KeyRef.values(); - Map timestamps = new HashMap<>(); - ByteBuffer buf = ByteBuffer.wrap(encoded); - for (int i = 0; buf.remaining() > 0; i++) { - timestamps.put(refs[i], buf.getInt()); - } - return timestamps; + } + + private static Map parseFingerprints(byte[] encoded) { + KeyRef[] refs = KeyRef.values(); + Map fingerprints = new HashMap<>(); + ByteBuffer buf = ByteBuffer.wrap(encoded); + byte[] fingerprint = new byte[20]; + for (int i = 0; buf.remaining() > 0; i++) { + buf.get(fingerprint); + fingerprints.put(refs[i], fingerprint); } - - private static Map parseKeyInformation(byte[] encoded) { - Map statuses = new HashMap<>(); - ByteBuffer buf = ByteBuffer.wrap(encoded); - for (int i = 0; buf.remaining() > 0; i++) { - statuses.put(KeyRef.fromValue(buf.get()), KeyStatus.fromValue(buf.get())); - } - return statuses; + return fingerprints; + } + + private static Map parseTimestamps(byte[] encoded) { + KeyRef[] refs = KeyRef.values(); + Map timestamps = new HashMap<>(); + ByteBuffer buf = ByteBuffer.wrap(encoded); + for (int i = 0; buf.remaining() > 0; i++) { + timestamps.put(refs[i], buf.getInt()); } - - static DiscretionaryDataObjects parse(byte[] encoded) { - Map data = Tlvs.decodeMap(encoded); - - return new DiscretionaryDataObjects( - ExtendedCapabilities.parse(data.get(TAG_EXTENDED_CAPABILITIES)), - AlgorithmAttributes.parse(data.get(Do.ALGORITHM_ATTRIBUTES_SIG)), - AlgorithmAttributes.parse(data.get(Do.ALGORITHM_ATTRIBUTES_DEC)), - AlgorithmAttributes.parse(data.get(Do.ALGORITHM_ATTRIBUTES_AUT)), - data.containsKey(Do.ALGORITHM_ATTRIBUTES_ATT) - ? AlgorithmAttributes.parse(data.get(Do.ALGORITHM_ATTRIBUTES_ATT)) - : null, - PwStatus.parse(data.get(Do.PW_STATUS_BYTES)), - parseFingerprints(data.get(TAG_FINGERPRINTS)), - parseFingerprints(data.get(TAG_CA_FINGERPRINTS)), - parseTimestamps(data.get(TAG_GENERATION_TIMES)), - parseKeyInformation(data.containsKey(TAG_KEY_INFORMATION) - ? data.get(TAG_KEY_INFORMATION) - : new byte[0]), - data.containsKey(Do.UIF_SIG) ? Uif.fromValue(data.get(Do.UIF_SIG)[0]) : null, - data.containsKey(Do.UIF_DEC) ? Uif.fromValue(data.get(Do.UIF_DEC)[0]) : null, - data.containsKey(Do.UIF_AUT) ? Uif.fromValue(data.get(Do.UIF_AUT)[0]) : null, - data.containsKey(Do.UIF_ATT) ? Uif.fromValue(data.get(Do.UIF_ATT)[0]) : null - ); + return timestamps; + } + + private static Map parseKeyInformation(byte[] encoded) { + Map statuses = new HashMap<>(); + ByteBuffer buf = ByteBuffer.wrap(encoded); + for (int i = 0; buf.remaining() > 0; i++) { + statuses.put(KeyRef.fromValue(buf.get()), KeyStatus.fromValue(buf.get())); } -} \ No newline at end of file + return statuses; + } + + static DiscretionaryDataObjects parse(byte[] encoded) { + Map data = Tlvs.decodeMap(encoded); + + return new DiscretionaryDataObjects( + ExtendedCapabilities.parse(data.get(TAG_EXTENDED_CAPABILITIES)), + AlgorithmAttributes.parse(data.get(Do.ALGORITHM_ATTRIBUTES_SIG)), + AlgorithmAttributes.parse(data.get(Do.ALGORITHM_ATTRIBUTES_DEC)), + AlgorithmAttributes.parse(data.get(Do.ALGORITHM_ATTRIBUTES_AUT)), + data.containsKey(Do.ALGORITHM_ATTRIBUTES_ATT) + ? AlgorithmAttributes.parse(data.get(Do.ALGORITHM_ATTRIBUTES_ATT)) + : null, + PwStatus.parse(data.get(Do.PW_STATUS_BYTES)), + parseFingerprints(data.get(TAG_FINGERPRINTS)), + parseFingerprints(data.get(TAG_CA_FINGERPRINTS)), + parseTimestamps(data.get(TAG_GENERATION_TIMES)), + parseKeyInformation( + data.containsKey(TAG_KEY_INFORMATION) ? data.get(TAG_KEY_INFORMATION) : new byte[0]), + data.containsKey(Do.UIF_SIG) ? Uif.fromValue(data.get(Do.UIF_SIG)[0]) : null, + data.containsKey(Do.UIF_DEC) ? Uif.fromValue(data.get(Do.UIF_DEC)[0]) : null, + data.containsKey(Do.UIF_AUT) ? Uif.fromValue(data.get(Do.UIF_AUT)[0]) : null, + data.containsKey(Do.UIF_ATT) ? Uif.fromValue(data.get(Do.UIF_ATT)[0]) : null); + } +} diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/Do.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/Do.java index 12a57cd3..6c4eff5a 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/Do.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/Do.java @@ -17,46 +17,46 @@ package com.yubico.yubikit.openpgp; public class Do { - public static final int PRIVATE_USE_1 = 0x0101; - public static final int PRIVATE_USE_2 = 0x0102; - public static final int PRIVATE_USE_3 = 0x0103; - public static final int PRIVATE_USE_4 = 0x0104; - public static final int AID = 0x4F; - public static final int NAME = 0x5B; - public static final int LOGIN_DATA = 0x5E; - public static final int LANGUAGE = 0xEF2D; - public static final int SEX = 0x5F35; - public static final int URL = 0x5F50; - public static final int HISTORICAL_BYTES = 0x5F52; - public static final int EXTENDED_LENGTH_INFO = 0x7F66; - public static final int GENERAL_FEATURE_MANAGEMENT = 0x7F74; - public static final int CARDHOLDER_RELATED_DATA = 0x65; - public static final int APPLICATION_RELATED_DATA = 0x6E; - public static final int ALGORITHM_ATTRIBUTES_SIG = 0xC1; - public static final int ALGORITHM_ATTRIBUTES_DEC = 0xC2; - public static final int ALGORITHM_ATTRIBUTES_AUT = 0xC3; - public static final int ALGORITHM_ATTRIBUTES_ATT = 0xDA; - public static final int PW_STATUS_BYTES = 0xC4; - public static final int FINGERPRINT_SIG = 0xC7; - public static final int FINGERPRINT_DEC = 0xC8; - public static final int FINGERPRINT_AUT = 0xC9; - public static final int FINGERPRINT_ATT = 0xDB; - public static final int CA_FINGERPRINT_1 = 0xCA; - public static final int CA_FINGERPRINT_2 = 0xCB; - public static final int CA_FINGERPRINT_3 = 0xCC; - public static final int CA_FINGERPRINT_4 = 0xDC; - public static final int GENERATION_TIME_SIG = 0xCE; - public static final int GENERATION_TIME_DEC = 0xCF; - public static final int GENERATION_TIME_AUT = 0xD0; - public static final int GENERATION_TIME_ATT = 0xDD; - public static final int RESETTING_CODE = 0xD3; - public static final int UIF_SIG = 0xD6; - public static final int UIF_DEC = 0xD7; - public static final int UIF_AUT = 0xD8; - public static final int UIF_ATT = 0xD9; - public static final int SECURITY_SUPPORT_TEMPLATE = 0x7A; - public static final int CARDHOLDER_CERTIFICATE = 0x7F21; - public static final int KDF = 0xF9; - public static final int ALGORITHM_INFORMATION = 0xFA; - public static final int ATT_CERTIFICATE = 0xFC; + public static final int PRIVATE_USE_1 = 0x0101; + public static final int PRIVATE_USE_2 = 0x0102; + public static final int PRIVATE_USE_3 = 0x0103; + public static final int PRIVATE_USE_4 = 0x0104; + public static final int AID = 0x4F; + public static final int NAME = 0x5B; + public static final int LOGIN_DATA = 0x5E; + public static final int LANGUAGE = 0xEF2D; + public static final int SEX = 0x5F35; + public static final int URL = 0x5F50; + public static final int HISTORICAL_BYTES = 0x5F52; + public static final int EXTENDED_LENGTH_INFO = 0x7F66; + public static final int GENERAL_FEATURE_MANAGEMENT = 0x7F74; + public static final int CARDHOLDER_RELATED_DATA = 0x65; + public static final int APPLICATION_RELATED_DATA = 0x6E; + public static final int ALGORITHM_ATTRIBUTES_SIG = 0xC1; + public static final int ALGORITHM_ATTRIBUTES_DEC = 0xC2; + public static final int ALGORITHM_ATTRIBUTES_AUT = 0xC3; + public static final int ALGORITHM_ATTRIBUTES_ATT = 0xDA; + public static final int PW_STATUS_BYTES = 0xC4; + public static final int FINGERPRINT_SIG = 0xC7; + public static final int FINGERPRINT_DEC = 0xC8; + public static final int FINGERPRINT_AUT = 0xC9; + public static final int FINGERPRINT_ATT = 0xDB; + public static final int CA_FINGERPRINT_1 = 0xCA; + public static final int CA_FINGERPRINT_2 = 0xCB; + public static final int CA_FINGERPRINT_3 = 0xCC; + public static final int CA_FINGERPRINT_4 = 0xDC; + public static final int GENERATION_TIME_SIG = 0xCE; + public static final int GENERATION_TIME_DEC = 0xCF; + public static final int GENERATION_TIME_AUT = 0xD0; + public static final int GENERATION_TIME_ATT = 0xDD; + public static final int RESETTING_CODE = 0xD3; + public static final int UIF_SIG = 0xD6; + public static final int UIF_DEC = 0xD7; + public static final int UIF_AUT = 0xD8; + public static final int UIF_ATT = 0xD9; + public static final int SECURITY_SUPPORT_TEMPLATE = 0x7A; + public static final int CARDHOLDER_CERTIFICATE = 0x7F21; + public static final int KDF = 0xF9; + public static final int ALGORITHM_INFORMATION = 0xFA; + public static final int ATT_CERTIFICATE = 0xFC; } diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/ExtendedCapabilities.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/ExtendedCapabilities.java index 0c8722c3..a94caf5c 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/ExtendedCapabilities.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/ExtendedCapabilities.java @@ -22,77 +22,75 @@ import java.util.Set; public class ExtendedCapabilities { - private final EnumSet flags; - private final int smAlgorithm; - private final int challengeMaxLength; - private final int certificateMaxLength; - private final int specialDoMaxLength; - private final boolean pinBlock2Format; - private final boolean mseCommand; + private final EnumSet flags; + private final int smAlgorithm; + private final int challengeMaxLength; + private final int certificateMaxLength; + private final int specialDoMaxLength; + private final boolean pinBlock2Format; + private final boolean mseCommand; - public ExtendedCapabilities( - EnumSet flags, - int smAlgorithm, - int challengeMaxLength, - int certificateMaxLength, - int specialDoMaxLength, - boolean pinBlock2Format, - boolean mseCommand - ) { - this.flags = flags; - this.smAlgorithm = smAlgorithm; - this.challengeMaxLength = challengeMaxLength; - this.certificateMaxLength = certificateMaxLength; - this.specialDoMaxLength = specialDoMaxLength; - this.pinBlock2Format = pinBlock2Format; - this.mseCommand = mseCommand; - } + public ExtendedCapabilities( + EnumSet flags, + int smAlgorithm, + int challengeMaxLength, + int certificateMaxLength, + int specialDoMaxLength, + boolean pinBlock2Format, + boolean mseCommand) { + this.flags = flags; + this.smAlgorithm = smAlgorithm; + this.challengeMaxLength = challengeMaxLength; + this.certificateMaxLength = certificateMaxLength; + this.specialDoMaxLength = specialDoMaxLength; + this.pinBlock2Format = pinBlock2Format; + this.mseCommand = mseCommand; + } - public EnumSet getFlags() { - return flags; - } + public EnumSet getFlags() { + return flags; + } - public int getSmAlgorithm() { - return smAlgorithm; - } + public int getSmAlgorithm() { + return smAlgorithm; + } - public int getChallengeMaxLength() { - return challengeMaxLength; - } + public int getChallengeMaxLength() { + return challengeMaxLength; + } - public int getCertificateMaxLength() { - return certificateMaxLength; - } + public int getCertificateMaxLength() { + return certificateMaxLength; + } - public int getSpecialDoMaxLength() { - return specialDoMaxLength; - } + public int getSpecialDoMaxLength() { + return specialDoMaxLength; + } - public boolean isPinBlock2Format() { - return pinBlock2Format; - } + public boolean isPinBlock2Format() { + return pinBlock2Format; + } - public boolean isMseCommand() { - return mseCommand; - } + public boolean isMseCommand() { + return mseCommand; + } - static ExtendedCapabilities parse(byte[] encoded) { - ByteBuffer buf = ByteBuffer.wrap(encoded); - byte flags = buf.get(); - Set flagSet = new HashSet<>(); - for (ExtendedCapabilityFlag flag : ExtendedCapabilityFlag.values()) { - if ((flag.value & flags) != 0) { - flagSet.add(flag); - } - } - return new ExtendedCapabilities( - EnumSet.copyOf(flagSet), - 0xffff & buf.get(), - 0xffff & buf.getShort(), - 0xffff & buf.getShort(), - 0xffff & buf.getShort(), - buf.get() == 1, - buf.get() == 1 - ); + static ExtendedCapabilities parse(byte[] encoded) { + ByteBuffer buf = ByteBuffer.wrap(encoded); + byte flags = buf.get(); + Set flagSet = new HashSet<>(); + for (ExtendedCapabilityFlag flag : ExtendedCapabilityFlag.values()) { + if ((flag.value & flags) != 0) { + flagSet.add(flag); + } } + return new ExtendedCapabilities( + EnumSet.copyOf(flagSet), + 0xffff & buf.get(), + 0xffff & buf.getShort(), + 0xffff & buf.getShort(), + 0xffff & buf.getShort(), + buf.get() == 1, + buf.get() == 1); + } } diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/ExtendedCapabilityFlag.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/ExtendedCapabilityFlag.java index a6853e26..5e4c4bf6 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/ExtendedCapabilityFlag.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/ExtendedCapabilityFlag.java @@ -17,18 +17,18 @@ package com.yubico.yubikit.openpgp; public enum ExtendedCapabilityFlag { - KDF((byte) 1), - PSO_DEC_ENC_AES((byte) (1 << 1)), - ALGORITHM_ATTRIBUTES_CHANGEABLE((byte) (1 << 2)), - PRIVATE_USE((byte) (1 << 3)), - PW_STATUS_CHANGEABLE((byte) (1 << 4)), - KEY_IMPORT((byte) (1 << 5)), - GET_CHALLENGE((byte) (1 << 6)), - SECURE_MESSAGING((byte) (1 << 7)); + KDF((byte) 1), + PSO_DEC_ENC_AES((byte) (1 << 1)), + ALGORITHM_ATTRIBUTES_CHANGEABLE((byte) (1 << 2)), + PRIVATE_USE((byte) (1 << 3)), + PW_STATUS_CHANGEABLE((byte) (1 << 4)), + KEY_IMPORT((byte) (1 << 5)), + GET_CHALLENGE((byte) (1 << 6)), + SECURE_MESSAGING((byte) (1 << 7)); - public final byte value; + public final byte value; - ExtendedCapabilityFlag(byte value) { - this.value = value; - } + ExtendedCapabilityFlag(byte value) { + this.value = value; + } } diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/ExtendedLengthInfo.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/ExtendedLengthInfo.java index 9d11777f..482c83ed 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/ExtendedLengthInfo.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/ExtendedLengthInfo.java @@ -19,36 +19,34 @@ import com.yubico.yubikit.core.application.BadResponseException; import com.yubico.yubikit.core.util.Tlv; import com.yubico.yubikit.core.util.Tlvs; - import java.nio.ByteBuffer; import java.util.List; public class ExtendedLengthInfo { - private final int requestMaxBytes; - private final int responseMaxBytes; - - ExtendedLengthInfo(int requestMaxBytes, int responseMaxBytes) { - this.requestMaxBytes = requestMaxBytes; - this.responseMaxBytes = responseMaxBytes; - } - - public int getRequestMaxBytes() { - return requestMaxBytes; - } - - public int getResponseMaxBytes() { - return responseMaxBytes; - } - - static ExtendedLengthInfo parse(byte[] encoded) { - List tlvs = Tlvs.decodeList(encoded); - try { - return new ExtendedLengthInfo( - 0xffff & ByteBuffer.wrap(Tlvs.unpackValue(0x02, tlvs.get(0).getBytes())).getShort(), - 0xffff & ByteBuffer.wrap(Tlvs.unpackValue(0x02, tlvs.get(1).getBytes())).getShort() - ); - } catch (BadResponseException e) { - throw new IllegalArgumentException(e); - } + private final int requestMaxBytes; + private final int responseMaxBytes; + + ExtendedLengthInfo(int requestMaxBytes, int responseMaxBytes) { + this.requestMaxBytes = requestMaxBytes; + this.responseMaxBytes = responseMaxBytes; + } + + public int getRequestMaxBytes() { + return requestMaxBytes; + } + + public int getResponseMaxBytes() { + return responseMaxBytes; + } + + static ExtendedLengthInfo parse(byte[] encoded) { + List tlvs = Tlvs.decodeList(encoded); + try { + return new ExtendedLengthInfo( + 0xffff & ByteBuffer.wrap(Tlvs.unpackValue(0x02, tlvs.get(0).getBytes())).getShort(), + 0xffff & ByteBuffer.wrap(Tlvs.unpackValue(0x02, tlvs.get(1).getBytes())).getShort()); + } catch (BadResponseException e) { + throw new IllegalArgumentException(e); } + } } diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/GeneralFeatureManagement.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/GeneralFeatureManagement.java index 69b49ad5..0407bd7d 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/GeneralFeatureManagement.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/GeneralFeatureManagement.java @@ -17,18 +17,18 @@ package com.yubico.yubikit.openpgp; public enum GeneralFeatureManagement { - TOUCHSCREEN((byte)1), - MICROPHONE((byte)(1 << 1)), - LOUDSPEAKER((byte)(1 << 2)), - LED((byte)(1 << 3)), - KEYPAD((byte)(1 << 4)), - BUTTON((byte)(1 << 5)), - BIOMETRIC((byte)(1 << 6)), - DISPLAY((byte)(1 << 7)); + TOUCHSCREEN((byte) 1), + MICROPHONE((byte) (1 << 1)), + LOUDSPEAKER((byte) (1 << 2)), + LED((byte) (1 << 3)), + KEYPAD((byte) (1 << 4)), + BUTTON((byte) (1 << 5)), + BIOMETRIC((byte) (1 << 6)), + DISPLAY((byte) (1 << 7)); - public final byte value; + public final byte value; - GeneralFeatureManagement(byte value) { - this.value = value; - } + GeneralFeatureManagement(byte value) { + this.value = value; + } } diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/Kdf.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/Kdf.java index a91284eb..bd69d6e6 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/Kdf.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/Kdf.java @@ -19,7 +19,6 @@ import com.yubico.yubikit.core.util.RandomUtils; import com.yubico.yubikit.core.util.Tlv; import com.yubico.yubikit.core.util.Tlvs; - import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.CharBuffer; @@ -30,224 +29,214 @@ import java.util.Arrays; import java.util.List; import java.util.Map; - import javax.annotation.Nullable; public abstract class Kdf { - protected final byte algorithm; - - protected Kdf(byte algorithm) { - this.algorithm = algorithm; - } + protected final byte algorithm; - public byte getAlgorithm() { - return algorithm; - } + protected Kdf(byte algorithm) { + this.algorithm = algorithm; + } - abstract byte[] process(Pw pw, char[] pin); + public byte getAlgorithm() { + return algorithm; + } - abstract byte[] getBytes(); + abstract byte[] process(Pw pw, char[] pin); - public static Kdf parse(byte[] encoded) { - Map data = Tlvs.decodeMap(encoded); - byte algorithm = data.getOrDefault(0x81, new byte[]{0})[0]; - if (algorithm == 3) { - return IterSaltedS2k.parseData(data); - } - return new None(); + abstract byte[] getBytes(); + public static Kdf parse(byte[] encoded) { + Map data = Tlvs.decodeMap(encoded); + byte algorithm = data.getOrDefault(0x81, new byte[] {0})[0]; + if (algorithm == 3) { + return IterSaltedS2k.parseData(data); } + return new None(); + } - public static class None extends Kdf { - public None() { - super((byte) 0); - } + public static class None extends Kdf { + public None() { + super((byte) 0); + } - @Override - public byte[] process(Pw pw, char[] pin) { - return pinBytes(pin); - } + @Override + public byte[] process(Pw pw, char[] pin) { + return pinBytes(pin); + } - @Override - public byte[] getBytes() { - return new Tlv(0x81, new byte[]{algorithm}).getBytes(); - } + @Override + public byte[] getBytes() { + return new Tlv(0x81, new byte[] {algorithm}).getBytes(); } + } - public static class IterSaltedS2k extends Kdf { - public enum HashAlgorithm { - SHA256((byte) 0x08), - SHA512((byte) 0x0a); - private final byte value; - - HashAlgorithm(byte value) { - this.value = value; - } - - private MessageDigest getMessageDigest() { - try { - return MessageDigest.getInstance(name()); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } - - static HashAlgorithm forValue(byte value) { - for (HashAlgorithm alg : HashAlgorithm.values()) { - if (alg.value == value) { - return alg; - } - } - throw new IllegalArgumentException("Not a valid HashAlgorithm"); - } - } + public static class IterSaltedS2k extends Kdf { + public enum HashAlgorithm { + SHA256((byte) 0x08), + SHA512((byte) 0x0a); + private final byte value; - private final HashAlgorithm hashAlgorithm; - private final int iterationCount; - private final byte[] saltUser; - @Nullable - private final byte[] saltReset; - @Nullable - private final byte[] saltAdmin; - @Nullable - private final byte[] initialHashUser; - @Nullable - private final byte[] initialHashAdmin; - - public IterSaltedS2k( - HashAlgorithm hashAlgorithm, - int iterationCount, - byte[] saltUser, - @Nullable byte[] saltReset, - @Nullable byte[] saltAdmin, - @Nullable byte[] initialHashUser, - @Nullable byte[] initialHashAdmin - ) { - super((byte) 3); - this.hashAlgorithm = hashAlgorithm; - this.iterationCount = iterationCount; - this.saltUser = saltUser; - this.saltReset = saltReset; - this.saltAdmin = saltAdmin; - this.initialHashUser = initialHashUser; - this.initialHashAdmin = initialHashAdmin; - } + HashAlgorithm(byte value) { + this.value = value; + } - static IterSaltedS2k parseData(Map data) { - return new IterSaltedS2k( - HashAlgorithm.forValue(data.get(0x82)[0]), - new BigInteger(1, data.get(0x83)).intValue(), - data.get(0x84), - data.get(0x85), - data.get(0x86), - data.get(0x87), - data.get(0x88) - ); + private MessageDigest getMessageDigest() { + try { + return MessageDigest.getInstance(name()); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); } + } - private byte[] getSalt(Pw pw) { - switch (pw) { - case USER: - return saltUser; - case RESET: - return saltReset != null ? saltReset : saltUser; - case ADMIN: - return saltAdmin != null ? saltAdmin : saltUser; - default: - throw new IllegalArgumentException(); - } + static HashAlgorithm forValue(byte value) { + for (HashAlgorithm alg : HashAlgorithm.values()) { + if (alg.value == value) { + return alg; + } } + throw new IllegalArgumentException("Not a valid HashAlgorithm"); + } + } - private static byte[] doProcess(HashAlgorithm hashAlgorithm, int iterationCount, byte[] data) { - //"iterationCount" is actually the total number of bytes to pass to the digest. - int dataCount = iterationCount / data.length; - int trailingBytes = iterationCount % data.length; - MessageDigest md = hashAlgorithm.getMessageDigest(); - for (int i = 0; i < dataCount; i++) { - md.update(data); - } - md.update(data, 0, trailingBytes); - return md.digest(); - } + private final HashAlgorithm hashAlgorithm; + private final int iterationCount; + private final byte[] saltUser; + @Nullable private final byte[] saltReset; + @Nullable private final byte[] saltAdmin; + @Nullable private final byte[] initialHashUser; + @Nullable private final byte[] initialHashAdmin; + + public IterSaltedS2k( + HashAlgorithm hashAlgorithm, + int iterationCount, + byte[] saltUser, + @Nullable byte[] saltReset, + @Nullable byte[] saltAdmin, + @Nullable byte[] initialHashUser, + @Nullable byte[] initialHashAdmin) { + super((byte) 3); + this.hashAlgorithm = hashAlgorithm; + this.iterationCount = iterationCount; + this.saltUser = saltUser; + this.saltReset = saltReset; + this.saltAdmin = saltAdmin; + this.initialHashUser = initialHashUser; + this.initialHashAdmin = initialHashAdmin; + } - @Override - public byte[] process(Pw pw, char[] pin) { - byte[] pinBytes = null; - byte[] data = null; - try { - final byte[] salt = getSalt(pw); - pinBytes = pinBytes(pin); - data = ByteBuffer.allocate(salt.length + pinBytes.length) - .put(salt) - .put(pinBytes) - .array(); - return doProcess( - hashAlgorithm, - iterationCount, - data - ); - } finally { - if (pinBytes != null) { - Arrays.fill(pinBytes, (byte) 0); - } - if (data != null) { - Arrays.fill(data, (byte) 0); - } - } - } + static IterSaltedS2k parseData(Map data) { + return new IterSaltedS2k( + HashAlgorithm.forValue(data.get(0x82)[0]), + new BigInteger(1, data.get(0x83)).intValue(), + data.get(0x84), + data.get(0x85), + data.get(0x86), + data.get(0x87), + data.get(0x88)); + } - @Override - public byte[] getBytes() { - List tlvs = new ArrayList<>(); - tlvs.add(new Tlv(0x81, new byte[]{algorithm})); - tlvs.add(new Tlv(0x82, new byte[]{hashAlgorithm.value})); - tlvs.add(new Tlv(0x83, ByteBuffer.allocate(4).putInt(iterationCount).array())); - tlvs.add(new Tlv(0x84, saltUser)); - if (saltReset != null) { - tlvs.add(new Tlv(0x85, saltReset)); - } - if (saltAdmin != null) { - tlvs.add(new Tlv(0x86, saltAdmin)); - } - if (initialHashUser != null) { - tlvs.add(new Tlv(0x87, initialHashUser)); - } - if (initialHashAdmin != null) { - tlvs.add(new Tlv(0x88, initialHashAdmin)); - } - - return Tlvs.encodeList(tlvs); - } + private byte[] getSalt(Pw pw) { + switch (pw) { + case USER: + return saltUser; + case RESET: + return saltReset != null ? saltReset : saltUser; + case ADMIN: + return saltAdmin != null ? saltAdmin : saltUser; + default: + throw new IllegalArgumentException(); + } + } - public static IterSaltedS2k create(HashAlgorithm hashAlgorithm, int iterationCount) { - byte[] saltUser = RandomUtils.getRandomBytes(8); - byte[] saltAdmin = RandomUtils.getRandomBytes(8); - byte[] defaultUserPinEncoded = pinBytes(Pw.DEFAULT_USER_PIN); - byte[] defaultAdminPinEncoded = pinBytes(Pw.DEFAULT_ADMIN_PIN); - return new IterSaltedS2k( - hashAlgorithm, - iterationCount, - saltUser, - RandomUtils.getRandomBytes(8), - saltAdmin, - doProcess(hashAlgorithm, iterationCount, ByteBuffer.allocate(8 + defaultUserPinEncoded.length) - .put(saltUser) - .put(defaultUserPinEncoded) - .array()), - doProcess(hashAlgorithm, iterationCount, ByteBuffer.allocate(8 + defaultAdminPinEncoded.length) - .put(saltAdmin) - .put(defaultAdminPinEncoded) - .array()) - ); - } + private static byte[] doProcess(HashAlgorithm hashAlgorithm, int iterationCount, byte[] data) { + // "iterationCount" is actually the total number of bytes to pass to the digest. + int dataCount = iterationCount / data.length; + int trailingBytes = iterationCount % data.length; + MessageDigest md = hashAlgorithm.getMessageDigest(); + for (int i = 0; i < dataCount; i++) { + md.update(data); + } + md.update(data, 0, trailingBytes); + return md.digest(); } - private static byte[] pinBytes(char[] pin) { - ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(pin)); - try { - return Arrays.copyOf(byteBuffer.array(), byteBuffer.limit()); - } finally { - Arrays.fill(byteBuffer.array(), (byte) 0); + @Override + public byte[] process(Pw pw, char[] pin) { + byte[] pinBytes = null; + byte[] data = null; + try { + final byte[] salt = getSalt(pw); + pinBytes = pinBytes(pin); + data = ByteBuffer.allocate(salt.length + pinBytes.length).put(salt).put(pinBytes).array(); + return doProcess(hashAlgorithm, iterationCount, data); + } finally { + if (pinBytes != null) { + Arrays.fill(pinBytes, (byte) 0); + } + if (data != null) { + Arrays.fill(data, (byte) 0); } + } + } + + @Override + public byte[] getBytes() { + List tlvs = new ArrayList<>(); + tlvs.add(new Tlv(0x81, new byte[] {algorithm})); + tlvs.add(new Tlv(0x82, new byte[] {hashAlgorithm.value})); + tlvs.add(new Tlv(0x83, ByteBuffer.allocate(4).putInt(iterationCount).array())); + tlvs.add(new Tlv(0x84, saltUser)); + if (saltReset != null) { + tlvs.add(new Tlv(0x85, saltReset)); + } + if (saltAdmin != null) { + tlvs.add(new Tlv(0x86, saltAdmin)); + } + if (initialHashUser != null) { + tlvs.add(new Tlv(0x87, initialHashUser)); + } + if (initialHashAdmin != null) { + tlvs.add(new Tlv(0x88, initialHashAdmin)); + } + + return Tlvs.encodeList(tlvs); + } + + public static IterSaltedS2k create(HashAlgorithm hashAlgorithm, int iterationCount) { + byte[] saltUser = RandomUtils.getRandomBytes(8); + byte[] saltAdmin = RandomUtils.getRandomBytes(8); + byte[] defaultUserPinEncoded = pinBytes(Pw.DEFAULT_USER_PIN); + byte[] defaultAdminPinEncoded = pinBytes(Pw.DEFAULT_ADMIN_PIN); + return new IterSaltedS2k( + hashAlgorithm, + iterationCount, + saltUser, + RandomUtils.getRandomBytes(8), + saltAdmin, + doProcess( + hashAlgorithm, + iterationCount, + ByteBuffer.allocate(8 + defaultUserPinEncoded.length) + .put(saltUser) + .put(defaultUserPinEncoded) + .array()), + doProcess( + hashAlgorithm, + iterationCount, + ByteBuffer.allocate(8 + defaultAdminPinEncoded.length) + .put(saltAdmin) + .put(defaultAdminPinEncoded) + .array())); + } + } + + private static byte[] pinBytes(char[] pin) { + ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(pin)); + try { + return Arrays.copyOf(byteBuffer.array(), byteBuffer.limit()); + } finally { + Arrays.fill(byteBuffer.array(), (byte) 0); } + } } diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/KeyRef.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/KeyRef.java index a1053b7d..cb7e26d1 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/KeyRef.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/KeyRef.java @@ -19,56 +19,86 @@ import java.util.Arrays; public enum KeyRef { - SIG((byte) 0x01, Do.ALGORITHM_ATTRIBUTES_SIG, Do.UIF_SIG, Do.FINGERPRINT_SIG, Do.GENERATION_TIME_SIG, Crt.SIG), - DEC((byte) 0x02, Do.ALGORITHM_ATTRIBUTES_DEC, Do.UIF_DEC, Do.FINGERPRINT_DEC, Do.GENERATION_TIME_DEC, Crt.DEC), - AUT((byte) 0x03, Do.ALGORITHM_ATTRIBUTES_AUT, Do.UIF_AUT, Do.FINGERPRINT_AUT, Do.GENERATION_TIME_AUT, Crt.AUT), - ATT((byte) 0x81, Do.ALGORITHM_ATTRIBUTES_ATT, Do.UIF_ATT, Do.FINGERPRINT_ATT, Do.GENERATION_TIME_ATT, Crt.ATT); - private final byte value; - private final int algorithmAttributes; - private final int uif; - private final int fingerprint; - private final int generationTime; - private final byte[] crt; + SIG( + (byte) 0x01, + Do.ALGORITHM_ATTRIBUTES_SIG, + Do.UIF_SIG, + Do.FINGERPRINT_SIG, + Do.GENERATION_TIME_SIG, + Crt.SIG), + DEC( + (byte) 0x02, + Do.ALGORITHM_ATTRIBUTES_DEC, + Do.UIF_DEC, + Do.FINGERPRINT_DEC, + Do.GENERATION_TIME_DEC, + Crt.DEC), + AUT( + (byte) 0x03, + Do.ALGORITHM_ATTRIBUTES_AUT, + Do.UIF_AUT, + Do.FINGERPRINT_AUT, + Do.GENERATION_TIME_AUT, + Crt.AUT), + ATT( + (byte) 0x81, + Do.ALGORITHM_ATTRIBUTES_ATT, + Do.UIF_ATT, + Do.FINGERPRINT_ATT, + Do.GENERATION_TIME_ATT, + Crt.ATT); + private final byte value; + private final int algorithmAttributes; + private final int uif; + private final int fingerprint; + private final int generationTime; + private final byte[] crt; - KeyRef(byte value, int algorithmAttributes, int uif, int fingerprint, int generationTime, byte[] crt) { - this.value = value; - this.algorithmAttributes = algorithmAttributes; - this.uif = uif; - this.fingerprint = fingerprint; - this.generationTime = generationTime; - this.crt = crt; - } + KeyRef( + byte value, + int algorithmAttributes, + int uif, + int fingerprint, + int generationTime, + byte[] crt) { + this.value = value; + this.algorithmAttributes = algorithmAttributes; + this.uif = uif; + this.fingerprint = fingerprint; + this.generationTime = generationTime; + this.crt = crt; + } - public byte getValue() { - return value; - } + public byte getValue() { + return value; + } - public int getAlgorithmAttributes() { - return algorithmAttributes; - } + public int getAlgorithmAttributes() { + return algorithmAttributes; + } - public int getUif() { - return uif; - } + public int getUif() { + return uif; + } - public int getFingerprint() { - return fingerprint; - } + public int getFingerprint() { + return fingerprint; + } - public int getGenerationTime() { - return generationTime; - } + public int getGenerationTime() { + return generationTime; + } - public byte[] getCrt() { - return Arrays.copyOf(crt, crt.length); - } + public byte[] getCrt() { + return Arrays.copyOf(crt, crt.length); + } - static KeyRef fromValue(byte value) { - for (KeyRef status : KeyRef.values()) { - if (status.value == value) { - return status; - } - } - throw new IllegalArgumentException("Not a valid KeyRef:" + value); + static KeyRef fromValue(byte value) { + for (KeyRef status : KeyRef.values()) { + if (status.value == value) { + return status; + } } + throw new IllegalArgumentException("Not a valid KeyRef:" + value); + } } diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/KeyStatus.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/KeyStatus.java index 9862d25b..1a029c07 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/KeyStatus.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/KeyStatus.java @@ -17,21 +17,21 @@ package com.yubico.yubikit.openpgp; public enum KeyStatus { - NONE((byte) 0), - GENERATED((byte) 1), - IMPORTED((byte) 2); - public final byte value; + NONE((byte) 0), + GENERATED((byte) 1), + IMPORTED((byte) 2); + public final byte value; - KeyStatus(byte value) { - this.value = value; - } + KeyStatus(byte value) { + this.value = value; + } - static KeyStatus fromValue(byte value) { - for (KeyStatus status : KeyStatus.values()) { - if (status.value == value) { - return status; - } - } - throw new IllegalArgumentException("Not a valid KeyStatus:" + value); + static KeyStatus fromValue(byte value) { + for (KeyStatus status : KeyStatus.values()) { + if (status.value == value) { + return status; + } } -} \ No newline at end of file + throw new IllegalArgumentException("Not a valid KeyStatus:" + value); + } +} diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpAid.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpAid.java index 3e1cf8bf..f7937d73 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpAid.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpAid.java @@ -19,63 +19,60 @@ import static com.yubico.yubikit.openpgp.OpenPgpUtils.decodeBcd; import com.yubico.yubikit.core.util.Pair; - import java.nio.ByteBuffer; import java.util.Arrays; /** - * OpenPGP Application Identifier (AID) - * The OpenPGP AID is a string of bytes identifying the OpenPGP application. - * It also embeds some values which are accessible though properties. + * OpenPGP Application Identifier (AID) The OpenPGP AID is a string of bytes identifying the OpenPGP + * application. It also embeds some values which are accessible though properties. */ public class OpenPgpAid { - private final byte[] bytes; + private final byte[] bytes; - OpenPgpAid(byte[] bytes) { - this.bytes = bytes; - } + OpenPgpAid(byte[] bytes) { + this.bytes = bytes; + } - public byte[] getBytes() { - return Arrays.copyOf(bytes, bytes.length); - } + public byte[] getBytes() { + return Arrays.copyOf(bytes, bytes.length); + } - /** - * OpenPGP version (tuple of 2 integers: main version, secondary version). - * - * @return a Pair of main version, secondary version. - */ - public Pair getVersion() { - return new Pair<>(decodeBcd(bytes[6]), decodeBcd(bytes[7])); - } + /** + * OpenPGP version (tuple of 2 integers: main version, secondary version). + * + * @return a Pair of main version, secondary version. + */ + public Pair getVersion() { + return new Pair<>(decodeBcd(bytes[6]), decodeBcd(bytes[7])); + } - /** - * 16-bit integer value identifying the manufacturer of the device. - * This should be 6 for Yubico devices. - * - * @return OpenPGP card manufacturer ID. - */ - public short getManufacturer() { - return ByteBuffer.wrap(bytes).getShort(6); - } + /** + * 16-bit integer value identifying the manufacturer of the device. This should be 6 for Yubico + * devices. + * + * @return OpenPGP card manufacturer ID. + */ + public short getManufacturer() { + return ByteBuffer.wrap(bytes).getShort(6); + } - /** - * The serial number of the YubiKey. - *

- * NOTE: This value is encoded in BCD. In the event of an invalid value (hex A-F) - * the entire 4 byte value will instead be decoded as an unsigned integer, - * and negated. - * - * @return The serial number of the YubiKey - */ - public int getSerial() { - int serial = 0; - try { - for (int i = 0; i < 4; i++) { - serial = serial * 100 + decodeBcd(bytes[10 + i]); - } - return serial; - } catch (IllegalArgumentException e) { - return -ByteBuffer.wrap(bytes).getInt(10); - } + /** + * The serial number of the YubiKey. + * + *

NOTE: This value is encoded in BCD. In the event of an invalid value (hex A-F) the entire 4 + * byte value will instead be decoded as an unsigned integer, and negated. + * + * @return The serial number of the YubiKey + */ + public int getSerial() { + int serial = 0; + try { + for (int i = 0; i < 4; i++) { + serial = serial * 100 + decodeBcd(bytes[10 + i]); + } + return serial; + } catch (IllegalArgumentException e) { + return -ByteBuffer.wrap(bytes).getInt(10); } + } } diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpCurve.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpCurve.java index 38a376d9..3236f075 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpCurve.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpCurve.java @@ -17,44 +17,55 @@ package com.yubico.yubikit.openpgp; import com.yubico.yubikit.core.keys.EllipticCurveValues; - import java.util.Arrays; public enum OpenPgpCurve { - SECP256R1(EllipticCurveValues.SECP256R1, new byte[]{0x2a, (byte) 0x86, 0x48, (byte) 0xce, 0x3d, 0x03, 0x01, 0x07}), - SECP256K1(EllipticCurveValues.SECP256K1, new byte[]{0x2b, (byte) 0x81, 0x04, 0x00, 0x0a}), - SECP384R1(EllipticCurveValues.SECP384R1, new byte[]{0x2b, (byte) 0x81, 0x04, 0x00, 0x22}), - SECP521R1(EllipticCurveValues.SECP521R1, new byte[]{0x2b, (byte) 0x81, 0x04, 0x00, 0x23}), - BrainpoolP256R1(EllipticCurveValues.BrainpoolP256R1, new byte[]{0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x07}), - BrainpoolP384R1(EllipticCurveValues.BrainpoolP384R1, new byte[]{0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0b}), - BrainpoolP512R1(EllipticCurveValues.BrainpoolP512R1, new byte[]{0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0d}), - X25519(EllipticCurveValues.X25519, new byte[]{0x2b, 0x06, 0x01, 0x04, 0x01, (byte) 0x97, 0x55, 0x01, 0x05, 0x01}), - Ed25519(EllipticCurveValues.Ed25519, new byte[]{0x2b, 0x06, 0x01, 0x04, 0x01, (byte) 0xda, 0x47, 0x0f, 0x01}); - - private final EllipticCurveValues ellipticCurveValues; - private final byte[] oid; - - OpenPgpCurve(EllipticCurveValues ellipticCurveValues, byte[] oid) { - this.ellipticCurveValues = ellipticCurveValues; - this.oid = oid; - } + SECP256R1( + EllipticCurveValues.SECP256R1, + new byte[] {0x2a, (byte) 0x86, 0x48, (byte) 0xce, 0x3d, 0x03, 0x01, 0x07}), + SECP256K1(EllipticCurveValues.SECP256K1, new byte[] {0x2b, (byte) 0x81, 0x04, 0x00, 0x0a}), + SECP384R1(EllipticCurveValues.SECP384R1, new byte[] {0x2b, (byte) 0x81, 0x04, 0x00, 0x22}), + SECP521R1(EllipticCurveValues.SECP521R1, new byte[] {0x2b, (byte) 0x81, 0x04, 0x00, 0x23}), + BrainpoolP256R1( + EllipticCurveValues.BrainpoolP256R1, + new byte[] {0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x07}), + BrainpoolP384R1( + EllipticCurveValues.BrainpoolP384R1, + new byte[] {0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0b}), + BrainpoolP512R1( + EllipticCurveValues.BrainpoolP512R1, + new byte[] {0x2b, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0d}), + X25519( + EllipticCurveValues.X25519, + new byte[] {0x2b, 0x06, 0x01, 0x04, 0x01, (byte) 0x97, 0x55, 0x01, 0x05, 0x01}), + Ed25519( + EllipticCurveValues.Ed25519, + new byte[] {0x2b, 0x06, 0x01, 0x04, 0x01, (byte) 0xda, 0x47, 0x0f, 0x01}); - byte[] getOid() { - return Arrays.copyOf(oid, oid.length); - } + private final EllipticCurveValues ellipticCurveValues; + private final byte[] oid; - EllipticCurveValues getValues() { - return ellipticCurveValues; - } + OpenPgpCurve(EllipticCurveValues ellipticCurveValues, byte[] oid) { + this.ellipticCurveValues = ellipticCurveValues; + this.oid = oid; + } + + byte[] getOid() { + return Arrays.copyOf(oid, oid.length); + } + + EllipticCurveValues getValues() { + return ellipticCurveValues; + } - static OpenPgpCurve fromOid(byte[] oid) { - for (OpenPgpCurve params : OpenPgpCurve.values()) { - // Allow given oid to have trailing zeroes. - byte[] compareOid = Arrays.copyOf(params.oid, Math.max(params.oid.length, oid.length)); - if (Arrays.equals(compareOid, oid)) { - return params; - } - } - throw new IllegalArgumentException("Not a supported curve OID"); + static OpenPgpCurve fromOid(byte[] oid) { + for (OpenPgpCurve params : OpenPgpCurve.values()) { + // Allow given oid to have trailing zeroes. + byte[] compareOid = Arrays.copyOf(params.oid, Math.max(params.oid.length, oid.length)); + if (Arrays.equals(compareOid, oid)) { + return params; + } } + throw new IllegalArgumentException("Not a supported curve OID"); + } } diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java index 9b0cfe0e..4cacb539 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpSession.java @@ -37,9 +37,6 @@ import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; import com.yubico.yubikit.core.util.Tlv; import com.yubico.yubikit.core.util.Tlvs; - -import org.slf4j.LoggerFactory; - import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -59,1281 +56,1269 @@ import java.util.List; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; +import org.slf4j.LoggerFactory; /** * OpenPGP card application as specified on gnupg.org. - *

- * Enables you to manage keys and data, as well as perform signing, decryption, and authentication - * operations. + * + *

Enables you to manage keys and data, as well as perform signing, decryption, and + * authentication operations. */ public class OpenPgpSession extends ApplicationSession { - /** - * Support for factory reset. - */ - public static final Feature FEATURE_RESET = new Feature.Versioned<>("Reset", 1, 0, 6); - /** - * Support for the User Interaction Flag (touch requirement). - */ - public static final Feature FEATURE_UIF = new Feature.Versioned<>("UIF", 4, 2, 0); - /** - * Support for public key attestation. - */ - public static final Feature FEATURE_ATTESTATION = new Feature.Versioned<>("Attestation", 5, 2, 1); - /** - * Support for the "cached" UIF settings. - */ - public static final Feature FEATURE_CACHED = new Feature.Versioned<>("Cached UIF", 5, 2, 1); - /** - * Support for 4096 (and 3072) bit RSA keys, in addition to 2048-bit. - */ - public static final Feature FEATURE_RSA4096_KEYS = new Feature.Versioned<>("RSA 4096 keys", 4, 0, 0); - /** - * Support for private keys using Elliptic Curve cryptography. - */ - public static final Feature FEATURE_EC_KEYS = new Feature.Versioned<>("Elliptic curve keys", 5, 2, 0); - - /** - * Support for resetting the PIN verified state. - */ - public static final Feature FEATURE_UNVERIFY_PIN = new Feature.Versioned<>("Unverify PIN", 5, 6, 0); - /** - * Support for changing the number of PIN attempts allowed before becoming blocked. - */ - public static final Feature FEATURE_PIN_ATTEMPTS = new Feature("Set PIN attempts") { - @Override - public boolean isSupportedBy(Version version) { - if (version.major == 1) { - // YubiKey NEO - return version.isAtLeast(1, 0, 7); - } - return version.isAtLeast(4, 3, 1); - } - }; - - /** - * Support for generating RSA keys. - */ - public static final Feature FEATURE_RSA_GENERATION = new Feature("RSA key generation") { - @Override - public boolean isSupportedBy(Version version) { - return version.isLessThan(4, 2, 6) || version.isAtLeast(4, 3, 5); - } - }; - - private static final byte INS_VERIFY = 0x20; - private static final byte INS_CHANGE_PIN = 0x24; - private static final byte INS_RESET_RETRY_COUNTER = 0x2c; - private static final byte INS_PSO = 0x2A; - private static final byte INS_ACTIVATE = 0x44; - private static final byte INS_GENERATE_ASYM = 0x47; - private static final byte INS_GET_CHALLENGE = (byte) 0x84; - private static final byte INS_INTERNAL_AUTHENTICATE = (byte) 0x88; - private static final byte INS_SELECT_DATA = (byte) 0xa5; - private static final byte INS_GET_DATA = (byte) 0xca; - private static final byte INS_PUT_DATA = (byte) 0xda; - private static final byte INS_PUT_DATA_ODD = (byte) 0xdb; - private static final byte INS_TERMINATE = (byte) 0xe6; - private static final byte INS_GET_VERSION = (byte) 0xf1; - private static final byte INS_SET_PIN_RETRIES = (byte) 0xf2; - private static final byte INS_GET_ATTESTATION = (byte) 0xfb; - - private static final int TAG_PUBLIC_KEY = 0x7F49; - - private static final byte[] INVALID_PIN = new byte[8]; - - private final SmartCardProtocol protocol; - private final Version version; - private final ApplicationRelatedData appData; - - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(OpenPgpSession.class); - - /** - * Create new instance of {@link OpenPgpSession} and selects the application for use. - * - * @param connection a smart card connection to a YubiKey - * @throws IOException in case of communication error - * @throws ApduException in case of an error response from the YubiKey - * @throws ApplicationNotAvailableException if the application is missing or disabled - */ - public OpenPgpSession(SmartCardConnection connection) throws - IOException, ApplicationNotAvailableException, ApduException { - this(connection, null); - } + /** Support for factory reset. */ + public static final Feature FEATURE_RESET = + new Feature.Versioned<>("Reset", 1, 0, 6); - /** - * Create new instance of {@link OpenPgpSession} and selects the application for use. - * - * @param connection a smart card connection to a YubiKey - * @param scpKeyParams SCP key parameters to establish a secure connection - * @throws IOException in case of communication error - * @throws ApduException in case of an error response from the YubiKey - * @throws ApplicationNotAvailableException if the application is missing or disabled - */ - public OpenPgpSession(SmartCardConnection connection, @Nullable ScpKeyParams scpKeyParams) throws - IOException, ApplicationNotAvailableException, ApduException { - protocol = new SmartCardProtocol(connection); + /** Support for the User Interaction Flag (touch requirement). */ + public static final Feature FEATURE_UIF = new Feature.Versioned<>("UIF", 4, 2, 0); - try { - protocol.select(AppId.OPENPGP); - } catch (IOException e) { - // The OpenPGP applet can be in an inactive state, in which case it needs activation. - activate(e); - } + /** Support for public key attestation. */ + public static final Feature FEATURE_ATTESTATION = + new Feature.Versioned<>("Attestation", 5, 2, 1); - if (scpKeyParams != null) { - try { - protocol.initScp(scpKeyParams); - } catch (BadResponseException e) { - throw new IOException("Failed setting up SCP session", e); - } - } + /** Support for the "cached" UIF settings. */ + public static final Feature FEATURE_CACHED = + new Feature.Versioned<>("Cached UIF", 5, 2, 1); - Logger.debug(logger, "Getting version number"); - byte[] versionBcd = protocol.sendAndReceive(new Apdu(0, INS_GET_VERSION, 0, 0, null)); - byte[] versionBytes = new byte[3]; - for (int i = 0; i < 3; i++) { - versionBytes[i] = decodeBcd(versionBcd[i]); - } - version = Version.fromBytes(versionBytes); - protocol.configure(version); + /** Support for 4096 (and 3072) bit RSA keys, in addition to 2048-bit. */ + public static final Feature FEATURE_RSA4096_KEYS = + new Feature.Versioned<>("RSA 4096 keys", 4, 0, 0); - // Note: This value is cached! - // Do not rely on contained information that can change! - appData = getApplicationRelatedData(); + /** Support for private keys using Elliptic Curve cryptography. */ + public static final Feature FEATURE_EC_KEYS = + new Feature.Versioned<>("Elliptic curve keys", 5, 2, 0); - Logger.debug(logger, "OpenPGP session initialized (version={})", version); - } + /** Support for resetting the PIN verified state. */ + public static final Feature FEATURE_UNVERIFY_PIN = + new Feature.Versioned<>("Unverify PIN", 5, 6, 0); - private void activate(IOException e) throws IOException, ApduException, ApplicationNotAvailableException { - Throwable cause = e.getCause(); - if (cause instanceof ApduException) { - short sw = ((ApduException) cause).getSw(); - if (sw == SW.NO_INPUT_DATA || sw == SW.CONDITIONS_NOT_SATISFIED) { - //Not activated, activate - Logger.warn(logger, "Application not active, sending ACTIVATE"); - protocol.sendAndReceive(new Apdu(0, INS_ACTIVATE, 0, 0, null)); - protocol.select(AppId.OPENPGP); - return; - } + /** Support for changing the number of PIN attempts allowed before becoming blocked. */ + public static final Feature FEATURE_PIN_ATTEMPTS = + new Feature("Set PIN attempts") { + @Override + public boolean isSupportedBy(Version version) { + if (version.major == 1) { + // YubiKey NEO + return version.isAtLeast(1, 0, 7); + } + return version.isAtLeast(4, 3, 1); } - throw e; - } + }; - @Override - public Version getVersion() { - return version; + /** Support for generating RSA keys. */ + public static final Feature FEATURE_RSA_GENERATION = + new Feature("RSA key generation") { + @Override + public boolean isSupportedBy(Version version) { + return version.isLessThan(4, 2, 6) || version.isAtLeast(4, 3, 5); + } + }; + + private static final byte INS_VERIFY = 0x20; + private static final byte INS_CHANGE_PIN = 0x24; + private static final byte INS_RESET_RETRY_COUNTER = 0x2c; + private static final byte INS_PSO = 0x2A; + private static final byte INS_ACTIVATE = 0x44; + private static final byte INS_GENERATE_ASYM = 0x47; + private static final byte INS_GET_CHALLENGE = (byte) 0x84; + private static final byte INS_INTERNAL_AUTHENTICATE = (byte) 0x88; + private static final byte INS_SELECT_DATA = (byte) 0xa5; + private static final byte INS_GET_DATA = (byte) 0xca; + private static final byte INS_PUT_DATA = (byte) 0xda; + private static final byte INS_PUT_DATA_ODD = (byte) 0xdb; + private static final byte INS_TERMINATE = (byte) 0xe6; + private static final byte INS_GET_VERSION = (byte) 0xf1; + private static final byte INS_SET_PIN_RETRIES = (byte) 0xf2; + private static final byte INS_GET_ATTESTATION = (byte) 0xfb; + + private static final int TAG_PUBLIC_KEY = 0x7F49; + + private static final byte[] INVALID_PIN = new byte[8]; + + private final SmartCardProtocol protocol; + private final Version version; + private final ApplicationRelatedData appData; + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(OpenPgpSession.class); + + /** + * Create new instance of {@link OpenPgpSession} and selects the application for use. + * + * @param connection a smart card connection to a YubiKey + * @throws IOException in case of communication error + * @throws ApduException in case of an error response from the YubiKey + * @throws ApplicationNotAvailableException if the application is missing or disabled + */ + public OpenPgpSession(SmartCardConnection connection) + throws IOException, ApplicationNotAvailableException, ApduException { + this(connection, null); + } + + /** + * Create new instance of {@link OpenPgpSession} and selects the application for use. + * + * @param connection a smart card connection to a YubiKey + * @param scpKeyParams SCP key parameters to establish a secure connection + * @throws IOException in case of communication error + * @throws ApduException in case of an error response from the YubiKey + * @throws ApplicationNotAvailableException if the application is missing or disabled + */ + public OpenPgpSession(SmartCardConnection connection, @Nullable ScpKeyParams scpKeyParams) + throws IOException, ApplicationNotAvailableException, ApduException { + protocol = new SmartCardProtocol(connection); + + try { + protocol.select(AppId.OPENPGP); + } catch (IOException e) { + // The OpenPGP applet can be in an inactive state, in which case it needs activation. + activate(e); } - @Override - public void close() throws IOException { - protocol.close(); + if (scpKeyParams != null) { + try { + protocol.initScp(scpKeyParams); + } catch (BadResponseException e) { + throw new IOException("Failed setting up SCP session", e); + } } - /** - * Read a Data Object from the YubiKey. - * - * @param doId the ID of the Data Object to read - * @return the value of the Data Object - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public byte[] getData(int doId) throws ApduException, IOException { - Logger.debug(logger, "Reading Data Object {}", doId); - return protocol.sendAndReceive(new Apdu(0, INS_GET_DATA, doId >> 8, doId & 0xff, null)); + Logger.debug(logger, "Getting version number"); + byte[] versionBcd = protocol.sendAndReceive(new Apdu(0, INS_GET_VERSION, 0, 0, null)); + byte[] versionBytes = new byte[3]; + for (int i = 0; i < 3; i++) { + versionBytes[i] = decodeBcd(versionBcd[i]); } - - /** - * Write a Data Object to the YubiKey. - * - * @param doId the ID of the Data Object to read - * @param data the value to write to the Data Object - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public void putData(int doId, byte[] data) throws ApduException, IOException { - protocol.sendAndReceive(new Apdu(0, INS_PUT_DATA, doId >> 8, doId & 0xff, data)); - Logger.debug(logger, "Wrote Data Object {}", doId); + version = Version.fromBytes(versionBytes); + protocol.configure(version); + + // Note: This value is cached! + // Do not rely on contained information that can change! + appData = getApplicationRelatedData(); + + Logger.debug(logger, "OpenPGP session initialized (version={})", version); + } + + private void activate(IOException e) + throws IOException, ApduException, ApplicationNotAvailableException { + Throwable cause = e.getCause(); + if (cause instanceof ApduException) { + short sw = ((ApduException) cause).getSw(); + if (sw == SW.NO_INPUT_DATA || sw == SW.CONDITIONS_NOT_SATISFIED) { + // Not activated, activate + Logger.warn(logger, "Application not active, sending ACTIVATE"); + protocol.sendAndReceive(new Apdu(0, INS_ACTIVATE, 0, 0, null)); + protocol.select(AppId.OPENPGP); + return; + } } - - /** - * Read the Application Related Data from the YubiKey. - * - * @return the parsed Application Related Data - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public ApplicationRelatedData getApplicationRelatedData() throws ApduException, IOException { - return ApplicationRelatedData.parse(getData(Do.APPLICATION_RELATED_DATA)); + throw e; + } + + @Override + public Version getVersion() { + return version; + } + + @Override + public void close() throws IOException { + protocol.close(); + } + + /** + * Read a Data Object from the YubiKey. + * + * @param doId the ID of the Data Object to read + * @return the value of the Data Object + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public byte[] getData(int doId) throws ApduException, IOException { + Logger.debug(logger, "Reading Data Object {}", doId); + return protocol.sendAndReceive(new Apdu(0, INS_GET_DATA, doId >> 8, doId & 0xff, null)); + } + + /** + * Write a Data Object to the YubiKey. + * + * @param doId the ID of the Data Object to read + * @param data the value to write to the Data Object + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public void putData(int doId, byte[] data) throws ApduException, IOException { + protocol.sendAndReceive(new Apdu(0, INS_PUT_DATA, doId >> 8, doId & 0xff, data)); + Logger.debug(logger, "Wrote Data Object {}", doId); + } + + /** + * Read the Application Related Data from the YubiKey. + * + * @return the parsed Application Related Data + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public ApplicationRelatedData getApplicationRelatedData() throws ApduException, IOException { + return ApplicationRelatedData.parse(getData(Do.APPLICATION_RELATED_DATA)); + } + + /** + * Get the AID for the OpenPGP application. + * + * @return the parsed OpenPgpAid + */ + public OpenPgpAid getAid() { + return appData.getAid(); + } + + /** + * Get the Extended Capabilities supported by the YubiKey. + * + * @return the parsed ExtendedCapabilities + */ + public ExtendedCapabilities getExtendedCapabilities() { + return appData.getDiscretionary().getExtendedCapabilities(); + } + + /** + * Get the current PIN configuration and status from the YubiKey. + * + * @return a PwStatus object with remaining attempts, maximum PIN lengths, and signature PIN + * policy + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public PwStatus getPinStatus() throws ApduException, IOException { + return PwStatus.parse(getData(Do.PW_STATUS_BYTES)); + } + + /** + * Read the current KDF settings configured for the YubiKey. + * + * @return a Kdf object, capable of deriving a key from a PIN + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public Kdf getKdf() throws ApduException, IOException { + ExtendedCapabilities capabilities = getExtendedCapabilities(); + if (!capabilities.getFlags().contains(ExtendedCapabilityFlag.KDF)) { + return new Kdf.None(); } - - /** - * Get the AID for the OpenPGP application. - * - * @return the parsed OpenPgpAid - */ - public OpenPgpAid getAid() { - return appData.getAid(); + return Kdf.parse(getData(Do.KDF)); + } + + /** + * Set up a PIN Key Derivation Function. + * + *

This enables (or disables) the use of a KDF for PIN verification, as well as resetting the + * User and Admin PINs to their default (initial) values. + * + *

If a Reset Code is present, it will be invalidated. + * + *

This command requires Admin PIN verification. + * + * @param kdf the KDF configuration to set + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public void setKdf(Kdf kdf) throws ApduException, IOException { + ExtendedCapabilities capabilities = getExtendedCapabilities(); + if (!capabilities.getFlags().contains(ExtendedCapabilityFlag.KDF)) { + throw new UnsupportedOperationException("KDF is not supported"); } - /** - * Get the Extended Capabilities supported by the YubiKey. - * - * @return the parsed ExtendedCapabilities - */ - public ExtendedCapabilities getExtendedCapabilities() { - return appData.getDiscretionary().getExtendedCapabilities(); + Logger.debug(logger, "Setting PIN KDF to algorithm: {}", kdf.getAlgorithm()); + putData(Do.KDF, kdf.getBytes()); + Logger.info(logger, "KDF settings changed"); + } + + private void doVerify(Pw pw, char[] pin) throws ApduException, IOException, InvalidPinException { + byte[] pinEnc = getKdf().process(pw, pin); + try { + protocol.sendAndReceive(new Apdu(0, INS_VERIFY, 0, pw.getValue(), pinEnc)); + } catch (ApduException e) { + if (e.getSw() == SW.SECURITY_CONDITION_NOT_SATISFIED) { + int remaining = getPinStatus().getAttempts(pw); + throw new InvalidPinException(remaining); + } + throw e; + } finally { + Arrays.fill(pinEnc, (byte) 0); } - - /** - * Get the current PIN configuration and status from the YubiKey. - * - * @return a PwStatus object with remaining attempts, maximum PIN lengths, and signature PIN policy - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public PwStatus getPinStatus() throws ApduException, IOException { - return PwStatus.parse(getData(Do.PW_STATUS_BYTES)); + } + + /** + * Verify the User PIN. + * + *

This will unlock functionality that requires User PIN verification. Note that with + * extended=false only sign operations are allowed. Inversely, with extended=true sign operations + * are NOT allowed. + * + * @param pin the User PIN to verify + * @param extended false to verify for signature use, true for other uses + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + * @throws InvalidPinException in case of the wrong PIN + */ + public void verifyUserPin(char[] pin, boolean extended) + throws ApduException, IOException, InvalidPinException { + doVerify(extended ? Pw.RESET : Pw.USER, pin); + } + + /** + * Verify the Admin PIN. + * + *

This will unlock functionality that requires Admin PIN verification. + * + * @param pin the Admin PIN to verify + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + * @throws InvalidPinException in case of the wrong PIN + */ + public void verifyAdminPin(char[] pin) throws ApduException, IOException, InvalidPinException { + doVerify(Pw.ADMIN, pin); + } + + private void doUnverifyPin(Pw pw) throws ApduException, IOException { + require(FEATURE_UNVERIFY_PIN); + Logger.debug(logger, "Resetting verification for {} PIN", pw.name()); + protocol.sendAndReceive(new Apdu(0, INS_VERIFY, 0xff, pw.getValue(), null)); + Logger.info(logger, "{} PIN unverified", pw.name()); + } + + /** + * Resets the verification state of the User PIN to unverified. + * + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public void unverifyUserPin() throws ApduException, IOException { + doUnverifyPin(Pw.USER); + } + + /** + * Resets the verification state of the Admin PIN to unverified. + * + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public void unverifyAdminPin() throws ApduException, IOException { + doUnverifyPin(Pw.ADMIN); + } + + /** + * Gets the number of signatures performed with the SIG key. + * + * @return the number of signatures + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public int getSignatureCounter() throws ApduException, IOException { + return SecuritySupportTemplate.parse(getData(Do.SECURITY_SUPPORT_TEMPLATE)) + .getSignatureCounter(); + } + + /** + * Generate random data on the YubiKey. + * + * @param length the number of bytes to generate + * @return random data of the given length + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public byte[] getChallenge(int length) throws ApduException, IOException { + ExtendedCapabilities capabilities = getExtendedCapabilities(); + if (!capabilities.getFlags().contains(ExtendedCapabilityFlag.GET_CHALLENGE)) { + throw new UnsupportedOperationException("GET_CHALLENGE is not supported"); } - - /** - * Read the current KDF settings configured for the YubiKey. - * - * @return a Kdf object, capable of deriving a key from a PIN - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public Kdf getKdf() throws ApduException, IOException { - ExtendedCapabilities capabilities = getExtendedCapabilities(); - if (!capabilities.getFlags().contains(ExtendedCapabilityFlag.KDF)) { - return new Kdf.None(); - } - return Kdf.parse(getData(Do.KDF)); + if (length < 0 || length > capabilities.getChallengeMaxLength()) { + throw new UnsupportedOperationException("Unsupported challenge length"); } - /** - * Set up a PIN Key Derivation Function. - *

- * This enables (or disables) the use of a KDF for PIN verification, as well - * as resetting the User and Admin PINs to their default (initial) values. - *

- * If a Reset Code is present, it will be invalidated. - *

- * This command requires Admin PIN verification. - * - * @param kdf the KDF configuration to set - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public void setKdf(Kdf kdf) throws ApduException, IOException { - ExtendedCapabilities capabilities = getExtendedCapabilities(); - if (!capabilities.getFlags().contains(ExtendedCapabilityFlag.KDF)) { - throw new UnsupportedOperationException("KDF is not supported"); - } - - Logger.debug(logger, "Setting PIN KDF to algorithm: {}", kdf.getAlgorithm()); - putData(Do.KDF, kdf.getBytes()); - Logger.info(logger, "KDF settings changed"); - } - - private void doVerify(Pw pw, char[] pin) throws ApduException, IOException, InvalidPinException { - byte[] pinEnc = getKdf().process(pw, pin); + Logger.debug(logger, "Getting {} random bytes", length); + return protocol.sendAndReceive(new Apdu(0, INS_GET_CHALLENGE, 0, 0, null, length)); + } + + /** + * Set the PIN policy for the signature key slot. + * + *

A PIN policy of ONCE (the default) requires the User PIN to be verified once per session + * prior to creating a signature. A policy of ALWAYS requires a new PIN verification prior to each + * signature made. + * + * @param pinPolicy the PIN policy to set + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public void setSignaturePinPolicy(PinPolicy pinPolicy) throws ApduException, IOException { + Logger.debug(logger, "Setting Signature PIN policy to {}", pinPolicy); + putData(Do.PW_STATUS_BYTES, new byte[] {pinPolicy.value}); + Logger.info(logger, "Signature PIN policy set"); + } + + /** + * Performs a factory reset on the OpenPGP application. + * + *

WARNING: This will delete all stored keys, certificates and other data. + * + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public void reset() throws ApduException, IOException { + require(FEATURE_RESET); + Logger.debug(logger, "Preparing OpenPGP reset"); + + // Ensure the User and Admin PINs are blocked + PwStatus status = getPinStatus(); + for (Pw pw : Arrays.asList(Pw.USER, Pw.ADMIN)) { + Logger.debug(logger, "Verify {} PIN with invalid attempts until blocked", pw); + for (int i = status.getAttempts(pw); i > 0; i--) { try { - protocol.sendAndReceive(new Apdu(0, INS_VERIFY, 0, pw.getValue(), pinEnc)); + protocol.sendAndReceive(new Apdu(0, INS_VERIFY, 0, pw.getValue(), INVALID_PIN)); } catch (ApduException e) { - if (e.getSw() == SW.SECURITY_CONDITION_NOT_SATISFIED) { - int remaining = getPinStatus().getAttempts(pw); - throw new InvalidPinException(remaining); - } - throw e; - } finally { - Arrays.fill(pinEnc, (byte) 0); + // Ignore } + } } - /** - * Verify the User PIN. - *

- * This will unlock functionality that requires User PIN verification. - * Note that with extended=false only sign operations are allowed. - * Inversely, with extended=true sign operations are NOT allowed. - * - * @param pin the User PIN to verify - * @param extended false to verify for signature use, true for other uses - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - * @throws InvalidPinException in case of the wrong PIN - */ - public void verifyUserPin(char[] pin, boolean extended) throws ApduException, IOException, InvalidPinException { - doVerify(extended ? Pw.RESET : Pw.USER, pin); + // Reset the application + Logger.debug(logger, "Sending TERMINATE, then ACTIVATE"); + protocol.sendAndReceive(new Apdu(0, INS_TERMINATE, 0, 0, null)); + protocol.sendAndReceive(new Apdu(0, INS_ACTIVATE, 0, 0, null)); + Logger.info(logger, "OpenPGP application data reset performed"); + } + + /** + * Set the number of PIN attempts to allow before blocking. + * + *

WARNING: On YubiKey NEO this will reset the PINs to their default values. + * + *

Requires Admin PIN verification. + * + * @param userAttempts the number of attempts for the User PIN + * @param resetAttempts the number of attempts for the Reset Code + * @param adminAttempts the number of attempts for the Admin PIN + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public void setPinAttempts(int userAttempts, int resetAttempts, int adminAttempts) + throws ApduException, IOException { + require(FEATURE_PIN_ATTEMPTS); + + Logger.debug( + logger, "Setting PIN attempts to ({}, {}, {})", userAttempts, resetAttempts, adminAttempts); + protocol.sendAndReceive( + new Apdu( + 0, + INS_SET_PIN_RETRIES, + 0, + 0, + new byte[] {(byte) userAttempts, (byte) resetAttempts, (byte) adminAttempts})); + Logger.info(logger, "Number of PIN attempts has been changed"); + } + + private void changePw(Pw pw, char[] pin, char[] newPin) + throws ApduException, IOException, InvalidPinException { + Logger.debug(logger, "Changing {} PIN", pw); + Kdf kdf = getKdf(); + byte[] pinBytes = null; + byte[] newPinBytes = null; + byte[] data = null; + try { + pinBytes = kdf.process(pw, pin); + newPinBytes = kdf.process(pw, newPin); + data = + ByteBuffer.allocate(pinBytes.length + newPinBytes.length) + .put(pinBytes) + .put(newPinBytes) + .array(); + protocol.sendAndReceive(new Apdu(0, INS_CHANGE_PIN, 0, pw.getValue(), data)); + + } catch (ApduException e) { + if (e.getSw() == SW.SECURITY_CONDITION_NOT_SATISFIED) { + int remaining = getPinStatus().getAttempts(pw); + throw new InvalidPinException(remaining); + } + throw e; + } finally { + if (data != null) { + Arrays.fill(data, (byte) 0); + } + if (pinBytes != null) { + Arrays.fill(pinBytes, (byte) 0); + } + if (newPinBytes != null) { + Arrays.fill(newPinBytes, (byte) 0); + } } - - /** - * Verify the Admin PIN. - *

- * This will unlock functionality that requires Admin PIN verification. - * - * @param pin the Admin PIN to verify - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - * @throws InvalidPinException in case of the wrong PIN - */ - public void verifyAdminPin(char[] pin) throws ApduException, IOException, InvalidPinException { - doVerify(Pw.ADMIN, pin); - } - - private void doUnverifyPin(Pw pw) throws ApduException, IOException { - require(FEATURE_UNVERIFY_PIN); - Logger.debug(logger, "Resetting verification for {} PIN", pw.name()); - protocol.sendAndReceive(new Apdu(0, INS_VERIFY, 0xff, pw.getValue(), null)); - Logger.info(logger, "{} PIN unverified", pw.name()); + Logger.info(logger, "New {} PIN set", pw); + } + + /** + * Change the User PIN. + * + * @param pin the current User PIN + * @param newPin the new User PIN to set + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + * @throws InvalidPinException in case of the wrong PIN in case of the wrong PIN + */ + public void changeUserPin(char[] pin, char[] newPin) + throws ApduException, IOException, InvalidPinException { + changePw(Pw.USER, pin, newPin); + } + + /** + * Change the Admin PIN. + * + * @param pin the current Admin PIN + * @param newPin the new Admin PIN to set + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + * @throws InvalidPinException in case of the wrong PIN + */ + public void changeAdminPin(char[] pin, char[] newPin) + throws ApduException, IOException, InvalidPinException { + changePw(Pw.ADMIN, pin, newPin); + } + + /** + * Set the Reset Code for User PIN. + * + *

The Reset Code can be used to set a new User PIN if it is lost or becomes blocked, using the + * reset_pin method. + * + *

This command requires Admin PIN verification. + * + * @param resetCode the Reset Code to set + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public void setResetCode(char[] resetCode) throws ApduException, IOException { + Logger.debug(logger, "Setting a new PIN Reset Code"); + byte[] data = null; + try { + data = getKdf().process(Pw.RESET, resetCode); + putData(Do.RESETTING_CODE, data); + } finally { + if (data != null) { + Arrays.fill(data, (byte) 0); + } } - /** - * Resets the verification state of the User PIN to unverified. - * - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public void unverifyUserPin() throws ApduException, IOException { - doUnverifyPin(Pw.USER); + Logger.info(logger, "New Reset Code has been set"); + } + + /** + * Resets the User PIN in case it is lost or blocked. + * + *

This can be done either after performing Admin PIN verification, or by providing the Reset + * Code. + * + *

This command requires Admin PIN verification, or the Reset Code. + * + * @param newPin the new User PIN to set + * @param resetCode the Reset Code, which is needed if the Admin pin has not been verified + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + * @throws InvalidPinException in case of the wrong PIN + */ + public void resetPin(char[] newPin, @Nullable char[] resetCode) + throws ApduException, IOException, InvalidPinException { + Logger.debug(logger, "Resetting User PIN"); + byte p1 = 2; + Kdf kdf = getKdf(); + byte[] data = kdf.process(Pw.USER, newPin); + if (resetCode != null) { + Logger.debug(logger, "Using Reset Code"); + byte[] resetCodeBytes = kdf.process(Pw.RESET, resetCode); + data = + ByteBuffer.allocate(resetCodeBytes.length + data.length) + .put(resetCodeBytes) + .put(data) + .array(); + p1 = 0; } - /** - * Resets the verification state of the Admin PIN to unverified. - * - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public void unverifyAdminPin() throws ApduException, IOException { - doUnverifyPin(Pw.ADMIN); + try { + protocol.sendAndReceive(new Apdu(0, INS_RESET_RETRY_COUNTER, p1, Pw.USER.getValue(), data)); + } catch (ApduException e) { + if (e.getSw() == SW.SECURITY_CONDITION_NOT_SATISFIED && resetCode != null) { + int resetRemaining = getPinStatus().getAttemptsReset(); + throw new InvalidPinException( + resetRemaining, "Invalid Reset Code, " + resetRemaining + " tries remaining"); + } + throw e; } - - /** - * Gets the number of signatures performed with the SIG key. - * - * @return the number of signatures - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public int getSignatureCounter() throws ApduException, IOException { - return SecuritySupportTemplate.parse(getData(Do.SECURITY_SUPPORT_TEMPLATE)).getSignatureCounter(); + Logger.info(logger, "New User PIN has been set"); + } + + /** + * Get the User Interaction Flag (touch requirement) for a key. + * + * @param keyRef the key slot to read UIF for + * @return the User Interaction Flag for the given slot + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public Uif getUif(KeyRef keyRef) throws ApduException, IOException { + try { + return Uif.fromValue(getData(keyRef.getUif())[0]); + } catch (ApduException e) { + if (e.getSw() == SW.WRONG_PARAMETERS_P1P2) { + // Not supported + return Uif.OFF; + } + throw e; } - - /** - * Generate random data on the YubiKey. - * - * @param length the number of bytes to generate - * @return random data of the given length - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public byte[] getChallenge(int length) throws ApduException, IOException { - ExtendedCapabilities capabilities = getExtendedCapabilities(); - if (!capabilities.getFlags().contains(ExtendedCapabilityFlag.GET_CHALLENGE)) { - throw new UnsupportedOperationException("GET_CHALLENGE is not supported"); - } - if (length < 0 || length > capabilities.getChallengeMaxLength()) { - throw new UnsupportedOperationException("Unsupported challenge length"); - } - - Logger.debug(logger, "Getting {} random bytes", length); - return protocol.sendAndReceive(new Apdu(0, INS_GET_CHALLENGE, 0, 0, null, length)); + } + + /** + * Set the User Interaction Flag (touch requirement) for a key. + * + *

Requires Admin PIN verification. + * + * @param keyRef the key slot to set UIF for + * @param uif the UIF setting to use for the key in the given slot + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public void setUif(KeyRef keyRef, Uif uif) throws ApduException, IOException { + require(FEATURE_UIF); + + if (keyRef == KeyRef.ATT) { + require(FEATURE_ATTESTATION); } - - /** - * Set the PIN policy for the signature key slot. - *

- * A PIN policy of ONCE (the default) requires the User PIN to be verified once per session - * prior to creating a signature. A policy of ALWAYS requires a new PIN verification prior to - * each signature made. - * - * @param pinPolicy the PIN policy to set - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public void setSignaturePinPolicy(PinPolicy pinPolicy) throws ApduException, IOException { - Logger.debug(logger, "Setting Signature PIN policy to {}", pinPolicy); - putData(Do.PW_STATUS_BYTES, new byte[]{pinPolicy.value}); - Logger.info(logger, "Signature PIN policy set"); + if (uif.isCached()) { + require(FEATURE_CACHED); } - /** - * Performs a factory reset on the OpenPGP application. - *

- * WARNING: This will delete all stored keys, certificates and other data. - * - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public void reset() throws ApduException, IOException { - require(FEATURE_RESET); - Logger.debug(logger, "Preparing OpenPGP reset"); - - // Ensure the User and Admin PINs are blocked - PwStatus status = getPinStatus(); - for (Pw pw : Arrays.asList(Pw.USER, Pw.ADMIN)) { - Logger.debug(logger, "Verify {} PIN with invalid attempts until blocked", pw); - for (int i = status.getAttempts(pw); i > 0; i--) { - try { - protocol.sendAndReceive(new Apdu(0, INS_VERIFY, 0, pw.getValue(), INVALID_PIN)); - } catch (ApduException e) { - // Ignore - } - } - } + Logger.debug(logger, "Setting UIF for {} to {}", keyRef, uif); - // Reset the application - Logger.debug(logger, "Sending TERMINATE, then ACTIVATE"); - protocol.sendAndReceive(new Apdu(0, INS_TERMINATE, 0, 0, null)); - protocol.sendAndReceive(new Apdu(0, INS_ACTIVATE, 0, 0, null)); - Logger.info(logger, "OpenPGP application data reset performed"); + if (getUif(keyRef).isFixed()) { + throw new IllegalStateException("Cannot change UIF when set to FIXED"); } - /** - * Set the number of PIN attempts to allow before blocking. - *

- * WARNING: On YubiKey NEO this will reset the PINs to their default values. - *

- * Requires Admin PIN verification. - * - * @param userAttempts the number of attempts for the User PIN - * @param resetAttempts the number of attempts for the Reset Code - * @param adminAttempts the number of attempts for the Admin PIN - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public void setPinAttempts(int userAttempts, int resetAttempts, int adminAttempts) throws - ApduException, IOException { - require(FEATURE_PIN_ATTEMPTS); - - Logger.debug(logger, "Setting PIN attempts to ({}, {}, {})", userAttempts, resetAttempts, adminAttempts); - protocol.sendAndReceive(new Apdu( - 0, - INS_SET_PIN_RETRIES, - 0, - 0, - new byte[]{(byte) userAttempts, (byte) resetAttempts, (byte) adminAttempts} - )); - Logger.info(logger, "Number of PIN attempts has been changed"); + putData(keyRef.getUif(), uif.getBytes()); + Logger.info(logger, "UIF changed for {}", keyRef); + } + + /** + * Get the supported key algorithms for each of the key slots. + * + * @return a mapping from key ref to list of supported algorithms + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + * @throws BadResponseException in case of incorrect YubiKey response + */ + public Map> getAlgorithmInformation() + throws ApduException, IOException, BadResponseException { + if (!getExtendedCapabilities() + .getFlags() + .contains(ExtendedCapabilityFlag.ALGORITHM_ATTRIBUTES_CHANGEABLE)) { + throw new UnsupportedOperationException("Writing Algorithm Attributes is not supported"); } - private void changePw(Pw pw, char[] pin, char[] newPin) throws ApduException, IOException, InvalidPinException { - Logger.debug(logger, "Changing {} PIN", pw); - Kdf kdf = getKdf(); - byte[] pinBytes = null; - byte[] newPinBytes = null; - byte[] data = null; - try { - pinBytes = kdf.process(pw, pin); - newPinBytes = kdf.process(pw, newPin); - data = ByteBuffer - .allocate(pinBytes.length + newPinBytes.length) - .put(pinBytes) - .put(newPinBytes) - .array(); - protocol.sendAndReceive(new Apdu( - 0, - INS_CHANGE_PIN, - 0, - pw.getValue(), - data - - )); - } catch (ApduException e) { - if (e.getSw() == SW.SECURITY_CONDITION_NOT_SATISFIED) { - int remaining = getPinStatus().getAttempts(pw); - throw new InvalidPinException(remaining); - - } - throw e; - } finally { - if (data != null) { - Arrays.fill(data, (byte) 0); - } - if (pinBytes != null) { - Arrays.fill(pinBytes, (byte) 0); - } - if (newPinBytes != null) { - Arrays.fill(newPinBytes, (byte) 0); - } + Map> data = new HashMap<>(); + if (version.isLessThan(5, 2, 0)) { + AlgorithmAttributes.Rsa.ImportFormat fmt; + List sizes = new ArrayList<>(); + sizes.add(2048); + if (version.isLessThan(4, 0, 0)) { + // Needed by Neo + fmt = AlgorithmAttributes.Rsa.ImportFormat.CRT_W_MOD; + } else { + fmt = AlgorithmAttributes.Rsa.ImportFormat.STANDARD; + if (!(version.major == 4 && version.minor == 4)) { + // Non-FIPS + sizes.add(3072); + sizes.add(4096); } - Logger.info(logger, "New {} PIN set", pw); - } - - /** - * Change the User PIN. - * - * @param pin the current User PIN - * @param newPin the new User PIN to set - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - * @throws InvalidPinException in case of the wrong PIN in case of the wrong PIN - */ - public void changeUserPin(char[] pin, char[] newPin) throws ApduException, IOException, InvalidPinException { - changePw(Pw.USER, pin, newPin); - } - - /** - * Change the Admin PIN. - * - * @param pin the current Admin PIN - * @param newPin the new Admin PIN to set - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - * @throws InvalidPinException in case of the wrong PIN - */ - public void changeAdminPin(char[] pin, char[] newPin) throws ApduException, IOException, InvalidPinException { - changePw(Pw.ADMIN, pin, newPin); - } - - /** - * Set the Reset Code for User PIN. - *

- * The Reset Code can be used to set a new User PIN if it is lost or becomes - * blocked, using the reset_pin method. - *

- * This command requires Admin PIN verification. - * - * @param resetCode the Reset Code to set - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public void setResetCode(char[] resetCode) throws ApduException, IOException { - Logger.debug(logger, "Setting a new PIN Reset Code"); - byte[] data = null; - try { - data = getKdf().process(Pw.RESET, resetCode); - putData(Do.RESETTING_CODE, data); - } finally { - if (data != null) { - Arrays.fill(data, (byte) 0); - } + } + List attributes = new ArrayList<>(); + for (int size : sizes) { + attributes.add(AlgorithmAttributes.Rsa.create(size, fmt)); + } + data.put(KeyRef.SIG, Collections.unmodifiableList(attributes)); + data.put(KeyRef.DEC, Collections.unmodifiableList(attributes)); + data.put(KeyRef.AUT, Collections.unmodifiableList(attributes)); + } else { + Logger.debug(logger, "Getting supported Algorithm Information"); + byte[] buf = getData(Do.ALGORITHM_INFORMATION); + try { + buf = Tlvs.unpackValue(Do.ALGORITHM_INFORMATION, buf); + } catch (BufferUnderflowException e) { + buf = Arrays.copyOf(buf, buf.length + 2); + buf = Tlvs.unpackValue(Do.ALGORITHM_INFORMATION, buf); + buf = Arrays.copyOf(buf, buf.length - 2); + } + Map refs = new HashMap<>(); + for (KeyRef ref : KeyRef.values()) { + refs.put(ref.getAlgorithmAttributes(), ref); + } + for (Tlv tlv : Tlvs.decodeList(buf)) { + KeyRef ref = refs.get(tlv.getTag()); + if (!data.containsKey(ref)) { + data.put(ref, new ArrayList<>()); + } + data.get(ref).add(AlgorithmAttributes.parse(tlv.getValue())); + } + + if (version.isLessThan(5, 6, 1)) { + // Fix for invalid Curve25519 entries: + // Remove X25519 with EdDSA from all keys + AlgorithmAttributes invalidX25519 = + new AlgorithmAttributes.Ec( + (byte) 0x16, OpenPgpCurve.X25519, AlgorithmAttributes.Ec.ImportFormat.STANDARD); + for (List values : data.values()) { + values.remove(invalidX25519); } - Logger.info(logger, "New Reset Code has been set"); - } + AlgorithmAttributes x25519 = + new AlgorithmAttributes.Ec( + (byte) 0x12, OpenPgpCurve.X25519, AlgorithmAttributes.Ec.ImportFormat.STANDARD); - /** - * Resets the User PIN in case it is lost or blocked. - *

- * This can be done either after performing Admin PIN verification, or by providing the Reset - * Code. - *

- * This command requires Admin PIN verification, or the Reset Code. - * - * @param newPin the new User PIN to set - * @param resetCode the Reset Code, which is needed if the Admin pin has not been verified - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - * @throws InvalidPinException in case of the wrong PIN - */ - public void resetPin(char[] newPin, @Nullable char[] resetCode) throws - ApduException, IOException, InvalidPinException { - Logger.debug(logger, "Resetting User PIN"); - byte p1 = 2; - Kdf kdf = getKdf(); - byte[] data = kdf.process(Pw.USER, newPin); - if (resetCode != null) { - Logger.debug(logger, "Using Reset Code"); - byte[] resetCodeBytes = kdf.process(Pw.RESET, resetCode); - data = ByteBuffer - .allocate(resetCodeBytes.length + data.length) - .put(resetCodeBytes) - .put(data) - .array(); - p1 = 0; + // Add X25519 ECDH for DEC + if (!data.get(KeyRef.DEC).contains(x25519)) { + data.get(KeyRef.DEC).add(x25519); } - try { - protocol.sendAndReceive(new Apdu(0, INS_RESET_RETRY_COUNTER, p1, Pw.USER.getValue(), data)); - } catch (ApduException e) { - if (e.getSw() == SW.SECURITY_CONDITION_NOT_SATISFIED && resetCode != null) { - int resetRemaining = getPinStatus().getAttemptsReset(); - throw new InvalidPinException(resetRemaining, - "Invalid Reset Code, " + resetRemaining + " tries remaining" - ); - } - throw e; - } - Logger.info(logger, "New User PIN has been set"); + // Remove EdDSA from DEC, ATT + AlgorithmAttributes ed25519 = + new AlgorithmAttributes.Ec( + (byte) 0x16, OpenPgpCurve.Ed25519, AlgorithmAttributes.Ec.ImportFormat.STANDARD); + data.get(KeyRef.DEC).remove(ed25519); + data.get(KeyRef.ATT).remove(ed25519); + } } - /** - * Get the User Interaction Flag (touch requirement) for a key. - * - * @param keyRef the key slot to read UIF for - * @return the User Interaction Flag for the given slot - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public Uif getUif(KeyRef keyRef) throws ApduException, IOException { - try { - return Uif.fromValue(getData(keyRef.getUif())[0]); - } catch (ApduException e) { - if (e.getSw() == SW.WRONG_PARAMETERS_P1P2) { - // Not supported - return Uif.OFF; - } - throw e; - } + return data; + } + + /** + * Sets the algorithm attributes to use for a key slot. + * + * @param keyRef the key slot to set attributes for + * @param attributes the algorithm attributes to set for the slot + * @throws BadResponseException in case of incorrect YubiKey response + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public void setAlgorithmAttributes(KeyRef keyRef, AlgorithmAttributes attributes) + throws BadResponseException, ApduException, IOException { + Logger.debug(logger, "Setting Algorithm Attributes for {}", keyRef); + + Map> supported = getAlgorithmInformation(); + if (!supported.containsKey(keyRef)) { + throw new UnsupportedOperationException("Key slot not supported"); } - - /** - * Set the User Interaction Flag (touch requirement) for a key. - *

- * Requires Admin PIN verification. - * - * @param keyRef the key slot to set UIF for - * @param uif the UIF setting to use for the key in the given slot - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public void setUif(KeyRef keyRef, Uif uif) throws ApduException, IOException { - require(FEATURE_UIF); - - if (keyRef == KeyRef.ATT) { - require(FEATURE_ATTESTATION); - } - if (uif.isCached()) { - require(FEATURE_CACHED); - } - - Logger.debug(logger, "Setting UIF for {} to {}", keyRef, uif); - - if (getUif(keyRef).isFixed()) { - throw new IllegalStateException("Cannot change UIF when set to FIXED"); - } - - putData(keyRef.getUif(), uif.getBytes()); - Logger.info(logger, "UIF changed for {}", keyRef); + List supportedAttributes = supported.get(keyRef); + if (!supportedAttributes.contains(attributes)) { + throw new UnsupportedOperationException("Algorithm attributes not supported: " + attributes); } - /** - * Get the supported key algorithms for each of the key slots. - * - * @return a mapping from key ref to list of supported algorithms - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - * @throws BadResponseException in case of incorrect YubiKey response - */ - public Map> getAlgorithmInformation() throws - ApduException, IOException, BadResponseException { - if (!getExtendedCapabilities().getFlags().contains(ExtendedCapabilityFlag.ALGORITHM_ATTRIBUTES_CHANGEABLE)) { - throw new UnsupportedOperationException("Writing Algorithm Attributes is not supported"); - } - - Map> data = new HashMap<>(); - if (version.isLessThan(5, 2, 0)) { - AlgorithmAttributes.Rsa.ImportFormat fmt; - List sizes = new ArrayList<>(); - sizes.add(2048); - if (version.isLessThan(4, 0, 0)) { - // Needed by Neo - fmt = AlgorithmAttributes.Rsa.ImportFormat.CRT_W_MOD; - } else { - fmt = AlgorithmAttributes.Rsa.ImportFormat.STANDARD; - if (!(version.major == 4 && version.minor == 4)) { - // Non-FIPS - sizes.add(3072); - sizes.add(4096); - } - } - List attributes = new ArrayList<>(); - for (int size : sizes) { - attributes.add(AlgorithmAttributes.Rsa.create(size, fmt)); - } - data.put(KeyRef.SIG, Collections.unmodifiableList(attributes)); - data.put(KeyRef.DEC, Collections.unmodifiableList(attributes)); - data.put(KeyRef.AUT, Collections.unmodifiableList(attributes)); - } else { - Logger.debug(logger, "Getting supported Algorithm Information"); - byte[] buf = getData(Do.ALGORITHM_INFORMATION); - try { - buf = Tlvs.unpackValue(Do.ALGORITHM_INFORMATION, buf); - } catch (BufferUnderflowException e) { - buf = Arrays.copyOf(buf, buf.length + 2); - buf = Tlvs.unpackValue(Do.ALGORITHM_INFORMATION, buf); - buf = Arrays.copyOf(buf, buf.length - 2); - } - Map refs = new HashMap<>(); - for (KeyRef ref : KeyRef.values()) { - refs.put(ref.getAlgorithmAttributes(), ref); - } - for (Tlv tlv : Tlvs.decodeList(buf)) { - KeyRef ref = refs.get(tlv.getTag()); - if (!data.containsKey(ref)) { - data.put(ref, new ArrayList<>()); - } - data.get(ref).add(AlgorithmAttributes.parse(tlv.getValue())); - } - - if (version.isLessThan(5, 6, 1)) { - // Fix for invalid Curve25519 entries: - // Remove X25519 with EdDSA from all keys - AlgorithmAttributes invalidX25519 = new AlgorithmAttributes.Ec( - (byte) 0x16, - OpenPgpCurve.X25519, - AlgorithmAttributes.Ec.ImportFormat.STANDARD - ); - for (List values : data.values()) { - values.remove(invalidX25519); - } - - AlgorithmAttributes x25519 = new AlgorithmAttributes.Ec( - (byte) 0x12, - OpenPgpCurve.X25519, - AlgorithmAttributes.Ec.ImportFormat.STANDARD - ); - - // Add X25519 ECDH for DEC - if (!data.get(KeyRef.DEC).contains(x25519)) { - data.get(KeyRef.DEC).add(x25519); - } - - // Remove EdDSA from DEC, ATT - AlgorithmAttributes ed25519 = new AlgorithmAttributes.Ec( - (byte) 0x16, - OpenPgpCurve.Ed25519, - AlgorithmAttributes.Ec.ImportFormat.STANDARD - ); - data.get(KeyRef.DEC).remove(ed25519); - data.get(KeyRef.ATT).remove(ed25519); - } - } - - return data; + putData(keyRef.getAlgorithmAttributes(), attributes.getBytes()); + Logger.info(logger, "Algorithm Attributes have been changed"); + } + + /** + * Set the generation timestamp of a key. + * + * @param keyRef the key slot to set the timestamp for + * @param timestamp the timestamp to set + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public void setGenerationTime(KeyRef keyRef, int timestamp) throws ApduException, IOException { + Logger.debug(logger, "Setting key generation timestamp for {}", keyRef); + putData(keyRef.getGenerationTime(), ByteBuffer.allocate(4).putInt(timestamp).array()); + Logger.info(logger, "Key generation timestamp set for {}", keyRef); + } + + /** + * Set the fingerprint of a key, format specified in RFC 4880. + * + * @param keyRef the slot of the key to set the fingerprint for + * @param fingerprint the fingerprint to set + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public void setFingerprint(KeyRef keyRef, byte[] fingerprint) throws ApduException, IOException { + Logger.debug(logger, "Setting key fingerprint for {}", keyRef); + putData(keyRef.getFingerprint(), fingerprint); + Logger.info(logger, "Key fingerprint set for {}", keyRef); + } + + private void selectCertificate(KeyRef keyRef) throws ApduException, IOException { + if (version.isAtLeast(5, 2, 0)) { + require(FEATURE_ATTESTATION); + byte[] data = + new Tlv( + 0x60, + new Tlv( + 0x5c, + new byte[] { + Do.CARDHOLDER_CERTIFICATE >> 8, Do.CARDHOLDER_CERTIFICATE & 0xff + }) + .getBytes()) + .getBytes(); + if (version.isLessThan(5, 4, 4)) { + // These use a non-standard byte in the command, prepend the length + data = ByteBuffer.allocate(1 + data.length).put((byte) data.length).put(data).array(); + } + protocol.sendAndReceive(new Apdu(0, INS_SELECT_DATA, 3 - keyRef.getValue(), 0x04, data)); + } else if (keyRef != KeyRef.AUT) { + // AUT is the default slot, any other slot fails + throw new UnsupportedOperationException("Selecting certificate not supported"); } - - /** - * Sets the algorithm attributes to use for a key slot. - * - * @param keyRef the key slot to set attributes for - * @param attributes the algorithm attributes to set for the slot - * @throws BadResponseException in case of incorrect YubiKey response - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public void setAlgorithmAttributes(KeyRef keyRef, AlgorithmAttributes attributes) throws - BadResponseException, ApduException, IOException { - Logger.debug(logger, "Setting Algorithm Attributes for {}", keyRef); - - Map> supported = getAlgorithmInformation(); - if (!supported.containsKey(keyRef)) { - throw new UnsupportedOperationException("Key slot not supported"); - } - List supportedAttributes = supported.get(keyRef); - if (!supportedAttributes.contains(attributes)) { - throw new UnsupportedOperationException("Algorithm attributes not supported: " + attributes); - } - - putData(keyRef.getAlgorithmAttributes(), attributes.getBytes()); - Logger.info(logger, "Algorithm Attributes have been changed"); + } + + /** + * Get a certificate from a slot. + * + * @param keyRef the slot to get a certificate from + * @return the certificate stored in the give slot + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + @Nullable public X509Certificate getCertificate(KeyRef keyRef) throws ApduException, IOException { + Logger.debug(logger, "Getting certificate for key {}", keyRef); + byte[] data; + if (keyRef == KeyRef.ATT) { + require(FEATURE_ATTESTATION); + data = getData(Do.ATT_CERTIFICATE); + } else { + selectCertificate(keyRef); + data = getData(Do.CARDHOLDER_CERTIFICATE); } - - /** - * Set the generation timestamp of a key. - * - * @param keyRef the key slot to set the timestamp for - * @param timestamp the timestamp to set - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public void setGenerationTime(KeyRef keyRef, int timestamp) throws - ApduException, IOException { - Logger.debug(logger, "Setting key generation timestamp for {}", keyRef); - putData(keyRef.getGenerationTime(), ByteBuffer.allocate(4).putInt(timestamp).array()); - Logger.info(logger, "Key generation timestamp set for {}", keyRef); + if (data.length == 0) { + return null; } - - /** - * Set the fingerprint of a key, format specified in RFC 4880. - * - * @param keyRef the slot of the key to set the fingerprint for - * @param fingerprint the fingerprint to set - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public void setFingerprint(KeyRef keyRef, byte[] fingerprint) throws - ApduException, IOException { - Logger.debug(logger, "Setting key fingerprint for {}", keyRef); - putData(keyRef.getFingerprint(), fingerprint); - Logger.info(logger, "Key fingerprint set for {}", keyRef); + try (InputStream stream = new ByteArrayInputStream(data)) { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(stream); + } catch (CertificateException e) { + throw new RuntimeException(e); } - - private void selectCertificate(KeyRef keyRef) throws ApduException, IOException { - if (version.isAtLeast(5, 2, 0)) { - require(FEATURE_ATTESTATION); - byte[] data = new Tlv( - 0x60, - new Tlv( - 0x5c, - new byte[]{Do.CARDHOLDER_CERTIFICATE >> 8, Do.CARDHOLDER_CERTIFICATE & 0xff} - ).getBytes() - ).getBytes(); - if (version.isLessThan(5, 4, 4)) { - // These use a non-standard byte in the command, prepend the length - data = ByteBuffer.allocate(1 + data.length).put((byte) data.length).put(data).array(); - } - protocol.sendAndReceive(new Apdu(0, INS_SELECT_DATA, 3 - keyRef.getValue(), 0x04, data)); - } else if (keyRef != KeyRef.AUT) { - // AUT is the default slot, any other slot fails - throw new UnsupportedOperationException("Selecting certificate not supported"); - } + } + + /** + * Imports a certificate into a slot. + * + *

Requires Admin PIN verification. + * + * @param keyRef the slot to put the certificate in + * @param certificate the certificate to import + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public void putCertificate(KeyRef keyRef, X509Certificate certificate) + throws ApduException, IOException { + byte[] certData; + try { + certData = certificate.getEncoded(); + } catch (CertificateEncodingException e) { + throw new IllegalArgumentException("Failed to get encoded version of certificate", e); } - - /** - * Get a certificate from a slot. - * - * @param keyRef the slot to get a certificate from - * @return the certificate stored in the give slot - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - @Nullable - public X509Certificate getCertificate(KeyRef keyRef) throws ApduException, IOException { - Logger.debug(logger, "Getting certificate for key {}", keyRef); - byte[] data; - if (keyRef == KeyRef.ATT) { - require(FEATURE_ATTESTATION); - data = getData(Do.ATT_CERTIFICATE); - } else { - selectCertificate(keyRef); - data = getData(Do.CARDHOLDER_CERTIFICATE); - } - if (data.length == 0) { - return null; - } - try (InputStream stream = new ByteArrayInputStream(data)) { - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - return (X509Certificate) cf.generateCertificate(stream); - } catch (CertificateException e) { - throw new RuntimeException(e); - } + Logger.debug(logger, "Importing certificate for key {}", keyRef); + if (keyRef == KeyRef.ATT) { + require(FEATURE_ATTESTATION); + putData(Do.ATT_CERTIFICATE, certData); + } else { + selectCertificate(keyRef); + putData(Do.CARDHOLDER_CERTIFICATE, certData); } - - /** - * Imports a certificate into a slot. - *

- * Requires Admin PIN verification. - * - * @param keyRef the slot to put the certificate in - * @param certificate the certificate to import - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public void putCertificate(KeyRef keyRef, X509Certificate certificate) throws - ApduException, IOException { - byte[] certData; - try { - certData = certificate.getEncoded(); - } catch (CertificateEncodingException e) { - throw new IllegalArgumentException("Failed to get encoded version of certificate", e); - } - Logger.debug(logger, "Importing certificate for key {}", keyRef); - if (keyRef == KeyRef.ATT) { - require(FEATURE_ATTESTATION); - putData(Do.ATT_CERTIFICATE, certData); - } else { - selectCertificate(keyRef); - putData(Do.CARDHOLDER_CERTIFICATE, certData); - } - Logger.info(logger, "Certificate imported for key {}", keyRef); + Logger.info(logger, "Certificate imported for key {}", keyRef); + } + + /** + * Deletes a certificate in a slot. + * + *

Requires Admin PIN verification. + * + * @param keyRef the slot in which to delete the certificate + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public void deleteCertificate(KeyRef keyRef) throws ApduException, IOException { + Logger.debug(logger, "Deleting certificate for key {}", keyRef); + if (keyRef == KeyRef.ATT) { + require(FEATURE_ATTESTATION); + putData(Do.ATT_CERTIFICATE, new byte[0]); + } else { + selectCertificate(keyRef); + putData(Do.CARDHOLDER_CERTIFICATE, new byte[0]); } - - /** - * Deletes a certificate in a slot. - *

- * Requires Admin PIN verification. - * - * @param keyRef the slot in which to delete the certificate - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public void deleteCertificate(KeyRef keyRef) throws ApduException, IOException { - Logger.debug(logger, "Deleting certificate for key {}", keyRef); - if (keyRef == KeyRef.ATT) { - require(FEATURE_ATTESTATION); - putData(Do.ATT_CERTIFICATE, new byte[0]); - } else { - selectCertificate(keyRef); - putData(Do.CARDHOLDER_CERTIFICATE, new byte[0]); - } - Logger.info(logger, "Certificate deleted for key {}", keyRef); + Logger.info(logger, "Certificate deleted for key {}", keyRef); + } + + static AlgorithmAttributes getKeyAttributes( + PrivateKeyValues values, KeyRef keyRef, Version version) { + if (values instanceof PrivateKeyValues.Rsa) { + return AlgorithmAttributes.Rsa.create( + values.getBitLength(), + version.isLessThan(4, 0, 0) + ? AlgorithmAttributes.Rsa.ImportFormat.CRT_W_MOD + : AlgorithmAttributes.Rsa.ImportFormat.STANDARD); + } else if (values instanceof PrivateKeyValues.Ec) { + return AlgorithmAttributes.Ec.create( + keyRef, OpenPgpCurve.valueOf(((PrivateKeyValues.Ec) values).getCurveParams().name())); + } else { + throw new IllegalArgumentException("Unsupported private key type"); } - - static AlgorithmAttributes getKeyAttributes(PrivateKeyValues values, KeyRef keyRef, Version version) { - if (values instanceof PrivateKeyValues.Rsa) { - return AlgorithmAttributes.Rsa.create( - values.getBitLength(), - version.isLessThan(4, 0, 0) - ? AlgorithmAttributes.Rsa.ImportFormat.CRT_W_MOD - : AlgorithmAttributes.Rsa.ImportFormat.STANDARD - ); - } else if (values instanceof PrivateKeyValues.Ec) { - return AlgorithmAttributes.Ec.create( - keyRef, - OpenPgpCurve.valueOf(((PrivateKeyValues.Ec) values).getCurveParams().name()) - ); - } else { - throw new IllegalArgumentException("Unsupported private key type"); - } + } + + static PrivateKeyTemplate getKeyTemplate(PrivateKeyValues values, KeyRef keyRef, boolean useCrt) { + if (values instanceof PrivateKeyValues.Rsa) { + int byteLength = values.getBitLength() / 8 / 2; + PrivateKeyValues.Rsa rsaValues = (PrivateKeyValues.Rsa) values; + if (useCrt) { + return new PrivateKeyTemplate.RsaCrt( + keyRef.getCrt(), + rsaValues.getPublicExponent().toByteArray(), + intToLength(rsaValues.getPrimeP(), byteLength), + intToLength(rsaValues.getPrimeQ(), byteLength), + intToLength(Objects.requireNonNull(rsaValues.getCrtCoefficient()), byteLength), + intToLength(Objects.requireNonNull(rsaValues.getPrimeExponentP()), byteLength), + intToLength(Objects.requireNonNull(rsaValues.getPrimeExponentQ()), byteLength), + intToLength(rsaValues.getModulus(), byteLength * 2)); + } else { + return new PrivateKeyTemplate.Rsa( + keyRef.getCrt(), + rsaValues.getPublicExponent().toByteArray(), + intToLength(rsaValues.getPrimeP(), byteLength), + intToLength(rsaValues.getPrimeQ(), byteLength)); + } + } else if (values instanceof PrivateKeyValues.Ec) { + return new PrivateKeyTemplate.Ec( + keyRef.getCrt(), ((PrivateKeyValues.Ec) values).getSecret(), null); } - - static PrivateKeyTemplate getKeyTemplate(PrivateKeyValues values, KeyRef keyRef, boolean useCrt) { - if (values instanceof PrivateKeyValues.Rsa) { - int byteLength = values.getBitLength() / 8 / 2; - PrivateKeyValues.Rsa rsaValues = (PrivateKeyValues.Rsa) values; - if (useCrt) { - return new PrivateKeyTemplate.RsaCrt( - keyRef.getCrt(), - rsaValues.getPublicExponent().toByteArray(), - intToLength(rsaValues.getPrimeP(), byteLength), - intToLength(rsaValues.getPrimeQ(), byteLength), - intToLength(Objects.requireNonNull(rsaValues.getCrtCoefficient()), byteLength), - intToLength(Objects.requireNonNull(rsaValues.getPrimeExponentP()), byteLength), - intToLength(Objects.requireNonNull(rsaValues.getPrimeExponentQ()), byteLength), - intToLength(rsaValues.getModulus(), byteLength * 2) - ); - } else { - return new PrivateKeyTemplate.Rsa( - keyRef.getCrt(), - rsaValues.getPublicExponent().toByteArray(), - intToLength(rsaValues.getPrimeP(), byteLength), - intToLength(rsaValues.getPrimeQ(), byteLength) - ); - } - } else if (values instanceof PrivateKeyValues.Ec) { - return new PrivateKeyTemplate.Ec( - keyRef.getCrt(), - ((PrivateKeyValues.Ec) values).getSecret(), - null - ); - } - throw new UnsupportedOperationException("Unsupported private key type"); + throw new UnsupportedOperationException("Unsupported private key type"); + } + + /** + * Generate an RSA key in the given slot. + * + *

Requires Admin PIN verification. + * + * @param keyRef the slot to generate the key in + * @param keySize the bitlength of the key to generate + * @return the public key of the generated key pair + * @throws BadResponseException in case of incorrect YubiKey response + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public PublicKeyValues generateRsaKey(KeyRef keyRef, int keySize) + throws BadResponseException, ApduException, IOException { + require(FEATURE_RSA_GENERATION); + Logger.debug(logger, "Generating RSA private key for {}", keyRef); + + if (getExtendedCapabilities() + .getFlags() + .contains(ExtendedCapabilityFlag.ALGORITHM_ATTRIBUTES_CHANGEABLE)) { + setAlgorithmAttributes( + keyRef, + AlgorithmAttributes.Rsa.create(keySize, AlgorithmAttributes.Rsa.ImportFormat.STANDARD)); + } else if (keySize != 2048) { + throw new UnsupportedOperationException("Algorithm attributes not supported"); } - /** - * Generate an RSA key in the given slot. - *

- * Requires Admin PIN verification. - * - * @param keyRef the slot to generate the key in - * @param keySize the bitlength of the key to generate - * @return the public key of the generated key pair - * @throws BadResponseException in case of incorrect YubiKey response - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public PublicKeyValues generateRsaKey(KeyRef keyRef, int keySize) throws - BadResponseException, ApduException, IOException { - require(FEATURE_RSA_GENERATION); - Logger.debug(logger, "Generating RSA private key for {}", keyRef); - - if (getExtendedCapabilities().getFlags().contains(ExtendedCapabilityFlag.ALGORITHM_ATTRIBUTES_CHANGEABLE)) { - setAlgorithmAttributes(keyRef, AlgorithmAttributes.Rsa.create( - keySize, - AlgorithmAttributes.Rsa.ImportFormat.STANDARD - )); - } else if (keySize != 2048) { - throw new UnsupportedOperationException("Algorithm attributes not supported"); - } - - byte[] resp = protocol.sendAndReceive(new Apdu(0, INS_GENERATE_ASYM, 0x80, 0x00, keyRef.getCrt())); - if (version.isLessThan(5, 0, 0)) { - setGenerationTime(keyRef, 0); - } - Map data = Tlvs.decodeMap(Tlvs.unpackValue(TAG_PUBLIC_KEY, resp)); - Logger.info(logger, "RSA key generated for {}", keyRef); - return new PublicKeyValues.Rsa( - new BigInteger(1, data.get(0x81)), - new BigInteger(1, data.get(0x82)) - ); + byte[] resp = + protocol.sendAndReceive(new Apdu(0, INS_GENERATE_ASYM, 0x80, 0x00, keyRef.getCrt())); + if (version.isLessThan(5, 0, 0)) { + setGenerationTime(keyRef, 0); } - - /** - * Generate an EC key in the given slot. - *

- * Requires Admin PIN verification. - * - * @param keyRef the key slot to generate a key in - * @param curve the elliptic curve of the key to generate - * @return the public key of the generated key pair - * @throws BadResponseException in case of incorrect YubiKey response - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public PublicKeyValues generateEcKey(KeyRef keyRef, OpenPgpCurve curve) throws - BadResponseException, ApduException, IOException { - require(FEATURE_EC_KEYS); - Logger.debug(logger, "Generating EC private key for {}", keyRef); - - setAlgorithmAttributes(keyRef, AlgorithmAttributes.Ec.create(keyRef, curve)); - - byte[] resp = protocol.sendAndReceive(new Apdu(0, INS_GENERATE_ASYM, 0x80, 0x00, keyRef.getCrt())); - if (version.isLessThan(5, 0, 0)) { - setGenerationTime(keyRef, 0); - } - Map data = Tlvs.decodeMap(Tlvs.unpackValue(TAG_PUBLIC_KEY, resp)); - Logger.info(logger, "EC key generated for {}", keyRef); - byte[] encoded = data.get(0x86); - if (curve == OpenPgpCurve.Ed25519 || curve == OpenPgpCurve.X25519) { - return new PublicKeyValues.Cv25519(curve.getValues(), encoded); - } - return PublicKeyValues.Ec.fromEncodedPoint(curve.getValues(), encoded); + Map data = Tlvs.decodeMap(Tlvs.unpackValue(TAG_PUBLIC_KEY, resp)); + Logger.info(logger, "RSA key generated for {}", keyRef); + return new PublicKeyValues.Rsa( + new BigInteger(1, data.get(0x81)), new BigInteger(1, data.get(0x82))); + } + + /** + * Generate an EC key in the given slot. + * + *

Requires Admin PIN verification. + * + * @param keyRef the key slot to generate a key in + * @param curve the elliptic curve of the key to generate + * @return the public key of the generated key pair + * @throws BadResponseException in case of incorrect YubiKey response + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public PublicKeyValues generateEcKey(KeyRef keyRef, OpenPgpCurve curve) + throws BadResponseException, ApduException, IOException { + require(FEATURE_EC_KEYS); + Logger.debug(logger, "Generating EC private key for {}", keyRef); + + setAlgorithmAttributes(keyRef, AlgorithmAttributes.Ec.create(keyRef, curve)); + + byte[] resp = + protocol.sendAndReceive(new Apdu(0, INS_GENERATE_ASYM, 0x80, 0x00, keyRef.getCrt())); + if (version.isLessThan(5, 0, 0)) { + setGenerationTime(keyRef, 0); } - - /** - * Import a private key into the give slot. - *

- * Requires Admin PIN verification. - * - * @param keyRef the slot to import the key into - * @param privateKey the private key to import - * @throws BadResponseException in case of incorrect YubiKey response - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public void putKey(KeyRef keyRef, PrivateKeyValues privateKey) throws - BadResponseException, ApduException, IOException { - Logger.debug(logger, "Importing a private key for {}", keyRef); - AlgorithmAttributes attributes = getKeyAttributes(privateKey, keyRef, version); - - if (getExtendedCapabilities().getFlags().contains(ExtendedCapabilityFlag.ALGORITHM_ATTRIBUTES_CHANGEABLE)) { - setAlgorithmAttributes(keyRef, attributes); - } else { - if (!(attributes instanceof AlgorithmAttributes.Rsa && - ((AlgorithmAttributes.Rsa) attributes).getNLen() == 2048)) { - throw new UnsupportedOperationException("This YubiKey only supports RSA 2048 keys"); - } - } - PrivateKeyTemplate template = null; - byte[] templateBytes = null; - try { - template = getKeyTemplate(privateKey, keyRef, version.isLessThan(4, 0, 0)); - templateBytes = template.getBytes(); - protocol.sendAndReceive(new Apdu(0, INS_PUT_DATA_ODD, 0x3f, 0xff, templateBytes)); - } finally { - if (templateBytes != null) { - Arrays.fill(templateBytes, (byte) 0); - } - if (template != null) { - template.destroy(); - } - } - - if (version.isLessThan(5, 0, 0)) { - setGenerationTime(keyRef, 0); - } - Logger.info(logger, "Private key imported for {}", keyRef); + Map data = Tlvs.decodeMap(Tlvs.unpackValue(TAG_PUBLIC_KEY, resp)); + Logger.info(logger, "EC key generated for {}", keyRef); + byte[] encoded = data.get(0x86); + if (curve == OpenPgpCurve.Ed25519 || curve == OpenPgpCurve.X25519) { + return new PublicKeyValues.Cv25519(curve.getValues(), encoded); } - - /** - * Read the public key from a slot. - * - * @param keyRef the key slot to read from - * @return the public key stored in the given slot - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - * @throws BadResponseException in case of incorrect YubiKey response - */ - public PublicKeyValues getPublicKey(KeyRef keyRef) throws - ApduException, IOException, BadResponseException { - Logger.debug(logger, "Getting public key for {}", keyRef); - byte[] resp = protocol.sendAndReceive(new Apdu(0, INS_GENERATE_ASYM, 0x81, 0x00, keyRef.getCrt())); - Map data = Tlvs.decodeMap(Tlvs.unpackValue(TAG_PUBLIC_KEY, resp)); - AlgorithmAttributes attributes = getApplicationRelatedData().getDiscretionary().getAlgorithmAttributes(keyRef); - if (attributes instanceof AlgorithmAttributes.Ec) { - byte[] encoded = data.get(0x86); - OpenPgpCurve curve = ((AlgorithmAttributes.Ec) attributes).getCurve(); - if (curve == OpenPgpCurve.Ed25519 || curve == OpenPgpCurve.X25519) { - return new PublicKeyValues.Cv25519(curve.getValues(), encoded); - } - return PublicKeyValues.Ec.fromEncodedPoint(curve.getValues(), encoded); - } else { - return new PublicKeyValues.Rsa( - new BigInteger(1, data.get(0x81)), - new BigInteger(1, data.get(0x82)) - ); - } + return PublicKeyValues.Ec.fromEncodedPoint(curve.getValues(), encoded); + } + + /** + * Import a private key into the give slot. + * + *

Requires Admin PIN verification. + * + * @param keyRef the slot to import the key into + * @param privateKey the private key to import + * @throws BadResponseException in case of incorrect YubiKey response + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public void putKey(KeyRef keyRef, PrivateKeyValues privateKey) + throws BadResponseException, ApduException, IOException { + Logger.debug(logger, "Importing a private key for {}", keyRef); + AlgorithmAttributes attributes = getKeyAttributes(privateKey, keyRef, version); + + if (getExtendedCapabilities() + .getFlags() + .contains(ExtendedCapabilityFlag.ALGORITHM_ATTRIBUTES_CHANGEABLE)) { + setAlgorithmAttributes(keyRef, attributes); + } else { + if (!(attributes instanceof AlgorithmAttributes.Rsa + && ((AlgorithmAttributes.Rsa) attributes).getNLen() == 2048)) { + throw new UnsupportedOperationException("This YubiKey only supports RSA 2048 keys"); + } } - - /** - * Deletes the key in a key slot. - * - * @param keyRef the slot to delete - * @throws BadResponseException in case of incorrect YubiKey response - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public void deleteKey(KeyRef keyRef) throws - BadResponseException, ApduException, IOException { - Logger.debug(logger, "Deleting private key for {}", keyRef); - if (version.isLessThan(4, 0, 0)) { - Logger.debug(logger, "Overwriting with dummy key"); - // Import over the key, using a dummy - try { - KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); - rsaGen.initialize(2048); - putKey(keyRef, PrivateKeyValues.fromPrivateKey(rsaGen.generateKeyPair().getPrivate())); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } else { - Logger.debug(logger, "Changing algorithm attributes"); - // Delete key by changing the key attributes twice. - // Use putData to avoid checking for RSA 4096 support - putData( - keyRef.getAlgorithmAttributes(), - AlgorithmAttributes.Rsa.create(4096, AlgorithmAttributes.Rsa.ImportFormat.STANDARD).getBytes() - ); - setAlgorithmAttributes( - keyRef, - AlgorithmAttributes.Rsa.create(2048, AlgorithmAttributes.Rsa.ImportFormat.STANDARD) - ); - } - Logger.info(logger, "Private key deleted for {}", keyRef); + PrivateKeyTemplate template = null; + byte[] templateBytes = null; + try { + template = getKeyTemplate(privateKey, keyRef, version.isLessThan(4, 0, 0)); + templateBytes = template.getBytes(); + protocol.sendAndReceive(new Apdu(0, INS_PUT_DATA_ODD, 0x3f, 0xff, templateBytes)); + } finally { + if (templateBytes != null) { + Arrays.fill(templateBytes, (byte) 0); + } + if (template != null) { + template.destroy(); + } } - static byte[] formatDssSignature(byte[] response) { - int split = response.length / 2; - BigInteger r = new BigInteger(1, Arrays.copyOfRange(response, 0, split)); - BigInteger s = new BigInteger(1, Arrays.copyOfRange(response, split, response.length)); - return new Tlv(0x30, Tlvs.encodeList(Arrays.asList( - new Tlv(0x02, r.toByteArray()), - new Tlv(0x02, s.toByteArray()) - ))).getBytes(); + if (version.isLessThan(5, 0, 0)) { + setGenerationTime(keyRef, 0); } - - /** - * Signs a message using the SIG key. - *

- * NOTE: This performs a raw signature. Messages should be hashed and/or padded prior. - * Requires User PIN verification. - * - * @param payload the message to sign - * @return the generated signature - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public byte[] sign(byte[] payload) throws ApduException, IOException { - AlgorithmAttributes attributes = Objects.requireNonNull( - getApplicationRelatedData().getDiscretionary().getAlgorithmAttributes(KeyRef.SIG) - ); - Logger.debug(logger, "Signing a message with {}", attributes); - byte[] response = protocol.sendAndReceive(new Apdu(0, INS_PSO, 0x9e, 0x9a, payload)); - Logger.info(logger, "Message signed"); - if (attributes.getAlgorithmId() == 0x13) { - return formatDssSignature(response); - } - return response; + Logger.info(logger, "Private key imported for {}", keyRef); + } + + /** + * Read the public key from a slot. + * + * @param keyRef the key slot to read from + * @return the public key stored in the given slot + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + * @throws BadResponseException in case of incorrect YubiKey response + */ + public PublicKeyValues getPublicKey(KeyRef keyRef) + throws ApduException, IOException, BadResponseException { + Logger.debug(logger, "Getting public key for {}", keyRef); + byte[] resp = + protocol.sendAndReceive(new Apdu(0, INS_GENERATE_ASYM, 0x81, 0x00, keyRef.getCrt())); + Map data = Tlvs.decodeMap(Tlvs.unpackValue(TAG_PUBLIC_KEY, resp)); + AlgorithmAttributes attributes = + getApplicationRelatedData().getDiscretionary().getAlgorithmAttributes(keyRef); + if (attributes instanceof AlgorithmAttributes.Ec) { + byte[] encoded = data.get(0x86); + OpenPgpCurve curve = ((AlgorithmAttributes.Ec) attributes).getCurve(); + if (curve == OpenPgpCurve.Ed25519 || curve == OpenPgpCurve.X25519) { + return new PublicKeyValues.Cv25519(curve.getValues(), encoded); + } + return PublicKeyValues.Ec.fromEncodedPoint(curve.getValues(), encoded); + } else { + return new PublicKeyValues.Rsa( + new BigInteger(1, data.get(0x81)), new BigInteger(1, data.get(0x82))); } - - /** - * Decrypts a value using the DEC key. - *

- * This method should be used for RSA keys to perform an RSA decryption using PKCS#1 v1.5 padding. - * For RSA the `value` should be an encrypted block. - * For ECDH the `value` should be a peer public-key to perform the key exchange - * with, and the result will be the derived shared secret. - *

- * Requires (extended) User PIN verification. - * - * @param payload the ciphertext to decrypt - * @return the decrypted and unpadded plaintext - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public byte[] decrypt(byte[] payload) throws ApduException, IOException { - Logger.debug(logger, "Decrypting a value"); - byte[] response = protocol.sendAndReceive( - new Apdu(0, INS_PSO, 0x80, 0x86, - ByteBuffer.allocate(payload.length + 1) - .put((byte) 0) - .put(payload).array() - ) - ); - Logger.info(logger, "Value decrypted"); - return response; + } + + /** + * Deletes the key in a key slot. + * + * @param keyRef the slot to delete + * @throws BadResponseException in case of incorrect YubiKey response + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public void deleteKey(KeyRef keyRef) throws BadResponseException, ApduException, IOException { + Logger.debug(logger, "Deleting private key for {}", keyRef); + if (version.isLessThan(4, 0, 0)) { + Logger.debug(logger, "Overwriting with dummy key"); + // Import over the key, using a dummy + try { + KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); + rsaGen.initialize(2048); + putKey(keyRef, PrivateKeyValues.fromPrivateKey(rsaGen.generateKeyPair().getPrivate())); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } else { + Logger.debug(logger, "Changing algorithm attributes"); + // Delete key by changing the key attributes twice. + // Use putData to avoid checking for RSA 4096 support + putData( + keyRef.getAlgorithmAttributes(), + AlgorithmAttributes.Rsa.create(4096, AlgorithmAttributes.Rsa.ImportFormat.STANDARD) + .getBytes()); + setAlgorithmAttributes( + keyRef, + AlgorithmAttributes.Rsa.create(2048, AlgorithmAttributes.Rsa.ImportFormat.STANDARD)); } - - /** - * Performs an ECDH key agreement using the DEC key. - *

- * This method should be used for EC keys where encryption is done using a shared secret. - * - * @param peerPublicKey the public key to perform the agreement with - * @return the key agreement shared secret - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public byte[] decrypt(PublicKeyValues peerPublicKey) throws ApduException, IOException { - byte[] encodedPoint; - if (peerPublicKey instanceof PublicKeyValues.Ec) { - encodedPoint = ((PublicKeyValues.Ec) peerPublicKey).getEncodedPoint(); - } else if (peerPublicKey instanceof PublicKeyValues.Cv25519) { - encodedPoint = ((PublicKeyValues.Cv25519) peerPublicKey).getBytes(); - } else { - throw new IllegalArgumentException("peerPublicKey must be an Elliptic Curve key"); - } - - byte[] response = protocol.sendAndReceive( - new Apdu(0, INS_PSO, 0x80, 0x86, - new Tlv(0xA6, - new Tlv(0x7F49, - new Tlv(0x86, encodedPoint).getBytes() - ).getBytes() - ).getBytes())); - Logger.info(logger, "ECDH key agreement performed"); - return response; + Logger.info(logger, "Private key deleted for {}", keyRef); + } + + static byte[] formatDssSignature(byte[] response) { + int split = response.length / 2; + BigInteger r = new BigInteger(1, Arrays.copyOfRange(response, 0, split)); + BigInteger s = new BigInteger(1, Arrays.copyOfRange(response, split, response.length)); + return new Tlv( + 0x30, + Tlvs.encodeList( + Arrays.asList(new Tlv(0x02, r.toByteArray()), new Tlv(0x02, s.toByteArray())))) + .getBytes(); + } + + /** + * Signs a message using the SIG key. + * + *

NOTE: This performs a raw signature. Messages should be hashed and/or padded prior. Requires + * User PIN verification. + * + * @param payload the message to sign + * @return the generated signature + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public byte[] sign(byte[] payload) throws ApduException, IOException { + AlgorithmAttributes attributes = + Objects.requireNonNull( + getApplicationRelatedData().getDiscretionary().getAlgorithmAttributes(KeyRef.SIG)); + Logger.debug(logger, "Signing a message with {}", attributes); + byte[] response = protocol.sendAndReceive(new Apdu(0, INS_PSO, 0x9e, 0x9a, payload)); + Logger.info(logger, "Message signed"); + if (attributes.getAlgorithmId() == 0x13) { + return formatDssSignature(response); } - - /** - * Authenticates a message using the AUT key. - *

- * Requires User PIN verification. - * - * @param payload the message to authenticate - * @return the generated signature - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public byte[] authenticate(byte[] payload) throws ApduException, IOException { - AlgorithmAttributes attributes = Objects.requireNonNull( - getApplicationRelatedData().getDiscretionary().getAlgorithmAttributes(KeyRef.AUT) - ); - Logger.debug(logger, "Authenticating a message with {}", attributes); - byte[] response = protocol.sendAndReceive( - new Apdu(0, INS_INTERNAL_AUTHENTICATE, 0x0, 0x0, payload) - ); - Logger.info(logger, "Message authenticated"); - if (attributes.getAlgorithmId() == 0x13) { - return formatDssSignature(response); - } - return response; + return response; + } + + /** + * Decrypts a value using the DEC key. + * + *

This method should be used for RSA keys to perform an RSA decryption using PKCS#1 v1.5 + * padding. For RSA the `value` should be an encrypted block. For ECDH the `value` should be a + * peer public-key to perform the key exchange with, and the result will be the derived shared + * secret. + * + *

Requires (extended) User PIN verification. + * + * @param payload the ciphertext to decrypt + * @return the decrypted and unpadded plaintext + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public byte[] decrypt(byte[] payload) throws ApduException, IOException { + Logger.debug(logger, "Decrypting a value"); + byte[] response = + protocol.sendAndReceive( + new Apdu( + 0, + INS_PSO, + 0x80, + 0x86, + ByteBuffer.allocate(payload.length + 1).put((byte) 0).put(payload).array())); + Logger.info(logger, "Value decrypted"); + return response; + } + + /** + * Performs an ECDH key agreement using the DEC key. + * + *

This method should be used for EC keys where encryption is done using a shared secret. + * + * @param peerPublicKey the public key to perform the agreement with + * @return the key agreement shared secret + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public byte[] decrypt(PublicKeyValues peerPublicKey) throws ApduException, IOException { + byte[] encodedPoint; + if (peerPublicKey instanceof PublicKeyValues.Ec) { + encodedPoint = ((PublicKeyValues.Ec) peerPublicKey).getEncodedPoint(); + } else if (peerPublicKey instanceof PublicKeyValues.Cv25519) { + encodedPoint = ((PublicKeyValues.Cv25519) peerPublicKey).getBytes(); + } else { + throw new IllegalArgumentException("peerPublicKey must be an Elliptic Curve key"); } - /** - * Creates an attestation certificate for a key. - *

- * The certificate is written to the certificate slot for the key, and its - * content is returned. - *

- * Requires User PIN verification. - * - * @param keyRef the slot to attest - * @return the attestation certificate - * @throws ApduException in case of an error response from the YubiKey - * @throws IOException in case of connection error - */ - public X509Certificate attestKey(KeyRef keyRef) throws ApduException, IOException { - require(FEATURE_ATTESTATION); - - Logger.debug(logger, "Attesting key {}", keyRef); - protocol.sendAndReceive(new Apdu(0x80, INS_GET_ATTESTATION, keyRef.getValue(), 0, null)); - Logger.info(logger, "Attestation certificate created for {}", keyRef); - - return Objects.requireNonNull(getCertificate(keyRef)); + byte[] response = + protocol.sendAndReceive( + new Apdu( + 0, + INS_PSO, + 0x80, + 0x86, + new Tlv(0xA6, new Tlv(0x7F49, new Tlv(0x86, encodedPoint).getBytes()).getBytes()) + .getBytes())); + Logger.info(logger, "ECDH key agreement performed"); + return response; + } + + /** + * Authenticates a message using the AUT key. + * + *

Requires User PIN verification. + * + * @param payload the message to authenticate + * @return the generated signature + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public byte[] authenticate(byte[] payload) throws ApduException, IOException { + AlgorithmAttributes attributes = + Objects.requireNonNull( + getApplicationRelatedData().getDiscretionary().getAlgorithmAttributes(KeyRef.AUT)); + Logger.debug(logger, "Authenticating a message with {}", attributes); + byte[] response = + protocol.sendAndReceive(new Apdu(0, INS_INTERNAL_AUTHENTICATE, 0x0, 0x0, payload)); + Logger.info(logger, "Message authenticated"); + if (attributes.getAlgorithmId() == 0x13) { + return formatDssSignature(response); } -} \ No newline at end of file + return response; + } + + /** + * Creates an attestation certificate for a key. + * + *

The certificate is written to the certificate slot for the key, and its content is returned. + * + *

Requires User PIN verification. + * + * @param keyRef the slot to attest + * @return the attestation certificate + * @throws ApduException in case of an error response from the YubiKey + * @throws IOException in case of connection error + */ + public X509Certificate attestKey(KeyRef keyRef) throws ApduException, IOException { + require(FEATURE_ATTESTATION); + + Logger.debug(logger, "Attesting key {}", keyRef); + protocol.sendAndReceive(new Apdu(0x80, INS_GET_ATTESTATION, keyRef.getValue(), 0, null)); + Logger.info(logger, "Attestation certificate created for {}", keyRef); + + return Objects.requireNonNull(getCertificate(keyRef)); + } +} diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpUtils.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpUtils.java index c17e6f95..29e2476a 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpUtils.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/OpenPgpUtils.java @@ -17,12 +17,12 @@ package com.yubico.yubikit.openpgp; public class OpenPgpUtils { - static byte decodeBcd(byte bcd) { - int high = (bcd & 0xf0) >> 4; - int low = bcd & 0x0f; - if(high > 9 || low > 9) { - throw new IllegalArgumentException("Invalid BCD value: " + bcd); - } - return (byte) (high * 10 + low); + static byte decodeBcd(byte bcd) { + int high = (bcd & 0xf0) >> 4; + int low = bcd & 0x0f; + if (high > 9 || low > 9) { + throw new IllegalArgumentException("Invalid BCD value: " + bcd); } + return (byte) (high * 10 + low); + } } diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/PinPolicy.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/PinPolicy.java index 513b778c..63d3e5a0 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/PinPolicy.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/PinPolicy.java @@ -17,18 +17,18 @@ package com.yubico.yubikit.openpgp; public enum PinPolicy { - ALWAYS((byte) 0x00), - ONCE((byte) 0x01); + ALWAYS((byte) 0x00), + ONCE((byte) 0x01); - public final byte value; + public final byte value; - PinPolicy(byte value) { - this.value = value; - } + PinPolicy(byte value) { + this.value = value; + } - @Override - public String toString() { - String name = name(); - return name.charAt(0) + name.substring(1).toLowerCase(); - } + @Override + public String toString() { + String name = name(); + return name.charAt(0) + name.substring(1).toLowerCase(); + } } diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/PrivateKeyTemplate.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/PrivateKeyTemplate.java index 5a6034dd..3ab82345 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/PrivateKeyTemplate.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/PrivateKeyTemplate.java @@ -18,160 +18,147 @@ import com.yubico.yubikit.core.util.Tlv; import com.yubico.yubikit.core.util.Tlvs; - import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; - import javax.annotation.Nullable; import javax.security.auth.Destroyable; abstract class PrivateKeyTemplate implements Destroyable { - private final byte[] crt; - - private boolean destroyed = false; - - PrivateKeyTemplate(byte[] crt) { - this.crt = crt; + private final byte[] crt; + + private boolean destroyed = false; + + PrivateKeyTemplate(byte[] crt) { + this.crt = crt; + } + + abstract List getTemplate(); + + @Override + public void destroy() { + Arrays.fill(crt, (byte) 0); + destroyed = true; + } + + @Override + public boolean isDestroyed() { + return destroyed; + } + + byte[] getBytes() { + ByteBuffer headers = ByteBuffer.allocate(1024); + ByteBuffer values = ByteBuffer.allocate(1024); + for (Tlv tlv : getTemplate()) { + byte[] tlvBytes = tlv.getBytes(); + headers.put(tlvBytes, 0, tlvBytes.length - tlv.getLength()); + values.put(tlv.getValue()); + } + headers.flip(); + byte[] headersBytes = new byte[headers.remaining()]; + headers.get(headersBytes); + + values.flip(); + byte[] valuesBytes = new byte[values.remaining()]; + values.get(valuesBytes); + + byte[] tlvBytes = + Tlvs.encodeList(Arrays.asList(new Tlv(0x7f48, headersBytes), new Tlv(0x5f48, valuesBytes))); + + return new Tlv( + 0x4d, ByteBuffer.allocate(crt.length + tlvBytes.length).put(crt).put(tlvBytes).array()) + .getBytes(); + } + + static class Rsa extends PrivateKeyTemplate { + final byte[] e; + final byte[] p; + final byte[] q; + + Rsa(byte[] crt, byte[] e, byte[] p, byte[] q) { + super(crt); + this.e = e; + this.p = p; + this.q = q; } - abstract List getTemplate(); + @Override + List getTemplate() { + return Arrays.asList(new Tlv(0x91, e), new Tlv(0x92, p), new Tlv(0x93, q)); + } @Override public void destroy() { - Arrays.fill(crt, (byte) 0); - destroyed = true; + Arrays.fill(e, (byte) 0); + Arrays.fill(p, (byte) 0); + Arrays.fill(q, (byte) 0); + super.destroy(); + } + } + + static class RsaCrt extends Rsa { + final byte[] iqmp; + final byte[] dmp1; + final byte[] dmq1; + final byte[] n; + + RsaCrt( + byte[] crt, byte[] e, byte[] p, byte[] q, byte[] iqmp, byte[] dmp1, byte[] dmq1, byte[] n) { + super(crt, e, p, q); + this.iqmp = iqmp; + this.dmp1 = dmp1; + this.dmq1 = dmq1; + this.n = n; } @Override - public boolean isDestroyed() { - return destroyed; - } + List getTemplate() { + List tlvs = new ArrayList<>(super.getTemplate()); + tlvs.addAll( + Arrays.asList( + new Tlv(0x94, iqmp), new Tlv(0x95, dmp1), new Tlv(0x96, dmq1), new Tlv(0x97, n))); - byte[] getBytes() { - ByteBuffer headers = ByteBuffer.allocate(1024); - ByteBuffer values = ByteBuffer.allocate(1024); - for (Tlv tlv : getTemplate()) { - byte[] tlvBytes = tlv.getBytes(); - headers.put(tlvBytes, 0, tlvBytes.length - tlv.getLength()); - values.put(tlv.getValue()); - } - headers.flip(); - byte[] headersBytes = new byte[headers.remaining()]; - headers.get(headersBytes); - - values.flip(); - byte[] valuesBytes = new byte[values.remaining()]; - values.get(valuesBytes); - - byte[] tlvBytes = Tlvs.encodeList( - Arrays.asList( - new Tlv(0x7f48, headersBytes), - new Tlv(0x5f48, valuesBytes))); - - return new Tlv(0x4d, ByteBuffer.allocate(crt.length + tlvBytes.length) - .put(crt) - .put(tlvBytes) - .array()).getBytes(); + return tlvs; } - static class Rsa extends PrivateKeyTemplate { - final byte[] e; - final byte[] p; - final byte[] q; - - Rsa(byte[] crt, byte[] e, byte[] p, byte[] q) { - super(crt); - this.e = e; - this.p = p; - this.q = q; - } - - @Override - List getTemplate() { - return Arrays.asList( - new Tlv(0x91, e), - new Tlv(0x92, p), - new Tlv(0x93, q) - ); - } - - @Override - public void destroy() { - Arrays.fill(e, (byte) 0); - Arrays.fill(p, (byte) 0); - Arrays.fill(q, (byte) 0); - super.destroy(); - } + @Override + public void destroy() { + Arrays.fill(iqmp, (byte) 0); + Arrays.fill(dmp1, (byte) 0); + Arrays.fill(dmq1, (byte) 0); + Arrays.fill(n, (byte) 0); + super.destroy(); } + } + + static class Ec extends PrivateKeyTemplate { + final byte[] privateKey; + @Nullable final byte[] publicKey; - static class RsaCrt extends Rsa { - final byte[] iqmp; - final byte[] dmp1; - final byte[] dmq1; - final byte[] n; - - RsaCrt(byte[] crt, byte[] e, byte[] p, byte[] q, byte[] iqmp, byte[] dmp1, byte[] dmq1, byte[] n) { - super(crt, e, p, q); - this.iqmp = iqmp; - this.dmp1 = dmp1; - this.dmq1 = dmq1; - this.n = n; - } - - @Override - List getTemplate() { - List tlvs = new ArrayList<>(super.getTemplate()); - tlvs.addAll(Arrays.asList( - new Tlv(0x94, iqmp), - new Tlv(0x95, dmp1), - new Tlv(0x96, dmq1), - new Tlv(0x97, n) - - )); - return tlvs; - } - - @Override - public void destroy() { - Arrays.fill(iqmp, (byte) 0); - Arrays.fill(dmp1, (byte) 0); - Arrays.fill(dmq1, (byte) 0); - Arrays.fill(n, (byte) 0); - super.destroy(); - } + Ec(byte[] crt, byte[] privateKey, @Nullable byte[] publicKey) { + super(crt); + this.privateKey = privateKey; + this.publicKey = publicKey; + } + @Override + List getTemplate() { + List tlvs = new ArrayList<>(); + tlvs.add(new Tlv(0x92, privateKey)); + if (publicKey != null) { + tlvs.add(new Tlv(0x99, publicKey)); + } + return tlvs; } - static class Ec extends PrivateKeyTemplate { - final byte[] privateKey; - @Nullable - final byte[] publicKey; - - Ec(byte[] crt, byte[] privateKey, @Nullable byte[] publicKey) { - super(crt); - this.privateKey = privateKey; - this.publicKey = publicKey; - } - - @Override - List getTemplate() { - List tlvs = new ArrayList<>(); - tlvs.add(new Tlv(0x92, privateKey)); - if (publicKey != null) { - tlvs.add(new Tlv(0x99, publicKey)); - } - return tlvs; - } - - @Override - public void destroy() { - Arrays.fill(privateKey, (byte) 0); - if (publicKey != null) { - Arrays.fill(publicKey, (byte) 0); - } - super.destroy(); - } + @Override + public void destroy() { + Arrays.fill(privateKey, (byte) 0); + if (publicKey != null) { + Arrays.fill(publicKey, (byte) 0); + } + super.destroy(); } + } } diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/Pw.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/Pw.java index c8019dd8..652f6213 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/Pw.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/Pw.java @@ -17,17 +17,19 @@ package com.yubico.yubikit.openpgp; public enum Pw { - USER((byte) 0x81), RESET((byte) 0x82), ADMIN((byte) 0x83); - public static final char[] DEFAULT_USER_PIN = "123456".toCharArray(); - public static final char[] DEFAULT_ADMIN_PIN = "12345678".toCharArray(); + USER((byte) 0x81), + RESET((byte) 0x82), + ADMIN((byte) 0x83); + public static final char[] DEFAULT_USER_PIN = "123456".toCharArray(); + public static final char[] DEFAULT_ADMIN_PIN = "12345678".toCharArray(); - private final byte value; + private final byte value; - Pw(byte value) { - this.value = value; - } + Pw(byte value) { + this.value = value; + } - public byte getValue() { - return value; - } + public byte getValue() { + return value; + } } diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/PwStatus.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/PwStatus.java index 1c589565..14ac113b 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/PwStatus.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/PwStatus.java @@ -19,82 +19,81 @@ import java.nio.ByteBuffer; public class PwStatus { - private final PinPolicy pinPolicyUser; - private final int maxLenUser; - private final int maxLenReset; - private final int maxLenAdmin; - private final int attemptsUser; - private final int attemptsReset; - private final int attemptsAdmin; + private final PinPolicy pinPolicyUser; + private final int maxLenUser; + private final int maxLenReset; + private final int maxLenAdmin; + private final int attemptsUser; + private final int attemptsReset; + private final int attemptsAdmin; - PwStatus(PinPolicy pinPolicyUser, - int maxLenUser, - int maxLenReset, - int maxLenAdmin, - int attemptsUser, - int attemptsReset, - int attemptsAdmin - ) { - this.pinPolicyUser = pinPolicyUser; - this.maxLenUser = maxLenUser; - this.maxLenReset = maxLenReset; - this.maxLenAdmin = maxLenAdmin; - this.attemptsUser = attemptsUser; - this.attemptsReset = attemptsReset; - this.attemptsAdmin = attemptsAdmin; - } + PwStatus( + PinPolicy pinPolicyUser, + int maxLenUser, + int maxLenReset, + int maxLenAdmin, + int attemptsUser, + int attemptsReset, + int attemptsAdmin) { + this.pinPolicyUser = pinPolicyUser; + this.maxLenUser = maxLenUser; + this.maxLenReset = maxLenReset; + this.maxLenAdmin = maxLenAdmin; + this.attemptsUser = attemptsUser; + this.attemptsReset = attemptsReset; + this.attemptsAdmin = attemptsAdmin; + } - public PinPolicy getPinPolicyUser() { - return pinPolicyUser; - } + public PinPolicy getPinPolicyUser() { + return pinPolicyUser; + } - public int getMaxLenUser() { - return maxLenUser; - } + public int getMaxLenUser() { + return maxLenUser; + } - public int getMaxLenReset() { - return maxLenReset; - } + public int getMaxLenReset() { + return maxLenReset; + } - public int getMaxLenAdmin() { - return maxLenAdmin; - } + public int getMaxLenAdmin() { + return maxLenAdmin; + } - public int getAttemptsUser() { - return attemptsUser; - } + public int getAttemptsUser() { + return attemptsUser; + } - public int getAttemptsReset() { - return attemptsReset; - } + public int getAttemptsReset() { + return attemptsReset; + } - public int getAttemptsAdmin() { - return attemptsAdmin; - } + public int getAttemptsAdmin() { + return attemptsAdmin; + } - int getAttempts(Pw pw) { - switch (pw) { - case USER: - return attemptsUser; - case RESET: - return attemptsReset; - case ADMIN: - return attemptsAdmin; - default: - throw new IllegalArgumentException(); - } + int getAttempts(Pw pw) { + switch (pw) { + case USER: + return attemptsUser; + case RESET: + return attemptsReset; + case ADMIN: + return attemptsAdmin; + default: + throw new IllegalArgumentException(); } + } - static PwStatus parse(byte[] encoded) { - ByteBuffer buf = ByteBuffer.wrap(encoded); - return new PwStatus( - buf.get() == 0 ? PinPolicy.ALWAYS : PinPolicy.ONCE, - 0xff & buf.get(), - 0xff & buf.get(), - 0xff & buf.get(), - 0xff & buf.get(), - 0xff & buf.get(), - 0xff & buf.get() - ); - } + static PwStatus parse(byte[] encoded) { + ByteBuffer buf = ByteBuffer.wrap(encoded); + return new PwStatus( + buf.get() == 0 ? PinPolicy.ALWAYS : PinPolicy.ONCE, + 0xff & buf.get(), + 0xff & buf.get(), + 0xff & buf.get(), + 0xff & buf.get(), + 0xff & buf.get(), + 0xff & buf.get()); + } } diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/SecuritySupportTemplate.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/SecuritySupportTemplate.java index 1a096685..b3d5c417 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/SecuritySupportTemplate.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/SecuritySupportTemplate.java @@ -18,29 +18,29 @@ import com.yubico.yubikit.core.application.BadResponseException; import com.yubico.yubikit.core.util.Tlvs; - import java.nio.ByteBuffer; import java.util.Map; public class SecuritySupportTemplate { - static private final int TAG_SIGNATURE_COUNTER = 0x93; - private final int signatureCounter; + private static final int TAG_SIGNATURE_COUNTER = 0x93; + private final int signatureCounter; - public SecuritySupportTemplate(int signatureCounter) { - this.signatureCounter = signatureCounter; - } + public SecuritySupportTemplate(int signatureCounter) { + this.signatureCounter = signatureCounter; + } - public int getSignatureCounter() { - return signatureCounter; - } + public int getSignatureCounter() { + return signatureCounter; + } - static SecuritySupportTemplate parse(byte[] encoded) { - try { - Map data = Tlvs.decodeMap(Tlvs.unpackValue(Do.SECURITY_SUPPORT_TEMPLATE, encoded)); - ByteBuffer buf = ByteBuffer.wrap(data.get(TAG_SIGNATURE_COUNTER)); - return new SecuritySupportTemplate(((buf.get() & 0xff) << 16) | buf.getShort()); - } catch (BadResponseException e) { - throw new IllegalArgumentException(e); - } + static SecuritySupportTemplate parse(byte[] encoded) { + try { + Map data = + Tlvs.decodeMap(Tlvs.unpackValue(Do.SECURITY_SUPPORT_TEMPLATE, encoded)); + ByteBuffer buf = ByteBuffer.wrap(data.get(TAG_SIGNATURE_COUNTER)); + return new SecuritySupportTemplate(((buf.get() & 0xff) << 16) | buf.getShort()); + } catch (BadResponseException e) { + throw new IllegalArgumentException(e); } + } } diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/Uif.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/Uif.java index 4dc665db..443964e3 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/Uif.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/Uif.java @@ -17,49 +17,49 @@ package com.yubico.yubikit.openpgp; public enum Uif { - OFF((byte) 0x00), - ON((byte) 0x01), - FIXED((byte) 0x02), - CACHED((byte) 0x03), - CACHED_FIXED((byte) 0x04); + OFF((byte) 0x00), + ON((byte) 0x01), + FIXED((byte) 0x02), + CACHED((byte) 0x03), + CACHED_FIXED((byte) 0x04); - public final byte value; + public final byte value; - Uif(byte value) { - this.value = value; - } - - public boolean isFixed() { - return this == Uif.FIXED || this == Uif.CACHED_FIXED; - } + Uif(byte value) { + this.value = value; + } - public boolean isCached() { - return this == Uif.CACHED || this == Uif.CACHED_FIXED; - } + public boolean isFixed() { + return this == Uif.FIXED || this == Uif.CACHED_FIXED; + } - @Override - public String toString() { - if (this == Uif.FIXED) { - return "On (fixed)"; - } - if (this == Uif.CACHED_FIXED) { - return "Cached (fixed)"; - } + public boolean isCached() { + return this == Uif.CACHED || this == Uif.CACHED_FIXED; + } - String name = name(); - return name.charAt(0) + name.substring(1).toLowerCase(); + @Override + public String toString() { + if (this == Uif.FIXED) { + return "On (fixed)"; } - - public static Uif fromValue(byte value) { - for (Uif type : Uif.values()) { - if (type.value == value) { - return type; - } - } - throw new IllegalArgumentException("Not a valid UIF:" + value); + if (this == Uif.CACHED_FIXED) { + return "Cached (fixed)"; } - public byte[] getBytes() { - return new byte[]{value, GeneralFeatureManagement.BUTTON.value}; + String name = name(); + return name.charAt(0) + name.substring(1).toLowerCase(); + } + + public static Uif fromValue(byte value) { + for (Uif type : Uif.values()) { + if (type.value == value) { + return type; + } } + throw new IllegalArgumentException("Not a valid UIF:" + value); + } + + public byte[] getBytes() { + return new byte[] {value, GeneralFeatureManagement.BUTTON.value}; + } } diff --git a/openpgp/src/main/java/com/yubico/yubikit/openpgp/package-info.java b/openpgp/src/main/java/com/yubico/yubikit/openpgp/package-info.java index 9a0f0d21..160685cd 100644 --- a/openpgp/src/main/java/com/yubico/yubikit/openpgp/package-info.java +++ b/openpgp/src/main/java/com/yubico/yubikit/openpgp/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.openpgp; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/piv/src/main/java/com/yubico/yubikit/piv/BioMetadata.java b/piv/src/main/java/com/yubico/yubikit/piv/BioMetadata.java index 4fec1020..c4daa5ae 100644 --- a/piv/src/main/java/com/yubico/yubikit/piv/BioMetadata.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/BioMetadata.java @@ -17,47 +17,47 @@ package com.yubico.yubikit.piv; public class BioMetadata { - final private boolean configured; - final private int attemptsRemaining; - final private boolean temporaryPin; + private final boolean configured; + private final int attemptsRemaining; + private final boolean temporaryPin; - public BioMetadata(boolean configured, int attemptsRemaining, boolean temporaryPin) { - this.configured = configured; - this.attemptsRemaining = attemptsRemaining; - this.temporaryPin = temporaryPin; - } + public BioMetadata(boolean configured, int attemptsRemaining, boolean temporaryPin) { + this.configured = configured; + this.attemptsRemaining = attemptsRemaining; + this.temporaryPin = temporaryPin; + } - /** - * Indicates whether biometrics are configured or not (fingerprints enrolled or not). - *

- * A false return value indicates a YubiKey Bio without biometrics configured and hence the - * client should fallback to a PIN based authentication. - * - * @return true if biometrics are configured or not. - */ - public boolean isConfigured() { - return configured; - } + /** + * Indicates whether biometrics are configured or not (fingerprints enrolled or not). + * + *

A false return value indicates a YubiKey Bio without biometrics configured and hence the + * client should fallback to a PIN based authentication. + * + * @return true if biometrics are configured or not. + */ + public boolean isConfigured() { + return configured; + } - /** - * Returns value of biometric match retry counter which states how many biometric match retries - * are left until a YubiKey Bio is blocked. - *

- * If this method returns 0 and {@link #isConfigured()} returns true, the device is blocked for - * biometric match and the client should invoke PIN based authentication to reset the biometric - * match retry counter. - */ - public int getAttemptsRemaining() { - return attemptsRemaining; - } + /** + * Returns value of biometric match retry counter which states how many biometric match retries + * are left until a YubiKey Bio is blocked. + * + *

If this method returns 0 and {@link #isConfigured()} returns true, the device is blocked for + * biometric match and the client should invoke PIN based authentication to reset the biometric + * match retry counter. + */ + public int getAttemptsRemaining() { + return attemptsRemaining; + } - /** - * Indicates whether a temporary PIN has been generated in the YubiKey in relation to a - * successful biometric match. - * - * @return true if a temporary PIN has been generated. - */ - public boolean hasTemporaryPin() { - return temporaryPin; - } + /** + * Indicates whether a temporary PIN has been generated in the YubiKey in relation to a successful + * biometric match. + * + * @return true if a temporary PIN has been generated. + */ + public boolean hasTemporaryPin() { + return temporaryPin; + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/GzipUtils.java b/piv/src/main/java/com/yubico/yubikit/piv/GzipUtils.java index c63f7092..5cdbce49 100644 --- a/piv/src/main/java/com/yubico/yubikit/piv/GzipUtils.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/GzipUtils.java @@ -17,57 +17,57 @@ package com.yubico.yubikit.piv; import com.yubico.yubikit.core.internal.Logger; - -import org.slf4j.LoggerFactory; - import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; +import org.slf4j.LoggerFactory; /** - * Utilities for GZIP (RFC1952) + * Utilities for GZIP + * + * @see RFC1952 */ public class GzipUtils { - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(GzipUtils.class); + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(GzipUtils.class); - /** - * @param input byte array to be compressed - * @return byte array of gzip compressed data - * @throws IOException if the compression failed - */ - static byte[] compress(byte[] input) throws IOException { - Logger.debug(logger, "Compressing {} bytes", input.length); - try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(input.length); - GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream)) { - gzipOutputStream.write(input); - gzipOutputStream.finish(); - Logger.debug(logger, "Compressed to {} bytes", byteArrayOutputStream.size()); - return byteArrayOutputStream.toByteArray(); - } + /** + * @param input byte array to be compressed + * @return byte array of gzip compressed data + * @throws IOException if the compression failed + */ + static byte[] compress(byte[] input) throws IOException { + Logger.debug(logger, "Compressing {} bytes", input.length); + try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(input.length); + GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream)) { + gzipOutputStream.write(input); + gzipOutputStream.finish(); + Logger.debug(logger, "Compressed to {} bytes", byteArrayOutputStream.size()); + return byteArrayOutputStream.toByteArray(); } + } - /** - * @param input byte array of gzip data to be uncompressed - * @return uncompressed data - * @throws IOException if the decompression failed - */ - static byte[] decompress(byte[] input) throws IOException { - Logger.debug(logger, "Decompressing {} bytes", input.length); - final int BUFFER_SIZE = 512; - byte[] buffer = new byte[BUFFER_SIZE]; - int bytesRead; - try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(input); - GZIPInputStream gzipInputStream = new GZIPInputStream(byteArrayInputStream)) { + /** + * @param input byte array of gzip data to be uncompressed + * @return uncompressed data + * @throws IOException if the decompression failed + */ + static byte[] decompress(byte[] input) throws IOException { + Logger.debug(logger, "Decompressing {} bytes", input.length); + final int BUFFER_SIZE = 512; + byte[] buffer = new byte[BUFFER_SIZE]; + int bytesRead; + try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(input); + GZIPInputStream gzipInputStream = new GZIPInputStream(byteArrayInputStream)) { - while ((bytesRead = gzipInputStream.read(buffer, 0, BUFFER_SIZE)) != -1) { - byteArrayOutputStream.write(buffer, 0, bytesRead); - } + while ((bytesRead = gzipInputStream.read(buffer, 0, BUFFER_SIZE)) != -1) { + byteArrayOutputStream.write(buffer, 0, bytesRead); + } - Logger.debug(logger, "Decompressed to {} bytes", byteArrayOutputStream.size()); - return byteArrayOutputStream.toByteArray(); - } + Logger.debug(logger, "Decompressed to {} bytes", byteArrayOutputStream.size()); + return byteArrayOutputStream.toByteArray(); } + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/InvalidPinException.java b/piv/src/main/java/com/yubico/yubikit/piv/InvalidPinException.java index 3a3d6cf3..65d1d261 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/InvalidPinException.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/InvalidPinException.java @@ -23,7 +23,7 @@ */ @Deprecated public class InvalidPinException extends com.yubico.yubikit.core.application.InvalidPinException { - public InvalidPinException(int attemptsRemaining) { - super(attemptsRemaining); - } + public InvalidPinException(int attemptsRemaining) { + super(attemptsRemaining); + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/KeyType.java b/piv/src/main/java/com/yubico/yubikit/piv/KeyType.java index 29632cda..770570d3 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/KeyType.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/KeyType.java @@ -19,175 +19,149 @@ import com.yubico.yubikit.core.keys.EllipticCurveValues; import com.yubico.yubikit.core.keys.PrivateKeyValues; import com.yubico.yubikit.core.keys.PublicKeyValues; - import java.security.Key; import java.security.PrivateKey; import java.security.PublicKey; import java.security.interfaces.RSAKey; - import javax.annotation.Nonnull; -/** - * Supported private key types for use with the PIV YubiKey application. - */ +/** Supported private key types for use with the PIV YubiKey application. */ public enum KeyType { - /** - * RSA with a 1024 bit key. - */ - RSA1024((byte) 0x06, new RsaKeyParams(1024)), - - /** - * RSA with a 2048 bit key. - */ - RSA2048((byte) 0x07, new RsaKeyParams(2048)), - - /** - * RSA with a 3072 bit key. - */ - RSA3072((byte) 0x05, new RsaKeyParams(3072)), - - /** - * RSA with a 4096 bit key. - */ - RSA4096((byte) 0x16, new RsaKeyParams(4096)), - - /** - * Elliptic Curve key, using NIST Curve P-256. - */ - ECCP256((byte) 0x11, new EcKeyParams(EllipticCurveValues.SECP256R1)), - /** - * Elliptic Curve key, using NIST Curve P-384. - */ - ECCP384((byte) 0x14, new EcKeyParams(EllipticCurveValues.SECP384R1)), - /** - * Edwards Digital Signature Algorithm (EdDSA) key, using Curve25519. - */ - ED25519((byte) 0xE0, new EcKeyParams(EllipticCurveValues.Ed25519)), - /** - * Elliptic-Curve Diffie-Hellman (ECDH) protocol key, using Curve25519. - */ - X25519((byte) 0xE1, new EcKeyParams(EllipticCurveValues.X25519)); - - public final byte value; - public final KeyParams params; - - KeyType(byte value, KeyParams params) { - this.value = value; - this.params = params; + /** RSA with a 1024 bit key. */ + RSA1024((byte) 0x06, new RsaKeyParams(1024)), + + /** RSA with a 2048 bit key. */ + RSA2048((byte) 0x07, new RsaKeyParams(2048)), + + /** RSA with a 3072 bit key. */ + RSA3072((byte) 0x05, new RsaKeyParams(3072)), + + /** RSA with a 4096 bit key. */ + RSA4096((byte) 0x16, new RsaKeyParams(4096)), + + /** Elliptic Curve key, using NIST Curve P-256. */ + ECCP256((byte) 0x11, new EcKeyParams(EllipticCurveValues.SECP256R1)), + /** Elliptic Curve key, using NIST Curve P-384. */ + ECCP384((byte) 0x14, new EcKeyParams(EllipticCurveValues.SECP384R1)), + /** Edwards Digital Signature Algorithm (EdDSA) key, using Curve25519. */ + ED25519((byte) 0xE0, new EcKeyParams(EllipticCurveValues.Ed25519)), + /** Elliptic-Curve Diffie-Hellman (ECDH) protocol key, using Curve25519. */ + X25519((byte) 0xE1, new EcKeyParams(EllipticCurveValues.X25519)); + + public final byte value; + public final KeyParams params; + + KeyType(byte value, KeyParams params) { + this.value = value; + this.params = params; + } + + /** Returns the key type corresponding to the given PIV algorithm constant. */ + public static KeyType fromValue(int value) { + for (KeyType type : KeyType.values()) { + if (type.value == value) { + return type; + } } - - /** - * Returns the key type corresponding to the given PIV algorithm constant. - */ - public static KeyType fromValue(int value) { - for (KeyType type : KeyType.values()) { - if (type.value == value) { - return type; - } + throw new IllegalArgumentException("Not a valid KeyType:" + value); + } + + public static KeyType fromKeyParams(PrivateKeyValues keyValues) { + if (keyValues instanceof PrivateKeyValues.Rsa) { + for (KeyType keyType : values()) { + if (keyType.params instanceof KeyType.RsaKeyParams) { + if (keyValues.getBitLength() == keyType.params.bitLength) { + return keyType; + } } - throw new IllegalArgumentException("Not a valid KeyType:" + value); - } - - public static KeyType fromKeyParams(PrivateKeyValues keyValues) { - if (keyValues instanceof PrivateKeyValues.Rsa) { - for (KeyType keyType : values()) { - if (keyType.params instanceof KeyType.RsaKeyParams) { - if (keyValues.getBitLength() == keyType.params.bitLength) { - return keyType; - } - } - } - } else if (keyValues instanceof PrivateKeyValues.Ec) { - for (KeyType keyType : values()) { - if (keyType.params instanceof KeyType.EcKeyParams) { - if (((PrivateKeyValues.Ec) keyValues).getCurveParams() == ((EcKeyParams) keyType.params).ellipticCurveValues) { - return keyType; - } - } - } + } + } else if (keyValues instanceof PrivateKeyValues.Ec) { + for (KeyType keyType : values()) { + if (keyType.params instanceof KeyType.EcKeyParams) { + if (((PrivateKeyValues.Ec) keyValues).getCurveParams() + == ((EcKeyParams) keyType.params).ellipticCurveValues) { + return keyType; + } } - throw new IllegalArgumentException("Unsupported key type"); + } } - - /** - * Returns the key type corresponding to the given key. - */ - public static KeyType fromKey(Key key) { - if (key instanceof RSAKey) { - for (KeyType keyType : values()) { - if (keyType.params.algorithm == Algorithm.RSA && keyType.params.bitLength == ((RSAKey) key).getModulus().bitLength()) { - return keyType; - } - } + throw new IllegalArgumentException("Unsupported key type"); + } + + /** Returns the key type corresponding to the given key. */ + public static KeyType fromKey(Key key) { + if (key instanceof RSAKey) { + for (KeyType keyType : values()) { + if (keyType.params.algorithm == Algorithm.RSA + && keyType.params.bitLength == ((RSAKey) key).getModulus().bitLength()) { + return keyType; + } + } + } else { + EllipticCurveValues ellipticCurveValues; + if (key instanceof PublicKey) { + PublicKeyValues publicKeyValues = PublicKeyValues.fromPublicKey((PublicKey) key); + if (publicKeyValues instanceof PublicKeyValues.Ec) { + ellipticCurveValues = ((PublicKeyValues.Ec) publicKeyValues).getCurveParams(); + } else if (publicKeyValues instanceof PublicKeyValues.Cv25519) { + ellipticCurveValues = ((PublicKeyValues.Cv25519) publicKeyValues).getCurveParams(); } else { - EllipticCurveValues ellipticCurveValues; - if (key instanceof PublicKey) { - PublicKeyValues publicKeyValues = PublicKeyValues.fromPublicKey( (PublicKey) key); - if (publicKeyValues instanceof PublicKeyValues.Ec) { - ellipticCurveValues = ((PublicKeyValues.Ec) publicKeyValues).getCurveParams(); - } else if (publicKeyValues instanceof PublicKeyValues.Cv25519) { - ellipticCurveValues = ((PublicKeyValues.Cv25519) publicKeyValues).getCurveParams(); - } else { - throw new IllegalArgumentException("Unsupported public key type"); - } - } else if (key instanceof PrivateKey) { - ellipticCurveValues = ((PrivateKeyValues.Ec) PrivateKeyValues.fromPrivateKey((PrivateKey) key)).getCurveParams(); - } else { - throw new IllegalArgumentException("Unsupported key type"); - } - - for (KeyType keyType : values()) { - if (keyType.params instanceof KeyType.EcKeyParams && ((EcKeyParams) keyType.params).ellipticCurveValues == ellipticCurveValues) { - return keyType; - } - } + throw new IllegalArgumentException("Unsupported public key type"); } + } else if (key instanceof PrivateKey) { + ellipticCurveValues = + ((PrivateKeyValues.Ec) PrivateKeyValues.fromPrivateKey((PrivateKey) key)) + .getCurveParams(); + } else { throw new IllegalArgumentException("Unsupported key type"); - } - - /** - * Key algorithm identifier. - */ - public enum Algorithm { - RSA, EC - } + } - /** - * Algorithm parameters used by a KeyType. - */ - public static abstract class KeyParams { - @Nonnull // Needed for Kotlin to use when() on algorithm and not have to null check. - public final Algorithm algorithm; - public final int bitLength; - - private KeyParams(Algorithm algorithm, int bitLength) { - this.algorithm = algorithm; - this.bitLength = bitLength; + for (KeyType keyType : values()) { + if (keyType.params instanceof KeyType.EcKeyParams + && ((EcKeyParams) keyType.params).ellipticCurveValues == ellipticCurveValues) { + return keyType; } + } + } + throw new IllegalArgumentException("Unsupported key type"); + } + + /** Key algorithm identifier. */ + public enum Algorithm { + RSA, + EC + } + + /** Algorithm parameters used by a KeyType. */ + public abstract static class KeyParams { + @Nonnull // Needed for Kotlin to use when() on algorithm and not have to null check. + public final Algorithm algorithm; + public final int bitLength; + + private KeyParams(Algorithm algorithm, int bitLength) { + this.algorithm = algorithm; + this.bitLength = bitLength; } + } - /** - * Algorithm parameters for RSA keys. - */ - public static final class RsaKeyParams extends KeyParams { - private RsaKeyParams(int bitLength) { - super(Algorithm.RSA, bitLength); - } + /** Algorithm parameters for RSA keys. */ + public static final class RsaKeyParams extends KeyParams { + private RsaKeyParams(int bitLength) { + super(Algorithm.RSA, bitLength); } + } - /** - * Algorithm parameters for EC keys. - */ - public static final class EcKeyParams extends KeyParams { - private final EllipticCurveValues ellipticCurveValues; + /** Algorithm parameters for EC keys. */ + public static final class EcKeyParams extends KeyParams { + private final EllipticCurveValues ellipticCurveValues; - private EcKeyParams(EllipticCurveValues ellipticCurveValues) { - super(Algorithm.EC, ellipticCurveValues.getBitLength()); - this.ellipticCurveValues = ellipticCurveValues; - } + private EcKeyParams(EllipticCurveValues ellipticCurveValues) { + super(Algorithm.EC, ellipticCurveValues.getBitLength()); + this.ellipticCurveValues = ellipticCurveValues; + } - EllipticCurveValues getCurveParams() { - return ellipticCurveValues; - } + EllipticCurveValues getCurveParams() { + return ellipticCurveValues; } + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/ManagementKeyMetadata.java b/piv/src/main/java/com/yubico/yubikit/piv/ManagementKeyMetadata.java index f4bffa75..dc52b18b 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/ManagementKeyMetadata.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/ManagementKeyMetadata.java @@ -15,45 +15,44 @@ */ package com.yubico.yubikit.piv; -/** - * Metadata about the card management key. - */ +/** Metadata about the card management key. */ public class ManagementKeyMetadata { - private final ManagementKeyType keyType; - private final boolean defaultValue; - private final TouchPolicy touchPolicy; + private final ManagementKeyType keyType; + private final boolean defaultValue; + private final TouchPolicy touchPolicy; - public ManagementKeyMetadata(ManagementKeyType keyType, boolean defaultValue, TouchPolicy touchPolicy) { - this.keyType = keyType; - this.defaultValue = defaultValue; - this.touchPolicy = touchPolicy; - } + public ManagementKeyMetadata( + ManagementKeyType keyType, boolean defaultValue, TouchPolicy touchPolicy) { + this.keyType = keyType; + this.defaultValue = defaultValue; + this.touchPolicy = touchPolicy; + } - /** - * Get the algorithm of key used for the Management Key. - * - * @return a ManagementKeyType value - */ - public ManagementKeyType getKeyType() { - return keyType; - } + /** + * Get the algorithm of key used for the Management Key. + * + * @return a ManagementKeyType value + */ + public ManagementKeyType getKeyType() { + return keyType; + } - /** - * Whether or not the default card management key is set. The key should be changed from the - * default to prevent unwanted modification to the application. - * - * @return true if the default key is set. - */ - public boolean isDefaultValue() { - return defaultValue; - } + /** + * Whether or not the default card management key is set. The key should be changed from the + * default to prevent unwanted modification to the application. + * + * @return true if the default key is set. + */ + public boolean isDefaultValue() { + return defaultValue; + } - /** - * Whether or not the YubiKey sensor needs to be touched when performing authentication. - * - * @return the touch policy of the card management key - */ - public TouchPolicy getTouchPolicy() { - return touchPolicy; - } + /** + * Whether or not the YubiKey sensor needs to be touched when performing authentication. + * + * @return the touch policy of the card management key + */ + public TouchPolicy getTouchPolicy() { + return touchPolicy; + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/ManagementKeyType.java b/piv/src/main/java/com/yubico/yubikit/piv/ManagementKeyType.java index 319bf210..905f0f9a 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/ManagementKeyType.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/ManagementKeyType.java @@ -16,45 +16,35 @@ package com.yubico.yubikit.piv; -/** - * Supported management key types for use with the PIV YubiKey application. - */ +/** Supported management key types for use with the PIV YubiKey application. */ public enum ManagementKeyType { - /** - * 3-DES (the default). - */ - TDES((byte) 0x03, "DESede", 24, 8), - /** - * AES-128. - */ - AES128((byte) 0x08, "AES", 16, 16), - /** - * AES-192. - */ - AES192((byte) 0x0a, "AES", 24, 16), - /** - * AES-256. - */ - AES256((byte) 0x0c, "AES", 32, 16); + /** 3-DES (the default). */ + TDES((byte) 0x03, "DESede", 24, 8), + /** AES-128. */ + AES128((byte) 0x08, "AES", 16, 16), + /** AES-192. */ + AES192((byte) 0x0a, "AES", 24, 16), + /** AES-256. */ + AES256((byte) 0x0c, "AES", 32, 16); - public final byte value; - public final String cipherName; - public final int keyLength; - public final int challengeLength; + public final byte value; + public final String cipherName; + public final int keyLength; + public final int challengeLength; - ManagementKeyType(byte value, String cipherName, int keyLength, int challengeLength) { - this.value = value; - this.cipherName = cipherName; - this.keyLength = keyLength; - this.challengeLength = challengeLength; - } + ManagementKeyType(byte value, String cipherName, int keyLength, int challengeLength) { + this.value = value; + this.cipherName = cipherName; + this.keyLength = keyLength; + this.challengeLength = challengeLength; + } - public static ManagementKeyType fromValue(byte value) { - for (ManagementKeyType type : ManagementKeyType.values()) { - if (type.value == value) { - return type; - } - } - throw new IllegalArgumentException("Not a valid ManagementKeyType:" + value); + public static ManagementKeyType fromValue(byte value) { + for (ManagementKeyType type : ManagementKeyType.values()) { + if (type.value == value) { + return type; + } } + throw new IllegalArgumentException("Not a valid ManagementKeyType:" + value); + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/ObjectId.java b/piv/src/main/java/com/yubico/yubikit/piv/ObjectId.java index 0506742d..b3f87bdf 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/ObjectId.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/ObjectId.java @@ -16,61 +16,59 @@ package com.yubico.yubikit.piv; -/** - * Constants used to specify PIV objects. - */ +/** Constants used to specify PIV objects. */ public class ObjectId { - public static final int CAPABILITY = 0x5fc107; - public static final int CHUID = 0x5fc102; - public static final int AUTHENTICATION = 0x5fc105; // cert for 9a key - public static final int FINGERPRINTS = 0x5fc103; - public static final int SECURITY = 0x5fc106; - public static final int FACIAL = 0x5fc108; - public static final int PRINTED = 0x5fc109; - public static final int SIGNATURE = 0x5fc10a; // cert for 9c key - public static final int KEY_MANAGEMENT = 0x5fc10b; // cert for 9d key - public static final int CARD_AUTH = 0x5fc101; // cert for 9e key - public static final int DISCOVERY = 0x7e; - public static final int KEY_HISTORY = 0x5fc10c; - public static final int IRIS = 0x5fc121; + public static final int CAPABILITY = 0x5fc107; + public static final int CHUID = 0x5fc102; + public static final int AUTHENTICATION = 0x5fc105; // cert for 9a key + public static final int FINGERPRINTS = 0x5fc103; + public static final int SECURITY = 0x5fc106; + public static final int FACIAL = 0x5fc108; + public static final int PRINTED = 0x5fc109; + public static final int SIGNATURE = 0x5fc10a; // cert for 9c key + public static final int KEY_MANAGEMENT = 0x5fc10b; // cert for 9d key + public static final int CARD_AUTH = 0x5fc101; // cert for 9e key + public static final int DISCOVERY = 0x7e; + public static final int KEY_HISTORY = 0x5fc10c; + public static final int IRIS = 0x5fc121; - public static final int RETIRED1 = 0x5fc10d; - public static final int RETIRED2 = 0x5fc10e; - public static final int RETIRED3 = 0x5fc10f; - public static final int RETIRED4 = 0x5fc110; - public static final int RETIRED5 = 0x5fc111; - public static final int RETIRED6 = 0x5fc112; - public static final int RETIRED7 = 0x5fc113; - public static final int RETIRED8 = 0x5fc114; - public static final int RETIRED9 = 0x5fc115; - public static final int RETIRED10 = 0x5fc116; - public static final int RETIRED11 = 0x5fc117; - public static final int RETIRED12 = 0x5fc118; - public static final int RETIRED13 = 0x5fc119; - public static final int RETIRED14 = 0x5fc11a; - public static final int RETIRED15 = 0x5fc11b; - public static final int RETIRED16 = 0x5fc11c; - public static final int RETIRED17 = 0x5fc11d; - public static final int RETIRED18 = 0x5fc11e; - public static final int RETIRED19 = 0x5fc11f; - public static final int RETIRED20 = 0x5fc120; + public static final int RETIRED1 = 0x5fc10d; + public static final int RETIRED2 = 0x5fc10e; + public static final int RETIRED3 = 0x5fc10f; + public static final int RETIRED4 = 0x5fc110; + public static final int RETIRED5 = 0x5fc111; + public static final int RETIRED6 = 0x5fc112; + public static final int RETIRED7 = 0x5fc113; + public static final int RETIRED8 = 0x5fc114; + public static final int RETIRED9 = 0x5fc115; + public static final int RETIRED10 = 0x5fc116; + public static final int RETIRED11 = 0x5fc117; + public static final int RETIRED12 = 0x5fc118; + public static final int RETIRED13 = 0x5fc119; + public static final int RETIRED14 = 0x5fc11a; + public static final int RETIRED15 = 0x5fc11b; + public static final int RETIRED16 = 0x5fc11c; + public static final int RETIRED17 = 0x5fc11d; + public static final int RETIRED18 = 0x5fc11e; + public static final int RETIRED19 = 0x5fc11f; + public static final int RETIRED20 = 0x5fc120; - public static final int PIVMAN_DATA = 0x5fff00; - public static final int PIVMAN_PROTECTED_DATA = PRINTED; // Use slot for printed information. - public static final int ATTESTATION = 0x5fff01; + public static final int PIVMAN_DATA = 0x5fff00; + public static final int PIVMAN_PROTECTED_DATA = PRINTED; // Use slot for printed information. + public static final int ATTESTATION = 0x5fff01; - /** - * Returns the object ID serialized as a byte array. - */ - public static byte[] getBytes(int objectId) { - if (objectId == ObjectId.DISCOVERY) { - return new byte[]{ObjectId.DISCOVERY}; - } else { - return new byte[]{(byte) ((objectId >> 16) & 0xff), (byte) ((objectId >> 8) & 0xff), (byte) (objectId & 0xff)}; - } + /** Returns the object ID serialized as a byte array. */ + public static byte[] getBytes(int objectId) { + if (objectId == ObjectId.DISCOVERY) { + return new byte[] {ObjectId.DISCOVERY}; + } else { + return new byte[] { + (byte) ((objectId >> 16) & 0xff), (byte) ((objectId >> 8) & 0xff), (byte) (objectId & 0xff) + }; } + } - private ObjectId() { - throw new IllegalStateException(); - } + private ObjectId() { + throw new IllegalStateException(); + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/Padding.java b/piv/src/main/java/com/yubico/yubikit/piv/Padding.java index fdc54574..6fa2a01c 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/Padding.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/Padding.java @@ -25,7 +25,6 @@ import java.util.Arrays; import java.util.regex.Matcher; import java.util.regex.Pattern; - import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; @@ -33,101 +32,109 @@ @Deprecated class Padding { - private static final String RAW_RSA = "RSA/ECB/NoPadding"; - private static final Pattern ECDSA_HASH_PATTERN = Pattern.compile("^(.+)withECDSA$", Pattern.CASE_INSENSITIVE); - private static final Pattern SHA_PATTERN = Pattern.compile("^SHA[0-9]+$", Pattern.CASE_INSENSITIVE); + private static final String RAW_RSA = "RSA/ECB/NoPadding"; + private static final Pattern ECDSA_HASH_PATTERN = + Pattern.compile("^(.+)withECDSA$", Pattern.CASE_INSENSITIVE); + private static final Pattern SHA_PATTERN = + Pattern.compile("^SHA[0-9]+$", Pattern.CASE_INSENSITIVE); - /** - * Prepares a message for signing. - * - * @param keyType the type of key to use for signing - * @param message the message to sign - * @param algorithm the signature algorithm to use - * @return the payload ready to be signed - * @throws NoSuchAlgorithmException if the algorithm isn't supported - */ - static byte[] pad(KeyType keyType, byte[] message, Signature algorithm) throws NoSuchAlgorithmException { - KeyType.KeyParams params = keyType.params; - byte[] payload; - switch (params.algorithm) { - case RSA: - // Sign using a dummy key - KeyPairGenerator kpg = KeyPairGenerator.getInstance(params.algorithm.name()); - kpg.initialize(params.bitLength); - KeyPair kp = kpg.generateKeyPair(); - try { - // Do a "raw encrypt" of the signature to get the padded message - algorithm.initSign(kp.getPrivate()); - algorithm.update(message); - Cipher rsa = Cipher.getInstance(RAW_RSA); - rsa.init(Cipher.ENCRYPT_MODE, kp.getPublic()); - payload = rsa.doFinal(algorithm.sign()); - } catch (SignatureException | BadPaddingException | IllegalBlockSizeException | InvalidKeyException e) { - throw new IllegalStateException(e); // Shouldn't happen - } catch (NoSuchPaddingException e) { - throw new UnsupportedOperationException("SecurityProvider doesn't support RSA without padding", e); - } - break; - case EC: - Matcher matcher = ECDSA_HASH_PATTERN.matcher(algorithm.getAlgorithm()); - if (!matcher.find()) { - throw new IllegalArgumentException("Invalid algorithm for given key"); - } - String hashAlgorithm = matcher.group(1); - byte[] hash; - if ("NONE".equals(hashAlgorithm)) { - hash = message; - } else { - if (SHA_PATTERN.matcher(hashAlgorithm).matches()) - //noinspection SpellCheckingInspection - { - //SHAXYZ needs to be renamed to SHA-XYZ - hashAlgorithm = hashAlgorithm.replace("SHA", "SHA-"); - } - hash = MessageDigest.getInstance(hashAlgorithm).digest(message); - } - int byteLength = params.bitLength / 8; - if (hash.length > byteLength) { - // Truncate - payload = Arrays.copyOf(hash, byteLength); - } else if (hash.length < byteLength) { - // Left pad, with no external dependencies! - payload = new byte[byteLength]; - System.arraycopy(hash, 0, payload, payload.length - hash.length, hash.length); - } else { - payload = hash; - } - break; - default: - throw new IllegalArgumentException(); + /** + * Prepares a message for signing. + * + * @param keyType the type of key to use for signing + * @param message the message to sign + * @param algorithm the signature algorithm to use + * @return the payload ready to be signed + * @throws NoSuchAlgorithmException if the algorithm isn't supported + */ + static byte[] pad(KeyType keyType, byte[] message, Signature algorithm) + throws NoSuchAlgorithmException { + KeyType.KeyParams params = keyType.params; + byte[] payload; + switch (params.algorithm) { + case RSA: + // Sign using a dummy key + KeyPairGenerator kpg = KeyPairGenerator.getInstance(params.algorithm.name()); + kpg.initialize(params.bitLength); + KeyPair kp = kpg.generateKeyPair(); + try { + // Do a "raw encrypt" of the signature to get the padded message + algorithm.initSign(kp.getPrivate()); + algorithm.update(message); + Cipher rsa = Cipher.getInstance(RAW_RSA); + rsa.init(Cipher.ENCRYPT_MODE, kp.getPublic()); + payload = rsa.doFinal(algorithm.sign()); + } catch (SignatureException + | BadPaddingException + | IllegalBlockSizeException + | InvalidKeyException e) { + throw new IllegalStateException(e); // Shouldn't happen + } catch (NoSuchPaddingException e) { + throw new UnsupportedOperationException( + "SecurityProvider doesn't support RSA without padding", e); } - - return payload; + break; + case EC: + Matcher matcher = ECDSA_HASH_PATTERN.matcher(algorithm.getAlgorithm()); + if (!matcher.find()) { + throw new IllegalArgumentException("Invalid algorithm for given key"); + } + String hashAlgorithm = matcher.group(1); + byte[] hash; + if ("NONE".equals(hashAlgorithm)) { + hash = message; + } else { + if (SHA_PATTERN.matcher(hashAlgorithm).matches()) + //noinspection SpellCheckingInspection + { + // SHAXYZ needs to be renamed to SHA-XYZ + hashAlgorithm = hashAlgorithm.replace("SHA", "SHA-"); + } + hash = MessageDigest.getInstance(hashAlgorithm).digest(message); + } + int byteLength = params.bitLength / 8; + if (hash.length > byteLength) { + // Truncate + payload = Arrays.copyOf(hash, byteLength); + } else if (hash.length < byteLength) { + // Left pad, with no external dependencies! + payload = new byte[byteLength]; + System.arraycopy(hash, 0, payload, payload.length - hash.length, hash.length); + } else { + payload = hash; + } + break; + default: + throw new IllegalArgumentException(); } - /** - * Verifies and removes padding from a decrypted RSA message. - * - * @param decrypted the decrypted (but still padded) payload - * @param algorithm the cipher algorithm used for encryption - * @return the un-padded plaintext - * @throws NoSuchPaddingException in case the padding algorithm isn't supported - * @throws NoSuchAlgorithmException in case the algorithm isn't supported - * @throws BadPaddingException in case of a padding error - */ - static byte[] unpad(byte[] decrypted, Cipher algorithm) throws NoSuchPaddingException, NoSuchAlgorithmException, BadPaddingException { - Cipher rsa = Cipher.getInstance(RAW_RSA); + return payload; + } - // Encrypt using a dummy key - KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyType.Algorithm.RSA.name()); - kpg.initialize(decrypted.length * 8); - KeyPair kp = kpg.generateKeyPair(); - try { - rsa.init(Cipher.ENCRYPT_MODE, kp.getPublic()); - algorithm.init(Cipher.DECRYPT_MODE, kp.getPrivate()); - return algorithm.doFinal(rsa.doFinal(decrypted)); - } catch (InvalidKeyException | IllegalBlockSizeException e) { - throw new IllegalStateException(e); // Shouldn't happen - } + /** + * Verifies and removes padding from a decrypted RSA message. + * + * @param decrypted the decrypted (but still padded) payload + * @param algorithm the cipher algorithm used for encryption + * @return the un-padded plaintext + * @throws NoSuchPaddingException in case the padding algorithm isn't supported + * @throws NoSuchAlgorithmException in case the algorithm isn't supported + * @throws BadPaddingException in case of a padding error + */ + static byte[] unpad(byte[] decrypted, Cipher algorithm) + throws NoSuchPaddingException, NoSuchAlgorithmException, BadPaddingException { + Cipher rsa = Cipher.getInstance(RAW_RSA); + + // Encrypt using a dummy key + KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyType.Algorithm.RSA.name()); + kpg.initialize(decrypted.length * 8); + KeyPair kp = kpg.generateKeyPair(); + try { + rsa.init(Cipher.ENCRYPT_MODE, kp.getPublic()); + algorithm.init(Cipher.DECRYPT_MODE, kp.getPrivate()); + return algorithm.doFinal(rsa.doFinal(decrypted)); + } catch (InvalidKeyException | IllegalBlockSizeException e) { + throw new IllegalStateException(e); // Shouldn't happen } + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/PinMetadata.java b/piv/src/main/java/com/yubico/yubikit/piv/PinMetadata.java index 40339022..e80f0d2a 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/PinMetadata.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/PinMetadata.java @@ -15,41 +15,35 @@ */ package com.yubico.yubikit.piv; -/** - * Metadata about the PIN or PUK. - */ +/** Metadata about the PIN or PUK. */ public class PinMetadata { - private final boolean defaultValue; - private final int totalAttempts; - private final int attemptsRemaining; + private final boolean defaultValue; + private final int totalAttempts; + private final int attemptsRemaining; - public PinMetadata(boolean defaultValue, int totalAttempts, int attemptsRemaining) { - this.defaultValue = defaultValue; - this.totalAttempts = totalAttempts; - this.attemptsRemaining = attemptsRemaining; - } + public PinMetadata(boolean defaultValue, int totalAttempts, int attemptsRemaining) { + this.defaultValue = defaultValue; + this.totalAttempts = totalAttempts; + this.attemptsRemaining = attemptsRemaining; + } - /** - * Whether or not the default PIN/PUK is set. The PIN/PUK should be changed from the default to - * prevent unwanted usage of the application. - * - * @return true if the default key is set. - */ - public boolean isDefaultValue() { - return defaultValue; - } + /** + * Whether or not the default PIN/PUK is set. The PIN/PUK should be changed from the default to + * prevent unwanted usage of the application. + * + * @return true if the default key is set. + */ + public boolean isDefaultValue() { + return defaultValue; + } - /** - * Returns the number of PIN/PUK attempts available after successful verification. - */ - public int getTotalAttempts() { - return totalAttempts; - } + /** Returns the number of PIN/PUK attempts available after successful verification. */ + public int getTotalAttempts() { + return totalAttempts; + } - /** - * Returns the number of PIN/PUK attempts currently remaining. - */ - public int getAttemptsRemaining() { - return attemptsRemaining; - } + /** Returns the number of PIN/PUK attempts currently remaining. */ + public int getAttemptsRemaining() { + return attemptsRemaining; + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/PinPolicy.java b/piv/src/main/java/com/yubico/yubikit/piv/PinPolicy.java index d40f7437..36e03423 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/PinPolicy.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/PinPolicy.java @@ -18,53 +18,39 @@ /** * The PIN policy of a private key defines whether or not a PIN is required to use the key. - *

- * Setting a PIN policy other than DEFAULT requires YubiKey 4 or later. + * + *

Setting a PIN policy other than DEFAULT requires YubiKey 4 or later. */ public enum PinPolicy { - /** - * The default behavior for the particular key slot is used. - */ - DEFAULT(0x0), + /** The default behavior for the particular key slot is used. */ + DEFAULT(0x0), - /** - * The PIN is never required for using the key. - */ - NEVER(0x1), + /** The PIN is never required for using the key. */ + NEVER(0x1), - /** - * The PIN must be verified for the session, prior to using the key. - */ - ONCE(0x2), + /** The PIN must be verified for the session, prior to using the key. */ + ONCE(0x2), - /** - * The PIN must be verified each time the key is to be used, just prior to using it. - */ - ALWAYS(0x3), + /** The PIN must be verified each time the key is to be used, just prior to using it. */ + ALWAYS(0x3), - /** - * PIN or biometrics must be verified for the session, prior to using the key. - */ - MATCH_ONCE(0x4), + /** PIN or biometrics must be verified for the session, prior to using the key. */ + MATCH_ONCE(0x4), - /** - * PIN or biometrics must be verified each time the key is to be used, just prior to using it. - */ - MATCH_ALWAYS(0x5); + /** PIN or biometrics must be verified each time the key is to be used, just prior to using it. */ + MATCH_ALWAYS(0x5); - public final int value; + public final int value; - PinPolicy(int value) { - this.value = value; - } + PinPolicy(int value) { + this.value = value; + } - /** - * Returns the PIN policy corresponding to the given PIV application constant. - */ - public static PinPolicy fromValue(int value) { - if (value >= 0 && value < PinPolicy.values().length) { - return PinPolicy.values()[value]; - } - throw new IllegalArgumentException("Not a valid PinPolicy :" + value); + /** Returns the PIN policy corresponding to the given PIV application constant. */ + public static PinPolicy fromValue(int value) { + if (value >= 0 && value < PinPolicy.values().length) { + return PinPolicy.values()[value]; } + throw new IllegalArgumentException("Not a valid PinPolicy :" + value); + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/PivSession.java b/piv/src/main/java/com/yubico/yubikit/piv/PivSession.java index 687e2d86..3b25aead 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/PivSession.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/PivSession.java @@ -38,9 +38,6 @@ import com.yubico.yubikit.core.util.StringUtils; import com.yubico.yubikit.core.util.Tlv; import com.yubico.yubikit.core.util.Tlvs; - -import org.slf4j.LoggerFactory; - import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -67,7 +64,6 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; @@ -75,1397 +71,1504 @@ import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; +import org.slf4j.LoggerFactory; /** * Personal Identity Verification (PIV) interface specified in NIST SP 800-73 document * "Cryptographic Algorithms and Key Sizes for PIV". - *

- * This enables you to perform RSA or ECC sign/decrypt operations using a private key stored on the - * smart card, through common transports like PKCS#11. + * + *

This enables you to perform RSA or ECC sign/decrypt operations using a private key stored on + * the smart card, through common transports like PKCS#11. */ public class PivSession extends ApplicationSession { - // Features - /** - * Support for the NIST P-348 elliptic curve. - */ - public static final Feature FEATURE_P384 = new Feature.Versioned<>("Curve P384", 4, 0, 0); - /** - * Support for custom PIN or Touch policy. - */ - public static final Feature FEATURE_USAGE_POLICY = new Feature.Versioned<>("PIN/Touch Policy", 4, 0, 0); - /** - * Support for the CACHED Touch policy. - */ - public static final Feature FEATURE_TOUCH_CACHED = new Feature.Versioned<>("Cached Touch Policy", 4, 3, 0); - /** - * Support for Attestation of generated keys. - */ - public static final Feature FEATURE_ATTESTATION = new Feature.Versioned<>("Attestation", 4, 3, 0); - /** - * Support for reading the YubiKey serial number. - */ - public static final Feature FEATURE_SERIAL = new Feature.Versioned<>("Serial Number", 5, 0, 0); - /** - * Support for getting PIN/PUK/Management key and private key metadata. - */ - public static final Feature FEATURE_METADATA = new Feature.Versioned<>("Metadata", 5, 3, 0); - /** - * Support for AES management keys. - */ - public static final Feature FEATURE_AES_KEY = new Feature.Versioned<>("AES Management Key", 5, 4, 0); - - /** - * Support for generating RSA keys. - */ - public static final Feature FEATURE_RSA_GENERATION = new Feature("RSA key generation") { + // Features + /** Support for the NIST P-348 elliptic curve. */ + public static final Feature FEATURE_P384 = + new Feature.Versioned<>("Curve P384", 4, 0, 0); + + /** Support for custom PIN or Touch policy. */ + public static final Feature FEATURE_USAGE_POLICY = + new Feature.Versioned<>("PIN/Touch Policy", 4, 0, 0); + + /** Support for the CACHED Touch policy. */ + public static final Feature FEATURE_TOUCH_CACHED = + new Feature.Versioned<>("Cached Touch Policy", 4, 3, 0); + + /** Support for Attestation of generated keys. */ + public static final Feature FEATURE_ATTESTATION = + new Feature.Versioned<>("Attestation", 4, 3, 0); + + /** Support for reading the YubiKey serial number. */ + public static final Feature FEATURE_SERIAL = + new Feature.Versioned<>("Serial Number", 5, 0, 0); + + /** Support for getting PIN/PUK/Management key and private key metadata. */ + public static final Feature FEATURE_METADATA = + new Feature.Versioned<>("Metadata", 5, 3, 0); + + /** Support for AES management keys. */ + public static final Feature FEATURE_AES_KEY = + new Feature.Versioned<>("AES Management Key", 5, 4, 0); + + /** Support for generating RSA keys. */ + public static final Feature FEATURE_RSA_GENERATION = + new Feature("RSA key generation") { @Override public boolean isSupportedBy(Version version) { - return version.isLessThan(4, 2, 6) || version.isAtLeast(4, 3, 5); + return version.isLessThan(4, 2, 6) || version.isAtLeast(4, 3, 5); } - }; - - /** - * Support for moving and deleting keys. - */ - public static final Feature FEATURE_MOVE_KEY = new Feature.Versioned<>("Move or delete keys", 5, 7, 0); - - /** - * Support for the curve 25519 keys. - */ - public static final Feature FEATURE_CV25519 = new Feature.Versioned<>("Curve 25519", 5, 7, 0); - - /** - * Support for larger RSA key sizes. - */ - public static final Feature FEATURE_RSA3072_RSA4096 = new Feature.Versioned<>("RSA3072 and RSA4096 keys", 5, 7, 0); - - private static final int PIN_LEN = 8; - private static final int TEMPORARY_PIN_LEN = 16; - - // Special slot for the Management Key - private static final int SLOT_CARD_MANAGEMENT = 0x9b; - - // Special slot for bio metadata - private static final int SLOT_OCC_AUTH = 0x96; - - // Instruction set - private static final byte INS_VERIFY = 0x20; - private static final byte INS_CHANGE_REFERENCE = 0x24; - private static final byte INS_RESET_RETRY = 0x2c; - private static final byte INS_GENERATE_ASYMMETRIC = 0x47; - private static final byte INS_AUTHENTICATE = (byte) 0x87; - private static final byte INS_GET_DATA = (byte) 0xcb; - private static final byte INS_PUT_DATA = (byte) 0xdb; - private static final byte INS_MOVE_KEY = (byte) 0xf6; - private static final byte INS_GET_METADATA = (byte) 0xf7; - private static final byte INS_GET_SERIAL = (byte) 0xf8; - private static final byte INS_ATTEST = (byte) 0xf9; - private static final byte INS_SET_PIN_RETRIES = (byte) 0xfa; - private static final byte INS_RESET = (byte) 0xfb; - private static final byte INS_GET_VERSION = (byte) 0xfd; - private static final byte INS_IMPORT_KEY = (byte) 0xfe; - private static final byte INS_SET_MGMKEY = (byte) 0xff; - - // Tags for parsing responses and preparing requests - private static final int TAG_AUTH_WITNESS = 0x80; - private static final int TAG_AUTH_CHALLENGE = 0x81; - private static final int TAG_AUTH_RESPONSE = 0x82; - private static final int TAG_AUTH_EXPONENTIATION = 0x85; - private static final int TAG_GEN_ALGORITHM = 0x80; - private static final int TAG_OBJ_DATA = 0x53; - private static final int TAG_OBJ_ID = 0x5c; - private static final int TAG_CERTIFICATE = 0x70; - private static final int TAG_CERT_INFO = 0x71; - private static final int TAG_DYN_AUTH = 0x7c; - private static final int TAG_LRC = 0xfe; - private static final int TAG_PIN_POLICY = 0xaa; - private static final int TAG_TOUCH_POLICY = 0xab; - - // Metadata tags - private static final int TAG_METADATA_ALGO = 0x01; - private static final int TAG_METADATA_POLICY = 0x02; - private static final int TAG_METADATA_ORIGIN = 0x03; - private static final int TAG_METADATA_PUBLIC_KEY = 0x04; - private static final int TAG_METADATA_IS_DEFAULT = 0x05; - private static final int TAG_METADATA_RETRIES = 0x06; - private static final int TAG_METADATA_BIO_CONFIGURED = 0x07; - private static final int TAG_METADATA_TEMPORARY_PIN = 0x08; - - private static final byte ORIGIN_GENERATED = 1; - private static final byte ORIGIN_IMPORTED = 2; - - private static final int INDEX_PIN_POLICY = 0; - private static final int INDEX_TOUCH_POLICY = 1; - private static final int INDEX_RETRIES_TOTAL = 0; - private static final int INDEX_RETRIES_REMAINING = 1; - - private static final byte PIN_P2 = (byte) 0x80; - private static final byte PUK_P2 = (byte) 0x81; - - private final SmartCardProtocol protocol; - private final Version version; - private int currentPinAttempts = 3; // Internal guess as to number of PIN retries. - private int maxPinAttempts = 3; // Internal guess as to max number of PIN retries. - private ManagementKeyType managementKeyType; - - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(PivSession.class); - - /** - * Create new instance of {@link PivSession} - * and selects the application for use - * - * @param connection connection with YubiKey - * @throws IOException in case of communication error - * @throws ApduException in case of an error response from the YubiKey - * @throws ApplicationNotAvailableException if the application is missing or disabled - */ - public PivSession(SmartCardConnection connection) throws IOException, ApduException, ApplicationNotAvailableException { - this(connection, null); + }; + + /** Support for moving and deleting keys. */ + public static final Feature FEATURE_MOVE_KEY = + new Feature.Versioned<>("Move or delete keys", 5, 7, 0); + + /** Support for the curve 25519 keys. */ + public static final Feature FEATURE_CV25519 = + new Feature.Versioned<>("Curve 25519", 5, 7, 0); + + /** Support for larger RSA key sizes. */ + public static final Feature FEATURE_RSA3072_RSA4096 = + new Feature.Versioned<>("RSA3072 and RSA4096 keys", 5, 7, 0); + + private static final int PIN_LEN = 8; + private static final int TEMPORARY_PIN_LEN = 16; + + // Special slot for the Management Key + private static final int SLOT_CARD_MANAGEMENT = 0x9b; + + // Special slot for bio metadata + private static final int SLOT_OCC_AUTH = 0x96; + + // Instruction set + private static final byte INS_VERIFY = 0x20; + private static final byte INS_CHANGE_REFERENCE = 0x24; + private static final byte INS_RESET_RETRY = 0x2c; + private static final byte INS_GENERATE_ASYMMETRIC = 0x47; + private static final byte INS_AUTHENTICATE = (byte) 0x87; + private static final byte INS_GET_DATA = (byte) 0xcb; + private static final byte INS_PUT_DATA = (byte) 0xdb; + private static final byte INS_MOVE_KEY = (byte) 0xf6; + private static final byte INS_GET_METADATA = (byte) 0xf7; + private static final byte INS_GET_SERIAL = (byte) 0xf8; + private static final byte INS_ATTEST = (byte) 0xf9; + private static final byte INS_SET_PIN_RETRIES = (byte) 0xfa; + private static final byte INS_RESET = (byte) 0xfb; + private static final byte INS_GET_VERSION = (byte) 0xfd; + private static final byte INS_IMPORT_KEY = (byte) 0xfe; + private static final byte INS_SET_MGMKEY = (byte) 0xff; + + // Tags for parsing responses and preparing requests + private static final int TAG_AUTH_WITNESS = 0x80; + private static final int TAG_AUTH_CHALLENGE = 0x81; + private static final int TAG_AUTH_RESPONSE = 0x82; + private static final int TAG_AUTH_EXPONENTIATION = 0x85; + private static final int TAG_GEN_ALGORITHM = 0x80; + private static final int TAG_OBJ_DATA = 0x53; + private static final int TAG_OBJ_ID = 0x5c; + private static final int TAG_CERTIFICATE = 0x70; + private static final int TAG_CERT_INFO = 0x71; + private static final int TAG_DYN_AUTH = 0x7c; + private static final int TAG_LRC = 0xfe; + private static final int TAG_PIN_POLICY = 0xaa; + private static final int TAG_TOUCH_POLICY = 0xab; + + // Metadata tags + private static final int TAG_METADATA_ALGO = 0x01; + private static final int TAG_METADATA_POLICY = 0x02; + private static final int TAG_METADATA_ORIGIN = 0x03; + private static final int TAG_METADATA_PUBLIC_KEY = 0x04; + private static final int TAG_METADATA_IS_DEFAULT = 0x05; + private static final int TAG_METADATA_RETRIES = 0x06; + private static final int TAG_METADATA_BIO_CONFIGURED = 0x07; + private static final int TAG_METADATA_TEMPORARY_PIN = 0x08; + + private static final byte ORIGIN_GENERATED = 1; + private static final byte ORIGIN_IMPORTED = 2; + + private static final int INDEX_PIN_POLICY = 0; + private static final int INDEX_TOUCH_POLICY = 1; + private static final int INDEX_RETRIES_TOTAL = 0; + private static final int INDEX_RETRIES_REMAINING = 1; + + private static final byte PIN_P2 = (byte) 0x80; + private static final byte PUK_P2 = (byte) 0x81; + + private final SmartCardProtocol protocol; + private final Version version; + private int currentPinAttempts = 3; // Internal guess as to number of PIN retries. + private int maxPinAttempts = 3; // Internal guess as to max number of PIN retries. + private ManagementKeyType managementKeyType; + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(PivSession.class); + + /** + * Create new instance of {@link PivSession} and selects the application for use + * + * @param connection connection with YubiKey + * @throws IOException in case of communication error + * @throws ApduException in case of an error response from the YubiKey + * @throws ApplicationNotAvailableException if the application is missing or disabled + */ + public PivSession(SmartCardConnection connection) + throws IOException, ApduException, ApplicationNotAvailableException { + this(connection, null); + } + + /** + * Create new instance of {@link PivSession} and selects the application for use + * + * @param connection connection with YubiKey + * @throws IOException in case of communication error + * @throws ApduException in case of an error response from the YubiKey + * @throws ApplicationNotAvailableException if the application is missing or disabled + */ + public PivSession(SmartCardConnection connection, @Nullable ScpKeyParams scpKeyParams) + throws IOException, ApduException, ApplicationNotAvailableException { + protocol = new SmartCardProtocol(connection); + protocol.select(AppId.PIV); + if (scpKeyParams != null) { + try { + protocol.initScp(scpKeyParams); + } catch (BadResponseException e) { + throw new IllegalStateException(e); + } } - /** - * Create new instance of {@link PivSession} - * and selects the application for use - * - * @param connection connection with YubiKey - * @throws IOException in case of communication error - * @throws ApduException in case of an error response from the YubiKey - * @throws ApplicationNotAvailableException if the application is missing or disabled - */ - public PivSession(SmartCardConnection connection, @Nullable ScpKeyParams scpKeyParams) throws IOException, ApduException, ApplicationNotAvailableException { - protocol = new SmartCardProtocol(connection); - protocol.select(AppId.PIV); - if(scpKeyParams != null) { - try { - protocol.initScp(scpKeyParams); - } catch (BadResponseException e) { - throw new IllegalStateException(e); - } - } - - version = Version.fromBytes(protocol.sendAndReceive(new Apdu(0, INS_GET_VERSION, 0, 0, null))); - protocol.configure(version); + version = Version.fromBytes(protocol.sendAndReceive(new Apdu(0, INS_GET_VERSION, 0, 0, null))); + protocol.configure(version); - try { - managementKeyType = getManagementKeyMetadata().getKeyType(); - } catch (UnsupportedOperationException unsupportedOperationException) { - managementKeyType = ManagementKeyType.TDES; - } - Logger.debug(logger, "PIV session initialized (version={})", version); + try { + managementKeyType = getManagementKeyMetadata().getKeyType(); + } catch (UnsupportedOperationException unsupportedOperationException) { + managementKeyType = ManagementKeyType.TDES; } - - @Override - public void close() throws IOException { - protocol.close(); + Logger.debug(logger, "PIV session initialized (version={})", version); + } + + @Override + public void close() throws IOException { + protocol.close(); + } + + /** + * Get the PIV application version from the YubiKey. For YubiKey 4 and later this will match the + * YubiKey firmware version. + * + * @return application version + */ + @Override + public Version getVersion() { + return version; + } + + /** + * Get the serial number from the YubiKey. NOTE: This requires the SERIAL_API_VISIBLE flag to be + * set on one of the YubiOTP slots (it is set by default). + * + *

This functionality requires support for {@link #FEATURE_SERIAL}, available on YubiKey 5 or + * later. + * + * @return The YubiKey's serial number + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + */ + public int getSerialNumber() throws IOException, ApduException { + require(FEATURE_SERIAL); + return ByteBuffer.wrap(protocol.sendAndReceive(new Apdu(0, INS_GET_SERIAL, 0, 0, null))) + .getInt(); + } + + /** + * Resets the application to just-installed state. + * + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + */ + public void reset() throws IOException, ApduException { + Logger.debug(logger, "Preparing PIV reset"); + + try { + BioMetadata bioMetadata = getBioMetadata(); + if (bioMetadata.isConfigured()) { + throw new IllegalArgumentException( + "Cannot perform PIV reset when biometrics are configured"); + } + } catch (UnsupportedOperationException e) { + // ignored } - /** - * Get the PIV application version from the YubiKey. - * For YubiKey 4 and later this will match the YubiKey firmware version. - * - * @return application version - */ - @Override - public Version getVersion() { - return version; + blockPin(); + blockPuk(); + Logger.debug(logger, "Sending reset"); + protocol.sendAndReceive(new Apdu(0, INS_RESET, 0, 0, null)); + currentPinAttempts = 3; + maxPinAttempts = 3; + + // update management key type + try { + managementKeyType = getManagementKeyMetadata().getKeyType(); + } catch (UnsupportedOperationException unsupportedOperationException) { + managementKeyType = ManagementKeyType.TDES; } - /** - * Get the serial number from the YubiKey. - * NOTE: This requires the SERIAL_API_VISIBLE flag to be set on one of the YubiOTP slots (it is set by default). - *

- * This functionality requires support for {@link #FEATURE_SERIAL}, available on YubiKey 5 or later. - * - * @return The YubiKey's serial number - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - */ - public int getSerialNumber() throws IOException, ApduException { - require(FEATURE_SERIAL); - return ByteBuffer.wrap(protocol.sendAndReceive(new Apdu(0, INS_GET_SERIAL, 0, 0, null))).getInt(); + Logger.info(logger, "PIV application data reset performed"); + } + + /** + * Authenticate with the Management Key. + * + * @param keyType the algorithm used for the management key The default key uses TDES + * @param managementKey management key as byte array The default 3DES/AES192 management key (9B) + * is 010203040506070801020304050607080102030405060708. + * @throws IllegalArgumentException in case of wrong keyType + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @throws BadResponseException in case of incorrect YubiKey response + * @deprecated Replaced by {@link #authenticate(byte[])} + */ + @Deprecated + public void authenticate(ManagementKeyType keyType, byte[] managementKey) + throws IOException, ApduException, BadResponseException { + if (keyType != managementKeyType) { + throw new IllegalArgumentException("Invalid Management Key type " + keyType.name()); } - - /** - * Resets the application to just-installed state. - * - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - */ - public void reset() throws IOException, ApduException { - Logger.debug(logger, "Preparing PIV reset"); - - try { - BioMetadata bioMetadata = getBioMetadata(); - if (bioMetadata.isConfigured()) { - throw new IllegalArgumentException("Cannot perform PIV reset when biometrics are configured"); - } - } catch (UnsupportedOperationException e) { - // ignored - } - - blockPin(); - blockPuk(); - Logger.debug(logger, "Sending reset"); - protocol.sendAndReceive(new Apdu(0, INS_RESET, 0, 0, null)); - currentPinAttempts = 3; - maxPinAttempts = 3; - - // update management key type - try { - managementKeyType = getManagementKeyMetadata().getKeyType(); - } catch (UnsupportedOperationException unsupportedOperationException) { - managementKeyType = ManagementKeyType.TDES; - } - - Logger.info(logger, "PIV application data reset performed"); + authenticate(managementKey); + } + + /** + * Authenticate with the Management Key. + * + * @param managementKey management key as byte array The default 3DES/AES192 management key (9B) + * is 010203040506070801020304050607080102030405060708. + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @throws BadResponseException in case of incorrect YubiKey response + */ + public void authenticate(byte[] managementKey) + throws IOException, ApduException, BadResponseException { + Logger.debug(logger, "Authenticating with key type: {}", managementKeyType); + if (managementKey.length != managementKeyType.keyLength) { + throw new IllegalArgumentException( + String.format("Management Key must be %d bytes", managementKeyType.keyLength)); } - - /** - * Authenticate with the Management Key. - * - * @param keyType the algorithm used for the management key - * The default key uses TDES - * @param managementKey management key as byte array - * The default 3DES/AES192 management key (9B) is 010203040506070801020304050607080102030405060708. - * @throws IllegalArgumentException in case of wrong keyType - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @throws BadResponseException in case of incorrect YubiKey response - * @deprecated Replaced by {@link #authenticate(byte[])} - */ - @Deprecated - public void authenticate(ManagementKeyType keyType, byte[] managementKey) throws IOException, ApduException, BadResponseException { - if (keyType != managementKeyType) { - throw new IllegalArgumentException("Invalid Management Key type " + keyType.name()); - } - authenticate(managementKey); + // An empty witness is a request for a witness. + byte[] request = new Tlv(TAG_DYN_AUTH, new Tlv(TAG_AUTH_WITNESS, null).getBytes()).getBytes(); + byte[] response = + protocol.sendAndReceive( + new Apdu(0, INS_AUTHENTICATE, managementKeyType.value, SLOT_CARD_MANAGEMENT, request)); + + // Witness (tag '80') contains encrypted data (unrevealed fact). + byte[] witness = Tlvs.unpackValue(TAG_AUTH_WITNESS, Tlvs.unpackValue(TAG_DYN_AUTH, response)); + SecretKey key = new SecretKeySpec(managementKey, managementKeyType.cipherName); + try { + Map dataTlvs = new LinkedHashMap<>(); + Cipher cipher = Cipher.getInstance(managementKeyType.cipherName + "/ECB/NoPadding"); + // This decrypted witness + cipher.init(Cipher.DECRYPT_MODE, key); + dataTlvs.put(TAG_AUTH_WITNESS, cipher.doFinal(witness)); + // The challenge (tag '81') contains clear data (byte sequence), + byte[] challenge = RandomUtils.getRandomBytes(managementKeyType.challengeLength); + dataTlvs.put(TAG_AUTH_CHALLENGE, challenge); + + request = new Tlv(TAG_DYN_AUTH, Tlvs.encodeMap(dataTlvs)).getBytes(); + response = + protocol.sendAndReceive( + new Apdu( + 0, INS_AUTHENTICATE, managementKeyType.value, SLOT_CARD_MANAGEMENT, request)); + + // (tag '82') contains either the decrypted data from tag '80' or the encrypted data from tag + // '81'. + byte[] encryptedData = + Tlvs.unpackValue(TAG_AUTH_RESPONSE, Tlvs.unpackValue(TAG_DYN_AUTH, response)); + cipher.init(Cipher.ENCRYPT_MODE, key); + byte[] expectedData = cipher.doFinal(challenge); + if (!MessageDigest.isEqual(encryptedData, expectedData)) { + Logger.trace( + logger, + "Expected response: {} and actual response {}", + StringUtils.bytesToHex(expectedData), + StringUtils.bytesToHex(encryptedData)); + throw new BadResponseException("Calculated response for challenge is incorrect"); + } + } catch (NoSuchAlgorithmException + | InvalidKeyException + | NoSuchPaddingException + | BadPaddingException + | IllegalBlockSizeException e) { + // This should never happen + throw new RuntimeException(e); } - - /** - * Authenticate with the Management Key. - * - * @param managementKey management key as byte array - * The default 3DES/AES192 management key (9B) is 010203040506070801020304050607080102030405060708. - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @throws BadResponseException in case of incorrect YubiKey response - */ - public void authenticate(byte[] managementKey) throws IOException, ApduException, BadResponseException { - Logger.debug(logger, "Authenticating with key type: {}", managementKeyType); - if (managementKey.length != managementKeyType.keyLength) { - throw new IllegalArgumentException(String.format("Management Key must be %d bytes", managementKeyType.keyLength)); - } - // An empty witness is a request for a witness. - byte[] request = new Tlv(TAG_DYN_AUTH, new Tlv(TAG_AUTH_WITNESS, null).getBytes()).getBytes(); - byte[] response = protocol.sendAndReceive(new Apdu(0, INS_AUTHENTICATE, managementKeyType.value, SLOT_CARD_MANAGEMENT, request)); - - // Witness (tag '80') contains encrypted data (unrevealed fact). - byte[] witness = Tlvs.unpackValue(TAG_AUTH_WITNESS, Tlvs.unpackValue(TAG_DYN_AUTH, response)); - SecretKey key = new SecretKeySpec(managementKey, managementKeyType.cipherName); - try { - Map dataTlvs = new LinkedHashMap<>(); - Cipher cipher = Cipher.getInstance(managementKeyType.cipherName + "/ECB/NoPadding"); - // This decrypted witness - cipher.init(Cipher.DECRYPT_MODE, key); - dataTlvs.put(TAG_AUTH_WITNESS, cipher.doFinal(witness)); - // The challenge (tag '81') contains clear data (byte sequence), - byte[] challenge = RandomUtils.getRandomBytes(managementKeyType.challengeLength); - dataTlvs.put(TAG_AUTH_CHALLENGE, challenge); - - request = new Tlv(TAG_DYN_AUTH, Tlvs.encodeMap(dataTlvs)).getBytes(); - response = protocol.sendAndReceive(new Apdu(0, INS_AUTHENTICATE, managementKeyType.value, SLOT_CARD_MANAGEMENT, request)); - - // (tag '82') contains either the decrypted data from tag '80' or the encrypted data from tag '81'. - byte[] encryptedData = Tlvs.unpackValue(TAG_AUTH_RESPONSE, Tlvs.unpackValue(TAG_DYN_AUTH, response)); - cipher.init(Cipher.ENCRYPT_MODE, key); - byte[] expectedData = cipher.doFinal(challenge); - if (!MessageDigest.isEqual(encryptedData, expectedData)) { - Logger.trace(logger, "Expected response: {} and actual response {}", - StringUtils.bytesToHex(expectedData), - StringUtils.bytesToHex(encryptedData)); - throw new BadResponseException("Calculated response for challenge is incorrect"); - } - } catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | - BadPaddingException | IllegalBlockSizeException e) { - //This should never happen - throw new RuntimeException(e); - } - } - - /** - * Create a signature for a given message. - *

- * The algorithm must be compatible with the given key type. - *

- * DEPRECATED: Use the PivProvider JCA Security Provider instead. - * - * @param slot the slot containing the private key to use - * @param keyType the type of the key stored in the slot - * @param message the message to hash - * @param algorithm the signing algorithm to use - * @return the signature - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @throws BadResponseException in case of incorrect YubiKey response - * @throws NoSuchAlgorithmException if the algorithm isn't supported - */ - @Deprecated - public byte[] sign(Slot slot, KeyType keyType, byte[] message, Signature algorithm) throws IOException, ApduException, BadResponseException, NoSuchAlgorithmException { - Logger.debug(logger, "Signing data with key in slot {} of type {} using algorithm {}", - slot, keyType, algorithm); - byte[] payload = Padding.pad(keyType, message, algorithm); - return usePrivateKey(slot, keyType, payload, false); + } + + /** + * Create a signature for a given message. + * + *

The algorithm must be compatible with the given key type. + * + *

DEPRECATED: Use the PivProvider JCA Security Provider instead. + * + * @param slot the slot containing the private key to use + * @param keyType the type of the key stored in the slot + * @param message the message to hash + * @param algorithm the signing algorithm to use + * @return the signature + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @throws BadResponseException in case of incorrect YubiKey response + * @throws NoSuchAlgorithmException if the algorithm isn't supported + */ + @Deprecated + public byte[] sign(Slot slot, KeyType keyType, byte[] message, Signature algorithm) + throws IOException, ApduException, BadResponseException, NoSuchAlgorithmException { + Logger.debug( + logger, + "Signing data with key in slot {} of type {} using algorithm {}", + slot, + keyType, + algorithm); + byte[] payload = Padding.pad(keyType, message, algorithm); + return usePrivateKey(slot, keyType, payload, false); + } + + /** + * Performs a private key operation on the given payload. Any hashing and/or padding required + * should already be done prior to calling this method. + * + *

More commonly, the JCA classes provided should be used instead of directly calling this. + * + * @param slot the slot containing the private key to use + * @param keyType the type of the key stored in the slot + * @param payload the data to operate on + * @return the result of the operation + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @throws BadResponseException in case of incorrect YubiKey response + */ + public byte[] rawSignOrDecrypt(Slot slot, KeyType keyType, byte[] payload) + throws IOException, ApduException, BadResponseException { + int byteLength = keyType.params.bitLength / 8; + byte[] padded; + if (keyType == KeyType.ED25519 || keyType == KeyType.X25519) { + padded = payload; + } else if (payload.length > byteLength) { + if (keyType.params.algorithm == KeyType.Algorithm.EC) { + // Truncate + padded = Arrays.copyOf(payload, byteLength); + } else { + throw new IllegalArgumentException("Payload too large for key"); + } + } else if (payload.length < byteLength) { + // Left pad, with no external dependencies! + padded = new byte[byteLength]; + System.arraycopy(payload, 0, padded, padded.length - payload.length, payload.length); + } else { + padded = payload; } - - /** - * Performs a private key operation on the given payload. - * Any hashing and/or padding required should already be done prior to calling this method. - *

- * More commonly, the JCA classes provided should be used instead of directly calling this. - * - * @param slot the slot containing the private key to use - * @param keyType the type of the key stored in the slot - * @param payload the data to operate on - * @return the result of the operation - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @throws BadResponseException in case of incorrect YubiKey response - */ - public byte[] rawSignOrDecrypt(Slot slot, KeyType keyType, byte[] payload) throws IOException, ApduException, BadResponseException { - int byteLength = keyType.params.bitLength / 8; - byte[] padded; - if (keyType == KeyType.ED25519 || keyType == KeyType.X25519) { - padded = payload; - } else if (payload.length > byteLength) { - if (keyType.params.algorithm == KeyType.Algorithm.EC) { - // Truncate - padded = Arrays.copyOf(payload, byteLength); - } else { - throw new IllegalArgumentException("Payload too large for key"); - } - } else if (payload.length < byteLength) { - // Left pad, with no external dependencies! - padded = new byte[byteLength]; - System.arraycopy(payload, 0, padded, padded.length - payload.length, payload.length); - } else { - padded = payload; - } - Logger.debug(logger, "Decrypting data with key in slot {} of type {}", slot, keyType); - return usePrivateKey(slot, keyType, padded, false); + Logger.debug(logger, "Decrypting data with key in slot {} of type {}", slot, keyType); + return usePrivateKey(slot, keyType, padded, false); + } + + /** + * Decrypt an RSA-encrypted message. + * + *

DEPRECATED: Use the PivProvider JCA Security Provider instead. + * + * @param slot the slot containing the RSA private key to use + * @param cipherText the encrypted payload to decrypt + * @param algorithm the algorithm used for encryption + * @return the decrypted plaintext + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @throws BadResponseException in case of incorrect YubiKey response + * @throws NoSuchPaddingException in case the padding algorithm isn't supported + * @throws NoSuchAlgorithmException in case the algorithm isn't supported + * @throws BadPaddingException in case of a padding error + */ + @Deprecated + public byte[] decrypt(Slot slot, byte[] cipherText, Cipher algorithm) + throws IOException, + ApduException, + BadResponseException, + NoSuchAlgorithmException, + NoSuchPaddingException, + BadPaddingException { + KeyType keyType; + switch (cipherText.length) { + case 1024 / 8: + keyType = KeyType.RSA1024; + break; + case 2048 / 8: + keyType = KeyType.RSA2048; + break; + case 3072 / 8: + keyType = KeyType.RSA3072; + break; + case 4096 / 8: + keyType = KeyType.RSA4096; + break; + default: + throw new IllegalArgumentException("Invalid length of ciphertext"); } - - /** - * Decrypt an RSA-encrypted message. - *

- * DEPRECATED: Use the PivProvider JCA Security Provider instead. - * - * @param slot the slot containing the RSA private key to use - * @param cipherText the encrypted payload to decrypt - * @param algorithm the algorithm used for encryption - * @return the decrypted plaintext - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @throws BadResponseException in case of incorrect YubiKey response - * @throws NoSuchPaddingException in case the padding algorithm isn't supported - * @throws NoSuchAlgorithmException in case the algorithm isn't supported - * @throws BadPaddingException in case of a padding error - */ - @Deprecated - public byte[] decrypt(Slot slot, byte[] cipherText, Cipher algorithm) throws IOException, ApduException, BadResponseException, NoSuchAlgorithmException, NoSuchPaddingException, BadPaddingException { - KeyType keyType; - switch (cipherText.length) { - case 1024 / 8: - keyType = KeyType.RSA1024; - break; - case 2048 / 8: - keyType = KeyType.RSA2048; - break; - case 3072 / 8: - keyType = KeyType.RSA3072; - break; - case 4096 / 8: - keyType = KeyType.RSA4096; - break; - default: - throw new IllegalArgumentException("Invalid length of ciphertext"); - } - Logger.debug(logger, "Decrypting data with key in slot {} of type {}", slot, keyType); - return Padding.unpad(usePrivateKey(slot, keyType, cipherText, false), algorithm); + Logger.debug(logger, "Decrypting data with key in slot {} of type {}", slot, keyType); + return Padding.unpad(usePrivateKey(slot, keyType, cipherText, false), algorithm); + } + + /** + * Perform an ECDH operation with a given public key to compute a shared secret. + * + * @param slot the slot containing the private EC key + * @param peerPublicKey the peer public key for the operation + * @return the shared secret, comprising the x-coordinate of the ECDH result point. + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @throws BadResponseException in case of incorrect YubiKey response + */ + @Deprecated + public byte[] calculateSecret(Slot slot, ECPublicKey peerPublicKey) + throws IOException, ApduException, BadResponseException { + return calculateSecret(slot, peerPublicKey.getW()); + } + + /** + * Perform an ECDH operation with a given public key to compute a shared secret. + * + * @param slot the slot containing the private EC key + * @param peerPublicKey the peer public key for the operation + * @return the shared secret, comprising the x-coordinate of the ECDH result point. + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @throws BadResponseException in case of incorrect YubiKey response + */ + @Deprecated + public byte[] calculateSecret(Slot slot, ECPoint peerPublicKey) + throws IOException, ApduException, BadResponseException { + KeyType keyType = + peerPublicKey.getAffineX().bitLength() > 256 ? KeyType.ECCP384 : KeyType.ECCP256; + byte[] encodedPoint = + new PublicKeyValues.Ec( + ((KeyType.EcKeyParams) keyType.params).getCurveParams(), + peerPublicKey.getAffineX(), + peerPublicKey.getAffineY()) + .getEncodedPoint(); + Logger.debug(logger, "Performing key agreement with key in slot {} of type {}", slot, keyType); + return usePrivateKey(slot, keyType, encodedPoint, true); + } + + /** + * Perform an ECDH operation with a given public key to compute a shared secret. + * + * @param slot the slot containing the private EC key + * @param peerPublicKeyValues the peer public key values for the operation + * @return the shared secret, comprising the x-coordinate of the ECDH result point. + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @throws BadResponseException in case of incorrect YubiKey response + * @throws NoSuchAlgorithmException in case of unsupported PublicKey type + */ + public byte[] calculateSecret(Slot slot, PublicKeyValues peerPublicKeyValues) + throws IOException, ApduException, BadResponseException, NoSuchAlgorithmException { + if (peerPublicKeyValues instanceof PublicKeyValues.Cv25519) { + PublicKeyValues.Cv25519 publicKeyValues = (PublicKeyValues.Cv25519) peerPublicKeyValues; + KeyType keyType; + if (publicKeyValues.getCurveParams() == EllipticCurveValues.X25519) { + keyType = KeyType.X25519; + } else { + throw new NoSuchAlgorithmException("Illegal public key"); + } + Logger.debug( + logger, "Performing key agreement with key in slot {} of type {}", slot, keyType); + return usePrivateKey(slot, keyType, publicKeyValues.getBytes(), true); + } else if (peerPublicKeyValues instanceof PublicKeyValues.Ec) { + PublicKeyValues.Ec publicKeyValues = (PublicKeyValues.Ec) peerPublicKeyValues; + EllipticCurveValues ellipticCurveValues = publicKeyValues.getCurveParams(); + KeyType keyType = + ellipticCurveValues.getBitLength() > 256 ? KeyType.ECCP384 : KeyType.ECCP256; + Logger.debug( + logger, "Performing key agreement with key in slot {} of type {}", slot, keyType); + return usePrivateKey(slot, keyType, publicKeyValues.getEncodedPoint(), true); + } else { + throw new NoSuchAlgorithmException("Illegal public key"); } - - /** - * Perform an ECDH operation with a given public key to compute a shared secret. - * - * @param slot the slot containing the private EC key - * @param peerPublicKey the peer public key for the operation - * @return the shared secret, comprising the x-coordinate of the ECDH result point. - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @throws BadResponseException in case of incorrect YubiKey response - */ - @Deprecated - public byte[] calculateSecret(Slot slot, ECPublicKey peerPublicKey) throws IOException, ApduException, BadResponseException { - return calculateSecret(slot, peerPublicKey.getW()); + } + + private byte[] usePrivateKey(Slot slot, KeyType keyType, byte[] message, boolean exponentiation) + throws IOException, ApduException, BadResponseException { + // using generic authentication for sign requests + Map dataTlvs = new LinkedHashMap<>(); + dataTlvs.put(TAG_AUTH_RESPONSE, null); + dataTlvs.put(exponentiation ? TAG_AUTH_EXPONENTIATION : TAG_AUTH_CHALLENGE, message); + byte[] request = new Tlv(TAG_DYN_AUTH, Tlvs.encodeMap(dataTlvs)).getBytes(); + + try { + byte[] response = + protocol.sendAndReceive( + new Apdu(0, INS_AUTHENTICATE, keyType.value, slot.value, request)); + return Tlvs.unpackValue(TAG_AUTH_RESPONSE, Tlvs.unpackValue(TAG_DYN_AUTH, response)); + } catch (ApduException e) { + if (SW.INCORRECT_PARAMETERS == e.getSw()) { + // TODO: Replace with new CommandException subclass, wrapping e. + throw new ApduException( + e.getSw(), + String.format( + Locale.ROOT, + "Make sure that %s key is generated on slot %02X", + keyType.name(), + slot.value)); + } + throw e; } - - /** - * Perform an ECDH operation with a given public key to compute a shared secret. - * - * @param slot the slot containing the private EC key - * @param peerPublicKey the peer public key for the operation - * @return the shared secret, comprising the x-coordinate of the ECDH result point. - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @throws BadResponseException in case of incorrect YubiKey response - */ - @Deprecated - public byte[] calculateSecret(Slot slot, ECPoint peerPublicKey) throws IOException, ApduException, BadResponseException { - KeyType keyType = peerPublicKey.getAffineX().bitLength() > 256 ? KeyType.ECCP384 : KeyType.ECCP256; - byte[] encodedPoint = new PublicKeyValues.Ec(((KeyType.EcKeyParams)keyType.params).getCurveParams(), peerPublicKey.getAffineX(), peerPublicKey.getAffineY()).getEncodedPoint(); - Logger.debug(logger, "Performing key agreement with key in slot {} of type {}", slot, keyType); - return usePrivateKey(slot, keyType, encodedPoint, true); + } + + /** + * Change management key This method requires authentication {@link #authenticate}. + * + *

Thi setting requireTouch=true requires support for {@link #FEATURE_USAGE_POLICY}, available + * on YubiKey 4 or later. + * + * @param managementKey new value of management key + * @param requireTouch true to require touch for authentication + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + */ + public void setManagementKey( + ManagementKeyType keyType, byte[] managementKey, boolean requireTouch) + throws IOException, ApduException { + Logger.debug(logger, "Setting management key of type: {}", keyType); + if (keyType != ManagementKeyType.TDES) { + require(FEATURE_AES_KEY); } - - /** - * Perform an ECDH operation with a given public key to compute a shared secret. - * - * @param slot the slot containing the private EC key - * @param peerPublicKeyValues the peer public key values for the operation - * @return the shared secret, comprising the x-coordinate of the ECDH result point. - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @throws BadResponseException in case of incorrect YubiKey response - * @throws NoSuchAlgorithmException in case of unsupported PublicKey type - */ - public byte[] calculateSecret(Slot slot, PublicKeyValues peerPublicKeyValues) throws IOException, ApduException, BadResponseException, NoSuchAlgorithmException { - if (peerPublicKeyValues instanceof PublicKeyValues.Cv25519) { - PublicKeyValues.Cv25519 publicKeyValues = (PublicKeyValues.Cv25519) peerPublicKeyValues; - KeyType keyType; - if (publicKeyValues.getCurveParams() == EllipticCurveValues.X25519) { - keyType = KeyType.X25519; - } else { - throw new NoSuchAlgorithmException("Illegal public key"); - } - Logger.debug(logger, "Performing key agreement with key in slot {} of type {}", slot, keyType); - return usePrivateKey(slot, keyType, publicKeyValues.getBytes(), true); - } else if (peerPublicKeyValues instanceof PublicKeyValues.Ec) { - PublicKeyValues.Ec publicKeyValues = (PublicKeyValues.Ec) peerPublicKeyValues; - EllipticCurveValues ellipticCurveValues = publicKeyValues.getCurveParams(); - KeyType keyType = ellipticCurveValues.getBitLength() > 256 ? KeyType.ECCP384 : KeyType.ECCP256; - Logger.debug(logger, "Performing key agreement with key in slot {} of type {}", slot, keyType); - return usePrivateKey(slot, keyType, publicKeyValues.getEncodedPoint(), true); - } else { - throw new NoSuchAlgorithmException("Illegal public key"); - } + if (requireTouch) { + require(FEATURE_USAGE_POLICY); } - - private byte[] usePrivateKey(Slot slot, KeyType keyType, byte[] message, boolean exponentiation) throws IOException, ApduException, BadResponseException { - // using generic authentication for sign requests - Map dataTlvs = new LinkedHashMap<>(); - dataTlvs.put(TAG_AUTH_RESPONSE, null); - dataTlvs.put(exponentiation ? TAG_AUTH_EXPONENTIATION : TAG_AUTH_CHALLENGE, message); - byte[] request = new Tlv(TAG_DYN_AUTH, Tlvs.encodeMap(dataTlvs)).getBytes(); - - try { - byte[] response = protocol.sendAndReceive(new Apdu(0, INS_AUTHENTICATE, keyType.value, slot.value, request)); - return Tlvs.unpackValue(TAG_AUTH_RESPONSE, Tlvs.unpackValue(TAG_DYN_AUTH, response)); - } catch (ApduException e) { - if (SW.INCORRECT_PARAMETERS == e.getSw()) { - //TODO: Replace with new CommandException subclass, wrapping e. - throw new ApduException(e.getSw(), String.format(Locale.ROOT, "Make sure that %s key is generated on slot %02X", keyType.name(), slot.value)); - } - throw e; - } + if (managementKey.length != keyType.keyLength) { + throw new IllegalArgumentException( + String.format("Management key must be %d bytes", keyType.keyLength)); } - /** - * Change management key - * This method requires authentication {@link #authenticate}. - *

- * Thi setting requireTouch=true requires support for {@link #FEATURE_USAGE_POLICY}, available on YubiKey 4 or later. - * - * @param managementKey new value of management key - * @param requireTouch true to require touch for authentication - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - */ - public void setManagementKey(ManagementKeyType keyType, byte[] managementKey, boolean requireTouch) throws IOException, ApduException { - Logger.debug(logger, "Setting management key of type: {}", keyType); - if (keyType != ManagementKeyType.TDES) { - require(FEATURE_AES_KEY); - } - if (requireTouch) { - require(FEATURE_USAGE_POLICY); - } - if (managementKey.length != keyType.keyLength) { - throw new IllegalArgumentException(String.format("Management key must be %d bytes", keyType.keyLength)); - } - - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - stream.write(keyType.value); - stream.write(new Tlv(SLOT_CARD_MANAGEMENT, managementKey).getBytes()); - - // NOTE: if p2=0xfe key requires touch - // Require touch is only available on YubiKey 4 & 5. - protocol.sendAndReceive(new Apdu(0, INS_SET_MGMKEY, 0xff, requireTouch ? 0xfe : 0xff, stream.toByteArray())); - managementKeyType = keyType; - Logger.info(logger, "Management key set"); + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + stream.write(keyType.value); + stream.write(new Tlv(SLOT_CARD_MANAGEMENT, managementKey).getBytes()); + + // NOTE: if p2=0xfe key requires touch + // Require touch is only available on YubiKey 4 & 5. + protocol.sendAndReceive( + new Apdu(0, INS_SET_MGMKEY, 0xff, requireTouch ? 0xfe : 0xff, stream.toByteArray())); + managementKeyType = keyType; + Logger.info(logger, "Management key set"); + } + + /** + * Authenticate with pin 0 - PIN authentication blocked. Note: that 15 is the highest value that + * will be returned even if remaining tries is higher. + * + * @param pin string with pin (UTF-8) The default PIN code is 123456. + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @throws InvalidPinException in case if pin is invalid + */ + public void verifyPin(char[] pin) throws IOException, ApduException, InvalidPinException { + try { + Logger.debug(logger, "Verifying PIN"); + protocol.sendAndReceive(new Apdu(0, INS_VERIFY, 0, PIN_P2, pinBytes(pin))); + currentPinAttempts = maxPinAttempts; + } catch (ApduException e) { + int retries = getRetriesFromCode(e.getSw()); + if (retries >= 0) { + currentPinAttempts = retries; + throw new InvalidPinException(retries); + } else { + // status code returned error, not number of retries + throw e; + } } - - /** - * Authenticate with pin - * 0 - PIN authentication blocked. - * Note: that 15 is the highest value that will be returned even if remaining tries is higher. - * - * @param pin string with pin (UTF-8) - * The default PIN code is 123456. - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @throws InvalidPinException in case if pin is invalid - */ - public void verifyPin(char[] pin) throws IOException, ApduException, InvalidPinException { - try { - Logger.debug(logger, "Verifying PIN"); - protocol.sendAndReceive(new Apdu(0, INS_VERIFY, 0, PIN_P2, pinBytes(pin))); - currentPinAttempts = maxPinAttempts; - } catch (ApduException e) { - int retries = getRetriesFromCode(e.getSw()); - if (retries >= 0) { - currentPinAttempts = retries; - throw new InvalidPinException(retries); - } else { - // status code returned error, not number of retries - throw e; - } - } + } + + /** + * Reads metadata specific to YubiKey Bio multi-protocol. + * + * @return metadata about a slot + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @throws UnsupportedOperationException in case the metadata cannot be retrieved + */ + public BioMetadata getBioMetadata() throws IOException, ApduException { + Logger.debug(logger, "Getting bio metadata"); + try { + Map data = + Tlvs.decodeMap( + protocol.sendAndReceive(new Apdu(0, INS_GET_METADATA, 0, SLOT_OCC_AUTH, null))); + return new BioMetadata( + data.get(TAG_METADATA_BIO_CONFIGURED)[0] == 1, + data.get(TAG_METADATA_RETRIES)[0], + data.get(TAG_METADATA_TEMPORARY_PIN)[0] == 1); + } catch (ApduException apduException) { + if (apduException.getSw() == SW.REFERENCED_DATA_NOT_FOUND) { + throw new UnsupportedOperationException( + "Biometric verification not supported by this YubiKey"); + } + throw apduException; } - - /** - * Reads metadata specific to YubiKey Bio multi-protocol. - * - * @return metadata about a slot - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @throws UnsupportedOperationException in case the metadata cannot be retrieved - */ - public BioMetadata getBioMetadata() throws IOException, ApduException { - Logger.debug(logger, "Getting bio metadata"); - try { - Map data = Tlvs.decodeMap( - protocol.sendAndReceive(new Apdu(0, INS_GET_METADATA, 0, SLOT_OCC_AUTH, null))); - return new BioMetadata( - data.get(TAG_METADATA_BIO_CONFIGURED)[0] == 1, - data.get(TAG_METADATA_RETRIES)[0], - data.get(TAG_METADATA_TEMPORARY_PIN)[0] == 1 - ); - } catch (ApduException apduException) { - if (apduException.getSw() == SW.REFERENCED_DATA_NOT_FOUND) { - throw new UnsupportedOperationException("Biometric verification not supported by this YubiKey"); - } - throw apduException; - } + } + + /** + * Authenticate with YubiKey Bio multi-protocol capabilities. + * + *

Before calling this method, clients must verify that the authenticator is bio-capable and + * not blocked for bio matching. + * + * @param requestTemporaryPin after successful match generate a temporary PIN + * @param checkOnly check verification state of biometrics, don't perform UV + * @return temporary pin if requestTemporaryPin is true, otherwise null. + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @throws InvalidPinException in case of unsuccessful match + * @throws IllegalArgumentException in case of invalid key configuration + * @throws UnsupportedOperationException in case bio specific verification is not supported + */ + @Nullable public byte[] verifyUv(boolean requestTemporaryPin, boolean checkOnly) + throws IOException, ApduException, com.yubico.yubikit.core.application.InvalidPinException { + if (requestTemporaryPin && checkOnly) { + throw new IllegalArgumentException( + "Cannot request temporary pin when doing check-only verification"); } - /** - * Authenticate with YubiKey Bio multi-protocol capabilities. - *

- * Before calling this method, clients must verify that the authenticator is bio-capable and - * not blocked for bio matching. - * - * @param requestTemporaryPin after successful match generate a temporary PIN - * @param checkOnly check verification state of biometrics, don't perform UV - * @return temporary pin if requestTemporaryPin is true, otherwise null. - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @throws InvalidPinException in case of unsuccessful match - * @throws IllegalArgumentException in case of invalid key configuration - * @throws UnsupportedOperationException in case bio specific verification is not supported - */ - @Nullable - public byte[] verifyUv(boolean requestTemporaryPin, boolean checkOnly) - throws IOException, ApduException, - com.yubico.yubikit.core.application.InvalidPinException { - if (requestTemporaryPin && checkOnly) { - throw new IllegalArgumentException("Cannot request temporary pin when doing check-only verification"); - } - - try { - final int TAG_GET_TEMPORARY_PIN = 0x02; - final int TAG_VERIFY_UV = 0x03; - byte[] data = null; - if (!checkOnly) { - if (requestTemporaryPin) { - data = new Tlv(TAG_GET_TEMPORARY_PIN, null).getBytes(); - } else { - data = new Tlv(TAG_VERIFY_UV, null).getBytes(); - } - } - - byte[] response = protocol.sendAndReceive(new Apdu(0, INS_VERIFY, 0, SLOT_OCC_AUTH, data)); - return requestTemporaryPin ? response : null; - } catch (ApduException e) { - if (e.getSw() == SW.REFERENCED_DATA_NOT_FOUND) { - throw new UnsupportedOperationException("Biometric verification not supported by this YubiKey"); - } - int retries = getRetriesFromCode(e.getSw()); - if (retries >= 0) { - throw new com.yubico.yubikit.core.application.InvalidPinException( - retries, "Fingerprint mismatch, " + retries + " attempts remaining"); - } else { - // status code returned error, not number of retries - throw e; - } + try { + final int TAG_GET_TEMPORARY_PIN = 0x02; + final int TAG_VERIFY_UV = 0x03; + byte[] data = null; + if (!checkOnly) { + if (requestTemporaryPin) { + data = new Tlv(TAG_GET_TEMPORARY_PIN, null).getBytes(); + } else { + data = new Tlv(TAG_VERIFY_UV, null).getBytes(); } + } + + byte[] response = protocol.sendAndReceive(new Apdu(0, INS_VERIFY, 0, SLOT_OCC_AUTH, data)); + return requestTemporaryPin ? response : null; + } catch (ApduException e) { + if (e.getSw() == SW.REFERENCED_DATA_NOT_FOUND) { + throw new UnsupportedOperationException( + "Biometric verification not supported by this YubiKey"); + } + int retries = getRetriesFromCode(e.getSw()); + if (retries >= 0) { + throw new com.yubico.yubikit.core.application.InvalidPinException( + retries, "Fingerprint mismatch, " + retries + " attempts remaining"); + } else { + // status code returned error, not number of retries + throw e; + } } - - /** - * Authenticate YubiKey Bio multi-protocol with temporary PIN. - *

- * The PIN has to be generated by calling {@link #verifyUv(boolean, boolean)} and is valid only - * for operations during this session and depending on slot {@link PinPolicy}. - *

- * Before calling this method, clients must verify that the authenticator is bio-capable and - * not blocked for bio matching. - * - * @param pin temporary pin - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @throws InvalidPinException in case of unsuccessful match - * @throws IllegalArgumentException in case of invalid key configuration - * @throws UnsupportedOperationException in case bio specific verification is not supported - */ - public void verifyTemporaryPin(byte[] pin) - throws IOException, ApduException, - com.yubico.yubikit.core.application.InvalidPinException { - if (pin.length != TEMPORARY_PIN_LEN) { - throw new IllegalArgumentException("Temporary PIN must be exactly " + TEMPORARY_PIN_LEN + " bytes"); - } - - try { - final int TAG_VERIFY_TEMPORARY_PIN = 0x01; - protocol.sendAndReceive(new Apdu(0, INS_VERIFY, 0, SLOT_OCC_AUTH, new Tlv(TAG_VERIFY_TEMPORARY_PIN, pin).getBytes())); - } catch (ApduException e) { - if (e.getSw() == SW.REFERENCED_DATA_NOT_FOUND) { - throw new UnsupportedOperationException("Biometric verification not supported by this YubiKey"); - } - int retries = getRetriesFromCode(e.getSw()); - if (retries >= 0) { - throw new com.yubico.yubikit.core.application.InvalidPinException( - retries, "Invalid temporary PIN, " + retries + " attempts remaining"); - } else { - // status code returned error, not number of retries - throw e; - } - } + } + + /** + * Authenticate YubiKey Bio multi-protocol with temporary PIN. + * + *

The PIN has to be generated by calling {@link #verifyUv(boolean, boolean)} and is valid only + * for operations during this session and depending on slot {@link PinPolicy}. + * + *

Before calling this method, clients must verify that the authenticator is bio-capable and + * not blocked for bio matching. + * + * @param pin temporary pin + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @throws InvalidPinException in case of unsuccessful match + * @throws IllegalArgumentException in case of invalid key configuration + * @throws UnsupportedOperationException in case bio specific verification is not supported + */ + public void verifyTemporaryPin(byte[] pin) + throws IOException, ApduException, com.yubico.yubikit.core.application.InvalidPinException { + if (pin.length != TEMPORARY_PIN_LEN) { + throw new IllegalArgumentException( + "Temporary PIN must be exactly " + TEMPORARY_PIN_LEN + " bytes"); } - /** - * Receive number of attempts left for PIN from YubiKey - *

- * NOTE: If this command is run in a session where the correct PIN has already been verified, - * the correct value will not be retrievable, and the value returned may be incorrect if the - * number of total attempts has been changed from the default. - * - * @return number of attempts left - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - */ - public int getPinAttempts() throws IOException, ApduException { - Logger.debug(logger, "Getting PIN attempts"); - if (supports(FEATURE_METADATA)) { - // If metadata is available, use that - return getPinMetadata().getAttemptsRemaining(); - } - try { - // Null as data will not cause actual tries to decrement - protocol.sendAndReceive(new Apdu(0, INS_VERIFY, 0, PIN_P2, null)); - // Already verified, no way to know true count - Logger.debug(logger, "Using cached value, may be incorrect"); - return currentPinAttempts; - } catch (ApduException e) { - int retries = getRetriesFromCode(e.getSw()); - if (retries >= 0) { - currentPinAttempts = retries; - Logger.debug(logger, "Using value from empty verify"); - return retries; - } else { - // status code returned error, not number of retries - throw e; - } - } + try { + final int TAG_VERIFY_TEMPORARY_PIN = 0x01; + protocol.sendAndReceive( + new Apdu( + 0, INS_VERIFY, 0, SLOT_OCC_AUTH, new Tlv(TAG_VERIFY_TEMPORARY_PIN, pin).getBytes())); + } catch (ApduException e) { + if (e.getSw() == SW.REFERENCED_DATA_NOT_FOUND) { + throw new UnsupportedOperationException( + "Biometric verification not supported by this YubiKey"); + } + int retries = getRetriesFromCode(e.getSw()); + if (retries >= 0) { + throw new com.yubico.yubikit.core.application.InvalidPinException( + retries, "Invalid temporary PIN, " + retries + " attempts remaining"); + } else { + // status code returned error, not number of retries + throw e; + } } - - /** - * Change PIN. - * - * @param oldPin old pin for verification - * @param newPin new pin to set - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @throws InvalidPinException in case if pin is invalid - */ - public void changePin(char[] oldPin, char[] newPin) throws IOException, ApduException, InvalidPinException { - Logger.debug(logger, "Changing PIN"); - changeReference(INS_CHANGE_REFERENCE, PIN_P2, oldPin, newPin); - Logger.info(logger, "New PIN set"); + } + + /** + * Receive number of attempts left for PIN from YubiKey + * + *

NOTE: If this command is run in a session where the correct PIN has already been verified, + * the correct value will not be retrievable, and the value returned may be incorrect if the + * number of total attempts has been changed from the default. + * + * @return number of attempts left + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + */ + public int getPinAttempts() throws IOException, ApduException { + Logger.debug(logger, "Getting PIN attempts"); + if (supports(FEATURE_METADATA)) { + // If metadata is available, use that + return getPinMetadata().getAttemptsRemaining(); } - - /** - * Change PUK. - * - * @param oldPuk old puk for verification - * @param newPuk new puk to set - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @throws InvalidPinException in case if puk is invalid - */ - public void changePuk(char[] oldPuk, char[] newPuk) throws IOException, ApduException, InvalidPinException { - Logger.debug(logger, "Changing PUK"); - changeReference(INS_CHANGE_REFERENCE, PUK_P2, oldPuk, newPuk); - Logger.info(logger, "New PUK set"); + try { + // Null as data will not cause actual tries to decrement + protocol.sendAndReceive(new Apdu(0, INS_VERIFY, 0, PIN_P2, null)); + // Already verified, no way to know true count + Logger.debug(logger, "Using cached value, may be incorrect"); + return currentPinAttempts; + } catch (ApduException e) { + int retries = getRetriesFromCode(e.getSw()); + if (retries >= 0) { + currentPinAttempts = retries; + Logger.debug(logger, "Using value from empty verify"); + return retries; + } else { + // status code returned error, not number of retries + throw e; + } } - - /** - * Reset a blocked PIN to a new value using the PUK. - * - * @param puk puk for verification - * The default PUK code is 12345678. - * @param newPin new pin to set - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @throws InvalidPinException in case if puk is invalid - */ - public void unblockPin(char[] puk, char[] newPin) throws IOException, ApduException, InvalidPinException { - Logger.debug(logger, "Using PUK to set new PIN"); - changeReference(INS_RESET_RETRY, PIN_P2, puk, newPin); - Logger.info(logger, "New PIN set"); + } + + /** + * Change PIN. + * + * @param oldPin old pin for verification + * @param newPin new pin to set + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @throws InvalidPinException in case if pin is invalid + */ + public void changePin(char[] oldPin, char[] newPin) + throws IOException, ApduException, InvalidPinException { + Logger.debug(logger, "Changing PIN"); + changeReference(INS_CHANGE_REFERENCE, PIN_P2, oldPin, newPin); + Logger.info(logger, "New PIN set"); + } + + /** + * Change PUK. + * + * @param oldPuk old puk for verification + * @param newPuk new puk to set + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @throws InvalidPinException in case if puk is invalid + */ + public void changePuk(char[] oldPuk, char[] newPuk) + throws IOException, ApduException, InvalidPinException { + Logger.debug(logger, "Changing PUK"); + changeReference(INS_CHANGE_REFERENCE, PUK_P2, oldPuk, newPuk); + Logger.info(logger, "New PUK set"); + } + + /** + * Reset a blocked PIN to a new value using the PUK. + * + * @param puk puk for verification The default PUK code is 12345678. + * @param newPin new pin to set + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @throws InvalidPinException in case if puk is invalid + */ + public void unblockPin(char[] puk, char[] newPin) + throws IOException, ApduException, InvalidPinException { + Logger.debug(logger, "Using PUK to set new PIN"); + changeReference(INS_RESET_RETRY, PIN_P2, puk, newPin); + Logger.info(logger, "New PIN set"); + } + + /** + * Set the number of retries available for PIN and PUK entry. + * + *

This method requires authentication {@link #authenticate} and verification with pin {@link + * #verifyPin(char[])}}. + * + * @param pinAttempts the number of attempts to allow for PIN entry before blocking the PIN + * @param pukAttempts the number of attempts to allow for PUK entry before blocking the PUK + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + */ + public void setPinAttempts(int pinAttempts, int pukAttempts) throws IOException, ApduException { + Logger.debug(logger, "Setting PIN/PUK attempts ({}, {})", pinAttempts, pukAttempts); + protocol.sendAndReceive(new Apdu(0, INS_SET_PIN_RETRIES, pinAttempts, pukAttempts, null)); + maxPinAttempts = pinAttempts; + currentPinAttempts = pinAttempts; + Logger.info(logger, "PIN/PUK attempts set"); + } + + /** + * Reads metadata about the PIN, such as total number of retries, attempts left, and if the PIN + * has been changed from the default value. + * + *

This functionality requires support for {@link #FEATURE_METADATA}, available on YubiKey 5.3 + * or later. + * + * @return metadata about the PIN + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + */ + public PinMetadata getPinMetadata() throws IOException, ApduException { + Logger.debug(logger, "Getting PIN metadata"); + return getPinPukMetadata(PIN_P2); + } + + /** + * Reads metadata about the PUK, such as total number of retries, attempts left, and if the PUK + * has been changed from the default value. + * + *

This functionality requires support for {@link #FEATURE_METADATA}, available on YubiKey 5.3 + * or later. + * + * @return metadata about the PUK + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + */ + public PinMetadata getPukMetadata() throws IOException, ApduException { + Logger.debug(logger, "Getting PUK metadata"); + return getPinPukMetadata(PUK_P2); + } + + private PinMetadata getPinPukMetadata(byte p2) throws IOException, ApduException { + require(FEATURE_METADATA); + Map data = + Tlvs.decodeMap(protocol.sendAndReceive(new Apdu(0, INS_GET_METADATA, 0, p2, null))); + byte[] retries = data.get(TAG_METADATA_RETRIES); + return new PinMetadata( + data.get(TAG_METADATA_IS_DEFAULT)[0] != 0, + retries[INDEX_RETRIES_TOTAL], + retries[INDEX_RETRIES_REMAINING]); + } + + /** + * Reads metadata about the card management key. + * + *

This functionality requires support for {@link #FEATURE_METADATA}, available on YubiKey 5.3 + * or later. + * + * @return metadata about the card management key, such as the Touch policy and if the default + * value has been changed + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + */ + public ManagementKeyMetadata getManagementKeyMetadata() throws IOException, ApduException { + Logger.debug(logger, "Getting management key metadata"); + require(FEATURE_METADATA); + Map data = + Tlvs.decodeMap( + protocol.sendAndReceive(new Apdu(0, INS_GET_METADATA, 0, SLOT_CARD_MANAGEMENT, null))); + return new ManagementKeyMetadata( + data.containsKey(TAG_METADATA_ALGO) + ? ManagementKeyType.fromValue(data.get(TAG_METADATA_ALGO)[0]) + : ManagementKeyType.TDES, + data.get(TAG_METADATA_IS_DEFAULT)[0] != 0, + TouchPolicy.fromValue(data.get(TAG_METADATA_POLICY)[INDEX_TOUCH_POLICY])); + } + + /** Get card management key type. */ + public ManagementKeyType getManagementKeyType() { + return managementKeyType; + } + + /** + * Reads metadata about the private key stored in a slot. + * + *

This functionality requires support for {@link #FEATURE_METADATA}, available on YubiKey 5.3 + * or later. + * + * @param slot the slot to read metadata about + * @return metadata about a slot + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + */ + public SlotMetadata getSlotMetadata(Slot slot) throws IOException, ApduException { + Logger.debug(logger, "Getting metadata for slot {}", slot); + require(FEATURE_METADATA); + Map data = + Tlvs.decodeMap(protocol.sendAndReceive(new Apdu(0, INS_GET_METADATA, 0, slot.value, null))); + byte[] policy = data.get(TAG_METADATA_POLICY); + return new SlotMetadata( + KeyType.fromValue(data.get(TAG_METADATA_ALGO)[0]), + PinPolicy.fromValue(policy[INDEX_PIN_POLICY]), + TouchPolicy.fromValue(policy[INDEX_TOUCH_POLICY]), + data.get(TAG_METADATA_ORIGIN)[0] == ORIGIN_GENERATED, + data.get(TAG_METADATA_PUBLIC_KEY)); + } + + /** + * Reads the X.509 certificate stored in a slot. + * + * @param slot Key reference '9A', '9C', '9D', or '9E'. {@link Slot}. + * @return certificate instance + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @throws BadResponseException in case of incorrect YubiKey response + */ + public X509Certificate getCertificate(Slot slot) + throws IOException, ApduException, BadResponseException { + Logger.debug(logger, "Reading certificate in slot {}", slot); + byte[] objectData = getObject(slot.objectId); + + Map certData = Tlvs.decodeMap(objectData); + byte[] certInfo = certData.get(TAG_CERT_INFO); + byte[] cert = certData.get(TAG_CERTIFICATE); + + boolean isCompressed = certInfo != null && certInfo.length > 0 && certInfo[0] != 0; + if (isCompressed) { + try { + cert = GzipUtils.decompress(cert); + } catch (IOException e) { + throw new BadResponseException("Failed to decompress certificate", e); + } } - /** - * Set the number of retries available for PIN and PUK entry. - *

- * This method requires authentication {@link #authenticate} - * and verification with pin {@link #verifyPin(char[])}}. - * - * @param pinAttempts the number of attempts to allow for PIN entry before blocking the PIN - * @param pukAttempts the number of attempts to allow for PUK entry before blocking the PUK - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - */ - public void setPinAttempts(int pinAttempts, int pukAttempts) throws IOException, ApduException { - Logger.debug(logger, "Setting PIN/PUK attempts ({}, {})", pinAttempts, pukAttempts); - protocol.sendAndReceive(new Apdu(0, INS_SET_PIN_RETRIES, pinAttempts, pukAttempts, null)); - maxPinAttempts = pinAttempts; - currentPinAttempts = pinAttempts; - Logger.info(logger, "PIN/PUK attempts set"); + try { + return parseCertificate(cert); + } catch (CertificateException e) { + throw new BadResponseException("Failed to parse certificate: ", e); } - - /** - * Reads metadata about the PIN, such as total number of retries, attempts left, and if the PIN has been changed from the default value. - *

- * This functionality requires support for {@link #FEATURE_METADATA}, available on YubiKey 5.3 or later. - * - * @return metadata about the PIN - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - */ - public PinMetadata getPinMetadata() throws IOException, ApduException { - Logger.debug(logger, "Getting PIN metadata"); - return getPinPukMetadata(PIN_P2); + } + + /** + * Writes an X.509 certificate to a slot on the YubiKey. This method requires authentication + * {@link #authenticate}. + * + * @param slot Key reference '9A', '9C', '9D', or '9E'. {@link Slot}. + * @param certificate certificate to write + * @param compress If true the certificate will be compressed before being stored on the YubiKey + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + */ + public void putCertificate(Slot slot, X509Certificate certificate, boolean compress) + throws IOException, ApduException { + byte[] certBytes; + byte[] certInfo = {compress ? (byte) 0x01 : (byte) 0x00}; + Logger.debug(logger, "Storing {}certificate in slot {}", compress ? "compressed " : "", slot); + try { + certBytes = certificate.getEncoded(); + } catch (CertificateEncodingException e) { + throw new IllegalArgumentException("Failed to get encoded version of certificate", e); } - /** - * Reads metadata about the PUK, such as total number of retries, attempts left, and if the PUK has been changed from the default value. - *

- * This functionality requires support for {@link #FEATURE_METADATA}, available on YubiKey 5.3 or later. - * - * @return metadata about the PUK - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - */ - public PinMetadata getPukMetadata() throws IOException, ApduException { - Logger.debug(logger, "Getting PUK metadata"); - return getPinPukMetadata(PUK_P2); + if (compress) { + certBytes = GzipUtils.compress(certBytes); } - private PinMetadata getPinPukMetadata(byte p2) throws IOException, ApduException { - require(FEATURE_METADATA); - Map data = Tlvs.decodeMap(protocol.sendAndReceive(new Apdu(0, INS_GET_METADATA, 0, p2, null))); - byte[] retries = data.get(TAG_METADATA_RETRIES); - return new PinMetadata( - data.get(TAG_METADATA_IS_DEFAULT)[0] != 0, - retries[INDEX_RETRIES_TOTAL], - retries[INDEX_RETRIES_REMAINING] - ); + Map requestTlv = new LinkedHashMap<>(); + requestTlv.put(TAG_CERTIFICATE, certBytes); + requestTlv.put(TAG_CERT_INFO, certInfo); + requestTlv.put(TAG_LRC, null); + putObject(slot.objectId, Tlvs.encodeMap(requestTlv)); + } + + /** + * Writes an uncompressed X.509 certificate to a slot on the YubiKey. This method requires + * authentication {@link #authenticate}. + * + * @param slot Key reference '9A', '9C', '9D', or '9E'. {@link Slot}. + * @param certificate certificate to write + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + */ + public void putCertificate(Slot slot, X509Certificate certificate) + throws IOException, ApduException { + putCertificate(slot, certificate, false); + } + + /** + * Creates an attestation certificate for a private key which was generated on the YubiKey. + * + *

This functionality requires support for {@link #FEATURE_ATTESTATION}, available on YubiKey + * 4.3 or later. + * + *

A high level description of the thinking and how this can be used can be found at https://developers.yubico.com/PIV/Introduction/PIV_attestation.html + * Attestation works through a special key slot called "f9" this comes pre-loaded from factory + * with a key and cert signed by Yubico, but can be overwritten. After a key has been generated in + * a normal slot it can be attested by this special key + * + *

This method requires authentication {@link #authenticate} This method requires key to be + * generated on slot {@link #generateKey(Slot, KeyType, PinPolicy, TouchPolicy)} + * + * @param slot Key reference '9A', '9C', '9D', or '9E'. {@link Slot}. + * @return an attestation certificate for the key in the given slot + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @throws BadResponseException in case of incorrect YubiKey response + */ + public X509Certificate attestKey(Slot slot) + throws IOException, ApduException, BadResponseException { + require(FEATURE_ATTESTATION); + try { + byte[] responseData = protocol.sendAndReceive(new Apdu(0, INS_ATTEST, slot.value, 0, null)); + Logger.debug(logger, "Attested key in slot {}", slot); + return parseCertificate(responseData); + } catch (ApduException e) { + if (SW.INCORRECT_PARAMETERS == e.getSw()) { + throw new ApduException( + e.getSw(), + String.format(Locale.ROOT, "Make sure that key is generated on slot %02X", slot.value)); + } + throw e; + } catch (CertificateException e) { + throw new BadResponseException("Failed to parse certificate", e); } - - /** - * Reads metadata about the card management key. - *

- * This functionality requires support for {@link #FEATURE_METADATA}, available on YubiKey 5.3 or later. - * - * @return metadata about the card management key, such as the Touch policy and if the default value has been changed - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - */ - public ManagementKeyMetadata getManagementKeyMetadata() throws IOException, ApduException { - Logger.debug(logger, "Getting management key metadata"); - require(FEATURE_METADATA); - Map data = Tlvs.decodeMap(protocol.sendAndReceive(new Apdu(0, INS_GET_METADATA, 0, SLOT_CARD_MANAGEMENT, null))); - return new ManagementKeyMetadata( - data.containsKey(TAG_METADATA_ALGO) ? ManagementKeyType.fromValue(data.get(TAG_METADATA_ALGO)[0]) : ManagementKeyType.TDES, - data.get(TAG_METADATA_IS_DEFAULT)[0] != 0, - TouchPolicy.fromValue(data.get(TAG_METADATA_POLICY)[INDEX_TOUCH_POLICY]) - ); + } + + /** + * Deletes a certificate from the YubiKey. This method requires authentication {@link + * #authenticate} + * + *

Note: This does NOT delete any corresponding private key. + * + * @param slot Key reference '9A', '9C', '9D', or '9E'. {@link Slot}. + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + */ + public void deleteCertificate(Slot slot) throws IOException, ApduException { + Logger.debug(logger, "Deleting certificate in slot {}", slot); + putObject(slot.objectId, null); + } + + /* Parses a PublicKey from data returned from a YubiKey. */ + static PublicKeyValues parsePublicKeyFromDevice(KeyType keyType, byte[] encoded) { + Map dataObjects = Tlvs.decodeMap(encoded); + + if (keyType.params.algorithm == KeyType.Algorithm.RSA) { + BigInteger modulus = new BigInteger(1, dataObjects.get(0x81)); + BigInteger exponent = new BigInteger(1, dataObjects.get(0x82)); + return new PublicKeyValues.Rsa(modulus, exponent); + } else { + if (keyType == KeyType.ED25519 || keyType == KeyType.X25519) { + return new PublicKeyValues.Cv25519( + ((KeyType.EcKeyParams) keyType.params).getCurveParams(), dataObjects.get(0x86)); + } + return PublicKeyValues.Ec.fromEncodedPoint( + ((KeyType.EcKeyParams) keyType.params).getCurveParams(), dataObjects.get(0x86)); } - - /** - * Get card management key type. - */ - public ManagementKeyType getManagementKeyType() { - return managementKeyType; + } + + /** + * Checks if a given firmware version of YubiKey supports a specific key type with given policies. + * + * @param keyType the type of key to check + * @param pinPolicy the PIN policy to check + * @param touchPolicy the touch policy to check + * @param generate true to check if key generation is supported, false to check key import. + */ + public void checkKeySupport( + KeyType keyType, PinPolicy pinPolicy, TouchPolicy touchPolicy, boolean generate) { + if (version.major == 0) { + return; } - /** - * Reads metadata about the private key stored in a slot. - *

- * This functionality requires support for {@link #FEATURE_METADATA}, available on YubiKey 5.3 or later. - * - * @param slot the slot to read metadata about - * @return metadata about a slot - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - */ - public SlotMetadata getSlotMetadata(Slot slot) throws IOException, ApduException { - Logger.debug(logger, "Getting metadata for slot {}", slot); - require(FEATURE_METADATA); - Map data = Tlvs.decodeMap(protocol.sendAndReceive(new Apdu(0, INS_GET_METADATA, 0, slot.value, null))); - byte[] policy = data.get(TAG_METADATA_POLICY); - return new SlotMetadata( - KeyType.fromValue(data.get(TAG_METADATA_ALGO)[0]), - PinPolicy.fromValue(policy[INDEX_PIN_POLICY]), - TouchPolicy.fromValue(policy[INDEX_TOUCH_POLICY]), - data.get(TAG_METADATA_ORIGIN)[0] == ORIGIN_GENERATED, - data.get(TAG_METADATA_PUBLIC_KEY) - ); + if (keyType == KeyType.ED25519 || keyType == KeyType.X25519) { + require(FEATURE_CV25519); } - - /** - * Reads the X.509 certificate stored in a slot. - * - * @param slot Key reference '9A', '9C', '9D', or '9E'. {@link Slot}. - * @return certificate instance - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @throws BadResponseException in case of incorrect YubiKey response - */ - public X509Certificate getCertificate(Slot slot) throws IOException, ApduException, BadResponseException { - Logger.debug(logger, "Reading certificate in slot {}", slot); - byte[] objectData = getObject(slot.objectId); - - Map certData = Tlvs.decodeMap(objectData); - byte[] certInfo = certData.get(TAG_CERT_INFO); - byte[] cert = certData.get(TAG_CERTIFICATE); - - boolean isCompressed = certInfo != null && certInfo.length > 0 && certInfo[0] != 0; - if (isCompressed) { - try { - cert = GzipUtils.decompress(cert); - } catch (IOException e) { - throw new BadResponseException("Failed to decompress certificate", e); - } - } - - try { - return parseCertificate(cert); - } catch (CertificateException e) { - throw new BadResponseException("Failed to parse certificate: ", e); - } + if (keyType == KeyType.ECCP384) { + require(FEATURE_P384); } - - /** - * Writes an X.509 certificate to a slot on the YubiKey. - * This method requires authentication {@link #authenticate}. - * - * @param slot Key reference '9A', '9C', '9D', or '9E'. {@link Slot}. - * @param certificate certificate to write - * @param compress If true the certificate will be compressed before being stored on the YubiKey - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - */ - public void putCertificate(Slot slot, X509Certificate certificate, boolean compress) throws IOException, ApduException { - byte[] certBytes; - byte[] certInfo = {compress ? (byte) 0x01 : (byte) 0x00}; - Logger.debug(logger, "Storing {}certificate in slot {}", - compress ? "compressed " : "", - slot); - try { - certBytes = certificate.getEncoded(); - } catch (CertificateEncodingException e) { - throw new IllegalArgumentException("Failed to get encoded version of certificate", e); - } - - if (compress) { - certBytes = GzipUtils.compress(certBytes); - } - - Map requestTlv = new LinkedHashMap<>(); - requestTlv.put(TAG_CERTIFICATE, certBytes); - requestTlv.put(TAG_CERT_INFO, certInfo); - requestTlv.put(TAG_LRC, null); - putObject(slot.objectId, Tlvs.encodeMap(requestTlv)); + if (pinPolicy != PinPolicy.DEFAULT || touchPolicy != TouchPolicy.DEFAULT) { + require(FEATURE_USAGE_POLICY); + if (touchPolicy == TouchPolicy.CACHED) { + require(FEATURE_TOUCH_CACHED); + } } - /** - * Writes an uncompressed X.509 certificate to a slot on the YubiKey. - * This method requires authentication {@link #authenticate}. - * - * @param slot Key reference '9A', '9C', '9D', or '9E'. {@link Slot}. - * @param certificate certificate to write - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - */ - public void putCertificate(Slot slot, X509Certificate certificate) throws IOException, ApduException { - putCertificate(slot, certificate, false); + // ROCA + if (keyType.params.algorithm == KeyType.Algorithm.RSA) { + if (generate) { + require(FEATURE_RSA_GENERATION); + } + if (keyType.params.bitLength == 3072 || keyType.params.bitLength == 4096) { + require(FEATURE_RSA3072_RSA4096); + } } - /** - * Creates an attestation certificate for a private key which was generated on the YubiKey. - *

- * This functionality requires support for {@link #FEATURE_ATTESTATION}, available on YubiKey 4.3 or later. - *

- * A high level description of the thinking and how this can be used can be found at - * https://developers.yubico.com/PIV/Introduction/PIV_attestation.html - * Attestation works through a special key slot called "f9" this comes pre-loaded from factory with a key and cert signed by Yubico, - * but can be overwritten. After a key has been generated in a normal slot it can be attested by this special key - *

- * This method requires authentication {@link #authenticate} - * This method requires key to be generated on slot {@link #generateKey(Slot, KeyType, PinPolicy, TouchPolicy)} - * - * @param slot Key reference '9A', '9C', '9D', or '9E'. {@link Slot}. - * @return an attestation certificate for the key in the given slot - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @throws BadResponseException in case of incorrect YubiKey response - */ - public X509Certificate attestKey(Slot slot) throws IOException, ApduException, BadResponseException { - require(FEATURE_ATTESTATION); - try { - byte[] responseData = protocol.sendAndReceive(new Apdu(0, INS_ATTEST, slot.value, 0, null)); - Logger.debug(logger, "Attested key in slot {}", slot); - return parseCertificate(responseData); - } catch (ApduException e) { - if (SW.INCORRECT_PARAMETERS == e.getSw()) { - throw new ApduException(e.getSw(), String.format(Locale.ROOT, "Make sure that key is generated on slot %02X", slot.value)); - } - throw e; - } catch (CertificateException e) { - throw new BadResponseException("Failed to parse certificate", e); - } + // FIPS + if (version.isAtLeast(4, 4, 0) && version.isLessThan(4, 5, 0)) { + if (keyType == KeyType.RSA1024) { + throw new UnsupportedOperationException("RSA 1024 is not supported on YubiKey FIPS"); + } + if (pinPolicy == PinPolicy.NEVER) { + throw new UnsupportedOperationException("PinPolicy.NEVER is not allowed on YubiKey FIPS"); + } } - - /** - * Deletes a certificate from the YubiKey. - * This method requires authentication {@link #authenticate} - *

- * Note: This does NOT delete any corresponding private key. - * - * @param slot Key reference '9A', '9C', '9D', or '9E'. {@link Slot}. - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - */ - public void deleteCertificate(Slot slot) throws IOException, ApduException { - Logger.debug(logger, "Deleting certificate in slot {}", slot); - putObject(slot.objectId, null); + } + + /** + * Generates a new key pair within the YubiKey. This method requires verification with pin {@link + * #verifyPin}} and authentication with management key {@link #authenticate}. + * + *

RSA key types require {@link #FEATURE_RSA_GENERATION}, available on YubiKeys OTHER THAN + * 4.2.6-4.3.4. KeyType P348 requires {@link #FEATURE_P384}, available on YubiKey 4 or later. + * PinPolicy or TouchPolicy other than default require {@link #FEATURE_USAGE_POLICY}, available on + * YubiKey 4 or later. TouchPolicy.CACHED requires {@link #FEATURE_TOUCH_CACHED}, available on + * YubiKey 4.3 or later. + * + *

NOTE: YubiKey FIPS does not allow RSA1024 nor PinProtocol.NEVER. NOTE: This method will be + * renamed to generateKey in the next major version release of this library. + * + * @param slot Key reference '9A', '9C', '9D', or '9E'. {@link Slot}. + * @param keyType which algorithm is used for key generation {@link KeyType} + * @param pinPolicy the PIN policy for using the private key + * @param touchPolicy the touch policy for using the private key + * @return the public key of the generated key pair + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @throws BadResponseException in case of incorrect YubiKey response + */ + public PublicKeyValues generateKeyValues( + Slot slot, KeyType keyType, PinPolicy pinPolicy, TouchPolicy touchPolicy) + throws IOException, ApduException, BadResponseException { + checkKeySupport(keyType, pinPolicy, touchPolicy, true); + + Map tlvs = new LinkedHashMap<>(); + tlvs.put(TAG_GEN_ALGORITHM, new byte[] {keyType.value}); + if (pinPolicy != PinPolicy.DEFAULT) { + tlvs.put(TAG_PIN_POLICY, new byte[] {(byte) pinPolicy.value}); } - - /* Parses a PublicKey from data returned from a YubiKey. */ - static PublicKeyValues parsePublicKeyFromDevice(KeyType keyType, byte[] encoded) { - Map dataObjects = Tlvs.decodeMap(encoded); - - if (keyType.params.algorithm == KeyType.Algorithm.RSA) { - BigInteger modulus = new BigInteger(1, dataObjects.get(0x81)); - BigInteger exponent = new BigInteger(1, dataObjects.get(0x82)); - return new PublicKeyValues.Rsa(modulus, exponent); - } else { - if (keyType == KeyType.ED25519 || keyType == KeyType.X25519) { - return new PublicKeyValues.Cv25519(((KeyType.EcKeyParams) keyType.params).getCurveParams(), dataObjects.get(0x86)); - } - return PublicKeyValues.Ec.fromEncodedPoint(((KeyType.EcKeyParams) keyType.params).getCurveParams(), dataObjects.get(0x86)); - } + if (touchPolicy != TouchPolicy.DEFAULT) { + tlvs.put(TAG_TOUCH_POLICY, new byte[] {(byte) touchPolicy.value}); } - /** - * Checks if a given firmware version of YubiKey supports a specific key type with given policies. - * - * @param keyType the type of key to check - * @param pinPolicy the PIN policy to check - * @param touchPolicy the touch policy to check - * @param generate true to check if key generation is supported, false to check key import. - */ - public void checkKeySupport(KeyType keyType, PinPolicy pinPolicy, TouchPolicy touchPolicy, boolean generate) { - if (version.major == 0) { - return; - } - - if (keyType == KeyType.ED25519 || keyType == KeyType.X25519) { - require(FEATURE_CV25519); - } - if (keyType == KeyType.ECCP384) { - require(FEATURE_P384); - } - if (pinPolicy != PinPolicy.DEFAULT || touchPolicy != TouchPolicy.DEFAULT) { - require(FEATURE_USAGE_POLICY); - if (touchPolicy == TouchPolicy.CACHED) { - require(FEATURE_TOUCH_CACHED); - } - } - - // ROCA - if (keyType.params.algorithm == KeyType.Algorithm.RSA) { - if (generate) { - require(FEATURE_RSA_GENERATION); - } - if (keyType.params.bitLength == 3072 || keyType.params.bitLength == 4096) { - require(FEATURE_RSA3072_RSA4096); - } - } - - // FIPS - if (version.isAtLeast(4, 4, 0) && version.isLessThan(4, 5, 0)) { - if (keyType == KeyType.RSA1024) { - throw new UnsupportedOperationException("RSA 1024 is not supported on YubiKey FIPS"); - } - if (pinPolicy == PinPolicy.NEVER) { - throw new UnsupportedOperationException("PinPolicy.NEVER is not allowed on YubiKey FIPS"); - } - } + Logger.debug( + logger, "Generating key with pin_policy={}, touch_policy={}", pinPolicy, touchPolicy); + byte[] response = + protocol.sendAndReceive( + new Apdu( + 0, + INS_GENERATE_ASYMMETRIC, + 0, + slot.value, + new Tlv((byte) 0xac, Tlvs.encodeMap(tlvs)).getBytes())); + Logger.info(logger, "Private key generated in slot {} of type {}", slot, keyType); + // Tag '7F49' contains data objects for RSA or ECC + return parsePublicKeyFromDevice(keyType, Tlvs.unpackValue(0x7F49, response)); + } + + /** + * Generates a new key pair within the YubiKey. This method requires verification with pin {@link + * #verifyPin}} and authentication with management key {@link #authenticate}. + * + *

RSA key types require {@link #FEATURE_RSA_GENERATION}, available on YubiKeys OTHER THAN + * 4.2.6-4.3.4. KeyType P348 requires {@link #FEATURE_P384}, available on YubiKey 4 or later. + * PinPolicy or TouchPolicy other than default require {@link #FEATURE_USAGE_POLICY}, available on + * YubiKey 4 or later. TouchPolicy.CACHED requires {@link #FEATURE_TOUCH_CACHED}, available on + * YubiKey 4.3 or later. + * + *

NOTE: YubiKey FIPS does not allow RSA1024 nor PinProtocol.NEVER. + * + * @param slot Key reference '9A', '9C', '9D', or '9E'. {@link Slot}. + * @param keyType which algorithm is used for key generation {@link KeyType} + * @param pinPolicy the PIN policy for using the private key + * @param touchPolicy the touch policy for using the private key + * @return the public key of the generated key pair + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @throws BadResponseException in case of incorrect YubiKey response + * @deprecated use generateKeyValues instead, which will replace this method in the next major + * version release + */ + @Deprecated + public PublicKey generateKey( + Slot slot, KeyType keyType, PinPolicy pinPolicy, TouchPolicy touchPolicy) + throws IOException, ApduException, BadResponseException { + try { + return generateKeyValues(slot, keyType, pinPolicy, touchPolicy).toPublicKey(); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new RuntimeException(e); } - - /** - * Generates a new key pair within the YubiKey. - * This method requires verification with pin {@link #verifyPin}} - * and authentication with management key {@link #authenticate}. - *

- * RSA key types require {@link #FEATURE_RSA_GENERATION}, available on YubiKeys OTHER THAN 4.2.6-4.3.4. - * KeyType P348 requires {@link #FEATURE_P384}, available on YubiKey 4 or later. - * PinPolicy or TouchPolicy other than default require {@link #FEATURE_USAGE_POLICY}, available on YubiKey 4 or later. - * TouchPolicy.CACHED requires {@link #FEATURE_TOUCH_CACHED}, available on YubiKey 4.3 or later. - *

- * NOTE: YubiKey FIPS does not allow RSA1024 nor PinProtocol.NEVER. - * NOTE: This method will be renamed to generateKey in the next major version release of this library. - * - * @param slot Key reference '9A', '9C', '9D', or '9E'. {@link Slot}. - * @param keyType which algorithm is used for key generation {@link KeyType} - * @param pinPolicy the PIN policy for using the private key - * @param touchPolicy the touch policy for using the private key - * @return the public key of the generated key pair - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @throws BadResponseException in case of incorrect YubiKey response - */ - public PublicKeyValues generateKeyValues(Slot slot, KeyType keyType, PinPolicy pinPolicy, TouchPolicy touchPolicy) throws IOException, ApduException, BadResponseException { - checkKeySupport(keyType, pinPolicy, touchPolicy, true); - - Map tlvs = new LinkedHashMap<>(); - tlvs.put(TAG_GEN_ALGORITHM, new byte[]{keyType.value}); - if (pinPolicy != PinPolicy.DEFAULT) { - tlvs.put(TAG_PIN_POLICY, new byte[]{(byte) pinPolicy.value}); - } - if (touchPolicy != TouchPolicy.DEFAULT) { - tlvs.put(TAG_TOUCH_POLICY, new byte[]{(byte) touchPolicy.value}); - } - - Logger.debug(logger, "Generating key with pin_policy={}, touch_policy={}", pinPolicy, touchPolicy); - byte[] response = protocol.sendAndReceive(new Apdu(0, INS_GENERATE_ASYMMETRIC, 0, slot.value, new Tlv((byte) 0xac, Tlvs.encodeMap(tlvs)).getBytes())); - Logger.info(logger, "Private key generated in slot {} of type {}", slot, keyType); - // Tag '7F49' contains data objects for RSA or ECC - return parsePublicKeyFromDevice(keyType, Tlvs.unpackValue(0x7F49, response)); + } + + /** + * Import a private key into a slot. This method requires authentication {@link #authenticate}. + * + *

KeyType P348 requires {@link #FEATURE_P384}, available on YubiKey 4 or later. PinPolicy or + * TouchPolicy other than default require {@link #FEATURE_USAGE_POLICY}, available on YubiKey 4 or + * later. + * + *

NOTE: YubiKey FIPS does not allow RSA1024 nor PinProtocol.NEVER. + * + * @param slot Key reference '9A', '9C', '9D', or '9E'. {@link Slot}. + * @param key the private key to import + * @param pinPolicy the PIN policy for using the private key + * @param touchPolicy the touch policy for using the private key + * @return the KeyType value of the imported key + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + */ + public KeyType putKey( + Slot slot, PrivateKeyValues key, PinPolicy pinPolicy, TouchPolicy touchPolicy) + throws IOException, ApduException { + KeyType keyType = KeyType.fromKeyParams(key); + checkKeySupport(keyType, pinPolicy, touchPolicy, false); + + KeyType.KeyParams params = keyType.params; + Map tlvs = new LinkedHashMap<>(); + + switch (params.algorithm) { + case RSA: + int byteLength = params.bitLength / 8 / 2; + PrivateKeyValues.Rsa values = (PrivateKeyValues.Rsa) key; + tlvs.put(0x01, intToLength(values.getPrimeP(), byteLength)); // p + tlvs.put(0x02, intToLength(values.getPrimeQ(), byteLength)); // q + tlvs.put( + 0x03, + intToLength(Objects.requireNonNull(values.getPrimeExponentP()), byteLength)); // dmp1 + tlvs.put( + 0x04, + intToLength(Objects.requireNonNull(values.getPrimeExponentQ()), byteLength)); // dmq1 + tlvs.put( + 0x05, + intToLength(Objects.requireNonNull(values.getCrtCoefficient()), byteLength)); // iqmp + break; + case EC: + PrivateKeyValues.Ec ecPrivateKey = (PrivateKeyValues.Ec) key; + tlvs.put( + keyType == KeyType.ED25519 ? 0x07 : keyType == KeyType.X25519 ? 0x08 : 0x06, + ecPrivateKey.getSecret()); // s + break; } - /** - * Generates a new key pair within the YubiKey. - * This method requires verification with pin {@link #verifyPin}} - * and authentication with management key {@link #authenticate}. - *

- * RSA key types require {@link #FEATURE_RSA_GENERATION}, available on YubiKeys OTHER THAN 4.2.6-4.3.4. - * KeyType P348 requires {@link #FEATURE_P384}, available on YubiKey 4 or later. - * PinPolicy or TouchPolicy other than default require {@link #FEATURE_USAGE_POLICY}, available on YubiKey 4 or later. - * TouchPolicy.CACHED requires {@link #FEATURE_TOUCH_CACHED}, available on YubiKey 4.3 or later. - *

- * NOTE: YubiKey FIPS does not allow RSA1024 nor PinProtocol.NEVER. - * - * @param slot Key reference '9A', '9C', '9D', or '9E'. {@link Slot}. - * @param keyType which algorithm is used for key generation {@link KeyType} - * @param pinPolicy the PIN policy for using the private key - * @param touchPolicy the touch policy for using the private key - * @return the public key of the generated key pair - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @throws BadResponseException in case of incorrect YubiKey response - * @deprecated use generateKeyValues instead, which will replace this method in the next major version release - */ - @Deprecated - public PublicKey generateKey(Slot slot, KeyType keyType, PinPolicy pinPolicy, TouchPolicy touchPolicy) throws IOException, ApduException, BadResponseException { - try { - return generateKeyValues(slot, keyType, pinPolicy, touchPolicy).toPublicKey(); - } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { - throw new RuntimeException(e); - } + if (pinPolicy != PinPolicy.DEFAULT) { + tlvs.put(TAG_PIN_POLICY, new byte[] {(byte) pinPolicy.value}); } - - /** - * Import a private key into a slot. - * This method requires authentication {@link #authenticate}. - *

- * KeyType P348 requires {@link #FEATURE_P384}, available on YubiKey 4 or later. - * PinPolicy or TouchPolicy other than default require {@link #FEATURE_USAGE_POLICY}, available on YubiKey 4 or later. - *

- * NOTE: YubiKey FIPS does not allow RSA1024 nor PinProtocol.NEVER. - * - * @param slot Key reference '9A', '9C', '9D', or '9E'. {@link Slot}. - * @param key the private key to import - * @param pinPolicy the PIN policy for using the private key - * @param touchPolicy the touch policy for using the private key - * @return the KeyType value of the imported key - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - */ - public KeyType putKey(Slot slot, PrivateKeyValues key, PinPolicy pinPolicy, TouchPolicy touchPolicy) throws IOException, ApduException { - KeyType keyType = KeyType.fromKeyParams(key); - checkKeySupport(keyType, pinPolicy, touchPolicy, false); - - KeyType.KeyParams params = keyType.params; - Map tlvs = new LinkedHashMap<>(); - - switch (params.algorithm) { - case RSA: - int byteLength = params.bitLength / 8 / 2; - PrivateKeyValues.Rsa values = (PrivateKeyValues.Rsa) key; - tlvs.put(0x01, intToLength(values.getPrimeP(), byteLength)); // p - tlvs.put(0x02, intToLength(values.getPrimeQ(), byteLength)); // q - tlvs.put(0x03, intToLength(Objects.requireNonNull(values.getPrimeExponentP()), byteLength)); // dmp1 - tlvs.put(0x04, intToLength(Objects.requireNonNull(values.getPrimeExponentQ()), byteLength)); // dmq1 - tlvs.put(0x05, intToLength(Objects.requireNonNull(values.getCrtCoefficient()), byteLength)); // iqmp - break; - case EC: - PrivateKeyValues.Ec ecPrivateKey = (PrivateKeyValues.Ec) key; - tlvs.put(keyType == KeyType.ED25519 - ? 0x07 - : keyType == KeyType.X25519 - ? 0x08 - : 0x06, ecPrivateKey.getSecret()); // s - break; - } - - if (pinPolicy != PinPolicy.DEFAULT) { - tlvs.put(TAG_PIN_POLICY, new byte[]{(byte) pinPolicy.value}); - } - if (touchPolicy != TouchPolicy.DEFAULT) { - tlvs.put(TAG_TOUCH_POLICY, new byte[]{(byte) touchPolicy.value}); - } - - Logger.debug(logger, "Importing key with pin_policy={}, touch_policy={}", pinPolicy, touchPolicy); - protocol.sendAndReceive(new Apdu(0, INS_IMPORT_KEY, keyType.value, slot.value, Tlvs.encodeMap(tlvs))); - Logger.info(logger, "Private key imported in slot {} of type {}", slot, keyType); - return keyType; + if (touchPolicy != TouchPolicy.DEFAULT) { + tlvs.put(TAG_TOUCH_POLICY, new byte[] {(byte) touchPolicy.value}); } - /** - * Import a private key into a slot. - * This method requires authentication {@link #authenticate}. - *

- * KeyType P348 requires {@link #FEATURE_P384}, available on YubiKey 4 or later. - * PinPolicy or TouchPolicy other than default require {@link #FEATURE_USAGE_POLICY}, available on YubiKey 4 or later. - *

- * NOTE: YubiKey FIPS does not allow RSA1024 nor PinProtocol.NEVER. - * - * @param slot Key reference '9A', '9C', '9D', or '9E'. {@link Slot}. - * @param key the private key to import - * @param pinPolicy the PIN policy for using the private key - * @param touchPolicy the touch policy for using the private key - * @return the KeyType value of the imported key - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @deprecated use {@link #putKey(Slot, PrivateKeyValues, PinPolicy, TouchPolicy)} instead - */ - @Deprecated - public KeyType putKey(Slot slot, PrivateKey key, PinPolicy pinPolicy, TouchPolicy touchPolicy) throws IOException, ApduException { - return putKey(slot, PrivateKeyValues.fromPrivateKey(key), pinPolicy, touchPolicy); + Logger.debug( + logger, "Importing key with pin_policy={}, touch_policy={}", pinPolicy, touchPolicy); + protocol.sendAndReceive( + new Apdu(0, INS_IMPORT_KEY, keyType.value, slot.value, Tlvs.encodeMap(tlvs))); + Logger.info(logger, "Private key imported in slot {} of type {}", slot, keyType); + return keyType; + } + + /** + * Import a private key into a slot. This method requires authentication {@link #authenticate}. + * + *

KeyType P348 requires {@link #FEATURE_P384}, available on YubiKey 4 or later. PinPolicy or + * TouchPolicy other than default require {@link #FEATURE_USAGE_POLICY}, available on YubiKey 4 or + * later. + * + *

NOTE: YubiKey FIPS does not allow RSA1024 nor PinProtocol.NEVER. + * + * @param slot Key reference '9A', '9C', '9D', or '9E'. {@link Slot}. + * @param key the private key to import + * @param pinPolicy the PIN policy for using the private key + * @param touchPolicy the touch policy for using the private key + * @return the KeyType value of the imported key + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @deprecated use {@link #putKey(Slot, PrivateKeyValues, PinPolicy, TouchPolicy)} instead + */ + @Deprecated + public KeyType putKey(Slot slot, PrivateKey key, PinPolicy pinPolicy, TouchPolicy touchPolicy) + throws IOException, ApduException { + return putKey(slot, PrivateKeyValues.fromPrivateKey(key), pinPolicy, touchPolicy); + } + + /** + * Move key from one slot to another. The source slot must not be {@link Slot#ATTESTATION} and the + * destination slot must be empty. This method requires authentication with management key {@link + * #authenticate}. + * + * @param sourceSlot Slot to move the key from + * @param destinationSlot Slot to move the key to + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @see Slot + */ + public void moveKey(Slot sourceSlot, Slot destinationSlot) throws IOException, ApduException { + require(FEATURE_MOVE_KEY); + if (sourceSlot == Slot.ATTESTATION) { + throw new IllegalArgumentException("Can't move Attestation key (F9)"); } - - /** - * Move key from one slot to another. The source slot must not be {@link Slot#ATTESTATION} and the - * destination slot must be empty. This method requires authentication with management key - * {@link #authenticate}. - * - * @param sourceSlot Slot to move the key from - * @param destinationSlot Slot to move the key to - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @see Slot - */ - public void moveKey(Slot sourceSlot, Slot destinationSlot) throws IOException, ApduException { - require(FEATURE_MOVE_KEY); - if (sourceSlot == Slot.ATTESTATION) { - throw new IllegalArgumentException("Can't move Attestation key (F9)"); + Logger.debug( + logger, + "Move key from {} to {}", + sourceSlot.getStringAlias(), + destinationSlot.getStringAlias()); + protocol.sendAndReceive( + new Apdu(0, INS_MOVE_KEY, destinationSlot.value, sourceSlot.value, null)); + Logger.info( + logger, + "Moved key from {} to {}", + sourceSlot.getStringAlias(), + destinationSlot.getStringAlias()); + } + + /** + * Delete key from slot. This method requires authentication with management key {@link + * #authenticate}. + * + * @param slot Slot to delete key from. It is not possible to delete key from {@link + * Slot#ATTESTATION} + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @see Slot + */ + public void deleteKey(Slot slot) throws IOException, ApduException { + require(FEATURE_MOVE_KEY); + Logger.debug(logger, "Delete key from {}", slot.getStringAlias()); + protocol.sendAndReceive(new Apdu(0, INS_MOVE_KEY, 0xff, slot.value, null)); + Logger.info(logger, "Deleted key from {}", slot.getStringAlias()); + } + + /** + * Read a data object from the YubiKey. + * + * @param objectId the ID of the object to read, see {@link ObjectId}. + * @return the stored data object contents + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + * @throws BadResponseException in case of incorrect YubiKey response + */ + public byte[] getObject(int objectId) throws IOException, ApduException, BadResponseException { + Logger.debug(logger, "Reading data from object slot {}", Integer.toString(objectId, 16)); + byte[] requestData = new Tlv(TAG_OBJ_ID, ObjectId.getBytes(objectId)).getBytes(); + byte[] responseData = + protocol.sendAndReceive(new Apdu(0, INS_GET_DATA, 0x3f, 0xff, requestData)); + return Tlvs.unpackValue(TAG_OBJ_DATA, responseData); + } + + /** + * Write a data object to the YubiKey. + * + * @param objectId the ID of the object to write, see {@link ObjectId}. + * @param objectData the data object contents to write + * @throws IOException in case of connection error + * @throws ApduException in case of an error response from the YubiKey + */ + public void putObject(int objectId, @Nullable byte[] objectData) + throws IOException, ApduException { + Logger.debug(logger, "Writing data to object slot {}", Integer.toString(objectId, 16)); + Map tlvs = new LinkedHashMap<>(); + tlvs.put(TAG_OBJ_ID, ObjectId.getBytes(objectId)); + tlvs.put(TAG_OBJ_DATA, objectData); + protocol.sendAndReceive(new Apdu(0, INS_PUT_DATA, 0x3f, 0xff, Tlvs.encodeMap(tlvs))); + } + + /* + * Parses x509 certificate object from byte array + */ + private X509Certificate parseCertificate(byte[] data) throws CertificateException { + InputStream stream = new ByteArrayInputStream(data); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(stream); + } + + private void changeReference(byte instruction, byte p2, char[] value1, char[] value2) + throws IOException, ApduException, InvalidPinException { + byte[] pinBytes = pinBytes(value1, value2); + try { + protocol.sendAndReceive(new Apdu(0, instruction, 0, p2, pinBytes)); + } catch (ApduException e) { + int retries = getRetriesFromCode(e.getSw()); + if (retries >= 0) { + if (p2 == PIN_P2) { + currentPinAttempts = retries; } - Logger.debug(logger, "Move key from {} to {}", sourceSlot.getStringAlias(), destinationSlot.getStringAlias()); - protocol.sendAndReceive(new Apdu(0, INS_MOVE_KEY, destinationSlot.value, sourceSlot.value, null)); - Logger.info(logger, "Moved key from {} to {}", sourceSlot.getStringAlias(), destinationSlot.getStringAlias()); + throw new InvalidPinException(retries); + } else { + throw e; + } + } finally { + Arrays.fill(pinBytes, (byte) 0); } - - /** - * Delete key from slot. This method requires authentication with management key {@link #authenticate}. - * - * @param slot Slot to delete key from. It is not possible to delete key from {@link Slot#ATTESTATION} - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @see Slot - */ - public void deleteKey(Slot slot) throws IOException, ApduException { - require(FEATURE_MOVE_KEY); - Logger.debug(logger, "Delete key from {}", slot.getStringAlias()); - protocol.sendAndReceive(new Apdu(0, INS_MOVE_KEY, 0xff, slot.value, null)); - Logger.info(logger, "Deleted key from {}", slot.getStringAlias()); + } + + private void blockPin() throws IOException, ApduException { + // Note: that 15 is the highest value that will be returned even if remaining tries is higher. + Logger.debug(logger, "Verify PIN with invalid attempts until blocked"); + int counter = getPinAttempts(); + while (counter > 0) { + try { + verifyPin(new char[0]); + } catch (InvalidPinException e) { + counter = e.getAttemptsRemaining(); + } } - /** - * Read a data object from the YubiKey. - * - * @param objectId the ID of the object to read, see {@link ObjectId}. - * @return the stored data object contents - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - * @throws BadResponseException in case of incorrect YubiKey response - */ - public byte[] getObject(int objectId) throws IOException, ApduException, BadResponseException { - Logger.debug(logger, "Reading data from object slot {}", Integer.toString(objectId, 16)); - byte[] requestData = new Tlv(TAG_OBJ_ID, ObjectId.getBytes(objectId)).getBytes(); - byte[] responseData = protocol.sendAndReceive(new Apdu(0, INS_GET_DATA, 0x3f, 0xff, requestData)); - return Tlvs.unpackValue(TAG_OBJ_DATA, responseData); + Logger.debug(logger, "PIN is blocked"); + } + + private void blockPuk() throws IOException, ApduException { + // A failed unblock pin will return number of PUK tries left and also uses one try. + Logger.debug(logger, "Verify PUK with invalid attempts until blocked"); + int counter = 1; + while (counter > 0) { + try { + changeReference(INS_RESET_RETRY, PIN_P2, new char[0], new char[0]); + } catch (InvalidPinException e) { + counter = e.getAttemptsRemaining(); + } } - - /** - * Write a data object to the YubiKey. - * - * @param objectId the ID of the object to write, see {@link ObjectId}. - * @param objectData the data object contents to write - * @throws IOException in case of connection error - * @throws ApduException in case of an error response from the YubiKey - */ - public void putObject(int objectId, @Nullable byte[] objectData) throws IOException, ApduException { - Logger.debug(logger, "Writing data to object slot {}", Integer.toString(objectId, 16)); - Map tlvs = new LinkedHashMap<>(); - tlvs.put(TAG_OBJ_ID, ObjectId.getBytes(objectId)); - tlvs.put(TAG_OBJ_DATA, objectData); - protocol.sendAndReceive(new Apdu(0, INS_PUT_DATA, 0x3f, 0xff, Tlvs.encodeMap(tlvs))); + Logger.debug(logger, "PUK is blocked"); + } + + private static byte[] pinBytes(char[] pin) { + ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(pin)); + try { + int byteLen = byteBuffer.limit() - byteBuffer.position(); + if (byteLen > PIN_LEN) { + throw new IllegalArgumentException("PIN/PUK must be no longer than 8 bytes"); + } + byte[] alignedPinByte = Arrays.copyOf(byteBuffer.array(), PIN_LEN); + Arrays.fill(alignedPinByte, byteLen, PIN_LEN, (byte) 0xff); + return alignedPinByte; + } finally { + Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data } - - /* - * Parses x509 certificate object from byte array - */ - private X509Certificate parseCertificate(byte[] data) throws CertificateException { - InputStream stream = new ByteArrayInputStream(data); - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - return (X509Certificate) cf.generateCertificate(stream); + } + + private static byte[] pinBytes(char[] pin1, char[] pin2) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + byte[] pinBytes1 = pinBytes(pin1); + byte[] pinBytes2 = pinBytes(pin2); + try { + stream.write(pinBytes1); + stream.write(pinBytes2); + return stream.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); // This shouldn't happen + } finally { + Arrays.fill(pinBytes1, (byte) 0); // clear sensitive data + Arrays.fill(pinBytes2, (byte) 0); // clear sensitive data } - - private void changeReference(byte instruction, byte p2, char[] value1, char[] value2) throws IOException, ApduException, InvalidPinException { - byte[] pinBytes = pinBytes(value1, value2); - try { - protocol.sendAndReceive(new Apdu(0, instruction, 0, p2, pinBytes)); - } catch (ApduException e) { - int retries = getRetriesFromCode(e.getSw()); - if (retries >= 0) { - if (p2 == PIN_P2) { - currentPinAttempts = retries; - } - throw new InvalidPinException(retries); - } else { - throw e; - } - } finally { - Arrays.fill(pinBytes, (byte) 0); - } + } + + /* + * Parses number of left attempts from status code + */ + private int getRetriesFromCode(int statusCode) { + if (statusCode == SW.AUTH_METHOD_BLOCKED) { + return 0; } - - private void blockPin() throws IOException, ApduException { - // Note: that 15 is the highest value that will be returned even if remaining tries is higher. - Logger.debug(logger, "Verify PIN with invalid attempts until blocked"); - int counter = getPinAttempts(); - while (counter > 0) { - try { - verifyPin(new char[0]); - } catch (InvalidPinException e) { - counter = e.getAttemptsRemaining(); - } - } - - Logger.debug(logger, "PIN is blocked"); - } - - private void blockPuk() throws IOException, ApduException { - // A failed unblock pin will return number of PUK tries left and also uses one try. - Logger.debug(logger, "Verify PUK with invalid attempts until blocked"); - int counter = 1; - while (counter > 0) { - try { - changeReference(INS_RESET_RETRY, PIN_P2, new char[0], new char[0]); - } catch (InvalidPinException e) { - counter = e.getAttemptsRemaining(); - } - } - Logger.debug(logger, "PUK is blocked"); - } - - private static byte[] pinBytes(char[] pin) { - ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(pin)); - try { - int byteLen = byteBuffer.limit() - byteBuffer.position(); - if (byteLen > PIN_LEN) { - throw new IllegalArgumentException("PIN/PUK must be no longer than 8 bytes"); - } - byte[] alignedPinByte = Arrays.copyOf(byteBuffer.array(), PIN_LEN); - Arrays.fill(alignedPinByte, byteLen, PIN_LEN, (byte) 0xff); - return alignedPinByte; - } finally { - Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data - } - } - - private static byte[] pinBytes(char[] pin1, char[] pin2) { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - byte[] pinBytes1 = pinBytes(pin1); - byte[] pinBytes2 = pinBytes(pin2); - try { - stream.write(pinBytes1); - stream.write(pinBytes2); - return stream.toByteArray(); - } catch (IOException e) { - throw new RuntimeException(e); // This shouldn't happen - } finally { - Arrays.fill(pinBytes1, (byte) 0); // clear sensitive data - Arrays.fill(pinBytes2, (byte) 0); // clear sensitive data - } - } - - /* - * Parses number of left attempts from status code - */ - private int getRetriesFromCode(int statusCode) { - if (statusCode == SW.AUTH_METHOD_BLOCKED) { - return 0; - } - if (version.isLessThan(1, 0, 4)) { - if (statusCode >= 0x6300 && statusCode <= 0x63ff) { - return statusCode & 0xff; - } - } else { - if (statusCode >= 0x63c0 && statusCode <= 0x63cf) { - return statusCode & 0xf; - } - } - return -1; + if (version.isLessThan(1, 0, 4)) { + if (statusCode >= 0x6300 && statusCode <= 0x63ff) { + return statusCode & 0xff; + } + } else { + if (statusCode >= 0x63c0 && statusCode <= 0x63cf) { + return statusCode & 0xf; + } } -} \ No newline at end of file + return -1; + } +} diff --git a/piv/src/main/java/com/yubico/yubikit/piv/Slot.java b/piv/src/main/java/com/yubico/yubikit/piv/Slot.java index aeb1fec8..358a6497 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/Slot.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/Slot.java @@ -20,73 +20,71 @@ * A PIV slot for storing a private key, with a corresponding object ID for storing a certificate. */ public enum Slot { - AUTHENTICATION(0x9a, ObjectId.AUTHENTICATION), - // CARD_MANAGEMENT (0x9b) is intentionally left out as it functions differently. - SIGNATURE(0x9c, ObjectId.SIGNATURE), - KEY_MANAGEMENT(0x9d, ObjectId.KEY_MANAGEMENT), - CARD_AUTH(0x9e, ObjectId.CARD_AUTH), + AUTHENTICATION(0x9a, ObjectId.AUTHENTICATION), + // CARD_MANAGEMENT (0x9b) is intentionally left out as it functions differently. + SIGNATURE(0x9c, ObjectId.SIGNATURE), + KEY_MANAGEMENT(0x9d, ObjectId.KEY_MANAGEMENT), + CARD_AUTH(0x9e, ObjectId.CARD_AUTH), - RETIRED1(0x82, ObjectId.RETIRED1), - RETIRED2(0x83, ObjectId.RETIRED2), - RETIRED3(0x84, ObjectId.RETIRED3), - RETIRED4(0x85, ObjectId.RETIRED4), - RETIRED5(0x86, ObjectId.RETIRED5), - RETIRED6(0x87, ObjectId.RETIRED6), - RETIRED7(0x88, ObjectId.RETIRED7), - RETIRED8(0x89, ObjectId.RETIRED8), - RETIRED9(0x8a, ObjectId.RETIRED9), - RETIRED10(0x8b, ObjectId.RETIRED10), - RETIRED11(0x8c, ObjectId.RETIRED11), - RETIRED12(0x8d, ObjectId.RETIRED12), - RETIRED13(0x8e, ObjectId.RETIRED13), - RETIRED14(0x8f, ObjectId.RETIRED14), - RETIRED15(0x90, ObjectId.RETIRED15), - RETIRED16(0x91, ObjectId.RETIRED16), - RETIRED17(0x92, ObjectId.RETIRED17), - RETIRED18(0x93, ObjectId.RETIRED18), - RETIRED19(0x94, ObjectId.RETIRED19), - RETIRED20(0x95, ObjectId.RETIRED20), + RETIRED1(0x82, ObjectId.RETIRED1), + RETIRED2(0x83, ObjectId.RETIRED2), + RETIRED3(0x84, ObjectId.RETIRED3), + RETIRED4(0x85, ObjectId.RETIRED4), + RETIRED5(0x86, ObjectId.RETIRED5), + RETIRED6(0x87, ObjectId.RETIRED6), + RETIRED7(0x88, ObjectId.RETIRED7), + RETIRED8(0x89, ObjectId.RETIRED8), + RETIRED9(0x8a, ObjectId.RETIRED9), + RETIRED10(0x8b, ObjectId.RETIRED10), + RETIRED11(0x8c, ObjectId.RETIRED11), + RETIRED12(0x8d, ObjectId.RETIRED12), + RETIRED13(0x8e, ObjectId.RETIRED13), + RETIRED14(0x8f, ObjectId.RETIRED14), + RETIRED15(0x90, ObjectId.RETIRED15), + RETIRED16(0x91, ObjectId.RETIRED16), + RETIRED17(0x92, ObjectId.RETIRED17), + RETIRED18(0x93, ObjectId.RETIRED18), + RETIRED19(0x94, ObjectId.RETIRED19), + RETIRED20(0x95, ObjectId.RETIRED20), - ATTESTATION(0xf9, ObjectId.ATTESTATION); + ATTESTATION(0xf9, ObjectId.ATTESTATION); - public final int value; - public final int objectId; + public final int value; + public final int objectId; - Slot(int value, int objectId) { - this.value = value; - this.objectId = objectId; - } + Slot(int value, int objectId) { + this.value = value; + this.objectId = objectId; + } - /** - * Gets the String alias for the slot, which is a HEX representation of the slot value. - * - * @return the slot alias - */ - public String getStringAlias() { - return Integer.toString(value, 16); - } + /** + * Gets the String alias for the slot, which is a HEX representation of the slot value. + * + * @return the slot alias + */ + public String getStringAlias() { + return Integer.toString(value, 16); + } - /** - * Returns the PIV slot corresponding to the given ID. - */ - public static Slot fromValue(int value) { - for (Slot type : Slot.values()) { - if (type.value == value) { - return type; - } - } - throw new IllegalArgumentException("Not a valid Slot :" + value); + /** Returns the PIV slot corresponding to the given ID. */ + public static Slot fromValue(int value) { + for (Slot type : Slot.values()) { + if (type.value == value) { + return type; + } } + throw new IllegalArgumentException("Not a valid Slot :" + value); + } - /** - * Returns the PIV slot corresponding to the given String alias. - *

- * The alias should be the HEX representation of the slot value. - * - * @param alias a slot value as HEX string - * @return a Slot - */ - public static Slot fromStringAlias(String alias) { - return fromValue(Integer.parseInt(alias, 16)); - } + /** + * Returns the PIV slot corresponding to the given String alias. + * + *

The alias should be the HEX representation of the slot value. + * + * @param alias a slot value as HEX string + * @return a Slot + */ + public static Slot fromStringAlias(String alias) { + return fromValue(Integer.parseInt(alias, 16)); + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/SlotMetadata.java b/piv/src/main/java/com/yubico/yubikit/piv/SlotMetadata.java index ea784ebc..ae31911e 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/SlotMetadata.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/SlotMetadata.java @@ -16,78 +16,73 @@ package com.yubico.yubikit.piv; import com.yubico.yubikit.core.keys.PublicKeyValues; - import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.spec.InvalidKeySpecException; import java.util.Arrays; -/** - * Metadata about a key in a slot. - */ +/** Metadata about a key in a slot. */ public class SlotMetadata { - private final KeyType keyType; - private final PinPolicy pinPolicy; - private final TouchPolicy touchPolicy; - private final boolean generated; - private final byte[] publicKeyEncoded; + private final KeyType keyType; + private final PinPolicy pinPolicy; + private final TouchPolicy touchPolicy; + private final boolean generated; + private final byte[] publicKeyEncoded; - public SlotMetadata(KeyType keyType, PinPolicy pinPolicy, TouchPolicy touchPolicy, boolean generated, byte[] publicKeyEncoded) { - this.keyType = keyType; - this.pinPolicy = pinPolicy; - this.touchPolicy = touchPolicy; - this.generated = generated; - this.publicKeyEncoded = Arrays.copyOf(publicKeyEncoded, publicKeyEncoded.length); - } + public SlotMetadata( + KeyType keyType, + PinPolicy pinPolicy, + TouchPolicy touchPolicy, + boolean generated, + byte[] publicKeyEncoded) { + this.keyType = keyType; + this.pinPolicy = pinPolicy; + this.touchPolicy = touchPolicy; + this.generated = generated; + this.publicKeyEncoded = Arrays.copyOf(publicKeyEncoded, publicKeyEncoded.length); + } - /** - * Returns the type of the key stored in a slot. - */ - public KeyType getKeyType() { - return keyType; - } + /** Returns the type of the key stored in a slot. */ + public KeyType getKeyType() { + return keyType; + } - /** - * Returns the PIN policy for using the key. - */ - public PinPolicy getPinPolicy() { - return pinPolicy; - } + /** Returns the PIN policy for using the key. */ + public PinPolicy getPinPolicy() { + return pinPolicy; + } - /** - * Returns the touch policy for using the key. - */ - public TouchPolicy getTouchPolicy() { - return touchPolicy; - } + /** Returns the touch policy for using the key. */ + public TouchPolicy getTouchPolicy() { + return touchPolicy; + } - /** - * Whether the key was generated on the YubiKey or imported. A generated key can be attested, - * and exists only in a single YubiKey. - * - * @return true if the key was generated on the YubiKey - */ - public boolean isGenerated() { - return generated; - } + /** + * Whether the key was generated on the YubiKey or imported. A generated key can be attested, and + * exists only in a single YubiKey. + * + * @return true if the key was generated on the YubiKey + */ + public boolean isGenerated() { + return generated; + } - /** - * Returns the public key corresponding to the key in the slot. - */ - public PublicKeyValues getPublicKeyValues() { - return PivSession.parsePublicKeyFromDevice(keyType, publicKeyEncoded); - } + /** Returns the public key corresponding to the key in the slot. */ + public PublicKeyValues getPublicKeyValues() { + return PivSession.parsePublicKeyFromDevice(keyType, publicKeyEncoded); + } - /** - * Returns the public key corresponding to the key in the slot. - * @deprecated Use {@link #getPublicKeyValues()}.toPublicKey() instead. - */ - @Deprecated - public PublicKey getPublicKey() { - try { - return getPublicKeyValues().toPublicKey(); - } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { - throw new RuntimeException(e); - } + /** + * Returns the public key corresponding to the key in the slot. + * + * @deprecated Use {@link #getPublicKeyValues()}.toPublicKey() instead. + */ + @Deprecated + public PublicKey getPublicKey() { + try { + return getPublicKeyValues().toPublicKey(); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new RuntimeException(e); } + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/TouchPolicy.java b/piv/src/main/java/com/yubico/yubikit/piv/TouchPolicy.java index 511884ac..afdb02d1 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/TouchPolicy.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/TouchPolicy.java @@ -17,48 +17,40 @@ package com.yubico.yubikit.piv; /** - * The touch policy of a private key defines whether or not a user presence check (physical touch) is required to use the key. - *

- * Setting a Touch policy other than DEFAULT requires YubiKey 4 or later. + * The touch policy of a private key defines whether or not a user presence check (physical touch) + * is required to use the key. + * + *

Setting a Touch policy other than DEFAULT requires YubiKey 4 or later. */ public enum TouchPolicy { - /** - * The default behavior for the particular key slot is used, which is always NEVER. - */ - DEFAULT(0x0), + /** The default behavior for the particular key slot is used, which is always NEVER. */ + DEFAULT(0x0), - /** - * Touch is never required for using the key. - */ - NEVER(0x1), + /** Touch is never required for using the key. */ + NEVER(0x1), - /** - * Touch is always required for using the key. - */ - ALWAYS(0x2), + /** Touch is always required for using the key. */ + ALWAYS(0x2), - /** - * Touch is required, but cached for 15s after use, allowing multiple uses. - * This setting requires YubiKey 4.3 or later. - */ - CACHED(0x3); + /** + * Touch is required, but cached for 15s after use, allowing multiple uses. This setting requires + * YubiKey 4.3 or later. + */ + CACHED(0x3); - public final int value; + public final int value; - TouchPolicy(int value) { - this.value = value; - } + TouchPolicy(int value) { + this.value = value; + } - /** - * Returns the touch policy corresponding to the given PIV application constant. - */ - public static TouchPolicy fromValue(int value) { - for (TouchPolicy type : TouchPolicy.values()) { - if (type.value == value) { - return type; - } - } - throw new IllegalArgumentException("Not a valid TouchPolicy :" + value); + /** Returns the touch policy corresponding to the given PIV application constant. */ + public static TouchPolicy fromValue(int value) { + for (TouchPolicy type : TouchPolicy.values()) { + if (type.value == value) { + return type; + } } - + throw new IllegalArgumentException("Not a valid TouchPolicy :" + value); + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivAlgorithmParameterSpec.java b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivAlgorithmParameterSpec.java index 6dd45439..cda24de5 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivAlgorithmParameterSpec.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivAlgorithmParameterSpec.java @@ -20,40 +20,42 @@ import com.yubico.yubikit.piv.PinPolicy; import com.yubico.yubikit.piv.Slot; import com.yubico.yubikit.piv.TouchPolicy; - import java.security.spec.AlgorithmParameterSpec; import java.util.Arrays; - import javax.annotation.Nullable; import javax.security.auth.Destroyable; public class PivAlgorithmParameterSpec implements AlgorithmParameterSpec, Destroyable { - final Slot slot; - final KeyType keyType; - final PinPolicy pinPolicy; - final TouchPolicy touchPolicy; - @Nullable - final char[] pin; - private boolean destroyed = false; - - public PivAlgorithmParameterSpec(Slot slot, KeyType keyType, @Nullable PinPolicy pinPolicy, @Nullable TouchPolicy touchPolicy, @Nullable char[] pin) { - this.slot = slot; - this.keyType = keyType; - this.pinPolicy = pinPolicy != null ? pinPolicy : PinPolicy.DEFAULT; - this.touchPolicy = touchPolicy != null ? touchPolicy : TouchPolicy.DEFAULT; - this.pin = pin != null ? Arrays.copyOf(pin, pin.length) : null; + final Slot slot; + final KeyType keyType; + final PinPolicy pinPolicy; + final TouchPolicy touchPolicy; + @Nullable final char[] pin; + private boolean destroyed = false; + + public PivAlgorithmParameterSpec( + Slot slot, + KeyType keyType, + @Nullable PinPolicy pinPolicy, + @Nullable TouchPolicy touchPolicy, + @Nullable char[] pin) { + this.slot = slot; + this.keyType = keyType; + this.pinPolicy = pinPolicy != null ? pinPolicy : PinPolicy.DEFAULT; + this.touchPolicy = touchPolicy != null ? touchPolicy : TouchPolicy.DEFAULT; + this.pin = pin != null ? Arrays.copyOf(pin, pin.length) : null; + } + + @Override + public void destroy() { + if (pin != null) { + Arrays.fill(pin, (char) 0); } + destroyed = true; + } - @Override - public void destroy() { - if (pin != null) { - Arrays.fill(pin, (char) 0); - } - destroyed = true; - } - - @Override - public boolean isDestroyed() { - return destroyed; - } + @Override + public boolean isDestroyed() { + return destroyed; + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivCipherSpi.java b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivCipherSpi.java index a0a652d0..dd61fa52 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivCipherSpi.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivCipherSpi.java @@ -21,9 +21,6 @@ import com.yubico.yubikit.core.util.Result; import com.yubico.yubikit.piv.KeyType; import com.yubico.yubikit.piv.PivSession; - -import org.slf4j.LoggerFactory; - import java.io.ByteArrayOutputStream; import java.security.AlgorithmParameters; import java.security.InvalidAlgorithmParameterException; @@ -34,7 +31,6 @@ import java.security.SecureRandom; import java.security.spec.AlgorithmParameterSpec; import java.util.Map; - import javax.annotation.Nullable; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; @@ -42,145 +38,156 @@ import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.ShortBufferException; +import org.slf4j.LoggerFactory; public class PivCipherSpi extends CipherSpi { - private final Callback>> provider; - private final Map dummyKeys; - private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - @Nullable - private PivPrivateKey privateKey; - @Nullable - private String mode; - @Nullable - private String padding; - private int opmode = -1; - - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(PivCipherSpi.class); - - PivCipherSpi(Callback>> provider, Map dummyKeys) throws NoSuchPaddingException { - this.provider = provider; - this.dummyKeys = dummyKeys; - } - - @Override - protected void engineSetMode(String mode) throws NoSuchAlgorithmException { - this.mode = mode; - } - - @Override - protected void engineSetPadding(String padding) throws NoSuchPaddingException { - this.padding = padding; - } - - @Override - protected int engineGetBlockSize() { - if (privateKey == null) { - throw new IllegalStateException("Cipher not initialized"); - } - return privateKey.keyType.params.bitLength / 8; - } - - @Override - protected int engineGetOutputSize(int inputLen) { - return engineGetBlockSize(); - } - - @Override - protected byte[] engineGetIV() { - return new byte[0]; + private final Callback>> provider; + private final Map dummyKeys; + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + @Nullable private PivPrivateKey privateKey; + @Nullable private String mode; + @Nullable private String padding; + private int opmode = -1; + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(PivCipherSpi.class); + + PivCipherSpi( + Callback>> provider, Map dummyKeys) + throws NoSuchPaddingException { + this.provider = provider; + this.dummyKeys = dummyKeys; + } + + @Override + protected void engineSetMode(String mode) throws NoSuchAlgorithmException { + this.mode = mode; + } + + @Override + protected void engineSetPadding(String padding) throws NoSuchPaddingException { + this.padding = padding; + } + + @Override + protected int engineGetBlockSize() { + if (privateKey == null) { + throw new IllegalStateException("Cipher not initialized"); } - - @Override - @Nullable - protected AlgorithmParameters engineGetParameters() { - return null; - } - - @Override - protected void engineInit(int opmode, Key key, SecureRandom random) throws InvalidKeyException { - Logger.debug(logger, "Engine init: mode={} padding={}", mode, padding); - if (key instanceof PivPrivateKey) { - if (!KeyType.Algorithm.RSA.name().equals(key.getAlgorithm())) { - throw new InvalidKeyException("Cipher only supports RSA."); - } - privateKey = (PivPrivateKey) key; - this.opmode = opmode; - buffer.reset(); - } else { - throw new InvalidKeyException("Unsupported key type"); - } + return privateKey.keyType.params.bitLength / 8; + } + + @Override + protected int engineGetOutputSize(int inputLen) { + return engineGetBlockSize(); + } + + @Override + protected byte[] engineGetIV() { + return new byte[0]; + } + + @Override + @Nullable protected AlgorithmParameters engineGetParameters() { + return null; + } + + @Override + protected void engineInit(int opmode, Key key, SecureRandom random) throws InvalidKeyException { + Logger.debug(logger, "Engine init: mode={} padding={}", mode, padding); + if (key instanceof PivPrivateKey) { + if (!KeyType.Algorithm.RSA.name().equals(key.getAlgorithm())) { + throw new InvalidKeyException("Cipher only supports RSA."); + } + privateKey = (PivPrivateKey) key; + this.opmode = opmode; + buffer.reset(); + } else { + throw new InvalidKeyException("Unsupported key type"); } - - @Override - protected void engineInit(int opmode, Key key, @Nullable AlgorithmParameterSpec params, SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException { - if(params != null) { - throw new InvalidAlgorithmParameterException("Cipher must be initialized with params = null"); - } - engineInit(opmode, key, random); + } + + @Override + protected void engineInit( + int opmode, Key key, @Nullable AlgorithmParameterSpec params, SecureRandom random) + throws InvalidKeyException, InvalidAlgorithmParameterException { + if (params != null) { + throw new InvalidAlgorithmParameterException("Cipher must be initialized with params = null"); } - - @Override - protected void engineInit(int opmode, Key key, @Nullable AlgorithmParameters params, SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException { - if(params != null) { - throw new InvalidAlgorithmParameterException("Cipher must be initialized with params = null"); - } - engineInit(opmode, key, random); + engineInit(opmode, key, random); + } + + @Override + protected void engineInit( + int opmode, Key key, @Nullable AlgorithmParameters params, SecureRandom random) + throws InvalidKeyException, InvalidAlgorithmParameterException { + if (params != null) { + throw new InvalidAlgorithmParameterException("Cipher must be initialized with params = null"); } - - @Override - protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) { - buffer.write(input, inputOffset, inputLen); - return new byte[0]; + engineInit(opmode, key, random); + } + + @Override + protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) { + buffer.write(input, inputOffset, inputLen); + return new byte[0]; + } + + @Override + protected int engineUpdate( + byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) + throws ShortBufferException { + buffer.write(input, inputOffset, inputLen); + return 0; + } + + @Override + protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) + throws IllegalBlockSizeException, BadPaddingException { + if (privateKey == null) { + throw new IllegalStateException("Cipher not initialized"); } - - @Override - protected int engineUpdate(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) throws ShortBufferException { - buffer.write(input, inputOffset, inputLen); - return 0; + if (inputLen > 0) { + buffer.write(input, inputOffset, inputLen); } - - @Override - protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) throws IllegalBlockSizeException, BadPaddingException { - if (privateKey == null) { - throw new IllegalStateException("Cipher not initialized"); - } - if (inputLen > 0) { - buffer.write(input, inputOffset, inputLen); - } - byte[] cipherText = buffer.toByteArray(); - try { - KeyPair dummy = dummyKeys.get(privateKey.keyType); - Cipher rawRsa = Cipher.getInstance("RSA/ECB/NoPadding"); - rawRsa.init(opmode, dummy.getPublic()); - Cipher delegate = Cipher.getInstance("RSA/" + mode + "/" + padding); - delegate.init(opmode, dummy.getPrivate()); - switch (opmode) { - case Cipher.DECRYPT_MODE: // Decrypt, unpad - return delegate.doFinal(rawRsa.doFinal(privateKey.rawSignOrDecrypt(provider, cipherText))); - case Cipher.ENCRYPT_MODE: // Pad, decrypt - try { - return privateKey.rawSignOrDecrypt(provider, rawRsa.doFinal(delegate.doFinal(cipherText))); - } catch (BadPaddingException | IllegalBlockSizeException e) { - throw new IllegalStateException(e); // Shouldn't happen - } - default: - throw new UnsupportedOperationException(); - } - } catch (NoSuchPaddingException e) { - throw new UnsupportedOperationException("SecurityProvider doesn't support RSA without padding", e); - } catch (Exception e) { - throw new IllegalStateException(e); - } + byte[] cipherText = buffer.toByteArray(); + try { + KeyPair dummy = dummyKeys.get(privateKey.keyType); + Cipher rawRsa = Cipher.getInstance("RSA/ECB/NoPadding"); + rawRsa.init(opmode, dummy.getPublic()); + Cipher delegate = Cipher.getInstance("RSA/" + mode + "/" + padding); + delegate.init(opmode, dummy.getPrivate()); + switch (opmode) { + case Cipher.DECRYPT_MODE: // Decrypt, unpad + return delegate.doFinal( + rawRsa.doFinal(privateKey.rawSignOrDecrypt(provider, cipherText))); + case Cipher.ENCRYPT_MODE: // Pad, decrypt + try { + return privateKey.rawSignOrDecrypt( + provider, rawRsa.doFinal(delegate.doFinal(cipherText))); + } catch (BadPaddingException | IllegalBlockSizeException e) { + throw new IllegalStateException(e); // Shouldn't happen + } + default: + throw new UnsupportedOperationException(); + } + } catch (NoSuchPaddingException e) { + throw new UnsupportedOperationException( + "SecurityProvider doesn't support RSA without padding", e); + } catch (Exception e) { + throw new IllegalStateException(e); } - - @Override - protected int engineDoFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) throws ShortBufferException, IllegalBlockSizeException, BadPaddingException { - byte[] result = engineDoFinal(input, inputOffset, inputLen); - try { - System.arraycopy(result, 0, output, outputOffset, result.length); - return result.length; - } catch (IndexOutOfBoundsException e) { - throw new ShortBufferException(); - } + } + + @Override + protected int engineDoFinal( + byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) + throws ShortBufferException, IllegalBlockSizeException, BadPaddingException { + byte[] result = engineDoFinal(input, inputOffset, inputLen); + try { + System.arraycopy(result, 0, output, outputOffset, result.length); + return result.length; + } catch (IndexOutOfBoundsException e) { + throw new ShortBufferException(); } + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivEcSignatureSpi.java b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivEcSignatureSpi.java index 4036c1f2..0115d131 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivEcSignatureSpi.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivEcSignatureSpi.java @@ -19,7 +19,6 @@ import com.yubico.yubikit.core.util.Callback; import com.yubico.yubikit.core.util.Result; import com.yubico.yubikit.piv.PivSession; - import java.io.ByteArrayOutputStream; import java.security.InvalidKeyException; import java.security.InvalidParameterException; @@ -29,143 +28,142 @@ import java.security.PublicKey; import java.security.SignatureException; import java.security.SignatureSpi; - import javax.annotation.Nullable; public abstract class PivEcSignatureSpi extends SignatureSpi { - private final Callback>> provider; - @Nullable - private PivPrivateKey privateKey; - - protected PivEcSignatureSpi(Callback>> provider) { - this.provider = provider; + private final Callback>> provider; + @Nullable private PivPrivateKey privateKey; + + protected PivEcSignatureSpi(Callback>> provider) { + this.provider = provider; + } + + @Override + protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException { + throw new InvalidKeyException("Can only be used for signing."); + } + + @Override + protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException { + if (privateKey instanceof PivPrivateKey.EcKey) { + this.privateKey = (PivPrivateKey.EcKey) privateKey; + } else if (privateKey instanceof PivPrivateKey.Ed25519Key) { + this.privateKey = (PivPrivateKey.Ed25519Key) privateKey; + } else if (privateKey instanceof PivPrivateKey.X25519Key) { + this.privateKey = (PivPrivateKey.X25519Key) privateKey; + } else { + throw new InvalidKeyException("Unsupported key type"); } + } - @Override - protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException { - throw new InvalidKeyException("Can only be used for signing."); + protected abstract void update(byte b); + + protected abstract void update(byte[] b, int off, int len); + + protected abstract byte[] digest(); + + @Override + protected void engineUpdate(byte b) throws SignatureException { + if (privateKey != null) { + update(b); + } else { + throw new SignatureException("Not initialized"); + } + } + + @Override + protected void engineUpdate(byte[] b, int off, int len) throws SignatureException { + if (privateKey != null) { + update(b, off, len); + } else { + throw new SignatureException("Not initialized"); } + } - @Override - protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException { - if (privateKey instanceof PivPrivateKey.EcKey) { - this.privateKey = (PivPrivateKey.EcKey) privateKey; - } else if (privateKey instanceof PivPrivateKey.Ed25519Key) { - this.privateKey = (PivPrivateKey.Ed25519Key) privateKey; - } else if (privateKey instanceof PivPrivateKey.X25519Key) { - this.privateKey = (PivPrivateKey.X25519Key) privateKey; - } else { - throw new InvalidKeyException("Unsupported key type"); - } + @Override + protected byte[] engineSign() throws SignatureException { + if (privateKey == null) { + throw new SignatureException("Not initialized"); + } + try { + return privateKey.rawSignOrDecrypt(provider, digest()); + } catch (Exception e) { + throw new SignatureException(e); } + } - protected abstract void update(byte b); + @Override + protected boolean engineVerify(byte[] sigBytes) throws SignatureException { + throw new SignatureException("Not initialized"); + } - protected abstract void update(byte[] b, int off, int len); + @Override + protected void engineSetParameter(String param, Object value) throws InvalidParameterException { + throw new InvalidParameterException("ECDSA doesn't take parameters"); + } - protected abstract byte[] digest(); + @Override + protected Object engineGetParameter(String param) throws InvalidParameterException { + throw new InvalidParameterException("ECDSA doesn't take parameters"); + } + + public static class Prehashed extends PivEcSignatureSpi { + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + Prehashed(Callback>> provider) { + super(provider); + } @Override - protected void engineUpdate(byte b) throws SignatureException { - if (privateKey != null) { - update(b); - } else { - throw new SignatureException("Not initialized"); - } + protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException { + super.engineInitSign(privateKey); + buffer.reset(); } @Override - protected void engineUpdate(byte[] b, int off, int len) throws SignatureException { - if (privateKey != null) { - update(b, off, len); - } else { - throw new SignatureException("Not initialized"); - } + protected void update(byte b) { + buffer.write(b); } @Override - protected byte[] engineSign() throws SignatureException { - if (privateKey == null) { - throw new SignatureException("Not initialized"); - } - try { - return privateKey.rawSignOrDecrypt(provider, digest()); - } catch (Exception e) { - throw new SignatureException(e); - } + protected void update(byte[] b, int off, int len) { + buffer.write(b, off, len); } @Override - protected boolean engineVerify(byte[] sigBytes) throws SignatureException { - throw new SignatureException("Not initialized"); + protected byte[] digest() { + return buffer.toByteArray(); + } + } + + public static class Hashed extends PivEcSignatureSpi { + private final MessageDigest digest; + + Hashed(Callback>> provider, String algorithm) + throws NoSuchAlgorithmException { + super(provider); + digest = MessageDigest.getInstance(algorithm); } @Override - protected void engineSetParameter(String param, Object value) throws InvalidParameterException { - throw new InvalidParameterException("ECDSA doesn't take parameters"); + protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException { + super.engineInitSign(privateKey); + digest.reset(); } @Override - protected Object engineGetParameter(String param) throws InvalidParameterException { - throw new InvalidParameterException("ECDSA doesn't take parameters"); + protected void update(byte b) { + digest.update(b); } - public static class Prehashed extends PivEcSignatureSpi { - private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - - Prehashed(Callback>> provider) { - super(provider); - } - - @Override - protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException { - super.engineInitSign(privateKey); - buffer.reset(); - } - - @Override - protected void update(byte b) { - buffer.write(b); - } - - @Override - protected void update(byte[] b, int off, int len) { - buffer.write(b, off, len); - } - - @Override - protected byte[] digest() { - return buffer.toByteArray(); - } + @Override + protected void update(byte[] b, int off, int len) { + digest.update(b, off, len); } - public static class Hashed extends PivEcSignatureSpi { - private final MessageDigest digest; - - Hashed(Callback>> provider, String algorithm) throws NoSuchAlgorithmException { - super(provider); - digest = MessageDigest.getInstance(algorithm); - } - - @Override - protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException { - super.engineInitSign(privateKey); - digest.reset(); - } - - @Override - protected void update(byte b) { - digest.update(b); - } - - @Override - protected void update(byte[] b, int off, int len) { - digest.update(b, off, len); - } - - @Override - protected byte[] digest() { - return digest.digest(); - } + @Override + protected byte[] digest() { + return digest.digest(); } + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivKeyAgreementSpi.java b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivKeyAgreementSpi.java index e1f33ee9..51ae119c 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivKeyAgreementSpi.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivKeyAgreementSpi.java @@ -21,7 +21,6 @@ import com.yubico.yubikit.core.util.Callback; import com.yubico.yubikit.core.util.Result; import com.yubico.yubikit.piv.PivSession; - import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.Key; @@ -30,101 +29,101 @@ import java.security.SecureRandom; import java.security.interfaces.ECPublicKey; import java.security.spec.AlgorithmParameterSpec; - import javax.annotation.Nullable; import javax.crypto.KeyAgreementSpi; import javax.crypto.SecretKey; import javax.crypto.ShortBufferException; public class PivKeyAgreementSpi extends KeyAgreementSpi { - private final Callback>> provider; - @Nullable - private PivPrivateKey privateKey; - @Nullable - private PublicKeyValues publicKeyValues; + private final Callback>> provider; + @Nullable private PivPrivateKey privateKey; + @Nullable private PublicKeyValues publicKeyValues; - PivKeyAgreementSpi(Callback>> provider) { - this.provider = provider; - } + PivKeyAgreementSpi(Callback>> provider) { + this.provider = provider; + } - @Override - protected void engineInit(Key key, SecureRandom random) throws InvalidKeyException { - if (key instanceof PivPrivateKey.EcKey) { - privateKey = (PivPrivateKey.EcKey) key; - } else if (key instanceof PivPrivateKey.X25519Key) { - privateKey = (PivPrivateKey.X25519Key) key; - } else { - throw new InvalidKeyException("Key must be instance of PivPrivateKey"); - } + @Override + protected void engineInit(Key key, SecureRandom random) throws InvalidKeyException { + if (key instanceof PivPrivateKey.EcKey) { + privateKey = (PivPrivateKey.EcKey) key; + } else if (key instanceof PivPrivateKey.X25519Key) { + privateKey = (PivPrivateKey.X25519Key) key; + } else { + throw new InvalidKeyException("Key must be instance of PivPrivateKey"); } + } - @Override - protected void engineInit(Key key, AlgorithmParameterSpec params, SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException { - engineInit(key, random); - } + @Override + protected void engineInit(Key key, AlgorithmParameterSpec params, SecureRandom random) + throws InvalidKeyException, InvalidAlgorithmParameterException { + engineInit(key, random); + } - @Override - @Nullable - protected Key engineDoPhase(Key key, boolean lastPhase) throws InvalidKeyException, IllegalStateException { - if (privateKey == null) { - throw new IllegalStateException("KeyAgreement not initialized"); - } - if (!lastPhase) { - throw new IllegalStateException("Multiple phases not supported"); - } + @Override + @Nullable protected Key engineDoPhase(Key key, boolean lastPhase) + throws InvalidKeyException, IllegalStateException { + if (privateKey == null) { + throw new IllegalStateException("KeyAgreement not initialized"); + } + if (!lastPhase) { + throw new IllegalStateException("Multiple phases not supported"); + } - if (privateKey instanceof PivPrivateKey.EcKey && key instanceof ECPublicKey) { - PivPrivateKey.EcKey pivEcPrivateKey = (PivPrivateKey.EcKey) privateKey; - ECPublicKey ecPublicKey = (ECPublicKey) key; + if (privateKey instanceof PivPrivateKey.EcKey && key instanceof ECPublicKey) { + PivPrivateKey.EcKey pivEcPrivateKey = (PivPrivateKey.EcKey) privateKey; + ECPublicKey ecPublicKey = (ECPublicKey) key; - if (pivEcPrivateKey.getParams().getCurve().equals(ecPublicKey.getParams().getCurve())) { - publicKeyValues = PublicKeyValues.fromPublicKey(ecPublicKey); - return null; - } - } else if (privateKey instanceof PivPrivateKey.X25519Key && key instanceof PublicKey) { - publicKeyValues = PublicKeyValues.fromPublicKey((PublicKey) key); - if (publicKeyValues instanceof PublicKeyValues.Cv25519) { - PublicKeyValues.Cv25519 cv25519PublicKeyValues = (PublicKeyValues.Cv25519) publicKeyValues; - if (cv25519PublicKeyValues.getCurveParams() == EllipticCurveValues.X25519) { - return null; - } - } + if (pivEcPrivateKey.getParams().getCurve().equals(ecPublicKey.getParams().getCurve())) { + publicKeyValues = PublicKeyValues.fromPublicKey(ecPublicKey); + return null; + } + } else if (privateKey instanceof PivPrivateKey.X25519Key && key instanceof PublicKey) { + publicKeyValues = PublicKeyValues.fromPublicKey((PublicKey) key); + if (publicKeyValues instanceof PublicKeyValues.Cv25519) { + PublicKeyValues.Cv25519 cv25519PublicKeyValues = (PublicKeyValues.Cv25519) publicKeyValues; + if (cv25519PublicKeyValues.getCurveParams() == EllipticCurveValues.X25519) { + return null; } - - throw new InvalidKeyException("Wrong key type"); + } } - @Override - protected byte[] engineGenerateSecret() throws IllegalStateException { - if (privateKey != null && publicKeyValues != null) { - try { - if (privateKey instanceof PivPrivateKey.EcKey) { - return ((PivPrivateKey.EcKey) privateKey).keyAgreement(provider, publicKeyValues); - } else if (privateKey instanceof PivPrivateKey.X25519Key) { - return ((PivPrivateKey.X25519Key) privateKey).keyAgreement(provider, publicKeyValues); - } - } catch (Exception e) { - throw new IllegalStateException(e); - } finally { - publicKeyValues = null; - } - } - throw new IllegalStateException("Not initialized with both private and public keys"); - } + throw new InvalidKeyException("Wrong key type"); + } - @Override - protected int engineGenerateSecret(byte[] sharedSecret, int offset) throws IllegalStateException, ShortBufferException { - byte[] result = engineGenerateSecret(); - try { - System.arraycopy(result, 0, sharedSecret, offset, result.length); - return result.length; - } catch (IndexOutOfBoundsException e) { - throw new ShortBufferException(); + @Override + protected byte[] engineGenerateSecret() throws IllegalStateException { + if (privateKey != null && publicKeyValues != null) { + try { + if (privateKey instanceof PivPrivateKey.EcKey) { + return ((PivPrivateKey.EcKey) privateKey).keyAgreement(provider, publicKeyValues); + } else if (privateKey instanceof PivPrivateKey.X25519Key) { + return ((PivPrivateKey.X25519Key) privateKey).keyAgreement(provider, publicKeyValues); } + } catch (Exception e) { + throw new IllegalStateException(e); + } finally { + publicKeyValues = null; + } } + throw new IllegalStateException("Not initialized with both private and public keys"); + } - @Override - protected SecretKey engineGenerateSecret(String algorithm) throws IllegalStateException, NoSuchAlgorithmException, InvalidKeyException { - throw new IllegalStateException("Not supported"); + @Override + protected int engineGenerateSecret(byte[] sharedSecret, int offset) + throws IllegalStateException, ShortBufferException { + byte[] result = engineGenerateSecret(); + try { + System.arraycopy(result, 0, sharedSecret, offset, result.length); + return result.length; + } catch (IndexOutOfBoundsException e) { + throw new ShortBufferException(); } + } + + @Override + protected SecretKey engineGenerateSecret(String algorithm) + throws IllegalStateException, NoSuchAlgorithmException, InvalidKeyException { + throw new IllegalStateException("Not supported"); + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivKeyManager.java b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivKeyManager.java index 561af7c3..2f3c4395 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivKeyManager.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivKeyManager.java @@ -21,45 +21,44 @@ import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.Arrays; - import javax.net.ssl.X509ExtendedKeyManager; public class PivKeyManager extends X509ExtendedKeyManager { - private final PivPrivateKey privateKey; - private final X509Certificate[] certificates; + private final PivPrivateKey privateKey; + private final X509Certificate[] certificates; - public PivKeyManager(PivPrivateKey privateKey, X509Certificate[] certificates) { - this.privateKey = privateKey; - this.certificates = Arrays.copyOf(certificates, certificates.length); - } + public PivKeyManager(PivPrivateKey privateKey, X509Certificate[] certificates) { + this.privateKey = privateKey; + this.certificates = Arrays.copyOf(certificates, certificates.length); + } - @Override - public String[] getClientAliases(String keyType, Principal[] issuers) { - return new String[]{"YKPiv"}; - } + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return new String[] {"YKPiv"}; + } - @Override - public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { - return "YKPiv"; - } + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + return "YKPiv"; + } - @Override - public String[] getServerAliases(String keyType, Principal[] issuers) { - return new String[]{"YKPiv"}; - } + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return new String[] {"YKPiv"}; + } - @Override - public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { - return "YKPiv"; - } + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + return "YKPiv"; + } - @Override - public X509Certificate[] getCertificateChain(String alias) { - return Arrays.copyOf(certificates, certificates.length); - } + @Override + public X509Certificate[] getCertificateChain(String alias) { + return Arrays.copyOf(certificates, certificates.length); + } - @Override - public PrivateKey getPrivateKey(String alias) { - return privateKey; - } + @Override + public PrivateKey getPrivateKey(String alias) { + return privateKey; + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivKeyPairGeneratorSpi.java b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivKeyPairGeneratorSpi.java index 3fffb752..7ddf8ff7 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivKeyPairGeneratorSpi.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivKeyPairGeneratorSpi.java @@ -20,7 +20,6 @@ import com.yubico.yubikit.core.util.Result; import com.yubico.yubikit.piv.KeyType; import com.yubico.yubikit.piv.PivSession; - import java.security.InvalidAlgorithmParameterException; import java.security.KeyPair; import java.security.KeyPairGeneratorSpi; @@ -30,66 +29,77 @@ import java.security.spec.AlgorithmParameterSpec; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; - import javax.annotation.Nullable; abstract class PivKeyPairGeneratorSpi extends KeyPairGeneratorSpi { - private final Callback>> provider; - private final KeyType.Algorithm algorithm; + private final Callback>> provider; + private final KeyType.Algorithm algorithm; - PivKeyPairGeneratorSpi(Callback>> provider, KeyType.Algorithm algorithm) { - this.provider = provider; - this.algorithm = algorithm; - } + PivKeyPairGeneratorSpi( + Callback>> provider, KeyType.Algorithm algorithm) { + this.provider = provider; + this.algorithm = algorithm; + } - @Nullable - PivAlgorithmParameterSpec spec; + @Nullable PivAlgorithmParameterSpec spec; - @Override - public void initialize(AlgorithmParameterSpec params, SecureRandom random) throws InvalidAlgorithmParameterException { - if (params instanceof PivAlgorithmParameterSpec) { - spec = (PivAlgorithmParameterSpec) params; - if (spec.keyType.params.algorithm != algorithm) { - throw new InvalidAlgorithmParameterException("Invalid key algorithm for this KeyPairGenerator"); - } - } else { - throw new InvalidAlgorithmParameterException("Must be instance of PivAlgorithmParameterSpec"); - } + @Override + public void initialize(AlgorithmParameterSpec params, SecureRandom random) + throws InvalidAlgorithmParameterException { + if (params instanceof PivAlgorithmParameterSpec) { + spec = (PivAlgorithmParameterSpec) params; + if (spec.keyType.params.algorithm != algorithm) { + throw new InvalidAlgorithmParameterException( + "Invalid key algorithm for this KeyPairGenerator"); + } + } else { + throw new InvalidAlgorithmParameterException("Must be instance of PivAlgorithmParameterSpec"); } + } - @Override - public void initialize(int keySize, SecureRandom random) { - throw new IllegalArgumentException("Initialize with PivAlgorithmParameterSpec!"); - } + @Override + public void initialize(int keySize, SecureRandom random) { + throw new IllegalArgumentException("Initialize with PivAlgorithmParameterSpec!"); + } - @Override - public KeyPair generateKeyPair() { - if (spec == null) { - throw new IllegalStateException("KeyPairGenerator not initialized!"); - } - try { - BlockingQueue> queue = new ArrayBlockingQueue<>(1); - provider.invoke(result -> queue.add(Result.of(() -> { - PivSession session = result.getValue(); - PublicKey publicKey = session.generateKeyValues(spec.slot, spec.keyType, spec.pinPolicy, spec.touchPolicy).toPublicKey(); - PrivateKey privateKey = PivPrivateKey.from(publicKey, spec.slot, spec.pinPolicy, spec.touchPolicy, spec.pin); - return new KeyPair(publicKey, privateKey); - }))); - return queue.take().getValue(); - } catch (Exception e) { - throw new IllegalStateException("An error occurred when generating the key pair", e); - } + @Override + public KeyPair generateKeyPair() { + if (spec == null) { + throw new IllegalStateException("KeyPairGenerator not initialized!"); + } + try { + BlockingQueue> queue = new ArrayBlockingQueue<>(1); + provider.invoke( + result -> + queue.add( + Result.of( + () -> { + PivSession session = result.getValue(); + PublicKey publicKey = + session + .generateKeyValues( + spec.slot, spec.keyType, spec.pinPolicy, spec.touchPolicy) + .toPublicKey(); + PrivateKey privateKey = + PivPrivateKey.from( + publicKey, spec.slot, spec.pinPolicy, spec.touchPolicy, spec.pin); + return new KeyPair(publicKey, privateKey); + }))); + return queue.take().getValue(); + } catch (Exception e) { + throw new IllegalStateException("An error occurred when generating the key pair", e); } + } - public static class Rsa extends PivKeyPairGeneratorSpi { - Rsa(Callback>> provider) { - super(provider, KeyType.Algorithm.RSA); - } + public static class Rsa extends PivKeyPairGeneratorSpi { + Rsa(Callback>> provider) { + super(provider, KeyType.Algorithm.RSA); } + } - public static class Ec extends PivKeyPairGeneratorSpi { - Ec(Callback>> provider) { - super(provider, KeyType.Algorithm.EC); - } + public static class Ec extends PivKeyPairGeneratorSpi { + Ec(Callback>> provider) { + super(provider, KeyType.Algorithm.EC); } + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivKeyStoreKeyParameters.java b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivKeyStoreKeyParameters.java index 8d0d0f4c..f7baf876 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivKeyStoreKeyParameters.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivKeyStoreKeyParameters.java @@ -18,15 +18,14 @@ import com.yubico.yubikit.piv.PinPolicy; import com.yubico.yubikit.piv.TouchPolicy; - import java.security.KeyStore; public class PivKeyStoreKeyParameters implements KeyStore.ProtectionParameter { - final PinPolicy pinPolicy; - final TouchPolicy touchPolicy; + final PinPolicy pinPolicy; + final TouchPolicy touchPolicy; - public PivKeyStoreKeyParameters(PinPolicy pinPolicy, TouchPolicy touchPolicy) { - this.pinPolicy = pinPolicy; - this.touchPolicy = touchPolicy; - } + public PivKeyStoreKeyParameters(PinPolicy pinPolicy, TouchPolicy touchPolicy) { + this.pinPolicy = pinPolicy; + this.touchPolicy = touchPolicy; + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivKeyStoreSpi.java b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivKeyStoreSpi.java index eea74f2e..d3fe0f28 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivKeyStoreSpi.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivKeyStoreSpi.java @@ -27,7 +27,6 @@ import com.yubico.yubikit.piv.Slot; import com.yubico.yubikit.piv.SlotMetadata; import com.yubico.yubikit.piv.TouchPolicy; - import java.io.InputStream; import java.io.OutputStream; import java.security.InvalidParameterException; @@ -45,294 +44,329 @@ import java.util.Enumeration; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; - import javax.annotation.Nullable; public class PivKeyStoreSpi extends KeyStoreSpi { - private final Callback>> provider; - - PivKeyStoreSpi(Callback>> provider) { - this.provider = provider; - } - - private void putEntry(Slot slot, @Nullable PrivateKey key, PinPolicy pinPolicy, TouchPolicy touchPolicy, @Nullable X509Certificate certificate) throws Exception { - BlockingQueue> queue = new ArrayBlockingQueue<>(1); - provider.invoke(result -> queue.add(Result.of(() -> { - PivSession piv = result.getValue(); - if (key != null) { - piv.putKey(slot, PrivateKeyValues.fromPrivateKey(key), pinPolicy, touchPolicy); - } - if (certificate != null) { - piv.putCertificate(slot, certificate); - } - return true; - }))); - queue.take().getValue(); - } - - @Override - @Nullable - public Key engineGetKey(String alias, char[] password) throws UnrecoverableKeyException { - Slot slot = Slot.fromStringAlias(alias); - try { - BlockingQueue> queue = new ArrayBlockingQueue<>(1); - provider.invoke(result -> queue.add(Result.of(() -> { - PivSession session = result.getValue(); - if (session.supports(PivSession.FEATURE_METADATA)) { - SlotMetadata data = session.getSlotMetadata(slot); - return PivPrivateKey.from(data.getPublicKeyValues().toPublicKey(), slot, data.getPinPolicy(), data.getTouchPolicy(), password); - } else { - PublicKey publicKey = session.getCertificate(slot).getPublicKey(); - return PivPrivateKey.from(publicKey, slot, null, null, password); - } - }))); - return queue.take().getValue(); - } catch (BadResponseException e) { - throw new UnrecoverableKeyException("No way to infer KeyType, make sure the matching certificate is stored"); - } catch (ApduException e) { - if (e.getSw() == SW.FILE_NOT_FOUND) { - return null; - } - throw new RuntimeException(e); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public Certificate[] engineGetCertificateChain(String alias) { - return new Certificate[]{engineGetCertificate(alias)}; - } - - @Override - @Nullable - public Certificate engineGetCertificate(String alias) { - Slot slot = Slot.fromStringAlias(alias); - BlockingQueue> queue = new ArrayBlockingQueue<>(1); - provider.invoke(result -> queue.add(Result.of(() -> result.getValue().getCertificate(slot)))); - - try { - return queue.take().getValue(); - } catch (BadResponseException e) { - // Malformed certificate? - return null; - } catch (ApduException e) { - if (e.getSw() == SW.FILE_NOT_FOUND) { - // Empty slot - return null; - } - throw new RuntimeException(e); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - @Nullable - public KeyStore.Entry engineGetEntry(String alias, KeyStore.ProtectionParameter protParam) throws - UnrecoverableEntryException { - Slot slot = Slot.fromStringAlias(alias); - try { - BlockingQueue> queue = new ArrayBlockingQueue<>(1); - provider.invoke(result -> queue.add(Result.of(() -> { - PivSession session = result.getValue(); - Certificate certificate = session.getCertificate(slot); - char[] pin = null; - if (protParam instanceof KeyStore.PasswordProtection) { - pin = ((KeyStore.PasswordProtection) protParam).getPassword(); - } - PrivateKey key; - if (session.supports(PivSession.FEATURE_METADATA)) { - SlotMetadata data = session.getSlotMetadata(slot); - key = PivPrivateKey.from(data.getPublicKeyValues().toPublicKey(), slot, data.getPinPolicy(), data.getTouchPolicy(), pin); - } else { - PublicKey publicKey = certificate.getPublicKey(); - key = PivPrivateKey.from(publicKey, slot, null, null, pin); - } - return new KeyStore.PrivateKeyEntry(key, new Certificate[]{certificate}); - }))); - return queue.take().getValue(); - } catch (BadResponseException e) { - throw new UnrecoverableEntryException("Make sure the matching certificate is stored"); - } catch (ApduException e) { - if (e.getSw() == SW.FILE_NOT_FOUND) { - // Empty slot - return null; - } - throw new RuntimeException(e); - } catch (Exception e) { - throw new RuntimeException(e); - } + private final Callback>> provider; + + PivKeyStoreSpi(Callback>> provider) { + this.provider = provider; + } + + private void putEntry( + Slot slot, + @Nullable PrivateKey key, + PinPolicy pinPolicy, + TouchPolicy touchPolicy, + @Nullable X509Certificate certificate) + throws Exception { + BlockingQueue> queue = new ArrayBlockingQueue<>(1); + provider.invoke( + result -> + queue.add( + Result.of( + () -> { + PivSession piv = result.getValue(); + if (key != null) { + piv.putKey( + slot, PrivateKeyValues.fromPrivateKey(key), pinPolicy, touchPolicy); + } + if (certificate != null) { + piv.putCertificate(slot, certificate); + } + return true; + }))); + queue.take().getValue(); + } + + @Override + @Nullable public Key engineGetKey(String alias, char[] password) throws UnrecoverableKeyException { + Slot slot = Slot.fromStringAlias(alias); + try { + BlockingQueue> queue = new ArrayBlockingQueue<>(1); + provider.invoke( + result -> + queue.add( + Result.of( + () -> { + PivSession session = result.getValue(); + if (session.supports(PivSession.FEATURE_METADATA)) { + SlotMetadata data = session.getSlotMetadata(slot); + return PivPrivateKey.from( + data.getPublicKeyValues().toPublicKey(), + slot, + data.getPinPolicy(), + data.getTouchPolicy(), + password); + } else { + PublicKey publicKey = session.getCertificate(slot).getPublicKey(); + return PivPrivateKey.from(publicKey, slot, null, null, password); + } + }))); + return queue.take().getValue(); + } catch (BadResponseException e) { + throw new UnrecoverableKeyException( + "No way to infer KeyType, make sure the matching certificate is stored"); + } catch (ApduException e) { + if (e.getSw() == SW.FILE_NOT_FOUND) { + return null; + } + throw new RuntimeException(e); + } catch (Exception e) { + throw new RuntimeException(e); } - - @Override - @Nullable - public Date engineGetCreationDate(String alias) { + } + + @Override + public Certificate[] engineGetCertificateChain(String alias) { + return new Certificate[] {engineGetCertificate(alias)}; + } + + @Override + @Nullable public Certificate engineGetCertificate(String alias) { + Slot slot = Slot.fromStringAlias(alias); + BlockingQueue> queue = new ArrayBlockingQueue<>(1); + provider.invoke(result -> queue.add(Result.of(() -> result.getValue().getCertificate(slot)))); + + try { + return queue.take().getValue(); + } catch (BadResponseException e) { + // Malformed certificate? + return null; + } catch (ApduException e) { + if (e.getSw() == SW.FILE_NOT_FOUND) { + // Empty slot return null; + } + throw new RuntimeException(e); + } catch (Exception e) { + throw new RuntimeException(e); } - - @Override - public void engineSetEntry(String alias, KeyStore.Entry - entry, @Nullable KeyStore.ProtectionParameter protParam) throws KeyStoreException { - Slot slot = Slot.fromStringAlias(alias); - - PrivateKey privateKey = null; - Certificate certificate; - if (entry instanceof KeyStore.TrustedCertificateEntry) { - if (protParam != null) { - throw new KeyStoreException("Certificate cannot use protParam"); - } - certificate = ((KeyStore.TrustedCertificateEntry) entry).getTrustedCertificate(); - } else if (entry instanceof KeyStore.PrivateKeyEntry) { - certificate = ((KeyStore.PrivateKeyEntry) entry).getCertificate(); - privateKey = ((KeyStore.PrivateKeyEntry) entry).getPrivateKey(); - } else { - throw new KeyStoreException("Unsupported KeyStore entry."); - } - - if (certificate != null) { - if (!(certificate instanceof X509Certificate)) { - throw new KeyStoreException("Certificate must be X509Certificate"); - } - } - - PinPolicy pinPolicy = PinPolicy.DEFAULT; - TouchPolicy touchPolicy = TouchPolicy.DEFAULT; - if (privateKey != null) { - if (protParam != null) { - if (protParam instanceof PivKeyStoreKeyParameters) { - pinPolicy = ((PivKeyStoreKeyParameters) protParam).pinPolicy; - touchPolicy = ((PivKeyStoreKeyParameters) protParam).touchPolicy; - } else { - throw new KeyStoreException("protParam must be an instance of PivKeyStoreKeyParameters"); - } - } - } - - try { - putEntry(slot, privateKey, pinPolicy, touchPolicy, (X509Certificate) certificate); - } catch (Exception e) { - throw new KeyStoreException(e); - } + } + + @Override + @Nullable public KeyStore.Entry engineGetEntry(String alias, KeyStore.ProtectionParameter protParam) + throws UnrecoverableEntryException { + Slot slot = Slot.fromStringAlias(alias); + try { + BlockingQueue> queue = new ArrayBlockingQueue<>(1); + provider.invoke( + result -> + queue.add( + Result.of( + () -> { + PivSession session = result.getValue(); + Certificate certificate = session.getCertificate(slot); + char[] pin = null; + if (protParam instanceof KeyStore.PasswordProtection) { + pin = ((KeyStore.PasswordProtection) protParam).getPassword(); + } + PrivateKey key; + if (session.supports(PivSession.FEATURE_METADATA)) { + SlotMetadata data = session.getSlotMetadata(slot); + key = + PivPrivateKey.from( + data.getPublicKeyValues().toPublicKey(), + slot, + data.getPinPolicy(), + data.getTouchPolicy(), + pin); + } else { + PublicKey publicKey = certificate.getPublicKey(); + key = PivPrivateKey.from(publicKey, slot, null, null, pin); + } + return new KeyStore.PrivateKeyEntry(key, new Certificate[] {certificate}); + }))); + return queue.take().getValue(); + } catch (BadResponseException e) { + throw new UnrecoverableEntryException("Make sure the matching certificate is stored"); + } catch (ApduException e) { + if (e.getSw() == SW.FILE_NOT_FOUND) { + // Empty slot + return null; + } + throw new RuntimeException(e); + } catch (Exception e) { + throw new RuntimeException(e); } - - @Override - public void engineSetKeyEntry(String alias, Key key, - @Nullable char[] password, Certificate[] chain) throws KeyStoreException { - Slot slot = Slot.fromStringAlias(alias); - - if (password != null) { - throw new KeyStoreException("Password can not be set"); - } - - if (chain.length != 1) { - throw new KeyStoreException("Certificate chain must be a single certificate, or empty"); - } - if (chain[0] instanceof X509Certificate) { - try { - putEntry(slot, (PrivateKey) key, PinPolicy.DEFAULT, TouchPolicy.DEFAULT, (X509Certificate) chain[0]); - } catch (Exception e) { - throw new KeyStoreException(e); - } - } else { - throw new KeyStoreException("Certificate must be X509Certificate"); - } + } + + @Override + @Nullable public Date engineGetCreationDate(String alias) { + return null; + } + + @Override + public void engineSetEntry( + String alias, KeyStore.Entry entry, @Nullable KeyStore.ProtectionParameter protParam) + throws KeyStoreException { + Slot slot = Slot.fromStringAlias(alias); + + PrivateKey privateKey = null; + Certificate certificate; + if (entry instanceof KeyStore.TrustedCertificateEntry) { + if (protParam != null) { + throw new KeyStoreException("Certificate cannot use protParam"); + } + certificate = ((KeyStore.TrustedCertificateEntry) entry).getTrustedCertificate(); + } else if (entry instanceof KeyStore.PrivateKeyEntry) { + certificate = ((KeyStore.PrivateKeyEntry) entry).getCertificate(); + privateKey = ((KeyStore.PrivateKeyEntry) entry).getPrivateKey(); + } else { + throw new KeyStoreException("Unsupported KeyStore entry."); } - @Override - public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) throws - KeyStoreException { - throw new KeyStoreException("Use setKeyEntry with a PrivateKey instance instead of byte[]"); + if (certificate != null) { + if (!(certificate instanceof X509Certificate)) { + throw new KeyStoreException("Certificate must be X509Certificate"); + } } - @Override - public void engineSetCertificateEntry(String alias, Certificate cert) throws - KeyStoreException { - Slot slot = Slot.fromStringAlias(alias); - if (cert instanceof X509Certificate) { - try { - putEntry(slot, null, PinPolicy.DEFAULT, TouchPolicy.DEFAULT, (X509Certificate) cert); - } catch (Exception e) { - throw new KeyStoreException(e); - } + PinPolicy pinPolicy = PinPolicy.DEFAULT; + TouchPolicy touchPolicy = TouchPolicy.DEFAULT; + if (privateKey != null) { + if (protParam != null) { + if (protParam instanceof PivKeyStoreKeyParameters) { + pinPolicy = ((PivKeyStoreKeyParameters) protParam).pinPolicy; + touchPolicy = ((PivKeyStoreKeyParameters) protParam).touchPolicy; } else { - throw new KeyStoreException("Certificate must be X509Certificate"); + throw new KeyStoreException("protParam must be an instance of PivKeyStoreKeyParameters"); } + } } - @Override - public void engineDeleteEntry(String alias) throws KeyStoreException { - Slot slot = Slot.fromStringAlias(alias); - - BlockingQueue> queue = new ArrayBlockingQueue<>(1); - provider.invoke(result -> queue.add(Result.of(() -> { - result.getValue().deleteCertificate(slot); - return true; - }))); - - try { - queue.take().getValue(); - } catch (Exception e) { - throw new KeyStoreException(e); - } + try { + putEntry(slot, privateKey, pinPolicy, touchPolicy, (X509Certificate) certificate); + } catch (Exception e) { + throw new KeyStoreException(e); } + } - @Override - public Enumeration engineAliases() { - throw new UnsupportedOperationException(); - } + @Override + public void engineSetKeyEntry( + String alias, Key key, @Nullable char[] password, Certificate[] chain) + throws KeyStoreException { + Slot slot = Slot.fromStringAlias(alias); - @Override - public boolean engineContainsAlias(String alias) { - try { - Slot.fromStringAlias(alias); - return true; - } catch (IllegalArgumentException e) { - return false; - } + if (password != null) { + throw new KeyStoreException("Password can not be set"); } - @Override - public int engineSize() { - return Slot.values().length; + if (chain.length != 1) { + throw new KeyStoreException("Certificate chain must be a single certificate, or empty"); } - - @Override - public boolean engineIsKeyEntry(String alias) { - return engineContainsAlias(alias); + if (chain[0] instanceof X509Certificate) { + try { + putEntry( + slot, + (PrivateKey) key, + PinPolicy.DEFAULT, + TouchPolicy.DEFAULT, + (X509Certificate) chain[0]); + } catch (Exception e) { + throw new KeyStoreException(e); + } + } else { + throw new KeyStoreException("Certificate must be X509Certificate"); } - - @Override - public boolean engineIsCertificateEntry(String alias) { - return engineGetCertificate(alias) != null; + } + + @Override + public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) + throws KeyStoreException { + throw new KeyStoreException("Use setKeyEntry with a PrivateKey instance instead of byte[]"); + } + + @Override + public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException { + Slot slot = Slot.fromStringAlias(alias); + if (cert instanceof X509Certificate) { + try { + putEntry(slot, null, PinPolicy.DEFAULT, TouchPolicy.DEFAULT, (X509Certificate) cert); + } catch (Exception e) { + throw new KeyStoreException(e); + } + } else { + throw new KeyStoreException("Certificate must be X509Certificate"); } - - @Override - @Nullable - public String engineGetCertificateAlias(Certificate cert) { - for (Slot slot : Slot.values()) { - String alias = slot.getStringAlias(); - if (cert.equals(engineGetCertificate(alias))) { - return alias; - } - } - return null; + } + + @Override + public void engineDeleteEntry(String alias) throws KeyStoreException { + Slot slot = Slot.fromStringAlias(alias); + + BlockingQueue> queue = new ArrayBlockingQueue<>(1); + provider.invoke( + result -> + queue.add( + Result.of( + () -> { + result.getValue().deleteCertificate(slot); + return true; + }))); + + try { + queue.take().getValue(); + } catch (Exception e) { + throw new KeyStoreException(e); } - - @Override - public void engineStore(OutputStream stream, char[] password) { - throw new UnsupportedOperationException(); + } + + @Override + public Enumeration engineAliases() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean engineContainsAlias(String alias) { + try { + Slot.fromStringAlias(alias); + return true; + } catch (IllegalArgumentException e) { + return false; } - - @Override - public void engineLoad(InputStream stream, char[] password) { - throw new InvalidParameterException("KeyStore must be loaded with a null LoadStoreParameter"); + } + + @Override + public int engineSize() { + return Slot.values().length; + } + + @Override + public boolean engineIsKeyEntry(String alias) { + return engineContainsAlias(alias); + } + + @Override + public boolean engineIsCertificateEntry(String alias) { + return engineGetCertificate(alias) != null; + } + + @Override + @Nullable public String engineGetCertificateAlias(Certificate cert) { + for (Slot slot : Slot.values()) { + String alias = slot.getStringAlias(); + if (cert.equals(engineGetCertificate(alias))) { + return alias; + } } - - @Override - public void engineLoad(@Nullable KeyStore.LoadStoreParameter param) { - if (param != null) { - throw new InvalidParameterException("KeyStore must be loaded with null"); - } + return null; + } + + @Override + public void engineStore(OutputStream stream, char[] password) { + throw new UnsupportedOperationException(); + } + + @Override + public void engineLoad(InputStream stream, char[] password) { + throw new InvalidParameterException("KeyStore must be loaded with a null LoadStoreParameter"); + } + + @Override + public void engineLoad(@Nullable KeyStore.LoadStoreParameter param) { + if (param != null) { + throw new InvalidParameterException("KeyStore must be loaded with null"); } + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivPrivateKey.java b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivPrivateKey.java index d569cece..f37bbb07 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivPrivateKey.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivPrivateKey.java @@ -24,7 +24,6 @@ import com.yubico.yubikit.piv.PivSession; import com.yubico.yubikit.piv.Slot; import com.yubico.yubikit.piv.TouchPolicy; - import java.math.BigInteger; import java.security.PrivateKey; import java.security.PublicKey; @@ -36,195 +35,225 @@ import java.util.Arrays; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; - import javax.annotation.Nullable; import javax.security.auth.Destroyable; public abstract class PivPrivateKey implements PrivateKey, Destroyable { - final Slot slot; - final KeyType keyType; - @Nullable - private final PinPolicy pinPolicy; - @Nullable - private final TouchPolicy touchPolicy; - @Nullable - protected char[] pin; - private boolean destroyed = false; - - static PivPrivateKey from(PublicKey publicKey, Slot slot, @Nullable PinPolicy pinPolicy, @Nullable TouchPolicy touchPolicy, @Nullable char[] pin) { - KeyType keyType = KeyType.fromKey(publicKey); - if (keyType.params.algorithm == KeyType.Algorithm.RSA) { - return new PivPrivateKey.RsaKey(slot, keyType, pinPolicy, touchPolicy, ((RSAPublicKey) publicKey).getModulus(), pin); - } else if (keyType == KeyType.ED25519) { - return new PivPrivateKey.Ed25519Key(slot, keyType, pinPolicy, touchPolicy, pin); - } else if (keyType == KeyType.X25519) { - return new PivPrivateKey.X25519Key(slot, keyType, pinPolicy, touchPolicy, pin); - } else { - return new PivPrivateKey.EcKey(slot, keyType, pinPolicy, touchPolicy, ((ECPublicKey) publicKey).getParams(), pin); - } - } + final Slot slot; + final KeyType keyType; + @Nullable private final PinPolicy pinPolicy; + @Nullable private final TouchPolicy touchPolicy; + @Nullable protected char[] pin; + private boolean destroyed = false; - protected PivPrivateKey(Slot slot, KeyType keyType, @Nullable PinPolicy pinPolicy, @Nullable TouchPolicy touchPolicy, @Nullable char[] pin) { - this.slot = slot; - this.keyType = keyType; - this.pinPolicy = pinPolicy; - this.touchPolicy = touchPolicy; - this.pin = pin != null ? Arrays.copyOf(pin, pin.length) : null; + static PivPrivateKey from( + PublicKey publicKey, + Slot slot, + @Nullable PinPolicy pinPolicy, + @Nullable TouchPolicy touchPolicy, + @Nullable char[] pin) { + KeyType keyType = KeyType.fromKey(publicKey); + if (keyType.params.algorithm == KeyType.Algorithm.RSA) { + return new PivPrivateKey.RsaKey( + slot, keyType, pinPolicy, touchPolicy, ((RSAPublicKey) publicKey).getModulus(), pin); + } else if (keyType == KeyType.ED25519) { + return new PivPrivateKey.Ed25519Key(slot, keyType, pinPolicy, touchPolicy, pin); + } else if (keyType == KeyType.X25519) { + return new PivPrivateKey.X25519Key(slot, keyType, pinPolicy, touchPolicy, pin); + } else { + return new PivPrivateKey.EcKey( + slot, keyType, pinPolicy, touchPolicy, ((ECPublicKey) publicKey).getParams(), pin); } + } - byte[] rawSignOrDecrypt(Callback>> provider, byte[] payload) throws Exception { - if (destroyed) { - throw new IllegalStateException("PivPrivateKey has been destroyed"); - } - BlockingQueue> queue = new ArrayBlockingQueue<>(1); - provider.invoke(result -> queue.add(Result.of(() -> { - PivSession session = result.getValue(); - if (pin != null) { - session.verifyPin(pin); - } - return session.rawSignOrDecrypt(slot, keyType, payload); - }))); - return queue.take().getValue(); - } + protected PivPrivateKey( + Slot slot, + KeyType keyType, + @Nullable PinPolicy pinPolicy, + @Nullable TouchPolicy touchPolicy, + @Nullable char[] pin) { + this.slot = slot; + this.keyType = keyType; + this.pinPolicy = pinPolicy; + this.touchPolicy = touchPolicy; + this.pin = pin != null ? Arrays.copyOf(pin, pin.length) : null; + } - /** - * Get the PIV slot where the private key is stored. - */ - public Slot getSlot() { - return slot; + byte[] rawSignOrDecrypt( + Callback>> provider, byte[] payload) throws Exception { + if (destroyed) { + throw new IllegalStateException("PivPrivateKey has been destroyed"); } + BlockingQueue> queue = new ArrayBlockingQueue<>(1); + provider.invoke( + result -> + queue.add( + Result.of( + () -> { + PivSession session = result.getValue(); + if (pin != null) { + session.verifyPin(pin); + } + return session.rawSignOrDecrypt(slot, keyType, payload); + }))); + return queue.take().getValue(); + } - /** - * Get the PIN policy of the key, if available. - */ - @Nullable - public PinPolicy getPinPolicy() { - return pinPolicy; - } + /** Get the PIV slot where the private key is stored. */ + public Slot getSlot() { + return slot; + } - /** - * Get the Touch policy of the key, if available. - */ - @Nullable - public TouchPolicy getTouchPolicy() { - return touchPolicy; - } + /** Get the PIN policy of the key, if available. */ + @Nullable public PinPolicy getPinPolicy() { + return pinPolicy; + } - /** - * Sets the PIN to use when performing key operations with this private key, or to null. - * Note that a copy is made of the PIN, which can be cleared out by calling {@link #destroy()}. - */ - public void setPin(@Nullable char[] pin) { - if (destroyed) { - throw new IllegalStateException("PivPrivateKey has been destroyed"); - } - // Zero out the old PIN, if one was set - if (this.pin != null) { - Arrays.fill(this.pin, (char) 0); - } - this.pin = pin != null ? Arrays.copyOf(pin, pin.length) : null; - } + /** Get the Touch policy of the key, if available. */ + @Nullable public TouchPolicy getTouchPolicy() { + return touchPolicy; + } - @Override - public void destroy() { - if (pin != null) { - Arrays.fill(pin, (char) 0); - } - destroyed = true; + /** + * Sets the PIN to use when performing key operations with this private key, or to null. Note that + * a copy is made of the PIN, which can be cleared out by calling {@link #destroy()}. + */ + public void setPin(@Nullable char[] pin) { + if (destroyed) { + throw new IllegalStateException("PivPrivateKey has been destroyed"); + } + // Zero out the old PIN, if one was set + if (this.pin != null) { + Arrays.fill(this.pin, (char) 0); } + this.pin = pin != null ? Arrays.copyOf(pin, pin.length) : null; + } - @Override - public boolean isDestroyed() { - return destroyed; + @Override + public void destroy() { + if (pin != null) { + Arrays.fill(pin, (char) 0); } + destroyed = true; + } - @Override - public String getAlgorithm() { - return keyType.params.algorithm.name(); + @Override + public boolean isDestroyed() { + return destroyed; + } + + @Override + public String getAlgorithm() { + return keyType.params.algorithm.name(); + } + + @Override + @Nullable public String getFormat() { + return null; + } + + @Override + @Nullable public byte[] getEncoded() { + return null; + } + + static class EcKey extends PivPrivateKey implements ECKey { + private final ECParameterSpec ecSpec; + + private EcKey( + Slot slot, + KeyType keyType, + @Nullable PinPolicy pinPolicy, + @Nullable TouchPolicy touchPolicy, + ECParameterSpec ecSpec, + @Nullable char[] pin) { + super(slot, keyType, pinPolicy, touchPolicy, pin); + this.ecSpec = ecSpec; } - @Override - @Nullable - public String getFormat() { - return null; + byte[] keyAgreement( + Callback>> provider, + PublicKeyValues peerPublicKeyValues) + throws Exception { + BlockingQueue> queue = new ArrayBlockingQueue<>(1); + provider.invoke( + result -> + queue.add( + Result.of( + () -> { + PivSession session = result.getValue(); + if (pin != null) { + session.verifyPin(pin); + } + return session.calculateSecret(slot, peerPublicKeyValues); + }))); + return queue.take().getValue(); } @Override - @Nullable - public byte[] getEncoded() { - return null; + public ECParameterSpec getParams() { + return ecSpec; } + } - static class EcKey extends PivPrivateKey implements ECKey { - private final ECParameterSpec ecSpec; - - private EcKey(Slot slot, KeyType keyType, @Nullable PinPolicy pinPolicy, @Nullable TouchPolicy touchPolicy, ECParameterSpec ecSpec, @Nullable char[] pin) { - super(slot, keyType, pinPolicy, touchPolicy, pin); - this.ecSpec = ecSpec; - } - - byte[] keyAgreement( - Callback>> provider, - PublicKeyValues peerPublicKeyValues) throws Exception { - BlockingQueue> queue = new ArrayBlockingQueue<>(1); - provider.invoke(result -> queue.add(Result.of(() -> { - PivSession session = result.getValue(); - if (pin != null) { - session.verifyPin(pin); - } - return session.calculateSecret(slot, peerPublicKeyValues); - }))); - return queue.take().getValue(); - } - - @Override - public ECParameterSpec getParams() { - return ecSpec; - } - } + static class RsaKey extends PivPrivateKey implements RSAKey { + private final BigInteger modulus; - static class RsaKey extends PivPrivateKey implements RSAKey { - private final BigInteger modulus; + private RsaKey( + Slot slot, + KeyType keyType, + @Nullable PinPolicy pinPolicy, + @Nullable TouchPolicy touchPolicy, + BigInteger modulus, + @Nullable char[] pin) { + super(slot, keyType, pinPolicy, touchPolicy, pin); + this.modulus = modulus; + } - private RsaKey(Slot slot, KeyType keyType, @Nullable PinPolicy pinPolicy, @Nullable TouchPolicy touchPolicy, BigInteger modulus, @Nullable char[] pin) { - super(slot, keyType, pinPolicy, touchPolicy, pin); - this.modulus = modulus; - } + @Override + public BigInteger getModulus() { + return modulus; + } + } - @Override - public BigInteger getModulus() { - return modulus; - } + static class Ed25519Key extends PivPrivateKey implements PrivateKey { + private Ed25519Key( + Slot slot, + KeyType keyType, + @Nullable PinPolicy pinPolicy, + @Nullable TouchPolicy touchPolicy, + @Nullable char[] pin) { + super(slot, keyType, pinPolicy, touchPolicy, pin); } + } - static class Ed25519Key extends PivPrivateKey implements PrivateKey { - private Ed25519Key(Slot slot, KeyType keyType, @Nullable PinPolicy pinPolicy, @Nullable TouchPolicy touchPolicy, @Nullable char[] pin) { - super(slot, keyType, pinPolicy, touchPolicy, pin); - } + static class X25519Key extends PivPrivateKey implements PrivateKey { + private X25519Key( + Slot slot, + KeyType keyType, + @Nullable PinPolicy pinPolicy, + @Nullable TouchPolicy touchPolicy, + @Nullable char[] pin) { + super(slot, keyType, pinPolicy, touchPolicy, pin); } - static class X25519Key extends PivPrivateKey implements PrivateKey { - private X25519Key( - Slot slot, - KeyType keyType, - @Nullable PinPolicy pinPolicy, - @Nullable TouchPolicy touchPolicy, - @Nullable char[] pin) { - super(slot, keyType, pinPolicy, touchPolicy, pin); - } - - byte[] keyAgreement( - Callback>> provider, - PublicKeyValues peerPublicKeyValues) throws Exception { - BlockingQueue> queue = new ArrayBlockingQueue<>(1); - provider.invoke(result -> queue.add(Result.of(() -> { - PivSession session = result.getValue(); - if (pin != null) { - session.verifyPin(pin); - } - return session.calculateSecret(slot, peerPublicKeyValues); - }))); - return queue.take().getValue(); - } + byte[] keyAgreement( + Callback>> provider, + PublicKeyValues peerPublicKeyValues) + throws Exception { + BlockingQueue> queue = new ArrayBlockingQueue<>(1); + provider.invoke( + result -> + queue.add( + Result.of( + () -> { + PivSession session = result.getValue(); + if (pin != null) { + session.verifyPin(pin); + } + return session.calculateSecret(slot, peerPublicKeyValues); + }))); + return queue.take().getValue(); } + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivProvider.java b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivProvider.java index 75b7bcec..601f00f8 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivProvider.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivProvider.java @@ -21,9 +21,6 @@ import com.yubico.yubikit.core.util.Result; import com.yubico.yubikit.piv.KeyType; import com.yubico.yubikit.piv.PivSession; - -import org.slf4j.LoggerFactory; - import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; @@ -34,180 +31,237 @@ import java.util.List; import java.util.Map; import java.util.Set; - import javax.annotation.Nullable; import javax.crypto.NoSuchPaddingException; +import org.slf4j.LoggerFactory; public class PivProvider extends Provider { - private static final Map ecAttributes = Collections.singletonMap("SupportedKeyClasses", PivPrivateKey.EcKey.class.getName()); - private static final Map rsaAttributes = Collections.singletonMap("SupportedKeyClasses", PivPrivateKey.RsaKey.class.getName()); - private static final Map ed25519Attributes = Collections.singletonMap("SupportedKeyClasses", PivPrivateKey.Ed25519Key.class.getName()); - private static final Map x25519Attributes = Collections.singletonMap("SupportedKeyClasses", PivPrivateKey.X25519Key.class.getName()); - - private final Callback>> sessionRequester; - private final Map rsaDummyKeys = new HashMap<>(); - - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(PivProvider.class); - - /** - * Creates a Security Provider wrapping an instance of a PivSession. - *

- * The PivSession must be active for as long as the Provider will be used. - * - * @param session A PivSession to use for YubiKey interaction. - */ - public PivProvider(PivSession session) { - this(callback -> callback.invoke(Result.success(session))); - } + private static final Map ecAttributes = + Collections.singletonMap("SupportedKeyClasses", PivPrivateKey.EcKey.class.getName()); + private static final Map rsaAttributes = + Collections.singletonMap("SupportedKeyClasses", PivPrivateKey.RsaKey.class.getName()); + private static final Map ed25519Attributes = + Collections.singletonMap("SupportedKeyClasses", PivPrivateKey.Ed25519Key.class.getName()); + private static final Map x25519Attributes = + Collections.singletonMap("SupportedKeyClasses", PivPrivateKey.X25519Key.class.getName()); + + private final Callback>> sessionRequester; + private final Map rsaDummyKeys = new HashMap<>(); + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(PivProvider.class); - /** - * Creates a Security Provider capable of using a PivSession with a YubiKey to perform key operations. - * - * @param sessionRequester a mechanism for the Provider to get an instance of a PivSession. - */ - public PivProvider(Callback>> sessionRequester) { - //noinspection deprecation - super("YKPiv", 1.0, "JCA Provider for YubiKey PIV"); - this.sessionRequester = sessionRequester; - - Logger.debug(logger, "EC attributes: {}", ecAttributes); - Logger.debug(logger, "RSA attributes: {}", rsaAttributes); - - //noinspection SpellCheckingInspection - putService(new Service(this, "Signature", "NONEwithECDSA", PivEcSignatureSpi.Prehashed.class.getName(), null, ecAttributes) { - @Override - public Object newInstance(Object constructorParameter) { - return new PivEcSignatureSpi.Prehashed(sessionRequester); - } + /** + * Creates a Security Provider wrapping an instance of a PivSession. + * + *

The PivSession must be active for as long as the Provider will be used. + * + * @param session A PivSession to use for YubiKey interaction. + */ + public PivProvider(PivSession session) { + this(callback -> callback.invoke(Result.success(session))); + } + + /** + * Creates a Security Provider capable of using a PivSession with a YubiKey to perform key + * operations. + * + * @param sessionRequester a mechanism for the Provider to get an instance of a PivSession. + */ + public PivProvider(Callback>> sessionRequester) { + //noinspection deprecation + super("YKPiv", 1.0, "JCA Provider for YubiKey PIV"); + this.sessionRequester = sessionRequester; + + Logger.debug(logger, "EC attributes: {}", ecAttributes); + Logger.debug(logger, "RSA attributes: {}", rsaAttributes); + + //noinspection SpellCheckingInspection + putService( + new Service( + this, + "Signature", + "NONEwithECDSA", + PivEcSignatureSpi.Prehashed.class.getName(), + null, + ecAttributes) { + @Override + public Object newInstance(Object constructorParameter) { + return new PivEcSignatureSpi.Prehashed(sessionRequester); + } }); - putService(new Service(this, "Signature", "Ed25519", PivEcSignatureSpi.Prehashed.class.getName(), null, ed25519Attributes) { - @Override - public Object newInstance(Object constructorParameter) { - return new PivEcSignatureSpi.Prehashed(sessionRequester); - } + putService( + new Service( + this, + "Signature", + "Ed25519", + PivEcSignatureSpi.Prehashed.class.getName(), + null, + ed25519Attributes) { + @Override + public Object newInstance(Object constructorParameter) { + return new PivEcSignatureSpi.Prehashed(sessionRequester); + } }); - putService(new Service(this, "KeyAgreement", "X25519", PivKeyAgreementSpi.class.getName(), null, x25519Attributes) { - @Override - public Object newInstance(Object constructorParameter) { - return new PivKeyAgreementSpi(sessionRequester); - } + putService( + new Service( + this, + "KeyAgreement", + "X25519", + PivKeyAgreementSpi.class.getName(), + null, + x25519Attributes) { + @Override + public Object newInstance(Object constructorParameter) { + return new PivKeyAgreementSpi(sessionRequester); + } }); - try { - KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); - long start = System.currentTimeMillis(); - for (KeyType keyType : new KeyType[]{KeyType.RSA1024, KeyType.RSA2048, KeyType.RSA3072, KeyType.RSA4096}) { - //TODO: import static keys to avoid slow generation? - rsaGen.initialize(keyType.params.bitLength); - rsaDummyKeys.put(keyType, rsaGen.generateKeyPair()); - } - long end = System.currentTimeMillis(); - Logger.debug(logger, "Time taken to generate dummy RSA keys: {}ms", (end - start)); - - putService(new PivRsaCipherService()); - } catch (NoSuchAlgorithmException e) { - Logger.error(logger, "Unable to support RSA, no underlying Provider with RSA capability", e); - } + try { + KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA"); + long start = System.currentTimeMillis(); + for (KeyType keyType : + new KeyType[] {KeyType.RSA1024, KeyType.RSA2048, KeyType.RSA3072, KeyType.RSA4096}) { + // TODO: import static keys to avoid slow generation? + rsaGen.initialize(keyType.params.bitLength); + rsaDummyKeys.put(keyType, rsaGen.generateKeyPair()); + } + long end = System.currentTimeMillis(); + Logger.debug(logger, "Time taken to generate dummy RSA keys: {}ms", (end - start)); - Set digests = Security.getAlgorithms("MessageDigest"); - for (String signatureOrig : Security.getAlgorithms("Signature")) { - String signature = signatureOrig.toUpperCase(); - if (signature.endsWith("WITHECDSA")) { - String digest = signature.substring(0, signature.length() - 9); - if (!digests.contains(digest)) { - // SHA names don't quite match between Signature and MessageDigest. - digest = digest.replace("SHA", "SHA-"); - - } - if (digests.contains(digest)) { - putService(new PivEcSignatureService(signature, digest, null)); - } - } else if (!rsaDummyKeys.isEmpty() && signature.endsWith("WITHRSA")) { - putService(new PivRsaSignatureService(signature)); - } else if (!rsaDummyKeys.isEmpty() && signature.endsWith("PSS")) { - putService(new PivRsaSignatureService(signature)); - } else if (signature.equals("ECDSA")) { - putService(new PivEcSignatureService("ECDSA", "SHA-1", Collections.singletonList("SHA1withECDSA"))); - } + putService(new PivRsaCipherService()); + } catch (NoSuchAlgorithmException e) { + Logger.error(logger, "Unable to support RSA, no underlying Provider with RSA capability", e); + } + + Set digests = Security.getAlgorithms("MessageDigest"); + for (String signatureOrig : Security.getAlgorithms("Signature")) { + String signature = signatureOrig.toUpperCase(); + if (signature.endsWith("WITHECDSA")) { + String digest = signature.substring(0, signature.length() - 9); + if (!digests.contains(digest)) { + // SHA names don't quite match between Signature and MessageDigest. + digest = digest.replace("SHA", "SHA-"); } + if (digests.contains(digest)) { + putService(new PivEcSignatureService(signature, digest, null)); + } + } else if (!rsaDummyKeys.isEmpty() && signature.endsWith("WITHRSA")) { + putService(new PivRsaSignatureService(signature)); + } else if (!rsaDummyKeys.isEmpty() && signature.endsWith("PSS")) { + putService(new PivRsaSignatureService(signature)); + } else if (signature.equals("ECDSA")) { + putService( + new PivEcSignatureService( + "ECDSA", "SHA-1", Collections.singletonList("SHA1withECDSA"))); + } + } - putService(new Service(this, "KeyPairGenerator", "YKPivRSA", PivKeyPairGeneratorSpi.Rsa.class.getName(), null, null) { - @Override - public Object newInstance(Object constructorParameter) { - return new PivKeyPairGeneratorSpi.Rsa(sessionRequester); - } + putService( + new Service( + this, + "KeyPairGenerator", + "YKPivRSA", + PivKeyPairGeneratorSpi.Rsa.class.getName(), + null, + null) { + @Override + public Object newInstance(Object constructorParameter) { + return new PivKeyPairGeneratorSpi.Rsa(sessionRequester); + } }); - putService(new Service(this, "KeyPairGenerator", "YKPivEC", PivKeyPairGeneratorSpi.Ec.class.getName(), null, null) { - @Override - public Object newInstance(Object constructorParameter) { - return new PivKeyPairGeneratorSpi.Ec(sessionRequester); - } + putService( + new Service( + this, + "KeyPairGenerator", + "YKPivEC", + PivKeyPairGeneratorSpi.Ec.class.getName(), + null, + null) { + @Override + public Object newInstance(Object constructorParameter) { + return new PivKeyPairGeneratorSpi.Ec(sessionRequester); + } }); - putService(new Service(this, "KeyStore", "YKPiv", PivKeyStoreSpi.class.getName(), null, null) { - @Override - public Object newInstance(Object constructorParameter) { - return new PivKeyStoreSpi(sessionRequester); - } + putService( + new Service(this, "KeyStore", "YKPiv", PivKeyStoreSpi.class.getName(), null, null) { + @Override + public Object newInstance(Object constructorParameter) { + return new PivKeyStoreSpi(sessionRequester); + } }); - - putService(new Service(this, "KeyAgreement", "ECDH", PivKeyAgreementSpi.class.getName(), null, ecAttributes) { - @Override - public Object newInstance(Object constructorParameter) { - return new PivKeyAgreementSpi(sessionRequester); - } + putService( + new Service( + this, "KeyAgreement", "ECDH", PivKeyAgreementSpi.class.getName(), null, ecAttributes) { + @Override + public Object newInstance(Object constructorParameter) { + return new PivKeyAgreementSpi(sessionRequester); + } }); - } + } - @Override - public synchronized boolean equals(Object o) { - return o instanceof PivProvider && super.equals(o); - } + @Override + public synchronized boolean equals(Object o) { + return o instanceof PivProvider && super.equals(o); + } - private class PivEcSignatureService extends Service { - private final String digest; + private class PivEcSignatureService extends Service { + private final String digest; - public PivEcSignatureService(String algorithm, String digest, @Nullable List aliases) { - super(PivProvider.this, "Signature", algorithm, PivEcSignatureSpi.Hashed.class.getName(), aliases, ecAttributes); - this.digest = digest; - } + public PivEcSignatureService(String algorithm, String digest, @Nullable List aliases) { + super( + PivProvider.this, + "Signature", + algorithm, + PivEcSignatureSpi.Hashed.class.getName(), + aliases, + ecAttributes); + this.digest = digest; + } - @Override - public Object newInstance(Object constructorParameter) throws NoSuchAlgorithmException { - return new PivEcSignatureSpi.Hashed(sessionRequester, digest); - } + @Override + public Object newInstance(Object constructorParameter) throws NoSuchAlgorithmException { + return new PivEcSignatureSpi.Hashed(sessionRequester, digest); } + } - private class PivRsaSignatureService extends Service { - public PivRsaSignatureService(String algorithm) { - super(PivProvider.this, "Signature", algorithm, PivRsaSignatureSpi.class.getName(), null, rsaAttributes); - } + private class PivRsaSignatureService extends Service { + public PivRsaSignatureService(String algorithm) { + super( + PivProvider.this, + "Signature", + algorithm, + PivRsaSignatureSpi.class.getName(), + null, + rsaAttributes); + } - @Override - public Object newInstance(Object constructorParameter) throws NoSuchAlgorithmException { - try { - return new PivRsaSignatureSpi(sessionRequester, rsaDummyKeys, getAlgorithm()); - } catch (NoSuchPaddingException e) { - throw new NoSuchAlgorithmException("No underlying Provider supporting " + getAlgorithm() + " available."); - } - } + @Override + public Object newInstance(Object constructorParameter) throws NoSuchAlgorithmException { + try { + return new PivRsaSignatureSpi(sessionRequester, rsaDummyKeys, getAlgorithm()); + } catch (NoSuchPaddingException e) { + throw new NoSuchAlgorithmException( + "No underlying Provider supporting " + getAlgorithm() + " available."); + } } + } - private class PivRsaCipherService extends Service { - public PivRsaCipherService() { - super(PivProvider.this, "Cipher", "RSA", PivCipherSpi.class.getName(), null, rsaAttributes); - } + private class PivRsaCipherService extends Service { + public PivRsaCipherService() { + super(PivProvider.this, "Cipher", "RSA", PivCipherSpi.class.getName(), null, rsaAttributes); + } - @Override - public Object newInstance(Object constructorParameter) throws NoSuchAlgorithmException { - try { - return new PivCipherSpi(sessionRequester, rsaDummyKeys); - } catch (NoSuchPaddingException e) { - throw new NoSuchAlgorithmException(e); - } - } + @Override + public Object newInstance(Object constructorParameter) throws NoSuchAlgorithmException { + try { + return new PivCipherSpi(sessionRequester, rsaDummyKeys); + } catch (NoSuchPaddingException e) { + throw new NoSuchAlgorithmException(e); + } } + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivRsaSignatureSpi.java b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivRsaSignatureSpi.java index ab90d734..d60d3674 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/jca/PivRsaSignatureSpi.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/jca/PivRsaSignatureSpi.java @@ -20,7 +20,6 @@ import com.yubico.yubikit.core.util.Result; import com.yubico.yubikit.piv.KeyType; import com.yubico.yubikit.piv.PivSession; - import java.security.AlgorithmParameters; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; @@ -34,139 +33,142 @@ import java.security.SignatureSpi; import java.security.spec.AlgorithmParameterSpec; import java.util.Map; - import javax.annotation.Nullable; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; public class PivRsaSignatureSpi extends SignatureSpi { - private final Callback>> provider; - private final Map dummyKeys; - private final String signature; - - @Nullable - private PivPrivateKey.RsaKey privateKey; - - @Nullable - private Signature delegate; - - PivRsaSignatureSpi(Callback>> provider, Map dummyKeys, String signature) throws NoSuchPaddingException { - this.provider = provider; - this.dummyKeys = dummyKeys; - this.signature = signature; - } - - private Signature getDelegate(boolean init) throws NoSuchAlgorithmException { - if (delegate == null) { - delegate = Signature.getInstance(signature); - // If parameters are set before initSign is called we need to initialize the delegate to choose Provider. - if (init) { - try { - // Key size may be wrong, but that will get fixes once initSign is called. - delegate.initSign(dummyKeys.get(KeyType.RSA2048).getPrivate()); - } catch (InvalidKeyException e) { - throw new NoSuchAlgorithmException(); - } - } + private final Callback>> provider; + private final Map dummyKeys; + private final String signature; + + @Nullable private PivPrivateKey.RsaKey privateKey; + + @Nullable private Signature delegate; + + PivRsaSignatureSpi( + Callback>> provider, + Map dummyKeys, + String signature) + throws NoSuchPaddingException { + this.provider = provider; + this.dummyKeys = dummyKeys; + this.signature = signature; + } + + private Signature getDelegate(boolean init) throws NoSuchAlgorithmException { + if (delegate == null) { + delegate = Signature.getInstance(signature); + // If parameters are set before initSign is called we need to initialize the delegate to + // choose Provider. + if (init) { + try { + // Key size may be wrong, but that will get fixes once initSign is called. + delegate.initSign(dummyKeys.get(KeyType.RSA2048).getPrivate()); + } catch (InvalidKeyException e) { + throw new NoSuchAlgorithmException(); } - return delegate; + } } - - @Override - protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException { - throw new InvalidKeyException("Can only be used for signing."); + return delegate; + } + + @Override + protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException { + throw new InvalidKeyException("Can only be used for signing."); + } + + @Override + protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException { + if (privateKey instanceof PivPrivateKey.RsaKey) { + this.privateKey = (PivPrivateKey.RsaKey) privateKey; + KeyPair dummyPair = dummyKeys.get(this.privateKey.keyType); + try { + getDelegate(false).initSign(dummyPair.getPrivate()); + } catch (NoSuchAlgorithmException e) { + throw new InvalidKeyException(e); + } + } else { + throw new InvalidKeyException("Unsupported key type"); } - - @Override - protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException { - if (privateKey instanceof PivPrivateKey.RsaKey) { - this.privateKey = (PivPrivateKey.RsaKey) privateKey; - KeyPair dummyPair = dummyKeys.get(this.privateKey.keyType); - try { - getDelegate(false).initSign(dummyPair.getPrivate()); - } catch (NoSuchAlgorithmException e) { - throw new InvalidKeyException(e); - } - } else { - throw new InvalidKeyException("Unsupported key type"); - } + } + + @Override + protected void engineUpdate(byte b) throws SignatureException { + if (delegate != null) { + delegate.update(b); + } else { + throw new SignatureException("Not initialized"); } - - @Override - protected void engineUpdate(byte b) throws SignatureException { - if (delegate != null) { - delegate.update(b); - } else { - throw new SignatureException("Not initialized"); - } - } - - @Override - protected void engineUpdate(byte[] b, int off, int len) throws SignatureException { - if (delegate != null) { - delegate.update(b, off, len); - } else { - throw new SignatureException("Not initialized"); - } + } + + @Override + protected void engineUpdate(byte[] b, int off, int len) throws SignatureException { + if (delegate != null) { + delegate.update(b, off, len); + } else { + throw new SignatureException("Not initialized"); } + } - @Override - protected byte[] engineSign() throws SignatureException { - if (privateKey == null || delegate == null) { - throw new SignatureException("Not initialized"); - } - try { - Cipher rawRsa = Cipher.getInstance("RSA/ECB/NoPadding"); - rawRsa.init(Cipher.ENCRYPT_MODE, dummyKeys.get(this.privateKey.keyType).getPublic()); - byte[] padded = rawRsa.doFinal(delegate.sign()); - return privateKey.rawSignOrDecrypt(provider, padded); - } catch (Exception e) { - throw new SignatureException(e); - } + @Override + protected byte[] engineSign() throws SignatureException { + if (privateKey == null || delegate == null) { + throw new SignatureException("Not initialized"); } - - @Override - protected boolean engineVerify(byte[] sigBytes) throws SignatureException { - throw new SignatureException("Not initialized"); + try { + Cipher rawRsa = Cipher.getInstance("RSA/ECB/NoPadding"); + rawRsa.init(Cipher.ENCRYPT_MODE, dummyKeys.get(this.privateKey.keyType).getPublic()); + byte[] padded = rawRsa.doFinal(delegate.sign()); + return privateKey.rawSignOrDecrypt(provider, padded); + } catch (Exception e) { + throw new SignatureException(e); } - - @SuppressWarnings("deprecation") - @Override - protected void engineSetParameter(String param, Object value) throws InvalidParameterException { - try { - //noinspection deprecation - getDelegate(true).setParameter(param, value); - } catch (NoSuchAlgorithmException e) { - throw new InvalidParameterException("Not initialized"); - } + } + + @Override + protected boolean engineVerify(byte[] sigBytes) throws SignatureException { + throw new SignatureException("Not initialized"); + } + + @SuppressWarnings("deprecation") + @Override + protected void engineSetParameter(String param, Object value) throws InvalidParameterException { + try { + //noinspection deprecation + getDelegate(true).setParameter(param, value); + } catch (NoSuchAlgorithmException e) { + throw new InvalidParameterException("Not initialized"); } - - @SuppressWarnings("deprecation") - @Override - protected Object engineGetParameter(String param) throws InvalidParameterException { - if (delegate != null) { - //noinspection deprecation - return delegate.getParameter(param); - } else { - throw new InvalidParameterException("Not initialized"); - } + } + + @SuppressWarnings("deprecation") + @Override + protected Object engineGetParameter(String param) throws InvalidParameterException { + if (delegate != null) { + //noinspection deprecation + return delegate.getParameter(param); + } else { + throw new InvalidParameterException("Not initialized"); } - - @Override - protected void engineSetParameter(AlgorithmParameterSpec params) throws InvalidAlgorithmParameterException { - try { - getDelegate(true).setParameter(params); - } catch (NoSuchAlgorithmException e) { - throw new InvalidParameterException("Not initialized"); - } + } + + @Override + protected void engineSetParameter(AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { + try { + getDelegate(true).setParameter(params); + } catch (NoSuchAlgorithmException e) { + throw new InvalidParameterException("Not initialized"); } - - @Override - protected AlgorithmParameters engineGetParameters() { - if (delegate != null) { - return delegate.getParameters(); - } else { - throw new InvalidParameterException("Not initialized"); - } + } + + @Override + protected AlgorithmParameters engineGetParameters() { + if (delegate != null) { + return delegate.getParameters(); + } else { + throw new InvalidParameterException("Not initialized"); } + } } diff --git a/piv/src/main/java/com/yubico/yubikit/piv/jca/package-info.java b/piv/src/main/java/com/yubico/yubikit/piv/jca/package-info.java index 77b33f7e..589dd8ce 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/jca/package-info.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/jca/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.piv.jca; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/piv/src/main/java/com/yubico/yubikit/piv/package-info.java b/piv/src/main/java/com/yubico/yubikit/piv/package-info.java index 7f4e2440..e148a588 100755 --- a/piv/src/main/java/com/yubico/yubikit/piv/package-info.java +++ b/piv/src/main/java/com/yubico/yubikit/piv/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.piv; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/piv/src/test/java/com/yubico/yubikit/piv/GzipUtilsTest.java b/piv/src/test/java/com/yubico/yubikit/piv/GzipUtilsTest.java index 4055062e..23e63b73 100644 --- a/piv/src/test/java/com/yubico/yubikit/piv/GzipUtilsTest.java +++ b/piv/src/test/java/com/yubico/yubikit/piv/GzipUtilsTest.java @@ -21,70 +21,66 @@ import com.yubico.yubikit.core.util.StringUtils; import com.yubico.yubikit.testing.Codec; - -import org.junit.Assert; -import org.junit.Test; -import org.slf4j.LoggerFactory; - import java.io.EOFException; import java.nio.charset.StandardCharsets; import java.util.zip.ZipException; +import org.junit.Assert; +import org.junit.Test; +import org.slf4j.LoggerFactory; public class GzipUtilsTest { - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(GzipUtilsTest.class); + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(GzipUtilsTest.class); - private final byte[] testData = Codec.fromHex("1f8b08000000000000008b2c4dcaf4ce" + - "2c5148cb2f5270cc4b29cacf4c512849" + - "2d2e5148492c49040003f7ef7d1d0000" + - "00" - ); + private final byte[] testData = + Codec.fromHex( + "1f8b08000000000000008b2c4dcaf4ce2c5148cb2f5270cc4b29cacf4c5128492d2e5148492c49040003f7e" + + "f7d1d000000"); - @Test - public void compressesEmptyData() throws Throwable { - compressAndDecompress(new byte[0]); - } + @Test + public void compressesEmptyData() throws Throwable { + compressAndDecompress(new byte[0]); + } - @Test - public void compressesShortData() throws Throwable { - compressAndDecompress("YubiKit" - .getBytes(StandardCharsets.ISO_8859_1)); - } + @Test + public void compressesShortData() throws Throwable { + compressAndDecompress("YubiKit".getBytes(StandardCharsets.ISO_8859_1)); + } - @Test - public void compressesBigData() throws Throwable { - byte[] data = new byte[128 * 1024]; // 128kB - for (int index = 0; index < 128 * 1024; index++) { - data[index] = (byte) ((index & 0xff) - (byte) (index >> 8) * (index & 0xef)); - } - compressAndDecompress(data); + @Test + public void compressesBigData() throws Throwable { + byte[] data = new byte[128 * 1024]; // 128kB + for (int index = 0; index < 128 * 1024; index++) { + data[index] = (byte) ((index & 0xff) - (byte) (index >> 8) * (index & 0xef)); } + compressAndDecompress(data); + } - @Test(expected = EOFException.class) - public void decompressEmptyData() throws Throwable { - byte[] d = decompress(new byte[0]); - Assert.assertEquals(0, d.length); - } + @Test(expected = EOFException.class) + public void decompressEmptyData() throws Throwable { + byte[] d = decompress(new byte[0]); + Assert.assertEquals(0, d.length); + } - @Test(expected = ZipException.class) - public void decompressInvalidData() throws Throwable { - decompress(new byte[]{1, 2, 3, 4}); - } + @Test(expected = ZipException.class) + public void decompressInvalidData() throws Throwable { + decompress(new byte[] {1, 2, 3, 4}); + } - @Test - public void decompressGzipedData() throws Throwable { - String s = new String(decompress(testData), StandardCharsets.ISO_8859_1); - Assert.assertEquals("YubiKit for Android test data", s); - } + @Test + public void decompressGzipedData() throws Throwable { + String s = new String(decompress(testData), StandardCharsets.ISO_8859_1); + Assert.assertEquals("YubiKit for Android test data", s); + } - private void compressAndDecompress(byte[] data) throws Throwable { - byte[] c = compress(data); - byte[] d = decompress(c); - if (data.length < 1024) { // don't log our 128kB test - logger.trace("Data to compress : {}", StringUtils.bytesToHex(data)); - logger.trace("compressed data : {}", StringUtils.bytesToHex(c)); - logger.trace("Decompressed data : {}", StringUtils.bytesToHex(d)); - } - Assert.assertArrayEquals(data, d); + private void compressAndDecompress(byte[] data) throws Throwable { + byte[] c = compress(data); + byte[] d = decompress(c); + if (data.length < 1024) { // don't log our 128kB test + logger.trace("Data to compress : {}", StringUtils.bytesToHex(data)); + logger.trace("compressed data : {}", StringUtils.bytesToHex(c)); + logger.trace("Decompressed data : {}", StringUtils.bytesToHex(d)); } + Assert.assertArrayEquals(data, d); + } } diff --git a/piv/src/test/java/com/yubico/yubikit/piv/KeyTypeTest.java b/piv/src/test/java/com/yubico/yubikit/piv/KeyTypeTest.java index 978154d5..df8be81d 100755 --- a/piv/src/test/java/com/yubico/yubikit/piv/KeyTypeTest.java +++ b/piv/src/test/java/com/yubico/yubikit/piv/KeyTypeTest.java @@ -15,82 +15,94 @@ */ package com.yubico.yubikit.piv; -import org.hamcrest.CoreMatchers; -import org.hamcrest.MatcherAssert; -import org.junit.BeforeClass; -import org.junit.Test; - import java.security.InvalidAlgorithmParameterException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.ECGenParameterSpec; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.BeforeClass; +import org.junit.Test; public class KeyTypeTest { - private static final SecureRandom secureRandom = new SecureRandom(); + private static final SecureRandom secureRandom = new SecureRandom(); - private static KeyPair secp256r1; - private static KeyPair secp384r1; - private static KeyPair secp521r1; - private static KeyPair rsa1024; - private static KeyPair rsa2048; - private static KeyPair rsa3072; - private static KeyPair rsa4096; + private static KeyPair secp256r1; + private static KeyPair secp384r1; + private static KeyPair secp521r1; + private static KeyPair rsa1024; + private static KeyPair rsa2048; + private static KeyPair rsa3072; + private static KeyPair rsa4096; - @BeforeClass - public static void setupKeys() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + @BeforeClass + public static void setupKeys() + throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { - KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyType.Algorithm.EC.name()); - kpg.initialize(new ECGenParameterSpec("secp256r1"), secureRandom); - secp256r1 = kpg.generateKeyPair(); - kpg.initialize(new ECGenParameterSpec("secp384r1"), secureRandom); - secp384r1 = kpg.generateKeyPair(); - kpg.initialize(new ECGenParameterSpec("secp521r1"), secureRandom); - secp521r1 = kpg.generateKeyPair(); + KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyType.Algorithm.EC.name()); + kpg.initialize(new ECGenParameterSpec("secp256r1"), secureRandom); + secp256r1 = kpg.generateKeyPair(); + kpg.initialize(new ECGenParameterSpec("secp384r1"), secureRandom); + secp384r1 = kpg.generateKeyPair(); + kpg.initialize(new ECGenParameterSpec("secp521r1"), secureRandom); + secp521r1 = kpg.generateKeyPair(); - kpg = KeyPairGenerator.getInstance(KeyType.Algorithm.RSA.name()); - kpg.initialize(1024); - rsa1024 = kpg.generateKeyPair(); - kpg.initialize(2048); - rsa2048 = kpg.generateKeyPair(); - kpg.initialize(3072); - rsa3072 = kpg.generateKeyPair(); - kpg.initialize(4096); - rsa4096 = kpg.generateKeyPair(); - } + kpg = KeyPairGenerator.getInstance(KeyType.Algorithm.RSA.name()); + kpg.initialize(1024); + rsa1024 = kpg.generateKeyPair(); + kpg.initialize(2048); + rsa2048 = kpg.generateKeyPair(); + kpg.initialize(3072); + rsa3072 = kpg.generateKeyPair(); + kpg.initialize(4096); + rsa4096 = kpg.generateKeyPair(); + } - @Test - public void testFromEcKey() { - MatcherAssert.assertThat(KeyType.fromKey(secp256r1.getPrivate()), CoreMatchers.is(KeyType.ECCP256)); - MatcherAssert.assertThat(KeyType.fromKey(secp256r1.getPublic()), CoreMatchers.is(KeyType.ECCP256)); + @Test + public void testFromEcKey() { + MatcherAssert.assertThat( + KeyType.fromKey(secp256r1.getPrivate()), CoreMatchers.is(KeyType.ECCP256)); + MatcherAssert.assertThat( + KeyType.fromKey(secp256r1.getPublic()), CoreMatchers.is(KeyType.ECCP256)); - MatcherAssert.assertThat(KeyType.fromKey(secp384r1.getPrivate()), CoreMatchers.is(KeyType.ECCP384)); - MatcherAssert.assertThat(KeyType.fromKey(secp384r1.getPublic()), CoreMatchers.is(KeyType.ECCP384)); - } + MatcherAssert.assertThat( + KeyType.fromKey(secp384r1.getPrivate()), CoreMatchers.is(KeyType.ECCP384)); + MatcherAssert.assertThat( + KeyType.fromKey(secp384r1.getPublic()), CoreMatchers.is(KeyType.ECCP384)); + } - @Test(expected = IllegalArgumentException.class) - public void testP521R1Public() { - KeyType.fromKey(secp521r1.getPublic()); - } + @Test(expected = IllegalArgumentException.class) + public void testP521R1Public() { + KeyType.fromKey(secp521r1.getPublic()); + } - @Test(expected = IllegalArgumentException.class) - public void testP521R1Private() { - KeyType.fromKey(secp521r1.getPrivate()); - } + @Test(expected = IllegalArgumentException.class) + public void testP521R1Private() { + KeyType.fromKey(secp521r1.getPrivate()); + } - @Test - public void testFromRsaKey() { - MatcherAssert.assertThat(KeyType.fromKey(rsa1024.getPrivate()), CoreMatchers.is(KeyType.RSA1024)); - MatcherAssert.assertThat(KeyType.fromKey(rsa1024.getPublic()), CoreMatchers.is(KeyType.RSA1024)); + @Test + public void testFromRsaKey() { + MatcherAssert.assertThat( + KeyType.fromKey(rsa1024.getPrivate()), CoreMatchers.is(KeyType.RSA1024)); + MatcherAssert.assertThat( + KeyType.fromKey(rsa1024.getPublic()), CoreMatchers.is(KeyType.RSA1024)); - MatcherAssert.assertThat(KeyType.fromKey(rsa2048.getPrivate()), CoreMatchers.is(KeyType.RSA2048)); - MatcherAssert.assertThat(KeyType.fromKey(rsa2048.getPublic()), CoreMatchers.is(KeyType.RSA2048)); + MatcherAssert.assertThat( + KeyType.fromKey(rsa2048.getPrivate()), CoreMatchers.is(KeyType.RSA2048)); + MatcherAssert.assertThat( + KeyType.fromKey(rsa2048.getPublic()), CoreMatchers.is(KeyType.RSA2048)); - MatcherAssert.assertThat(KeyType.fromKey(rsa3072.getPrivate()), CoreMatchers.is(KeyType.RSA3072)); - MatcherAssert.assertThat(KeyType.fromKey(rsa3072.getPublic()), CoreMatchers.is(KeyType.RSA3072)); + MatcherAssert.assertThat( + KeyType.fromKey(rsa3072.getPrivate()), CoreMatchers.is(KeyType.RSA3072)); + MatcherAssert.assertThat( + KeyType.fromKey(rsa3072.getPublic()), CoreMatchers.is(KeyType.RSA3072)); - MatcherAssert.assertThat(KeyType.fromKey(rsa4096.getPrivate()), CoreMatchers.is(KeyType.RSA4096)); - MatcherAssert.assertThat(KeyType.fromKey(rsa4096.getPublic()), CoreMatchers.is(KeyType.RSA4096)); - } + MatcherAssert.assertThat( + KeyType.fromKey(rsa4096.getPrivate()), CoreMatchers.is(KeyType.RSA4096)); + MatcherAssert.assertThat( + KeyType.fromKey(rsa4096.getPublic()), CoreMatchers.is(KeyType.RSA4096)); + } } diff --git a/piv/src/test/java/com/yubico/yubikit/piv/PaddingTest.java b/piv/src/test/java/com/yubico/yubikit/piv/PaddingTest.java index 68b183cc..bfdcbb8b 100755 --- a/piv/src/test/java/com/yubico/yubikit/piv/PaddingTest.java +++ b/piv/src/test/java/com/yubico/yubikit/piv/PaddingTest.java @@ -16,64 +16,67 @@ package com.yubico.yubikit.piv; import com.yubico.yubikit.testing.Codec; - -import org.junit.Assert; -import org.junit.Test; - import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.security.Signature; +import org.junit.Assert; +import org.junit.Test; @SuppressWarnings("SpellCheckingInspection") public class PaddingTest { - @Test - public void testPkcs1v1_5() throws NoSuchAlgorithmException { - byte[] message = "Hello world!".getBytes(StandardCharsets.UTF_8); + @Test + public void testPkcs1v1_5() throws NoSuchAlgorithmException { + byte[] message = "Hello world!".getBytes(StandardCharsets.UTF_8); - Assert.assertArrayEquals( - Codec.fromHex("0001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff003031300d060960864801650304020105000420c0535e4be2b79ffd93291305436bf889314e4a3faec05ecffcbb7df31ad9e51a"), - Padding.pad(KeyType.RSA1024, message, Signature.getInstance("SHA256withRSA")) - ); + Assert.assertArrayEquals( + Codec.fromHex( + "0001fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff003031300d06" + + "0960864801650304020105000420c0535e4be2b79ffd93291305436bf889314e4a3faec05ecffcb" + + "b7df31ad9e51a"), + Padding.pad(KeyType.RSA1024, message, Signature.getInstance("SHA256withRSA"))); - Assert.assertArrayEquals( - Codec.fromHex("0001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff003021300906052b0e03021a05000414d3486ae9136e7856bc42212385ea797094475802"), - Padding.pad(KeyType.RSA1024, message, Signature.getInstance("SHA1withRSA")) - ); - } + Assert.assertArrayEquals( + Codec.fromHex( + "0001fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "ffffffffffffffffffff003021300906052b0e03021a05000414d3486ae9136e7856bc42212385e" + + "a797094475802"), + Padding.pad(KeyType.RSA1024, message, Signature.getInstance("SHA1withRSA"))); + } - @Test - public void testEcdsa() throws NoSuchAlgorithmException { - byte[] message = "Hello world!".getBytes(StandardCharsets.UTF_8); + @Test + public void testEcdsa() throws NoSuchAlgorithmException { + byte[] message = "Hello world!".getBytes(StandardCharsets.UTF_8); - Assert.assertArrayEquals( - Codec.fromHex("c0535e4be2b79ffd93291305436bf889314e4a3faec05ecffcbb7df31ad9e51a"), - Padding.pad(KeyType.ECCP256, message, Signature.getInstance("SHA256withECDSA")) - ); + Assert.assertArrayEquals( + Codec.fromHex("c0535e4be2b79ffd93291305436bf889314e4a3faec05ecffcbb7df31ad9e51a"), + Padding.pad(KeyType.ECCP256, message, Signature.getInstance("SHA256withECDSA"))); - Assert.assertArrayEquals( - Codec.fromHex("00000000000000000000000000000000c0535e4be2b79ffd93291305436bf889314e4a3faec05ecffcbb7df31ad9e51a"), - Padding.pad(KeyType.ECCP384, message, Signature.getInstance("SHA256withECDSA")) - ); + Assert.assertArrayEquals( + Codec.fromHex( + "00000000000000000000000000000000c0535e4be2b79ffd93291305436bf889314e4a3faec05ecffcbb7" + + "df31ad9e51a"), + Padding.pad(KeyType.ECCP384, message, Signature.getInstance("SHA256withECDSA"))); - Assert.assertArrayEquals( - Codec.fromHex("000000000000000000000000d3486ae9136e7856bc42212385ea797094475802"), - Padding.pad(KeyType.ECCP256, message, Signature.getInstance("SHA1withECDSA")) - ); + Assert.assertArrayEquals( + Codec.fromHex("000000000000000000000000d3486ae9136e7856bc42212385ea797094475802"), + Padding.pad(KeyType.ECCP256, message, Signature.getInstance("SHA1withECDSA"))); - Assert.assertArrayEquals( - Codec.fromHex("f6cde2a0f819314cdde55fc227d8d7dae3d28cc556222a0a8ad66d91ccad4aad"), - Padding.pad(KeyType.ECCP256, message, Signature.getInstance("SHA512withECDSA")) - ); + Assert.assertArrayEquals( + Codec.fromHex("f6cde2a0f819314cdde55fc227d8d7dae3d28cc556222a0a8ad66d91ccad4aad"), + Padding.pad(KeyType.ECCP256, message, Signature.getInstance("SHA512withECDSA"))); - Assert.assertArrayEquals( - Codec.fromHex("f6cde2a0f819314cdde55fc227d8d7dae3d28cc556222a0a8ad66d91ccad4aad6094f517a2182360c9aacf6a3dc32316"), - Padding.pad(KeyType.ECCP384, message, Signature.getInstance("SHA512withECDSA")) - ); + Assert.assertArrayEquals( + Codec.fromHex( + "f6cde2a0f819314cdde55fc227d8d7dae3d28cc556222a0a8ad66d91ccad4aad6094f517a2182360c9aac" + + "f6a3dc32316"), + Padding.pad(KeyType.ECCP384, message, Signature.getInstance("SHA512withECDSA"))); - byte[] preHashedMessage = Codec.fromHex("c0535e4be2b79ffd93291305436bf889314e4a3faec05ecffcbb7df31ad9e51a"); - Assert.assertArrayEquals( - preHashedMessage, - Padding.pad(KeyType.ECCP256, preHashedMessage, Signature.getInstance("NONEwithECDSA")) - ); - } + byte[] preHashedMessage = + Codec.fromHex("c0535e4be2b79ffd93291305436bf889314e4a3faec05ecffcbb7df31ad9e51a"); + Assert.assertArrayEquals( + preHashedMessage, + Padding.pad(KeyType.ECCP256, preHashedMessage, Signature.getInstance("NONEwithECDSA"))); + } } diff --git a/piv/src/test/java/com/yubico/yubikit/piv/PivProviderTest.java b/piv/src/test/java/com/yubico/yubikit/piv/PivProviderTest.java index 1bae7b1f..79b37131 100755 --- a/piv/src/test/java/com/yubico/yubikit/piv/PivProviderTest.java +++ b/piv/src/test/java/com/yubico/yubikit/piv/PivProviderTest.java @@ -19,26 +19,27 @@ import com.yubico.yubikit.core.util.Result; import com.yubico.yubikit.piv.jca.PivProvider; import com.yubico.yubikit.testing.piv.PivTestUtils; - -import org.junit.Test; - import java.security.Security; +import org.junit.Test; public class PivProviderTest { - @Test - public void testStandardAlgorithms() throws Exception { - PivTestUtils.rsaTests(); - PivTestUtils.ecTests(); - PivTestUtils.cv25519Tests(); - } + @Test + public void testStandardAlgorithms() throws Exception { + PivTestUtils.rsaTests(); + PivTestUtils.ecTests(); + PivTestUtils.cv25519Tests(); + } - @Test - public void testAlgorithmsWithProvider() throws Exception { - // This doesn't actually use the provider, it makes sure the provider doesn't interfere. - Security.insertProviderAt(new PivProvider(callback -> callback.invoke(Result.failure(new UnsupportedOperationException()))), 1); + @Test + public void testAlgorithmsWithProvider() throws Exception { + // This doesn't actually use the provider, it makes sure the provider doesn't interfere. + Security.insertProviderAt( + new PivProvider( + callback -> callback.invoke(Result.failure(new UnsupportedOperationException()))), + 1); - PivTestUtils.rsaTests(); - PivTestUtils.ecTests(); - PivTestUtils.cv25519Tests(); - } + PivTestUtils.rsaTests(); + PivTestUtils.ecTests(); + PivTestUtils.cv25519Tests(); + } } diff --git a/support/src/main/java/com/yubico/yubikit/support/DeviceUtil.java b/support/src/main/java/com/yubico/yubikit/support/DeviceUtil.java index 8de477da..bd3890ea 100644 --- a/support/src/main/java/com/yubico/yubikit/support/DeviceUtil.java +++ b/support/src/main/java/com/yubico/yubikit/support/DeviceUtil.java @@ -36,530 +36,527 @@ import com.yubico.yubikit.management.FormFactor; import com.yubico.yubikit.management.ManagementSession; import com.yubico.yubikit.yubiotp.YubiOtpSession; - -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.util.ArrayList; import java.util.EnumMap; import java.util.List; import java.util.Map; - import javax.annotation.Nullable; +import org.slf4j.LoggerFactory; public class DeviceUtil { - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(DeviceUtil.class); - private static final Integer baseNeoApps = Capability.OTP.bit | Capability.OATH.bit | Capability.PIV.bit | Capability.OPENPGP.bit; + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(DeviceUtil.class); + private static final Integer baseNeoApps = + Capability.OTP.bit | Capability.OATH.bit | Capability.PIV.bit | Capability.OPENPGP.bit; - static class OtpData { - final Version version; - final @Nullable Integer serial; + static class OtpData { + final Version version; + final @Nullable Integer serial; - public OtpData(Version version, @Nullable Integer serial) { - this.version = version; - this.serial = serial; - } + public OtpData(Version version, @Nullable Integer serial) { + this.version = version; + this.serial = serial; } + } - static OtpData readOtpData(SmartCardConnection connection) - throws ApplicationNotAvailableException, IOException { + static OtpData readOtpData(SmartCardConnection connection) + throws ApplicationNotAvailableException, IOException { - YubiOtpSession otpSession = new YubiOtpSession(connection); + YubiOtpSession otpSession = new YubiOtpSession(connection); - Integer serialNumber = null; - try { - serialNumber = otpSession.getSerialNumber(); - } catch (CommandException commandException) { - Logger.error(logger, "Unable to read serial over OTP, no serial", commandException); - } + Integer serialNumber = null; + try { + serialNumber = otpSession.getSerialNumber(); + } catch (CommandException commandException) { + Logger.error(logger, "Unable to read serial over OTP, no serial", commandException); + } - return new OtpData(otpSession.getVersion(), serialNumber); + return new OtpData(otpSession.getVersion(), serialNumber); + } + + static DeviceInfo readInfoCcid(SmartCardConnection connection, int interfaces) + throws IOException { + + boolean managementAvailable = true; + Version version = null; + + try { + ManagementSession managementSession = new ManagementSession(connection); + version = managementSession.getVersion(); + try { + return managementSession.getDeviceInfo(); + } catch (UnsupportedOperationException | CommandException ignored) { + // device does not support FEATURE_DEVICE_INFO + // we ignore this exception and synthesize the information + } + } catch (ApplicationNotAvailableException ignored) { + managementAvailable = false; + Logger.debug(logger, "Couldn't select Management application, use fallback"); } - static DeviceInfo readInfoCcid(SmartCardConnection connection, int interfaces) - throws IOException { + int capabilities = 0; + Integer serial = null; + + try { + OtpData otpData = readOtpData(connection); + capabilities |= Capability.OTP.bit; + if (version == null) { + version = otpData.version; + } + serial = otpData.serial; + } catch (IOException e) { + Logger.debug(logger, "Failure when selecting OTP application, serial unknown"); + } catch (ApplicationNotAvailableException e) { + if (!managementAvailable) { + // this is not a known YubiKey + Logger.debug(logger, "Hardware key could not be identified"); + throw new IllegalArgumentException("Hardware key could not be identified"); + } + Logger.debug(logger, "Couldn't select OTP application, serial unknown"); + } - boolean managementAvailable = true; - Version version = null; + if (version == null) { + Logger.debug(logger, "Firmware version unknown, using 3.0.0 as a baseline"); + version = new Version(3, 0, 0); + } - try { - ManagementSession managementSession = new ManagementSession(connection); - version = managementSession.getVersion(); - try { - return managementSession.getDeviceInfo(); - } catch (UnsupportedOperationException | CommandException ignored) { - // device does not support FEATURE_DEVICE_INFO - // we ignore this exception and synthesize the information - } - } catch (ApplicationNotAvailableException ignored) { - managementAvailable = false; - Logger.debug(logger, "Couldn't select Management application, use fallback"); - } + Logger.debug(logger, "Scan for available ccid applications"); + SmartCardProtocol protocol = new SmartCardProtocol(connection); + for (final CcidApplet applet : CcidApplet.values()) { + try { + protocol.select(applet.aid); + capabilities |= applet.capability.bit; + } catch (ApplicationNotAvailableException applicationNotAvailableException) { + Logger.debug( + logger, "Missing applet {}, capability {}", applet.name(), applet.capability.name()); + } catch (IOException ioException) { + Logger.warn( + logger, + "IOException selecting applet {}, capability {}", + applet.name(), + applet.capability.name(), + ioException); + } + } - int capabilities = 0; - Integer serial = null; + if (((interfaces & UsbInterface.FIDO) != 0) || version.isAtLeast(3, 3, 0)) { + capabilities |= Capability.U2F.bit; + } - try { - OtpData otpData = readOtpData(connection); - capabilities |= Capability.OTP.bit; - if (version == null) { - version = otpData.version; - } - serial = otpData.serial; - } catch (IOException e) { - Logger.debug(logger, "Failure when selecting OTP application, serial unknown"); - } catch (ApplicationNotAvailableException e) { - if (!managementAvailable) { - // this is not a known YubiKey - Logger.debug(logger, "Hardware key could not be identified"); - throw new IllegalArgumentException("Hardware key could not be identified"); - } - Logger.debug(logger, "Couldn't select OTP application, serial unknown"); - } + Map supportedCapabilities = new EnumMap<>(Transport.class); + supportedCapabilities.put(Transport.USB, capabilities); + supportedCapabilities.put(Transport.NFC, capabilities); - if (version == null) { - Logger.debug(logger, "Firmware version unknown, using 3.0.0 as a baseline"); - version = new Version(3, 0, 0); - } + return new DeviceInfo.Builder() + .serialNumber(serial) + .version(version) + .supportedCapabilities(supportedCapabilities) + .build(); + } - Logger.debug(logger, "Scan for available ccid applications"); - SmartCardProtocol protocol = new SmartCardProtocol(connection); - for (final CcidApplet applet : CcidApplet.values()) { - try { - protocol.select(applet.aid); - capabilities |= applet.capability.bit; - } catch (ApplicationNotAvailableException applicationNotAvailableException) { - Logger.debug(logger, "Missing applet {}, capability {}", - applet.name(), applet.capability.name()); - } catch (IOException ioException) { - Logger.warn(logger, "IOException selecting applet {}, capability {}", - applet.name(), applet.capability.name(), ioException); - } - } + static DeviceInfo readInfoOtp(OtpConnection connection, YubiKeyType keyType, int interfaces) + throws IOException { - if (((interfaces & UsbInterface.FIDO) != 0) || version.isAtLeast(3, 3, 0)) { - capabilities |= Capability.U2F.bit; - } + ManagementSession managementSession = null; + YubiOtpSession otpSession = null; - Map supportedCapabilities = new EnumMap<>(Transport.class); - supportedCapabilities.put(Transport.USB, capabilities); - supportedCapabilities.put(Transport.NFC, capabilities); + int serial = 0; + Version version; - return new DeviceInfo.Builder() - .serialNumber(serial) - .version(version) - .supportedCapabilities(supportedCapabilities) - .build(); + try { + managementSession = new ManagementSession(connection); + } catch (ApplicationNotAvailableException ignored) { + // we could not get the management session for this connection + // we try to get the YubiOtpSession + otpSession = new YubiOtpSession(connection); } - static DeviceInfo readInfoOtp(OtpConnection connection, YubiKeyType keyType, int interfaces) - throws IOException { - - ManagementSession managementSession = null; - YubiOtpSession otpSession = null; - - int serial = 0; - Version version; - - try { - managementSession = new ManagementSession(connection); - } catch (ApplicationNotAvailableException ignored) { - // we could not get the management session for this connection - // we try to get the YubiOtpSession - otpSession = new YubiOtpSession(connection); - } - - // Retry during potential reclaim timeout period (~3s). - for (int i = 0; i < 8; i++) { - try { - if (otpSession == null) { - if (managementSession.supports(ManagementSession.FEATURE_DEVICE_INFO)) { - return managementSession.getDeviceInfo(); - } else { - otpSession = new YubiOtpSession(connection); - serial = otpSession.getSerialNumber(); - break; - } - } - } catch (CommandException commandException) { - Logger.debug(logger, "Caught Command Exception", commandException); - if (otpSession != null && interfaces == UsbInterface.OTP) { - Logger.debug(logger, "This is not reclaim"); - break; // Can't be reclaim with only one interface - } - // can be caused by reclaim state - try { - Logger.debug(logger, "Potential reclaim, sleep..."); - Thread.sleep(500); - } catch (InterruptedException ignored) { - // ignoring interrupted exception - } - } - } - + // Retry during potential reclaim timeout period (~3s). + for (int i = 0; i < 8; i++) { + try { if (otpSession == null) { + if (managementSession.supports(ManagementSession.FEATURE_DEVICE_INFO)) { + return managementSession.getDeviceInfo(); + } else { otpSession = new YubiOtpSession(connection); + serial = otpSession.getSerialNumber(); + break; + } } - - version = otpSession.getVersion(); - int usbSupported; - Map capabilities = new EnumMap<>(Transport.class); - - if (keyType == YubiKeyType.NEO) { - usbSupported = baseNeoApps; - if ((interfaces & UsbInterface.FIDO) != 0 || version.isAtLeast(3, 0, 0)) { - usbSupported |= Capability.U2F.bit; - } - capabilities.put(Transport.USB, usbSupported); - capabilities.put(Transport.NFC, usbSupported); - } else if (keyType == YubiKeyType.YKP) { - capabilities.put(Transport.USB, Capability.OTP.bit | Capability.U2F.bit); - } else { - capabilities.put(Transport.USB, Capability.OTP.bit); + } catch (CommandException commandException) { + Logger.debug(logger, "Caught Command Exception", commandException); + if (otpSession != null && interfaces == UsbInterface.OTP) { + Logger.debug(logger, "This is not reclaim"); + break; // Can't be reclaim with only one interface } - - return new DeviceInfo.Builder() - .serialNumber(serial) - .version(version) - .supportedCapabilities(capabilities) - .build(); - } - - static DeviceInfo readInfoFido(FidoConnection connection, YubiKeyType keyType) - throws IOException { + // can be caused by reclaim state try { - ManagementSession session = new ManagementSession(connection); - return session.getDeviceInfo(); - } catch (CommandException exception) { - Logger.debug(logger, "Unable to get info via Management application, using fallback"); - - final Version version = - keyType == YubiKeyType.YKP ? - new Version(4, 0, 0) : - new Version(3, 0, 0); - - Map supportedApps = new EnumMap<>(Transport.class); - supportedApps.put(Transport.USB, Capability.U2F.bit); - if (keyType == YubiKeyType.NEO) { - supportedApps.put(Transport.USB, Capability.U2F.bit | baseNeoApps); - supportedApps.put(Transport.NFC, supportedApps.get(Transport.USB)); - } - - return new DeviceInfo.Builder() - .version(version) - .formFactor(FormFactor.USB_A_KEYCHAIN) - .supportedCapabilities(supportedApps) - .build(); + Logger.debug(logger, "Potential reclaim, sleep..."); + Thread.sleep(500); + } catch (InterruptedException ignored) { + // ignoring interrupted exception } + } } - static boolean isPreviewVersion(Version version) { - return (version.isAtLeast(5, 0, 0) && version.isLessThan(5, 1, 0)) - || (version.isAtLeast(5, 2, 0) && version.isLessThan(5, 2, 3)) - || (version.isAtLeast(5, 5, 0) && version.isLessThan(5, 5, 2)); + if (otpSession == null) { + otpSession = new YubiOtpSession(connection); } - /** - * Reads out DeviceInfo from a YubiKey, or attempts to synthesize the data. - *

- * Reading DeviceInfo from a ManagementSession is only supported for newer YubiKeys. - * This function attempts to read that information, but will fall back to gathering the - * data using other mechanisms if needed. It will also make adjustments to the data if - * required, for example to "fix" known bad values. - *

- *

- * The pid parameter must be provided whenever the YubiKey is connected via USB, - *

- * - * @param connection {@link SmartCardConnection}, {@link OtpConnection} or - * {@link FidoConnection} connection to the YubiKey - * @param pid USB product ID of the YubiKey, can be null if unknown - * @throws IOException in case of connection error - * @throws IllegalArgumentException in case of pid is null for USB connection - * @throws IllegalArgumentException in case of connection is not {@link SmartCardConnection}, - * {@link OtpConnection} or {@link FidoConnection} - * @throws IllegalArgumentException when the hardware key could not be identified - */ - public static DeviceInfo readInfo(YubiKeyConnection connection, @Nullable UsbPid pid) - throws IOException, IllegalArgumentException { - - YubiKeyType keyType = null; - int interfaces = 0; - - if (pid != null) { - keyType = pid.type; - interfaces = pid.usbInterfaces; - } else if (!(connection instanceof SmartCardConnection) || - ((SmartCardConnection) connection).getTransport() == Transport.USB) { - throw new IllegalArgumentException("pid missing for usb connection"); - } - - DeviceInfo info; - if (connection instanceof SmartCardConnection) { - info = readInfoCcid((SmartCardConnection) connection, interfaces); - } else if (connection instanceof OtpConnection) { - info = readInfoOtp((OtpConnection) connection, keyType, interfaces); - } else if (connection instanceof FidoConnection) { - info = readInfoFido((FidoConnection) connection, keyType); - } else { - throw new IllegalArgumentException("Invalid connection type"); - } + version = otpSession.getVersion(); + int usbSupported; + Map capabilities = new EnumMap<>(Transport.class); + + if (keyType == YubiKeyType.NEO) { + usbSupported = baseNeoApps; + if ((interfaces & UsbInterface.FIDO) != 0 || version.isAtLeast(3, 0, 0)) { + usbSupported |= Capability.U2F.bit; + } + capabilities.put(Transport.USB, usbSupported); + capabilities.put(Transport.NFC, usbSupported); + } else if (keyType == YubiKeyType.YKP) { + capabilities.put(Transport.USB, Capability.OTP.bit | Capability.U2F.bit); + } else { + capabilities.put(Transport.USB, Capability.OTP.bit); + } - Logger.debug(logger, "Read info {}", info); - return adjustDeviceInfo(info, keyType, interfaces); + return new DeviceInfo.Builder() + .serialNumber(serial) + .version(version) + .supportedCapabilities(capabilities) + .build(); + } + + static DeviceInfo readInfoFido(FidoConnection connection, YubiKeyType keyType) + throws IOException { + try { + ManagementSession session = new ManagementSession(connection); + return session.getDeviceInfo(); + } catch (CommandException exception) { + Logger.debug(logger, "Unable to get info via Management application, using fallback"); + + final Version version = + keyType == YubiKeyType.YKP ? new Version(4, 0, 0) : new Version(3, 0, 0); + + Map supportedApps = new EnumMap<>(Transport.class); + supportedApps.put(Transport.USB, Capability.U2F.bit); + if (keyType == YubiKeyType.NEO) { + supportedApps.put(Transport.USB, Capability.U2F.bit | baseNeoApps); + supportedApps.put(Transport.NFC, supportedApps.get(Transport.USB)); + } + + return new DeviceInfo.Builder() + .version(version) + .formFactor(FormFactor.USB_A_KEYCHAIN) + .supportedCapabilities(supportedApps) + .build(); + } + } + + static boolean isPreviewVersion(Version version) { + return (version.isAtLeast(5, 0, 0) && version.isLessThan(5, 1, 0)) + || (version.isAtLeast(5, 2, 0) && version.isLessThan(5, 2, 3)) + || (version.isAtLeast(5, 5, 0) && version.isLessThan(5, 5, 2)); + } + + /** + * Reads out DeviceInfo from a YubiKey, or attempts to synthesize the data. + * + *

Reading DeviceInfo from a ManagementSession is only supported for newer YubiKeys. This + * function attempts to read that information, but will fall back to gathering the data using + * other mechanisms if needed. It will also make adjustments to the data if required, for example + * to "fix" known bad values. + * + *

The pid parameter must be provided whenever the YubiKey is connected via USB, + * + * @param connection {@link SmartCardConnection}, {@link OtpConnection} or {@link FidoConnection} + * connection to the YubiKey + * @param pid USB product ID of the YubiKey, can be null if unknown + * @throws IOException in case of connection error + * @throws IllegalArgumentException in case of pid is null for USB connection + * @throws IllegalArgumentException in case of connection is not {@link SmartCardConnection}, + * {@link OtpConnection} or {@link FidoConnection} + * @throws IllegalArgumentException when the hardware key could not be identified + */ + public static DeviceInfo readInfo(YubiKeyConnection connection, @Nullable UsbPid pid) + throws IOException, IllegalArgumentException { + + YubiKeyType keyType = null; + int interfaces = 0; + + if (pid != null) { + keyType = pid.type; + interfaces = pid.usbInterfaces; + } else if (!(connection instanceof SmartCardConnection) + || ((SmartCardConnection) connection).getTransport() == Transport.USB) { + throw new IllegalArgumentException("pid missing for usb connection"); } - /** - * This method adjusts the input DeviceInfo if required, for example it fixes known bad values. - */ - static DeviceInfo adjustDeviceInfo( - DeviceInfo info, - @Nullable YubiKeyType keyType, - int interfaces) { - final DeviceConfig config = info.getConfig(); - final Version version = info.getVersion(); - final FormFactor formFactor = info.getFormFactor(); - - int supportedUsbCapabilities = info.getSupportedCapabilities(Transport.USB); - int supportedNfcCapabilities = info.getSupportedCapabilities(Transport.NFC); - - Integer enabledUsbCapabilities = config.getEnabledCapabilities(Transport.USB); - Integer enabledNfcCapabilities = config.getEnabledCapabilities(Transport.NFC); - - // Set usbEnabled if missing (pre YubiKey 5) - if (info.hasTransport(Transport.USB) && enabledUsbCapabilities == null) { - - int usbEnabled = supportedUsbCapabilities; - if (usbEnabled == (Capability.OTP.bit | Capability.U2F.bit | UsbInterface.CCID)) { - // YubiKey Edge, hide unusable CCID interface from supported - supportedUsbCapabilities = Capability.OTP.bit | Capability.U2F.bit; - } - - if ((interfaces & UsbInterface.OTP) == 0) { - usbEnabled &= ~Capability.OTP.bit; - } - - if ((interfaces & UsbInterface.FIDO) == 0) { - usbEnabled &= ~(Capability.U2F.bit | Capability.FIDO2.bit); - } - - if ((interfaces & UsbInterface.CCID) == 0) { - usbEnabled &= ~(UsbInterface.CCID | Capability.OATH.bit | Capability.OPENPGP.bit | Capability.PIV.bit); - } - - enabledUsbCapabilities = usbEnabled; - } + DeviceInfo info; + if (connection instanceof SmartCardConnection) { + info = readInfoCcid((SmartCardConnection) connection, interfaces); + } else if (connection instanceof OtpConnection) { + info = readInfoOtp((OtpConnection) connection, keyType, interfaces); + } else if (connection instanceof FidoConnection) { + info = readInfoFido((FidoConnection) connection, keyType); + } else { + throw new IllegalArgumentException("Invalid connection type"); + } - final boolean isSky = info.isSky() || keyType == YubiKeyType.SKY; - final boolean isFips = info.isFips() || - (version.isAtLeast(4, 4, 0) && version.isLessThan(4, 5, 0)); - final boolean pinComplexity = info.getPinComplexity(); + Logger.debug(logger, "Read info {}", info); + return adjustDeviceInfo(info, keyType, interfaces); + } + + /** + * This method adjusts the input DeviceInfo if required, for example it fixes known bad values. + */ + static DeviceInfo adjustDeviceInfo( + DeviceInfo info, @Nullable YubiKeyType keyType, int interfaces) { + final DeviceConfig config = info.getConfig(); + final Version version = info.getVersion(); + final FormFactor formFactor = info.getFormFactor(); + + int supportedUsbCapabilities = info.getSupportedCapabilities(Transport.USB); + int supportedNfcCapabilities = info.getSupportedCapabilities(Transport.NFC); + + Integer enabledUsbCapabilities = config.getEnabledCapabilities(Transport.USB); + Integer enabledNfcCapabilities = config.getEnabledCapabilities(Transport.NFC); + + // Set usbEnabled if missing (pre YubiKey 5) + if (info.hasTransport(Transport.USB) && enabledUsbCapabilities == null) { + + int usbEnabled = supportedUsbCapabilities; + if (usbEnabled == (Capability.OTP.bit | Capability.U2F.bit | UsbInterface.CCID)) { + // YubiKey Edge, hide unusable CCID interface from supported + supportedUsbCapabilities = Capability.OTP.bit | Capability.U2F.bit; + } + + if ((interfaces & UsbInterface.OTP) == 0) { + usbEnabled &= ~Capability.OTP.bit; + } + + if ((interfaces & UsbInterface.FIDO) == 0) { + usbEnabled &= ~(Capability.U2F.bit | Capability.FIDO2.bit); + } + + if ((interfaces & UsbInterface.CCID) == 0) { + usbEnabled &= + ~(UsbInterface.CCID + | Capability.OATH.bit + | Capability.OPENPGP.bit + | Capability.PIV.bit); + } + + enabledUsbCapabilities = usbEnabled; + } - // Set nfc_enabled if missing (pre YubiKey 5) - if (info.hasTransport(Transport.NFC) && enabledNfcCapabilities == null) { - enabledNfcCapabilities = supportedNfcCapabilities; - } + final boolean isSky = info.isSky() || keyType == YubiKeyType.SKY; + final boolean isFips = + info.isFips() || (version.isAtLeast(4, 4, 0) && version.isLessThan(4, 5, 0)); + final boolean pinComplexity = info.getPinComplexity(); - // Workaround for invalid configurations. - if (version.isAtLeast(4, 0, 0)) { - if (formFactor == FormFactor.USB_A_NANO - || formFactor == FormFactor.USB_C_NANO - || formFactor == FormFactor.USB_C_LIGHTNING - || (formFactor == FormFactor.USB_C_KEYCHAIN - && version.isLessThan(5, 2, 4))) { - // Known not to have NFC - supportedNfcCapabilities = 0; - enabledNfcCapabilities = null; - } - } + // Set nfc_enabled if missing (pre YubiKey 5) + if (info.hasTransport(Transport.NFC) && enabledNfcCapabilities == null) { + enabledNfcCapabilities = supportedNfcCapabilities; + } - final Integer deviceFlags = config.getDeviceFlags(); - final Short autoEjectTimeout = config.getAutoEjectTimeout(); - final Byte challengeResponseTimeout = config.getChallengeResponseTimeout(); - final Boolean isNfcRestricted = config.getNfcRestricted(); + // Workaround for invalid configurations. + if (version.isAtLeast(4, 0, 0)) { + if (formFactor == FormFactor.USB_A_NANO + || formFactor == FormFactor.USB_C_NANO + || formFactor == FormFactor.USB_C_LIGHTNING + || (formFactor == FormFactor.USB_C_KEYCHAIN && version.isLessThan(5, 2, 4))) { + // Known not to have NFC + supportedNfcCapabilities = 0; + enabledNfcCapabilities = null; + } + } - DeviceConfig.Builder configBuilder = new DeviceConfig.Builder(); - if (deviceFlags != null) { - configBuilder.deviceFlags(deviceFlags); - } + final Integer deviceFlags = config.getDeviceFlags(); + final Short autoEjectTimeout = config.getAutoEjectTimeout(); + final Byte challengeResponseTimeout = config.getChallengeResponseTimeout(); + final Boolean isNfcRestricted = config.getNfcRestricted(); - if (autoEjectTimeout != null) { - configBuilder.autoEjectTimeout(autoEjectTimeout); - } + DeviceConfig.Builder configBuilder = new DeviceConfig.Builder(); + if (deviceFlags != null) { + configBuilder.deviceFlags(deviceFlags); + } - if (challengeResponseTimeout != null) { - configBuilder.challengeResponseTimeout(challengeResponseTimeout); - } + if (autoEjectTimeout != null) { + configBuilder.autoEjectTimeout(autoEjectTimeout); + } - if (enabledNfcCapabilities != null) { - configBuilder.enabledCapabilities(Transport.NFC, enabledNfcCapabilities); - } + if (challengeResponseTimeout != null) { + configBuilder.challengeResponseTimeout(challengeResponseTimeout); + } - if (enabledUsbCapabilities != null) { - configBuilder.enabledCapabilities(Transport.USB, enabledUsbCapabilities); - } + if (enabledNfcCapabilities != null) { + configBuilder.enabledCapabilities(Transport.NFC, enabledNfcCapabilities); + } - configBuilder.nfcRestricted(isNfcRestricted); + if (enabledUsbCapabilities != null) { + configBuilder.enabledCapabilities(Transport.USB, enabledUsbCapabilities); + } - Map capabilities = new EnumMap<>(Transport.class); - if (supportedUsbCapabilities != 0) { - capabilities.put(Transport.USB, supportedUsbCapabilities); - } - if (supportedNfcCapabilities != 0) { - capabilities.put(Transport.NFC, supportedNfcCapabilities); - } + configBuilder.nfcRestricted(isNfcRestricted); - return new DeviceInfo.Builder() - .config(configBuilder.build()) - .version(version) - .formFactor(formFactor) - .serialNumber(info.getSerialNumber()) - .supportedCapabilities(capabilities) - .isLocked(info.isLocked()) - .isFips(isFips) - .isSky(isSky) - .partNumber(info.getPartNumber()) - .fipsCapable(info.getFipsCapable()) - .fipsApproved(info.getFipsApproved()) - .pinComplexity(pinComplexity) - .resetBlocked(info.getResetBlocked()) - .fpsVersion(info.getFpsVersion()) - .stmVersion(info.getStmVersion()) - .build(); + Map capabilities = new EnumMap<>(Transport.class); + if (supportedUsbCapabilities != 0) { + capabilities.put(Transport.USB, supportedUsbCapabilities); + } + if (supportedNfcCapabilities != 0) { + capabilities.put(Transport.NFC, supportedNfcCapabilities); } - /** - * Determine the product name of a YubiKey - */ - public static String getName(DeviceInfo info, @Nullable YubiKeyType keyType) { - - final Version version = info.getVersion(); - final FormFactor formFactor = info.getFormFactor(); - - final int supportedUsbCapabilities = info.getSupportedCapabilities(Transport.USB); - final boolean isFidoOnly = (supportedUsbCapabilities & ~(Capability.U2F.bit | Capability.FIDO2.bit)) == 0; - - final YubiKeyType yubiKeyType = keyType != null ? - keyType : (info.getSerialNumber() == null && isFidoOnly) ? - YubiKeyType.SKY : (version.major == 3) ? - YubiKeyType.NEO : YubiKeyType.YK4; - - String deviceName = yubiKeyType.name; - - if (yubiKeyType == YubiKeyType.SKY) { - if ((supportedUsbCapabilities & Capability.FIDO2.bit) == Capability.FIDO2.bit) { - deviceName = "FIDO U2F Security Key"; // SKY 1 - } - if (info.hasTransport(Transport.NFC)) { - deviceName = "Security Key NFC"; - } - } else if (yubiKeyType == YubiKeyType.YK4) { - int majorVersion = version.major; - if (majorVersion < 4) { - if (majorVersion == 0) { - return "YubiKey (" + version + ")"; - } else { - return "YubiKey"; - } - } else if (majorVersion == 4) { - if (info.isFips()) { - //YK4 FIPS - deviceName = "YubiKey FIPS"; - } else if (supportedUsbCapabilities == (Capability.OTP.bit | Capability.U2F.bit)) { - deviceName = "YubiKey Edge"; - } else { - deviceName = "YubiKey 4"; - } - } + return new DeviceInfo.Builder() + .config(configBuilder.build()) + .version(version) + .formFactor(formFactor) + .serialNumber(info.getSerialNumber()) + .supportedCapabilities(capabilities) + .isLocked(info.isLocked()) + .isFips(isFips) + .isSky(isSky) + .partNumber(info.getPartNumber()) + .fipsCapable(info.getFipsCapable()) + .fipsApproved(info.getFipsApproved()) + .pinComplexity(pinComplexity) + .resetBlocked(info.getResetBlocked()) + .fpsVersion(info.getFpsVersion()) + .stmVersion(info.getStmVersion()) + .build(); + } + + /** Determine the product name of a YubiKey */ + public static String getName(DeviceInfo info, @Nullable YubiKeyType keyType) { + + final Version version = info.getVersion(); + final FormFactor formFactor = info.getFormFactor(); + + final int supportedUsbCapabilities = info.getSupportedCapabilities(Transport.USB); + final boolean isFidoOnly = + (supportedUsbCapabilities & ~(Capability.U2F.bit | Capability.FIDO2.bit)) == 0; + + final YubiKeyType yubiKeyType = + keyType != null + ? keyType + : (info.getSerialNumber() == null && isFidoOnly) + ? YubiKeyType.SKY + : (version.major == 3) ? YubiKeyType.NEO : YubiKeyType.YK4; + + String deviceName = yubiKeyType.name; + + if (yubiKeyType == YubiKeyType.SKY) { + if ((supportedUsbCapabilities & Capability.FIDO2.bit) == Capability.FIDO2.bit) { + deviceName = "FIDO U2F Security Key"; // SKY 1 + } + if (info.hasTransport(Transport.NFC)) { + deviceName = "Security Key NFC"; + } + } else if (yubiKeyType == YubiKeyType.YK4) { + int majorVersion = version.major; + if (majorVersion < 4) { + if (majorVersion == 0) { + return "YubiKey (" + version + ")"; + } else { + return "YubiKey"; } - - if (isPreviewVersion(version)) { - deviceName = "YubiKey Preview"; - } else if (version.isAtLeast(5, 1, 0)) { - boolean isNano = formFactor == FormFactor.USB_A_NANO - || formFactor == FormFactor.USB_C_NANO; - boolean isBio = formFactor == FormFactor.USB_A_BIO - || formFactor == FormFactor.USB_C_BIO; - // does not include Ci - boolean isC = formFactor == FormFactor.USB_C_KEYCHAIN - || formFactor == FormFactor.USB_C_NANO - || formFactor == FormFactor.USB_C_BIO; - - - List namePartsList = new ArrayList<>(); - if (info.isSky()) { - namePartsList.add("Security Key"); - } else { - namePartsList.add("YubiKey"); - if (!isBio) { - namePartsList.add("5"); - } - } - - if (isC) { - namePartsList.add("C"); - } else if (formFactor == FormFactor.USB_C_LIGHTNING) { - namePartsList.add("Ci"); - } - - if (isNano) { - namePartsList.add("Nano"); - } - - if (info.hasTransport(Transport.NFC)) { - namePartsList.add("NFC"); - } else if (formFactor == FormFactor.USB_A_KEYCHAIN) { - namePartsList.add("A"); // only for non-NFC A Keychain - } - - if (isBio) { - namePartsList.add("Bio"); - if (isFidoOnly) { - namePartsList.add("- FIDO Edition"); - } else if ((supportedUsbCapabilities & Capability.PIV.bit) == Capability.PIV.bit) { - namePartsList.add("- Multi-protocol Edition"); - } - } - - if (info.isFips()) { - namePartsList.add("FIPS"); - } - - if (info.isSky() && info.getSerialNumber() != null) { - namePartsList.add("- Enterprise Edition"); - } - - StringBuilder builder = new StringBuilder(); - for (int partCount = 0; partCount < namePartsList.size(); partCount++) { - String s = namePartsList.get(partCount); - builder.append(s); - if (partCount < namePartsList.size() - 1) { - builder.append(" "); - } - } - deviceName = builder.toString() - .replace("5 C", "5C") - .replace("5 A", "5A"); + } else if (majorVersion == 4) { + if (info.isFips()) { + // YK4 FIPS + deviceName = "YubiKey FIPS"; + } else if (supportedUsbCapabilities == (Capability.OTP.bit | Capability.U2F.bit)) { + deviceName = "YubiKey Edge"; + } else { + deviceName = "YubiKey 4"; } - return deviceName; + } } - // Applet and capability it provides - enum CcidApplet { - OPENPGP(AppId.OPENPGP, Capability.OPENPGP), - OATH(AppId.OATH, Capability.OATH), - PIV(AppId.PIV, Capability.PIV), - FIDO(AppId.FIDO, Capability.U2F), - AID_U2F_YUBICO(new byte[]{(byte) 0xa0, 0x00, 0x00, 0x05, 0x27, 0x10, 0x02}, Capability.U2F); // Old U2F AID - - final public byte[] aid; - final public Capability capability; - - CcidApplet(byte[] aid, Capability capability) { - this.aid = aid; - this.capability = capability; + if (isPreviewVersion(version)) { + deviceName = "YubiKey Preview"; + } else if (version.isAtLeast(5, 1, 0)) { + boolean isNano = formFactor == FormFactor.USB_A_NANO || formFactor == FormFactor.USB_C_NANO; + boolean isBio = formFactor == FormFactor.USB_A_BIO || formFactor == FormFactor.USB_C_BIO; + // does not include Ci + boolean isC = + formFactor == FormFactor.USB_C_KEYCHAIN + || formFactor == FormFactor.USB_C_NANO + || formFactor == FormFactor.USB_C_BIO; + + List namePartsList = new ArrayList<>(); + if (info.isSky()) { + namePartsList.add("Security Key"); + } else { + namePartsList.add("YubiKey"); + if (!isBio) { + namePartsList.add("5"); } + } + + if (isC) { + namePartsList.add("C"); + } else if (formFactor == FormFactor.USB_C_LIGHTNING) { + namePartsList.add("Ci"); + } + + if (isNano) { + namePartsList.add("Nano"); + } + + if (info.hasTransport(Transport.NFC)) { + namePartsList.add("NFC"); + } else if (formFactor == FormFactor.USB_A_KEYCHAIN) { + namePartsList.add("A"); // only for non-NFC A Keychain + } + + if (isBio) { + namePartsList.add("Bio"); + if (isFidoOnly) { + namePartsList.add("- FIDO Edition"); + } else if ((supportedUsbCapabilities & Capability.PIV.bit) == Capability.PIV.bit) { + namePartsList.add("- Multi-protocol Edition"); + } + } + + if (info.isFips()) { + namePartsList.add("FIPS"); + } + + if (info.isSky() && info.getSerialNumber() != null) { + namePartsList.add("- Enterprise Edition"); + } + + StringBuilder builder = new StringBuilder(); + for (int partCount = 0; partCount < namePartsList.size(); partCount++) { + String s = namePartsList.get(partCount); + builder.append(s); + if (partCount < namePartsList.size() - 1) { + builder.append(" "); + } + } + deviceName = builder.toString().replace("5 C", "5C").replace("5 A", "5A"); } - + return deviceName; + } + + // Applet and capability it provides + enum CcidApplet { + OPENPGP(AppId.OPENPGP, Capability.OPENPGP), + OATH(AppId.OATH, Capability.OATH), + PIV(AppId.PIV, Capability.PIV), + FIDO(AppId.FIDO, Capability.U2F), + AID_U2F_YUBICO( + new byte[] {(byte) 0xa0, 0x00, 0x00, 0x05, 0x27, 0x10, 0x02}, + Capability.U2F); // Old U2F AID + + public final byte[] aid; + public final Capability capability; + + CcidApplet(byte[] aid, Capability capability) { + this.aid = aid; + this.capability = capability; + } + } } diff --git a/support/src/main/java/com/yubico/yubikit/support/package-info.java b/support/src/main/java/com/yubico/yubikit/support/package-info.java index fb089555..471d0e07 100755 --- a/support/src/main/java/com/yubico/yubikit/support/package-info.java +++ b/support/src/main/java/com/yubico/yubikit/support/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.support; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/support/src/test/java/com/yubico/yubikit/support/AdjustDeviceInfoTest.java b/support/src/test/java/com/yubico/yubikit/support/AdjustDeviceInfoTest.java index 7d5d5ffd..892200bd 100644 --- a/support/src/test/java/com/yubico/yubikit/support/AdjustDeviceInfoTest.java +++ b/support/src/test/java/com/yubico/yubikit/support/AdjustDeviceInfoTest.java @@ -14,16 +14,15 @@ * limitations under the License. */ - package com.yubico.yubikit.support; import static com.yubico.yubikit.support.TestUtil.config; +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static java.lang.Boolean.FALSE; -import static java.lang.Boolean.TRUE; import com.yubico.yubikit.core.Transport; import com.yubico.yubikit.core.UsbInterface; @@ -31,525 +30,396 @@ import com.yubico.yubikit.core.YubiKeyType; import com.yubico.yubikit.management.DeviceInfo; import com.yubico.yubikit.management.FormFactor; - -import org.junit.Test; - import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; - import javax.annotation.Nullable; +import org.junit.Test; public class AdjustDeviceInfoTest { - @Test - public void testConfigDeviceFlags() { - assertNull( - adjustedInfo(i -> { - }).getConfig().getDeviceFlags()); - - assertEquals( - Integer.valueOf(123456), - adjustedInfo(i -> i.config(config(c -> c.deviceFlags(123456)))) - .getConfig().getDeviceFlags()); - } - - @Test - public void testConfigAutoEjectTimeout() { - assertNull( - adjustedInfo(i -> { - }).getConfig().getAutoEjectTimeout()); - - assertEquals( - Short.valueOf((short) 13288), - adjustedInfo(i -> i.config(config(c -> c.autoEjectTimeout((short) 13288)))) - .getConfig().getAutoEjectTimeout()); - } - - @Test - public void testConfigChallengeResponseTimeout() { - assertNull( - adjustedInfo(i -> { - }).getConfig().getChallengeResponseTimeout()); - - assertEquals( - Byte.valueOf((byte) 84), - adjustedInfo(i -> i.config(config(c -> c.challengeResponseTimeout((byte) 84)))) - .getConfig().getChallengeResponseTimeout()); - } - - @Test - public void testConfigEnabledCapabilitiesUsb() { - assertNull( - adjustedInfo(i -> { - }).getConfig().getEnabledCapabilities(Transport.USB)); - - DeviceInfo info = adjustedInfo(i -> i.config(config(c -> c.enabledCapabilities(Transport.USB, 124)))); - assertEquals( - Integer.valueOf(124), - info.getConfig().getEnabledCapabilities(Transport.USB)); - assertNull( - info.getConfig().getEnabledCapabilities(Transport.NFC)); - } - - @Test - public void testConfigEnabledCapabilitiesNfc() { - assertNull( - adjustedInfo(i -> { - }).getConfig().getEnabledCapabilities(Transport.NFC)); - - DeviceInfo info = adjustedInfo(i -> i.config(config(c -> c.enabledCapabilities(Transport.NFC, 552)))); - assertEquals( - Integer.valueOf(552), - info.getConfig().getEnabledCapabilities(Transport.NFC)); - assertNull( - info.getConfig().getEnabledCapabilities(Transport.USB)); - } - - @Test - public void testConfigNfcRestricted() { - assertNull( - adjustedInfo(i -> { - }).getConfig().getNfcRestricted()); - - assertEquals( - TRUE, - adjustedInfo(i -> i.config(config(c -> c.nfcRestricted(true)))) - .getConfig().getNfcRestricted()); - - assertEquals( - FALSE, - adjustedInfo(i -> i.config(config(c -> c.nfcRestricted(false)))) - .getConfig().getNfcRestricted()); - } - - @Test - public void testVersion() { - assertEquals( - new Version(0, 0, 0), - adjustedInfo(i -> { - }).getVersion()); - - assertEquals( - new Version(5, 7, 1), - adjustedInfo(i -> i.version(new Version(5, 7, 1))) - .getVersion()); - } - - @Test - public void testFormFactor() { - assertEquals( - FormFactor.UNKNOWN, - adjustedInfo(i -> { - }).getFormFactor()); - - for (FormFactor formFactor : FormFactor.values()) { - assertEquals( - formFactor, - adjustedInfo(i -> i.formFactor(formFactor)) - .getFormFactor()); - } - } - - @Test - public void testSerialNumber() { - assertNull( - adjustedInfo(i -> { - }).getSerialNumber()); - - assertEquals( - Integer.valueOf(232325454), - adjustedInfo(i -> i.serialNumber(232325454)) - .getSerialNumber()); - } - - @Test - public void testIsLocked() { - assertFalse( - adjustedInfo(i -> { - }).isLocked()); - - assertTrue( - adjustedInfo(i -> i.isLocked(true)) - .isLocked()); - - assertFalse( - adjustedInfo(i -> i.isLocked(false)) - .isLocked()); - } - - @Test - public void testIsFips() { - assertFalse( - adjustedInfo(i -> { - }).isFips()); - - assertTrue( - adjustedInfo(i -> i.isFips(true)) - .isFips()); - - assertFalse( - adjustedInfo(i -> i.isFips(false)) - .isFips()); - } - - @Test - public void testIsSky() { - assertFalse( - adjustedInfo(i -> { - }).isSky()); - - assertTrue( - adjustedInfo(i -> i.isSky(true)) - .isSky()); - - assertTrue( - adjustedInfo(i -> i.isSky(false), YubiKeyType.SKY, 0) - .isSky()); - - assertFalse( - adjustedInfo(i -> i.isSky(false)) - .isSky()); - } - - @Test - public void testFipsCapable() { - assertEquals( - 0, - adjustedInfo(i -> { - }).getFipsCapable()); - - assertEquals( - 16384, - adjustedInfo(i -> i.fipsCapable(16384)) - .getFipsCapable()); - } - - @Test - public void testFipsApproved() { - assertEquals( - 0, - adjustedInfo(i -> { - }).getFipsApproved()); - - assertEquals( - 65535, - adjustedInfo(i -> i.fipsApproved(65535)) - .getFipsApproved()); - } - - @Test - public void testPartNumber() { - assertEquals( - "", - adjustedInfo(i -> { - }).getPartNumber()); - - assertEquals( - "0102030405060708", - adjustedInfo(i -> i.partNumber("0102030405060708")) - .getPartNumber()); - } - - @Test - public void testPinComplexity() { - assertFalse( - adjustedInfo(i -> { - }).getPinComplexity()); - - assertTrue( - adjustedInfo(i -> i.pinComplexity(true)) - .getPinComplexity()); - - assertFalse( - adjustedInfo(i -> i.pinComplexity(false)) - .getPinComplexity()); - } - - @Test - public void testResetBlocked() { - assertEquals( - 0, - adjustedInfo(i -> { - }).getResetBlocked()); - - assertEquals( - 22647, - adjustedInfo(i -> i.resetBlocked(22647)) - .getResetBlocked()); + @Test + public void testConfigDeviceFlags() { + assertNull(adjustedInfo(i -> {}).getConfig().getDeviceFlags()); + + assertEquals( + Integer.valueOf(123456), + adjustedInfo(i -> i.config(config(c -> c.deviceFlags(123456)))) + .getConfig() + .getDeviceFlags()); + } + + @Test + public void testConfigAutoEjectTimeout() { + assertNull(adjustedInfo(i -> {}).getConfig().getAutoEjectTimeout()); + + assertEquals( + Short.valueOf((short) 13288), + adjustedInfo(i -> i.config(config(c -> c.autoEjectTimeout((short) 13288)))) + .getConfig() + .getAutoEjectTimeout()); + } + + @Test + public void testConfigChallengeResponseTimeout() { + assertNull(adjustedInfo(i -> {}).getConfig().getChallengeResponseTimeout()); + + assertEquals( + Byte.valueOf((byte) 84), + adjustedInfo(i -> i.config(config(c -> c.challengeResponseTimeout((byte) 84)))) + .getConfig() + .getChallengeResponseTimeout()); + } + + @Test + public void testConfigEnabledCapabilitiesUsb() { + assertNull(adjustedInfo(i -> {}).getConfig().getEnabledCapabilities(Transport.USB)); + + DeviceInfo info = + adjustedInfo(i -> i.config(config(c -> c.enabledCapabilities(Transport.USB, 124)))); + assertEquals(Integer.valueOf(124), info.getConfig().getEnabledCapabilities(Transport.USB)); + assertNull(info.getConfig().getEnabledCapabilities(Transport.NFC)); + } + + @Test + public void testConfigEnabledCapabilitiesNfc() { + assertNull(adjustedInfo(i -> {}).getConfig().getEnabledCapabilities(Transport.NFC)); + + DeviceInfo info = + adjustedInfo(i -> i.config(config(c -> c.enabledCapabilities(Transport.NFC, 552)))); + assertEquals(Integer.valueOf(552), info.getConfig().getEnabledCapabilities(Transport.NFC)); + assertNull(info.getConfig().getEnabledCapabilities(Transport.USB)); + } + + @Test + public void testConfigNfcRestricted() { + assertNull(adjustedInfo(i -> {}).getConfig().getNfcRestricted()); + + assertEquals( + TRUE, + adjustedInfo(i -> i.config(config(c -> c.nfcRestricted(true)))) + .getConfig() + .getNfcRestricted()); + + assertEquals( + FALSE, + adjustedInfo(i -> i.config(config(c -> c.nfcRestricted(false)))) + .getConfig() + .getNfcRestricted()); + } + + @Test + public void testVersion() { + assertEquals(new Version(0, 0, 0), adjustedInfo(i -> {}).getVersion()); + + assertEquals( + new Version(5, 7, 1), adjustedInfo(i -> i.version(new Version(5, 7, 1))).getVersion()); + } + + @Test + public void testFormFactor() { + assertEquals(FormFactor.UNKNOWN, adjustedInfo(i -> {}).getFormFactor()); + + for (FormFactor formFactor : FormFactor.values()) { + assertEquals(formFactor, adjustedInfo(i -> i.formFactor(formFactor)).getFormFactor()); } + } - @Test - public void testFpsVersion() { - assertNull( - adjustedInfo(i -> { - }).getFpsVersion()); + @Test + public void testSerialNumber() { + assertNull(adjustedInfo(i -> {}).getSerialNumber()); - assertEquals( - new Version(1, 4, 2), - adjustedInfo(i -> i.fpsVersion(new Version(1, 4, 2))) - .getFpsVersion()); - } + assertEquals( + Integer.valueOf(232325454), adjustedInfo(i -> i.serialNumber(232325454)).getSerialNumber()); + } - @Test - public void testStmVersion() { - assertNull( - adjustedInfo(i -> { - }).getStmVersion()); + @Test + public void testIsLocked() { + assertFalse(adjustedInfo(i -> {}).isLocked()); - assertEquals( - new Version(2, 4, 2), - adjustedInfo(i -> i.stmVersion(new Version(2, 4, 2))) - .getStmVersion()); - } + assertTrue(adjustedInfo(i -> i.isLocked(true)).isLocked()); - @Test - public void testSupportedCapabilities() { - // USB - assertEquals( - 0, - adjustedInfo(i -> { - }).getSupportedCapabilities(Transport.USB)); - - Map supportedUsbCapabilities = new HashMap<>(); - supportedUsbCapabilities.put(Transport.USB, 4096); - assertEquals( - 4096, - adjustedInfo(i -> i.supportedCapabilities(supportedUsbCapabilities)) - .getSupportedCapabilities(Transport.USB)); - - Map supportedNfcCapabilities = new HashMap<>(); - supportedNfcCapabilities.put(Transport.NFC, 4096); - assertEquals( - 0, - adjustedInfo(i -> i.supportedCapabilities(supportedNfcCapabilities)) - .getSupportedCapabilities(Transport.USB)); - assertEquals( - 4096, - adjustedInfo(i -> i.supportedCapabilities(supportedNfcCapabilities)) - .getSupportedCapabilities(Transport.NFC)); - - Map supportedCapabilities = new HashMap<>(); - supportedCapabilities.put(Transport.NFC, 8192); - supportedCapabilities.put(Transport.USB, 16384); - assertEquals( - 16384, - adjustedInfo(i -> i.supportedCapabilities(supportedCapabilities)) - .getSupportedCapabilities(Transport.USB)); - assertEquals( - 8192, - adjustedInfo(i -> i.supportedCapabilities(supportedCapabilities)) - .getSupportedCapabilities(Transport.NFC)); - } + assertFalse(adjustedInfo(i -> i.isLocked(false)).isLocked()); + } - @Test - public void testEnabledNfcCapabilities() { - Map supportedCapabilities = new HashMap<>(); - supportedCapabilities.put(Transport.NFC, 8192); - assertEquals( - Integer.valueOf(4096), - adjustedInfo(i -> { - i.supportedCapabilities(supportedCapabilities); - i.config(config(c -> c.enabledCapabilities(Transport.NFC, 4096))); - }).getConfig().getEnabledCapabilities(Transport.NFC)); + @Test + public void testIsFips() { + assertFalse(adjustedInfo(i -> {}).isFips()); - // null enabled capabilities - assertEquals( - Integer.valueOf(8192), - adjustedInfo(i -> i.supportedCapabilities(supportedCapabilities)) - .getConfig().getEnabledCapabilities(Transport.NFC)); + assertTrue(adjustedInfo(i -> i.isFips(true)).isFips()); - List usbOnlyFactors = new ArrayList<>(); - usbOnlyFactors.add(FormFactor.USB_A_NANO); - usbOnlyFactors.add(FormFactor.USB_C_NANO); - usbOnlyFactors.add(FormFactor.USB_C_LIGHTNING); - usbOnlyFactors.add(FormFactor.USB_C_KEYCHAIN); - - for (FormFactor formFactor : usbOnlyFactors) { - - DeviceInfo info = adjustedInfo(i -> { + assertFalse(adjustedInfo(i -> i.isFips(false)).isFips()); + } + + @Test + public void testIsSky() { + assertFalse(adjustedInfo(i -> {}).isSky()); + + assertTrue(adjustedInfo(i -> i.isSky(true)).isSky()); + + assertTrue(adjustedInfo(i -> i.isSky(false), YubiKeyType.SKY, 0).isSky()); + + assertFalse(adjustedInfo(i -> i.isSky(false)).isSky()); + } + + @Test + public void testFipsCapable() { + assertEquals(0, adjustedInfo(i -> {}).getFipsCapable()); + + assertEquals(16384, adjustedInfo(i -> i.fipsCapable(16384)).getFipsCapable()); + } + + @Test + public void testFipsApproved() { + assertEquals(0, adjustedInfo(i -> {}).getFipsApproved()); + + assertEquals(65535, adjustedInfo(i -> i.fipsApproved(65535)).getFipsApproved()); + } + + @Test + public void testPartNumber() { + assertEquals("", adjustedInfo(i -> {}).getPartNumber()); + + assertEquals( + "0102030405060708", adjustedInfo(i -> i.partNumber("0102030405060708")).getPartNumber()); + } + + @Test + public void testPinComplexity() { + assertFalse(adjustedInfo(i -> {}).getPinComplexity()); + + assertTrue(adjustedInfo(i -> i.pinComplexity(true)).getPinComplexity()); + + assertFalse(adjustedInfo(i -> i.pinComplexity(false)).getPinComplexity()); + } + + @Test + public void testResetBlocked() { + assertEquals(0, adjustedInfo(i -> {}).getResetBlocked()); + + assertEquals(22647, adjustedInfo(i -> i.resetBlocked(22647)).getResetBlocked()); + } + + @Test + public void testFpsVersion() { + assertNull(adjustedInfo(i -> {}).getFpsVersion()); + + assertEquals( + new Version(1, 4, 2), + adjustedInfo(i -> i.fpsVersion(new Version(1, 4, 2))).getFpsVersion()); + } + + @Test + public void testStmVersion() { + assertNull(adjustedInfo(i -> {}).getStmVersion()); + + assertEquals( + new Version(2, 4, 2), + adjustedInfo(i -> i.stmVersion(new Version(2, 4, 2))).getStmVersion()); + } + + @Test + public void testSupportedCapabilities() { + // USB + assertEquals(0, adjustedInfo(i -> {}).getSupportedCapabilities(Transport.USB)); + + Map supportedUsbCapabilities = new HashMap<>(); + supportedUsbCapabilities.put(Transport.USB, 4096); + assertEquals( + 4096, + adjustedInfo(i -> i.supportedCapabilities(supportedUsbCapabilities)) + .getSupportedCapabilities(Transport.USB)); + + Map supportedNfcCapabilities = new HashMap<>(); + supportedNfcCapabilities.put(Transport.NFC, 4096); + assertEquals( + 0, + adjustedInfo(i -> i.supportedCapabilities(supportedNfcCapabilities)) + .getSupportedCapabilities(Transport.USB)); + assertEquals( + 4096, + adjustedInfo(i -> i.supportedCapabilities(supportedNfcCapabilities)) + .getSupportedCapabilities(Transport.NFC)); + + Map supportedCapabilities = new HashMap<>(); + supportedCapabilities.put(Transport.NFC, 8192); + supportedCapabilities.put(Transport.USB, 16384); + assertEquals( + 16384, + adjustedInfo(i -> i.supportedCapabilities(supportedCapabilities)) + .getSupportedCapabilities(Transport.USB)); + assertEquals( + 8192, + adjustedInfo(i -> i.supportedCapabilities(supportedCapabilities)) + .getSupportedCapabilities(Transport.NFC)); + } + + @Test + public void testEnabledNfcCapabilities() { + Map supportedCapabilities = new HashMap<>(); + supportedCapabilities.put(Transport.NFC, 8192); + assertEquals( + Integer.valueOf(4096), + adjustedInfo( + i -> { + i.supportedCapabilities(supportedCapabilities); + i.config(config(c -> c.enabledCapabilities(Transport.NFC, 4096))); + }) + .getConfig() + .getEnabledCapabilities(Transport.NFC)); + + // null enabled capabilities + assertEquals( + Integer.valueOf(8192), + adjustedInfo(i -> i.supportedCapabilities(supportedCapabilities)) + .getConfig() + .getEnabledCapabilities(Transport.NFC)); + + List usbOnlyFactors = new ArrayList<>(); + usbOnlyFactors.add(FormFactor.USB_A_NANO); + usbOnlyFactors.add(FormFactor.USB_C_NANO); + usbOnlyFactors.add(FormFactor.USB_C_LIGHTNING); + usbOnlyFactors.add(FormFactor.USB_C_KEYCHAIN); + + for (FormFactor formFactor : usbOnlyFactors) { + + DeviceInfo info = + adjustedInfo( + i -> { i.formFactor(formFactor); i.supportedCapabilities(supportedCapabilities); i.version(new Version(5, 2, 3)); i.config(config(c -> c.enabledCapabilities(Transport.NFC, 4096))); - }); + }); - assertNull( - info.getConfig().getEnabledCapabilities(Transport.NFC)); + assertNull(info.getConfig().getEnabledCapabilities(Transport.NFC)); - assertEquals( - 0, - info.getSupportedCapabilities(Transport.NFC) - ); + assertEquals(0, info.getSupportedCapabilities(Transport.NFC)); - if (formFactor == FormFactor.USB_C_KEYCHAIN) { - info = adjustedInfo(i -> { - i.formFactor(formFactor); - i.supportedCapabilities(supportedCapabilities); - i.version(new Version(5, 2, 4)); - i.config(config(c -> c.enabledCapabilities(Transport.NFC, 4096))); + if (formFactor == FormFactor.USB_C_KEYCHAIN) { + info = + adjustedInfo( + i -> { + i.formFactor(formFactor); + i.supportedCapabilities(supportedCapabilities); + i.version(new Version(5, 2, 4)); + i.config(config(c -> c.enabledCapabilities(Transport.NFC, 4096))); }); - assertEquals( - Integer.valueOf(4096), - info.getConfig().getEnabledCapabilities(Transport.NFC)); + assertEquals(Integer.valueOf(4096), info.getConfig().getEnabledCapabilities(Transport.NFC)); - assertEquals( - 8192, - info.getSupportedCapabilities(Transport.NFC) - ); + assertEquals(8192, info.getSupportedCapabilities(Transport.NFC)); - // null enabled capabilities - info = adjustedInfo(i -> { - i.formFactor(formFactor); - i.supportedCapabilities(supportedCapabilities); - i.version(new Version(5, 2, 4)); + // null enabled capabilities + info = + adjustedInfo( + i -> { + i.formFactor(formFactor); + i.supportedCapabilities(supportedCapabilities); + i.version(new Version(5, 2, 4)); }); - assertEquals( - Integer.valueOf(8192), - info.getConfig().getEnabledCapabilities(Transport.NFC)); - - assertEquals( - 8192, - info.getSupportedCapabilities(Transport.NFC) - ); - } - } + assertEquals(Integer.valueOf(8192), info.getConfig().getEnabledCapabilities(Transport.NFC)); + assertEquals(8192, info.getSupportedCapabilities(Transport.NFC)); + } } + } + + @Test + public void testEnabledUsbCapabilities() { + Map supportedCapabilities = new HashMap<>(); + supportedCapabilities.put(Transport.USB, 0b0111); + + // enabled usb capabilities are not null + DeviceInfo info = + adjustedInfo( + i -> { + i.supportedCapabilities(supportedCapabilities); + i.config(config(c -> c.enabledCapabilities(Transport.USB, 0b1011))); + }); - @Test - public void testEnabledUsbCapabilities() { - Map supportedCapabilities = new HashMap<>(); - supportedCapabilities.put(Transport.USB, 0b0111); + assertEquals(Integer.valueOf(0b1011), info.getConfig().getEnabledCapabilities(Transport.USB)); - // enabled usb capabilities are not null - DeviceInfo info = adjustedInfo(i -> { - i.supportedCapabilities(supportedCapabilities); - i.config(config(c -> c.enabledCapabilities(Transport.USB, 0b1011))); - }); + assertEquals(0b0111, info.getSupportedCapabilities(Transport.USB)); - assertEquals( - Integer.valueOf(0b1011), - info.getConfig().getEnabledCapabilities(Transport.USB)); + // no usb transport support + // enabled usb capabilities are not null + info = adjustedInfo(i -> i.config(config(c -> c.enabledCapabilities(Transport.USB, 0b1011)))); - assertEquals( - 0b0111, - info.getSupportedCapabilities(Transport.USB)); + assertEquals(Integer.valueOf(0b1011), info.getConfig().getEnabledCapabilities(Transport.USB)); - // no usb transport support - // enabled usb capabilities are not null - info = adjustedInfo(i -> i.config(config(c -> c.enabledCapabilities(Transport.USB, 0b1011)))); + assertEquals(0, info.getSupportedCapabilities(Transport.USB)); - assertEquals( - Integer.valueOf(0b1011), - info.getConfig().getEnabledCapabilities(Transport.USB)); + // null enabled capabilities + info = adjustedInfo(i -> i.supportedCapabilities(supportedCapabilities)); - assertEquals( - 0, - info.getSupportedCapabilities(Transport.USB)); + assertEquals(Integer.valueOf(0b0111), info.getConfig().getEnabledCapabilities(Transport.USB)); - // null enabled capabilities - info = adjustedInfo(i -> i.supportedCapabilities(supportedCapabilities)); + assertEquals(0b0011, info.getSupportedCapabilities(Transport.USB)); - assertEquals( - Integer.valueOf(0b0111), - info.getConfig().getEnabledCapabilities(Transport.USB)); + // with OTP interface + info = adjustedInfo(i -> i.supportedCapabilities(supportedCapabilities), null, 0b0111); - assertEquals( - 0b0011, - info.getSupportedCapabilities(Transport.USB)); + assertEquals(Integer.valueOf(0b0111), info.getConfig().getEnabledCapabilities(Transport.USB)); - // with OTP interface - info = adjustedInfo(i -> i.supportedCapabilities(supportedCapabilities), null, 0b0111); + assertEquals(0b0011, info.getSupportedCapabilities(Transport.USB)); - assertEquals( - Integer.valueOf(0b0111), - info.getConfig().getEnabledCapabilities(Transport.USB)); + // without OTP interface + info = adjustedInfo(i -> i.supportedCapabilities(supportedCapabilities), null, 0b0110); - assertEquals( - 0b0011, - info.getSupportedCapabilities(Transport.USB)); + assertEquals(Integer.valueOf(0b0110), info.getConfig().getEnabledCapabilities(Transport.USB)); - // without OTP interface - info = adjustedInfo(i -> i.supportedCapabilities(supportedCapabilities), null, 0b0110); + assertEquals(0b0011, info.getSupportedCapabilities(Transport.USB)); - assertEquals( - Integer.valueOf(0b0110), - info.getConfig().getEnabledCapabilities(Transport.USB)); + // add FIDO2 capability + supportedCapabilities.put(Transport.USB, 0x207); + // with FIDO interface + info = adjustedInfo(i -> i.supportedCapabilities(supportedCapabilities), null, 0b0111); - assertEquals( - 0b0011, - info.getSupportedCapabilities(Transport.USB)); + assertEquals(Integer.valueOf(0x207), info.getConfig().getEnabledCapabilities(Transport.USB)); - // add FIDO2 capability - supportedCapabilities.put(Transport.USB, 0x207); - // with FIDO interface - info = adjustedInfo(i -> i.supportedCapabilities(supportedCapabilities), null, 0b0111); + assertEquals(0x207, info.getSupportedCapabilities(Transport.USB)); - assertEquals( - Integer.valueOf(0x207), - info.getConfig().getEnabledCapabilities(Transport.USB)); + // without FIDO interface + supportedCapabilities.put(Transport.USB, 0x207); + info = adjustedInfo(i -> i.supportedCapabilities(supportedCapabilities), null, 0b0101); - assertEquals( - 0x207, - info.getSupportedCapabilities(Transport.USB)); + assertEquals(Integer.valueOf(0x5), info.getConfig().getEnabledCapabilities(Transport.USB)); - // without FIDO interface - supportedCapabilities.put(Transport.USB, 0x207); - info = adjustedInfo(i -> i.supportedCapabilities(supportedCapabilities), null, 0b0101); + assertEquals(0x207, info.getSupportedCapabilities(Transport.USB)); - assertEquals( - Integer.valueOf(0x5), - info.getConfig().getEnabledCapabilities(Transport.USB)); + // all CCID capabilities (and FIDO2+U2F) + supportedCapabilities.put(Transport.USB, 0x23A); + // with CCID interface + info = adjustedInfo(i -> i.supportedCapabilities(supportedCapabilities), null, 0b0111); - assertEquals( - 0x207, - info.getSupportedCapabilities(Transport.USB)); + assertEquals(Integer.valueOf(0x23A), info.getConfig().getEnabledCapabilities(Transport.USB)); - // all CCID capabilities (and FIDO2+U2F) - supportedCapabilities.put(Transport.USB, 0x23A); - // with CCID interface - info = adjustedInfo(i -> i.supportedCapabilities(supportedCapabilities), null, 0b0111); + assertEquals(0x23A, info.getSupportedCapabilities(Transport.USB)); - assertEquals( - Integer.valueOf(0x23A), - info.getConfig().getEnabledCapabilities(Transport.USB)); + // without FIDO interface + supportedCapabilities.put(Transport.USB, 0x23A); + info = adjustedInfo(i -> i.supportedCapabilities(supportedCapabilities), null, 0b0011); - assertEquals( - 0x23A, - info.getSupportedCapabilities(Transport.USB)); + assertEquals(Integer.valueOf(0x202), info.getConfig().getEnabledCapabilities(Transport.USB)); - // without FIDO interface - supportedCapabilities.put(Transport.USB, 0x23A); - info = adjustedInfo(i -> i.supportedCapabilities(supportedCapabilities), null, 0b0011); + assertEquals(0x23A, info.getSupportedCapabilities(Transport.USB)); + } - assertEquals( - Integer.valueOf(0x202), - info.getConfig().getEnabledCapabilities(Transport.USB)); + DeviceInfo adjustedInfo(TestUtil.DeviceInfoBuilder infoBuilder) { + YubiKeyType yubiKeyType = YubiKeyType.YK4; + int interfaces = UsbInterface.CCID | UsbInterface.OTP | UsbInterface.FIDO; - assertEquals( - 0x23A, - info.getSupportedCapabilities(Transport.USB)); - - } + return adjustedInfo(infoBuilder, yubiKeyType, interfaces); + } - DeviceInfo adjustedInfo(TestUtil.DeviceInfoBuilder infoBuilder) { - YubiKeyType yubiKeyType = YubiKeyType.YK4; - int interfaces = UsbInterface.CCID | UsbInterface.OTP | UsbInterface.FIDO; - - return adjustedInfo(infoBuilder, yubiKeyType, interfaces); - } - - // call the function under test DeviceUtil.adjustDeviceInfo - DeviceInfo adjustedInfo( - TestUtil.DeviceInfoBuilder infoBuilder, - @Nullable YubiKeyType keyType, - int interfaces) { - DeviceInfo.Builder builder = new DeviceInfo.Builder(); - infoBuilder.createWith(builder); - return DeviceUtil.adjustDeviceInfo(builder.build(), keyType, interfaces); - } + // call the function under test DeviceUtil.adjustDeviceInfo + DeviceInfo adjustedInfo( + TestUtil.DeviceInfoBuilder infoBuilder, @Nullable YubiKeyType keyType, int interfaces) { + DeviceInfo.Builder builder = new DeviceInfo.Builder(); + infoBuilder.createWith(builder); + return DeviceUtil.adjustDeviceInfo(builder.build(), keyType, interfaces); + } } diff --git a/support/src/test/java/com/yubico/yubikit/support/DeviceUtilTest.java b/support/src/test/java/com/yubico/yubikit/support/DeviceUtilTest.java index 0515eb91..475c026c 100644 --- a/support/src/test/java/com/yubico/yubikit/support/DeviceUtilTest.java +++ b/support/src/test/java/com/yubico/yubikit/support/DeviceUtilTest.java @@ -22,25 +22,24 @@ import com.yubico.yubikit.core.Version; import com.yubico.yubikit.core.smartcard.AppId; - import org.junit.Test; public class DeviceUtilTest { - @Test - public void ccidAppletTest() { - assertArrayEquals(AppId.OPENPGP, DeviceUtil.CcidApplet.OPENPGP.aid); - assertArrayEquals(AppId.OATH, DeviceUtil.CcidApplet.OATH.aid); - assertArrayEquals(AppId.PIV, DeviceUtil.CcidApplet.PIV.aid); - assertArrayEquals(AppId.FIDO, DeviceUtil.CcidApplet.FIDO.aid); - assertArrayEquals( - new byte[]{(byte) 0xa0, 0x00, 0x00, 0x05, 0x27, 0x10, 0x02}, - DeviceUtil.CcidApplet.AID_U2F_YUBICO.aid); - } + @Test + public void ccidAppletTest() { + assertArrayEquals(AppId.OPENPGP, DeviceUtil.CcidApplet.OPENPGP.aid); + assertArrayEquals(AppId.OATH, DeviceUtil.CcidApplet.OATH.aid); + assertArrayEquals(AppId.PIV, DeviceUtil.CcidApplet.PIV.aid); + assertArrayEquals(AppId.FIDO, DeviceUtil.CcidApplet.FIDO.aid); + assertArrayEquals( + new byte[] {(byte) 0xa0, 0x00, 0x00, 0x05, 0x27, 0x10, 0x02}, + DeviceUtil.CcidApplet.AID_U2F_YUBICO.aid); + } - @Test - public void otpDataTest() { - assertEquals(new Version(1, 2, 3), new DeviceUtil.OtpData(new Version(1, 2, 3), null).version); - assertNull(new DeviceUtil.OtpData(new Version(1, 2, 3), null).serial); - assertEquals(Integer.valueOf(123), new DeviceUtil.OtpData(new Version(1, 2, 3), 123).serial); - } + @Test + public void otpDataTest() { + assertEquals(new Version(1, 2, 3), new DeviceUtil.OtpData(new Version(1, 2, 3), null).version); + assertNull(new DeviceUtil.OtpData(new Version(1, 2, 3), null).serial); + assertEquals(Integer.valueOf(123), new DeviceUtil.OtpData(new Version(1, 2, 3), 123).serial); + } } diff --git a/support/src/test/java/com/yubico/yubikit/support/GetNameTest.java b/support/src/test/java/com/yubico/yubikit/support/GetNameTest.java index 33010467..6147e808 100644 --- a/support/src/test/java/com/yubico/yubikit/support/GetNameTest.java +++ b/support/src/test/java/com/yubico/yubikit/support/GetNameTest.java @@ -33,457 +33,620 @@ import com.yubico.yubikit.core.Version; import com.yubico.yubikit.core.YubiKeyType; import com.yubico.yubikit.management.Capability; - -import org.junit.Test; - import java.util.HashMap; +import org.junit.Test; public class GetNameTest { - @Test - public void testYubiKeyUnknownFormFactor() { - assertEquals( - "YubiKey 5", - DeviceUtil.getName(info(i -> { - i.formFactor(UNKNOWN); - i.version(new Version(5, 4, 3)); - i.supportedCapabilities(yk5UsbOnlyCapabilities); - }), YubiKeyType.YK4)); - - assertEquals( - "YubiKey 5 NFC", - DeviceUtil.getName(info(i -> { - i.formFactor(UNKNOWN); - i.version(new Version(5, 4, 3)); - i.supportedCapabilities(yk5Capabilities); - }), YubiKeyType.YK4)); - } - - @Test - public void testYubiKey5() { - assertEquals( - "YubiKey 5A", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_A_KEYCHAIN); - i.version(new Version(5, 4, 3)); - i.supportedCapabilities(yk5UsbOnlyCapabilities); - }), YubiKeyType.YK4)); - - assertEquals( - "YubiKey 5C", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_C_KEYCHAIN); - i.version(new Version(5, 4, 3)); - i.supportedCapabilities(yk5UsbOnlyCapabilities); - }), YubiKeyType.YK4)); - } - - @Test - public void testYubiKey5Nfc() { - assertEquals( - "YubiKey 5 NFC", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_A_KEYCHAIN); - i.version(new Version(5, 4, 3)); - i.supportedCapabilities(yk5Capabilities); - }), YubiKeyType.YK4)); - - assertEquals( - "YubiKey 5C NFC", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_C_KEYCHAIN); - i.version(new Version(5, 4, 3)); - i.supportedCapabilities(yk5Capabilities); - }), YubiKeyType.YK4)); - } - - @Test - public void testYubiKey5Nano() { - assertEquals( - "YubiKey 5 Nano", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_A_NANO); - i.version(new Version(5, 4, 3)); - i.supportedCapabilities(yk5UsbOnlyCapabilities); - }), YubiKeyType.YK4)); - - assertEquals( - "YubiKey 5C Nano", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_C_NANO); - i.version(new Version(5, 4, 3)); - i.supportedCapabilities(yk5UsbOnlyCapabilities); - }), YubiKeyType.YK4)); - } - - @Test - public void testYubiKey5Lightning() { - assertEquals( - "YubiKey 5Ci", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_C_LIGHTNING); - i.version(new Version(5, 4, 3)); - i.supportedCapabilities(yk5UsbOnlyCapabilities); - }), YubiKeyType.YK4)); - } - - - @Test - public void testSecurityKey() { - - assertEquals( - "FIDO U2F Security Key", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_A_KEYCHAIN); - i.supportedCapabilities(new HashMap() {{ - put(Transport.USB, fidoBits); - }}); - }), YubiKeyType.SKY)); - - assertEquals( - "Security Key by Yubico", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_A_KEYCHAIN); - i.supportedCapabilities(new HashMap() {{ - put(Transport.USB, Capability.U2F.bit); - }}); - }), YubiKeyType.SKY)); - - assertEquals( - "Security Key NFC", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_A_KEYCHAIN); - i.supportedCapabilities(new HashMap() {{ - put(Transport.NFC, fidoBits); - }}); - }), YubiKeyType.SKY)); - - assertEquals( - "Security Key NFC", - DeviceUtil.getName(info(i -> { - i.isSky(true); - i.formFactor(USB_A_KEYCHAIN); - i.version(new Version(5, 6, 0)); - i.supportedCapabilities(fidoCapabilities); - }), YubiKeyType.YK4)); - - assertEquals( - "Security Key C NFC", - DeviceUtil.getName(info(i -> { - i.isSky(true); - i.formFactor(USB_C_KEYCHAIN); - i.version(new Version(5, 6, 0)); - i.supportedCapabilities(fidoCapabilities); - }), YubiKeyType.YK4)); - - assertEquals( - "Security Key NFC", - DeviceUtil.getName(info(i -> { - i.version(new Version(3, 2, 0)); - i.supportedCapabilities(fidoCapabilities); - }), null)); - } - - @Test - public void testFips() { - - assertEquals( - "YubiKey 5 NFC FIPS", - DeviceUtil.getName(info(i -> { - i.isFips(true); - i.formFactor(USB_A_KEYCHAIN); - i.version(new Version(5, 6, 0)); - i.supportedCapabilities(yk5Capabilities); - }), YubiKeyType.YK4)); - - assertEquals( - "YubiKey 5C NFC FIPS", - DeviceUtil.getName(info(i -> { - i.isFips(true); - i.formFactor(USB_C_KEYCHAIN); - i.version(new Version(5, 6, 0)); - i.supportedCapabilities(yk5Capabilities); - }), YubiKeyType.YK4)); - - assertEquals( - "YubiKey 5A FIPS", - DeviceUtil.getName(info(i -> { - i.isFips(true); - i.formFactor(USB_A_KEYCHAIN); - i.version(new Version(5, 6, 0)); - i.supportedCapabilities(yk5UsbOnlyCapabilities); - }), YubiKeyType.YK4)); - - assertEquals( - "YubiKey 5C FIPS", - DeviceUtil.getName(info(i -> { - i.isFips(true); - i.formFactor(USB_C_KEYCHAIN); - i.version(new Version(5, 6, 0)); - i.supportedCapabilities(yk5UsbOnlyCapabilities); - }), YubiKeyType.YK4)); - } - - @Test - public void testYubiKey4Fips() { - assertEquals( - "YubiKey FIPS", - DeviceUtil.getName(info(i -> { - i.isFips(true); - i.formFactor(USB_A_KEYCHAIN); - i.version(new Version(4, 0, 0)); - i.supportedCapabilities(yk4Capabilities); - }), YubiKeyType.YK4)); - } - - @Test - public void testYubiKeyEdge() { - assertEquals( - "YubiKey Edge", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_A_KEYCHAIN); - i.version(new Version(4, 0, 0)); - i.supportedCapabilities(edgeCapabilities); - }), YubiKeyType.YK4)); - } - - @Test - public void testYubiKey4() { - assertEquals( - "YubiKey 4", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_A_KEYCHAIN); - i.version(new Version(4, 0, 0)); - i.supportedCapabilities(yk4Capabilities); - }), YubiKeyType.YK4)); - - assertEquals( - "YubiKey 4", - DeviceUtil.getName(info(i -> { - i.version(new Version(4, 2, 0)); - i.supportedCapabilities(yk4Capabilities); - }), null)); - } - - @Test - public void testBioSeriesFidoEdition() { - assertEquals( - "YubiKey Bio - FIDO Edition", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_A_BIO); - i.version(new Version(5, 6, 6)); - i.supportedCapabilities(bioCapabilities); - }), YubiKeyType.YK4)); - - assertEquals( - "YubiKey C Bio - FIDO Edition", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_C_BIO); - i.version(new Version(5, 6, 6)); - i.supportedCapabilities(bioCapabilities); - }), YubiKeyType.YK4)); - } - - @Test - public void testBioSeriesMultiProtocolEdition() { - // multi-protocol has PIV and a serial number - assertEquals( - "YubiKey Bio - Multi-protocol Edition", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_A_BIO); - i.version(new Version(5, 6, 6)); - i.supportedCapabilities(bioMultiProtocolCapabilities); - i.serialNumber(12345); - }), YubiKeyType.YK4)); - - assertEquals( - "YubiKey C Bio - Multi-protocol Edition", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_C_BIO); - i.version(new Version(5, 6, 6)); - i.supportedCapabilities(bioMultiProtocolCapabilities); - i.serialNumber(12345); - }), YubiKeyType.YK4)); - } - - @Test - public void testBioSeries() { - // these are neither Enterprise nor Multi-protocol Edition Bios - assertEquals( - "YubiKey Bio", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_A_BIO); - i.version(new Version(5, 6, 6)); - i.supportedCapabilities(new HashMap() {{ - put(Transport.USB, fidoBits | UsbInterface.CCID); - }}); - }), YubiKeyType.YK4)); - - assertEquals( - "YubiKey C Bio", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_C_BIO); - i.version(new Version(5, 6, 6)); - i.supportedCapabilities(new HashMap() {{ - put(Transport.USB, fidoBits | UsbInterface.CCID); - }}); - }), YubiKeyType.YK4)); - } - - @Test - public void testSecurityKeyEnterpriseEdition() { - assertEquals( - "Security Key NFC - Enterprise Edition", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_A_KEYCHAIN); - i.version(new Version(5, 4, 3)); - i.isSky(true); - i.supportedCapabilities(fidoCapabilities); - i.serialNumber(65454545); - }), YubiKeyType.YK4)); - - assertEquals( - "Security Key C NFC - Enterprise Edition", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_C_KEYCHAIN); - i.version(new Version(5, 4, 3)); - i.isSky(true); - i.supportedCapabilities(fidoCapabilities); - i.serialNumber(65454545); - }), YubiKeyType.YK4)); - } - - @Test - public void testYubiKeyPreview() { - assertEquals( - "YubiKey Preview", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_A_KEYCHAIN); - i.version(new Version(5, 0, 0)); - i.supportedCapabilities(yk5UsbOnlyCapabilities); - }), YubiKeyType.YK4)); - - assertEquals( - "YubiKey Preview", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_A_KEYCHAIN); - i.version(new Version(5, 0, 10)); - i.supportedCapabilities(yk5UsbOnlyCapabilities); - }), YubiKeyType.YK4)); - - assertNotEquals( - "YubiKey Preview", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_A_KEYCHAIN); - i.version(new Version(5, 1, 0)); - i.supportedCapabilities(yk5UsbOnlyCapabilities); - }), YubiKeyType.YK4)); - - assertEquals( - "YubiKey Preview", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_A_KEYCHAIN); - i.version(new Version(5, 2, 2)); - i.supportedCapabilities(yk5UsbOnlyCapabilities); - }), YubiKeyType.YK4)); - - assertNotEquals( - "YubiKey Preview", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_A_KEYCHAIN); - i.version(new Version(5, 2, 3)); - i.supportedCapabilities(yk5UsbOnlyCapabilities); - }), YubiKeyType.YK4)); - - assertEquals( - "YubiKey Preview", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_A_KEYCHAIN); - i.version(new Version(5, 5, 1)); - i.supportedCapabilities(yk5UsbOnlyCapabilities); - }), YubiKeyType.YK4)); - - assertNotEquals( - "YubiKey Preview", - DeviceUtil.getName(info(i -> { - i.formFactor(USB_A_KEYCHAIN); - i.version(new Version(5, 5, 3)); - i.supportedCapabilities(yk5UsbOnlyCapabilities); - }), YubiKeyType.YK4)); - } - - @Test - public void testYubiKeyNeo() { - assertEquals( - "YubiKey NEO", - DeviceUtil.getName(info(i -> { - }), YubiKeyType.NEO)); - - // NEO always has a serial number - assertEquals( - "YubiKey NEO", - DeviceUtil.getName(info(i -> { - i.serialNumber(1234343); - i.version(new Version(3, 2, 0)); - }), null)); - } - - @Test - public void testLegacyKeys() { - assertEquals( - "YubiKey Standard", - DeviceUtil.getName(info(i -> { - }), YubiKeyType.YKS)); - - assertEquals( - "YubiKey Plus", - DeviceUtil.getName(info(i -> { - }), YubiKeyType.YKP)); - - assertEquals( - "YubiKey (0.3.2)", - DeviceUtil.getName(info(i -> { - i.version(new Version(0, 3, 2)); - }), YubiKeyType.YK4)); - - assertEquals( - "YubiKey", - DeviceUtil.getName(info(i -> { - i.version(new Version(3, 3, 2)); - }), YubiKeyType.YK4)); - } - - final static int fidoBits = Capability.FIDO2.bit | Capability.U2F.bit; - final static HashMap fidoCapabilities = new HashMap() {{ - put(Transport.USB, fidoBits); - put(Transport.NFC, fidoBits); - }}; - - final static HashMap bioCapabilities = new HashMap() {{ - put(Transport.USB, fidoBits); - }}; - - final static HashMap bioMultiProtocolCapabilities = new HashMap() {{ - put(Transport.USB, fidoBits | Capability.PIV.bit); - }}; - - final static HashMap yk5UsbOnlyCapabilities = new HashMap() {{ - put(Transport.USB, fidoBits | Capability.OATH.bit | Capability.PIV.bit | Capability.OPENPGP.bit | Capability.OTP.bit); - }}; - - final static HashMap yk5Capabilities = new HashMap() {{ - int capabilities = fidoBits | Capability.OATH.bit | Capability.PIV.bit | Capability.OPENPGP.bit | Capability.OTP.bit; - put(Transport.USB, capabilities); - put(Transport.NFC, capabilities); - }}; - - final static HashMap yk4Capabilities = new HashMap() {{ - int capabilities = Capability.U2F.bit | Capability.OATH.bit | Capability.PIV.bit | Capability.OPENPGP.bit | Capability.OTP.bit; - put(Transport.USB, capabilities); - put(Transport.NFC, capabilities); - }}; - - final static HashMap edgeCapabilities = new HashMap() {{ - int capabilities = Capability.U2F.bit | Capability.OTP.bit; - put(Transport.USB, capabilities); - }}; + @Test + public void testYubiKeyUnknownFormFactor() { + assertEquals( + "YubiKey 5", + DeviceUtil.getName( + info( + i -> { + i.formFactor(UNKNOWN); + i.version(new Version(5, 4, 3)); + i.supportedCapabilities(yk5UsbOnlyCapabilities); + }), + YubiKeyType.YK4)); + + assertEquals( + "YubiKey 5 NFC", + DeviceUtil.getName( + info( + i -> { + i.formFactor(UNKNOWN); + i.version(new Version(5, 4, 3)); + i.supportedCapabilities(yk5Capabilities); + }), + YubiKeyType.YK4)); + } + + @Test + public void testYubiKey5() { + assertEquals( + "YubiKey 5A", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_A_KEYCHAIN); + i.version(new Version(5, 4, 3)); + i.supportedCapabilities(yk5UsbOnlyCapabilities); + }), + YubiKeyType.YK4)); + + assertEquals( + "YubiKey 5C", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_C_KEYCHAIN); + i.version(new Version(5, 4, 3)); + i.supportedCapabilities(yk5UsbOnlyCapabilities); + }), + YubiKeyType.YK4)); + } + + @Test + public void testYubiKey5Nfc() { + assertEquals( + "YubiKey 5 NFC", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_A_KEYCHAIN); + i.version(new Version(5, 4, 3)); + i.supportedCapabilities(yk5Capabilities); + }), + YubiKeyType.YK4)); + + assertEquals( + "YubiKey 5C NFC", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_C_KEYCHAIN); + i.version(new Version(5, 4, 3)); + i.supportedCapabilities(yk5Capabilities); + }), + YubiKeyType.YK4)); + } + + @Test + public void testYubiKey5Nano() { + assertEquals( + "YubiKey 5 Nano", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_A_NANO); + i.version(new Version(5, 4, 3)); + i.supportedCapabilities(yk5UsbOnlyCapabilities); + }), + YubiKeyType.YK4)); + + assertEquals( + "YubiKey 5C Nano", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_C_NANO); + i.version(new Version(5, 4, 3)); + i.supportedCapabilities(yk5UsbOnlyCapabilities); + }), + YubiKeyType.YK4)); + } + + @Test + public void testYubiKey5Lightning() { + assertEquals( + "YubiKey 5Ci", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_C_LIGHTNING); + i.version(new Version(5, 4, 3)); + i.supportedCapabilities(yk5UsbOnlyCapabilities); + }), + YubiKeyType.YK4)); + } + + @Test + public void testSecurityKey() { + + assertEquals( + "FIDO U2F Security Key", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_A_KEYCHAIN); + i.supportedCapabilities( + new HashMap() { + { + put(Transport.USB, fidoBits); + } + }); + }), + YubiKeyType.SKY)); + + assertEquals( + "Security Key by Yubico", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_A_KEYCHAIN); + i.supportedCapabilities( + new HashMap() { + { + put(Transport.USB, Capability.U2F.bit); + } + }); + }), + YubiKeyType.SKY)); + + assertEquals( + "Security Key NFC", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_A_KEYCHAIN); + i.supportedCapabilities( + new HashMap() { + { + put(Transport.NFC, fidoBits); + } + }); + }), + YubiKeyType.SKY)); + + assertEquals( + "Security Key NFC", + DeviceUtil.getName( + info( + i -> { + i.isSky(true); + i.formFactor(USB_A_KEYCHAIN); + i.version(new Version(5, 6, 0)); + i.supportedCapabilities(fidoCapabilities); + }), + YubiKeyType.YK4)); + + assertEquals( + "Security Key C NFC", + DeviceUtil.getName( + info( + i -> { + i.isSky(true); + i.formFactor(USB_C_KEYCHAIN); + i.version(new Version(5, 6, 0)); + i.supportedCapabilities(fidoCapabilities); + }), + YubiKeyType.YK4)); + + assertEquals( + "Security Key NFC", + DeviceUtil.getName( + info( + i -> { + i.version(new Version(3, 2, 0)); + i.supportedCapabilities(fidoCapabilities); + }), + null)); + } + + @Test + public void testFips() { + + assertEquals( + "YubiKey 5 NFC FIPS", + DeviceUtil.getName( + info( + i -> { + i.isFips(true); + i.formFactor(USB_A_KEYCHAIN); + i.version(new Version(5, 6, 0)); + i.supportedCapabilities(yk5Capabilities); + }), + YubiKeyType.YK4)); + + assertEquals( + "YubiKey 5C NFC FIPS", + DeviceUtil.getName( + info( + i -> { + i.isFips(true); + i.formFactor(USB_C_KEYCHAIN); + i.version(new Version(5, 6, 0)); + i.supportedCapabilities(yk5Capabilities); + }), + YubiKeyType.YK4)); + + assertEquals( + "YubiKey 5A FIPS", + DeviceUtil.getName( + info( + i -> { + i.isFips(true); + i.formFactor(USB_A_KEYCHAIN); + i.version(new Version(5, 6, 0)); + i.supportedCapabilities(yk5UsbOnlyCapabilities); + }), + YubiKeyType.YK4)); + + assertEquals( + "YubiKey 5C FIPS", + DeviceUtil.getName( + info( + i -> { + i.isFips(true); + i.formFactor(USB_C_KEYCHAIN); + i.version(new Version(5, 6, 0)); + i.supportedCapabilities(yk5UsbOnlyCapabilities); + }), + YubiKeyType.YK4)); + } + + @Test + public void testYubiKey4Fips() { + assertEquals( + "YubiKey FIPS", + DeviceUtil.getName( + info( + i -> { + i.isFips(true); + i.formFactor(USB_A_KEYCHAIN); + i.version(new Version(4, 0, 0)); + i.supportedCapabilities(yk4Capabilities); + }), + YubiKeyType.YK4)); + } + + @Test + public void testYubiKeyEdge() { + assertEquals( + "YubiKey Edge", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_A_KEYCHAIN); + i.version(new Version(4, 0, 0)); + i.supportedCapabilities(edgeCapabilities); + }), + YubiKeyType.YK4)); + } + + @Test + public void testYubiKey4() { + assertEquals( + "YubiKey 4", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_A_KEYCHAIN); + i.version(new Version(4, 0, 0)); + i.supportedCapabilities(yk4Capabilities); + }), + YubiKeyType.YK4)); + + assertEquals( + "YubiKey 4", + DeviceUtil.getName( + info( + i -> { + i.version(new Version(4, 2, 0)); + i.supportedCapabilities(yk4Capabilities); + }), + null)); + } + + @Test + public void testBioSeriesFidoEdition() { + assertEquals( + "YubiKey Bio - FIDO Edition", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_A_BIO); + i.version(new Version(5, 6, 6)); + i.supportedCapabilities(bioCapabilities); + }), + YubiKeyType.YK4)); + + assertEquals( + "YubiKey C Bio - FIDO Edition", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_C_BIO); + i.version(new Version(5, 6, 6)); + i.supportedCapabilities(bioCapabilities); + }), + YubiKeyType.YK4)); + } + + @Test + public void testBioSeriesMultiProtocolEdition() { + // multi-protocol has PIV and a serial number + assertEquals( + "YubiKey Bio - Multi-protocol Edition", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_A_BIO); + i.version(new Version(5, 6, 6)); + i.supportedCapabilities(bioMultiProtocolCapabilities); + i.serialNumber(12345); + }), + YubiKeyType.YK4)); + + assertEquals( + "YubiKey C Bio - Multi-protocol Edition", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_C_BIO); + i.version(new Version(5, 6, 6)); + i.supportedCapabilities(bioMultiProtocolCapabilities); + i.serialNumber(12345); + }), + YubiKeyType.YK4)); + } + + @Test + public void testBioSeries() { + // these are neither Enterprise nor Multi-protocol Edition Bios + assertEquals( + "YubiKey Bio", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_A_BIO); + i.version(new Version(5, 6, 6)); + i.supportedCapabilities( + new HashMap() { + { + put(Transport.USB, fidoBits | UsbInterface.CCID); + } + }); + }), + YubiKeyType.YK4)); + + assertEquals( + "YubiKey C Bio", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_C_BIO); + i.version(new Version(5, 6, 6)); + i.supportedCapabilities( + new HashMap() { + { + put(Transport.USB, fidoBits | UsbInterface.CCID); + } + }); + }), + YubiKeyType.YK4)); + } + + @Test + public void testSecurityKeyEnterpriseEdition() { + assertEquals( + "Security Key NFC - Enterprise Edition", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_A_KEYCHAIN); + i.version(new Version(5, 4, 3)); + i.isSky(true); + i.supportedCapabilities(fidoCapabilities); + i.serialNumber(65454545); + }), + YubiKeyType.YK4)); + + assertEquals( + "Security Key C NFC - Enterprise Edition", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_C_KEYCHAIN); + i.version(new Version(5, 4, 3)); + i.isSky(true); + i.supportedCapabilities(fidoCapabilities); + i.serialNumber(65454545); + }), + YubiKeyType.YK4)); + } + + @Test + public void testYubiKeyPreview() { + assertEquals( + "YubiKey Preview", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_A_KEYCHAIN); + i.version(new Version(5, 0, 0)); + i.supportedCapabilities(yk5UsbOnlyCapabilities); + }), + YubiKeyType.YK4)); + + assertEquals( + "YubiKey Preview", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_A_KEYCHAIN); + i.version(new Version(5, 0, 10)); + i.supportedCapabilities(yk5UsbOnlyCapabilities); + }), + YubiKeyType.YK4)); + + assertNotEquals( + "YubiKey Preview", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_A_KEYCHAIN); + i.version(new Version(5, 1, 0)); + i.supportedCapabilities(yk5UsbOnlyCapabilities); + }), + YubiKeyType.YK4)); + + assertEquals( + "YubiKey Preview", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_A_KEYCHAIN); + i.version(new Version(5, 2, 2)); + i.supportedCapabilities(yk5UsbOnlyCapabilities); + }), + YubiKeyType.YK4)); + + assertNotEquals( + "YubiKey Preview", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_A_KEYCHAIN); + i.version(new Version(5, 2, 3)); + i.supportedCapabilities(yk5UsbOnlyCapabilities); + }), + YubiKeyType.YK4)); + + assertEquals( + "YubiKey Preview", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_A_KEYCHAIN); + i.version(new Version(5, 5, 1)); + i.supportedCapabilities(yk5UsbOnlyCapabilities); + }), + YubiKeyType.YK4)); + + assertNotEquals( + "YubiKey Preview", + DeviceUtil.getName( + info( + i -> { + i.formFactor(USB_A_KEYCHAIN); + i.version(new Version(5, 5, 3)); + i.supportedCapabilities(yk5UsbOnlyCapabilities); + }), + YubiKeyType.YK4)); + } + + @Test + public void testYubiKeyNeo() { + assertEquals("YubiKey NEO", DeviceUtil.getName(info(i -> {}), YubiKeyType.NEO)); + + // NEO always has a serial number + assertEquals( + "YubiKey NEO", + DeviceUtil.getName( + info( + i -> { + i.serialNumber(1234343); + i.version(new Version(3, 2, 0)); + }), + null)); + } + + @Test + public void testLegacyKeys() { + assertEquals("YubiKey Standard", DeviceUtil.getName(info(i -> {}), YubiKeyType.YKS)); + + assertEquals("YubiKey Plus", DeviceUtil.getName(info(i -> {}), YubiKeyType.YKP)); + + assertEquals( + "YubiKey (0.3.2)", + DeviceUtil.getName( + info( + i -> { + i.version(new Version(0, 3, 2)); + }), + YubiKeyType.YK4)); + + assertEquals( + "YubiKey", + DeviceUtil.getName( + info( + i -> { + i.version(new Version(3, 3, 2)); + }), + YubiKeyType.YK4)); + } + + static final int fidoBits = Capability.FIDO2.bit | Capability.U2F.bit; + static final HashMap fidoCapabilities = + new HashMap() { + { + put(Transport.USB, fidoBits); + put(Transport.NFC, fidoBits); + } + }; + + static final HashMap bioCapabilities = + new HashMap() { + { + put(Transport.USB, fidoBits); + } + }; + + static final HashMap bioMultiProtocolCapabilities = + new HashMap() { + { + put(Transport.USB, fidoBits | Capability.PIV.bit); + } + }; + + static final HashMap yk5UsbOnlyCapabilities = + new HashMap() { + { + put( + Transport.USB, + fidoBits + | Capability.OATH.bit + | Capability.PIV.bit + | Capability.OPENPGP.bit + | Capability.OTP.bit); + } + }; + + static final HashMap yk5Capabilities = + new HashMap() { + { + int capabilities = + fidoBits + | Capability.OATH.bit + | Capability.PIV.bit + | Capability.OPENPGP.bit + | Capability.OTP.bit; + put(Transport.USB, capabilities); + put(Transport.NFC, capabilities); + } + }; + + static final HashMap yk4Capabilities = + new HashMap() { + { + int capabilities = + Capability.U2F.bit + | Capability.OATH.bit + | Capability.PIV.bit + | Capability.OPENPGP.bit + | Capability.OTP.bit; + put(Transport.USB, capabilities); + put(Transport.NFC, capabilities); + } + }; + + static final HashMap edgeCapabilities = + new HashMap() { + { + int capabilities = Capability.U2F.bit | Capability.OTP.bit; + put(Transport.USB, capabilities); + } + }; } diff --git a/support/src/test/java/com/yubico/yubikit/support/TestUtil.java b/support/src/test/java/com/yubico/yubikit/support/TestUtil.java index 0cb8d8b7..a58bb080 100644 --- a/support/src/test/java/com/yubico/yubikit/support/TestUtil.java +++ b/support/src/test/java/com/yubico/yubikit/support/TestUtil.java @@ -20,23 +20,23 @@ import com.yubico.yubikit.management.DeviceInfo; class TestUtil { - interface DeviceConfigBuilder { - void createWith(DeviceConfig.Builder builder); - } + interface DeviceConfigBuilder { + void createWith(DeviceConfig.Builder builder); + } - static DeviceConfig config(DeviceConfigBuilder configBuilder) { - DeviceConfig.Builder builder = new DeviceConfig.Builder(); - configBuilder.createWith(builder); - return builder.build(); - } + static DeviceConfig config(DeviceConfigBuilder configBuilder) { + DeviceConfig.Builder builder = new DeviceConfig.Builder(); + configBuilder.createWith(builder); + return builder.build(); + } - interface DeviceInfoBuilder { - void createWith(DeviceInfo.Builder builder); - } + interface DeviceInfoBuilder { + void createWith(DeviceInfo.Builder builder); + } - static DeviceInfo info(DeviceInfoBuilder infoBuilder) { - DeviceInfo.Builder builder = new DeviceInfo.Builder(); - infoBuilder.createWith(builder); - return builder.build(); - } + static DeviceInfo info(DeviceInfoBuilder infoBuilder) { + DeviceInfo.Builder builder = new DeviceInfo.Builder(); + infoBuilder.createWith(builder); + return builder.build(); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/Codec.java b/testing/src/main/java/com/yubico/yubikit/testing/Codec.java index 6fc9eb05..bcecba42 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/Codec.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/Codec.java @@ -18,7 +18,7 @@ import org.bouncycastle.util.encoders.Hex; public class Codec { - public static byte[] fromHex(String hex) { - return Hex.decode(hex); - } + public static byte[] fromHex(String hex) { + return Hex.decode(hex); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/ScpParameters.java b/testing/src/main/java/com/yubico/yubikit/testing/ScpParameters.java index 9ac2f829..a690be06 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/ScpParameters.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/ScpParameters.java @@ -25,69 +25,63 @@ import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; import com.yubico.yubikit.core.smartcard.scp.ScpKid; import com.yubico.yubikit.core.smartcard.scp.SecurityDomainSession; - import java.io.IOException; import java.security.cert.X509Certificate; import java.util.List; import java.util.Map; - import javax.annotation.Nullable; public class ScpParameters { - @Nullable - private final Byte kid; - @Nullable - private ScpKeyParams keyParams = null; + @Nullable private final Byte kid; + @Nullable private ScpKeyParams keyParams = null; - public ScpParameters(YubiKeyDevice device, @Nullable Byte kid) { - this.kid = kid; - try { - keyParams = readScpKeyParams(device); - } catch (Throwable e) { - keyParams = null; - } + public ScpParameters(YubiKeyDevice device, @Nullable Byte kid) { + this.kid = kid; + try { + keyParams = readScpKeyParams(device); + } catch (Throwable e) { + keyParams = null; } + } - @Nullable - public Byte getKid() { - return kid; - } + @Nullable public Byte getKid() { + return kid; + } - @Nullable - public ScpKeyParams getKeyParams() { - return keyParams; - } + @Nullable public ScpKeyParams getKeyParams() { + return keyParams; + } - private ScpKeyParams readScpKeyParams(YubiKeyDevice device) throws Throwable { - if (kid == null) { - return null; - } - try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { - SecurityDomainSession scp = new SecurityDomainSession(connection); - KeyRef keyRef = getKeyRef(scp, kid); - if (keyRef == null) { - return null; - } - List certs = scp.getCertificateBundle(keyRef); + private ScpKeyParams readScpKeyParams(YubiKeyDevice device) throws Throwable { + if (kid == null) { + return null; + } + try (SmartCardConnection connection = device.openConnection(SmartCardConnection.class)) { + SecurityDomainSession scp = new SecurityDomainSession(connection); + KeyRef keyRef = getKeyRef(scp, kid); + if (keyRef == null) { + return null; + } + List certs = scp.getCertificateBundle(keyRef); - return certs.isEmpty() - ? null - : kid == ScpKid.SCP03 - ? null // TODO implement SCP03 support - : new Scp11KeyParams(keyRef, certs.get(certs.size() - 1).getPublicKey()); - } + return certs.isEmpty() + ? null + : kid == ScpKid.SCP03 + ? null // TODO implement SCP03 support + : new Scp11KeyParams(keyRef, certs.get(certs.size() - 1).getPublicKey()); } + } - private KeyRef getKeyRef(SecurityDomainSession scp, byte kid) - throws ApduException, IOException, BadResponseException { - Map> keyInformation = scp.getKeyInformation(); - KeyRef keyRef = null; - for (KeyRef info : keyInformation.keySet()) { - if (info.getKid() == kid) { - keyRef = info; - break; - } - } - return keyRef; + private KeyRef getKeyRef(SecurityDomainSession scp, byte kid) + throws ApduException, IOException, BadResponseException { + Map> keyInformation = scp.getKeyInformation(); + KeyRef keyRef = null; + for (KeyRef info : keyInformation.keySet()) { + if (info.getKid() == kid) { + keyRef = info; + break; + } } + return keyRef; + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/TestState.java b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java index 53a4843d..1aef8218 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/TestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/TestState.java @@ -29,170 +29,161 @@ import com.yubico.yubikit.management.DeviceInfo; import com.yubico.yubikit.management.ManagementSession; import com.yubico.yubikit.support.DeviceUtil; - -import org.bouncycastle.jce.provider.BouncyCastleProvider; - import java.io.IOException; import java.security.Security; - import javax.annotation.Nullable; +import org.bouncycastle.jce.provider.BouncyCastleProvider; public class TestState { - public abstract static class Builder> { - final protected YubiKeyDevice device; - @Nullable - final private UsbPid usbPid; - @Nullable - private Byte scpKid = null; - @Nullable - private ReconnectDeviceCallback reconnectDeviceCallback = null; - - public abstract T getThis(); - - public Builder(YubiKeyDevice device, @Nullable UsbPid usbPid) { - this.device = device; - this.usbPid = usbPid; - } - - public T scpKid(@Nullable Byte scpKid) { - this.scpKid = scpKid; - return getThis(); - } - - public T reconnectDeviceCallback(@Nullable ReconnectDeviceCallback reconnectDeviceCallback) { - this.reconnectDeviceCallback = reconnectDeviceCallback; - return getThis(); - } - - public abstract TestState build() throws Throwable; - } + public abstract static class Builder> { + protected final YubiKeyDevice device; + @Nullable private final UsbPid usbPid; + @Nullable private Byte scpKid = null; + @Nullable private ReconnectDeviceCallback reconnectDeviceCallback = null; - protected YubiKeyDevice currentDevice; - protected ScpParameters scpParameters; - @Nullable - public final UsbPid usbPid; - @Nullable - public final Byte scpKid; - @Nullable - private final ReconnectDeviceCallback reconnectDeviceCallback; - private final boolean isUsbTransport; - - protected TestState(Builder builder) { - this.currentDevice = builder.device; - this.usbPid = builder.usbPid; - this.scpKid = builder.scpKid; - this.scpParameters = new ScpParameters(builder.device, this.scpKid); - this.reconnectDeviceCallback = builder.reconnectDeviceCallback; - this.isUsbTransport = builder.device.getTransport() == Transport.USB; - - setupJca(); - } + public abstract T getThis(); - private void setupJca() { - Security.removeProvider("BC"); - Security.insertProviderAt(new BouncyCastleProvider(), 1); + public Builder(YubiKeyDevice device, @Nullable UsbPid usbPid) { + this.device = device; + this.usbPid = usbPid; } - public boolean isUsbTransport() { - return isUsbTransport; + public T scpKid(@Nullable Byte scpKid) { + this.scpKid = scpKid; + return getThis(); } - public interface StatefulDeviceCallback { - void invoke(S state) throws Throwable; + public T reconnectDeviceCallback(@Nullable ReconnectDeviceCallback reconnectDeviceCallback) { + this.reconnectDeviceCallback = reconnectDeviceCallback; + return getThis(); } - public interface SessionCallback> { - void invoke(T session) throws Throwable; - } + public abstract TestState build() throws Throwable; + } - public interface StatefulSessionCallback, S extends TestState> { - void invoke(T session, S state) throws Throwable; - } + protected YubiKeyDevice currentDevice; + protected ScpParameters scpParameters; + @Nullable public final UsbPid usbPid; + @Nullable public final Byte scpKid; + @Nullable private final ReconnectDeviceCallback reconnectDeviceCallback; + private final boolean isUsbTransport; - public interface SessionCallbackT, R> { - R invoke(T session) throws Throwable; - } + protected TestState(Builder builder) { + this.currentDevice = builder.device; + this.usbPid = builder.usbPid; + this.scpKid = builder.scpKid; + this.scpParameters = new ScpParameters(builder.device, this.scpKid); + this.reconnectDeviceCallback = builder.reconnectDeviceCallback; + this.isUsbTransport = builder.device.getTransport() == Transport.USB; - public interface ReconnectDeviceCallback { - YubiKeyDevice invoke(); - } + setupJca(); + } - protected void reconnect() { - if (reconnectDeviceCallback != null) { - currentDevice = reconnectDeviceCallback.invoke(); - scpParameters = new ScpParameters(currentDevice, scpKid); - } - } + private void setupJca() { + Security.removeProvider("BC"); + Security.insertProviderAt(new BouncyCastleProvider(), 1); + } - public boolean isFipsCapable(DeviceInfo deviceInfo, Capability capability) { - return deviceInfo != null && - (deviceInfo.getFipsCapable() & capability.bit) == capability.bit; - } + public boolean isUsbTransport() { + return isUsbTransport; + } - public boolean isFipsCapable(Capability capability) { - return isFipsCapable(getDeviceInfo(), capability); - } + public interface StatefulDeviceCallback { + void invoke(S state) throws Throwable; + } - public boolean isFipsApproved(Capability capability) { - return isFipsApproved(getDeviceInfo(), capability); - } + public interface SessionCallback> { + void invoke(T session) throws Throwable; + } - public boolean isFipsApproved(DeviceInfo deviceInfo, Capability capability) { - return deviceInfo != null && - (deviceInfo.getFipsApproved() & capability.bit) == capability.bit; - } + public interface StatefulSessionCallback, S extends TestState> { + void invoke(T session, S state) throws Throwable; + } - // connection helpers - protected SmartCardConnection openSmartCardConnection() throws IOException { - if (currentDevice.supportsConnection(SmartCardConnection.class)) { - return currentDevice.openConnection(SmartCardConnection.class); - } - return null; - } + public interface SessionCallbackT, R> { + R invoke(T session) throws Throwable; + } + + public interface ReconnectDeviceCallback { + YubiKeyDevice invoke(); + } - protected YubiKeyConnection openConnection() throws IOException { - if (currentDevice.supportsConnection(FidoConnection.class)) { - return currentDevice.openConnection(FidoConnection.class); - } - if (currentDevice.supportsConnection(SmartCardConnection.class)) { - return currentDevice.openConnection(SmartCardConnection.class); - } - throw new IllegalArgumentException("Device does not support FIDO or SmartCard connection"); + protected void reconnect() { + if (reconnectDeviceCallback != null) { + currentDevice = reconnectDeviceCallback.invoke(); + scpParameters = new ScpParameters(currentDevice, scpKid); } + } + + public boolean isFipsCapable(DeviceInfo deviceInfo, Capability capability) { + return deviceInfo != null && (deviceInfo.getFipsCapable() & capability.bit) == capability.bit; + } - // common utils - public DeviceInfo getDeviceInfo() { - DeviceInfo deviceInfo = null; - try (YubiKeyConnection connection = openConnection()) { - deviceInfo = DeviceUtil.readInfo(connection, usbPid); - } catch (IOException | UnsupportedOperationException ignoredException) { - } + public boolean isFipsCapable(Capability capability) { + return isFipsCapable(getDeviceInfo(), capability); + } - return deviceInfo; + public boolean isFipsApproved(Capability capability) { + return isFipsApproved(getDeviceInfo(), capability); + } + + public boolean isFipsApproved(DeviceInfo deviceInfo, Capability capability) { + return deviceInfo != null && (deviceInfo.getFipsApproved() & capability.bit) == capability.bit; + } + + // connection helpers + protected SmartCardConnection openSmartCardConnection() throws IOException { + if (currentDevice.supportsConnection(SmartCardConnection.class)) { + return currentDevice.openConnection(SmartCardConnection.class); } + return null; + } - protected ManagementSession getManagementSession(YubiKeyConnection connection, ScpParameters scpParameters) - throws IOException, CommandException { - ScpKeyParams keyParams = scpParameters != null ? scpParameters.getKeyParams() : null; - ManagementSession session = (connection instanceof FidoConnection) - ? new ManagementSession((FidoConnection) connection) - : connection instanceof SmartCardConnection + protected YubiKeyConnection openConnection() throws IOException { + if (currentDevice.supportsConnection(FidoConnection.class)) { + return currentDevice.openConnection(FidoConnection.class); + } + if (currentDevice.supportsConnection(SmartCardConnection.class)) { + return currentDevice.openConnection(SmartCardConnection.class); + } + throw new IllegalArgumentException("Device does not support FIDO or SmartCard connection"); + } + + // common utils + public DeviceInfo getDeviceInfo() { + DeviceInfo deviceInfo = null; + try (YubiKeyConnection connection = openConnection()) { + deviceInfo = DeviceUtil.readInfo(connection, usbPid); + } catch (IOException | UnsupportedOperationException ignoredException) { + } + + return deviceInfo; + } + + protected ManagementSession getManagementSession( + YubiKeyConnection connection, ScpParameters scpParameters) + throws IOException, CommandException { + ScpKeyParams keyParams = scpParameters != null ? scpParameters.getKeyParams() : null; + ManagementSession session = + (connection instanceof FidoConnection) + ? new ManagementSession((FidoConnection) connection) + : connection instanceof SmartCardConnection ? new ManagementSession((SmartCardConnection) connection, keyParams) : null; - if (session == null) { - throw new IllegalArgumentException("Connection does not support ManagementSession"); - } - - return session; + if (session == null) { + throw new IllegalArgumentException("Connection does not support ManagementSession"); } - protected boolean isMpe(DeviceInfo deviceInfo) { - if (deviceInfo == null) { - return false; - } - final String name = DeviceUtil.getName(deviceInfo, null); - return name.equals("YubiKey Bio - Multi-protocol Edition") || - name.equals("YubiKey C Bio - Multi-protocol Edition"); + return session; + } + + protected boolean isMpe(DeviceInfo deviceInfo) { + if (deviceInfo == null) { + return false; } + final String name = DeviceUtil.getName(deviceInfo, null); + return name.equals("YubiKey Bio - Multi-protocol Edition") + || name.equals("YubiKey C Bio - Multi-protocol Edition"); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientTests.java index 2dd66b7e..13d829da 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientTests.java @@ -53,9 +53,6 @@ import com.yubico.yubikit.fido.webauthn.UserVerificationRequirement; import com.yubico.yubikit.testing.fido.utils.ClientHelper; import com.yubico.yubikit.testing.fido.utils.CreationOptionsBuilder; - -import org.junit.Assert; - import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -66,785 +63,787 @@ import java.util.List; import java.util.Map; import java.util.Objects; - import javax.annotation.Nonnull; import javax.annotation.Nullable; +import org.junit.Assert; public class BasicWebAuthnClientTests { - public static void testMakeCredentialGetAssertionTokenUvOnly(FidoTestState state) throws Throwable { - state.withCtap2(session -> { - assumeTrue("UV Token not supported", ClientPin.isTokenSupported(session.getCachedInfo())); + public static void testMakeCredentialGetAssertionTokenUvOnly(FidoTestState state) + throws Throwable { + state.withCtap2( + session -> { + assumeTrue("UV Token not supported", ClientPin.isTokenSupported(session.getCachedInfo())); }); - testMakeCredentialGetAssertion(state); - } - - public static void testMakeCredentialGetAssertion(FidoTestState state) throws Throwable { - List deleteCredIds = new ArrayList<>(); - - // Make a non rk credential - state.withCtap2(session -> { - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - - PublicKeyCredentialCreationOptions creationOptionsNonRk = getCreateOptions( - new PublicKeyCredentialUserEntity( - "user", - "user".getBytes(StandardCharsets.UTF_8), - "User" - ), - false, - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), - null - ); - PublicKeyCredential credNonRk = webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - creationOptionsNonRk, - Objects.requireNonNull(creationOptionsNonRk.getRp().getId()), - TestData.PIN, - null, - null - ); - AuthenticatorAttestationResponse responseNonRk = (AuthenticatorAttestationResponse) credNonRk.getResponse(); - assertNotNull("Failed to make non resident key credential", responseNonRk); - assertNotNull("Credential missing attestation object", responseNonRk.getAttestationObject()); - assertNotNull("Credential missing client data JSON", responseNonRk.getClientDataJson()); + testMakeCredentialGetAssertion(state); + } + + public static void testMakeCredentialGetAssertion(FidoTestState state) throws Throwable { + List deleteCredIds = new ArrayList<>(); + + // Make a non rk credential + state.withCtap2( + session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + + PublicKeyCredentialCreationOptions creationOptionsNonRk = + getCreateOptions( + new PublicKeyCredentialUserEntity( + "user", "user".getBytes(StandardCharsets.UTF_8), "User"), + false, + Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), + null); + PublicKeyCredential credNonRk = + webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + creationOptionsNonRk, + Objects.requireNonNull(creationOptionsNonRk.getRp().getId()), + TestData.PIN, + null, + null); + AuthenticatorAttestationResponse responseNonRk = + (AuthenticatorAttestationResponse) credNonRk.getResponse(); + assertNotNull("Failed to make non resident key credential", responseNonRk); + assertNotNull( + "Credential missing attestation object", responseNonRk.getAttestationObject()); + assertNotNull("Credential missing client data JSON", responseNonRk.getClientDataJson()); }); - // make a rk credential - state.withCtap2(session -> { - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - PublicKeyCredentialCreationOptions creationOptionsRk = getCreateOptions( - new PublicKeyCredentialUserEntity( - "rkuser", - "rkuser".getBytes(StandardCharsets.UTF_8), - "RkUser" - ), - true, - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), - null); - PublicKeyCredential credRk = webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - creationOptionsRk, - Objects.requireNonNull(creationOptionsRk.getRp().getId()), - TestData.PIN, - null, - null - ); - AuthenticatorAttestationResponse responseRk = (AuthenticatorAttestationResponse) credRk.getResponse(); - assertNotNull("Failed to make resident key credential", responseRk); - assertNotNull("Credential missing attestation object", responseRk.getAttestationObject()); - assertNotNull("Credential missing client data JSON", responseRk.getClientDataJson()); - deleteCredIds.add((byte[]) parseCredentialData(getAuthenticatorDataFromAttestationResponse(responseRk)).get("credId")); + // make a rk credential + state.withCtap2( + session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredentialCreationOptions creationOptionsRk = + getCreateOptions( + new PublicKeyCredentialUserEntity( + "rkuser", "rkuser".getBytes(StandardCharsets.UTF_8), "RkUser"), + true, + Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), + null); + PublicKeyCredential credRk = + webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + creationOptionsRk, + Objects.requireNonNull(creationOptionsRk.getRp().getId()), + TestData.PIN, + null, + null); + AuthenticatorAttestationResponse responseRk = + (AuthenticatorAttestationResponse) credRk.getResponse(); + assertNotNull("Failed to make resident key credential", responseRk); + assertNotNull("Credential missing attestation object", responseRk.getAttestationObject()); + assertNotNull("Credential missing client data JSON", responseRk.getClientDataJson()); + deleteCredIds.add( + (byte[]) + parseCredentialData(getAuthenticatorDataFromAttestationResponse(responseRk)) + .get("credId")); }); - // Get assertions - state.withCtap2(session -> { - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - PublicKeyCredentialRequestOptions requestOptions = new PublicKeyCredentialRequestOptions( - TestData.CHALLENGE, - (long) 90000, + // Get assertions + state.withCtap2( + session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredentialRequestOptions requestOptions = + new PublicKeyCredentialRequestOptions( + TestData.CHALLENGE, (long) 90000, TestData.RP_ID, null, null, null); + + try { + PublicKeyCredential credential = + webauthn.getAssertion( + TestData.CLIENT_DATA_JSON_GET, + requestOptions, TestData.RP_ID, - null, - null, - null - ); - - try { - PublicKeyCredential credential = webauthn.getAssertion( - TestData.CLIENT_DATA_JSON_GET, - requestOptions, - TestData.RP_ID, - TestData.PIN, - null - ); - AuthenticatorAssertionResponse response = (AuthenticatorAssertionResponse) credential.getResponse(); - assertNotNull("Assertion response missing authenticator data", response.getAuthenticatorData()); - assertNotNull("Assertion response missing signature", response.getSignature()); - assertNotNull("Assertion response missing user handle", response.getUserHandle()); - } catch (MultipleAssertionsAvailable multipleAssertionsAvailable) { - fail("Got MultipleAssertionsAvailable even though there should only be one credential"); - } - - deleteCredentials(webauthn, deleteCredIds); + TestData.PIN, + null); + AuthenticatorAssertionResponse response = + (AuthenticatorAssertionResponse) credential.getResponse(); + assertNotNull( + "Assertion response missing authenticator data", response.getAuthenticatorData()); + assertNotNull("Assertion response missing signature", response.getSignature()); + assertNotNull("Assertion response missing user handle", response.getUserHandle()); + } catch (MultipleAssertionsAvailable multipleAssertionsAvailable) { + fail("Got MultipleAssertionsAvailable even though there should only be one credential"); + } + + deleteCredentials(webauthn, deleteCredIds); }); - } - - public static void testUvDiscouragedMcGa_withPin(FidoTestState state) throws Throwable { - state.withCtap2(session -> { - assumeTrue("Device has no PIN set", - Boolean.TRUE.equals(session.getCachedInfo().getOptions().get("clientPin"))); + } + + public static void testUvDiscouragedMcGa_withPin(FidoTestState state) throws Throwable { + state.withCtap2( + session -> { + assumeTrue( + "Device has no PIN set", + Boolean.TRUE.equals(session.getCachedInfo().getOptions().get("clientPin"))); }); - testUvDiscouragedMakeCredentialGetAssertion(state); - } - - public static void testUvDiscouragedMcGa_noPin(FidoTestState state) throws Throwable { - state.withCtap2(session -> { - assumeFalse("Device has PIN set. Reset and try again.", - Boolean.TRUE.equals(session.getCachedInfo().getOptions().get("clientPin"))); - assumeFalse("Ignoring FIPS approved devices", state.isFipsApproved()); + testUvDiscouragedMakeCredentialGetAssertion(state); + } + + public static void testUvDiscouragedMcGa_noPin(FidoTestState state) throws Throwable { + state.withCtap2( + session -> { + assumeFalse( + "Device has PIN set. Reset and try again.", + Boolean.TRUE.equals(session.getCachedInfo().getOptions().get("clientPin"))); + assumeFalse("Ignoring FIPS approved devices", state.isFipsApproved()); }); - testUvDiscouragedMakeCredentialGetAssertion(state); - } - - private static void testUvDiscouragedMakeCredentialGetAssertion(FidoTestState state) throws Throwable { - // Test non rk credential - PublicKeyCredential credNonRk = state.withCtap2(session -> { - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + testUvDiscouragedMakeCredentialGetAssertion(state); + } + + private static void testUvDiscouragedMakeCredentialGetAssertion(FidoTestState state) + throws Throwable { + // Test non rk credential + PublicKeyCredential credNonRk = + state.withCtap2( + session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + + PublicKeyCredentialCreationOptions creationOptionsNonRk = + getCreateOptions( + new PublicKeyCredentialUserEntity( + "user", "user".getBytes(StandardCharsets.UTF_8), "User"), + false, + Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), + null, + UserVerificationRequirement.DISCOURAGED); + PublicKeyCredential publicKeyCredential = + webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + creationOptionsNonRk, + Objects.requireNonNull(creationOptionsNonRk.getRp().getId()), + null, + null, + null); + + AuthenticatorAttestationResponse responseNonRk = + (AuthenticatorAttestationResponse) publicKeyCredential.getResponse(); + assertNotNull("Failed to make non resident key credential", responseNonRk); + assertNotNull( + "Credential missing attestation object", responseNonRk.getAttestationObject()); + assertNotNull( + "Credential missing client data JSON", responseNonRk.getClientDataJson()); + return publicKeyCredential; + }); - PublicKeyCredentialCreationOptions creationOptionsNonRk = getCreateOptions( - new PublicKeyCredentialUserEntity( - "user", - "user".getBytes(StandardCharsets.UTF_8), - "User" - ), - false, - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), - null, - UserVerificationRequirement.DISCOURAGED - ); - PublicKeyCredential publicKeyCredential = webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - creationOptionsNonRk, - Objects.requireNonNull(creationOptionsNonRk.getRp().getId()), - null, - null, - null - ); - - AuthenticatorAttestationResponse responseNonRk = (AuthenticatorAttestationResponse) publicKeyCredential.getResponse(); - assertNotNull("Failed to make non resident key credential", responseNonRk); - assertNotNull("Credential missing attestation object", responseNonRk.getAttestationObject()); - assertNotNull("Credential missing client data JSON", responseNonRk.getClientDataJson()); - return publicKeyCredential; + // Get assertions + state.withCtap2( + session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredentialRequestOptions requestOptionsNonRk = + new PublicKeyCredentialRequestOptions( + TestData.CHALLENGE, + (long) 90000, + TestData.RP_ID, + Collections.singletonList( + new PublicKeyCredentialDescriptor(credNonRk.getType(), credNonRk.getRawId())), + UserVerificationRequirement.DISCOURAGED, + null); + + try { + PublicKeyCredential credential = + webauthn.getAssertion( + TestData.CLIENT_DATA_JSON_GET, requestOptionsNonRk, TestData.RP_ID, null, null); + AuthenticatorAssertionResponse response = + (AuthenticatorAssertionResponse) credential.getResponse(); + assertNotNull( + "Assertion response missing authenticator data", response.getAuthenticatorData()); + assertNotNull("Assertion response missing signature", response.getSignature()); + // User identifiable information (name, DisplayName, icon) MUST NOT be returned if user + // verification is not done by the authenticator. + assertNull("Assertion response contains user handle", response.getUserHandle()); + } catch (MultipleAssertionsAvailable multipleAssertionsAvailable) { + fail("Got MultipleAssertionsAvailable even though there should only be one credential"); + } }); - // Get assertions - state.withCtap2(session -> { - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - PublicKeyCredentialRequestOptions requestOptionsNonRk = new PublicKeyCredentialRequestOptions( - TestData.CHALLENGE, - (long) 90000, - TestData.RP_ID, - Collections.singletonList(new PublicKeyCredentialDescriptor(credNonRk.getType(), credNonRk.getRawId())), - UserVerificationRequirement.DISCOURAGED, - null - ); + // test rk credential + PublicKeyCredential credRk = + state.withCtap2( + session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredentialCreationOptions creationOptionsRk = + getCreateOptions( + new PublicKeyCredentialUserEntity( + "rkuser", "rkuser".getBytes(StandardCharsets.UTF_8), "RkUser"), + true, + Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), + null, + UserVerificationRequirement.DISCOURAGED); + PublicKeyCredential publicKeyCredential = + webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + creationOptionsRk, + Objects.requireNonNull(creationOptionsRk.getRp().getId()), + null, + null, + null); + + AuthenticatorAttestationResponse responseRk = + (AuthenticatorAttestationResponse) publicKeyCredential.getResponse(); + assertNotNull("Failed to make non resident key credential", responseRk); + assertNotNull( + "Credential missing attestation object", responseRk.getAttestationObject()); + assertNotNull("Credential missing client data JSON", responseRk.getClientDataJson()); + return publicKeyCredential; + }); - try { - PublicKeyCredential credential = webauthn.getAssertion( - TestData.CLIENT_DATA_JSON_GET, - requestOptionsNonRk, - TestData.RP_ID, - null, - null - ); - AuthenticatorAssertionResponse response = (AuthenticatorAssertionResponse) credential.getResponse(); - assertNotNull("Assertion response missing authenticator data", response.getAuthenticatorData()); - assertNotNull("Assertion response missing signature", response.getSignature()); - // User identifiable information (name, DisplayName, icon) MUST NOT be returned if user verification is not done by the authenticator. - assertNull("Assertion response contains user handle", response.getUserHandle()); - } catch (MultipleAssertionsAvailable multipleAssertionsAvailable) { - fail("Got MultipleAssertionsAvailable even though there should only be one credential"); - } + // Get assertions + state.withCtap2( + session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredentialRequestOptions requestOptionsRk = + new PublicKeyCredentialRequestOptions( + TestData.CHALLENGE, + (long) 90000, + TestData.RP_ID, + Collections.singletonList( + new PublicKeyCredentialDescriptor(credRk.getType(), credRk.getRawId())), + UserVerificationRequirement.DISCOURAGED, + null); + + try { + PublicKeyCredential credential = + webauthn.getAssertion( + TestData.CLIENT_DATA_JSON_GET, requestOptionsRk, TestData.RP_ID, null, null); + AuthenticatorAssertionResponse response = + (AuthenticatorAssertionResponse) credential.getResponse(); + assertNotNull( + "Assertion response missing authenticator data", response.getAuthenticatorData()); + assertNotNull("Assertion response missing signature", response.getSignature()); + assertNotNull("Assertion response missing user handle", response.getUserHandle()); + } catch (MultipleAssertionsAvailable multipleAssertionsAvailable) { + fail("Got MultipleAssertionsAvailable even though there should only be one credential"); + } }); + } + + public static void testGetAssertionMultipleUsersRk(FidoTestState state) throws Throwable { + List deleteCredIds = new ArrayList<>(); + Map userIdCredIdMap = new HashMap<>(); - // test rk credential - PublicKeyCredential credRk = state.withCtap2(session -> { + // make 3 rk credential + for (int i = 0; i < 3; i++) { + final int userIndex = i; + state.withCtap2( + session -> { BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - PublicKeyCredentialCreationOptions creationOptionsRk = getCreateOptions( - new PublicKeyCredentialUserEntity( - "rkuser", - "rkuser".getBytes(StandardCharsets.UTF_8), - "RkUser" - ), + PublicKeyCredentialUserEntity user = + new PublicKeyCredentialUserEntity( + "user" + userIndex, + ("user" + userIndex).getBytes(StandardCharsets.UTF_8), + "User" + userIndex); + PublicKeyCredentialCreationOptions creationOptions = + getCreateOptions( + user, true, Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), - null, - UserVerificationRequirement.DISCOURAGED - ); - PublicKeyCredential publicKeyCredential = webauthn.makeCredential( + null); + PublicKeyCredential credential = + webauthn.makeCredential( TestData.CLIENT_DATA_JSON_CREATE, - creationOptionsRk, - Objects.requireNonNull(creationOptionsRk.getRp().getId()), - null, + creationOptions, + Objects.requireNonNull(creationOptions.getRp().getId()), + TestData.PIN, null, - null - ); - - AuthenticatorAttestationResponse responseRk = (AuthenticatorAttestationResponse) publicKeyCredential.getResponse(); - assertNotNull("Failed to make non resident key credential", responseRk); - assertNotNull("Credential missing attestation object", responseRk.getAttestationObject()); - assertNotNull("Credential missing client data JSON", responseRk.getClientDataJson()); - return publicKeyCredential; - }); - - // Get assertions - state.withCtap2(session -> { - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - PublicKeyCredentialRequestOptions requestOptionsRk = new PublicKeyCredentialRequestOptions( - TestData.CHALLENGE, - (long) 90000, - TestData.RP_ID, - Collections.singletonList(new PublicKeyCredentialDescriptor(credRk.getType(), credRk.getRawId())), - UserVerificationRequirement.DISCOURAGED, - null - ); - - try { - PublicKeyCredential credential = webauthn.getAssertion( - TestData.CLIENT_DATA_JSON_GET, - requestOptionsRk, - TestData.RP_ID, - null, - null - ); - AuthenticatorAssertionResponse response = (AuthenticatorAssertionResponse) credential.getResponse(); - assertNotNull("Assertion response missing authenticator data", response.getAuthenticatorData()); - assertNotNull("Assertion response missing signature", response.getSignature()); - assertNotNull("Assertion response missing user handle", response.getUserHandle()); - } catch (MultipleAssertionsAvailable multipleAssertionsAvailable) { - fail("Got MultipleAssertionsAvailable even though there should only be one credential"); - } - }); + null); + AuthenticatorAttestationResponse response = + (AuthenticatorAttestationResponse) credential.getResponse(); + byte[] credId = + (byte[]) + parseCredentialData(getAuthenticatorDataFromAttestationResponse(response)) + .get("credId"); + userIdCredIdMap.put(user.getId(), credId); + deleteCredIds.add(credId); + }); } - public static void testGetAssertionMultipleUsersRk(FidoTestState state) throws Throwable { - List deleteCredIds = new ArrayList<>(); - Map userIdCredIdMap = new HashMap<>(); - - // make 3 rk credential - for (int i = 0; i < 3; i++) { - final int userIndex = i; - state.withCtap2(session -> { - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - PublicKeyCredentialUserEntity user = new PublicKeyCredentialUserEntity( - "user" + userIndex, - ("user" + userIndex).getBytes(StandardCharsets.UTF_8), - "User" + userIndex - ); - PublicKeyCredentialCreationOptions creationOptions = getCreateOptions( - user, - true, - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), - null - ); - PublicKeyCredential credential = webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - creationOptions, - Objects.requireNonNull(creationOptions.getRp().getId()), - TestData.PIN, - null, - null - ); - AuthenticatorAttestationResponse response = (AuthenticatorAttestationResponse) credential.getResponse(); - byte[] credId = (byte[]) parseCredentialData(getAuthenticatorDataFromAttestationResponse(response)).get("credId"); - userIdCredIdMap.put(user.getId(), credId); - deleteCredIds.add(credId); - }); - } - - // Get assertions - PublicKeyCredentialRequestOptions requestOptions = new PublicKeyCredentialRequestOptions( - TestData.CHALLENGE, - (long) 90000, - TestData.RP_ID, - null, - null, - null - ); - - for (int i = 0; i < 3; i++) { - final int userIndex = i; - state.withCtap2(session -> { - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - try { - webauthn.getAssertion( - TestData.CLIENT_DATA_JSON_GET, - requestOptions, - TestData.RP_ID, - TestData.PIN, - null - ); - fail("Got single assertion even though multiple credentials exist"); - } catch (MultipleAssertionsAvailable multipleAssertionsAvailable) { - List users = multipleAssertionsAvailable.getUsers(); - assertNotNull("Assertion failed to return user list", users); - assertTrue("There should be at least 3 users found", users.size() >= 3); - PublicKeyCredentialUserEntity user = users.get(userIndex); - assertNotNull(user.getId()); - assertNotNull(user.getName()); - assertNotNull(user.getDisplayName()); - if (userIdCredIdMap.containsKey(user.getId())) { - PublicKeyCredential credential = multipleAssertionsAvailable.select(userIndex); - AuthenticatorAssertionResponse assertion = (AuthenticatorAssertionResponse) credential.getResponse(); - assertNotNull("Failed to get assertion", assertion); - assertNotNull("Assertion response missing authenticator data", assertion.getAuthenticatorData()); - assertNotNull("Assertion response missing signature", assertion.getSignature()); - assertNotNull("Assertion response missing user handle", assertion.getUserHandle()); - assertArrayEquals(userIdCredIdMap.get(users.get(userIndex) - .getId()), credential.getRawId()); - } - } - }); - } + // Get assertions + PublicKeyCredentialRequestOptions requestOptions = + new PublicKeyCredentialRequestOptions( + TestData.CHALLENGE, (long) 90000, TestData.RP_ID, null, null, null); - state.withCtap2(session -> { + for (int i = 0; i < 3; i++) { + final int userIndex = i; + state.withCtap2( + session -> { BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - if (CredentialManagement.isSupported(session.getCachedInfo())) { - deleteCredentials(webauthn, deleteCredIds); + try { + webauthn.getAssertion( + TestData.CLIENT_DATA_JSON_GET, + requestOptions, + TestData.RP_ID, + TestData.PIN, + null); + fail("Got single assertion even though multiple credentials exist"); + } catch (MultipleAssertionsAvailable multipleAssertionsAvailable) { + List users = multipleAssertionsAvailable.getUsers(); + assertNotNull("Assertion failed to return user list", users); + assertTrue("There should be at least 3 users found", users.size() >= 3); + PublicKeyCredentialUserEntity user = users.get(userIndex); + assertNotNull(user.getId()); + assertNotNull(user.getName()); + assertNotNull(user.getDisplayName()); + if (userIdCredIdMap.containsKey(user.getId())) { + PublicKeyCredential credential = multipleAssertionsAvailable.select(userIndex); + AuthenticatorAssertionResponse assertion = + (AuthenticatorAssertionResponse) credential.getResponse(); + assertNotNull("Failed to get assertion", assertion); + assertNotNull( + "Assertion response missing authenticator data", + assertion.getAuthenticatorData()); + assertNotNull("Assertion response missing signature", assertion.getSignature()); + assertNotNull("Assertion response missing user handle", assertion.getUserHandle()); + assertArrayEquals( + userIdCredIdMap.get(users.get(userIndex).getId()), credential.getRawId()); + } } - }); + }); } - public static void testGetAssertionWithAllowList(FidoTestState state) throws Throwable { - - PublicKeyCredential cred1 = state.withCtap2(session -> { - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - - // Make 2 new credentials - PublicKeyCredentialCreationOptions options = getCreateOptions( - new PublicKeyCredentialUserEntity( - "user1", - "user1".getBytes(StandardCharsets.UTF_8), - "testUser1" - ), - false, - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), - null - ); - - - return webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - options, - Objects.requireNonNull(TestData.RP.getId()), - TestData.PIN, - null, - null - ); + state.withCtap2( + session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + if (CredentialManagement.isSupported(session.getCachedInfo())) { + deleteCredentials(webauthn, deleteCredIds); + } }); + } + + public static void testGetAssertionWithAllowList(FidoTestState state) throws Throwable { + + PublicKeyCredential cred1 = + state.withCtap2( + session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + + // Make 2 new credentials + PublicKeyCredentialCreationOptions options = + getCreateOptions( + new PublicKeyCredentialUserEntity( + "user1", "user1".getBytes(StandardCharsets.UTF_8), "testUser1"), + false, + Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), + null); + + return webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + options, + Objects.requireNonNull(TestData.RP.getId()), + TestData.PIN, + null, + null); + }); - PublicKeyCredential cred2 = state.withCtap2(session -> { - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - - PublicKeyCredentialCreationOptions options = getCreateOptions( - new PublicKeyCredentialUserEntity( - "user2", - "user2".getBytes(StandardCharsets.UTF_8), - "testUser2" - ), - false, - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), - null - ); + PublicKeyCredential cred2 = + state.withCtap2( + session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + + PublicKeyCredentialCreationOptions options = + getCreateOptions( + new PublicKeyCredentialUserEntity( + "user2", "user2".getBytes(StandardCharsets.UTF_8), "testUser2"), + false, + Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), + null); + + return webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + options, + Objects.requireNonNull(TestData.RP.getId()), + TestData.PIN, + null, + null); + }); - return webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - options, - Objects.requireNonNull(TestData.RP.getId()), - TestData.PIN, - null, - null - ); + state.withCtap2( + session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + + // GetAssertions with allowList containing only credId1 + List allowCreds = + Collections.singletonList( + new PublicKeyCredentialDescriptor( + PublicKeyCredentialType.PUBLIC_KEY, cred1.getRawId(), null)); + PublicKeyCredentialRequestOptions requestOptions = + new PublicKeyCredentialRequestOptions( + TestData.CHALLENGE, (long) 90000, TestData.RP_ID, allowCreds, null, null); + + PublicKeyCredential credential = + webauthn.getAssertion( + TestData.CLIENT_DATA_JSON_GET, + requestOptions, + TestData.RP_ID, + TestData.PIN, + null); + assertArrayEquals(cred1.getRawId(), credential.getRawId()); }); - state.withCtap2(session -> { - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - - // GetAssertions with allowList containing only credId1 - List allowCreds = Collections.singletonList( - new PublicKeyCredentialDescriptor( - PublicKeyCredentialType.PUBLIC_KEY, - cred1.getRawId(), - null - ) - ); - PublicKeyCredentialRequestOptions requestOptions = new PublicKeyCredentialRequestOptions( - TestData.CHALLENGE, - (long) 90000, - TestData.RP_ID, - allowCreds, - null, - null - ); - - PublicKeyCredential credential = webauthn.getAssertion( - TestData.CLIENT_DATA_JSON_GET, - requestOptions, - TestData.RP_ID, - TestData.PIN, - null - ); - assertArrayEquals(cred1.getRawId(), credential.getRawId()); - + state.withCtap2( + session -> { + // GetAssertions with allowList containing only credId2 + List allowCreds = + Collections.singletonList( + new PublicKeyCredentialDescriptor( + PublicKeyCredentialType.PUBLIC_KEY, cred2.getRawId(), null)); + PublicKeyCredentialRequestOptions requestOptions = + new PublicKeyCredentialRequestOptions( + TestData.CHALLENGE, (long) 90000, TestData.RP_ID, allowCreds, null, null); + + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredential credential = + webauthn.getAssertion( + TestData.CLIENT_DATA_JSON_GET, + requestOptions, + TestData.RP_ID, + TestData.PIN, + null); + assertArrayEquals(cred2.getRawId(), credential.getRawId()); }); + } + + public static void testMakeCredentialWithExcludeList(FidoTestState state) throws Throwable { + + // non-discoverable + { + PublicKeyCredential cred = + state.withCtap2( + session -> { + return new ClientHelper(session) + .makeCredential(new CreationOptionsBuilder().build()); + }); + + // Make another non RK credential with exclude list including credId. Should fail + state.withCtap2( + session -> { + try { + new ClientHelper(session) + .makeCredential(new CreationOptionsBuilder().excludeCredentials(cred).build()); + fail("Succeeded in making credential even though the credential was excluded"); + } catch (ClientError clientError) { + assertEquals(ClientError.Code.DEVICE_INELIGIBLE, clientError.getErrorCode()); + } + }); - state.withCtap2(session -> { - // GetAssertions with allowList containing only credId2 - List allowCreds = Collections.singletonList( - new PublicKeyCredentialDescriptor(PublicKeyCredentialType.PUBLIC_KEY, cred2.getRawId(), null)); - PublicKeyCredentialRequestOptions requestOptions = new PublicKeyCredentialRequestOptions( - TestData.CHALLENGE, (long) 90000, TestData.RP_ID, allowCreds, null, null); - - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - PublicKeyCredential credential = webauthn.getAssertion( - TestData.CLIENT_DATA_JSON_GET, - requestOptions, - TestData.RP_ID, - TestData.PIN, - null - ); - assertArrayEquals(cred2.getRawId(), credential.getRawId()); - }); + // Make another non RK credential with exclude list null. Should succeed + state.withCtap2( + session -> { + return new ClientHelper(session).makeCredential(new CreationOptionsBuilder().build()); + }); } - public static void testMakeCredentialWithExcludeList(FidoTestState state) throws Throwable { - - // non-discoverable - { - PublicKeyCredential cred = state.withCtap2(session -> { - return new ClientHelper(session).makeCredential( - new CreationOptionsBuilder().build() - ); - }); - - // Make another non RK credential with exclude list including credId. Should fail - state.withCtap2(session -> { - try { - new ClientHelper(session).makeCredential( - new CreationOptionsBuilder() - .excludeCredentials(cred) - .build() - ); - fail("Succeeded in making credential even though the credential was excluded"); - } catch (ClientError clientError) { - assertEquals(ClientError.Code.DEVICE_INELIGIBLE, clientError.getErrorCode()); - } + // discoverable + { + List creds = new ArrayList<>(); + for (int index = 0; index < 17; index++) { + final int i = index; + state.withCtap2( + session -> { + creds.add( + new ClientHelper(session) + .makeCredential( + new CreationOptionsBuilder() + .userEntity("User " + i) + .residentKey(true) + .build())); }); + } - - // Make another non RK credential with exclude list null. Should succeed - state.withCtap2(session -> { - return new ClientHelper(session).makeCredential( - new CreationOptionsBuilder().build() - ); - }); - } - - // discoverable - { - List creds = new ArrayList<>(); - for (int index = 0; index < 17; index++) { - final int i = index; - state.withCtap2(session -> { - creds.add(new ClientHelper(session).makeCredential( - new CreationOptionsBuilder() - .userEntity("User " + i) - .residentKey(true) - .build() - )); - }); + // Make another non RK credential with exclude list including credId. Should fail + state.withCtap2( + session -> { + try { + new ClientHelper(session) + .makeCredential( + new CreationOptionsBuilder() + .userEntity("Not allowed user") + .residentKey(true) + .excludeCredentials(creds) + .build()); + fail("Succeeded in making credential even though the credential was excluded"); + } catch (ClientError clientError) { + assertEquals(ClientError.Code.DEVICE_INELIGIBLE, clientError.getErrorCode()); } - - // Make another non RK credential with exclude list including credId. Should fail - state.withCtap2(session -> { - try { - new ClientHelper(session).makeCredential( - new CreationOptionsBuilder() - .userEntity("Not allowed user") - .residentKey(true) - .excludeCredentials(creds) - .build() - ); - fail("Succeeded in making credential even though the credential was excluded"); - } catch (ClientError clientError) { - assertEquals(ClientError.Code.DEVICE_INELIGIBLE, clientError.getErrorCode()); - } - }); - - - // Make another non RK credential with exclude list null. Should succeed - state.withCtap2(session -> { - creds.add(new ClientHelper(session).makeCredential( + }); + + // Make another non RK credential with exclude list null. Should succeed + state.withCtap2( + session -> { + creds.add( + new ClientHelper(session) + .makeCredential( new CreationOptionsBuilder() - .userEntity("User3") - .residentKey(true) - .build() - )); - }); - - // remove credentials - state.withCtap2(session -> { - ClientHelper clientHelper = new ClientHelper(session); - clientHelper.deleteCredentials(creds); - }); - } - + .userEntity("User3") + .residentKey(true) + .build())); + }); + + // remove credentials + state.withCtap2( + session -> { + ClientHelper clientHelper = new ClientHelper(session); + clientHelper.deleteCredentials(creds); + }); } + } - public static void testMakeCredentialKeyAlgorithms(FidoTestState state) throws Throwable { - - List allCredParams = Arrays.asList( - TestData.PUB_KEY_CRED_PARAMS_ES256, - TestData.PUB_KEY_CRED_PARAMS_EDDSA); - - // Test individual algorithms - for (PublicKeyCredentialParameters param : allCredParams) { - state.withCtap2(session -> { - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - - PublicKeyCredentialCreationOptions creationOptions = getCreateOptions( - null, false, Collections.singletonList(param), null); - PublicKeyCredential credential = webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - creationOptions, - Objects.requireNonNull(creationOptions.getRp().getId()), - TestData.PIN, - null, - null - ); - AuthenticatorAttestationResponse attestation = (AuthenticatorAttestationResponse) credential.getResponse(); - int alg = (Integer) Objects.requireNonNull( - parseCredentialData( - getAuthenticatorDataFromAttestationResponse(attestation) - ).get("keyAlgo") - ); - assertEquals(param.getAlg(), alg); - }); - } + public static void testMakeCredentialKeyAlgorithms(FidoTestState state) throws Throwable { - state.withCtap2(session -> { - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - // Test algorithm order: ES256 - EdDSA - List credParams = Arrays.asList( - allCredParams.get(0), - allCredParams.get(1)); - PublicKeyCredentialCreationOptions creationOptions = getCreateOptions( - null, - false, - credParams, - null - ); - PublicKeyCredential credential = webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - creationOptions, - Objects.requireNonNull(creationOptions.getRp().getId()), - TestData.PIN, - null, - null - ); - AuthenticatorAttestationResponse attestation = (AuthenticatorAttestationResponse) credential.getResponse(); - int alg = (Integer) Objects.requireNonNull( - parseCredentialData( - getAuthenticatorDataFromAttestationResponse(attestation) - ).get("keyAlgo") - ); - assertEquals(credParams.get(0).getAlg(), alg); - }); + List allCredParams = + Arrays.asList(TestData.PUB_KEY_CRED_PARAMS_ES256, TestData.PUB_KEY_CRED_PARAMS_EDDSA); - state.withCtap2(session -> { + // Test individual algorithms + for (PublicKeyCredentialParameters param : allCredParams) { + state.withCtap2( + session -> { BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - // Test algorithm order: ALG_EdDSA - ALG_ES256 - List credParams = Arrays.asList( - allCredParams.get(1), - allCredParams.get(0)); - PublicKeyCredentialCreationOptions creationOptions = getCreateOptions(null, false, credParams, null); - PublicKeyCredential credential = webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - creationOptions, - Objects.requireNonNull(creationOptions.getRp().getId()), - TestData.PIN, - null, - null - ); - AuthenticatorAttestationResponse attestation = (AuthenticatorAttestationResponse) credential.getResponse(); - int alg = (Integer) Objects.requireNonNull( - parseCredentialData( - getAuthenticatorDataFromAttestationResponse(attestation) - ).get("keyAlgo") - ); - assertEquals(credParams.get(0).getAlg(), alg); - }); - } - public static void testClientPinManagement(FidoTestState state) throws Throwable { - state.withCtap2(session -> { - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - assumeTrue("Pin not supported", webauthn.isPinSupported()); - assertTrue(webauthn.isPinConfigured()); - - webauthn.changePin(TestData.PIN, TestData.OTHER_PIN); - - try { - webauthn.changePin(TestData.PIN, TestData.OTHER_PIN); - fail("Wrong PIN was accepted"); - } catch (ClientError e) { - assertThat(e.getErrorCode(), equalTo(ClientError.Code.BAD_REQUEST)); - assertThat(e.getCause(), instanceOf(CtapException.class)); - assertThat(((CtapException) Objects.requireNonNull(e.getCause())).getCtapError(), - is(CtapException.ERR_PIN_INVALID)); - } - - webauthn.changePin(TestData.OTHER_PIN, TestData.PIN); - }); - } - - public static void testClientCredentialManagement(FidoTestState state) throws Throwable { - state.withCtap2(session -> { - assumeTrue("Credential management not supported", - CredentialManagement.isSupported(session.getCachedInfo())); - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - PublicKeyCredentialCreationOptions creationOptions = getCreateOptions(null, true, - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), - null); - webauthn.makeCredential( + PublicKeyCredentialCreationOptions creationOptions = + getCreateOptions(null, false, Collections.singletonList(param), null); + PublicKeyCredential credential = + webauthn.makeCredential( TestData.CLIENT_DATA_JSON_CREATE, creationOptions, Objects.requireNonNull(creationOptions.getRp().getId()), TestData.PIN, null, null); - - CredentialManager credentialManager = webauthn.getCredentialManager(TestData.PIN); - - assertThat(credentialManager.getCredentialCount(), equalTo(1)); - - List rpIds = credentialManager.getRpIdList(); - assertThat(rpIds, equalTo(Collections.singletonList(TestData.RP_ID))); - - Map credentials = credentialManager.getCredentials(TestData.RP_ID); - assertThat(credentials.size(), equalTo(1)); - PublicKeyCredentialDescriptor key = credentials.entrySet().iterator().next().getKey(); - assertThat(Objects.requireNonNull(credentials.get(key)) - .getId(), equalTo(TestData.USER_ID)); - - try { - PublicKeyCredentialUserEntity updatedUser = new PublicKeyCredentialUserEntity( - "New name", credentials.get(key).getId(), "New display name" - ); - credentialManager.updateUserInformation(key, updatedUser); - - // verify new information - Map updatedCreds = - credentialManager.getCredentials(TestData.RP_ID); - assertThat(updatedCreds.size(), equalTo(1)); - PublicKeyCredentialDescriptor updatedKey = updatedCreds.keySet().iterator().next(); - PublicKeyCredentialUserEntity updatedUserEntity = Objects.requireNonNull(updatedCreds.get(updatedKey)); - assertThat(updatedUserEntity.getId(), equalTo(TestData.USER_ID)); - assertThat(updatedUserEntity.getName(), equalTo("New name")); - assertThat(updatedUserEntity.getDisplayName(), equalTo("New display name")); - } catch (UnsupportedOperationException unsupportedOperationException) { - // ignored - } - - credentialManager.deleteCredential(key); - assertThat(credentialManager.getCredentialCount(), equalTo(0)); - assertTrue(credentialManager.getCredentials(TestData.RP_ID).isEmpty()); - assertTrue(credentialManager.getRpIdList().isEmpty()); - }); - } - - private static PublicKeyCredentialCreationOptions getCreateOptions( - @Nullable PublicKeyCredentialUserEntity user, - boolean rk, - List credParams, - @Nullable List excludeCredentials - ) { - return getCreateOptions( - user, - rk, - credParams, - excludeCredentials, - null); - } - - private static PublicKeyCredentialCreationOptions getCreateOptions( - @Nullable PublicKeyCredentialUserEntity user, - boolean rk, - List credParams, - @Nullable List excludeCredentials, - @Nullable String userVerification - ) { - if (user == null) { - user = TestData.USER; - } - PublicKeyCredentialRpEntity rp = TestData.RP; - AuthenticatorSelectionCriteria criteria = new AuthenticatorSelectionCriteria( - null, - rk - ? ResidentKeyRequirement.REQUIRED - : ResidentKeyRequirement.DISCOURAGED, - userVerification - ); - return new PublicKeyCredentialCreationOptions( - rp, - user, - TestData.CHALLENGE, - credParams, - (long) 90000, - excludeCredentials, - criteria, - null, - null - ); - } - - private static byte[] getAuthenticatorDataFromAttestationResponse(AuthenticatorAttestationResponse response) { - byte[] attestObjBytes = response.getAttestationObject(); - @SuppressWarnings("unchecked") - Map attestObj = (Map) Cbor.decode(attestObjBytes); - Assert.assertNotNull(attestObj); - return (byte[]) attestObj.get("authData"); + AuthenticatorAttestationResponse attestation = + (AuthenticatorAttestationResponse) credential.getResponse(); + int alg = + (Integer) + Objects.requireNonNull( + parseCredentialData( + getAuthenticatorDataFromAttestationResponse(attestation)) + .get("keyAlgo")); + assertEquals(param.getAlg(), alg); + }); } - private static Map parseCredentialData(final byte[] data) { - ByteBuffer bb = ByteBuffer.wrap(data); - byte[] rpIdHash = new byte[32]; - bb.get(rpIdHash); - - byte flags = bb.get(); - - int signCount = bb.getInt(); + state.withCtap2( + session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + // Test algorithm order: ES256 - EdDSA + List credParams = + Arrays.asList(allCredParams.get(0), allCredParams.get(1)); + PublicKeyCredentialCreationOptions creationOptions = + getCreateOptions(null, false, credParams, null); + PublicKeyCredential credential = + webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + creationOptions, + Objects.requireNonNull(creationOptions.getRp().getId()), + TestData.PIN, + null, + null); + AuthenticatorAttestationResponse attestation = + (AuthenticatorAttestationResponse) credential.getResponse(); + int alg = + (Integer) + Objects.requireNonNull( + parseCredentialData(getAuthenticatorDataFromAttestationResponse(attestation)) + .get("keyAlgo")); + assertEquals(credParams.get(0).getAlg(), alg); + }); - byte[] aaguid = new byte[16]; - bb.get(aaguid); + state.withCtap2( + session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + // Test algorithm order: ALG_EdDSA - ALG_ES256 + List credParams = + Arrays.asList(allCredParams.get(1), allCredParams.get(0)); + PublicKeyCredentialCreationOptions creationOptions = + getCreateOptions(null, false, credParams, null); + PublicKeyCredential credential = + webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + creationOptions, + Objects.requireNonNull(creationOptions.getRp().getId()), + TestData.PIN, + null, + null); + AuthenticatorAttestationResponse attestation = + (AuthenticatorAttestationResponse) credential.getResponse(); + int alg = + (Integer) + Objects.requireNonNull( + parseCredentialData(getAuthenticatorDataFromAttestationResponse(attestation)) + .get("keyAlgo")); + assertEquals(credParams.get(0).getAlg(), alg); + }); + } - short idLength = bb.getShort(); - byte[] credId = new byte[idLength]; - bb.get(credId); + public static void testClientPinManagement(FidoTestState state) throws Throwable { + state.withCtap2( + session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + assumeTrue("Pin not supported", webauthn.isPinSupported()); + assertTrue(webauthn.isPinConfigured()); - byte[] key = new byte[bb.remaining()]; - bb.get(key); + webauthn.changePin(TestData.PIN, TestData.OTHER_PIN); - Map credData = new HashMap<>(); - credData.put("rpIdHash", rpIdHash); - credData.put("flags", flags); - credData.put("signCount", signCount); - credData.put("aaguid", aaguid); - credData.put("credId", credId); - credData.put("pubkey", key); - credData.put("keyAlgo", getAlgoFromCredentialPublicKey(key)); - return credData; - } - - private static int getAlgoFromCredentialPublicKey(byte[] pubKey) { - @SuppressWarnings("unchecked") - Map credPublicKey = (Map) Cbor.decode(pubKey); - Assert.assertNotNull(credPublicKey); - return (Integer) Objects.requireNonNull(credPublicKey.get(3)); + try { + webauthn.changePin(TestData.PIN, TestData.OTHER_PIN); + fail("Wrong PIN was accepted"); + } catch (ClientError e) { + assertThat(e.getErrorCode(), equalTo(ClientError.Code.BAD_REQUEST)); + assertThat(e.getCause(), instanceOf(CtapException.class)); + assertThat( + ((CtapException) Objects.requireNonNull(e.getCause())).getCtapError(), + is(CtapException.ERR_PIN_INVALID)); + } + + webauthn.changePin(TestData.OTHER_PIN, TestData.PIN); + }); + } + + public static void testClientCredentialManagement(FidoTestState state) throws Throwable { + state.withCtap2( + session -> { + assumeTrue( + "Credential management not supported", + CredentialManagement.isSupported(session.getCachedInfo())); + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredentialCreationOptions creationOptions = + getCreateOptions( + null, true, Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), null); + webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + creationOptions, + Objects.requireNonNull(creationOptions.getRp().getId()), + TestData.PIN, + null, + null); + + CredentialManager credentialManager = webauthn.getCredentialManager(TestData.PIN); + + assertThat(credentialManager.getCredentialCount(), equalTo(1)); + + List rpIds = credentialManager.getRpIdList(); + assertThat(rpIds, equalTo(Collections.singletonList(TestData.RP_ID))); + + Map credentials = + credentialManager.getCredentials(TestData.RP_ID); + assertThat(credentials.size(), equalTo(1)); + PublicKeyCredentialDescriptor key = credentials.entrySet().iterator().next().getKey(); + assertThat( + Objects.requireNonNull(credentials.get(key)).getId(), equalTo(TestData.USER_ID)); + + try { + PublicKeyCredentialUserEntity updatedUser = + new PublicKeyCredentialUserEntity( + "New name", credentials.get(key).getId(), "New display name"); + credentialManager.updateUserInformation(key, updatedUser); + + // verify new information + Map updatedCreds = + credentialManager.getCredentials(TestData.RP_ID); + assertThat(updatedCreds.size(), equalTo(1)); + PublicKeyCredentialDescriptor updatedKey = updatedCreds.keySet().iterator().next(); + PublicKeyCredentialUserEntity updatedUserEntity = + Objects.requireNonNull(updatedCreds.get(updatedKey)); + assertThat(updatedUserEntity.getId(), equalTo(TestData.USER_ID)); + assertThat(updatedUserEntity.getName(), equalTo("New name")); + assertThat(updatedUserEntity.getDisplayName(), equalTo("New display name")); + } catch (UnsupportedOperationException unsupportedOperationException) { + // ignored + } + + credentialManager.deleteCredential(key); + assertThat(credentialManager.getCredentialCount(), equalTo(0)); + assertTrue(credentialManager.getCredentials(TestData.RP_ID).isEmpty()); + assertTrue(credentialManager.getRpIdList().isEmpty()); + }); + } + + private static PublicKeyCredentialCreationOptions getCreateOptions( + @Nullable PublicKeyCredentialUserEntity user, + boolean rk, + List credParams, + @Nullable List excludeCredentials) { + return getCreateOptions(user, rk, credParams, excludeCredentials, null); + } + + private static PublicKeyCredentialCreationOptions getCreateOptions( + @Nullable PublicKeyCredentialUserEntity user, + boolean rk, + List credParams, + @Nullable List excludeCredentials, + @Nullable String userVerification) { + if (user == null) { + user = TestData.USER; } - - private static void deleteCredentials( - @Nonnull BasicWebAuthnClient webAuthnClient, - @Nonnull List credIds - ) throws IOException, CommandException, ClientError { - CredentialManager credentialManager = webAuthnClient.getCredentialManager(TestData.PIN); - for (byte[] credId : credIds) { - credentialManager.deleteCredential( - new PublicKeyCredentialDescriptor( - PublicKeyCredentialType.PUBLIC_KEY, - credId, - null)); - } + PublicKeyCredentialRpEntity rp = TestData.RP; + AuthenticatorSelectionCriteria criteria = + new AuthenticatorSelectionCriteria( + null, + rk ? ResidentKeyRequirement.REQUIRED : ResidentKeyRequirement.DISCOURAGED, + userVerification); + return new PublicKeyCredentialCreationOptions( + rp, + user, + TestData.CHALLENGE, + credParams, + (long) 90000, + excludeCredentials, + criteria, + null, + null); + } + + private static byte[] getAuthenticatorDataFromAttestationResponse( + AuthenticatorAttestationResponse response) { + byte[] attestObjBytes = response.getAttestationObject(); + @SuppressWarnings("unchecked") + Map attestObj = (Map) Cbor.decode(attestObjBytes); + Assert.assertNotNull(attestObj); + return (byte[]) attestObj.get("authData"); + } + + private static Map parseCredentialData(final byte[] data) { + ByteBuffer bb = ByteBuffer.wrap(data); + byte[] rpIdHash = new byte[32]; + bb.get(rpIdHash); + + byte flags = bb.get(); + + int signCount = bb.getInt(); + + byte[] aaguid = new byte[16]; + bb.get(aaguid); + + short idLength = bb.getShort(); + byte[] credId = new byte[idLength]; + bb.get(credId); + + byte[] key = new byte[bb.remaining()]; + bb.get(key); + + Map credData = new HashMap<>(); + credData.put("rpIdHash", rpIdHash); + credData.put("flags", flags); + credData.put("signCount", signCount); + credData.put("aaguid", aaguid); + credData.put("credId", credId); + credData.put("pubkey", key); + credData.put("keyAlgo", getAlgoFromCredentialPublicKey(key)); + return credData; + } + + private static int getAlgoFromCredentialPublicKey(byte[] pubKey) { + @SuppressWarnings("unchecked") + Map credPublicKey = (Map) Cbor.decode(pubKey); + Assert.assertNotNull(credPublicKey); + return (Integer) Objects.requireNonNull(credPublicKey.get(3)); + } + + private static void deleteCredentials( + @Nonnull BasicWebAuthnClient webAuthnClient, @Nonnull List credIds) + throws IOException, CommandException, ClientError { + CredentialManager credentialManager = webAuthnClient.getCredentialManager(TestData.PIN); + for (byte[] credId : credIds) { + credentialManager.deleteCredential( + new PublicKeyCredentialDescriptor(PublicKeyCredentialType.PUBLIC_KEY, credId, null)); } -} \ No newline at end of file + } +} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentTests.java index 55e0c558..e0580aa5 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2BioEnrollmentTests.java @@ -27,129 +27,122 @@ import com.yubico.yubikit.fido.ctap.ClientPin; import com.yubico.yubikit.fido.ctap.Ctap2Session; import com.yubico.yubikit.fido.ctap.FingerprintBioEnrollment; -import com.yubico.yubikit.testing.piv.PivCertificateTests; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.Arrays; import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class Ctap2BioEnrollmentTests { - private static final Logger logger = LoggerFactory.getLogger(Ctap2BioEnrollmentTests.class); + private static final Logger logger = LoggerFactory.getLogger(Ctap2BioEnrollmentTests.class); - public static void testFingerprintEnrollment(Ctap2Session session, FidoTestState state) - throws Throwable { + public static void testFingerprintEnrollment(Ctap2Session session, FidoTestState state) + throws Throwable { - assumeTrue("Bio enrollment not supported", - BioEnrollment.isSupported(session.getCachedInfo())); + assumeTrue("Bio enrollment not supported", BioEnrollment.isSupported(session.getCachedInfo())); - final FingerprintBioEnrollment fingerprintBioEnrollment = fpBioEnrollment(session, state); + final FingerprintBioEnrollment fingerprintBioEnrollment = fpBioEnrollment(session, state); - removeAllFingerprints(fingerprintBioEnrollment); + removeAllFingerprints(fingerprintBioEnrollment); - final byte[] templateId = enrollFingerprint(fingerprintBioEnrollment); + final byte[] templateId = enrollFingerprint(fingerprintBioEnrollment); - Map enrollments = fingerprintBioEnrollment.enumerateEnrollments(); - assertTrue(isEnrolled(templateId, enrollments)); + Map enrollments = fingerprintBioEnrollment.enumerateEnrollments(); + assertTrue(isEnrolled(templateId, enrollments)); - final int maxNameLen = fingerprintBioEnrollment - .getSensorInfo() - .getMaxTemplateFriendlyName(); + final int maxNameLen = fingerprintBioEnrollment.getSensorInfo().getMaxTemplateFriendlyName(); - renameFingerprint(fingerprintBioEnrollment, templateId, maxNameLen); - try { - renameFingerprint(fingerprintBioEnrollment, templateId, maxNameLen + 1); - fail("Expected exception after rename with long name"); - } catch (CtapException e) { - assertEquals(CtapException.ERR_INVALID_LENGTH, e.getCtapError()); - logger.debug("Caught ERR_INVALID_LENGTH when using long name."); - } - - fingerprintBioEnrollment.removeEnrollment(templateId); - enrollments = fingerprintBioEnrollment.enumerateEnrollments(); - assertThat("Fingerprints still exists after removal", enrollments.isEmpty()); + renameFingerprint(fingerprintBioEnrollment, templateId, maxNameLen); + try { + renameFingerprint(fingerprintBioEnrollment, templateId, maxNameLen + 1); + fail("Expected exception after rename with long name"); + } catch (CtapException e) { + assertEquals(CtapException.ERR_INVALID_LENGTH, e.getCtapError()); + logger.debug("Caught ERR_INVALID_LENGTH when using long name."); } - private static byte[] enrollFingerprint(FingerprintBioEnrollment bioEnrollment) { - - final FingerprintBioEnrollment.Context context = bioEnrollment.enroll(null); - - byte[] templateId = null; - while (templateId == null) { - logger.debug("Touch the fingerprint"); - try { - templateId = context.capture(null); - } catch (FingerprintBioEnrollment.CaptureError captureError) { - // capture errors are expected - logger.debug("Received capture error: ", captureError); - } catch (CtapException ctapException) { - assertThat("Received CTAP2_ERR_FP_DATABASE_FULL exception - " + - "remove fingerprints before running this test", - ctapException.getCtapError() != CtapException.ERR_FP_DATABASE_FULL); - fail("Received unexpected CTAP2 exception " + ctapException.getCtapError()); - } catch (Throwable exception) { - fail("Received unexpected exception " + exception.getMessage()); - } - } - - logger.debug("Enrolled: {}", templateId); - - return templateId; + fingerprintBioEnrollment.removeEnrollment(templateId); + enrollments = fingerprintBioEnrollment.enumerateEnrollments(); + assertThat("Fingerprints still exists after removal", enrollments.isEmpty()); + } + + private static byte[] enrollFingerprint(FingerprintBioEnrollment bioEnrollment) { + + final FingerprintBioEnrollment.Context context = bioEnrollment.enroll(null); + + byte[] templateId = null; + while (templateId == null) { + logger.debug("Touch the fingerprint"); + try { + templateId = context.capture(null); + } catch (FingerprintBioEnrollment.CaptureError captureError) { + // capture errors are expected + logger.debug("Received capture error: ", captureError); + } catch (CtapException ctapException) { + assertThat( + "Received CTAP2_ERR_FP_DATABASE_FULL exception - " + + "remove fingerprints before running this test", + ctapException.getCtapError() != CtapException.ERR_FP_DATABASE_FULL); + fail("Received unexpected CTAP2 exception " + ctapException.getCtapError()); + } catch (Throwable exception) { + fail("Received unexpected exception " + exception.getMessage()); + } } - private static FingerprintBioEnrollment fpBioEnrollment(Ctap2Session session, FidoTestState state) throws Throwable { + logger.debug("Enrolled: {}", templateId); - final ClientPin pin = new ClientPin(session, state.getPinUvAuthProtocol()); - final byte[] pinToken = pin.getPinToken( - TestData.PIN, - ClientPin.PIN_PERMISSION_BE, - "localhost"); + return templateId; + } - return new FingerprintBioEnrollment(session, state.getPinUvAuthProtocol(), pinToken); - } + private static FingerprintBioEnrollment fpBioEnrollment(Ctap2Session session, FidoTestState state) + throws Throwable { - public static void renameFingerprint( - FingerprintBioEnrollment fingerprintBioEnrollment, - byte[] templateId, - int newNameLen) throws Throwable { + final ClientPin pin = new ClientPin(session, state.getPinUvAuthProtocol()); + final byte[] pinToken = pin.getPinToken(TestData.PIN, ClientPin.PIN_PERMISSION_BE, "localhost"); - char[] charArray = new char[newNameLen]; - Arrays.fill(charArray, 'A'); - String newName = new String(charArray); + return new FingerprintBioEnrollment(session, state.getPinUvAuthProtocol(), pinToken); + } - fingerprintBioEnrollment.setName(templateId, newName); - Map enrollments = fingerprintBioEnrollment.enumerateEnrollments(); - assertEquals(newName, getName(templateId, enrollments)); - } + public static void renameFingerprint( + FingerprintBioEnrollment fingerprintBioEnrollment, byte[] templateId, int newNameLen) + throws Throwable { - public static void removeAllFingerprints(FingerprintBioEnrollment fingerprintBioEnrollment) throws Throwable { - Map enrollments = fingerprintBioEnrollment.enumerateEnrollments(); + char[] charArray = new char[newNameLen]; + Arrays.fill(charArray, 'A'); + String newName = new String(charArray); - for (byte[] templateId : enrollments.keySet()) { - fingerprintBioEnrollment.removeEnrollment(templateId); - } + fingerprintBioEnrollment.setName(templateId, newName); + Map enrollments = fingerprintBioEnrollment.enumerateEnrollments(); + assertEquals(newName, getName(templateId, enrollments)); + } - enrollments = fingerprintBioEnrollment.enumerateEnrollments(); - assertThat("Fingerprints still exists after removal", enrollments.isEmpty()); - } + public static void removeAllFingerprints(FingerprintBioEnrollment fingerprintBioEnrollment) + throws Throwable { + Map enrollments = fingerprintBioEnrollment.enumerateEnrollments(); - public static boolean isEnrolled(byte[] templateId, Map enrollments) { - for (byte[] enrolledTemplateId : enrollments.keySet()) { - if (Arrays.equals(templateId, enrolledTemplateId)) { - return true; - } - } - return false; + for (byte[] templateId : enrollments.keySet()) { + fingerprintBioEnrollment.removeEnrollment(templateId); } - public static String getName(byte[] templateId, Map enrollments) { - for (Map.Entry enrollment : enrollments.entrySet()) { - if (Arrays.equals(templateId, enrollment.getKey())) { - return enrollments.get(enrollment.getKey()); - } - } - return null; + enrollments = fingerprintBioEnrollment.enumerateEnrollments(); + assertThat("Fingerprints still exists after removal", enrollments.isEmpty()); + } + + public static boolean isEnrolled(byte[] templateId, Map enrollments) { + for (byte[] enrolledTemplateId : enrollments.keySet()) { + if (Arrays.equals(templateId, enrolledTemplateId)) { + return true; + } + } + return false; + } + + public static String getName(byte[] templateId, Map enrollments) { + for (Map.Entry enrollment : enrollments.entrySet()) { + if (Arrays.equals(templateId, enrollment.getKey())) { + return enrollments.get(enrollment.getKey()); + } } + return null; + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinTests.java index 58459321..4a1f5497 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ClientPinTests.java @@ -30,76 +30,75 @@ import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol; import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV2; import com.yubico.yubikit.management.DeviceInfo; - import java.util.Objects; public class Ctap2ClientPinTests { - public static void testClientPin(Ctap2Session session, FidoTestState state) throws Throwable { - Integer permissions = ClientPin.PIN_PERMISSION_MC | ClientPin.PIN_PERMISSION_GA; - String permissionRpId = "localhost"; - - ClientPin pin = new ClientPin(session, state.getPinUvAuthProtocol()); - assertThat(pin.getPinUvAuth().getVersion(), is(state.getPinUvAuthProtocol().getVersion())); - assertThat(pin.getPinRetries().getCount(), is(8)); - - pin.changePin(TestData.PIN, TestData.OTHER_PIN); - try { - pin.getPinToken(TestData.PIN, permissions, permissionRpId); - fail("Wrong PIN was accepted"); - } catch (CtapException e) { - assertThat(e.getCtapError(), is(CtapException.ERR_PIN_INVALID)); - - } - assertThat(pin.getPinRetries().getCount(), is(7)); - - assertThat(pin.getPinToken(TestData.OTHER_PIN, permissions, permissionRpId), notNullValue()); - assertThat(pin.getPinRetries().getCount(), is(8)); - pin.changePin(TestData.OTHER_PIN, TestData.PIN); + public static void testClientPin(Ctap2Session session, FidoTestState state) throws Throwable { + Integer permissions = ClientPin.PIN_PERMISSION_MC | ClientPin.PIN_PERMISSION_GA; + String permissionRpId = "localhost"; + + ClientPin pin = new ClientPin(session, state.getPinUvAuthProtocol()); + assertThat(pin.getPinUvAuth().getVersion(), is(state.getPinUvAuthProtocol().getVersion())); + assertThat(pin.getPinRetries().getCount(), is(8)); + + pin.changePin(TestData.PIN, TestData.OTHER_PIN); + try { + pin.getPinToken(TestData.PIN, permissions, permissionRpId); + fail("Wrong PIN was accepted"); + } catch (CtapException e) { + assertThat(e.getCtapError(), is(CtapException.ERR_PIN_INVALID)); } - - public static void testPinComplexity(FidoTestState state) throws Throwable { - - final DeviceInfo deviceInfo = state.getDeviceInfo(); - assumeTrue("Device does not support PIN complexity", deviceInfo != null); - assumeTrue("Device does not require PIN complexity", deviceInfo.getPinComplexity()); - - state.withCtap2(session -> { - PinUvAuthProtocol pinUvAuthProtocol = new PinUvAuthProtocolV2(); - char[] defaultPin = "11234567".toCharArray(); - - Ctap2Session.InfoData info = session.getCachedInfo(); - ClientPin pin = new ClientPin(session, pinUvAuthProtocol); - boolean pinSet = Objects.requireNonNull((Boolean) info.getOptions().get("clientPin")); - - try { - if (!pinSet) { - pin.setPin(defaultPin); - } else { - pin.getPinToken( - defaultPin, - ClientPin.PIN_PERMISSION_MC | ClientPin.PIN_PERMISSION_GA, - "localhost"); - } - } catch (ApduException e) { - fail("Failed to set or use PIN. Reset the device and try again"); - } - - assertThat(pin.getPinUvAuth().getVersion(), is(pinUvAuthProtocol.getVersion())); - assertThat(pin.getPinRetries().getCount(), is(8)); - - char[] weakPin = "33333333".toCharArray(); - try { - pin.changePin(defaultPin, weakPin); - fail("Weak PIN was accepted"); - } catch (CtapException e) { - assertThat(e.getCtapError(), is(ERR_PIN_POLICY_VIOLATION)); + assertThat(pin.getPinRetries().getCount(), is(7)); + + assertThat(pin.getPinToken(TestData.OTHER_PIN, permissions, permissionRpId), notNullValue()); + assertThat(pin.getPinRetries().getCount(), is(8)); + pin.changePin(TestData.OTHER_PIN, TestData.PIN); + } + + public static void testPinComplexity(FidoTestState state) throws Throwable { + + final DeviceInfo deviceInfo = state.getDeviceInfo(); + assumeTrue("Device does not support PIN complexity", deviceInfo != null); + assumeTrue("Device does not require PIN complexity", deviceInfo.getPinComplexity()); + + state.withCtap2( + session -> { + PinUvAuthProtocol pinUvAuthProtocol = new PinUvAuthProtocolV2(); + char[] defaultPin = "11234567".toCharArray(); + + Ctap2Session.InfoData info = session.getCachedInfo(); + ClientPin pin = new ClientPin(session, pinUvAuthProtocol); + boolean pinSet = Objects.requireNonNull((Boolean) info.getOptions().get("clientPin")); + + try { + if (!pinSet) { + pin.setPin(defaultPin); + } else { + pin.getPinToken( + defaultPin, + ClientPin.PIN_PERMISSION_MC | ClientPin.PIN_PERMISSION_GA, + "localhost"); } - - char[] strongPin = "STRONG PIN".toCharArray(); - pin.changePin(defaultPin, strongPin); - pin.changePin(strongPin, defaultPin); - - assertThat(pin.getPinRetries().getCount(), is(8)); + } catch (ApduException e) { + fail("Failed to set or use PIN. Reset the device and try again"); + } + + assertThat(pin.getPinUvAuth().getVersion(), is(pinUvAuthProtocol.getVersion())); + assertThat(pin.getPinRetries().getCount(), is(8)); + + char[] weakPin = "33333333".toCharArray(); + try { + pin.changePin(defaultPin, weakPin); + fail("Weak PIN was accepted"); + } catch (CtapException e) { + assertThat(e.getCtapError(), is(ERR_PIN_POLICY_VIOLATION)); + } + + char[] strongPin = "STRONG PIN".toCharArray(); + pin.changePin(defaultPin, strongPin); + pin.changePin(strongPin, defaultPin); + + assertThat(pin.getPinRetries().getCount(), is(8)); }); - } + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ConfigTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ConfigTests.java index 93523c06..d04b9035 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ConfigTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2ConfigTests.java @@ -17,75 +17,75 @@ package com.yubico.yubikit.testing.fido; import static com.yubico.yubikit.testing.fido.utils.ConfigHelper.getConfig; +import static java.lang.Boolean.TRUE; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; -import static java.lang.Boolean.TRUE; import com.yubico.yubikit.core.application.CommandException; import com.yubico.yubikit.fido.ctap.ClientPin; import com.yubico.yubikit.fido.ctap.Config; import com.yubico.yubikit.fido.ctap.Ctap2Session; - import java.io.IOException; public class Ctap2ConfigTests { - public static void testReadWriteEnterpriseAttestation(Ctap2Session session, FidoTestState state) throws Throwable { - assumeTrue("Enterprise attestation not supported", - session.getInfo().getOptions().containsKey("ep")); - Config config = getConfig(session, state); - config.enableEnterpriseAttestation(); - assertEquals(TRUE, session.getInfo().getOptions().get("ep")); - } - - public static void testToggleAlwaysUv(Ctap2Session session, FidoTestState state) throws Throwable { - assumeTrue("Device does not support alwaysUv", - session.getInfo().getOptions().containsKey("alwaysUv")); - Config config = getConfig(session, state); - Object alwaysUv = getAlwaysUv(session); - config.toggleAlwaysUv(); - assertNotSame(getAlwaysUv(session), alwaysUv); - } + public static void testReadWriteEnterpriseAttestation(Ctap2Session session, FidoTestState state) + throws Throwable { + assumeTrue( + "Enterprise attestation not supported", session.getInfo().getOptions().containsKey("ep")); + Config config = getConfig(session, state); + config.enableEnterpriseAttestation(); + assertEquals(TRUE, session.getInfo().getOptions().get("ep")); + } - public static void testSetForcePinChange(Ctap2Session session, FidoTestState state) throws Throwable { - assumeTrue("authenticatorConfig not supported", - Config.isSupported(session.getCachedInfo())); - assumeFalse("Force PIN change already set. Reset key and retry", session.getInfo().getForcePinChange()); - Config config = getConfig(session, state); - config.setMinPinLength(null, null, true); - assertTrue(session.getInfo().getForcePinChange()); + public static void testToggleAlwaysUv(Ctap2Session session, FidoTestState state) + throws Throwable { + assumeTrue( + "Device does not support alwaysUv", session.getInfo().getOptions().containsKey("alwaysUv")); + Config config = getConfig(session, state); + Object alwaysUv = getAlwaysUv(session); + config.toggleAlwaysUv(); + assertNotSame(getAlwaysUv(session), alwaysUv); + } - // set a new PIN - ClientPin pin = new ClientPin(session, state.getPinUvAuthProtocol()); - assertThat(pin.getPinUvAuth().getVersion(), is(state.getPinUvAuthProtocol().getVersion())); - assertThat(pin.getPinRetries().getCount(), is(8)); + public static void testSetForcePinChange(Ctap2Session session, FidoTestState state) + throws Throwable { + assumeTrue("authenticatorConfig not supported", Config.isSupported(session.getCachedInfo())); + assumeFalse( + "Force PIN change already set. Reset key and retry", session.getInfo().getForcePinChange()); + Config config = getConfig(session, state); + config.setMinPinLength(null, null, true); + assertTrue(session.getInfo().getForcePinChange()); - pin.changePin(TestData.PIN, TestData.OTHER_PIN); - assertFalse(session.getInfo().getForcePinChange()); + // set a new PIN + ClientPin pin = new ClientPin(session, state.getPinUvAuthProtocol()); + assertThat(pin.getPinUvAuth().getVersion(), is(state.getPinUvAuthProtocol().getVersion())); + assertThat(pin.getPinRetries().getCount(), is(8)); - // set to a default PIN - pin.changePin(TestData.OTHER_PIN, TestData.PIN); - assertFalse(session.getInfo().getForcePinChange()); + pin.changePin(TestData.PIN, TestData.OTHER_PIN); + assertFalse(session.getInfo().getForcePinChange()); - } + // set to a default PIN + pin.changePin(TestData.OTHER_PIN, TestData.PIN); + assertFalse(session.getInfo().getForcePinChange()); + } - public static void testSetMinPinLength(Ctap2Session session, FidoTestState state) throws Throwable { - assumeTrue("authenticatorConfig not supported", - Config.isSupported(session.getCachedInfo())); - Config config = getConfig(session, state); - // after calling this the key must be reset to get the default min pin length value - config.setMinPinLength(50, null, null); - assertEquals(50, session.getInfo().getMinPinLength()); - } + public static void testSetMinPinLength(Ctap2Session session, FidoTestState state) + throws Throwable { + assumeTrue("authenticatorConfig not supported", Config.isSupported(session.getCachedInfo())); + Config config = getConfig(session, state); + // after calling this the key must be reset to get the default min pin length value + config.setMinPinLength(50, null, null); + assertEquals(50, session.getInfo().getMinPinLength()); + } - static boolean getAlwaysUv(Ctap2Session session) throws IOException, CommandException { - return TRUE.equals(session.getInfo().getOptions().get("alwaysUv")); - } -} \ No newline at end of file + static boolean getAlwaysUv(Ctap2Session session) throws IOException, CommandException { + return TRUE.equals(session.getInfo().getOptions().get("alwaysUv")); + } +} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementTests.java index 4ef9a858..eb87c3c9 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementTests.java @@ -28,147 +28,147 @@ import com.yubico.yubikit.fido.ctap.Ctap2Session; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialUserEntity; import com.yubico.yubikit.fido.webauthn.SerializationType; - import java.io.IOException; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; public class Ctap2CredentialManagementTests { - /** - * Deletes all resident keys. Assumes TestData.PIN is currently set as the PIN. - */ - public static void deleteAllCredentials(CredentialManagement credentialManagement) - throws IOException, CommandException { - - for (CredentialManagement.RpData rpData : credentialManagement.enumerateRps()) { - for (CredentialManagement.CredentialData credData : - credentialManagement.enumerateCredentials(rpData.getRpIdHash())) { - credentialManagement.deleteCredential(credData.getCredentialId()); - } - } - - assertThat(credentialManagement.getMetadata().getExistingResidentCredentialsCount(), equalTo(0)); - } - - private static CredentialManagement setupCredentialManagement( - Ctap2Session session, FidoTestState state - ) throws IOException, CommandException { - - assumeTrue("Credential management not supported", - CredentialManagement.isSupported(session.getCachedInfo())); - - ClientPin clientPin = new ClientPin(session, state.getPinUvAuthProtocol()); - - return new CredentialManagement( - session, - clientPin.getPinUvAuth(), - clientPin.getPinToken(TestData.PIN, ClientPin.PIN_PERMISSION_CM, null) - ); - } - - public static void testReadMetadata(Ctap2Session session, FidoTestState state) throws Throwable { - CredentialManagement credentialManagement = setupCredentialManagement(session, state); - - CredentialManagement.Metadata metadata = credentialManagement.getMetadata(); - - assertThat(metadata.getExistingResidentCredentialsCount(), equalTo(0)); - assertThat(metadata.getMaxPossibleRemainingResidentCredentialsCount(), greaterThan(0)); + /** Deletes all resident keys. Assumes TestData.PIN is currently set as the PIN. */ + public static void deleteAllCredentials(CredentialManagement credentialManagement) + throws IOException, CommandException { + + for (CredentialManagement.RpData rpData : credentialManagement.enumerateRps()) { + for (CredentialManagement.CredentialData credData : + credentialManagement.enumerateCredentials(rpData.getRpIdHash())) { + credentialManagement.deleteCredential(credData.getCredentialId()); + } } - public static void testManagement(Ctap2Session session, FidoTestState state) throws Throwable { - - CredentialManagement credentialManagement = setupCredentialManagement(session, state); - assertThat(credentialManagement.enumerateRps(), empty()); - - byte[] pinToken = new ClientPin(session, credentialManagement.getPinUvAuth()).getPinToken( - TestData.PIN, - ClientPin.PIN_PERMISSION_MC, - TestData.RP.getId()); + assertThat( + credentialManagement.getMetadata().getExistingResidentCredentialsCount(), equalTo(0)); + } - byte[] pinAuth = credentialManagement.getPinUvAuth().authenticate(pinToken, TestData.CLIENT_DATA_HASH); - makeTestCredential(state, session, pinAuth); - - // this sets correct permission for handling credential management commands - credentialManagement = setupCredentialManagement(session, state); - CredentialManagement.CredentialData credData = getFirstTestCredential(credentialManagement); - - Map userData = credData.getUser(); - assertThat(userData.get("id"), equalTo(TestData.USER_ID)); - assertThat(userData.get("name"), equalTo(TestData.USER_NAME)); - assertThat(userData.get("displayName"), equalTo(TestData.USER_DISPLAY_NAME)); - - deleteAllCredentials(credentialManagement); - } + private static CredentialManagement setupCredentialManagement( + Ctap2Session session, FidoTestState state) throws IOException, CommandException { - public static void testUpdateUserInformation(Ctap2Session session, FidoTestState state) throws Throwable { + assumeTrue( + "Credential management not supported", + CredentialManagement.isSupported(session.getCachedInfo())); - CredentialManagement credentialManagement = setupCredentialManagement(session, state); - - assumeTrue("Update user information is supported", - credentialManagement.isUpdateUserInformationSupported()); - - assertThat(credentialManagement.enumerateRps(), empty()); - - byte[] pinToken = new ClientPin(session, credentialManagement.getPinUvAuth()).getPinToken( - TestData.PIN, - ClientPin.PIN_PERMISSION_MC, - TestData.RP.getId()); - - byte[] pinAuth = credentialManagement.getPinUvAuth().authenticate(pinToken, TestData.CLIENT_DATA_HASH); - makeTestCredential(state, session, pinAuth); - - // this sets correct permission for handling credential management commands - credentialManagement = setupCredentialManagement(session, state); - CredentialManagement.CredentialData credData = getFirstTestCredential(credentialManagement); - - // change user name and display name - PublicKeyCredentialUserEntity updated = new PublicKeyCredentialUserEntity( - "UPDATED NAME", - (byte[]) credData.getUser().get("id"), - "UPDATED DISPLAY NAME"); - - // function under test - credentialManagement.updateUserInformation(credData.getCredentialId(), updated.toMap(SerializationType.CBOR)); - - // verify that information has been changed - CredentialManagement.CredentialData updatedCredData = getFirstTestCredential(credentialManagement); - Map updatedUserData = updatedCredData.getUser(); - - assertThat(updatedUserData.get("id"), equalTo(TestData.USER_ID)); - assertThat(updatedUserData.get("name"), equalTo("UPDATED NAME")); - assertThat(updatedUserData.get("displayName"), equalTo("UPDATED DISPLAY NAME")); - - deleteAllCredentials(credentialManagement); - } - - // helper methods - private static void makeTestCredential(FidoTestState state, Ctap2Session session, byte[] pinAuth) throws IOException, CommandException { - final SerializationType cborType = SerializationType.CBOR; - session.makeCredential( - TestData.CLIENT_DATA_HASH, - TestData.RP.toMap(cborType), - TestData.USER.toMap(cborType), - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256.toMap(cborType)), - null, - null, - Collections.singletonMap("rk", true), - pinAuth, - state.getPinUvAuthProtocol().getVersion(), - null, - null - ); - } - - private static CredentialManagement.CredentialData getFirstTestCredential(CredentialManagement credentialManagement) throws IOException, CommandException { - List rps = credentialManagement.enumerateRps(); - assertThat(rps.size(), equalTo(1)); - CredentialManagement.RpData rpData = rps.get(0); - assertThat(rpData.getRp().get("id"), equalTo(TestData.RP_ID)); - List creds = credentialManagement.enumerateCredentials(rpData.getRpIdHash()); - assertThat(creds.size(), equalTo(1)); - return creds.get(0); - } + ClientPin clientPin = new ClientPin(session, state.getPinUvAuthProtocol()); + return new CredentialManagement( + session, + clientPin.getPinUvAuth(), + clientPin.getPinToken(TestData.PIN, ClientPin.PIN_PERMISSION_CM, null)); + } + + public static void testReadMetadata(Ctap2Session session, FidoTestState state) throws Throwable { + CredentialManagement credentialManagement = setupCredentialManagement(session, state); + + CredentialManagement.Metadata metadata = credentialManagement.getMetadata(); + + assertThat(metadata.getExistingResidentCredentialsCount(), equalTo(0)); + assertThat(metadata.getMaxPossibleRemainingResidentCredentialsCount(), greaterThan(0)); + } + + public static void testManagement(Ctap2Session session, FidoTestState state) throws Throwable { + + CredentialManagement credentialManagement = setupCredentialManagement(session, state); + assertThat(credentialManagement.enumerateRps(), empty()); + + byte[] pinToken = + new ClientPin(session, credentialManagement.getPinUvAuth()) + .getPinToken(TestData.PIN, ClientPin.PIN_PERMISSION_MC, TestData.RP.getId()); + + byte[] pinAuth = + credentialManagement.getPinUvAuth().authenticate(pinToken, TestData.CLIENT_DATA_HASH); + makeTestCredential(state, session, pinAuth); + + // this sets correct permission for handling credential management commands + credentialManagement = setupCredentialManagement(session, state); + CredentialManagement.CredentialData credData = getFirstTestCredential(credentialManagement); + + Map userData = credData.getUser(); + assertThat(userData.get("id"), equalTo(TestData.USER_ID)); + assertThat(userData.get("name"), equalTo(TestData.USER_NAME)); + assertThat(userData.get("displayName"), equalTo(TestData.USER_DISPLAY_NAME)); + + deleteAllCredentials(credentialManagement); + } + + public static void testUpdateUserInformation(Ctap2Session session, FidoTestState state) + throws Throwable { + + CredentialManagement credentialManagement = setupCredentialManagement(session, state); + + assumeTrue( + "Update user information is supported", + credentialManagement.isUpdateUserInformationSupported()); + + assertThat(credentialManagement.enumerateRps(), empty()); + + byte[] pinToken = + new ClientPin(session, credentialManagement.getPinUvAuth()) + .getPinToken(TestData.PIN, ClientPin.PIN_PERMISSION_MC, TestData.RP.getId()); + + byte[] pinAuth = + credentialManagement.getPinUvAuth().authenticate(pinToken, TestData.CLIENT_DATA_HASH); + makeTestCredential(state, session, pinAuth); + + // this sets correct permission for handling credential management commands + credentialManagement = setupCredentialManagement(session, state); + CredentialManagement.CredentialData credData = getFirstTestCredential(credentialManagement); + + // change user name and display name + PublicKeyCredentialUserEntity updated = + new PublicKeyCredentialUserEntity( + "UPDATED NAME", (byte[]) credData.getUser().get("id"), "UPDATED DISPLAY NAME"); + + // function under test + credentialManagement.updateUserInformation( + credData.getCredentialId(), updated.toMap(SerializationType.CBOR)); + + // verify that information has been changed + CredentialManagement.CredentialData updatedCredData = + getFirstTestCredential(credentialManagement); + Map updatedUserData = updatedCredData.getUser(); + + assertThat(updatedUserData.get("id"), equalTo(TestData.USER_ID)); + assertThat(updatedUserData.get("name"), equalTo("UPDATED NAME")); + assertThat(updatedUserData.get("displayName"), equalTo("UPDATED DISPLAY NAME")); + + deleteAllCredentials(credentialManagement); + } + + // helper methods + private static void makeTestCredential(FidoTestState state, Ctap2Session session, byte[] pinAuth) + throws IOException, CommandException { + final SerializationType cborType = SerializationType.CBOR; + session.makeCredential( + TestData.CLIENT_DATA_HASH, + TestData.RP.toMap(cborType), + TestData.USER.toMap(cborType), + Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256.toMap(cborType)), + null, + null, + Collections.singletonMap("rk", true), + pinAuth, + state.getPinUvAuthProtocol().getVersion(), + null, + null); + } + + private static CredentialManagement.CredentialData getFirstTestCredential( + CredentialManagement credentialManagement) throws IOException, CommandException { + List rps = credentialManagement.enumerateRps(); + assertThat(rps.size(), equalTo(1)); + CredentialManagement.RpData rpData = rps.get(0); + assertThat(rpData.getRp().get("id"), equalTo(TestData.RP_ID)); + List creds = + credentialManagement.enumerateCredentials(rpData.getRpIdHash()); + assertThat(creds.size(), equalTo(1)); + return creds.get(0); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2SessionTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2SessionTests.java index d38e9023..2417495a 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2SessionTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/Ctap2SessionTests.java @@ -32,7 +32,6 @@ import com.yubico.yubikit.fido.ctap.ClientPin; import com.yubico.yubikit.fido.ctap.Ctap2Session; import com.yubico.yubikit.fido.webauthn.SerializationType; - import java.util.Collections; import java.util.List; import java.util.Map; @@ -41,105 +40,110 @@ public class Ctap2SessionTests { - public static void testCtap2GetInfo(Ctap2Session session, FidoTestState state) { - Ctap2Session.InfoData info = session.getCachedInfo(); - - List versions = info.getVersions(); - assertTrue("Returned version does not contain any recognized version", - versions.contains("U2F_V2") || - versions.contains("FIDO_2_0") || - versions.contains("FIDO_2_1_PRE") || - versions.contains("FIDO_2_1")); - - // Check AAGUID - byte[] aaguid = info.getAaguid(); - assertEquals("AAGUID incorrect length", 16, aaguid.length); - - // Check options - Map options = info.getOptions(); - assertEquals("Option 'plat' incorrect", false, options.get("plat")); - assertEquals("Option 'rk' incorrect", true, options.get("rk")); - assertEquals("Option 'up' incorrect", true, options.get("up")); - assertTrue("Options do not contain 'clientPIN'", options.containsKey("clientPin")); - - // Check PIN/UV Auth protocol - List pinUvAuthProtocols = info.getPinUvAuthProtocols(); - assertThat("Number of PIN protocols incorrect", pinUvAuthProtocols.size(), greaterThanOrEqualTo(1)); - - if (state.isFipsApproved() && !state.isUsbTransport()) { - // FIPS only supports PIN/UV Auth protocol 2 over NFC - assertThat("Number of PIN protocols incorrect", pinUvAuthProtocols.size(), equalTo(1)); - assertTrue("PIN protocol incorrect", pinUvAuthProtocols.contains(2)); - } else { - // we expect at least protocol 1 to be present - assertThat("Number of PIN protocols incorrect", pinUvAuthProtocols.size(), greaterThanOrEqualTo(1)); - assertTrue("PIN protocol incorrect", pinUvAuthProtocols.contains(1)); - } + public static void testCtap2GetInfo(Ctap2Session session, FidoTestState state) { + Ctap2Session.InfoData info = session.getCachedInfo(); + + List versions = info.getVersions(); + assertTrue( + "Returned version does not contain any recognized version", + versions.contains("U2F_V2") + || versions.contains("FIDO_2_0") + || versions.contains("FIDO_2_1_PRE") + || versions.contains("FIDO_2_1")); + + // Check AAGUID + byte[] aaguid = info.getAaguid(); + assertEquals("AAGUID incorrect length", 16, aaguid.length); + + // Check options + Map options = info.getOptions(); + assertEquals("Option 'plat' incorrect", false, options.get("plat")); + assertEquals("Option 'rk' incorrect", true, options.get("rk")); + assertEquals("Option 'up' incorrect", true, options.get("up")); + assertTrue("Options do not contain 'clientPIN'", options.containsKey("clientPin")); + + // Check PIN/UV Auth protocol + List pinUvAuthProtocols = info.getPinUvAuthProtocols(); + assertThat( + "Number of PIN protocols incorrect", pinUvAuthProtocols.size(), greaterThanOrEqualTo(1)); + + if (state.isFipsApproved() && !state.isUsbTransport()) { + // FIPS only supports PIN/UV Auth protocol 2 over NFC + assertThat("Number of PIN protocols incorrect", pinUvAuthProtocols.size(), equalTo(1)); + assertTrue("PIN protocol incorrect", pinUvAuthProtocols.contains(2)); + } else { + // we expect at least protocol 1 to be present + assertThat( + "Number of PIN protocols incorrect", pinUvAuthProtocols.size(), greaterThanOrEqualTo(1)); + assertTrue("PIN protocol incorrect", pinUvAuthProtocols.contains(1)); } + } - public static void testCancelCborCommandImmediate(Ctap2Session session, FidoTestState state) throws Throwable { - doTestCancelCborCommand(session, state, false); - } + public static void testCancelCborCommandImmediate(Ctap2Session session, FidoTestState state) + throws Throwable { + doTestCancelCborCommand(session, state, false); + } - public static void testCancelCborCommandAfterDelay(Ctap2Session session, FidoTestState state) throws Throwable { - doTestCancelCborCommand(session, state, true); - } + public static void testCancelCborCommandAfterDelay(Ctap2Session session, FidoTestState state) + throws Throwable { + doTestCancelCborCommand(session, state, true); + } - public static void testReset(FidoTestState state) throws Throwable { + public static void testReset(FidoTestState state) throws Throwable { - state.withCtap2(session -> { - assumeFalse("Skipping reset test - authenticator supports bio enrollment", - session.getCachedInfo().getOptions().containsKey("bioEnroll")); + state.withCtap2( + session -> { + assumeFalse( + "Skipping reset test - authenticator supports bio enrollment", + session.getCachedInfo().getOptions().containsKey("bioEnroll")); - session.reset(null); + session.reset(null); - // Verify that the pin is no longer configured - Boolean clientPin = (Boolean) session.getInfo().getOptions().get("clientPin"); - boolean pinConfigured = (clientPin != null) && clientPin; - assertFalse("PIN should not be configured after a reset", pinConfigured); + // Verify that the pin is no longer configured + Boolean clientPin = (Boolean) session.getInfo().getOptions().get("clientPin"); + boolean pinConfigured = (clientPin != null) && clientPin; + assertFalse("PIN should not be configured after a reset", pinConfigured); }); + } + + private static void doTestCancelCborCommand( + Ctap2Session session, FidoTestState testState, boolean delay) throws Throwable { + + assumeTrue("Not a USB connection", testState.isUsbTransport()); + + ClientPin pin = new ClientPin(session, testState.getPinUvAuthProtocol()); + byte[] pinToken = + pin.getPinToken(TestData.PIN, ClientPin.PIN_PERMISSION_MC, TestData.RP.getId()); + byte[] pinAuth = pin.getPinUvAuth().authenticate(pinToken, TestData.CLIENT_DATA_HASH); + + CommandState state = new CommandState(); + if (delay) { + Executors.newSingleThreadScheduledExecutor() + .schedule(state::cancel, 500, TimeUnit.MILLISECONDS); + } else { + state.cancel(); } - private static void doTestCancelCborCommand( - Ctap2Session session, - FidoTestState testState, - boolean delay - ) throws Throwable { - - assumeTrue("Not a USB connection", testState.isUsbTransport()); - - ClientPin pin = new ClientPin(session, testState.getPinUvAuthProtocol()); - byte[] pinToken = pin.getPinToken(TestData.PIN, ClientPin.PIN_PERMISSION_MC, TestData.RP.getId()); - byte[] pinAuth = pin.getPinUvAuth().authenticate(pinToken, TestData.CLIENT_DATA_HASH); - - CommandState state = new CommandState(); - if (delay) { - Executors.newSingleThreadScheduledExecutor() - .schedule(state::cancel, 500, TimeUnit.MILLISECONDS); - } else { - state.cancel(); - } - - final SerializationType cborType = SerializationType.CBOR; - - try { - session.makeCredential( - TestData.CLIENT_DATA_HASH, - TestData.RP.toMap(cborType), - TestData.USER.toMap(cborType), - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256.toMap(cborType)), - null, - null, - null, - pinAuth, - pin.getPinUvAuth().getVersion(), - null, - state); - fail("Make credential completed without being cancelled."); - } catch (CtapException e) { - assertThat(e.getCtapError(), is(CtapException.ERR_KEEPALIVE_CANCEL)); - } - - session.getInfo(); //Make sure connection still works. + final SerializationType cborType = SerializationType.CBOR; + + try { + session.makeCredential( + TestData.CLIENT_DATA_HASH, + TestData.RP.toMap(cborType), + TestData.USER.toMap(cborType), + Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256.toMap(cborType)), + null, + null, + null, + pinAuth, + pin.getPinUvAuth().getVersion(), + null, + state); + fail("Make credential completed without being cancelled."); + } catch (CtapException e) { + assertThat(e.getCtapError(), is(CtapException.ERR_KEEPALIVE_CANCEL)); } -} \ No newline at end of file + + session.getInfo(); // Make sure connection still works. + } +} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationTests.java index 649f7fe9..ad38d8c4 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/EnterpriseAttestationTests.java @@ -16,12 +16,12 @@ package com.yubico.yubikit.testing.fido; +import static java.lang.Boolean.FALSE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; -import static java.lang.Boolean.FALSE; import com.yubico.yubikit.core.application.CommandException; import com.yubico.yubikit.fido.Cbor; @@ -39,191 +39,203 @@ import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialRpEntity; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialUserEntity; import com.yubico.yubikit.fido.webauthn.ResidentKeyRequirement; - import java.io.IOException; import java.util.Collections; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; public class EnterpriseAttestationTests { - static void enableEp(Ctap2Session session, FidoTestState state) - throws CommandException, IOException { - // enable ep if not enabled - if (FALSE.equals(session.getCachedInfo().getOptions().get("ep"))) { - - ClientPin clientPin = new ClientPin(session, state.getPinUvAuthProtocol()); - byte[] pinToken = clientPin.getPinToken(TestData.PIN, ClientPin.PIN_PERMISSION_ACFG, null); - final Config config = new Config(session, state.getPinUvAuthProtocol(), pinToken); - config.enableEnterpriseAttestation(); - - } - } - - // test with RP ID in platform RP ID list - public static void testSupportedPlatformManagedEA(Ctap2Session session, FidoTestState state) throws Throwable { - assumeTrue("Enterprise attestation not supported", - session.getCachedInfo().getOptions().containsKey("ep")); - enableEp(session, state); - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - webauthn.getUserAgentConfiguration().setEpSupportedRpIds(Collections.singletonList(TestData.RP_ID)); + static void enableEp(Ctap2Session session, FidoTestState state) + throws CommandException, IOException { + // enable ep if not enabled + if (FALSE.equals(session.getCachedInfo().getOptions().get("ep"))) { - PublicKeyCredential credential = makeCredential(webauthn, AttestationConveyancePreference.ENTERPRISE, 2); - - final Map attestationObject = getAttestationObject(credential.getResponse()); - assertNotNull(attestationObject); - assertTrue((Boolean) attestationObject.get("epAtt")); - } - - // test with RP ID which is not in platform RP ID list - public static void testUnsupportedPlatformManagedEA(Ctap2Session session, FidoTestState state) throws Throwable { - assumeTrue("Enterprise attestation not supported", - session.getCachedInfo().getOptions().containsKey("ep")); - - enableEp(session, state); - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - - PublicKeyCredential credential = makeCredential(webauthn, AttestationConveyancePreference.ENTERPRISE, 2); - - Map attestationObject = getAttestationObject(credential.getResponse()); - assertNotNull(attestationObject); - assertTrue(!attestationObject.containsKey("epAtt") || - FALSE.equals(attestationObject.get("epAtt"))); + ClientPin clientPin = new ClientPin(session, state.getPinUvAuthProtocol()); + byte[] pinToken = clientPin.getPinToken(TestData.PIN, ClientPin.PIN_PERMISSION_ACFG, null); + final Config config = new Config(session, state.getPinUvAuthProtocol(), pinToken); + config.enableEnterpriseAttestation(); } - - public static void testVendorFacilitatedEA(Ctap2Session session, FidoTestState state) throws Throwable { - assumeTrue("Enterprise attestation not supported", - session.getCachedInfo().getOptions().containsKey("ep")); - - enableEp(session, state); - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - webauthn.getUserAgentConfiguration().setEpSupportedRpIds(Collections.singletonList(TestData.RP_ID)); - - PublicKeyCredential credential = makeCredential(webauthn, AttestationConveyancePreference.ENTERPRISE, 1); - - final Map attestationObject = getAttestationObject(credential.getResponse()); - assertNotNull(attestationObject); - assertEquals(Boolean.TRUE, attestationObject.get("epAtt")); - } - - // test with different PublicKeyCredentialCreationOptions AttestationConveyancePreference - // values - public static void testCreateOptionsAttestationPreference(FidoTestState state) throws Throwable { - - state.withCtap2(session -> { - assumeTrue("Enterprise attestation not supported", - session.getCachedInfo().getOptions().containsKey("ep")); - enableEp(session, state); + } + + // test with RP ID in platform RP ID list + public static void testSupportedPlatformManagedEA(Ctap2Session session, FidoTestState state) + throws Throwable { + assumeTrue( + "Enterprise attestation not supported", + session.getCachedInfo().getOptions().containsKey("ep")); + enableEp(session, state); + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + webauthn + .getUserAgentConfiguration() + .setEpSupportedRpIds(Collections.singletonList(TestData.RP_ID)); + + PublicKeyCredential credential = + makeCredential(webauthn, AttestationConveyancePreference.ENTERPRISE, 2); + + final Map attestationObject = getAttestationObject(credential.getResponse()); + assertNotNull(attestationObject); + assertTrue((Boolean) attestationObject.get("epAtt")); + } + + // test with RP ID which is not in platform RP ID list + public static void testUnsupportedPlatformManagedEA(Ctap2Session session, FidoTestState state) + throws Throwable { + assumeTrue( + "Enterprise attestation not supported", + session.getCachedInfo().getOptions().containsKey("ep")); + + enableEp(session, state); + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + + PublicKeyCredential credential = + makeCredential(webauthn, AttestationConveyancePreference.ENTERPRISE, 2); + + Map attestationObject = getAttestationObject(credential.getResponse()); + assertNotNull(attestationObject); + assertTrue( + !attestationObject.containsKey("epAtt") || FALSE.equals(attestationObject.get("epAtt"))); + } + + public static void testVendorFacilitatedEA(Ctap2Session session, FidoTestState state) + throws Throwable { + assumeTrue( + "Enterprise attestation not supported", + session.getCachedInfo().getOptions().containsKey("ep")); + + enableEp(session, state); + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + webauthn + .getUserAgentConfiguration() + .setEpSupportedRpIds(Collections.singletonList(TestData.RP_ID)); + + PublicKeyCredential credential = + makeCredential(webauthn, AttestationConveyancePreference.ENTERPRISE, 1); + + final Map attestationObject = getAttestationObject(credential.getResponse()); + assertNotNull(attestationObject); + assertEquals(Boolean.TRUE, attestationObject.get("epAtt")); + } + + // test with different PublicKeyCredentialCreationOptions AttestationConveyancePreference + // values + public static void testCreateOptionsAttestationPreference(FidoTestState state) throws Throwable { + + state.withCtap2( + session -> { + assumeTrue( + "Enterprise attestation not supported", + session.getCachedInfo().getOptions().containsKey("ep")); + enableEp(session, state); }); - // attestation = null - state.withCtap2(session -> { - final BasicWebAuthnClient webauthn = setupClient(session); - PublicKeyCredential credential = makeCredential(webauthn, null, 2); + // attestation = null + state.withCtap2( + session -> { + final BasicWebAuthnClient webauthn = setupClient(session); + PublicKeyCredential credential = makeCredential(webauthn, null, 2); - Map attestationObject = getAttestationObject(credential.getResponse()); - assertNull(attestationObject.get("epAtt")); + Map attestationObject = getAttestationObject(credential.getResponse()); + assertNull(attestationObject.get("epAtt")); }); - // attestation = DIRECT - state.withCtap2(session -> { - final BasicWebAuthnClient webauthn = setupClient(session); - PublicKeyCredential credential = makeCredential(webauthn, AttestationConveyancePreference.DIRECT, 2); - Map attestationObject = getAttestationObject(credential.getResponse()); - assertNull(attestationObject.get("epAtt")); + // attestation = DIRECT + state.withCtap2( + session -> { + final BasicWebAuthnClient webauthn = setupClient(session); + PublicKeyCredential credential = + makeCredential(webauthn, AttestationConveyancePreference.DIRECT, 2); + Map attestationObject = getAttestationObject(credential.getResponse()); + assertNull(attestationObject.get("epAtt")); }); + // attestation = INDIRECT + state.withCtap2( + session -> { + final BasicWebAuthnClient webauthn = setupClient(session); + PublicKeyCredential credential = + makeCredential(webauthn, AttestationConveyancePreference.DIRECT, 2); - // attestation = INDIRECT - state.withCtap2(session -> { - final BasicWebAuthnClient webauthn = setupClient(session); - PublicKeyCredential credential = makeCredential(webauthn, AttestationConveyancePreference.DIRECT, 2); - - Map attestationObject = getAttestationObject(credential.getResponse()); - assertNull(attestationObject.get("epAtt")); + Map attestationObject = getAttestationObject(credential.getResponse()); + assertNull(attestationObject.get("epAtt")); }); - // attestation = ENTERPRISE but null enterpriseAttestation - state.withCtap2(session -> { - final BasicWebAuthnClient webauthn = setupClient(session); - PublicKeyCredential credential = makeCredential(webauthn, AttestationConveyancePreference.ENTERPRISE, null); + // attestation = ENTERPRISE but null enterpriseAttestation + state.withCtap2( + session -> { + final BasicWebAuthnClient webauthn = setupClient(session); + PublicKeyCredential credential = + makeCredential(webauthn, AttestationConveyancePreference.ENTERPRISE, null); - Map attestationObject = getAttestationObject(credential.getResponse()); - assertNull(attestationObject.get("epAtt")); + Map attestationObject = getAttestationObject(credential.getResponse()); + assertNull(attestationObject.get("epAtt")); }); - // attestation = ENTERPRISE - state.withCtap2(session -> { - final BasicWebAuthnClient webauthn = setupClient(session); - PublicKeyCredential credential = makeCredential(webauthn, AttestationConveyancePreference.ENTERPRISE, 2); + // attestation = ENTERPRISE + state.withCtap2( + session -> { + final BasicWebAuthnClient webauthn = setupClient(session); + PublicKeyCredential credential = + makeCredential(webauthn, AttestationConveyancePreference.ENTERPRISE, 2); - Map attestationObject = getAttestationObject(credential.getResponse()); - assertEquals(Boolean.TRUE, attestationObject.get("epAtt")); + Map attestationObject = getAttestationObject(credential.getResponse()); + assertEquals(Boolean.TRUE, attestationObject.get("epAtt")); }); - } - - private static BasicWebAuthnClient setupClient(Ctap2Session session) throws IOException, CommandException { - BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - webauthn.getUserAgentConfiguration().setEpSupportedRpIds(Collections.singletonList( - TestData.RP_ID - )); - return webauthn; - } - - /** - * Helper method which creates test PublicKeyCredentialCreationOptions - */ - private static PublicKeyCredentialCreationOptions getCredentialCreationOptions( - @Nullable String attestation - ) { - PublicKeyCredentialUserEntity user = TestData.USER; - PublicKeyCredentialRpEntity rp = TestData.RP; - AuthenticatorSelectionCriteria criteria = new AuthenticatorSelectionCriteria( - null, - ResidentKeyRequirement.REQUIRED, - null - ); - return new PublicKeyCredentialCreationOptions( - rp, - user, - TestData.CHALLENGE, - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), - (long) 90000, - null, - criteria, - attestation, - null - ); - } - - /** - * Helper method which extracts AuthenticatorAttestationResponse from the credential - */ - @SuppressWarnings("unchecked") - private static Map getAttestationObject(AuthenticatorResponse response) { - AuthenticatorAttestationResponse authenticatorAttestationResponse = - (AuthenticatorAttestationResponse) response; - return (Map) - Cbor.decode(authenticatorAttestationResponse.getAttestationObject()); - } - - /** - * Helper method which creates a PublicKeyCredential with specific attestation and enterpriseAttestation - */ - private static PublicKeyCredential makeCredential( - BasicWebAuthnClient webauthn, - @Nullable String attestation, - @Nullable Integer enterpriseAttestation - ) throws ClientError, IOException, CommandException { - PublicKeyCredentialCreationOptions creationOptions = getCredentialCreationOptions(attestation); - return webauthn.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - creationOptions, - Objects.requireNonNull(creationOptions.getRp().getId()), - TestData.PIN, - enterpriseAttestation, - null); - } -} \ No newline at end of file + } + + private static BasicWebAuthnClient setupClient(Ctap2Session session) + throws IOException, CommandException { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + webauthn + .getUserAgentConfiguration() + .setEpSupportedRpIds(Collections.singletonList(TestData.RP_ID)); + return webauthn; + } + + /** Helper method which creates test PublicKeyCredentialCreationOptions */ + private static PublicKeyCredentialCreationOptions getCredentialCreationOptions( + @Nullable String attestation) { + PublicKeyCredentialUserEntity user = TestData.USER; + PublicKeyCredentialRpEntity rp = TestData.RP; + AuthenticatorSelectionCriteria criteria = + new AuthenticatorSelectionCriteria(null, ResidentKeyRequirement.REQUIRED, null); + return new PublicKeyCredentialCreationOptions( + rp, + user, + TestData.CHALLENGE, + Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), + (long) 90000, + null, + criteria, + attestation, + null); + } + + /** Helper method which extracts AuthenticatorAttestationResponse from the credential */ + @SuppressWarnings("unchecked") + private static Map getAttestationObject(AuthenticatorResponse response) { + AuthenticatorAttestationResponse authenticatorAttestationResponse = + (AuthenticatorAttestationResponse) response; + return (Map) Cbor.decode(authenticatorAttestationResponse.getAttestationObject()); + } + + /** + * Helper method which creates a PublicKeyCredential with specific attestation and + * enterpriseAttestation + */ + private static PublicKeyCredential makeCredential( + BasicWebAuthnClient webauthn, + @Nullable String attestation, + @Nullable Integer enterpriseAttestation) + throws ClientError, IOException, CommandException { + PublicKeyCredentialCreationOptions creationOptions = getCredentialCreationOptions(attestation); + return webauthn.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, + creationOptions, + Objects.requireNonNull(creationOptions.getRp().getId()), + TestData.PIN, + enterpriseAttestation, + null); + } +} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestState.java index fa6392ec..59e604b7 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/FidoTestState.java @@ -43,220 +43,212 @@ import com.yubico.yubikit.management.DeviceInfo; import com.yubico.yubikit.support.DeviceUtil; import com.yubico.yubikit.testing.TestState; - import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Objects; - import javax.annotation.Nullable; public class FidoTestState extends TestState { - private final PinUvAuthProtocol pinUvAuthProtocol; - private final boolean isFipsApproved; - public final boolean alwaysUv; - - public static class Builder extends TestState.Builder { + private final PinUvAuthProtocol pinUvAuthProtocol; + private final boolean isFipsApproved; + public final boolean alwaysUv; - private final PinUvAuthProtocol pinUvAuthProtocol; - private boolean setPin = false; + public static class Builder extends TestState.Builder { - public Builder(YubiKeyDevice device, UsbPid usbPid, PinUvAuthProtocol pinUvAuthProtocol) { - super(device, usbPid); - this.pinUvAuthProtocol = pinUvAuthProtocol; - } - - @Override - public Builder getThis() { - return this; - } - - public Builder setPin(boolean setPin) { - this.setPin = setPin; - return this; - } + private final PinUvAuthProtocol pinUvAuthProtocol; + private boolean setPin = false; - public FidoTestState build() throws Throwable { - return new FidoTestState(this); - } + public Builder(YubiKeyDevice device, UsbPid usbPid, PinUvAuthProtocol pinUvAuthProtocol) { + super(device, usbPid); + this.pinUvAuthProtocol = pinUvAuthProtocol; } - private FidoTestState(Builder builder) throws Throwable { - super(builder); - - this.pinUvAuthProtocol = builder.pinUvAuthProtocol; - - boolean isFidoFipsCapable = false; - DeviceInfo deviceInfo = null; - - try (YubiKeyConnection connection = openConnection()) { - try { - deviceInfo = DeviceUtil.readInfo(connection, null); - assertNotNull(deviceInfo); - isFidoFipsCapable = - (deviceInfo.getFipsCapable() & Capability.FIDO2.bit) == Capability.FIDO2.bit; - - assumeTrue("This YubiKey does not support FIDO2", - deviceInfo.getVersion().isAtLeast(5, 0, 0)); - } catch (IllegalArgumentException ignored) { - // failed to get device info, this is not a YubiKey - } - - Ctap2Session session = getCtap2Session(connection); - assumeTrue("CTAP2 not supported", session != null); - assumeTrue("PIN UV Protocol not supported", - supportsPinUvAuthProtocol(session, pinUvAuthProtocol)); - - if (isFidoFipsCapable) { - assumeTrue("Ignoring FIPS tests which don't use PinUvAuthProtocolV2", - pinUvAuthProtocol.getVersion() == 2); - } - - if (builder.setPin) { - verifyOrSetPin(session); - } - - @Nullable - Boolean alwaysUv = (Boolean) session.getInfo().getOptions().get("alwaysUv"); - if (isFidoFipsCapable && Boolean.FALSE.equals(alwaysUv)) { - // set always UV on - Config config = getConfig(session, this); - config.toggleAlwaysUv(); - alwaysUv = true; - } - this.alwaysUv = Boolean.TRUE.equals(alwaysUv); - - boolean fipsApproved = false; - try { - deviceInfo = DeviceUtil.readInfo(connection, null); - fipsApproved = - (deviceInfo.getFipsApproved() & Capability.FIDO2.bit) == Capability.FIDO2.bit; - } catch (IllegalArgumentException ignored) { - // not a YubiKey - } - - this.isFipsApproved = fipsApproved; - - // after changing the PIN and setting alwaysUv, we expect a FIPS capable device - // to be FIPS approved - if (builder.setPin && isFidoFipsCapable) { - assertNotNull(deviceInfo); - assertTrue("Device not FIDO FIPS approved as expected", this.isFipsApproved); - } - - // remove existing credentials - if (builder.setPin) { - // cannot use CredentialManager if there is no PIN set - session = getCtap2Session(connection); - deleteExistingCredentials(session); - } - } + @Override + public Builder getThis() { + return this; } - public boolean isFipsApproved() { - return isFipsApproved; + public Builder setPin(boolean setPin) { + this.setPin = setPin; + return this; } - public PinUvAuthProtocol getPinUvAuthProtocol() { - return pinUvAuthProtocol; + public FidoTestState build() throws Throwable { + return new FidoTestState(this); } - - boolean supportsPinUvAuthProtocol( - Ctap2Session session, - PinUvAuthProtocol pinUvAuthProtocol) { - final List pinUvAuthProtocols = session.getCachedInfo().getPinUvAuthProtocols(); - return pinUvAuthProtocols.contains(pinUvAuthProtocol.getVersion()); + } + + private FidoTestState(Builder builder) throws Throwable { + super(builder); + + this.pinUvAuthProtocol = builder.pinUvAuthProtocol; + + boolean isFidoFipsCapable = false; + DeviceInfo deviceInfo = null; + + try (YubiKeyConnection connection = openConnection()) { + try { + deviceInfo = DeviceUtil.readInfo(connection, null); + assertNotNull(deviceInfo); + isFidoFipsCapable = + (deviceInfo.getFipsCapable() & Capability.FIDO2.bit) == Capability.FIDO2.bit; + + assumeTrue( + "This YubiKey does not support FIDO2", deviceInfo.getVersion().isAtLeast(5, 0, 0)); + } catch (IllegalArgumentException ignored) { + // failed to get device info, this is not a YubiKey + } + + Ctap2Session session = getCtap2Session(connection); + assumeTrue("CTAP2 not supported", session != null); + assumeTrue( + "PIN UV Protocol not supported", supportsPinUvAuthProtocol(session, pinUvAuthProtocol)); + + if (isFidoFipsCapable) { + assumeTrue( + "Ignoring FIPS tests which don't use PinUvAuthProtocolV2", + pinUvAuthProtocol.getVersion() == 2); + } + + if (builder.setPin) { + verifyOrSetPin(session); + } + + @Nullable Boolean alwaysUv = (Boolean) session.getInfo().getOptions().get("alwaysUv"); + if (isFidoFipsCapable && Boolean.FALSE.equals(alwaysUv)) { + // set always UV on + Config config = getConfig(session, this); + config.toggleAlwaysUv(); + alwaysUv = true; + } + this.alwaysUv = Boolean.TRUE.equals(alwaysUv); + + boolean fipsApproved = false; + try { + deviceInfo = DeviceUtil.readInfo(connection, null); + fipsApproved = + (deviceInfo.getFipsApproved() & Capability.FIDO2.bit) == Capability.FIDO2.bit; + } catch (IllegalArgumentException ignored) { + // not a YubiKey + } + + this.isFipsApproved = fipsApproved; + + // after changing the PIN and setting alwaysUv, we expect a FIPS capable device + // to be FIPS approved + if (builder.setPin && isFidoFipsCapable) { + assertNotNull(deviceInfo); + assertTrue("Device not FIDO FIPS approved as expected", this.isFipsApproved); + } + + // remove existing credentials + if (builder.setPin) { + // cannot use CredentialManager if there is no PIN set + session = getCtap2Session(connection); + deleteExistingCredentials(session); + } } - - void deleteExistingCredentials(Ctap2Session session) - throws IOException, CommandException, ClientError { - final BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); - if (!CredentialManagement.isSupported(session.getCachedInfo())) { - return; - } - CredentialManager credentialManager = webauthn.getCredentialManager(TestData.PIN); - final List rpIds = credentialManager.getRpIdList(); - for (String rpId : rpIds) { - Map credentials - = credentialManager.getCredentials(rpId); - for (PublicKeyCredentialDescriptor credential : credentials.keySet()) { - credentialManager.deleteCredential(credential); - } - } - assertEquals("Failed to remove all credentials", 0, credentialManager.getCredentialCount()); + } + + public boolean isFipsApproved() { + return isFipsApproved; + } + + public PinUvAuthProtocol getPinUvAuthProtocol() { + return pinUvAuthProtocol; + } + + boolean supportsPinUvAuthProtocol(Ctap2Session session, PinUvAuthProtocol pinUvAuthProtocol) { + final List pinUvAuthProtocols = session.getCachedInfo().getPinUvAuthProtocols(); + return pinUvAuthProtocols.contains(pinUvAuthProtocol.getVersion()); + } + + void deleteExistingCredentials(Ctap2Session session) + throws IOException, CommandException, ClientError { + final BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + if (!CredentialManagement.isSupported(session.getCachedInfo())) { + return; } - - /** - * Attempts to set (or verify) the default PIN, or fails. - */ - void verifyOrSetPin(Ctap2Session session) throws IOException, CommandException { - - Ctap2Session.InfoData info = session.getInfo(); - - ClientPin pin = new ClientPin(session, pinUvAuthProtocol); - boolean pinSet = Objects.requireNonNull((Boolean) info.getOptions().get("clientPin")); - - try { - if (!pinSet) { - pin.setPin(TestData.PIN); - } else { - pin.getPinToken( - TestData.PIN, - ClientPin.PIN_PERMISSION_MC | ClientPin.PIN_PERMISSION_GA, - "localhost"); - } - } catch (CommandException e) { - fail("YubiKey cannot be used for test, failed to set/verify PIN. Please reset " + - "and try again."); - } + CredentialManager credentialManager = webauthn.getCredentialManager(TestData.PIN); + final List rpIds = credentialManager.getRpIdList(); + for (String rpId : rpIds) { + Map credentials = + credentialManager.getCredentials(rpId); + for (PublicKeyCredentialDescriptor credential : credentials.keySet()) { + credentialManager.deleteCredential(credential); + } } - - public void withDeviceCallback(StatefulDeviceCallback callback) throws Throwable { - callback.invoke(this); + assertEquals("Failed to remove all credentials", 0, credentialManager.getCredentialCount()); + } + + /** Attempts to set (or verify) the default PIN, or fails. */ + void verifyOrSetPin(Ctap2Session session) throws IOException, CommandException { + + Ctap2Session.InfoData info = session.getInfo(); + + ClientPin pin = new ClientPin(session, pinUvAuthProtocol); + boolean pinSet = Objects.requireNonNull((Boolean) info.getOptions().get("clientPin")); + + try { + if (!pinSet) { + pin.setPin(TestData.PIN); + } else { + pin.getPinToken( + TestData.PIN, ClientPin.PIN_PERMISSION_MC | ClientPin.PIN_PERMISSION_GA, "localhost"); + } + } catch (CommandException e) { + fail( + "YubiKey cannot be used for test, failed to set/verify PIN. Please reset " + + "and try again."); } - - public void withCtap2(TestState.StatefulSessionCallback callback) - throws Throwable { - try (YubiKeyConnection connection = openConnection()) { - final Ctap2Session ctap2 = getCtap2Session(connection); - assumeTrue("No CTAP2 support", ctap2 != null); - callback.invoke(ctap2, this); - } - reconnect(); + } + + public void withDeviceCallback(StatefulDeviceCallback callback) throws Throwable { + callback.invoke(this); + } + + public void withCtap2(TestState.StatefulSessionCallback callback) + throws Throwable { + try (YubiKeyConnection connection = openConnection()) { + final Ctap2Session ctap2 = getCtap2Session(connection); + assumeTrue("No CTAP2 support", ctap2 != null); + callback.invoke(ctap2, this); } - - public R withCtap2(SessionCallbackT callback) throws Throwable { - R result; - try (YubiKeyConnection connection = openConnection()) { - final Ctap2Session ctap2 = getCtap2Session(connection); - assumeTrue("No CTAP2 support", ctap2 != null); - result = callback.invoke(ctap2); - } - reconnect(); - return result; + reconnect(); + } + + public R withCtap2(SessionCallbackT callback) throws Throwable { + R result; + try (YubiKeyConnection connection = openConnection()) { + final Ctap2Session ctap2 = getCtap2Session(connection); + assumeTrue("No CTAP2 support", ctap2 != null); + result = callback.invoke(ctap2); } - - public void withCtap2(SessionCallback callback) throws Throwable { - try (YubiKeyConnection connection = openConnection()) { - final Ctap2Session ctap2 = getCtap2Session(connection); - assumeTrue("No CTAP2 support", ctap2 != null); - callback.invoke(ctap2); - } - reconnect(); + reconnect(); + return result; + } + + public void withCtap2(SessionCallback callback) throws Throwable { + try (YubiKeyConnection connection = openConnection()) { + final Ctap2Session ctap2 = getCtap2Session(connection); + assumeTrue("No CTAP2 support", ctap2 != null); + callback.invoke(ctap2); } - - @Nullable - public static Ctap2Session getCtap2Session(YubiKeyConnection connection) { - try { - return (connection instanceof FidoConnection) - ? new Ctap2Session((FidoConnection) connection) - : connection instanceof SmartCardConnection - ? new Ctap2Session((SmartCardConnection) connection) - : null; - } catch (IOException | CommandException ignored) { - // device does not provide CTAP2 - return null; - } + reconnect(); + } + + @Nullable public static Ctap2Session getCtap2Session(YubiKeyConnection connection) { + try { + return (connection instanceof FidoConnection) + ? new Ctap2Session((FidoConnection) connection) + : connection instanceof SmartCardConnection + ? new Ctap2Session((SmartCardConnection) connection) + : null; + } catch (IOException | CommandException ignored) { + // device does not provide CTAP2 + return null; } + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/TestData.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/TestData.java index 5684899f..3e994cc5 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/TestData.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/TestData.java @@ -21,61 +21,75 @@ import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialRpEntity; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialType; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialUserEntity; - -import org.bouncycastle.util.encoders.Base64; - import java.nio.charset.StandardCharsets; +import org.bouncycastle.util.encoders.Base64; class TestData { - static class ClientData { - @SuppressWarnings("unused") public final String type; - @SuppressWarnings("unused") public final String origin; - @SuppressWarnings("unused") public final String challenge; - @SuppressWarnings("unused") public final String androidPackageName; - - public ClientData(String type, String origin, byte[] challenge, String androidPackageName) { - this.type = type; - this.origin = origin; - this.challenge = Base64.toBase64String(challenge); - this.androidPackageName = androidPackageName; - } - } - - public static final char[] PIN = "11234567".toCharArray(); - public static final char[] OTHER_PIN = "11231234".toCharArray(); + static class ClientData { + @SuppressWarnings("unused") + public final String type; - public static final String RP_ID = "example.com"; - public static final String RP_NAME = "Example Company"; - public static final PublicKeyCredentialRpEntity RP = new PublicKeyCredentialRpEntity(RP_NAME, RP_ID); + @SuppressWarnings("unused") + public final String origin; - public static final String USER_NAME = "john.doe@example.com"; - public static final byte[] USER_ID = USER_NAME.getBytes(StandardCharsets.UTF_8); - public static final String USER_DISPLAY_NAME = "John Doe"; - public static final PublicKeyCredentialUserEntity USER = new PublicKeyCredentialUserEntity(USER_NAME, USER_ID, USER_DISPLAY_NAME); + @SuppressWarnings("unused") + public final String challenge; - public static final String ORIGIN = "https://" + RP_ID; - public static final byte[] CHALLENGE = new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + @SuppressWarnings("unused") + public final String androidPackageName; - private static final String PACKAGE_NAME = "TestPackage"; - - public static final byte[] CLIENT_DATA_JSON_CREATE = new Moshi.Builder().build().adapter(ClientData.class).toJson(new ClientData( - "webauthn.create", - ORIGIN, - CHALLENGE, - PACKAGE_NAME - )).getBytes(StandardCharsets.UTF_8); - - public static final byte[] CLIENT_DATA_JSON_GET = new Moshi.Builder().build().adapter(ClientData.class).toJson(new ClientData( - "webauthn.get", - ORIGIN, - CHALLENGE, - PACKAGE_NAME - )).getBytes(StandardCharsets.UTF_8); - - public static final byte[] CLIENT_DATA_HASH = new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; - - public static final PublicKeyCredentialParameters PUB_KEY_CRED_PARAMS_ES256 = new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, -7); - - public static final PublicKeyCredentialParameters PUB_KEY_CRED_PARAMS_EDDSA = new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, -8); + public ClientData(String type, String origin, byte[] challenge, String androidPackageName) { + this.type = type; + this.origin = origin; + this.challenge = Base64.toBase64String(challenge); + this.androidPackageName = androidPackageName; + } + } + + public static final char[] PIN = "11234567".toCharArray(); + public static final char[] OTHER_PIN = "11231234".toCharArray(); + + public static final String RP_ID = "example.com"; + public static final String RP_NAME = "Example Company"; + public static final PublicKeyCredentialRpEntity RP = + new PublicKeyCredentialRpEntity(RP_NAME, RP_ID); + + public static final String USER_NAME = "john.doe@example.com"; + public static final byte[] USER_ID = USER_NAME.getBytes(StandardCharsets.UTF_8); + public static final String USER_DISPLAY_NAME = "John Doe"; + public static final PublicKeyCredentialUserEntity USER = + new PublicKeyCredentialUserEntity(USER_NAME, USER_ID, USER_DISPLAY_NAME); + + public static final String ORIGIN = "https://" + RP_ID; + public static final byte[] CHALLENGE = + new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + + private static final String PACKAGE_NAME = "TestPackage"; + + public static final byte[] CLIENT_DATA_JSON_CREATE = + new Moshi.Builder() + .build() + .adapter(ClientData.class) + .toJson(new ClientData("webauthn.create", ORIGIN, CHALLENGE, PACKAGE_NAME)) + .getBytes(StandardCharsets.UTF_8); + + public static final byte[] CLIENT_DATA_JSON_GET = + new Moshi.Builder() + .build() + .adapter(ClientData.class) + .toJson(new ClientData("webauthn.get", ORIGIN, CHALLENGE, PACKAGE_NAME)) + .getBytes(StandardCharsets.UTF_8); + + public static final byte[] CLIENT_DATA_HASH = + new byte[] { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + 12, 13, 14, 15 + }; + + public static final PublicKeyCredentialParameters PUB_KEY_CRED_PARAMS_ES256 = + new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, -7); + + public static final PublicKeyCredentialParameters PUB_KEY_CRED_PARAMS_EDDSA = + new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, -8); } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/CredBlobExtensionTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/CredBlobExtensionTests.java index 7913ea7e..509da327 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/CredBlobExtensionTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/CredBlobExtensionTests.java @@ -25,136 +25,134 @@ import com.yubico.yubikit.testing.fido.utils.ClientHelper; import com.yubico.yubikit.testing.fido.utils.CreationOptionsBuilder; import com.yubico.yubikit.testing.fido.utils.RequestOptionsBuilder; - -import org.junit.Assert; -import org.junit.Assume; - import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Collections; import java.util.Map; - import javax.annotation.Nullable; +import org.junit.Assert; +import org.junit.Assume; public class CredBlobExtensionTests { - private static final String CRED_BLOB = "credBlob"; - private static final String GET_CRED_BLOB = "getCredBlob"; - private static final byte[] CRED_BLOB_DATA = { - (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, - (byte) 0x02, (byte) 0x02, (byte) 0x02, (byte) 0x02, - (byte) 0x03, (byte) 0x03, (byte) 0x03, (byte) 0x03, - (byte) 0x04, (byte) 0x04, (byte) 0x04, (byte) 0x04, - (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, - (byte) 0x06, (byte) 0x06, (byte) 0x06, (byte) 0x06, - (byte) 0x07, (byte) 0x07, (byte) 0x07, (byte) 0x07, - (byte) 0x08, (byte) 0x08, (byte) 0x08, (byte) 0x08, - }; - - public static void test(FidoTestState state) throws Throwable { - CredBlobExtensionTests extTest = new CredBlobExtensionTests(); - extTest.runTest(state); - } - - private CredBlobExtensionTests() { - - } - - private void runTest(FidoTestState state) throws Throwable { - // no output if extension not requestedΩ - state.withCtap2(session -> { - Assume.assumeTrue("credBlob not supported", - session.getCachedInfo().getExtensions().contains(CRED_BLOB)); - PublicKeyCredential cred = new ClientHelper(session).makeCredential(); - Assert.assertNull(getAttestationResult(cred)); + private static final String CRED_BLOB = "credBlob"; + private static final String GET_CRED_BLOB = "getCredBlob"; + private static final byte[] CRED_BLOB_DATA = { + (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, + (byte) 0x02, (byte) 0x02, (byte) 0x02, (byte) 0x02, + (byte) 0x03, (byte) 0x03, (byte) 0x03, (byte) 0x03, + (byte) 0x04, (byte) 0x04, (byte) 0x04, (byte) 0x04, + (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, + (byte) 0x06, (byte) 0x06, (byte) 0x06, (byte) 0x06, + (byte) 0x07, (byte) 0x07, (byte) 0x07, (byte) 0x07, + (byte) 0x08, (byte) 0x08, (byte) 0x08, (byte) 0x08, + }; + + public static void test(FidoTestState state) throws Throwable { + CredBlobExtensionTests extTest = new CredBlobExtensionTests(); + extTest.runTest(state); + } + + private CredBlobExtensionTests() {} + + private void runTest(FidoTestState state) throws Throwable { + // no output if extension not requestedΩ + state.withCtap2( + session -> { + Assume.assumeTrue( + "credBlob not supported", + session.getCachedInfo().getExtensions().contains(CRED_BLOB)); + PublicKeyCredential cred = new ClientHelper(session).makeCredential(); + Assert.assertNull(getAttestationResult(cred)); }); - // try to create with data > maxCredBlobLength - state.withCtap2(session -> { - ClientHelper client = new ClientHelper(session); - int maxCredBlobLength = session.getCachedInfo().getMaxCredBlobLength(); - byte[] data = new byte[maxCredBlobLength + 1]; - Arrays.fill(data, (byte) 0x01); - - PublicKeyCredential cred = client - .makeCredential( - new CreationOptionsBuilder() - .residentKey(true) - .extensions(Collections.singletonMap(CRED_BLOB, - Base64.toUrlSafeString(data))) - .build() - ); - - Object result = getAttestationResult(cred); - client.deleteCredentials(cred); - Assert.assertNull(result); + // try to create with data > maxCredBlobLength + state.withCtap2( + session -> { + ClientHelper client = new ClientHelper(session); + int maxCredBlobLength = session.getCachedInfo().getMaxCredBlobLength(); + byte[] data = new byte[maxCredBlobLength + 1]; + Arrays.fill(data, (byte) 0x01); + + PublicKeyCredential cred = + client.makeCredential( + new CreationOptionsBuilder() + .residentKey(true) + .extensions(Collections.singletonMap(CRED_BLOB, Base64.toUrlSafeString(data))) + .build()); + + Object result = getAttestationResult(cred); + client.deleteCredentials(cred); + Assert.assertNull(result); }); - // store value - PublicKeyCredential publicKeyCredential = state.withCtap2(session -> { - PublicKeyCredential cred = new ClientHelper(session) - .makeCredential( - new CreationOptionsBuilder() - .residentKey(true) - .extensions(Collections.singletonMap(CRED_BLOB, - Base64.toUrlSafeString(CRED_BLOB_DATA))) - .build() - ); - Assert.assertEquals(Boolean.TRUE, getAttestationResult(cred)); - return cred; + // store value + PublicKeyCredential publicKeyCredential = + state.withCtap2( + session -> { + PublicKeyCredential cred = + new ClientHelper(session) + .makeCredential( + new CreationOptionsBuilder() + .residentKey(true) + .extensions( + Collections.singletonMap( + CRED_BLOB, Base64.toUrlSafeString(CRED_BLOB_DATA))) + .build()); + Assert.assertEquals(Boolean.TRUE, getAttestationResult(cred)); + return cred; + }); + + // no value when extension not requested + state.withCtap2( + session -> { + PublicKeyCredential cred = + new ClientHelper(session) + .getAssertions( + new RequestOptionsBuilder().allowedCredentials(publicKeyCredential).build()); + Assert.assertNull(getAssertionResult(cred)); }); - // no value when extension not requested - state.withCtap2(session -> { - PublicKeyCredential cred = new ClientHelper(session).getAssertions( - new RequestOptionsBuilder() - .allowedCredentials(publicKeyCredential) - .build() - ); - Assert.assertNull(getAssertionResult(cred)); + // no value when extension not explicitly refused + state.withCtap2( + session -> { + PublicKeyCredential cred = + new ClientHelper(session) + .getAssertions( + new RequestOptionsBuilder() + .allowedCredentials(publicKeyCredential) + .extensions(Collections.singletonMap(GET_CRED_BLOB, false)) + .build()); + Assert.assertNull(getAssertionResult(cred)); }); - // no value when extension not explicitly refused - state.withCtap2(session -> { - PublicKeyCredential cred = new ClientHelper(session).getAssertions( - new RequestOptionsBuilder() - .allowedCredentials(publicKeyCredential) - .extensions(Collections.singletonMap(GET_CRED_BLOB, false)) - .build() - ); - Assert.assertNull(getAssertionResult(cred)); + // read value + state.withCtap2( + session -> { + ClientHelper client = new ClientHelper(session); + PublicKeyCredential cred = + client.getAssertions( + new RequestOptionsBuilder() + .allowedCredentials(publicKeyCredential) + .extensions(Collections.singletonMap(GET_CRED_BLOB, true)) + .build()); + Assert.assertArrayEquals(CRED_BLOB_DATA, getAssertionResult(cred)); + client.deleteCredentials(publicKeyCredential); }); - - // read value - state.withCtap2(session -> { - ClientHelper client = new ClientHelper(session); - PublicKeyCredential cred = client.getAssertions( - new RequestOptionsBuilder() - .allowedCredentials(publicKeyCredential) - .extensions(Collections.singletonMap(GET_CRED_BLOB, true)) - .build() - ); - Assert.assertArrayEquals(CRED_BLOB_DATA, getAssertionResult(cred)); - client.deleteCredentials(publicKeyCredential); - }); - } - - @Nullable - private Boolean getAttestationResult(PublicKeyCredential cred) { - AuthenticatorAttestationResponse response = - (AuthenticatorAttestationResponse) cred.getResponse(); - Map extensions = response.getAuthenticatorData().getExtensions(); - return extensions != null ? (Boolean) extensions.get(CRED_BLOB) : null; - } - - @Nullable - private byte[] getAssertionResult(PublicKeyCredential cred) { - AuthenticatorAssertionResponse response = - (AuthenticatorAssertionResponse) cred.getResponse(); - AuthenticatorData authenticatorData = AuthenticatorData.parseFrom( - ByteBuffer.wrap(response.getAuthenticatorData())); - Map extensions = authenticatorData.getExtensions(); - return extensions != null ? (byte[]) extensions.get(CRED_BLOB) : null; - } - + } + + @Nullable private Boolean getAttestationResult(PublicKeyCredential cred) { + AuthenticatorAttestationResponse response = + (AuthenticatorAttestationResponse) cred.getResponse(); + Map extensions = response.getAuthenticatorData().getExtensions(); + return extensions != null ? (Boolean) extensions.get(CRED_BLOB) : null; + } + + @Nullable private byte[] getAssertionResult(PublicKeyCredential cred) { + AuthenticatorAssertionResponse response = (AuthenticatorAssertionResponse) cred.getResponse(); + AuthenticatorData authenticatorData = + AuthenticatorData.parseFrom(ByteBuffer.wrap(response.getAuthenticatorData())); + Map extensions = authenticatorData.getExtensions(); + return extensions != null ? (byte[]) extensions.get(CRED_BLOB) : null; + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/CredPropsExtensionTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/CredPropsExtensionTests.java index a9ba84c7..8b774faf 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/CredPropsExtensionTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/CredPropsExtensionTests.java @@ -22,75 +22,71 @@ import com.yubico.yubikit.testing.fido.FidoTestState; import com.yubico.yubikit.testing.fido.utils.ClientHelper; import com.yubico.yubikit.testing.fido.utils.CreationOptionsBuilder; - -import org.junit.Assert; - import java.util.Collections; import java.util.Map; - import javax.annotation.Nullable; +import org.junit.Assert; public class CredPropsExtensionTests { - private static final String CRED_PROPS = "credProps"; + private static final String CRED_PROPS = "credProps"; - public static void test(FidoTestState state) throws Throwable { - CredPropsExtensionTests extTest = new CredPropsExtensionTests(); - extTest.runTest(state); - } + public static void test(FidoTestState state) throws Throwable { + CredPropsExtensionTests extTest = new CredPropsExtensionTests(); + extTest.runTest(state); + } - private CredPropsExtensionTests() { + private CredPropsExtensionTests() {} - } - - private void runTest(FidoTestState state) throws Throwable { - // no output in results if extension not requested - state.withCtap2(session -> { - PublicKeyCredential cred = new ClientHelper(session).makeCredential(); - Assert.assertNull(getResult(cred)); + private void runTest(FidoTestState state) throws Throwable { + // no output in results if extension not requested + state.withCtap2( + session -> { + PublicKeyCredential cred = new ClientHelper(session).makeCredential(); + Assert.assertNull(getResult(cred)); }); - // rk value is correct (false) during registration - state.withCtap2(session -> { - PublicKeyCredential cred = new ClientHelper(session) - .makeCredential( - new CreationOptionsBuilder() - .residentKey(false) - .extensions(Collections.singletonMap(CRED_PROPS, true)) - .build() - ); - - Assert.assertEquals(Boolean.FALSE, getRkValue(cred)); + // rk value is correct (false) during registration + state.withCtap2( + session -> { + PublicKeyCredential cred = + new ClientHelper(session) + .makeCredential( + new CreationOptionsBuilder() + .residentKey(false) + .extensions(Collections.singletonMap(CRED_PROPS, true)) + .build()); + + Assert.assertEquals(Boolean.FALSE, getRkValue(cred)); }); - // rk value is correct (true) during registration - state.withCtap2(session -> { - ClientHelper client = new ClientHelper(session); - PublicKeyCredential cred = client.makeCredential( - new CreationOptionsBuilder() - .residentKey(true) - .extensions(Collections.singletonMap(CRED_PROPS, true)) - .build() - ); - - Assert.assertEquals(Boolean.TRUE, getRkValue(cred)); - client.deleteCredentials(cred); + // rk value is correct (true) during registration + state.withCtap2( + session -> { + ClientHelper client = new ClientHelper(session); + PublicKeyCredential cred = + client.makeCredential( + new CreationOptionsBuilder() + .residentKey(true) + .extensions(Collections.singletonMap(CRED_PROPS, true)) + .build()); + + Assert.assertEquals(Boolean.TRUE, getRkValue(cred)); + client.deleteCredentials(cred); }); - } - - @SuppressWarnings("unchecked") - @Nullable - private Map getResult(PublicKeyCredential credential) { - ClientExtensionResults results = credential.getClientExtensionResults(); - Assert.assertNotNull(results); - Map resultsMap = results.toMap(SerializationType.JSON); - return (Map) resultsMap.get(CRED_PROPS); - } - - @Nullable - private Object getRkValue(PublicKeyCredential credential) { - Map credProps = getResult(credential); - Assert.assertNotNull(credProps); - return credProps.get("rk"); - } + } + + @SuppressWarnings("unchecked") + @Nullable private Map getResult(PublicKeyCredential credential) { + ClientExtensionResults results = credential.getClientExtensionResults(); + Assert.assertNotNull(results); + Map resultsMap = results.toMap(SerializationType.JSON); + return (Map) resultsMap.get(CRED_PROPS); + } + + @Nullable private Object getRkValue(PublicKeyCredential credential) { + Map credProps = getResult(credential); + Assert.assertNotNull(credProps); + return credProps.get("rk"); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/CredProtectExtensionTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/CredProtectExtensionTests.java index 0b14157f..78c4af8b 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/CredProtectExtensionTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/CredProtectExtensionTests.java @@ -21,90 +21,87 @@ import com.yubico.yubikit.testing.fido.FidoTestState; import com.yubico.yubikit.testing.fido.utils.ClientHelper; import com.yubico.yubikit.testing.fido.utils.CreationOptionsBuilder; - -import org.junit.Assert; -import org.junit.Assume; - import java.util.Collections; import java.util.Map; - import javax.annotation.Nullable; +import org.junit.Assert; +import org.junit.Assume; public class CredProtectExtensionTests { - private static final String CRED_PROTECT = "credProtect"; - private static final String POLICY = "credentialProtectionPolicy"; - private static final String POLICY_OPTIONAL = "userVerificationOptional"; - private static final String POLICY_WITH_LIST = "userVerificationOptionalWithCredentialIDList"; - private static final String POLICY_REQUIRED = "userVerificationRequired"; + private static final String CRED_PROTECT = "credProtect"; + private static final String POLICY = "credentialProtectionPolicy"; + private static final String POLICY_OPTIONAL = "userVerificationOptional"; + private static final String POLICY_WITH_LIST = "userVerificationOptionalWithCredentialIDList"; + private static final String POLICY_REQUIRED = "userVerificationRequired"; - public static void test(FidoTestState state) throws Throwable { - CredProtectExtensionTests extTest = new CredProtectExtensionTests(); - extTest.runTest(state); - } + public static void test(FidoTestState state) throws Throwable { + CredProtectExtensionTests extTest = new CredProtectExtensionTests(); + extTest.runTest(state); + } - private CredProtectExtensionTests() { + private CredProtectExtensionTests() {} - } + private void runTest(FidoTestState state) throws Throwable { - private void runTest(FidoTestState state) throws Throwable { - - state.withCtap2(session -> { - Assume.assumeTrue("credProtect not supported", - session.getCachedInfo().getExtensions().contains(CRED_PROTECT)); - PublicKeyCredential cred = new ClientHelper(session).makeCredential(); - Assert.assertNull(getCredProtectResult(cred)); + state.withCtap2( + session -> { + Assume.assumeTrue( + "credProtect not supported", + session.getCachedInfo().getExtensions().contains(CRED_PROTECT)); + PublicKeyCredential cred = new ClientHelper(session).makeCredential(); + Assert.assertNull(getCredProtectResult(cred)); }); - state.withCtap2(session -> { - PublicKeyCredential cred = new ClientHelper(session) - .makeCredential( - new CreationOptionsBuilder() - .extensions(Collections.singletonMap(POLICY, POLICY_OPTIONAL)) - .build() - ); - - Integer credProtect = getCredProtectResult(cred); - Assert.assertNotNull(credProtect); - Assert.assertEquals(0x01, credProtect.intValue()); + state.withCtap2( + session -> { + PublicKeyCredential cred = + new ClientHelper(session) + .makeCredential( + new CreationOptionsBuilder() + .extensions(Collections.singletonMap(POLICY, POLICY_OPTIONAL)) + .build()); + + Integer credProtect = getCredProtectResult(cred); + Assert.assertNotNull(credProtect); + Assert.assertEquals(0x01, credProtect.intValue()); }); - state.withCtap2(session -> { - PublicKeyCredential cred = new ClientHelper(session) - .makeCredential( - new CreationOptionsBuilder() - .extensions(Collections.singletonMap(POLICY, POLICY_WITH_LIST)) - .build() - ); - - Integer credProtect = getCredProtectResult(cred); - Assert.assertNotNull(credProtect); - Assert.assertEquals(0x02, credProtect.intValue()); + state.withCtap2( + session -> { + PublicKeyCredential cred = + new ClientHelper(session) + .makeCredential( + new CreationOptionsBuilder() + .extensions(Collections.singletonMap(POLICY, POLICY_WITH_LIST)) + .build()); + + Integer credProtect = getCredProtectResult(cred); + Assert.assertNotNull(credProtect); + Assert.assertEquals(0x02, credProtect.intValue()); }); - state.withCtap2(session -> { - ClientHelper client = new ClientHelper(session); - PublicKeyCredential cred = client - .makeCredential( - new CreationOptionsBuilder() - .residentKey(true) - .extensions(Collections.singletonMap(POLICY, POLICY_REQUIRED)) - .build() - ); - - Integer credProtect = getCredProtectResult(cred); - Assert.assertNotNull(credProtect); - Assert.assertEquals(0x03, credProtect.intValue()); - client.deleteCredentials(cred); + state.withCtap2( + session -> { + ClientHelper client = new ClientHelper(session); + PublicKeyCredential cred = + client.makeCredential( + new CreationOptionsBuilder() + .residentKey(true) + .extensions(Collections.singletonMap(POLICY, POLICY_REQUIRED)) + .build()); + + Integer credProtect = getCredProtectResult(cred); + Assert.assertNotNull(credProtect); + Assert.assertEquals(0x03, credProtect.intValue()); + client.deleteCredentials(cred); }); - - } - - @Nullable - private Integer getCredProtectResult(PublicKeyCredential cred) { - AuthenticatorAttestationResponse response = - (AuthenticatorAttestationResponse) cred.getResponse(); - Map extensions = response.getAuthenticatorData().getExtensions(); - return extensions != null ? (Integer) extensions.get(CRED_PROTECT) : null; - } + } + + @Nullable private Integer getCredProtectResult(PublicKeyCredential cred) { + AuthenticatorAttestationResponse response = + (AuthenticatorAttestationResponse) cred.getResponse(); + Map extensions = response.getAuthenticatorData().getExtensions(); + return extensions != null ? (Integer) extensions.get(CRED_PROTECT) : null; + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/HmacSecretExtensionTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/HmacSecretExtensionTests.java index 991517f5..583c356f 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/HmacSecretExtensionTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/HmacSecretExtensionTests.java @@ -26,31 +26,30 @@ import com.yubico.yubikit.testing.fido.utils.ClientHelper; import com.yubico.yubikit.testing.fido.utils.CreationOptionsBuilder; import com.yubico.yubikit.testing.fido.utils.RequestOptionsBuilder; - -import org.junit.Assert; -import org.junit.Assume; - import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; - import javax.annotation.Nullable; +import org.junit.Assert; +import org.junit.Assume; public class HmacSecretExtensionTests { - private static final String KEY_HMAC_SECRET = "hmac-secret"; - private static final String KEY_HMAC_CREATE_SECRET = "hmacCreateSecret"; - private static final String KEY_HMAC_GET_SECRET = "hmacGetSecret"; - private static final String KEY_SALT1 = "salt1"; - private static final String KEY_SALT2 = "salt2"; - private static final String KEY_OUTPUT1 = "output1"; - private static final String KEY_OUTPUT2 = "output2"; + private static final String KEY_HMAC_SECRET = "hmac-secret"; + private static final String KEY_HMAC_CREATE_SECRET = "hmacCreateSecret"; + private static final String KEY_HMAC_GET_SECRET = "hmacGetSecret"; + private static final String KEY_SALT1 = "salt1"; + private static final String KEY_SALT2 = "salt2"; + private static final String KEY_OUTPUT1 = "output1"; + private static final String KEY_OUTPUT2 = "output2"; - private static final List extensions = - Collections.singletonList(new HmacSecretExtension(true)); + private static final List extensions = + Collections.singletonList(new HmacSecretExtension(true)); - private static final String VALUE_SALT1 = Base64.toUrlSafeString(new byte[]{ + private static final String VALUE_SALT1 = + Base64.toUrlSafeString( + new byte[] { 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, @@ -59,222 +58,232 @@ public class HmacSecretExtensionTests { 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, - }); + }); - private final String VALUE_SALT2 = Base64.toUrlSafeString(new byte[]{ + private final String VALUE_SALT2 = + Base64.toUrlSafeString( + new byte[] { 0x01, 0x07, 0x02, 0x08, 0x03, 0x06, 0x04, 0x05, - }); - - public static void test(FidoTestState state) throws Throwable { - HmacSecretExtensionTests extTests = new HmacSecretExtensionTests(); - extTests.runTest(state); - } - - // this test is active only on devices without hmac-secret - public static void testNoExtensionSupport(FidoTestState state) throws Throwable { - HmacSecretExtensionTests extTests = new HmacSecretExtensionTests(); - extTests.runNoSupportTest(state); - } - - private HmacSecretExtensionTests() { - - } - - private void runTest(FidoTestState state) throws Throwable { - - // non-discoverable credential - { - // no output when no input - state.withCtap2(session -> { - Assume.assumeTrue(session.getCachedInfo().getExtensions() - .contains(KEY_HMAC_SECRET)); - PublicKeyCredential cred = new ClientHelper(session, extensions).makeCredential(); - Assert.assertNull(getCreateResult(cred)); - }); - - // no output when hmac-secret not allowed - // input: { hmacSecretCreate: true } - // output: { } - state.withCtap2(session -> { - PublicKeyCredential cred = new ClientHelper(session).makeCredential( - new CreationOptionsBuilder() - .extensions(Collections.singletonMap(KEY_HMAC_CREATE_SECRET, true)) - .build() - ); - - Assert.assertNull(getCreateResult(cred)); - }); - - // input: { hmacSecretCreate: true } - // output: { hmacSecretCreate: true } - PublicKeyCredential publicKeyCredential = state.withCtap2(session -> { - PublicKeyCredential cred = new ClientHelper(session, extensions).makeCredential( + }); + + public static void test(FidoTestState state) throws Throwable { + HmacSecretExtensionTests extTests = new HmacSecretExtensionTests(); + extTests.runTest(state); + } + + // this test is active only on devices without hmac-secret + public static void testNoExtensionSupport(FidoTestState state) throws Throwable { + HmacSecretExtensionTests extTests = new HmacSecretExtensionTests(); + extTests.runNoSupportTest(state); + } + + private HmacSecretExtensionTests() {} + + private void runTest(FidoTestState state) throws Throwable { + + // non-discoverable credential + { + // no output when no input + state.withCtap2( + session -> { + Assume.assumeTrue(session.getCachedInfo().getExtensions().contains(KEY_HMAC_SECRET)); + PublicKeyCredential cred = new ClientHelper(session, extensions).makeCredential(); + Assert.assertNull(getCreateResult(cred)); + }); + + // no output when hmac-secret not allowed + // input: { hmacSecretCreate: true } + // output: { } + state.withCtap2( + session -> { + PublicKeyCredential cred = + new ClientHelper(session) + .makeCredential( new CreationOptionsBuilder() + .extensions(Collections.singletonMap(KEY_HMAC_CREATE_SECRET, true)) + .build()); + + Assert.assertNull(getCreateResult(cred)); + }); + + // input: { hmacSecretCreate: true } + // output: { hmacSecretCreate: true } + PublicKeyCredential publicKeyCredential = + state.withCtap2( + session -> { + PublicKeyCredential cred = + new ClientHelper(session, extensions) + .makeCredential( + new CreationOptionsBuilder() .extensions(Collections.singletonMap(KEY_HMAC_CREATE_SECRET, true)) - .build() - ); + .build()); Assert.assertEquals(Boolean.TRUE, getCreateResult(cred)); return cred; - }); - - // input: { hmacGetSecret: { salt1: String } } - // output: { hmacGetSecret: { output1: String } } - state.withCtap2(session -> { - PublicKeyCredential cred = new ClientHelper(session, extensions) - .getAssertions( - new RequestOptionsBuilder() - // this is no discoverable key, we have to pass the id - .allowedCredentials(publicKeyCredential) - .extensions(Collections.singletonMap(KEY_HMAC_GET_SECRET, - Collections.singletonMap(KEY_SALT1, VALUE_SALT1))) - .build() - ); - - Assert.assertNotNull(getGetResultsValue(cred, KEY_OUTPUT1)); - Assert.assertNull(getGetResultsValue(cred, KEY_OUTPUT2)); - }); - - // input: { hmacGetSecret: { salt1: String, salt2: String } } - // output: { hmacGetSecret: { output1: String, output2: String } } - state.withCtap2(session -> { - - Map salts = new HashMap<>(); - salts.put(KEY_SALT1, VALUE_SALT1); - salts.put(KEY_SALT2, VALUE_SALT2); - - PublicKeyCredential cred = new ClientHelper(session, extensions).getAssertions( + }); + + // input: { hmacGetSecret: { salt1: String } } + // output: { hmacGetSecret: { output1: String } } + state.withCtap2( + session -> { + PublicKeyCredential cred = + new ClientHelper(session, extensions) + .getAssertions( new RequestOptionsBuilder() - // this is no discoverable key, we have to pass the id - .allowedCredentials(publicKeyCredential) - .extensions( - Collections.singletonMap(KEY_HMAC_GET_SECRET, salts)) - .build() - ); - - Assert.assertNotNull(getGetResultsValue(cred, KEY_OUTPUT1)); - Assert.assertNotNull(getGetResultsValue(cred, KEY_OUTPUT2)); - }); - } - - // discoverable credential - { - // no output when no input - state.withCtap2(session -> { - Assume.assumeTrue(session.getCachedInfo().getExtensions() - .contains(KEY_HMAC_SECRET)); - ClientHelper client = new ClientHelper(session, extensions); - PublicKeyCredential cred = client.makeCredential( - new CreationOptionsBuilder() - .residentKey(true) - .build() - ); - Assert.assertNull(getCreateResult(cred)); - client.deleteCredentials(cred); - }); - - // no output when hmac-secret not allowed - // input: { hmacSecretCreate: true } - // output: { } - state.withCtap2(session -> { - ClientHelper client = new ClientHelper(session); - PublicKeyCredential cred = client.makeCredential( - new CreationOptionsBuilder() - .residentKey(true) - .userEntity("tempCred") - .extensions(Collections.singletonMap(KEY_HMAC_CREATE_SECRET, true)) - .build() - ); - Assert.assertNull(getCreateResult(cred)); - // directly delete the temporary credential - client.deleteCredentials(cred); - }); - - // input: { hmacSecretCreate: true } - // output: { hmacSecretCreate: true } - PublicKeyCredential publicKeyCredential = state.withCtap2(session -> { - PublicKeyCredential cred = new ClientHelper(session, extensions).makeCredential( - new CreationOptionsBuilder() + // this is no discoverable key, we have to pass the id + .allowedCredentials(publicKeyCredential) + .extensions( + Collections.singletonMap( + KEY_HMAC_GET_SECRET, + Collections.singletonMap(KEY_SALT1, VALUE_SALT1))) + .build()); + + Assert.assertNotNull(getGetResultsValue(cred, KEY_OUTPUT1)); + Assert.assertNull(getGetResultsValue(cred, KEY_OUTPUT2)); + }); + + // input: { hmacGetSecret: { salt1: String, salt2: String } } + // output: { hmacGetSecret: { output1: String, output2: String } } + state.withCtap2( + session -> { + Map salts = new HashMap<>(); + salts.put(KEY_SALT1, VALUE_SALT1); + salts.put(KEY_SALT2, VALUE_SALT2); + + PublicKeyCredential cred = + new ClientHelper(session, extensions) + .getAssertions( + new RequestOptionsBuilder() + // this is no discoverable key, we have to pass the id + .allowedCredentials(publicKeyCredential) + .extensions(Collections.singletonMap(KEY_HMAC_GET_SECRET, salts)) + .build()); + + Assert.assertNotNull(getGetResultsValue(cred, KEY_OUTPUT1)); + Assert.assertNotNull(getGetResultsValue(cred, KEY_OUTPUT2)); + }); + } + + // discoverable credential + { + // no output when no input + state.withCtap2( + session -> { + Assume.assumeTrue(session.getCachedInfo().getExtensions().contains(KEY_HMAC_SECRET)); + ClientHelper client = new ClientHelper(session, extensions); + PublicKeyCredential cred = + client.makeCredential(new CreationOptionsBuilder().residentKey(true).build()); + Assert.assertNull(getCreateResult(cred)); + client.deleteCredentials(cred); + }); + + // no output when hmac-secret not allowed + // input: { hmacSecretCreate: true } + // output: { } + state.withCtap2( + session -> { + ClientHelper client = new ClientHelper(session); + PublicKeyCredential cred = + client.makeCredential( + new CreationOptionsBuilder() + .residentKey(true) + .userEntity("tempCred") + .extensions(Collections.singletonMap(KEY_HMAC_CREATE_SECRET, true)) + .build()); + Assert.assertNull(getCreateResult(cred)); + // directly delete the temporary credential + client.deleteCredentials(cred); + }); + + // input: { hmacSecretCreate: true } + // output: { hmacSecretCreate: true } + PublicKeyCredential publicKeyCredential = + state.withCtap2( + session -> { + PublicKeyCredential cred = + new ClientHelper(session, extensions) + .makeCredential( + new CreationOptionsBuilder() .residentKey(true) .extensions(Collections.singletonMap(KEY_HMAC_CREATE_SECRET, true)) - .build() - ); + .build()); Assert.assertEquals(Boolean.TRUE, getCreateResult(cred)); return cred; - }); - - // input: { hmacGetSecret: { salt1: String } } - // output: { hmacGetSecret: { output1: String } } - state.withCtap2(session -> { - PublicKeyCredential cred = new ClientHelper(session, extensions) - .getAssertions( - new RequestOptionsBuilder() - .extensions(Collections.singletonMap(KEY_HMAC_GET_SECRET, - Collections.singletonMap(KEY_SALT1, VALUE_SALT1))) - .build() - ); - Assert.assertNotNull(getGetResultsValue(cred, KEY_OUTPUT1)); - Assert.assertNull(getGetResultsValue(cred, KEY_OUTPUT2)); - }); - - // input: { hmacGetSecret: { salt1: String, salt2: String } } - // output: { hmacGetSecret: { output1: String, output2: String } } - state.withCtap2(session -> { - - Map salts = new HashMap<>(); - salts.put(KEY_SALT1, VALUE_SALT1); - salts.put(KEY_SALT2, VALUE_SALT2); - - ClientHelper client = new ClientHelper(session, extensions); - PublicKeyCredential cred = client.getAssertions( + }); + + // input: { hmacGetSecret: { salt1: String } } + // output: { hmacGetSecret: { output1: String } } + state.withCtap2( + session -> { + PublicKeyCredential cred = + new ClientHelper(session, extensions) + .getAssertions( new RequestOptionsBuilder() - .extensions( - Collections.singletonMap(KEY_HMAC_GET_SECRET, salts)) - .build() - ); - - Assert.assertNotNull(getGetResultsValue(cred, KEY_OUTPUT1)); - Assert.assertNotNull(getGetResultsValue(cred, KEY_OUTPUT2)); - - client.deleteCredentials(publicKeyCredential); - }); - } + .extensions( + Collections.singletonMap( + KEY_HMAC_GET_SECRET, + Collections.singletonMap(KEY_SALT1, VALUE_SALT1))) + .build()); + Assert.assertNotNull(getGetResultsValue(cred, KEY_OUTPUT1)); + Assert.assertNull(getGetResultsValue(cred, KEY_OUTPUT2)); + }); + + // input: { hmacGetSecret: { salt1: String, salt2: String } } + // output: { hmacGetSecret: { output1: String, output2: String } } + state.withCtap2( + session -> { + Map salts = new HashMap<>(); + salts.put(KEY_SALT1, VALUE_SALT1); + salts.put(KEY_SALT2, VALUE_SALT2); + + ClientHelper client = new ClientHelper(session, extensions); + PublicKeyCredential cred = + client.getAssertions( + new RequestOptionsBuilder() + .extensions(Collections.singletonMap(KEY_HMAC_GET_SECRET, salts)) + .build()); + + Assert.assertNotNull(getGetResultsValue(cred, KEY_OUTPUT1)); + Assert.assertNotNull(getGetResultsValue(cred, KEY_OUTPUT2)); + + client.deleteCredentials(publicKeyCredential); + }); } - - private void runNoSupportTest(FidoTestState state) throws Throwable { - // input: { hmacCreateSecret: true } - // output: { hmacCreateSecret: false } - state.withCtap2(session -> { - Assume.assumeFalse(session.getCachedInfo().getExtensions().contains(KEY_HMAC_SECRET)); - PublicKeyCredential cred = new ClientHelper(session, extensions) - .makeCredential( - new CreationOptionsBuilder() - .extensions( - Collections.singletonMap(KEY_HMAC_CREATE_SECRET, - Collections.emptyMap())) - .build() - ); - - Assert.assertEquals(Boolean.FALSE, getCreateResult(cred)); + } + + private void runNoSupportTest(FidoTestState state) throws Throwable { + // input: { hmacCreateSecret: true } + // output: { hmacCreateSecret: false } + state.withCtap2( + session -> { + Assume.assumeFalse(session.getCachedInfo().getExtensions().contains(KEY_HMAC_SECRET)); + PublicKeyCredential cred = + new ClientHelper(session, extensions) + .makeCredential( + new CreationOptionsBuilder() + .extensions( + Collections.singletonMap( + KEY_HMAC_CREATE_SECRET, Collections.emptyMap())) + .build()); + + Assert.assertEquals(Boolean.FALSE, getCreateResult(cred)); }); - } - - @Nullable - private Boolean getCreateResult(PublicKeyCredential credential) { - ClientExtensionResults results = credential.getClientExtensionResults(); - Assert.assertNotNull(results); - Map resultsMap = results.toMap(SerializationType.JSON); - return (Boolean) resultsMap.get(KEY_HMAC_CREATE_SECRET); - } - - @SuppressWarnings("unchecked") - @Nullable - private byte[] getGetResultsValue(PublicKeyCredential credential, String key) { - ClientExtensionResults extensionResults = credential.getClientExtensionResults(); - Assert.assertNotNull(extensionResults); - Map resultsMap = extensionResults.toMap(SerializationType.CBOR); - Map getSecretMap = (Map) resultsMap.get(KEY_HMAC_GET_SECRET); - Assert.assertNotNull(getSecretMap); - return (byte[]) getSecretMap.get(key); - } + } + + @Nullable private Boolean getCreateResult(PublicKeyCredential credential) { + ClientExtensionResults results = credential.getClientExtensionResults(); + Assert.assertNotNull(results); + Map resultsMap = results.toMap(SerializationType.JSON); + return (Boolean) resultsMap.get(KEY_HMAC_CREATE_SECRET); + } + + @SuppressWarnings("unchecked") + @Nullable private byte[] getGetResultsValue(PublicKeyCredential credential, String key) { + ClientExtensionResults extensionResults = credential.getClientExtensionResults(); + Assert.assertNotNull(extensionResults); + Map resultsMap = extensionResults.toMap(SerializationType.CBOR); + Map getSecretMap = (Map) resultsMap.get(KEY_HMAC_GET_SECRET); + Assert.assertNotNull(getSecretMap); + return (byte[]) getSecretMap.get(key); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/LargeBlobExtensionTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/LargeBlobExtensionTests.java index af0c921f..0435fb1c 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/LargeBlobExtensionTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/LargeBlobExtensionTests.java @@ -25,181 +25,190 @@ import com.yubico.yubikit.testing.fido.utils.ClientHelper; import com.yubico.yubikit.testing.fido.utils.CreationOptionsBuilder; import com.yubico.yubikit.testing.fido.utils.RequestOptionsBuilder; - -import org.junit.Assert; -import org.junit.Assume; - import java.util.Collections; import java.util.Map; - import javax.annotation.Nullable; +import org.junit.Assert; +import org.junit.Assume; public class LargeBlobExtensionTests { - private static final String LARGE_BLOB = "largeBlob"; - private static final String LARGE_BLOB_KEY = "largeBlobKey"; - private static final String KEY_SUPPORT = "support"; - private static final String KEY_SUPPORTED = "supported"; - private static final String KEY_READ = "read"; - private static final String KEY_WRITE = "write"; - private static final String KEY_BLOB = "blob"; - private static final String KEY_WRITTEN = "written"; - private static final String ATTR_PREFERRED = "preferred"; - private static final String ATTR_REQUIRED = "required"; - - public static void test(FidoTestState state) throws Throwable { - LargeBlobExtensionTests largeBlobExtensionTests = new LargeBlobExtensionTests(); - largeBlobExtensionTests.runTest(state); - } - - private LargeBlobExtensionTests() { - } - - private void runTest(FidoTestState state) throws Throwable { - final byte[] data1 = Codec.fromHex("112211221122112211221122112211"); - final byte[] data2 = Codec.fromHex("990099009900990099009900990099"); - - // no output when no input - state.withCtap2(session -> { - Assume.assumeTrue(session.getCachedInfo().getExtensions().contains(LARGE_BLOB_KEY)); - PublicKeyCredential cred = new ClientHelper(session).makeCredential(); - Map result = getResult(cred); - Assert.assertNull(result); + private static final String LARGE_BLOB = "largeBlob"; + private static final String LARGE_BLOB_KEY = "largeBlobKey"; + private static final String KEY_SUPPORT = "support"; + private static final String KEY_SUPPORTED = "supported"; + private static final String KEY_READ = "read"; + private static final String KEY_WRITE = "write"; + private static final String KEY_BLOB = "blob"; + private static final String KEY_WRITTEN = "written"; + private static final String ATTR_PREFERRED = "preferred"; + private static final String ATTR_REQUIRED = "required"; + + public static void test(FidoTestState state) throws Throwable { + LargeBlobExtensionTests largeBlobExtensionTests = new LargeBlobExtensionTests(); + largeBlobExtensionTests.runTest(state); + } + + private LargeBlobExtensionTests() {} + + private void runTest(FidoTestState state) throws Throwable { + final byte[] data1 = Codec.fromHex("112211221122112211221122112211"); + final byte[] data2 = Codec.fromHex("990099009900990099009900990099"); + + // no output when no input + state.withCtap2( + session -> { + Assume.assumeTrue(session.getCachedInfo().getExtensions().contains(LARGE_BLOB_KEY)); + PublicKeyCredential cred = new ClientHelper(session).makeCredential(); + Map result = getResult(cred); + Assert.assertNull(result); }); - state.withCtap2(session -> { - ClientHelper client = new ClientHelper(session); - PublicKeyCredential cred = client - .makeCredential(new CreationOptionsBuilder() - .residentKey(true) - .extensions(Collections.singletonMap(LARGE_BLOB, - Collections.singletonMap(KEY_SUPPORT, ATTR_PREFERRED))) - .build()); - - Assert.assertEquals(Boolean.TRUE, getResultValue(cred, KEY_SUPPORTED)); - client.deleteCredentials(cred); + state.withCtap2( + session -> { + ClientHelper client = new ClientHelper(session); + PublicKeyCredential cred = + client.makeCredential( + new CreationOptionsBuilder() + .residentKey(true) + .extensions( + Collections.singletonMap( + LARGE_BLOB, Collections.singletonMap(KEY_SUPPORT, ATTR_PREFERRED))) + .build()); + + Assert.assertEquals(Boolean.TRUE, getResultValue(cred, KEY_SUPPORTED)); + client.deleteCredentials(cred); }); - // read and write to different credentials and verify the contents is correct - PublicKeyCredential cred1 = makeCred(state, "User1", Codec.fromHex("010101")); - PublicKeyCredential cred2 = makeCred(state, "User2", Codec.fromHex("020202")); - - Assert.assertEquals(0, readBlob(state, cred1).length); - Assert.assertEquals(0, readBlob(state, cred2).length); - - Assert.assertTrue(writeBlob(state, data1, cred1)); - Assert.assertArrayEquals(data1, readBlob(state, cred1)); - - Assert.assertEquals(0, readBlob(state, cred2).length); - - Assert.assertTrue(writeBlob(state, data2, cred1)); - Assert.assertArrayEquals(data2, readBlob(state, cred1)); - - Assert.assertEquals(0, readBlob(state, cred2).length); - - Assert.assertTrue(writeBlob(state, data1, cred2)); - Assert.assertArrayEquals(data1, readBlob(state, cred2)); - Assert.assertArrayEquals(data2, readBlob(state, cred1)); - - deleteCreds(state, cred1, cred2); - } - - private byte[] readBlob( - FidoTestState state, - @Nullable PublicKeyCredential allowedCredential) throws Throwable { - return state.withCtap2(session -> { - PublicKeyCredential cred = new ClientHelper(session) - .getAssertions( - new RequestOptionsBuilder() - .allowedCredentials(allowedCredential) - .extensions(Collections.singletonMap(LARGE_BLOB, - Collections.singletonMap(KEY_READ, true))) - .build()); - - Map result = getResult(cred); - Assert.assertNotNull(result); // nothing has been written yet - if (result.isEmpty()) { - return new byte[0]; - } else { - Assert.assertNull(getResultValue(cred, KEY_WRITTEN)); - byte[] data = getBlob(cred); - Assert.assertNotNull(data); - return data; - } + // read and write to different credentials and verify the contents is correct + PublicKeyCredential cred1 = makeCred(state, "User1", Codec.fromHex("010101")); + PublicKeyCredential cred2 = makeCred(state, "User2", Codec.fromHex("020202")); + + Assert.assertEquals(0, readBlob(state, cred1).length); + Assert.assertEquals(0, readBlob(state, cred2).length); + + Assert.assertTrue(writeBlob(state, data1, cred1)); + Assert.assertArrayEquals(data1, readBlob(state, cred1)); + + Assert.assertEquals(0, readBlob(state, cred2).length); + + Assert.assertTrue(writeBlob(state, data2, cred1)); + Assert.assertArrayEquals(data2, readBlob(state, cred1)); + + Assert.assertEquals(0, readBlob(state, cred2).length); + + Assert.assertTrue(writeBlob(state, data1, cred2)); + Assert.assertArrayEquals(data1, readBlob(state, cred2)); + Assert.assertArrayEquals(data2, readBlob(state, cred1)); + + deleteCreds(state, cred1, cred2); + } + + private byte[] readBlob(FidoTestState state, @Nullable PublicKeyCredential allowedCredential) + throws Throwable { + return state.withCtap2( + session -> { + PublicKeyCredential cred = + new ClientHelper(session) + .getAssertions( + new RequestOptionsBuilder() + .allowedCredentials(allowedCredential) + .extensions( + Collections.singletonMap( + LARGE_BLOB, Collections.singletonMap(KEY_READ, true))) + .build()); + + Map result = getResult(cred); + Assert.assertNotNull(result); // nothing has been written yet + if (result.isEmpty()) { + return new byte[0]; + } else { + Assert.assertNull(getResultValue(cred, KEY_WRITTEN)); + byte[] data = getBlob(cred); + Assert.assertNotNull(data); + return data; + } }); - } - - private boolean writeBlob( - FidoTestState state, byte[] data, - @Nullable PublicKeyCredential allowedCredential) throws Throwable { - return state.withCtap2(session -> { - PublicKeyCredential cred = new ClientHelper(session) - .getAssertions(new RequestOptionsBuilder() - .allowedCredentials(allowedCredential) - .extensions(Collections.singletonMap(LARGE_BLOB, - Collections.singletonMap(KEY_WRITE, Base64.toUrlSafeString(data)))) - .build()); - - Assert.assertNull(getResultValue(cred, KEY_BLOB)); - Boolean writtenValue = (Boolean) getResultValue(cred, KEY_WRITTEN); - Assert.assertEquals(Boolean.TRUE, writtenValue); - return writtenValue; + } + + private boolean writeBlob( + FidoTestState state, byte[] data, @Nullable PublicKeyCredential allowedCredential) + throws Throwable { + return state.withCtap2( + session -> { + PublicKeyCredential cred = + new ClientHelper(session) + .getAssertions( + new RequestOptionsBuilder() + .allowedCredentials(allowedCredential) + .extensions( + Collections.singletonMap( + LARGE_BLOB, + Collections.singletonMap( + KEY_WRITE, Base64.toUrlSafeString(data)))) + .build()); + + Assert.assertNull(getResultValue(cred, KEY_BLOB)); + Boolean writtenValue = (Boolean) getResultValue(cred, KEY_WRITTEN); + Assert.assertEquals(Boolean.TRUE, writtenValue); + return writtenValue; }); - } - - private PublicKeyCredential makeCred( - FidoTestState state, - String name, - byte[] id) throws Throwable { - return state.withCtap2(session -> { - PublicKeyCredential cred = new ClientHelper(session) - .makeCredential(new CreationOptionsBuilder() - .userEntity(name, id) - .residentKey(true) - .extensions(Collections.singletonMap(LARGE_BLOB, - Collections.singletonMap(KEY_SUPPORT, ATTR_REQUIRED))) - .build()); - Assert.assertEquals(Boolean.TRUE, getResultValue(cred, KEY_SUPPORTED)); - return cred; + } + + private PublicKeyCredential makeCred(FidoTestState state, String name, byte[] id) + throws Throwable { + return state.withCtap2( + session -> { + PublicKeyCredential cred = + new ClientHelper(session) + .makeCredential( + new CreationOptionsBuilder() + .userEntity(name, id) + .residentKey(true) + .extensions( + Collections.singletonMap( + LARGE_BLOB, Collections.singletonMap(KEY_SUPPORT, ATTR_REQUIRED))) + .build()); + Assert.assertEquals(Boolean.TRUE, getResultValue(cred, KEY_SUPPORTED)); + return cred; }); - } + } - private void deleteCreds(FidoTestState state, PublicKeyCredential... creds) throws Throwable { - state.withCtap2(session -> { - ClientHelper client = new ClientHelper(session); - client.deleteCredentials(creds); + private void deleteCreds(FidoTestState state, PublicKeyCredential... creds) throws Throwable { + state.withCtap2( + session -> { + ClientHelper client = new ClientHelper(session); + client.deleteCredentials(creds); }); - } - - @Nullable - private Object getResultValue(PublicKeyCredential credential, String key) { - Map largeBlob = getResult(credential); - Assert.assertNotNull(largeBlob); - return largeBlob.get(key); - } - - @Nullable - private byte[] getBlob(PublicKeyCredential credential) { - Map largeBlob = getResult(credential, SerializationType.CBOR); - Assert.assertNotNull(largeBlob); - return (byte[]) largeBlob.get(KEY_BLOB); - } - - @SuppressWarnings("unchecked") - @Nullable - private Map getResult(PublicKeyCredential cred, SerializationType serializationType) { - ClientExtensionResults results = cred.getClientExtensionResults();; - Assert.assertNotNull(results); - Map resultsMap = results.toMap(serializationType); - return (Map) resultsMap.get(LARGE_BLOB); - } - - @SuppressWarnings("unchecked") - @Nullable - private Map getResult(PublicKeyCredential cred) { - ClientExtensionResults results = cred.getClientExtensionResults();; - Assert.assertNotNull(results); - Map resultsMap = results.toMap(SerializationType.JSON); - return (Map) resultsMap.get(LARGE_BLOB); - } -} \ No newline at end of file + } + + @Nullable private Object getResultValue(PublicKeyCredential credential, String key) { + Map largeBlob = getResult(credential); + Assert.assertNotNull(largeBlob); + return largeBlob.get(key); + } + + @Nullable private byte[] getBlob(PublicKeyCredential credential) { + Map largeBlob = getResult(credential, SerializationType.CBOR); + Assert.assertNotNull(largeBlob); + return (byte[]) largeBlob.get(KEY_BLOB); + } + + @SuppressWarnings("unchecked") + @Nullable private Map getResult(PublicKeyCredential cred, SerializationType serializationType) { + ClientExtensionResults results = cred.getClientExtensionResults(); + ; + Assert.assertNotNull(results); + Map resultsMap = results.toMap(serializationType); + return (Map) resultsMap.get(LARGE_BLOB); + } + + @SuppressWarnings("unchecked") + @Nullable private Map getResult(PublicKeyCredential cred) { + ClientExtensionResults results = cred.getClientExtensionResults(); + ; + Assert.assertNotNull(results); + Map resultsMap = results.toMap(SerializationType.JSON); + return (Map) resultsMap.get(LARGE_BLOB); + } +} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/MinPinLengthExtensionTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/MinPinLengthExtensionTests.java index e25ab48e..2b0c5c14 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/MinPinLengthExtensionTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/MinPinLengthExtensionTests.java @@ -23,77 +23,75 @@ import com.yubico.yubikit.testing.fido.utils.ClientHelper; import com.yubico.yubikit.testing.fido.utils.ConfigHelper; import com.yubico.yubikit.testing.fido.utils.CreationOptionsBuilder; - -import org.junit.Assert; -import org.junit.Assume; - import java.util.Collections; import java.util.Map; - import javax.annotation.Nullable; +import org.junit.Assert; +import org.junit.Assume; public class MinPinLengthExtensionTests { - private static final String MIN_PIN_LENGTH = "minPinLength"; + private static final String MIN_PIN_LENGTH = "minPinLength"; - public static void test(FidoTestState state) throws Throwable { - MinPinLengthExtensionTests extTest = new MinPinLengthExtensionTests(); - extTest.runTest(state); - } + public static void test(FidoTestState state) throws Throwable { + MinPinLengthExtensionTests extTest = new MinPinLengthExtensionTests(); + extTest.runTest(state); + } - private MinPinLengthExtensionTests() { + private MinPinLengthExtensionTests() {} - } + private void runTest(FidoTestState state) throws Throwable { - private void runTest(FidoTestState state) throws Throwable { - - state.withCtap2(session -> { - Assume.assumeTrue("minPinLength not supported", - session.getCachedInfo().getExtensions().contains(MIN_PIN_LENGTH)); - PublicKeyCredential cred = new ClientHelper(session).makeCredential(); - Assert.assertNull(getMinPinLength(cred)); + state.withCtap2( + session -> { + Assume.assumeTrue( + "minPinLength not supported", + session.getCachedInfo().getExtensions().contains(MIN_PIN_LENGTH)); + PublicKeyCredential cred = new ClientHelper(session).makeCredential(); + Assert.assertNull(getMinPinLength(cred)); }); - state.withCtap2(session -> { - // setup the authenticator to contain incorrect minPinLengthRPIDs - Config config = ConfigHelper.getConfig(session, state); - config.setMinPinLength(null, Collections.singletonList("wrongrpid.com"), null); - - PublicKeyCredential cred = new ClientHelper(session) - .makeCredential( - new CreationOptionsBuilder() - .extensions(Collections.singletonMap(MIN_PIN_LENGTH, true)) - .build() - ); - - Integer minPinLength = getMinPinLength(cred); - Assert.assertNull(minPinLength); + state.withCtap2( + session -> { + // setup the authenticator to contain incorrect minPinLengthRPIDs + Config config = ConfigHelper.getConfig(session, state); + config.setMinPinLength(null, Collections.singletonList("wrongrpid.com"), null); + + PublicKeyCredential cred = + new ClientHelper(session) + .makeCredential( + new CreationOptionsBuilder() + .extensions(Collections.singletonMap(MIN_PIN_LENGTH, true)) + .build()); + + Integer minPinLength = getMinPinLength(cred); + Assert.assertNull(minPinLength); }); - state.withCtap2(session -> { - // setup the authenticator to contain correct minPinLengthRPIDs - Config config = ConfigHelper.getConfig(session, state); - config.setMinPinLength(null, Collections.singletonList("example.com"), null); - - PublicKeyCredential cred = new ClientHelper(session) - .makeCredential( - new CreationOptionsBuilder() - .extensions(Collections.singletonMap(MIN_PIN_LENGTH, true)) - .build() - ); - - Integer optionsMinPinLength = session.getCachedInfo().getMinPinLength(); - Assert.assertNotNull(optionsMinPinLength); - Integer minPinLength = getMinPinLength(cred); - Assert.assertNotNull(minPinLength); - Assert.assertEquals(optionsMinPinLength, minPinLength); + state.withCtap2( + session -> { + // setup the authenticator to contain correct minPinLengthRPIDs + Config config = ConfigHelper.getConfig(session, state); + config.setMinPinLength(null, Collections.singletonList("example.com"), null); + + PublicKeyCredential cred = + new ClientHelper(session) + .makeCredential( + new CreationOptionsBuilder() + .extensions(Collections.singletonMap(MIN_PIN_LENGTH, true)) + .build()); + + Integer optionsMinPinLength = session.getCachedInfo().getMinPinLength(); + Assert.assertNotNull(optionsMinPinLength); + Integer minPinLength = getMinPinLength(cred); + Assert.assertNotNull(minPinLength); + Assert.assertEquals(optionsMinPinLength, minPinLength); }); - } - - @Nullable - private Integer getMinPinLength(PublicKeyCredential cred) { - AuthenticatorAttestationResponse response = - (AuthenticatorAttestationResponse) cred.getResponse(); - Map extensions = response.getAuthenticatorData().getExtensions(); - return extensions != null ? (Integer) extensions.get(MIN_PIN_LENGTH) : null; - } + } + + @Nullable private Integer getMinPinLength(PublicKeyCredential cred) { + AuthenticatorAttestationResponse response = + (AuthenticatorAttestationResponse) cred.getResponse(); + Map extensions = response.getAuthenticatorData().getExtensions(); + return extensions != null ? (Integer) extensions.get(MIN_PIN_LENGTH) : null; + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/PrfExtensionTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/PrfExtensionTests.java index 1a80fcb7..8cdcf563 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/PrfExtensionTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/extensions/PrfExtensionTests.java @@ -24,294 +24,288 @@ import com.yubico.yubikit.testing.fido.utils.ClientHelper; import com.yubico.yubikit.testing.fido.utils.CreationOptionsBuilder; import com.yubico.yubikit.testing.fido.utils.RequestOptionsBuilder; - -import org.junit.Assert; -import org.junit.Assume; - import java.util.Collections; import java.util.HashMap; import java.util.Map; - import javax.annotation.Nullable; +import org.junit.Assert; +import org.junit.Assume; public class PrfExtensionTests { - private static final String KEY_HMAC_SECRET = "hmac-secret"; - private static final String PRF_EXT = "prf"; - private static final String KEY_ENABLED = "enabled"; - private static final String KEY_EVAL = "eval"; - private static final String KEY_EVAL_BY_CREDENTIAL = "evalByCredential"; - private static final String KEY_FIRST = "first"; - private static final String KEY_SECOND = "second"; - - public static void test(FidoTestState state) throws Throwable { - PrfExtensionTests extTests = new PrfExtensionTests(); - extTests.runTest(state); - } - - // this test is active only on devices without hmac-secret - public static void testNoExtensionSupport(FidoTestState state) throws Throwable { - PrfExtensionTests extTests = new PrfExtensionTests(); - extTests.runNoSupportTest(state); - } - - private PrfExtensionTests() { - - } - - private void runTest(FidoTestState state) throws Throwable { - - // non-discoverable credential - { - // no output when no input - state.withCtap2(session -> { - Assume.assumeTrue(session.getCachedInfo().getExtensions() - .contains(KEY_HMAC_SECRET)); - PublicKeyCredential cred = new ClientHelper(session).makeCredential(); - Map result = getResult(cred); - Assert.assertNull(result); - }); - - // input: { prf: {} } - // output: { prf: { enabled: true } } - PublicKeyCredential publicKeyCredential = state.withCtap2(session -> { - PublicKeyCredential cred = new ClientHelper(session) + private static final String KEY_HMAC_SECRET = "hmac-secret"; + private static final String PRF_EXT = "prf"; + private static final String KEY_ENABLED = "enabled"; + private static final String KEY_EVAL = "eval"; + private static final String KEY_EVAL_BY_CREDENTIAL = "evalByCredential"; + private static final String KEY_FIRST = "first"; + private static final String KEY_SECOND = "second"; + + public static void test(FidoTestState state) throws Throwable { + PrfExtensionTests extTests = new PrfExtensionTests(); + extTests.runTest(state); + } + + // this test is active only on devices without hmac-secret + public static void testNoExtensionSupport(FidoTestState state) throws Throwable { + PrfExtensionTests extTests = new PrfExtensionTests(); + extTests.runNoSupportTest(state); + } + + private PrfExtensionTests() {} + + private void runTest(FidoTestState state) throws Throwable { + + // non-discoverable credential + { + // no output when no input + state.withCtap2( + session -> { + Assume.assumeTrue(session.getCachedInfo().getExtensions().contains(KEY_HMAC_SECRET)); + PublicKeyCredential cred = new ClientHelper(session).makeCredential(); + Map result = getResult(cred); + Assert.assertNull(result); + }); + + // input: { prf: {} } + // output: { prf: { enabled: true } } + PublicKeyCredential publicKeyCredential = + state.withCtap2( + session -> { + PublicKeyCredential cred = + new ClientHelper(session) .makeCredential( - new CreationOptionsBuilder() - .extensions( - Collections.singletonMap(PRF_EXT, - Collections.emptyMap())) - .build() - ); + new CreationOptionsBuilder() + .extensions( + Collections.singletonMap(PRF_EXT, Collections.emptyMap())) + .build()); Assert.assertEquals(Boolean.TRUE, getResultValue(cred, KEY_ENABLED)); return cred; - }); - - // input: { prf: { eval: { first: String } } } - // output: { prf: { results: { first: String } } } - state.withCtap2(session -> { - PublicKeyCredential cred = new ClientHelper(session) - .getAssertions( - new RequestOptionsBuilder() - // this is no discoverable key, we have to pass the id - .allowedCredentials(publicKeyCredential) - .extensions( - Collections.singletonMap(PRF_EXT, - Collections.singletonMap(KEY_EVAL, - Collections.singletonMap(KEY_FIRST, - "abba")))) - .build() - ); - - Assert.assertNull(getResultValue(cred, KEY_ENABLED)); - Assert.assertTrue(getResultsValue(cred, KEY_FIRST) instanceof String); - Assert.assertNull(getResultsValue(cred, KEY_SECOND)); - }); - - // input: { prf: { eval: { first: String, second: String } } } - // output: { prf: { results: { first: String, second: String } } } - state.withCtap2(session -> { - - Map eval = new HashMap<>(); - eval.put(KEY_FIRST, "abba"); - eval.put(KEY_SECOND, "bebe"); - - PublicKeyCredential cred = new ClientHelper(session) - .getAssertions( - new RequestOptionsBuilder() - // this is no discoverable key, we have to pass the id - .allowedCredentials(publicKeyCredential) - .extensions(Collections.singletonMap(PRF_EXT, - Collections.singletonMap(KEY_EVAL, eval))) - .build() - ); - - Assert.assertNull(getResultValue(cred, KEY_ENABLED)); - Assert.assertTrue(getResultsValue(cred, KEY_FIRST) instanceof String); - Assert.assertTrue(getResultsValue(cred, KEY_SECOND) instanceof String); - - }); - - - // create 2 more credentials - PublicKeyCredential publicKeyCredential2 = state.withCtap2(session -> { - PublicKeyCredential cred = new ClientHelper(session) + }); + + // input: { prf: { eval: { first: String } } } + // output: { prf: { results: { first: String } } } + state.withCtap2( + session -> { + PublicKeyCredential cred = + new ClientHelper(session) + .getAssertions( + new RequestOptionsBuilder() + // this is no discoverable key, we have to pass the id + .allowedCredentials(publicKeyCredential) + .extensions( + Collections.singletonMap( + PRF_EXT, + Collections.singletonMap( + KEY_EVAL, Collections.singletonMap(KEY_FIRST, "abba")))) + .build()); + + Assert.assertNull(getResultValue(cred, KEY_ENABLED)); + Assert.assertTrue(getResultsValue(cred, KEY_FIRST) instanceof String); + Assert.assertNull(getResultsValue(cred, KEY_SECOND)); + }); + + // input: { prf: { eval: { first: String, second: String } } } + // output: { prf: { results: { first: String, second: String } } } + state.withCtap2( + session -> { + Map eval = new HashMap<>(); + eval.put(KEY_FIRST, "abba"); + eval.put(KEY_SECOND, "bebe"); + + PublicKeyCredential cred = + new ClientHelper(session) + .getAssertions( + new RequestOptionsBuilder() + // this is no discoverable key, we have to pass the id + .allowedCredentials(publicKeyCredential) + .extensions( + Collections.singletonMap( + PRF_EXT, Collections.singletonMap(KEY_EVAL, eval))) + .build()); + + Assert.assertNull(getResultValue(cred, KEY_ENABLED)); + Assert.assertTrue(getResultsValue(cred, KEY_FIRST) instanceof String); + Assert.assertTrue(getResultsValue(cred, KEY_SECOND) instanceof String); + }); + + // create 2 more credentials + PublicKeyCredential publicKeyCredential2 = + state.withCtap2( + session -> { + PublicKeyCredential cred = + new ClientHelper(session) .makeCredential( - new CreationOptionsBuilder() - .extensions( - Collections.singletonMap(PRF_EXT, - Collections.emptyMap())) - .build() - ); + new CreationOptionsBuilder() + .extensions( + Collections.singletonMap(PRF_EXT, Collections.emptyMap())) + .build()); Assert.assertEquals(Boolean.TRUE, getResultValue(cred, KEY_ENABLED)); return cred; - }); + }); - PublicKeyCredential publicKeyCredential3 = state.withCtap2(session -> { - PublicKeyCredential cred = new ClientHelper(session) + PublicKeyCredential publicKeyCredential3 = + state.withCtap2( + session -> { + PublicKeyCredential cred = + new ClientHelper(session) .makeCredential( - new CreationOptionsBuilder() - .extensions( - Collections.singletonMap(PRF_EXT, - Collections.emptyMap())) - .build() - ); + new CreationOptionsBuilder() + .extensions( + Collections.singletonMap(PRF_EXT, Collections.emptyMap())) + .build()); Assert.assertEquals(Boolean.TRUE, getResultValue(cred, KEY_ENABLED)); return cred; - }); - - // evalByCredential - state.withCtap2(session -> { - Map evalByCredential = new HashMap<>(); - evalByCredential.put( - Base64.toUrlSafeString(publicKeyCredential3.getRawId()), - Collections.singletonMap(KEY_FIRST, "abba")); - evalByCredential.put( - Base64.toUrlSafeString(publicKeyCredential2.getRawId()), - Collections.singletonMap(KEY_FIRST, "bebe")); - evalByCredential.put( - Base64.toUrlSafeString(publicKeyCredential.getRawId()), - Collections.singletonMap(KEY_FIRST, "cece")); - - - PublicKeyCredential cred = new ClientHelper(session) - .getAssertions( - new RequestOptionsBuilder() - // evalByCredential requires allow list - .allowedCredentials( - publicKeyCredential, - publicKeyCredential2, - publicKeyCredential3) - .extensions(Collections.singletonMap(PRF_EXT, - Collections.singletonMap(KEY_EVAL_BY_CREDENTIAL, - evalByCredential - ))) - .build() - ); - - Assert.assertNull(getResultValue(cred, KEY_ENABLED)); - Assert.assertTrue(getResultsValue(cred, KEY_FIRST) instanceof String); - Assert.assertNull(getResultsValue(cred, KEY_SECOND)); - }); - - - } - - // discoverable credential - { - // no output when no input - state.withCtap2(session -> { - Assume.assumeTrue(session.getCachedInfo().getExtensions() - .contains(KEY_HMAC_SECRET)); - ClientHelper client = new ClientHelper(session); - PublicKeyCredential cred = client.makeCredential( - new CreationOptionsBuilder() - .residentKey(true) - .build() - ); - Assert.assertNull(getResult(cred)); - client.deleteCredentials(cred); - }); - - // input: { prf: {} } - // output: { prf: { enabled: true } } - PublicKeyCredential publicKeyCredential = state.withCtap2(session -> { - PublicKeyCredential cred = new ClientHelper(session) + }); + + // evalByCredential + state.withCtap2( + session -> { + Map evalByCredential = new HashMap<>(); + evalByCredential.put( + Base64.toUrlSafeString(publicKeyCredential3.getRawId()), + Collections.singletonMap(KEY_FIRST, "abba")); + evalByCredential.put( + Base64.toUrlSafeString(publicKeyCredential2.getRawId()), + Collections.singletonMap(KEY_FIRST, "bebe")); + evalByCredential.put( + Base64.toUrlSafeString(publicKeyCredential.getRawId()), + Collections.singletonMap(KEY_FIRST, "cece")); + + PublicKeyCredential cred = + new ClientHelper(session) + .getAssertions( + new RequestOptionsBuilder() + // evalByCredential requires allow list + .allowedCredentials( + publicKeyCredential, publicKeyCredential2, publicKeyCredential3) + .extensions( + Collections.singletonMap( + PRF_EXT, + Collections.singletonMap( + KEY_EVAL_BY_CREDENTIAL, evalByCredential))) + .build()); + + Assert.assertNull(getResultValue(cred, KEY_ENABLED)); + Assert.assertTrue(getResultsValue(cred, KEY_FIRST) instanceof String); + Assert.assertNull(getResultsValue(cred, KEY_SECOND)); + }); + } + + // discoverable credential + { + // no output when no input + state.withCtap2( + session -> { + Assume.assumeTrue(session.getCachedInfo().getExtensions().contains(KEY_HMAC_SECRET)); + ClientHelper client = new ClientHelper(session); + PublicKeyCredential cred = + client.makeCredential(new CreationOptionsBuilder().residentKey(true).build()); + Assert.assertNull(getResult(cred)); + client.deleteCredentials(cred); + }); + + // input: { prf: {} } + // output: { prf: { enabled: true } } + PublicKeyCredential publicKeyCredential = + state.withCtap2( + session -> { + PublicKeyCredential cred = + new ClientHelper(session) .makeCredential( - new CreationOptionsBuilder() - .residentKey(true) - .extensions(Collections.singletonMap(PRF_EXT, - Collections.emptyMap())) - .build() - ); + new CreationOptionsBuilder() + .residentKey(true) + .extensions( + Collections.singletonMap(PRF_EXT, Collections.emptyMap())) + .build()); Assert.assertEquals(Boolean.TRUE, getResultValue(cred, KEY_ENABLED)); return cred; - }); - - // input: { prf: { eval: { first: String } } } - // output: { prf: { results: { first: String } } } - state.withCtap2(session -> { - PublicKeyCredential cred = new ClientHelper(session) - .getAssertions( - new RequestOptionsBuilder() - .extensions(Collections.singletonMap(PRF_EXT, - Collections.singletonMap(KEY_EVAL, - Collections.singletonMap(KEY_FIRST, - "abba")))) - .build() - ); - Assert.assertNull(getResultValue(cred, KEY_ENABLED)); - Assert.assertTrue(getResultsValue(cred, KEY_FIRST) instanceof String); - Assert.assertNull(getResultsValue(cred, KEY_SECOND)); - }); - - // input: { prf: { eval: { first: String, second: String } } } - // output: { prf: { results: { first: String, second: String } } } - state.withCtap2(session -> { - - Map eval = new HashMap<>(); - eval.put(KEY_FIRST, "abba"); - eval.put(KEY_SECOND, "bebe"); - - ClientHelper client = new ClientHelper(session); - PublicKeyCredential cred = client.getAssertions( + }); + + // input: { prf: { eval: { first: String } } } + // output: { prf: { results: { first: String } } } + state.withCtap2( + session -> { + PublicKeyCredential cred = + new ClientHelper(session) + .getAssertions( new RequestOptionsBuilder() - .extensions(Collections.singletonMap(PRF_EXT, - Collections.singletonMap(KEY_EVAL, eval))) - .build() - ); - Assert.assertNull(getResultValue(cred, KEY_ENABLED)); - Assert.assertTrue(getResultsValue(cred, KEY_FIRST) instanceof String); - Assert.assertTrue(getResultsValue(cred, KEY_SECOND) instanceof String); - - client.deleteCredentials(publicKeyCredential); - }); - } + .extensions( + Collections.singletonMap( + PRF_EXT, + Collections.singletonMap( + KEY_EVAL, Collections.singletonMap(KEY_FIRST, "abba")))) + .build()); + Assert.assertNull(getResultValue(cred, KEY_ENABLED)); + Assert.assertTrue(getResultsValue(cred, KEY_FIRST) instanceof String); + Assert.assertNull(getResultsValue(cred, KEY_SECOND)); + }); + + // input: { prf: { eval: { first: String, second: String } } } + // output: { prf: { results: { first: String, second: String } } } + state.withCtap2( + session -> { + Map eval = new HashMap<>(); + eval.put(KEY_FIRST, "abba"); + eval.put(KEY_SECOND, "bebe"); + + ClientHelper client = new ClientHelper(session); + PublicKeyCredential cred = + client.getAssertions( + new RequestOptionsBuilder() + .extensions( + Collections.singletonMap( + PRF_EXT, Collections.singletonMap(KEY_EVAL, eval))) + .build()); + Assert.assertNull(getResultValue(cred, KEY_ENABLED)); + Assert.assertTrue(getResultsValue(cred, KEY_FIRST) instanceof String); + Assert.assertTrue(getResultsValue(cred, KEY_SECOND) instanceof String); + + client.deleteCredentials(publicKeyCredential); + }); } - - private void runNoSupportTest(FidoTestState state) throws Throwable { - // input: { prf: {} } - // output: { prf: { enabled: false } } - state.withCtap2(session -> { - Assume.assumeFalse(session.getCachedInfo().getExtensions().contains(KEY_HMAC_SECRET)); - PublicKeyCredential cred = new ClientHelper(session) - .makeCredential( - new CreationOptionsBuilder() - .extensions(Collections.singletonMap(PRF_EXT, - Collections.emptyMap())) - .build() - ); - Map result = getResult(cred); - Assert.assertNotNull(result); - Assert.assertEquals(Boolean.FALSE, result.get(KEY_ENABLED)); + } + + private void runNoSupportTest(FidoTestState state) throws Throwable { + // input: { prf: {} } + // output: { prf: { enabled: false } } + state.withCtap2( + session -> { + Assume.assumeFalse(session.getCachedInfo().getExtensions().contains(KEY_HMAC_SECRET)); + PublicKeyCredential cred = + new ClientHelper(session) + .makeCredential( + new CreationOptionsBuilder() + .extensions(Collections.singletonMap(PRF_EXT, Collections.emptyMap())) + .build()); + Map result = getResult(cred); + Assert.assertNotNull(result); + Assert.assertEquals(Boolean.FALSE, result.get(KEY_ENABLED)); }); - } - - @SuppressWarnings("unchecked") - @Nullable - private Object getResultsValue(PublicKeyCredential credential, String key) { - String KEY_RESULTS = "results"; - Map results = (Map) getResultValue(credential, KEY_RESULTS); - Assert.assertNotNull(results); - return results.get(key); - } - - @Nullable - private Object getResultValue(PublicKeyCredential credential, String key) { - Map prf = getResult(credential); - Assert.assertNotNull(prf); - return prf.get(key); - } - - @SuppressWarnings("unchecked") - @Nullable - private Map getResult(PublicKeyCredential credential) { - ClientExtensionResults results = credential.getClientExtensionResults(); - Assert.assertNotNull(results); - Map resultsMap = results.toMap(SerializationType.JSON); - return (Map) resultsMap.get(PRF_EXT); - } + } + + @SuppressWarnings("unchecked") + @Nullable private Object getResultsValue(PublicKeyCredential credential, String key) { + String KEY_RESULTS = "results"; + Map results = (Map) getResultValue(credential, KEY_RESULTS); + Assert.assertNotNull(results); + return results.get(key); + } + + @Nullable private Object getResultValue(PublicKeyCredential credential, String key) { + Map prf = getResult(credential); + Assert.assertNotNull(prf); + return prf.get(key); + } + + @SuppressWarnings("unchecked") + @Nullable private Map getResult(PublicKeyCredential credential) { + ClientExtensionResults results = credential.getClientExtensionResults(); + Assert.assertNotNull(results); + Map resultsMap = results.toMap(SerializationType.JSON); + return (Map) resultsMap.get(PRF_EXT); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/utils/ClientHelper.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/utils/ClientHelper.java index 8348bf3e..8c9ed9fd 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/utils/ClientHelper.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/utils/ClientHelper.java @@ -28,83 +28,66 @@ import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialDescriptor; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialRequestOptions; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialType; - import java.io.IOException; import java.util.ArrayList; import java.util.List; public class ClientHelper { - final BasicWebAuthnClient client; + final BasicWebAuthnClient client; - public ClientHelper(Ctap2Session ctap) throws IOException, CommandException { - this.client = new BasicWebAuthnClient(ctap); - } + public ClientHelper(Ctap2Session ctap) throws IOException, CommandException { + this.client = new BasicWebAuthnClient(ctap); + } - public ClientHelper(Ctap2Session ctap, List extensions) - throws IOException, CommandException { - this.client = new BasicWebAuthnClient(ctap, extensions); - } + public ClientHelper(Ctap2Session ctap, List extensions) + throws IOException, CommandException { + this.client = new BasicWebAuthnClient(ctap, extensions); + } - public PublicKeyCredential makeCredential() throws IOException, CommandException, ClientError { - return makeCredential(new CreationOptionsBuilder().build()); - } + public PublicKeyCredential makeCredential() throws IOException, CommandException, ClientError { + return makeCredential(new CreationOptionsBuilder().build()); + } - public PublicKeyCredential makeCredential( - PublicKeyCredentialCreationOptions options - ) throws IOException, CommandException, ClientError { - return client.makeCredential( - TestData.CLIENT_DATA_JSON_CREATE, - options, - TestData.RP_ID, - TestData.PIN, - null, - null - ); - } + public PublicKeyCredential makeCredential(PublicKeyCredentialCreationOptions options) + throws IOException, CommandException, ClientError { + return client.makeCredential( + TestData.CLIENT_DATA_JSON_CREATE, options, TestData.RP_ID, TestData.PIN, null, null); + } - public PublicKeyCredential getAssertions(PublicKeyCredentialRequestOptions options) - throws IOException, CommandException, ClientError, MultipleAssertionsAvailable { - return client.getAssertion( - TestData.CLIENT_DATA_JSON_GET, - options, - TestData.RP_ID, - TestData.PIN, - null - ); - } + public PublicKeyCredential getAssertions(PublicKeyCredentialRequestOptions options) + throws IOException, CommandException, ClientError, MultipleAssertionsAvailable { + return client.getAssertion( + TestData.CLIENT_DATA_JSON_GET, options, TestData.RP_ID, TestData.PIN, null); + } - public void deleteCredentialsByIds( - List credIds - ) throws IOException, CommandException, ClientError { - try { - CredentialManager credentialManager = client.getCredentialManager(TestData.PIN); - for (byte[] credId : credIds) { - credentialManager.deleteCredential( - new PublicKeyCredentialDescriptor( - PublicKeyCredentialType.PUBLIC_KEY, - credId, - null)); - } - } catch (IllegalStateException ignored) { - // credential manager might not be supported - } + public void deleteCredentialsByIds(List credIds) + throws IOException, CommandException, ClientError { + try { + CredentialManager credentialManager = client.getCredentialManager(TestData.PIN); + for (byte[] credId : credIds) { + credentialManager.deleteCredential( + new PublicKeyCredentialDescriptor(PublicKeyCredentialType.PUBLIC_KEY, credId, null)); + } + } catch (IllegalStateException ignored) { + // credential manager might not be supported } + } - public void deleteCredentials(List credentials) - throws IOException, CommandException, ClientError { - List credIds = new ArrayList<>(); - for (PublicKeyCredential credential : credentials) { - credIds.add(credential.getRawId()); - } - deleteCredentialsByIds(credIds); + public void deleteCredentials(List credentials) + throws IOException, CommandException, ClientError { + List credIds = new ArrayList<>(); + for (PublicKeyCredential credential : credentials) { + credIds.add(credential.getRawId()); } + deleteCredentialsByIds(credIds); + } - public void deleteCredentials(PublicKeyCredential... credentials) - throws IOException, CommandException, ClientError { - List credIds = new ArrayList<>(); - for (PublicKeyCredential credential : credentials) { - credIds.add(credential.getRawId()); - } - deleteCredentialsByIds(credIds); + public void deleteCredentials(PublicKeyCredential... credentials) + throws IOException, CommandException, ClientError { + List credIds = new ArrayList<>(); + for (PublicKeyCredential credential : credentials) { + credIds.add(credential.getRawId()); } -} \ No newline at end of file + deleteCredentialsByIds(credIds); + } +} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/utils/ConfigHelper.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/utils/ConfigHelper.java index 89110a36..12b47adf 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/utils/ConfigHelper.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/utils/ConfigHelper.java @@ -21,16 +21,13 @@ import com.yubico.yubikit.fido.ctap.Config; import com.yubico.yubikit.fido.ctap.Ctap2Session; import com.yubico.yubikit.testing.fido.FidoTestState; - import java.io.IOException; public class ConfigHelper { - public static Config getConfig( - Ctap2Session session, - FidoTestState state - ) throws IOException, CommandException { - ClientPin clientPin = new ClientPin(session, state.getPinUvAuthProtocol()); - byte[] pinToken = clientPin.getPinToken(TestData.PIN, ClientPin.PIN_PERMISSION_ACFG, null); - return new Config(session, state.getPinUvAuthProtocol(), pinToken); - } + public static Config getConfig(Ctap2Session session, FidoTestState state) + throws IOException, CommandException { + ClientPin clientPin = new ClientPin(session, state.getPinUvAuthProtocol()); + byte[] pinToken = clientPin.getPinToken(TestData.PIN, ClientPin.PIN_PERMISSION_ACFG, null); + return new Config(session, state.getPinUvAuthProtocol(), pinToken); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/utils/CreationOptionsBuilder.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/utils/CreationOptionsBuilder.java index ca0acf3f..a9d6a83d 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/utils/CreationOptionsBuilder.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/utils/CreationOptionsBuilder.java @@ -26,102 +26,91 @@ import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialRpEntity; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialUserEntity; import com.yubico.yubikit.fido.webauthn.ResidentKeyRequirement; - import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; - import javax.annotation.Nullable; public class CreationOptionsBuilder { - boolean residentKey = false; - @Nullable - Extensions extensions = null; - @Nullable - PublicKeyCredentialUserEntity userEntity = null; - @Nullable - List excludeCredentials = null; - - public CreationOptionsBuilder residentKey(boolean residentKey) { - this.residentKey = residentKey; - return this; + boolean residentKey = false; + @Nullable Extensions extensions = null; + @Nullable PublicKeyCredentialUserEntity userEntity = null; + @Nullable List excludeCredentials = null; + + public CreationOptionsBuilder residentKey(boolean residentKey) { + this.residentKey = residentKey; + return this; + } + + public CreationOptionsBuilder extensions(@Nullable Map extensions) { + this.extensions = extensions == null ? null : Extensions.fromMap(extensions); + return this; + } + + public CreationOptionsBuilder userEntity(String name) { + this.userEntity = + new PublicKeyCredentialUserEntity(name, name.getBytes(StandardCharsets.UTF_8), name); + return this; + } + + public CreationOptionsBuilder userEntity(String name, byte[] id) { + this.userEntity = new PublicKeyCredentialUserEntity(name, id, name); + return this; + } + + public CreationOptionsBuilder userEntity(PublicKeyCredentialUserEntity userEntity) { + this.userEntity = userEntity; + return this; + } + + public CreationOptionsBuilder excludeCredentialDescriptors( + List excludeCredentials) { + this.excludeCredentials = excludeCredentials; + return this; + } + + public CreationOptionsBuilder excludeCredentials(List credentials) { + List list = new ArrayList<>(); + for (PublicKeyCredential credential : credentials) { + list.add(new PublicKeyCredentialDescriptor(PUBLIC_KEY, credential.getRawId())); } - - public CreationOptionsBuilder extensions(@Nullable Map extensions) { - this.extensions = extensions == null - ? null - : Extensions.fromMap(extensions); - return this; - } - - public CreationOptionsBuilder userEntity(String name) { - this.userEntity = new PublicKeyCredentialUserEntity( - name, - name.getBytes(StandardCharsets.UTF_8), - name); - return this; + excludeCredentials = list; + return this; + } + + public CreationOptionsBuilder excludeCredentials( + PublicKeyCredentialDescriptor... excludeCredentials) { + this.excludeCredentials = Arrays.asList(excludeCredentials); + return this; + } + + public CreationOptionsBuilder excludeCredentials(PublicKeyCredential... credentials) { + excludeCredentials = new ArrayList<>(); + for (PublicKeyCredential cred : credentials) { + excludeCredentials.add(new PublicKeyCredentialDescriptor(PUBLIC_KEY, cred.getRawId())); } - - public CreationOptionsBuilder userEntity(String name, byte[] id) { - this.userEntity = new PublicKeyCredentialUserEntity(name, id, name); - return this; - } - - public CreationOptionsBuilder userEntity(PublicKeyCredentialUserEntity userEntity) { - this.userEntity = userEntity; - return this; - } - - public CreationOptionsBuilder excludeCredentialDescriptors( - List excludeCredentials) { - this.excludeCredentials = excludeCredentials; - return this; - } - - public CreationOptionsBuilder excludeCredentials(List credentials) { - List list = new ArrayList<>(); - for (PublicKeyCredential credential : credentials) { - list.add(new PublicKeyCredentialDescriptor(PUBLIC_KEY, credential.getRawId())); - } - excludeCredentials = list; - return this; - } - - public CreationOptionsBuilder excludeCredentials( - PublicKeyCredentialDescriptor... excludeCredentials) { - this.excludeCredentials = Arrays.asList(excludeCredentials); - return this; - } - - public CreationOptionsBuilder excludeCredentials(PublicKeyCredential... credentials) { - excludeCredentials = new ArrayList<>(); - for (PublicKeyCredential cred : credentials) { - excludeCredentials.add(new PublicKeyCredentialDescriptor(PUBLIC_KEY, cred.getRawId())); - } - return this; - } - - public PublicKeyCredentialCreationOptions build() { - PublicKeyCredentialRpEntity rp = TestData.RP; - AuthenticatorSelectionCriteria criteria = new AuthenticatorSelectionCriteria( - null, - residentKey ? ResidentKeyRequirement.REQUIRED : ResidentKeyRequirement.DISCOURAGED, - null - ); - return new PublicKeyCredentialCreationOptions( - rp, - userEntity != null ? userEntity : TestData.USER, - TestData.CHALLENGE, - Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), - (long) 90000, - excludeCredentials, - criteria, - null, - extensions - ); - } - + return this; + } + + public PublicKeyCredentialCreationOptions build() { + PublicKeyCredentialRpEntity rp = TestData.RP; + AuthenticatorSelectionCriteria criteria = + new AuthenticatorSelectionCriteria( + null, + residentKey ? ResidentKeyRequirement.REQUIRED : ResidentKeyRequirement.DISCOURAGED, + null); + return new PublicKeyCredentialCreationOptions( + rp, + userEntity != null ? userEntity : TestData.USER, + TestData.CHALLENGE, + Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256), + (long) 90000, + excludeCredentials, + criteria, + null, + extensions); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/utils/RequestOptionsBuilder.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/utils/RequestOptionsBuilder.java index a3674ba4..256503a4 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/utils/RequestOptionsBuilder.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/utils/RequestOptionsBuilder.java @@ -22,65 +22,60 @@ import com.yubico.yubikit.fido.webauthn.PublicKeyCredential; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialDescriptor; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialRequestOptions; - import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; - import javax.annotation.Nullable; public class RequestOptionsBuilder { - @Nullable - Extensions extensions = null; - @Nullable - List allowedCredentials = null; + @Nullable Extensions extensions = null; + @Nullable List allowedCredentials = null; - public RequestOptionsBuilder extensions(@Nullable Map extensions) { - this.extensions = extensions == null - ? null - : Extensions.fromMap(extensions); - return this; - } + public RequestOptionsBuilder extensions(@Nullable Map extensions) { + this.extensions = extensions == null ? null : Extensions.fromMap(extensions); + return this; + } - public RequestOptionsBuilder allowedCredentials(byte[]... allowedCredentials) { - this.allowedCredentials = allowedCredentials.length > 0 ? Arrays.stream(allowedCredentials) + public RequestOptionsBuilder allowedCredentials(byte[]... allowedCredentials) { + this.allowedCredentials = + allowedCredentials.length > 0 + ? Arrays.stream(allowedCredentials) .map(id -> new PublicKeyCredentialDescriptor(PUBLIC_KEY, id)) - .collect(Collectors.toList()) : null; - return this; - } + .collect(Collectors.toList()) + : null; + return this; + } - public RequestOptionsBuilder allowedCredentials(PublicKeyCredential... allowedCredentials) { - this.allowedCredentials = allowedCredentials.length > 0 ? Arrays.stream(allowedCredentials) - .map(publicKeyCredential -> new PublicKeyCredentialDescriptor( - PUBLIC_KEY, publicKeyCredential.getRawId())) - .collect(Collectors.toList()) : null; - return this; - } + public RequestOptionsBuilder allowedCredentials(PublicKeyCredential... allowedCredentials) { + this.allowedCredentials = + allowedCredentials.length > 0 + ? Arrays.stream(allowedCredentials) + .map( + publicKeyCredential -> + new PublicKeyCredentialDescriptor( + PUBLIC_KEY, publicKeyCredential.getRawId())) + .collect(Collectors.toList()) + : null; + return this; + } - public RequestOptionsBuilder allowedCredentials( - PublicKeyCredentialDescriptor... allowedCredentials) { - this.allowedCredentials = allowedCredentials.length > 0 - ? Arrays.asList(allowedCredentials) - : null; - return this; - } + public RequestOptionsBuilder allowedCredentials( + PublicKeyCredentialDescriptor... allowedCredentials) { + this.allowedCredentials = + allowedCredentials.length > 0 ? Arrays.asList(allowedCredentials) : null; + return this; + } - public RequestOptionsBuilder allowedCredentials( - List allowedCredentials) { - this.allowedCredentials = allowedCredentials; - return this; - } + public RequestOptionsBuilder allowedCredentials( + List allowedCredentials) { + this.allowedCredentials = allowedCredentials; + return this; + } - public PublicKeyCredentialRequestOptions build() { + public PublicKeyCredentialRequestOptions build() { - return new PublicKeyCredentialRequestOptions( - TestData.CHALLENGE, - (long) 90000, - TestData.RP_ID, - allowedCredentials, - null, - extensions - ); - } -} \ No newline at end of file + return new PublicKeyCredentialRequestOptions( + TestData.CHALLENGE, (long) 90000, TestData.RP_ID, allowedCredentials, null, extensions); + } +} diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/utils/TestData.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/utils/TestData.java index 398625ca..a5eaf707 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/utils/TestData.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/utils/TestData.java @@ -21,61 +21,75 @@ import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialRpEntity; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialType; import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialUserEntity; - -import org.bouncycastle.util.encoders.Base64; - import java.nio.charset.StandardCharsets; +import org.bouncycastle.util.encoders.Base64; class TestData { - static class ClientData { - @SuppressWarnings("unused") public final String type; - @SuppressWarnings("unused") public final String origin; - @SuppressWarnings("unused") public final String challenge; - @SuppressWarnings("unused") public final String androidPackageName; - - public ClientData(String type, String origin, byte[] challenge, String androidPackageName) { - this.type = type; - this.origin = origin; - this.challenge = Base64.toBase64String(challenge); - this.androidPackageName = androidPackageName; - } - } - - public static final char[] PIN = "11234567".toCharArray(); - public static final char[] OTHER_PIN = "11231234".toCharArray(); + static class ClientData { + @SuppressWarnings("unused") + public final String type; - public static final String RP_ID = "example.com"; - public static final String RP_NAME = "Example Company"; - public static final PublicKeyCredentialRpEntity RP = new PublicKeyCredentialRpEntity(RP_NAME, RP_ID); + @SuppressWarnings("unused") + public final String origin; - public static final String USER_NAME = "john.doe@example.com"; - public static final byte[] USER_ID = USER_NAME.getBytes(StandardCharsets.UTF_8); - public static final String USER_DISPLAY_NAME = "John Doe"; - public static final PublicKeyCredentialUserEntity USER = new PublicKeyCredentialUserEntity(USER_NAME, USER_ID, USER_DISPLAY_NAME); + @SuppressWarnings("unused") + public final String challenge; - public static final String ORIGIN = "https://" + RP_ID; - public static final byte[] CHALLENGE = new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + @SuppressWarnings("unused") + public final String androidPackageName; - private static final String PACKAGE_NAME = "TestPackage"; - - public static final byte[] CLIENT_DATA_JSON_CREATE = new Moshi.Builder().build().adapter(ClientData.class).toJson(new ClientData( - "webauthn.create", - ORIGIN, - CHALLENGE, - PACKAGE_NAME - )).getBytes(StandardCharsets.UTF_8); - - public static final byte[] CLIENT_DATA_JSON_GET = new Moshi.Builder().build().adapter(ClientData.class).toJson(new ClientData( - "webauthn.get", - ORIGIN, - CHALLENGE, - PACKAGE_NAME - )).getBytes(StandardCharsets.UTF_8); - - public static final byte[] CLIENT_DATA_HASH = new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; - - public static final PublicKeyCredentialParameters PUB_KEY_CRED_PARAMS_ES256 = new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, -7); - - public static final PublicKeyCredentialParameters PUB_KEY_CRED_PARAMS_EDDSA = new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, -8); + public ClientData(String type, String origin, byte[] challenge, String androidPackageName) { + this.type = type; + this.origin = origin; + this.challenge = Base64.toBase64String(challenge); + this.androidPackageName = androidPackageName; + } + } + + public static final char[] PIN = "11234567".toCharArray(); + public static final char[] OTHER_PIN = "11231234".toCharArray(); + + public static final String RP_ID = "example.com"; + public static final String RP_NAME = "Example Company"; + public static final PublicKeyCredentialRpEntity RP = + new PublicKeyCredentialRpEntity(RP_NAME, RP_ID); + + public static final String USER_NAME = "john.doe@example.com"; + public static final byte[] USER_ID = USER_NAME.getBytes(StandardCharsets.UTF_8); + public static final String USER_DISPLAY_NAME = "John Doe"; + public static final PublicKeyCredentialUserEntity USER = + new PublicKeyCredentialUserEntity(USER_NAME, USER_ID, USER_DISPLAY_NAME); + + public static final String ORIGIN = "https://" + RP_ID; + public static final byte[] CHALLENGE = + new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + + private static final String PACKAGE_NAME = "TestPackage"; + + public static final byte[] CLIENT_DATA_JSON_CREATE = + new Moshi.Builder() + .build() + .adapter(ClientData.class) + .toJson(new ClientData("webauthn.create", ORIGIN, CHALLENGE, PACKAGE_NAME)) + .getBytes(StandardCharsets.UTF_8); + + public static final byte[] CLIENT_DATA_JSON_GET = + new Moshi.Builder() + .build() + .adapter(ClientData.class) + .toJson(new ClientData("webauthn.get", ORIGIN, CHALLENGE, PACKAGE_NAME)) + .getBytes(StandardCharsets.UTF_8); + + public static final byte[] CLIENT_DATA_HASH = + new byte[] { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + 12, 13, 14, 15 + }; + + public static final PublicKeyCredentialParameters PUB_KEY_CRED_PARAMS_ES256 = + new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, -7); + + public static final PublicKeyCredentialParameters PUB_KEY_CRED_PARAMS_EDDSA = + new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, -8); } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/management/ManagementDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/management/ManagementDeviceTests.java index 6c5c61d8..e9235fdf 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/management/ManagementDeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/management/ManagementDeviceTests.java @@ -18,17 +18,16 @@ import com.yubico.yubikit.management.DeviceConfig; import com.yubico.yubikit.management.ManagementSession; - import org.junit.Assert; import org.junit.Assume; public class ManagementDeviceTests { - public static void testNfcRestricted(ManagementSession managementSession) throws Exception { - Assume.assumeTrue(managementSession.getVersion().isAtLeast(5,7,0)); - managementSession.updateDeviceConfig( - new DeviceConfig.Builder().nfcRestricted(true).build(), - false, null, null); + public static void testNfcRestricted(ManagementSession managementSession) throws Exception { + Assume.assumeTrue(managementSession.getVersion().isAtLeast(5, 7, 0)); + managementSession.updateDeviceConfig( + new DeviceConfig.Builder().nfcRestricted(true).build(), false, null, null); - Assert.assertEquals(Boolean.TRUE, managementSession.getDeviceInfo().getConfig().getNfcRestricted()); - } + Assert.assertEquals( + Boolean.TRUE, managementSession.getDeviceInfo().getConfig().getNfcRestricted()); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/mpe/MpeTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/mpe/MpeTestState.java index 9714187d..483e6c70 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/mpe/MpeTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/mpe/MpeTestState.java @@ -33,66 +33,65 @@ import com.yubico.yubikit.testing.piv.PivTestState; public class MpeTestState extends TestState { - public static class Builder extends TestState.Builder { + public static class Builder extends TestState.Builder { - public Builder(YubiKeyDevice device, UsbPid usbPid) { - super(device, usbPid); - } - - @Override - public Builder getThis() { - return this; - } + public Builder(YubiKeyDevice device, UsbPid usbPid) { + super(device, usbPid); + } - public MpeTestState build() throws Throwable { - return new MpeTestState(this); - } + @Override + public Builder getThis() { + return this; } - protected MpeTestState(MpeTestState.Builder builder) throws Throwable { - super(builder); + public MpeTestState build() throws Throwable { + return new MpeTestState(this); + } + } - DeviceInfo deviceInfo = getDeviceInfo(); - assumeTrue("Cannot get device information", deviceInfo != null); - assumeTrue("Not a MPE device", isMpe(deviceInfo)); + protected MpeTestState(MpeTestState.Builder builder) throws Throwable { + super(builder); - try (SmartCardConnection connection = openSmartCardConnection()) { - final ManagementSession managementSession = getManagementSession(connection, scpParameters); - managementSession.deviceReset(); - } + DeviceInfo deviceInfo = getDeviceInfo(); + assumeTrue("Cannot get device information", deviceInfo != null); + assumeTrue("Not a MPE device", isMpe(deviceInfo)); - // PIV and FIDO2 should not be reset blocked - assertFalse(isPivResetBlocked()); - assertFalse(isFidoResetBlocked()); + try (SmartCardConnection connection = openSmartCardConnection()) { + final ManagementSession managementSession = getManagementSession(connection, scpParameters); + managementSession.deviceReset(); } - boolean isPivResetBlocked() { - final DeviceInfo deviceInfo = getDeviceInfo(); - return (deviceInfo.getResetBlocked() & Capability.PIV.bit) == Capability.PIV.bit; - } + // PIV and FIDO2 should not be reset blocked + assertFalse(isPivResetBlocked()); + assertFalse(isFidoResetBlocked()); + } - boolean isFidoResetBlocked() { - final DeviceInfo deviceInfo = getDeviceInfo(); - return (deviceInfo.getResetBlocked() & Capability.FIDO2.bit) == Capability.FIDO2.bit; - } + boolean isPivResetBlocked() { + final DeviceInfo deviceInfo = getDeviceInfo(); + return (deviceInfo.getResetBlocked() & Capability.PIV.bit) == Capability.PIV.bit; + } + + boolean isFidoResetBlocked() { + final DeviceInfo deviceInfo = getDeviceInfo(); + return (deviceInfo.getResetBlocked() & Capability.FIDO2.bit) == Capability.FIDO2.bit; + } - public void withPiv(StatefulSessionCallback callback) - throws Throwable { - try (SmartCardConnection connection = openSmartCardConnection()) { - final PivSession piv = PivTestState.getPivSession(connection, scpParameters); - assumeTrue("No PIV support", piv != null); - callback.invoke(piv, this); - } - reconnect(); + public void withPiv(StatefulSessionCallback callback) throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + final PivSession piv = PivTestState.getPivSession(connection, scpParameters); + assumeTrue("No PIV support", piv != null); + callback.invoke(piv, this); } + reconnect(); + } - public void withCtap2(TestState.StatefulSessionCallback callback) - throws Throwable { - try (YubiKeyConnection connection = openConnection()) { - final Ctap2Session ctap2 = FidoTestState.getCtap2Session(connection); - assumeTrue("No CTAP2 support", ctap2 != null); - callback.invoke(ctap2, this); - } - reconnect(); + public void withCtap2(TestState.StatefulSessionCallback callback) + throws Throwable { + try (YubiKeyConnection connection = openConnection()) { + final Ctap2Session ctap2 = FidoTestState.getCtap2Session(connection); + assumeTrue("No CTAP2 support", ctap2 != null); + callback.invoke(ctap2, this); } + reconnect(); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/mpe/MultiProtocolResetDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/mpe/MultiProtocolResetDeviceTests.java index a1f94615..b2f26f86 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/mpe/MultiProtocolResetDeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/mpe/MultiProtocolResetDeviceTests.java @@ -30,48 +30,48 @@ import com.yubico.yubikit.piv.Slot; import com.yubico.yubikit.piv.TouchPolicy; import com.yubico.yubikit.testing.piv.PivTestUtils; - -import org.bouncycastle.util.encoders.Hex; - import java.io.IOException; import java.security.KeyPair; import java.util.Objects; +import org.bouncycastle.util.encoders.Hex; public class MultiProtocolResetDeviceTests { - public static void testSettingPivPinBlocksFidoReset(PivSession piv, MpeTestState state) throws Throwable { - piv.changePin("123456".toCharArray(), "multipin".toCharArray()); - assertFalse(state.isPivResetBlocked()); - assertTrue(state.isFidoResetBlocked()); - } + public static void testSettingPivPinBlocksFidoReset(PivSession piv, MpeTestState state) + throws Throwable { + piv.changePin("123456".toCharArray(), "multipin".toCharArray()); + assertFalse(state.isPivResetBlocked()); + assertTrue(state.isFidoResetBlocked()); + } - public static void testPivOperationBlocksFidoReset(PivSession piv, MpeTestState state) throws IOException, CommandException { - KeyPair rsaKeyPair = PivTestUtils.loadKey(KeyType.RSA1024); - piv.authenticate(Hex.decode("010203040506070801020304050607080102030405060708")); - piv.putKey(Slot.RETIRED1, - PrivateKeyValues.fromPrivateKey( - rsaKeyPair.getPrivate()), - PinPolicy.DEFAULT, - TouchPolicy.DEFAULT); + public static void testPivOperationBlocksFidoReset(PivSession piv, MpeTestState state) + throws IOException, CommandException { + KeyPair rsaKeyPair = PivTestUtils.loadKey(KeyType.RSA1024); + piv.authenticate(Hex.decode("010203040506070801020304050607080102030405060708")); + piv.putKey( + Slot.RETIRED1, + PrivateKeyValues.fromPrivateKey(rsaKeyPair.getPrivate()), + PinPolicy.DEFAULT, + TouchPolicy.DEFAULT); - assertFalse(state.isPivResetBlocked()); - assertTrue(state.isFidoResetBlocked()); - } + assertFalse(state.isPivResetBlocked()); + assertTrue(state.isFidoResetBlocked()); + } - public static void testSettingFidoPinBlocksPivReset(Ctap2Session ctap2, MpeTestState state) throws IOException, CommandException { + public static void testSettingFidoPinBlocksPivReset(Ctap2Session ctap2, MpeTestState state) + throws IOException, CommandException { - PinUvAuthProtocol pinUvAuthProtocol = new PinUvAuthProtocolV2(); - // note that max PIN length is 8 because it is shared with PIV - char[] defaultPin = "11234567".toCharArray(); + PinUvAuthProtocol pinUvAuthProtocol = new PinUvAuthProtocolV2(); + // note that max PIN length is 8 because it is shared with PIV + char[] defaultPin = "11234567".toCharArray(); - Ctap2Session.InfoData info = ctap2.getCachedInfo(); - ClientPin pin = new ClientPin(ctap2, pinUvAuthProtocol); - boolean pinSet = Objects.requireNonNull((Boolean) info.getOptions().get("clientPin")); - assertFalse(pinSet); - pin.setPin(defaultPin); + Ctap2Session.InfoData info = ctap2.getCachedInfo(); + ClientPin pin = new ClientPin(ctap2, pinUvAuthProtocol); + boolean pinSet = Objects.requireNonNull((Boolean) info.getOptions().get("clientPin")); + assertFalse(pinSet); + pin.setPin(defaultPin); - assertTrue(state.isPivResetBlocked()); - assertFalse(state.isFidoResetBlocked()); - } + assertTrue(state.isPivResetBlocked()); + assertFalse(state.isFidoResetBlocked()); + } } - diff --git a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathDeviceTests.java index dc26f71e..4676d877 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathDeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathDeviceTests.java @@ -28,95 +28,95 @@ import com.yubico.yubikit.oath.Credential; import com.yubico.yubikit.oath.CredentialData; import com.yubico.yubikit.oath.OathSession; - -import org.junit.Assume; - import java.net.URI; import java.util.List; +import org.junit.Assume; public class OathDeviceTests { - private static final char[] CHANGED_PASSWORD = "12341234".toCharArray(); + private static final char[] CHANGED_PASSWORD = "12341234".toCharArray(); - public static void testChangePassword(OathTestState state) throws Throwable { + public static void testChangePassword(OathTestState state) throws Throwable { - state.withOath(oath -> { - assertTrue(oath.isAccessKeySet()); - assertTrue(oath.isLocked()); - assertFalse(oath.unlock(CHANGED_PASSWORD)); - assertTrue(oath.unlock(state.password)); - oath.setPassword(CHANGED_PASSWORD); + state.withOath( + oath -> { + assertTrue(oath.isAccessKeySet()); + assertTrue(oath.isLocked()); + assertFalse(oath.unlock(CHANGED_PASSWORD)); + assertTrue(oath.unlock(state.password)); + oath.setPassword(CHANGED_PASSWORD); }); - state.withOath(oath -> { - assertTrue(oath.isAccessKeySet()); - assertTrue(oath.isLocked()); - assertTrue(oath.unlock(CHANGED_PASSWORD)); + state.withOath( + oath -> { + assertTrue(oath.isAccessKeySet()); + assertTrue(oath.isLocked()); + assertTrue(oath.unlock(CHANGED_PASSWORD)); }); + } + + public static void testRemovePassword(OathSession oath, OathTestState state) throws Exception { + assertTrue(oath.isAccessKeySet()); + assertTrue(oath.isLocked()); + assertTrue(oath.unlock(state.password)); + + if (state.isFipsApproved) { + // trying remove password from a FIPS approved key throws specific ApduException + ApduException apduException = assertThrows(ApduException.class, oath::deleteAccessKey); + assertEquals(SW.CONDITIONS_NOT_SATISFIED, apduException.getSw()); + // the key is still password protected + assertTrue(oath.isAccessKeySet()); + } else { + oath.deleteAccessKey(); + assertFalse(oath.isAccessKeySet()); + assertFalse(oath.isLocked()); } - - public static void testRemovePassword(OathSession oath, OathTestState state) throws Exception { - assertTrue(oath.isAccessKeySet()); - assertTrue(oath.isLocked()); - assertTrue(oath.unlock(state.password)); - - if (state.isFipsApproved) { - // trying remove password from a FIPS approved key throws specific ApduException - ApduException apduException = assertThrows(ApduException.class, oath::deleteAccessKey); - assertEquals(SW.CONDITIONS_NOT_SATISFIED, apduException.getSw()); - // the key is still password protected - assertTrue(oath.isAccessKeySet()); - } else { - oath.deleteAccessKey(); - assertFalse(oath.isAccessKeySet()); - assertFalse(oath.isLocked()); - } - } - - public static void testAccountManagement(OathSession oath, OathTestState state) throws Exception { - assertTrue(oath.unlock(state.password)); - List credentials = oath.getCredentials(); - assertEquals(0, credentials.size()); - final String uri = "otpauth://totp/foobar:bob@example.com?secret=abba"; - CredentialData credentialData = CredentialData.parseUri(new URI(uri)); - oath.putCredential(credentialData, false); - - credentials = oath.getCredentials(); - assertEquals(1, credentials.size()); - Credential credential = credentials.get(0); - assertEquals("bob@example.com", credential.getAccountName()); - assertEquals("foobar", credential.getIssuer()); - - oath.deleteCredential(credential.getId()); - credentials = oath.getCredentials(); - assertEquals(0, credentials.size()); - } - - public static void testRenameAccount(OathSession oath, OathTestState state) throws Exception { - Assume.assumeTrue(oath.supports(FEATURE_RENAME)); - assertTrue(oath.unlock(state.password)); - List credentials = oath.getCredentials(); - assertEquals(0, credentials.size()); - final String uri = "otpauth://totp/foobar:bob@example.com?secret=abba"; - CredentialData credentialData = CredentialData.parseUri(new URI(uri)); - oath.putCredential(credentialData, false); - - credentials = oath.getCredentials(); - assertEquals(1, credentials.size()); - - Credential credential = credentials.get(0); - credential = oath.renameCredential(credential, "ann@example.com", null); - assertEquals("ann@example.com", credential.getAccountName()); - assertNull(credential.getIssuer()); - - credentials = oath.getCredentials(); - assertEquals(1, credentials.size()); - credential = credentials.get(0); - assertEquals("ann@example.com", credential.getAccountName()); - assertNull(credential.getIssuer()); - - oath.deleteCredential(credential.getId()); - credentials = oath.getCredentials(); - assertEquals(0, credentials.size()); - } + } + + public static void testAccountManagement(OathSession oath, OathTestState state) throws Exception { + assertTrue(oath.unlock(state.password)); + List credentials = oath.getCredentials(); + assertEquals(0, credentials.size()); + final String uri = "otpauth://totp/foobar:bob@example.com?secret=abba"; + CredentialData credentialData = CredentialData.parseUri(new URI(uri)); + oath.putCredential(credentialData, false); + + credentials = oath.getCredentials(); + assertEquals(1, credentials.size()); + Credential credential = credentials.get(0); + assertEquals("bob@example.com", credential.getAccountName()); + assertEquals("foobar", credential.getIssuer()); + + oath.deleteCredential(credential.getId()); + credentials = oath.getCredentials(); + assertEquals(0, credentials.size()); + } + + public static void testRenameAccount(OathSession oath, OathTestState state) throws Exception { + Assume.assumeTrue(oath.supports(FEATURE_RENAME)); + assertTrue(oath.unlock(state.password)); + List credentials = oath.getCredentials(); + assertEquals(0, credentials.size()); + final String uri = "otpauth://totp/foobar:bob@example.com?secret=abba"; + CredentialData credentialData = CredentialData.parseUri(new URI(uri)); + oath.putCredential(credentialData, false); + + credentials = oath.getCredentials(); + assertEquals(1, credentials.size()); + + Credential credential = credentials.get(0); + credential = oath.renameCredential(credential, "ann@example.com", null); + assertEquals("ann@example.com", credential.getAccountName()); + assertNull(credential.getIssuer()); + + credentials = oath.getCredentials(); + assertEquals(1, credentials.size()); + credential = credentials.get(0); + assertEquals("ann@example.com", credential.getAccountName()); + assertNull(credential.getIssuer()); + + oath.deleteCredential(credential.getId()); + credentials = oath.getCredentials(); + assertEquals(0, credentials.size()); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java index b597e300..bd1f67ef 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/oath/OathTestState.java @@ -27,98 +27,93 @@ import com.yubico.yubikit.oath.OathSession; import com.yubico.yubikit.testing.ScpParameters; import com.yubico.yubikit.testing.TestState; - import java.io.IOException; - import javax.annotation.Nullable; public class OathTestState extends TestState { - public boolean isFipsApproved; - public char[] password; + public boolean isFipsApproved; + public char[] password; - public static class Builder extends TestState.Builder { + public static class Builder extends TestState.Builder { - public Builder(YubiKeyDevice device, UsbPid usbPid) { - super(device, usbPid); - } + public Builder(YubiKeyDevice device, UsbPid usbPid) { + super(device, usbPid); + } - @Override - public Builder getThis() { - return this; - } + @Override + public Builder getThis() { + return this; + } - public OathTestState build() throws Throwable { - return new OathTestState(this); - } + public OathTestState build() throws Throwable { + return new OathTestState(this); } + } - protected OathTestState(OathTestState.Builder builder) throws Throwable { - super(builder); + protected OathTestState(OathTestState.Builder builder) throws Throwable { + super(builder); - password = "".toCharArray(); + password = "".toCharArray(); - boolean isOathFipsCapable = isFipsCapable(Capability.OATH); + boolean isOathFipsCapable = isFipsCapable(Capability.OATH); - if (scpParameters.getKid() == null && isOathFipsCapable) { - assumeTrue("Trying to use OATH FIPS capable device over NFC without SCP", isUsbTransport()); - } + if (scpParameters.getKid() == null && isOathFipsCapable) { + assumeTrue("Trying to use OATH FIPS capable device over NFC without SCP", isUsbTransport()); + } - if (scpParameters.getKid() != null) { - // skip the test if the connected key does not provide matching SCP keys - assumeTrue( - "No matching key params found for required kid", - scpParameters.getKeyParams() != null - ); - } + if (scpParameters.getKid() != null) { + // skip the test if the connected key does not provide matching SCP keys + assumeTrue( + "No matching key params found for required kid", scpParameters.getKeyParams() != null); + } - try (SmartCardConnection connection = openSmartCardConnection()) { - assumeTrue("Smart card not available", connection != null); + try (SmartCardConnection connection = openSmartCardConnection()) { + assumeTrue("Smart card not available", connection != null); - OathSession oath = getOathSession(connection, scpParameters); + OathSession oath = getOathSession(connection, scpParameters); - assumeTrue("OATH not available", oath != null); - oath.reset(); + assumeTrue("OATH not available", oath != null); + oath.reset(); - final char[] complexPassword = "11234567".toCharArray(); - oath.setPassword(complexPassword); - password = complexPassword; - } + final char[] complexPassword = "11234567".toCharArray(); + oath.setPassword(complexPassword); + password = complexPassword; + } - isFipsApproved = isFipsApproved(Capability.OATH); + isFipsApproved = isFipsApproved(Capability.OATH); - // after changing the OATH password, we expect a FIPS capable device to be FIPS approved - if (isOathFipsCapable) { - assertTrue("Device not OATH FIPS approved as expected", isFipsApproved); - } + // after changing the OATH password, we expect a FIPS capable device to be FIPS approved + if (isOathFipsCapable) { + assertTrue("Device not OATH FIPS approved as expected", isFipsApproved); } + } - public void withDeviceCallback(StatefulDeviceCallback callback) throws Throwable { - callback.invoke(this); - } + public void withDeviceCallback(StatefulDeviceCallback callback) throws Throwable { + callback.invoke(this); + } - public void withOath(StatefulSessionCallback callback) - throws Throwable { - try (SmartCardConnection connection = openSmartCardConnection()) { - callback.invoke(getOathSession(connection, scpParameters), this); - } - reconnect(); + public void withOath(StatefulSessionCallback callback) + throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + callback.invoke(getOathSession(connection, scpParameters), this); } + reconnect(); + } - public void withOath(SessionCallback callback) throws Throwable { - try (SmartCardConnection connection = openSmartCardConnection()) { - callback.invoke(getOathSession(connection, scpParameters)); - } - reconnect(); + public void withOath(SessionCallback callback) throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + callback.invoke(getOathSession(connection, scpParameters)); } - - @Nullable - public static OathSession getOathSession(SmartCardConnection connection, ScpParameters scpParameters) - throws IOException { - try { - return new OathSession(connection, scpParameters.getKeyParams()); - } catch (ApplicationNotAvailableException ignored) { - // no OATH support - } - return null; + reconnect(); + } + + @Nullable public static OathSession getOathSession( + SmartCardConnection connection, ScpParameters scpParameters) throws IOException { + try { + return new OathSession(connection, scpParameters.getKeyParams()); + } catch (ApplicationNotAvailableException ignored) { + // no OATH support } + return null; + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpDeviceTests.java index 8db2b293..d962891c 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpDeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpDeviceTests.java @@ -32,21 +32,6 @@ import com.yubico.yubikit.openpgp.PinPolicy; import com.yubico.yubikit.openpgp.Pw; import com.yubico.yubikit.openpgp.Uif; - -import org.bouncycastle.asn1.ASN1Sequence; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; -import org.bouncycastle.cert.X509CertificateHolder; -import org.bouncycastle.cert.X509v3CertificateBuilder; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.operator.ContentSigner; -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; -import org.bouncycastle.util.encoders.Hex; -import org.junit.Assert; -import org.junit.Assume; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.ByteArrayInputStream; import java.io.InputStream; import java.math.BigInteger; @@ -54,7 +39,6 @@ import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PublicKey; -import java.security.Security; import java.security.Signature; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; @@ -65,593 +49,637 @@ import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; - import javax.crypto.Cipher; import javax.crypto.KeyAgreement; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Assert; +import org.junit.Assume; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class OpenPgpDeviceTests { - private static final char[] CHANGED_PIN = "12341234".toCharArray(); - private static final char[] RESET_CODE = "43214321".toCharArray(); - private static final Logger logger = LoggerFactory.getLogger(OpenPgpDeviceTests.class); - - private static final List ecdsaCurves = Stream.of(OpenPgpCurve.values()) - .filter(curve -> !Arrays.asList(OpenPgpCurve.Ed25519, OpenPgpCurve.X25519) - .contains(curve)) - .collect(Collectors.toList()); - - private static int[] getSupportedRsaKeySizes(OpenPgpSession openpgp) { - return openpgp.supports(OpenPgpSession.FEATURE_RSA4096_KEYS) ? new int[]{2048, 3072, 4096} : new int[]{2048}; + private static final char[] CHANGED_PIN = "12341234".toCharArray(); + private static final char[] RESET_CODE = "43214321".toCharArray(); + private static final Logger logger = LoggerFactory.getLogger(OpenPgpDeviceTests.class); + + private static final List ecdsaCurves = + Stream.of(OpenPgpCurve.values()) + .filter( + curve -> !Arrays.asList(OpenPgpCurve.Ed25519, OpenPgpCurve.X25519).contains(curve)) + .collect(Collectors.toList()); + + private static int[] getSupportedRsaKeySizes(OpenPgpSession openpgp) { + return openpgp.supports(OpenPgpSession.FEATURE_RSA4096_KEYS) + ? new int[] {2048, 3072, 4096} + : new int[] {2048}; + } + + public static void testGenerateRequiresAdmin(OpenPgpSession openpgp, OpenPgpTestState state) + throws Exception { + + try { + openpgp.generateEcKey(KeyRef.DEC, OpenPgpCurve.BrainpoolP256R1); + Assert.fail(); + } catch (ApduException e) { + Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } - public static void testGenerateRequiresAdmin(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - - try { - openpgp.generateEcKey(KeyRef.DEC, OpenPgpCurve.BrainpoolP256R1); - Assert.fail(); - } catch (ApduException e) { - Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); - } - - if (!state.isFipsApproved) { - try { - openpgp.generateRsaKey(KeyRef.DEC, 2048); - Assert.fail(); - } catch (ApduException e) { - Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); - } - } + if (!state.isFipsApproved) { + try { + openpgp.generateRsaKey(KeyRef.DEC, 2048); + Assert.fail(); + } catch (ApduException e) { + Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); + } } - - public static void testChangePin(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - openpgp.verifyUserPin(state.defaultUserPin, false); - Assert.assertThrows(InvalidPinException.class, () -> openpgp.verifyUserPin(CHANGED_PIN, false)); - Assert.assertThrows(InvalidPinException.class, () -> openpgp.changeUserPin(CHANGED_PIN, state.defaultUserPin)); - - openpgp.changeUserPin(state.defaultUserPin, CHANGED_PIN); + } + + public static void testChangePin(OpenPgpSession openpgp, OpenPgpTestState state) + throws Exception { + openpgp.verifyUserPin(state.defaultUserPin, false); + Assert.assertThrows(InvalidPinException.class, () -> openpgp.verifyUserPin(CHANGED_PIN, false)); + Assert.assertThrows( + InvalidPinException.class, () -> openpgp.changeUserPin(CHANGED_PIN, state.defaultUserPin)); + + openpgp.changeUserPin(state.defaultUserPin, CHANGED_PIN); + openpgp.verifyUserPin(CHANGED_PIN, false); + openpgp.changeUserPin(CHANGED_PIN, state.defaultUserPin); + openpgp.verifyUserPin(state.defaultUserPin, false); + } + + public static void testResetPin(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { + int remaining = openpgp.getPinStatus().getAttemptsUser(); + for (int i = remaining; i > 0; i--) { + try { openpgp.verifyUserPin(CHANGED_PIN, false); - openpgp.changeUserPin(CHANGED_PIN, state.defaultUserPin); - openpgp.verifyUserPin(state.defaultUserPin, false); + Assert.fail(); + } catch (InvalidPinException e) { + Assert.assertEquals(e.getAttemptsRemaining(), i - 1); + } } + assert openpgp.getPinStatus().getAttemptsUser() == 0; - public static void testResetPin(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - int remaining = openpgp.getPinStatus().getAttemptsUser(); - for (int i = remaining; i > 0; i--) { - try { - openpgp.verifyUserPin(CHANGED_PIN, false); - Assert.fail(); - } catch (InvalidPinException e) { - Assert.assertEquals(e.getAttemptsRemaining(), i - 1); - } - } - assert openpgp.getPinStatus().getAttemptsUser() == 0; - - try { - openpgp.resetPin(state.defaultUserPin, null); - Assert.fail(); - } catch (ApduException e) { - Assert.assertEquals(e.getSw(), SW.SECURITY_CONDITION_NOT_SATISFIED); - } - - // Reset PIN using Admin PIN - openpgp.verifyAdminPin(state.defaultAdminPin); - openpgp.resetPin(state.defaultUserPin, null); - remaining = openpgp.getPinStatus().getAttemptsUser(); - assert remaining > 0; - for (int i = remaining; i > 0; i--) { - try { - openpgp.verifyUserPin(CHANGED_PIN, false); - Assert.fail(); - } catch (InvalidPinException e) { - Assert.assertEquals(e.getAttemptsRemaining(), i - 1); - } - } - assert openpgp.getPinStatus().getAttemptsUser() == 0; - - // Reset PIN using Reset Code - openpgp.setResetCode(RESET_CODE); - Assert.assertThrows(InvalidPinException.class, () -> openpgp.resetPin(state.defaultUserPin, CHANGED_PIN)); - openpgp.resetPin(state.defaultUserPin, RESET_CODE); - assert openpgp.getPinStatus().getAttemptsUser() > 0; + try { + openpgp.resetPin(state.defaultUserPin, null); + Assert.fail(); + } catch (ApduException e) { + Assert.assertEquals(e.getSw(), SW.SECURITY_CONDITION_NOT_SATISFIED); } - public static void testSetPinAttempts(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("RSA key generation", openpgp.supports(OpenPgpSession.FEATURE_PIN_ATTEMPTS)); - - openpgp.verifyAdminPin(state.defaultAdminPin); - openpgp.setPinAttempts(6, 3, 3); - assert openpgp.getPinStatus().getAttemptsUser() == 6; - - try { - openpgp.verifyUserPin(CHANGED_PIN, false); - } catch (InvalidPinException e) { - // Ignore - } - assert openpgp.getPinStatus().getAttemptsUser() == 5; - - openpgp.setPinAttempts(3, 3, 3); - assert openpgp.getPinStatus().getAttemptsUser() == 3; + // Reset PIN using Admin PIN + openpgp.verifyAdminPin(state.defaultAdminPin); + openpgp.resetPin(state.defaultUserPin, null); + remaining = openpgp.getPinStatus().getAttemptsUser(); + assert remaining > 0; + for (int i = remaining; i > 0; i--) { + try { + openpgp.verifyUserPin(CHANGED_PIN, false); + Assert.fail(); + } catch (InvalidPinException e) { + Assert.assertEquals(e.getAttemptsRemaining(), i - 1); + } } - - public static void testGenerateRsaKeys(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("RSA key generation", openpgp.supports(OpenPgpSession.FEATURE_RSA_GENERATION)); - - openpgp.verifyAdminPin(state.defaultAdminPin); - - byte[] message = "hello".getBytes(StandardCharsets.UTF_8); - for (int keySize : getSupportedRsaKeySizes(openpgp)) { - logger.info("RSA key size: {}", keySize); - PublicKey publicKey = openpgp.generateRsaKey(KeyRef.SIG, keySize).toPublicKey(); - openpgp.verifyUserPin(state.defaultUserPin, false); - byte[] signature = openpgp.sign(message); - Signature verifier = Signature.getInstance("NONEwithRSA"); - verifier.initVerify(publicKey); - verifier.update(message); - assert verifier.verify(signature); - - if (!state.isFipsApproved) { - publicKey = openpgp.generateRsaKey(KeyRef.DEC, keySize).toPublicKey(); - Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); - cipher.init(Cipher.ENCRYPT_MODE, publicKey); - byte[] cipherText = cipher.doFinal(message); - - openpgp.verifyUserPin(state.defaultUserPin, true); - byte[] decrypted = openpgp.decrypt(cipherText); - Assert.assertArrayEquals(message, decrypted); - } - } + assert openpgp.getPinStatus().getAttemptsUser() == 0; + + // Reset PIN using Reset Code + openpgp.setResetCode(RESET_CODE); + Assert.assertThrows( + InvalidPinException.class, () -> openpgp.resetPin(state.defaultUserPin, CHANGED_PIN)); + openpgp.resetPin(state.defaultUserPin, RESET_CODE); + assert openpgp.getPinStatus().getAttemptsUser() > 0; + } + + public static void testSetPinAttempts(OpenPgpSession openpgp, OpenPgpTestState state) + throws Exception { + Assume.assumeTrue("RSA key generation", openpgp.supports(OpenPgpSession.FEATURE_PIN_ATTEMPTS)); + + openpgp.verifyAdminPin(state.defaultAdminPin); + openpgp.setPinAttempts(6, 3, 3); + assert openpgp.getPinStatus().getAttemptsUser() == 6; + + try { + openpgp.verifyUserPin(CHANGED_PIN, false); + } catch (InvalidPinException e) { + // Ignore } + assert openpgp.getPinStatus().getAttemptsUser() == 5; + + openpgp.setPinAttempts(3, 3, 3); + assert openpgp.getPinStatus().getAttemptsUser() == 3; + } + + public static void testGenerateRsaKeys(OpenPgpSession openpgp, OpenPgpTestState state) + throws Exception { + Assume.assumeTrue( + "RSA key generation", openpgp.supports(OpenPgpSession.FEATURE_RSA_GENERATION)); + + openpgp.verifyAdminPin(state.defaultAdminPin); + + byte[] message = "hello".getBytes(StandardCharsets.UTF_8); + for (int keySize : getSupportedRsaKeySizes(openpgp)) { + logger.info("RSA key size: {}", keySize); + PublicKey publicKey = openpgp.generateRsaKey(KeyRef.SIG, keySize).toPublicKey(); + openpgp.verifyUserPin(state.defaultUserPin, false); + byte[] signature = openpgp.sign(message); + Signature verifier = Signature.getInstance("NONEwithRSA"); + verifier.initVerify(publicKey); + verifier.update(message); + assert verifier.verify(signature); + + if (!state.isFipsApproved) { + publicKey = openpgp.generateRsaKey(KeyRef.DEC, keySize).toPublicKey(); + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + byte[] cipherText = cipher.doFinal(message); - public static void testGenerateEcKeys(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); - - openpgp.verifyAdminPin(state.defaultAdminPin); - - byte[] message = "hello".getBytes(StandardCharsets.UTF_8); - for (OpenPgpCurve curve : ecdsaCurves) { - if (state.isFipsApproved) { - if (curve == OpenPgpCurve.SECP256K1) { - continue; - } - } - logger.info("Curve: {}", curve); - PublicKey publicKey = openpgp.generateEcKey(KeyRef.SIG, curve).toPublicKey(); - openpgp.verifyUserPin(state.defaultUserPin, false); - byte[] signature = openpgp.sign(message); - - Signature verifier = Signature.getInstance("NONEwithECDSA"); - verifier.initVerify(publicKey); - verifier.update(message); - assert verifier.verify(signature); - - publicKey = openpgp.generateEcKey(KeyRef.DEC, curve).toPublicKey(); - KeyPairGenerator kpg = KeyPairGenerator.getInstance("ECDH"); - kpg.initialize(new ECGenParameterSpec(curve.name())); - KeyPair pair = kpg.generateKeyPair(); - - openpgp.verifyUserPin(state.defaultUserPin, true); - byte[] actual = openpgp.decrypt(PublicKeyValues.fromPublicKey(pair.getPublic())); - KeyAgreement ka = KeyAgreement.getInstance("ECDH"); - ka.init(pair.getPrivate()); - ka.doPhase(publicKey, true); - byte[] expected = ka.generateSecret(); - Assert.assertArrayEquals(expected, actual); - } + openpgp.verifyUserPin(state.defaultUserPin, true); + byte[] decrypted = openpgp.decrypt(cipherText); + Assert.assertArrayEquals(message, decrypted); + } } + } - public static void testGenerateEd25519(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); - - openpgp.verifyAdminPin(state.defaultAdminPin); + public static void testGenerateEcKeys(OpenPgpSession openpgp, OpenPgpTestState state) + throws Exception { + Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); - byte[] message = "hello".getBytes(StandardCharsets.UTF_8); - PublicKey publicKey = openpgp.generateEcKey(KeyRef.SIG, OpenPgpCurve.Ed25519).toPublicKey(); - openpgp.verifyUserPin(state.defaultUserPin, false); - byte[] signature = openpgp.sign(message); + openpgp.verifyAdminPin(state.defaultAdminPin); - Signature verifier = Signature.getInstance("Ed25519"); - verifier.initVerify(publicKey); - verifier.update(message); - assert verifier.verify(signature); + byte[] message = "hello".getBytes(StandardCharsets.UTF_8); + for (OpenPgpCurve curve : ecdsaCurves) { + if (state.isFipsApproved) { + if (curve == OpenPgpCurve.SECP256K1) { + continue; + } + } + logger.info("Curve: {}", curve); + PublicKey publicKey = openpgp.generateEcKey(KeyRef.SIG, curve).toPublicKey(); + openpgp.verifyUserPin(state.defaultUserPin, false); + byte[] signature = openpgp.sign(message); + + Signature verifier = Signature.getInstance("NONEwithECDSA"); + verifier.initVerify(publicKey); + verifier.update(message); + assert verifier.verify(signature); + + publicKey = openpgp.generateEcKey(KeyRef.DEC, curve).toPublicKey(); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("ECDH"); + kpg.initialize(new ECGenParameterSpec(curve.name())); + KeyPair pair = kpg.generateKeyPair(); + + openpgp.verifyUserPin(state.defaultUserPin, true); + byte[] actual = openpgp.decrypt(PublicKeyValues.fromPublicKey(pair.getPublic())); + KeyAgreement ka = KeyAgreement.getInstance("ECDH"); + ka.init(pair.getPrivate()); + ka.doPhase(publicKey, true); + byte[] expected = ka.generateSecret(); + Assert.assertArrayEquals(expected, actual); } - - public static void testGenerateX25519(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); - - Assume.assumeFalse("X25519 not supported in FIPS OpenPGP.", state.isFipsApproved); - - openpgp.verifyAdminPin(state.defaultAdminPin); - - PublicKey publicKey = openpgp.generateEcKey(KeyRef.DEC, OpenPgpCurve.X25519).toPublicKey(); - KeyPairGenerator kpg = KeyPairGenerator.getInstance("X25519"); - KeyPair pair = kpg.generateKeyPair(); + } + + public static void testGenerateEd25519(OpenPgpSession openpgp, OpenPgpTestState state) + throws Exception { + Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); + + openpgp.verifyAdminPin(state.defaultAdminPin); + + byte[] message = "hello".getBytes(StandardCharsets.UTF_8); + PublicKey publicKey = openpgp.generateEcKey(KeyRef.SIG, OpenPgpCurve.Ed25519).toPublicKey(); + openpgp.verifyUserPin(state.defaultUserPin, false); + byte[] signature = openpgp.sign(message); + + Signature verifier = Signature.getInstance("Ed25519"); + verifier.initVerify(publicKey); + verifier.update(message); + assert verifier.verify(signature); + } + + public static void testGenerateX25519(OpenPgpSession openpgp, OpenPgpTestState state) + throws Exception { + Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); + + Assume.assumeFalse("X25519 not supported in FIPS OpenPGP.", state.isFipsApproved); + + openpgp.verifyAdminPin(state.defaultAdminPin); + + PublicKey publicKey = openpgp.generateEcKey(KeyRef.DEC, OpenPgpCurve.X25519).toPublicKey(); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("X25519"); + KeyPair pair = kpg.generateKeyPair(); + + openpgp.verifyUserPin(state.defaultUserPin, true); + byte[] actual = openpgp.decrypt(PublicKeyValues.fromPublicKey(pair.getPublic())); + KeyAgreement ka = KeyAgreement.getInstance("XDH"); + ka.init(pair.getPrivate()); + ka.doPhase(publicKey, true); + byte[] expected = ka.generateSecret(); + Assert.assertArrayEquals(expected, actual); + } + + public static void testImportRsaKeys(OpenPgpSession openpgp, OpenPgpTestState state) + throws Exception { + openpgp.verifyAdminPin(state.defaultAdminPin); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + byte[] message = "hello".getBytes(StandardCharsets.UTF_8); + for (int keySize : getSupportedRsaKeySizes(openpgp)) { + logger.info("RSA key size: {}", keySize); + kpg.initialize(keySize); + KeyPair pair = kpg.generateKeyPair(); + openpgp.putKey(KeyRef.SIG, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); + + byte[] encoded = openpgp.getPublicKey(KeyRef.SIG).getEncoded(); + Assert.assertArrayEquals(pair.getPublic().getEncoded(), encoded); + + PublicKey publicKey = openpgp.getPublicKey(KeyRef.SIG).toPublicKey(); + openpgp.verifyUserPin(state.defaultUserPin, false); + byte[] signature = openpgp.sign(message); + + Signature verifier = Signature.getInstance("NONEwithRSA"); + verifier.initVerify(publicKey); + verifier.update(message); + assert verifier.verify(signature); + + if (!state.isFipsApproved) { + openpgp.putKey(KeyRef.DEC, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + byte[] cipherText = cipher.doFinal(message); openpgp.verifyUserPin(state.defaultUserPin, true); - byte[] actual = openpgp.decrypt(PublicKeyValues.fromPublicKey(pair.getPublic())); - KeyAgreement ka = KeyAgreement.getInstance("XDH"); - ka.init(pair.getPrivate()); - ka.doPhase(publicKey, true); - byte[] expected = ka.generateSecret(); - Assert.assertArrayEquals(expected, actual); + byte[] decrypted = openpgp.decrypt(cipherText); + Assert.assertArrayEquals(message, decrypted); + } } + } - public static void testImportRsaKeys(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - openpgp.verifyAdminPin(state.defaultAdminPin); - - KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); - byte[] message = "hello".getBytes(StandardCharsets.UTF_8); - for (int keySize : getSupportedRsaKeySizes(openpgp)) { - logger.info("RSA key size: {}", keySize); - kpg.initialize(keySize); - KeyPair pair = kpg.generateKeyPair(); - openpgp.putKey(KeyRef.SIG, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); + public static void testImportEcDsaKeys(OpenPgpSession openpgp, OpenPgpTestState state) + throws Exception { + Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); - byte[] encoded = openpgp.getPublicKey(KeyRef.SIG).getEncoded(); - Assert.assertArrayEquals(pair.getPublic().getEncoded(), encoded); + openpgp.verifyAdminPin(state.defaultAdminPin); - PublicKey publicKey = openpgp.getPublicKey(KeyRef.SIG).toPublicKey(); - openpgp.verifyUserPin(state.defaultUserPin, false); - byte[] signature = openpgp.sign(message); - - Signature verifier = Signature.getInstance("NONEwithRSA"); - verifier.initVerify(publicKey); - verifier.update(message); - assert verifier.verify(signature); - - if (!state.isFipsApproved) { - openpgp.putKey(KeyRef.DEC, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); - Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); - cipher.init(Cipher.ENCRYPT_MODE, publicKey); - byte[] cipherText = cipher.doFinal(message); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("ECDSA"); + List curves = new ArrayList<>(Arrays.asList(OpenPgpCurve.values())); + curves.remove(OpenPgpCurve.Ed25519); + curves.remove(OpenPgpCurve.X25519); + if (state.isFipsApproved) { + curves.remove(OpenPgpCurve.SECP256K1); + } - openpgp.verifyUserPin(state.defaultUserPin, true); - byte[] decrypted = openpgp.decrypt(cipherText); - Assert.assertArrayEquals(message, decrypted); - } - } + byte[] message = "hello".getBytes(StandardCharsets.UTF_8); + for (OpenPgpCurve curve : curves) { + logger.info("Curve: {}", curve); + kpg.initialize(new ECGenParameterSpec(curve.name())); + KeyPair pair = kpg.generateKeyPair(); + openpgp.putKey(KeyRef.SIG, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); + PublicKeyValues values = openpgp.getPublicKey(KeyRef.SIG); + Assert.assertArrayEquals(pair.getPublic().getEncoded(), values.getEncoded()); + PublicKey publicKey = values.toPublicKey(); + openpgp.verifyUserPin(state.defaultUserPin, false); + byte[] signature = openpgp.sign(message); + + Signature verifier = Signature.getInstance("NONEwithECDSA"); + verifier.initVerify(publicKey); + verifier.update(message); + assert verifier.verify(signature); + + openpgp.putKey(KeyRef.DEC, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); + KeyPair pair2 = kpg.generateKeyPair(); + KeyAgreement ka = KeyAgreement.getInstance("ECDH"); + ka.init(pair2.getPrivate()); + ka.doPhase(openpgp.getPublicKey(KeyRef.DEC).toPublicKey(), true); + byte[] expected = ka.generateSecret(); + + openpgp.verifyUserPin(state.defaultUserPin, true); + byte[] agreement = openpgp.decrypt(PublicKeyValues.fromPublicKey(pair2.getPublic())); + + Assert.assertArrayEquals(expected, agreement); } + } - public static void testImportEcDsaKeys(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); + public static void testImportEd25519(OpenPgpSession openpgp, OpenPgpTestState state) + throws Exception { + Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); - openpgp.verifyAdminPin(state.defaultAdminPin); + openpgp.verifyAdminPin(state.defaultAdminPin); - KeyPairGenerator kpg = KeyPairGenerator.getInstance("ECDSA"); - List curves = new ArrayList<>(Arrays.asList(OpenPgpCurve.values())); - curves.remove(OpenPgpCurve.Ed25519); - curves.remove(OpenPgpCurve.X25519); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519"); + KeyPair pair = kpg.generateKeyPair(); + openpgp.putKey(KeyRef.SIG, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); - if (state.isFipsApproved) { - curves.remove(OpenPgpCurve.SECP256K1); - } + byte[] message = "hello".getBytes(StandardCharsets.UTF_8); - byte[] message = "hello".getBytes(StandardCharsets.UTF_8); - for (OpenPgpCurve curve : curves) { - logger.info("Curve: {}", curve); - kpg.initialize(new ECGenParameterSpec(curve.name())); - KeyPair pair = kpg.generateKeyPair(); - openpgp.putKey(KeyRef.SIG, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); - PublicKeyValues values = openpgp.getPublicKey(KeyRef.SIG); - Assert.assertArrayEquals(pair.getPublic().getEncoded(), values.getEncoded()); - PublicKey publicKey = values.toPublicKey(); - openpgp.verifyUserPin(state.defaultUserPin, false); - byte[] signature = openpgp.sign(message); - - Signature verifier = Signature.getInstance("NONEwithECDSA"); - verifier.initVerify(publicKey); - verifier.update(message); - assert verifier.verify(signature); - - openpgp.putKey(KeyRef.DEC, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); - KeyPair pair2 = kpg.generateKeyPair(); - KeyAgreement ka = KeyAgreement.getInstance("ECDH"); - ka.init(pair2.getPrivate()); - ka.doPhase(openpgp.getPublicKey(KeyRef.DEC).toPublicKey(), true); - byte[] expected = ka.generateSecret(); - - openpgp.verifyUserPin(state.defaultUserPin, true); - byte[] agreement = openpgp.decrypt(PublicKeyValues.fromPublicKey(pair2.getPublic())); - - Assert.assertArrayEquals(expected, agreement); - } - } + openpgp.verifyUserPin(state.defaultUserPin, false); + byte[] signature = openpgp.sign(message); - public static void testImportEd25519(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); + Signature verifier = Signature.getInstance("Ed25519"); + verifier.initVerify(openpgp.getPublicKey(KeyRef.SIG).toPublicKey()); + verifier.update(message); + assert verifier.verify(signature); - openpgp.verifyAdminPin(state.defaultAdminPin); + Assert.assertArrayEquals( + pair.getPublic().getEncoded(), openpgp.getPublicKey(KeyRef.SIG).getEncoded()); + } - KeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519"); - KeyPair pair = kpg.generateKeyPair(); - openpgp.putKey(KeyRef.SIG, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); + public static void testImportX25519(OpenPgpSession openpgp, OpenPgpTestState state) + throws Exception { + Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); + Assume.assumeFalse("X25519 not supported in FIPS OpenPGP.", state.isFipsApproved); - byte[] message = "hello".getBytes(StandardCharsets.UTF_8); + openpgp.verifyAdminPin(state.defaultAdminPin); - openpgp.verifyUserPin(state.defaultUserPin, false); - byte[] signature = openpgp.sign(message); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("X25519"); + KeyPair pair = kpg.generateKeyPair(); + openpgp.putKey(KeyRef.DEC, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); - Signature verifier = Signature.getInstance("Ed25519"); - verifier.initVerify(openpgp.getPublicKey(KeyRef.SIG).toPublicKey()); - verifier.update(message); - assert verifier.verify(signature); + KeyPair pair2 = kpg.generateKeyPair(); - Assert.assertArrayEquals(pair.getPublic().getEncoded(), openpgp.getPublicKey(KeyRef.SIG).getEncoded()); - } + KeyAgreement ka = KeyAgreement.getInstance("X25519"); + ka.init(pair2.getPrivate()); + ka.doPhase(openpgp.getPublicKey(KeyRef.DEC).toPublicKey(), true); + byte[] expected = ka.generateSecret(); - public static void testImportX25519(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("EC support", openpgp.supports(OpenPgpSession.FEATURE_EC_KEYS)); - Assume.assumeFalse("X25519 not supported in FIPS OpenPGP.", state.isFipsApproved); + openpgp.verifyUserPin(state.defaultUserPin, true); + byte[] agreement = openpgp.decrypt(PublicKeyValues.Ec.fromPublicKey(pair2.getPublic())); - openpgp.verifyAdminPin(state.defaultAdminPin); + Assert.assertArrayEquals(expected, agreement); + } - KeyPairGenerator kpg = KeyPairGenerator.getInstance("X25519"); - KeyPair pair = kpg.generateKeyPair(); - openpgp.putKey(KeyRef.DEC, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); + public static void testAttestation(OpenPgpSession openpgp, OpenPgpTestState state) + throws Exception { + Assume.assumeTrue("Attestation support", openpgp.supports(OpenPgpSession.FEATURE_ATTESTATION)); - KeyPair pair2 = kpg.generateKeyPair(); + openpgp.verifyAdminPin(state.defaultAdminPin); - KeyAgreement ka = KeyAgreement.getInstance("X25519"); - ka.init(pair2.getPrivate()); - ka.doPhase(openpgp.getPublicKey(KeyRef.DEC).toPublicKey(), true); - byte[] expected = ka.generateSecret(); + PublicKey publicKey = openpgp.generateEcKey(KeyRef.SIG, OpenPgpCurve.SECP256R1).toPublicKey(); - openpgp.verifyUserPin(state.defaultUserPin, true); - byte[] agreement = openpgp.decrypt(PublicKeyValues.Ec.fromPublicKey(pair2.getPublic())); + openpgp.verifyUserPin(state.defaultUserPin, false); + X509Certificate cert = openpgp.attestKey(KeyRef.SIG); - Assert.assertArrayEquals(expected, agreement); - } - - public static void testAttestation(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("Attestation support", openpgp.supports(OpenPgpSession.FEATURE_ATTESTATION)); + Assert.assertEquals(publicKey, cert.getPublicKey()); + } - openpgp.verifyAdminPin(state.defaultAdminPin); + public static void testSigPinPolicy(OpenPgpSession openpgp, OpenPgpTestState state) + throws Exception { + openpgp.verifyAdminPin(state.defaultAdminPin); - PublicKey publicKey = openpgp.generateEcKey(KeyRef.SIG, OpenPgpCurve.SECP256R1).toPublicKey(); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + byte[] message = "hello".getBytes(StandardCharsets.UTF_8); + kpg.initialize(2048); + KeyPair pair = kpg.generateKeyPair(); + openpgp.putKey(KeyRef.SIG, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); + Assert.assertEquals(0, openpgp.getSignatureCounter()); - openpgp.verifyUserPin(state.defaultUserPin, false); - X509Certificate cert = openpgp.attestKey(KeyRef.SIG); - - Assert.assertEquals(publicKey, cert.getPublicKey()); + try { + openpgp.sign(message); + Assert.fail(); + } catch (ApduException e) { + Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } - public static void testSigPinPolicy(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - openpgp.verifyAdminPin(state.defaultAdminPin); - - KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); - byte[] message = "hello".getBytes(StandardCharsets.UTF_8); - kpg.initialize(2048); - KeyPair pair = kpg.generateKeyPair(); - openpgp.putKey(KeyRef.SIG, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); - Assert.assertEquals(0, openpgp.getSignatureCounter()); - - try { - openpgp.sign(message); - Assert.fail(); - } catch (ApduException e) { - Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); - } - - openpgp.setSignaturePinPolicy(PinPolicy.ALWAYS); - openpgp.verifyUserPin(state.defaultUserPin, false); - openpgp.sign(message); - Assert.assertEquals(1, openpgp.getSignatureCounter()); - try { - openpgp.sign(message); - Assert.fail(); - } catch (ApduException e) { - Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); - } - Assert.assertEquals(1, openpgp.getSignatureCounter()); - - openpgp.setSignaturePinPolicy(PinPolicy.ONCE); - openpgp.verifyUserPin(state.defaultUserPin, false); - openpgp.sign(message); - Assert.assertEquals(2, openpgp.getSignatureCounter()); - openpgp.sign(message); - Assert.assertEquals(3, openpgp.getSignatureCounter()); + openpgp.setSignaturePinPolicy(PinPolicy.ALWAYS); + openpgp.verifyUserPin(state.defaultUserPin, false); + openpgp.sign(message); + Assert.assertEquals(1, openpgp.getSignatureCounter()); + try { + openpgp.sign(message); + Assert.fail(); + } catch (ApduException e) { + Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } - - public static void testKdf(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("KDF Support", openpgp.getExtendedCapabilities().getFlags().contains(ExtendedCapabilityFlag.KDF)); - - // Test setting KDF without admin PIN verification - try { - openpgp.setKdf(new Kdf.None()); - Assert.fail(); - } catch (ApduException e) { - Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); - } - - // Set a non-default PINs to ensure that they reset - openpgp.changeUserPin(state.defaultUserPin, CHANGED_PIN); - openpgp.changeAdminPin(state.defaultAdminPin, CHANGED_PIN); - - openpgp.verifyAdminPin(CHANGED_PIN); - openpgp.setKdf( - Kdf.IterSaltedS2k.create(Kdf.IterSaltedS2k.HashAlgorithm.SHA256, 0x780000) - ); - // this reset the device to defaults - it is not FIPS approved anymore and - // default PINs have to be used - openpgp.verifyUserPin(Pw.DEFAULT_USER_PIN, false); - openpgp.verifyAdminPin(Pw.DEFAULT_ADMIN_PIN); - - openpgp.changeUserPin(Pw.DEFAULT_USER_PIN, CHANGED_PIN); - openpgp.verifyUserPin(CHANGED_PIN, false); - openpgp.changeAdminPin(Pw.DEFAULT_ADMIN_PIN, CHANGED_PIN); - openpgp.verifyAdminPin(CHANGED_PIN); - - openpgp.setKdf(new Kdf.None()); - openpgp.verifyAdminPin(Pw.DEFAULT_ADMIN_PIN); - openpgp.verifyUserPin(Pw.DEFAULT_USER_PIN, false); + Assert.assertEquals(1, openpgp.getSignatureCounter()); + + openpgp.setSignaturePinPolicy(PinPolicy.ONCE); + openpgp.verifyUserPin(state.defaultUserPin, false); + openpgp.sign(message); + Assert.assertEquals(2, openpgp.getSignatureCounter()); + openpgp.sign(message); + Assert.assertEquals(3, openpgp.getSignatureCounter()); + } + + public static void testKdf(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { + Assume.assumeTrue( + "KDF Support", + openpgp.getExtendedCapabilities().getFlags().contains(ExtendedCapabilityFlag.KDF)); + + // Test setting KDF without admin PIN verification + try { + openpgp.setKdf(new Kdf.None()); + Assert.fail(); + } catch (ApduException e) { + Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } - public static void testUnverifyPin(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("Unverify PIN Support", openpgp.supports(OpenPgpSession.FEATURE_UNVERIFY_PIN)); - - KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); - kpg.initialize(2048); - KeyPair pair = kpg.generateKeyPair(); - - openpgp.verifyAdminPin(state.defaultAdminPin); - openpgp.putKey(KeyRef.SIG, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); - openpgp.setSignaturePinPolicy(PinPolicy.ONCE); - - openpgp.unverifyAdminPin(); - // Test import key after unverify - try { - openpgp.putKey(KeyRef.AUT, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); - Assert.fail(); - } catch (ApduException e) { - Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); - } - - openpgp.verifyUserPin(state.defaultUserPin, false); - byte[] message = "hello".getBytes(StandardCharsets.UTF_8); - openpgp.sign(message); - - openpgp.unverifyUserPin(); - // Test sign after unverify - try { - openpgp.sign(message); - Assert.fail(); - } catch (ApduException e) { - Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); - } + // Set a non-default PINs to ensure that they reset + openpgp.changeUserPin(state.defaultUserPin, CHANGED_PIN); + openpgp.changeAdminPin(state.defaultAdminPin, CHANGED_PIN); + + openpgp.verifyAdminPin(CHANGED_PIN); + openpgp.setKdf(Kdf.IterSaltedS2k.create(Kdf.IterSaltedS2k.HashAlgorithm.SHA256, 0x780000)); + // this reset the device to defaults - it is not FIPS approved anymore and + // default PINs have to be used + openpgp.verifyUserPin(Pw.DEFAULT_USER_PIN, false); + openpgp.verifyAdminPin(Pw.DEFAULT_ADMIN_PIN); + + openpgp.changeUserPin(Pw.DEFAULT_USER_PIN, CHANGED_PIN); + openpgp.verifyUserPin(CHANGED_PIN, false); + openpgp.changeAdminPin(Pw.DEFAULT_ADMIN_PIN, CHANGED_PIN); + openpgp.verifyAdminPin(CHANGED_PIN); + + openpgp.setKdf(new Kdf.None()); + openpgp.verifyAdminPin(Pw.DEFAULT_ADMIN_PIN); + openpgp.verifyUserPin(Pw.DEFAULT_USER_PIN, false); + } + + public static void testUnverifyPin(OpenPgpSession openpgp, OpenPgpTestState state) + throws Exception { + Assume.assumeTrue( + "Unverify PIN Support", openpgp.supports(OpenPgpSession.FEATURE_UNVERIFY_PIN)); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + KeyPair pair = kpg.generateKeyPair(); + + openpgp.verifyAdminPin(state.defaultAdminPin); + openpgp.putKey(KeyRef.SIG, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); + openpgp.setSignaturePinPolicy(PinPolicy.ONCE); + + openpgp.unverifyAdminPin(); + // Test import key after unverify + try { + openpgp.putKey(KeyRef.AUT, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); + Assert.fail(); + } catch (ApduException e) { + Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } - public static void testDeleteKey(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); - kpg.initialize(2048); - KeyPair pair = kpg.generateKeyPair(); - - openpgp.verifyAdminPin(state.defaultAdminPin); - openpgp.putKey(KeyRef.SIG, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); - - openpgp.verifyUserPin(state.defaultUserPin, false); - byte[] message = "hello".getBytes(StandardCharsets.UTF_8); - openpgp.sign(message); - - openpgp.deleteKey(KeyRef.SIG); - try { - openpgp.sign(message); - Assert.fail(); - } catch (ApduException e) { - Assert.assertEquals(CONDITIONS_NOT_SATISFIED, e.getSw()); - } + openpgp.verifyUserPin(state.defaultUserPin, false); + byte[] message = "hello".getBytes(StandardCharsets.UTF_8); + openpgp.sign(message); + + openpgp.unverifyUserPin(); + // Test sign after unverify + try { + openpgp.sign(message); + Assert.fail(); + } catch (ApduException e) { + Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } - - public static void testCertificateManagement(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); - kpg.initialize(2048); - KeyPair pair = kpg.generateKeyPair(); - - X500Name name = new X500Name("CN=Example"); - X509v3CertificateBuilder serverCertGen = new X509v3CertificateBuilder( - name, - new BigInteger("123456789"), - new Date(), - new Date(), - name, - SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(pair.getPublic().getEncoded())) - ); - ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSA").build(pair.getPrivate()); - X509CertificateHolder holder = serverCertGen.build(contentSigner); - - InputStream stream = new ByteArrayInputStream(holder.getEncoded()); - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - X509Certificate cert = (X509Certificate) cf.generateCertificate(stream); - - openpgp.verifyAdminPin(state.defaultAdminPin); - openpgp.putCertificate(KeyRef.SIG, cert); - - X509Certificate actual = openpgp.getCertificate(KeyRef.SIG); - Assert.assertNotNull(actual); - Assert.assertArrayEquals(cert.getEncoded(), actual.getEncoded()); - - openpgp.deleteCertificate(KeyRef.SIG); - Assert.assertNull(openpgp.getCertificate(KeyRef.SIG)); + } + + public static void testDeleteKey(OpenPgpSession openpgp, OpenPgpTestState state) + throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + KeyPair pair = kpg.generateKeyPair(); + + openpgp.verifyAdminPin(state.defaultAdminPin); + openpgp.putKey(KeyRef.SIG, PrivateKeyValues.fromPrivateKey(pair.getPrivate())); + + openpgp.verifyUserPin(state.defaultUserPin, false); + byte[] message = "hello".getBytes(StandardCharsets.UTF_8); + openpgp.sign(message); + + openpgp.deleteKey(KeyRef.SIG); + try { + openpgp.sign(message); + Assert.fail(); + } catch (ApduException e) { + Assert.assertEquals(CONDITIONS_NOT_SATISFIED, e.getSw()); } - - public static void testGetChallenge(OpenPgpSession openpgp, OpenPgpTestState ignored) throws Exception { - Assume.assumeTrue("Get Challenge Support", openpgp.getExtendedCapabilities().getFlags().contains(ExtendedCapabilityFlag.GET_CHALLENGE)); - - byte[] challenge = openpgp.getChallenge(1); - Assert.assertEquals(1, challenge.length); - - challenge = openpgp.getChallenge(8); - Assert.assertEquals(8, challenge.length); - // Make sure it's not all zero - Assert.assertNotEquals(Hex.toHexString(new byte[8]), Hex.toHexString(challenge)); - // Make sure it changes - Assert.assertNotEquals(Hex.toHexString(openpgp.getChallenge(8)), Hex.toHexString(challenge)); - - challenge = openpgp.getChallenge(255); - Assert.assertEquals(255, challenge.length); + } + + public static void testCertificateManagement(OpenPgpSession openpgp, OpenPgpTestState state) + throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + KeyPair pair = kpg.generateKeyPair(); + + X500Name name = new X500Name("CN=Example"); + X509v3CertificateBuilder serverCertGen = + new X509v3CertificateBuilder( + name, + new BigInteger("123456789"), + new Date(), + new Date(), + name, + SubjectPublicKeyInfo.getInstance( + ASN1Sequence.getInstance(pair.getPublic().getEncoded()))); + ContentSigner contentSigner = + new JcaContentSignerBuilder("SHA256WithRSA").build(pair.getPrivate()); + X509CertificateHolder holder = serverCertGen.build(contentSigner); + + InputStream stream = new ByteArrayInputStream(holder.getEncoded()); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate cert = (X509Certificate) cf.generateCertificate(stream); + + openpgp.verifyAdminPin(state.defaultAdminPin); + openpgp.putCertificate(KeyRef.SIG, cert); + + X509Certificate actual = openpgp.getCertificate(KeyRef.SIG); + Assert.assertNotNull(actual); + Assert.assertArrayEquals(cert.getEncoded(), actual.getEncoded()); + + openpgp.deleteCertificate(KeyRef.SIG); + Assert.assertNull(openpgp.getCertificate(KeyRef.SIG)); + } + + public static void testGetChallenge(OpenPgpSession openpgp, OpenPgpTestState ignored) + throws Exception { + Assume.assumeTrue( + "Get Challenge Support", + openpgp + .getExtendedCapabilities() + .getFlags() + .contains(ExtendedCapabilityFlag.GET_CHALLENGE)); + + byte[] challenge = openpgp.getChallenge(1); + Assert.assertEquals(1, challenge.length); + + challenge = openpgp.getChallenge(8); + Assert.assertEquals(8, challenge.length); + // Make sure it's not all zero + Assert.assertNotEquals(Hex.toHexString(new byte[8]), Hex.toHexString(challenge)); + // Make sure it changes + Assert.assertNotEquals(Hex.toHexString(openpgp.getChallenge(8)), Hex.toHexString(challenge)); + + challenge = openpgp.getChallenge(255); + Assert.assertEquals(255, challenge.length); + } + + public static void testSetUif(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { + Assume.assumeTrue("UIF Support", openpgp.supports(OpenPgpSession.FEATURE_UIF)); + + try { + openpgp.setUif(KeyRef.SIG, Uif.ON); + Assert.fail(); + } catch (ApduException e) { + Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); } - public static void testSetUif(OpenPgpSession openpgp, OpenPgpTestState state) throws Exception { - Assume.assumeTrue("UIF Support", openpgp.supports(OpenPgpSession.FEATURE_UIF)); - - try { - openpgp.setUif(KeyRef.SIG, Uif.ON); - Assert.fail(); - } catch (ApduException e) { - Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); - } - - openpgp.verifyAdminPin(state.defaultAdminPin); - openpgp.setUif(KeyRef.SIG, Uif.ON); - Assert.assertEquals(Uif.ON, openpgp.getUif(KeyRef.SIG)); - - openpgp.setUif(KeyRef.SIG, Uif.OFF); - Assert.assertEquals(Uif.OFF, openpgp.getUif(KeyRef.SIG)); - - openpgp.setUif(KeyRef.SIG, Uif.FIXED); - Assert.assertThrows(IllegalStateException.class, () -> openpgp.setUif(KeyRef.SIG, Uif.OFF)); - - // Reset to remove FIXED UIF. - openpgp.reset(); + openpgp.verifyAdminPin(state.defaultAdminPin); + openpgp.setUif(KeyRef.SIG, Uif.ON); + Assert.assertEquals(Uif.ON, openpgp.getUif(KeyRef.SIG)); + + openpgp.setUif(KeyRef.SIG, Uif.OFF); + Assert.assertEquals(Uif.OFF, openpgp.getUif(KeyRef.SIG)); + + openpgp.setUif(KeyRef.SIG, Uif.FIXED); + Assert.assertThrows(IllegalStateException.class, () -> openpgp.setUif(KeyRef.SIG, Uif.OFF)); + + // Reset to remove FIXED UIF. + openpgp.reset(); + } + + /** + * For this test, one needs a key with PIN complexity set on. The test will change PINs. + * + *

The test will verify that trying to set a weak user PIN for OpenPgp produces expected + * exceptions. + * + * @see DeviceInfo#getPinComplexity() + */ + public static void testPinComplexity(OpenPgpSession openpgp, OpenPgpTestState state) + throws Throwable { + + final DeviceInfo deviceInfo = state.getDeviceInfo(); + Assume.assumeTrue("Device does not support PIN complexity", deviceInfo != null); + Assume.assumeTrue("Device does not require PIN complexity", deviceInfo.getPinComplexity()); + + openpgp.reset(); + + // after reset we cannot use the pin values from the state as it has been updated based + // on the connected device + openpgp.verifyUserPin(Pw.DEFAULT_USER_PIN, false); + + char[] weakPin = "33333333".toCharArray(); + try { + openpgp.changeUserPin(Pw.DEFAULT_USER_PIN, weakPin); + } catch (ApduException apduException) { + if (apduException.getSw() != CONDITIONS_NOT_SATISFIED) { + Assert.fail("Unexpected exception"); + } + } catch (Exception e) { + Assert.fail("Unexpected exception"); } - /** - * For this test, one needs a key with PIN complexity set on. The test will change PINs. - *

- * The test will verify that trying to set a weak user PIN for OpenPgp produces expected exceptions. - * - * @see DeviceInfo#getPinComplexity() - */ - public static void testPinComplexity(OpenPgpSession openpgp, OpenPgpTestState state) throws Throwable { - - final DeviceInfo deviceInfo = state.getDeviceInfo(); - Assume.assumeTrue("Device does not support PIN complexity", deviceInfo != null); - Assume.assumeTrue("Device does not require PIN complexity", deviceInfo.getPinComplexity()); - - openpgp.reset(); - - // after reset we cannot use the pin values from the state as it has been updated based - // on the connected device - openpgp.verifyUserPin(Pw.DEFAULT_USER_PIN, false); - - char[] weakPin = "33333333".toCharArray(); - try { - openpgp.changeUserPin(Pw.DEFAULT_USER_PIN, weakPin); - } catch (ApduException apduException) { - if (apduException.getSw() != CONDITIONS_NOT_SATISFIED) { - Assert.fail("Unexpected exception"); - } - } catch (Exception e) { - Assert.fail("Unexpected exception"); - } - - // set complex pin - char[] complexPin = "CMPLXPIN".toCharArray(); - try { - openpgp.changeUserPin(Pw.DEFAULT_USER_PIN, complexPin); - } catch (Exception e) { - Assert.fail("Unexpected exception"); - } - - // change the user pin to value stated in the state as it is correct for PIN complexity - openpgp.changeUserPin(complexPin, state.defaultUserPin); + // set complex pin + char[] complexPin = "CMPLXPIN".toCharArray(); + try { + openpgp.changeUserPin(Pw.DEFAULT_USER_PIN, complexPin); + } catch (Exception e) { + Assert.fail("Unexpected exception"); } + + // change the user pin to value stated in the state as it is correct for PIN complexity + openpgp.changeUserPin(complexPin, state.defaultUserPin); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestState.java index 1659ae9c..6fe121d9 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/openpgp/OpenPgpTestState.java @@ -31,105 +31,100 @@ import com.yubico.yubikit.openpgp.Pw; import com.yubico.yubikit.testing.ScpParameters; import com.yubico.yubikit.testing.TestState; - -import org.junit.Assume; - import java.io.IOException; - import javax.annotation.Nullable; +import org.junit.Assume; public class OpenPgpTestState extends TestState { - private static final char[] COMPLEX_USER_PIN = "112345678".toCharArray(); - private static final char[] COMPLEX_ADMIN_PIN = "112345678".toCharArray(); + private static final char[] COMPLEX_USER_PIN = "112345678".toCharArray(); + private static final char[] COMPLEX_ADMIN_PIN = "112345678".toCharArray(); - public final boolean isFipsApproved; - public char[] defaultUserPin; - public char[] defaultAdminPin; + public final boolean isFipsApproved; + public char[] defaultUserPin; + public char[] defaultAdminPin; - public static class Builder extends TestState.Builder { + public static class Builder extends TestState.Builder { - public Builder(YubiKeyDevice device, UsbPid usbPid) { - super(device, usbPid); - } + public Builder(YubiKeyDevice device, UsbPid usbPid) { + super(device, usbPid); + } - @Override - public Builder getThis() { - return this; - } + @Override + public Builder getThis() { + return this; + } - public OpenPgpTestState build() throws Throwable { - return new OpenPgpTestState(this); - } + public OpenPgpTestState build() throws Throwable { + return new OpenPgpTestState(this); } + } + + protected OpenPgpTestState(OpenPgpTestState.Builder builder) throws Throwable { + super(builder); + + defaultUserPin = Pw.DEFAULT_USER_PIN; + defaultAdminPin = Pw.DEFAULT_ADMIN_PIN; + + DeviceInfo deviceInfo = getDeviceInfo(); + boolean isOpenPgpFipsCapable = isFipsCapable(deviceInfo, Capability.OPENPGP); + boolean hasPinComplexity = deviceInfo != null && deviceInfo.getPinComplexity(); - protected OpenPgpTestState(OpenPgpTestState.Builder builder) throws Throwable { - super(builder); - - defaultUserPin = Pw.DEFAULT_USER_PIN; - defaultAdminPin = Pw.DEFAULT_ADMIN_PIN; - - DeviceInfo deviceInfo = getDeviceInfo(); - boolean isOpenPgpFipsCapable = isFipsCapable(deviceInfo, Capability.OPENPGP); - boolean hasPinComplexity = deviceInfo != null && deviceInfo.getPinComplexity(); - - if (scpParameters.getKid() == null && isOpenPgpFipsCapable) { - Assume.assumeTrue("Trying to use OpenPgp FIPS capable device over NFC without SCP", - isUsbTransport()); - } - - if (scpParameters.getKid() != null) { - // skip the test if the connected key does not provide matching SCP keys - Assume.assumeTrue( - "No matching key params found for required kid", - scpParameters.getKeyParams() != null - ); - } - - try (SmartCardConnection connection = openSmartCardConnection()) { - assumeTrue("Smart card not available", connection != null); - - OpenPgpSession openPgp = getOpenPgpSession(connection, scpParameters); - - assumeTrue("OpenPGP not available", openPgp != null); - openPgp.reset(); - - if (hasPinComplexity) { - // only use complex pins if pin complexity is required - openPgp.changeUserPin(defaultUserPin, COMPLEX_USER_PIN); - openPgp.changeAdminPin(defaultAdminPin, COMPLEX_ADMIN_PIN); - defaultUserPin = COMPLEX_USER_PIN; - defaultAdminPin = COMPLEX_ADMIN_PIN; - } - } - - deviceInfo = getDeviceInfo(); - isFipsApproved = isFipsApproved(deviceInfo, Capability.OPENPGP); - - // after changing the user and admin PINs, we expect a FIPS capable device - // to be FIPS approved - if (isOpenPgpFipsCapable) { - assertNotNull(deviceInfo); - assertTrue("Device not OpenPgp FIPS approved as expected", isFipsApproved); - } + if (scpParameters.getKid() == null && isOpenPgpFipsCapable) { + Assume.assumeTrue( + "Trying to use OpenPgp FIPS capable device over NFC without SCP", isUsbTransport()); } - public void withOpenPgp(StatefulSessionCallback callback) - throws Throwable { - try (SmartCardConnection connection = openSmartCardConnection()) { - callback.invoke(getOpenPgpSession(connection, scpParameters), this); - } - reconnect(); + if (scpParameters.getKid() != null) { + // skip the test if the connected key does not provide matching SCP keys + Assume.assumeTrue( + "No matching key params found for required kid", scpParameters.getKeyParams() != null); } - @Nullable - public static OpenPgpSession getOpenPgpSession(SmartCardConnection connection, ScpParameters scpParameters) - throws IOException, CommandException { - try { - return new OpenPgpSession(connection, scpParameters.getKeyParams()); - } catch (ApplicationNotAvailableException ignored) { - // no OpenPgp support - } - return null; + try (SmartCardConnection connection = openSmartCardConnection()) { + assumeTrue("Smart card not available", connection != null); + + OpenPgpSession openPgp = getOpenPgpSession(connection, scpParameters); + + assumeTrue("OpenPGP not available", openPgp != null); + openPgp.reset(); + + if (hasPinComplexity) { + // only use complex pins if pin complexity is required + openPgp.changeUserPin(defaultUserPin, COMPLEX_USER_PIN); + openPgp.changeAdminPin(defaultAdminPin, COMPLEX_ADMIN_PIN); + defaultUserPin = COMPLEX_USER_PIN; + defaultAdminPin = COMPLEX_ADMIN_PIN; + } + } + + deviceInfo = getDeviceInfo(); + isFipsApproved = isFipsApproved(deviceInfo, Capability.OPENPGP); + + // after changing the user and admin PINs, we expect a FIPS capable device + // to be FIPS approved + if (isOpenPgpFipsCapable) { + assertNotNull(deviceInfo); + assertTrue("Device not OpenPgp FIPS approved as expected", isFipsApproved); + } + } + + public void withOpenPgp(StatefulSessionCallback callback) + throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + callback.invoke(getOpenPgpSession(connection, scpParameters), this); + } + reconnect(); + } + + @Nullable public static OpenPgpSession getOpenPgpSession( + SmartCardConnection connection, ScpParameters scpParameters) + throws IOException, CommandException { + try { + return new OpenPgpSession(connection, scpParameters.getKeyParams()); + } catch (ApplicationNotAvailableException ignored) { + // no OpenPgp support } + return null; + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivBioMultiProtocolDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivBioMultiProtocolDeviceTests.java index e147d855..f542d937 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivBioMultiProtocolDeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivBioMultiProtocolDeviceTests.java @@ -26,42 +26,42 @@ import com.yubico.yubikit.core.smartcard.ApduException; import com.yubico.yubikit.piv.BioMetadata; import com.yubico.yubikit.piv.PivSession; - import java.io.IOException; public class PivBioMultiProtocolDeviceTests { - /** - * Verify authentication with YubiKey Bio Multi-protocol. - *

- * To run the test, create a PIN and enroll at least one fingerprint. The test will ask twice - * for fingerprint authentication. - */ - public static void testAuthenticate(PivSession piv, PivTestState ignored) throws IOException, ApduException, InvalidPinException { - try { - BioMetadata bioMetadata = piv.getBioMetadata(); + /** + * Verify authentication with YubiKey Bio Multi-protocol. + * + *

To run the test, create a PIN and enroll at least one fingerprint. The test will ask twice + * for fingerprint authentication. + */ + public static void testAuthenticate(PivSession piv, PivTestState ignored) + throws IOException, ApduException, InvalidPinException { + try { + BioMetadata bioMetadata = piv.getBioMetadata(); - // we have correct key, is it configured? - assumeTrue("Key has no bio multi-protocol functionality", bioMetadata.isConfigured()); - assumeTrue("Key has no matches left", bioMetadata.getAttemptsRemaining() > 0); + // we have correct key, is it configured? + assumeTrue("Key has no bio multi-protocol functionality", bioMetadata.isConfigured()); + assumeTrue("Key has no matches left", bioMetadata.getAttemptsRemaining() > 0); - assertNull(piv.verifyUv(false, false)); - assertFalse(piv.getBioMetadata().hasTemporaryPin()); + assertNull(piv.verifyUv(false, false)); + assertFalse(piv.getBioMetadata().hasTemporaryPin()); - // check verified state - assertNull(piv.verifyUv(false, true)); + // check verified state + assertNull(piv.verifyUv(false, true)); - byte[] pin = piv.verifyUv(true, false); - assertNotNull(pin); - assertTrue(piv.getBioMetadata().hasTemporaryPin()); + byte[] pin = piv.verifyUv(true, false); + assertNotNull(pin); + assertTrue(piv.getBioMetadata().hasTemporaryPin()); - // check verified state - assertNull(piv.verifyUv(false, true)); + // check verified state + assertNull(piv.verifyUv(false, true)); - piv.verifyTemporaryPin(pin); + piv.verifyTemporaryPin(pin); - } catch (UnsupportedOperationException e) { - assumeNoException("Key has no bio multi-protocol functionality", e); - } + } catch (UnsupportedOperationException e) { + assumeNoException("Key has no bio multi-protocol functionality", e); } + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivCertificateTests.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivCertificateTests.java index 42a71fca..ac5c0f9a 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivCertificateTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivCertificateTests.java @@ -23,52 +23,60 @@ import com.yubico.yubikit.piv.KeyType; import com.yubico.yubikit.piv.PivSession; import com.yubico.yubikit.piv.Slot; - -import org.junit.Assert; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.security.KeyPair; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Arrays; +import org.junit.Assert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class PivCertificateTests { - private static final Logger logger = LoggerFactory.getLogger(PivCertificateTests.class); + private static final Logger logger = LoggerFactory.getLogger(PivCertificateTests.class); - public static void putUncompressedCertificate(PivSession piv, PivTestState state) throws IOException, ApduException, CertificateException, BadResponseException { - putCertificate(piv, state, false); - } + public static void putUncompressedCertificate(PivSession piv, PivTestState state) + throws IOException, ApduException, CertificateException, BadResponseException { + putCertificate(piv, state, false); + } - public static void putCompressedCertificate(PivSession piv, PivTestState state) throws IOException, ApduException, CertificateException, BadResponseException { - putCertificate(piv, state, true); - } + public static void putCompressedCertificate(PivSession piv, PivTestState state) + throws IOException, ApduException, CertificateException, BadResponseException { + putCertificate(piv, state, true); + } - private static void putCertificate(PivSession piv, PivTestState state, boolean compressed) throws IOException, ApduException, CertificateException, BadResponseException { - piv.authenticate(state.managementKey); + private static void putCertificate(PivSession piv, PivTestState state, boolean compressed) + throws IOException, ApduException, CertificateException, BadResponseException { + piv.authenticate(state.managementKey); - for (KeyType keyType : Arrays.asList(KeyType.ECCP256, KeyType.ECCP384, KeyType.RSA1024, KeyType.RSA2048, KeyType.RSA3072, KeyType.RSA4096)) { + for (KeyType keyType : + Arrays.asList( + KeyType.ECCP256, + KeyType.ECCP384, + KeyType.RSA1024, + KeyType.RSA2048, + KeyType.RSA3072, + KeyType.RSA4096)) { - if (((keyType == KeyType.RSA3072 || keyType == KeyType.RSA4096) && !piv.supports(FEATURE_RSA3072_RSA4096))){ - continue; // Run only on compatible keys - } + if (((keyType == KeyType.RSA3072 || keyType == KeyType.RSA4096) + && !piv.supports(FEATURE_RSA3072_RSA4096))) { + continue; // Run only on compatible keys + } - Slot slot = Slot.SIGNATURE; - logger.info("Putting {} {} certificate to slot {}", - compressed ? "compressed" : "not compressed", - keyType.name(), - slot.name()); - KeyPair keyPair = PivTestUtils.loadKey(keyType); - X509Certificate cert = PivTestUtils.createCertificate(keyPair); - piv.putCertificate(slot, cert, compressed); + Slot slot = Slot.SIGNATURE; + logger.info( + "Putting {} {} certificate to slot {}", + compressed ? "compressed" : "not compressed", + keyType.name(), + slot.name()); + KeyPair keyPair = PivTestUtils.loadKey(keyType); + X509Certificate cert = PivTestUtils.createCertificate(keyPair); + piv.putCertificate(slot, cert, compressed); - // get and compare cert - logger.debug("Getting {} certificate from slot {}", - keyType.name(), - slot.name()); - X509Certificate loadedCert = piv.getCertificate(slot); - Assert.assertEquals(cert, loadedCert); - } + // get and compare cert + logger.debug("Getting {} certificate from slot {}", keyType.name(), slot.name()); + X509Certificate loadedCert = piv.getCertificate(slot); + Assert.assertEquals(cert, loadedCert); } + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivDeviceTests.java index 5c9d3e3f..b4e9c3c3 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivDeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivDeviceTests.java @@ -23,7 +23,7 @@ import com.yubico.yubikit.piv.InvalidPinException; import com.yubico.yubikit.piv.ManagementKeyType; import com.yubico.yubikit.piv.PivSession; - +import java.io.IOException; import org.bouncycastle.util.encoders.Hex; import org.hamcrest.CoreMatchers; import org.hamcrest.MatcherAssert; @@ -32,155 +32,157 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; - public class PivDeviceTests { - private static final Logger logger = LoggerFactory.getLogger(PivDeviceTests.class); + private static final Logger logger = LoggerFactory.getLogger(PivDeviceTests.class); + + public static void testManagementKey(PivSession piv, PivTestState state) + throws BadResponseException, IOException, ApduException { + byte[] key2 = Hex.decode("010203040102030401020304010203040102030401020304"); + + ManagementKeyType managementKeyType = piv.getManagementKeyType(); + + logger.debug("Authenticate with the wrong key"); + try { + piv.authenticate(key2); + Assert.fail("Authenticated with wrong key"); + } catch (ApduException e) { + Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); + } + + logger.debug("Change management key"); + piv.authenticate(state.managementKey); + piv.setManagementKey(managementKeyType, key2, false); - public static void testManagementKey(PivSession piv, PivTestState state) throws BadResponseException, IOException, ApduException { - byte[] key2 = Hex.decode("010203040102030401020304010203040102030401020304"); + logger.debug("Authenticate with the old key"); + try { + piv.authenticate(state.managementKey); + Assert.fail("Authenticated with wrong key"); + } catch (ApduException e) { + Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); + } - ManagementKeyType managementKeyType = piv.getManagementKeyType(); + logger.debug("Change management key"); + piv.authenticate(key2); + piv.setManagementKey(managementKeyType, state.managementKey, false); + } - logger.debug("Authenticate with the wrong key"); - try { - piv.authenticate(key2); - Assert.fail("Authenticated with wrong key"); - } catch (ApduException e) { - Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); - } + public static void testManagementKeyType(PivSession piv, PivTestState state) + throws BadResponseException, IOException, ApduException { + Assume.assumeTrue("No AES key support", piv.supports(FEATURE_AES_KEY)); - logger.debug("Change management key"); - piv.authenticate(state.managementKey); - piv.setManagementKey(managementKeyType, key2, false); + ManagementKeyType managementKeyType = piv.getManagementKeyType(); + byte[] aes128Key = Hex.decode("01020304010203040102030401020304"); - logger.debug("Authenticate with the old key"); - try { - piv.authenticate(state.managementKey); - Assert.fail("Authenticated with wrong key"); - } catch (ApduException e) { - Assert.assertEquals(SW.SECURITY_CONDITION_NOT_SATISFIED, e.getSw()); - } + logger.debug("Change management key type"); + piv.authenticate(state.managementKey); + piv.setManagementKey(ManagementKeyType.AES128, aes128Key, false); + Assert.assertEquals(ManagementKeyType.AES128, piv.getManagementKeyType()); - logger.debug("Change management key"); - piv.authenticate(key2); - piv.setManagementKey(managementKeyType, state.managementKey, false); + try { + piv.authenticate(state.managementKey); + Assert.fail("Authenticated with wrong key type"); + } catch (IllegalArgumentException e) { + // ignored } - public static void testManagementKeyType(PivSession piv, PivTestState state) throws BadResponseException, IOException, ApduException { - Assume.assumeTrue("No AES key support", piv.supports(FEATURE_AES_KEY)); + // set original management key type + piv.authenticate(aes128Key); + piv.setManagementKey(managementKeyType, state.managementKey, false); + } + + public static void testPin(PivSession piv, PivTestState state) + throws ApduException, InvalidPinException, IOException, BadResponseException { + // Ensure we only try this if the default management key is set. + piv.authenticate(state.managementKey); + + logger.debug("Verify PIN"); + char[] pin2 = "11231123".toCharArray(); + piv.verifyPin(state.pin); + MatcherAssert.assertThat(piv.getPinAttempts(), CoreMatchers.equalTo(3)); + + logger.debug("Verify with wrong PIN"); + try { + piv.verifyPin(pin2); + Assert.fail("Verify with wrong PIN"); + } catch (InvalidPinException e) { + MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(2)); + MatcherAssert.assertThat(piv.getPinAttempts(), CoreMatchers.equalTo(2)); + } - ManagementKeyType managementKeyType = piv.getManagementKeyType(); - byte[] aes128Key = Hex.decode("01020304010203040102030401020304"); + logger.debug("Change PIN with wrong PIN"); + try { + piv.changePin(pin2, state.pin); + Assert.fail("Change PIN with wrong PIN"); + } catch (InvalidPinException e) { + MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(1)); + MatcherAssert.assertThat(piv.getPinAttempts(), CoreMatchers.equalTo(1)); + } - logger.debug("Change management key type"); - piv.authenticate(state.managementKey); - piv.setManagementKey(ManagementKeyType.AES128, aes128Key, false); - Assert.assertEquals(ManagementKeyType.AES128, piv.getManagementKeyType()); + logger.debug("Change PIN"); + piv.changePin(state.pin, pin2); + piv.verifyPin(pin2); + + logger.debug("Verify with wrong PIN"); + try { + piv.verifyPin(state.pin); + Assert.fail("Verify with wrong PIN"); + } catch (InvalidPinException e) { + MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(2)); + MatcherAssert.assertThat(piv.getPinAttempts(), CoreMatchers.equalTo(2)); + } - try { - piv.authenticate(state.managementKey); - Assert.fail("Authenticated with wrong key type"); - } catch (IllegalArgumentException e) { - // ignored - } + logger.debug("Change PIN"); + piv.changePin(pin2, state.pin); + } + + public static void testPuk(PivSession piv, PivTestState state) + throws ApduException, InvalidPinException, IOException, BadResponseException { + // Ensure we only try this if the default management key is set. + piv.authenticate(state.managementKey); + + // Change PUK + char[] puk2 = "12341234".toCharArray(); + piv.changePuk(state.puk, puk2); + piv.verifyPin(state.pin); + + // Block PIN + while (piv.getPinAttempts() > 0) { + try { + piv.verifyPin(puk2); + } catch (InvalidPinException e) { + // Re-run until blocked... + } + } - // set original management key type - piv.authenticate(aes128Key); - piv.setManagementKey(managementKeyType, state.managementKey, false); + // Verify PIN blocked + try { + piv.verifyPin(state.pin); + } catch (InvalidPinException e) { + MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(0)); + MatcherAssert.assertThat(piv.getPinAttempts(), CoreMatchers.equalTo(0)); } - public static void testPin(PivSession piv, PivTestState state) throws ApduException, InvalidPinException, IOException, BadResponseException { - // Ensure we only try this if the default management key is set. - piv.authenticate(state.managementKey); - - logger.debug("Verify PIN"); - char[] pin2 = "11231123".toCharArray(); - piv.verifyPin(state.pin); - MatcherAssert.assertThat(piv.getPinAttempts(), CoreMatchers.equalTo(3)); - - logger.debug("Verify with wrong PIN"); - try { - piv.verifyPin(pin2); - Assert.fail("Verify with wrong PIN"); - } catch (InvalidPinException e) { - MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(2)); - MatcherAssert.assertThat(piv.getPinAttempts(), CoreMatchers.equalTo(2)); - } - - logger.debug("Change PIN with wrong PIN"); - try { - piv.changePin(pin2, state.pin); - Assert.fail("Change PIN with wrong PIN"); - } catch (InvalidPinException e) { - MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(1)); - MatcherAssert.assertThat(piv.getPinAttempts(), CoreMatchers.equalTo(1)); - } - - logger.debug("Change PIN"); - piv.changePin(state.pin, pin2); - piv.verifyPin(pin2); - - logger.debug("Verify with wrong PIN"); - try { - piv.verifyPin(state.pin); - Assert.fail("Verify with wrong PIN"); - } catch (InvalidPinException e) { - MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(2)); - MatcherAssert.assertThat(piv.getPinAttempts(), CoreMatchers.equalTo(2)); - } - - logger.debug("Change PIN"); - piv.changePin(pin2, state.pin); + // Try unblock with wrong PUK + try { + piv.unblockPin(state.puk, state.pin); + Assert.fail("Unblock with wrong PUK"); + } catch (InvalidPinException e) { + MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(2)); } - public static void testPuk(PivSession piv, PivTestState state) throws ApduException, InvalidPinException, IOException, BadResponseException { - // Ensure we only try this if the default management key is set. - piv.authenticate(state.managementKey); - - // Change PUK - char[] puk2 = "12341234".toCharArray(); - piv.changePuk(state.puk, puk2); - piv.verifyPin(state.pin); - - // Block PIN - while (piv.getPinAttempts() > 0) { - try { - piv.verifyPin(puk2); - } catch (InvalidPinException e) { - //Re-run until blocked... - } - } - - // Verify PIN blocked - try { - piv.verifyPin(state.pin); - } catch (InvalidPinException e) { - MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(0)); - MatcherAssert.assertThat(piv.getPinAttempts(), CoreMatchers.equalTo(0)); - } - - // Try unblock with wrong PUK - try { - piv.unblockPin(state.puk, state.pin); - Assert.fail("Unblock with wrong PUK"); - } catch (InvalidPinException e) { - MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(2)); - } - - // Unblock PIN - piv.unblockPin(puk2, state.pin); - - // Try to change PUK with wrong PUK - try { - piv.changePuk(state.puk, puk2); - Assert.fail("Change PUK with wrong PUK"); - } catch (InvalidPinException e) { - MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(2)); - } - - // Change PUK - piv.changePuk(puk2, state.puk); + // Unblock PIN + piv.unblockPin(puk2, state.pin); + + // Try to change PUK with wrong PUK + try { + piv.changePuk(state.puk, puk2); + Assert.fail("Change PUK with wrong PUK"); + } catch (InvalidPinException e) { + MatcherAssert.assertThat(e.getAttemptsRemaining(), CoreMatchers.equalTo(2)); } + + // Change PUK + piv.changePuk(puk2, state.puk); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaDecryptTests.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaDecryptTests.java index 650c956e..0fb5f04f 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaDecryptTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaDecryptTests.java @@ -29,12 +29,6 @@ import com.yubico.yubikit.piv.Slot; import com.yubico.yubikit.piv.TouchPolicy; import com.yubico.yubikit.piv.jca.PivAlgorithmParameterSpec; - -import org.junit.Assert; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; @@ -42,64 +36,92 @@ import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; - import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; +import org.junit.Assert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class PivJcaDecryptTests { - private static final Logger logger = LoggerFactory.getLogger(PivJcaDecryptTests.class); - - public static void testDecrypt(PivSession piv, PivTestState state) throws BadResponseException, IOException, ApduException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException { - setupJca(piv); - for (KeyType keyType : KeyType.values()) { - if (((keyType == KeyType.RSA3072 || keyType == KeyType.RSA4096) && !piv.supports(FEATURE_RSA3072_RSA4096))) { - continue; // Run only on compatible keys - } - if (keyType.params.algorithm.name().equals("RSA")) { - testDecrypt(piv, state, keyType); - } - } - tearDownJca(); + private static final Logger logger = LoggerFactory.getLogger(PivJcaDecryptTests.class); + + public static void testDecrypt(PivSession piv, PivTestState state) + throws BadResponseException, + IOException, + ApduException, + NoSuchPaddingException, + NoSuchAlgorithmException, + InvalidKeyException, + BadPaddingException, + IllegalBlockSizeException, + InvalidAlgorithmParameterException { + setupJca(piv); + for (KeyType keyType : KeyType.values()) { + if (((keyType == KeyType.RSA3072 || keyType == KeyType.RSA4096) + && !piv.supports(FEATURE_RSA3072_RSA4096))) { + continue; // Run only on compatible keys + } + if (keyType.params.algorithm.name().equals("RSA")) { + testDecrypt(piv, state, keyType); + } } - - public static void testDecrypt(PivSession piv, PivTestState state, KeyType keyType) throws BadResponseException, IOException, ApduException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException { - - if (keyType.params.algorithm != KeyType.Algorithm.RSA) { - throw new IllegalArgumentException("Unsupported"); - } - - if (state.isInvalidKeyType(keyType)) { - return; - } - - piv.authenticate(state.managementKey); - logger.debug("Generate key: {}", keyType); - KeyPairGenerator kpg = KeyPairGenerator.getInstance("YKPivRSA"); - kpg.initialize(new PivAlgorithmParameterSpec(Slot.KEY_MANAGEMENT, keyType, PinPolicy.DEFAULT, TouchPolicy.DEFAULT, state.pin)); - KeyPair pair = kpg.generateKeyPair(); - - testDecrypt(pair, Cipher.getInstance("RSA/ECB/PKCS1Padding")); - testDecrypt(pair, Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding")); - testDecrypt(pair, Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")); + tearDownJca(); + } + + public static void testDecrypt(PivSession piv, PivTestState state, KeyType keyType) + throws BadResponseException, + IOException, + ApduException, + NoSuchPaddingException, + NoSuchAlgorithmException, + InvalidKeyException, + BadPaddingException, + IllegalBlockSizeException, + InvalidAlgorithmParameterException { + + if (keyType.params.algorithm != KeyType.Algorithm.RSA) { + throw new IllegalArgumentException("Unsupported"); } - public static void testDecrypt(KeyPair keyPair, Cipher cipher) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { - byte[] message = "Hello world!".getBytes(StandardCharsets.UTF_8); - - logger.debug("Using cipher {}", cipher.getAlgorithm()); - - cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic()); - byte[] ct = cipher.doFinal(message); - logger.debug("Cipher text {}: {}", ct.length, StringUtils.bytesToHex(ct)); - - Cipher decryptCipher = Cipher.getInstance(cipher.getAlgorithm()); - decryptCipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate()); - byte[] pt = decryptCipher.doFinal(ct); - - Assert.assertArrayEquals(message, pt); - logger.debug("Decrypt successful for {}", cipher.getAlgorithm()); + if (state.isInvalidKeyType(keyType)) { + return; } + + piv.authenticate(state.managementKey); + logger.debug("Generate key: {}", keyType); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("YKPivRSA"); + kpg.initialize( + new PivAlgorithmParameterSpec( + Slot.KEY_MANAGEMENT, keyType, PinPolicy.DEFAULT, TouchPolicy.DEFAULT, state.pin)); + KeyPair pair = kpg.generateKeyPair(); + + testDecrypt(pair, Cipher.getInstance("RSA/ECB/PKCS1Padding")); + testDecrypt(pair, Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding")); + testDecrypt(pair, Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")); + } + + public static void testDecrypt(KeyPair keyPair, Cipher cipher) + throws NoSuchPaddingException, + NoSuchAlgorithmException, + InvalidKeyException, + BadPaddingException, + IllegalBlockSizeException { + byte[] message = "Hello world!".getBytes(StandardCharsets.UTF_8); + + logger.debug("Using cipher {}", cipher.getAlgorithm()); + + cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic()); + byte[] ct = cipher.doFinal(message); + logger.debug("Cipher text {}: {}", ct.length, StringUtils.bytesToHex(ct)); + + Cipher decryptCipher = Cipher.getInstance(cipher.getAlgorithm()); + decryptCipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate()); + byte[] pt = decryptCipher.doFinal(ct); + + Assert.assertArrayEquals(message, pt); + logger.debug("Decrypt successful for {}", cipher.getAlgorithm()); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaDeviceTests.java index 36c98d77..904f8210 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaDeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaDeviceTests.java @@ -21,7 +21,6 @@ import static com.yubico.yubikit.testing.piv.PivJcaUtils.setupJca; import static com.yubico.yubikit.testing.piv.PivJcaUtils.tearDownJca; -import com.yubico.yubikit.core.Version; import com.yubico.yubikit.piv.KeyType; import com.yubico.yubikit.piv.PinPolicy; import com.yubico.yubikit.piv.PivSession; @@ -30,10 +29,6 @@ import com.yubico.yubikit.piv.jca.PivAlgorithmParameterSpec; import com.yubico.yubikit.piv.jca.PivKeyStoreKeyParameters; import com.yubico.yubikit.piv.jca.PivProvider; - -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.junit.Assert; - import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; @@ -42,171 +37,189 @@ import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.Arrays; - import javax.security.auth.Destroyable; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.Assert; public class PivJcaDeviceTests { - @SuppressWarnings("NewApi") // casting to Destroyable is supported from API 26 - public static void testImportKeys(PivSession piv, PivTestState state) throws Exception { - setupJca(piv); - piv.authenticate(state.managementKey); - - KeyStore keyStore = KeyStore.getInstance("YKPiv"); - keyStore.load(null); + @SuppressWarnings("NewApi") // casting to Destroyable is supported from API 26 + public static void testImportKeys(PivSession piv, PivTestState state) throws Exception { + setupJca(piv); + piv.authenticate(state.managementKey); - for (KeyType keyType : Arrays.asList(KeyType.RSA1024, KeyType.RSA2048, KeyType.RSA3072, KeyType.RSA4096)) { + KeyStore keyStore = KeyStore.getInstance("YKPiv"); + keyStore.load(null); - if (state.isInvalidKeyType(keyType)) { - continue; - } + for (KeyType keyType : + Arrays.asList(KeyType.RSA1024, KeyType.RSA2048, KeyType.RSA3072, KeyType.RSA4096)) { - if (!piv.supports(FEATURE_RSA3072_RSA4096) && (keyType == KeyType.RSA3072 || keyType == KeyType.RSA4096)) { - continue; - } + if (state.isInvalidKeyType(keyType)) { + continue; + } - String alias = Slot.SIGNATURE.getStringAlias(); + if (!piv.supports(FEATURE_RSA3072_RSA4096) + && (keyType == KeyType.RSA3072 || keyType == KeyType.RSA4096)) { + continue; + } - KeyPair keyPair = PivTestUtils.loadKey(keyType); - X509Certificate cert = PivTestUtils.createCertificate(keyPair); - keyStore.setEntry(alias, new KeyStore.PrivateKeyEntry(keyPair.getPrivate(), new Certificate[]{cert}), new PivKeyStoreKeyParameters(PinPolicy.DEFAULT, TouchPolicy.DEFAULT)); - PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, state.pin); + String alias = Slot.SIGNATURE.getStringAlias(); - PivTestUtils.rsaEncryptAndDecrypt(privateKey, keyPair.getPublic()); - PivTestUtils.rsaSignAndVerify(privateKey, keyPair.getPublic()); + KeyPair keyPair = PivTestUtils.loadKey(keyType); + X509Certificate cert = PivTestUtils.createCertificate(keyPair); + keyStore.setEntry( + alias, + new KeyStore.PrivateKeyEntry(keyPair.getPrivate(), new Certificate[] {cert}), + new PivKeyStoreKeyParameters(PinPolicy.DEFAULT, TouchPolicy.DEFAULT)); + PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, state.pin); - //noinspection RedundantCast - ((Destroyable) privateKey).destroy(); - } - - for (KeyType keyType : Arrays.asList(KeyType.ECCP256, KeyType.ECCP384)) { + PivTestUtils.rsaEncryptAndDecrypt(privateKey, keyPair.getPublic()); + PivTestUtils.rsaSignAndVerify(privateKey, keyPair.getPublic()); - if (!piv.supports(FEATURE_P384) && keyType == KeyType.ECCP384) { - continue; - } + //noinspection RedundantCast + ((Destroyable) privateKey).destroy(); + } - String alias = Slot.SIGNATURE.getStringAlias(); + for (KeyType keyType : Arrays.asList(KeyType.ECCP256, KeyType.ECCP384)) { - KeyPair keyPair = PivTestUtils.loadKey(keyType); - X509Certificate cert = PivTestUtils.createCertificate(keyPair); + if (!piv.supports(FEATURE_P384) && keyType == KeyType.ECCP384) { + continue; + } - keyStore.setEntry(alias, new KeyStore.PrivateKeyEntry(keyPair.getPrivate(), new Certificate[]{cert}), new PivKeyStoreKeyParameters(PinPolicy.DEFAULT, TouchPolicy.DEFAULT)); - PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, state.pin); + String alias = Slot.SIGNATURE.getStringAlias(); - PivTestUtils.ecKeyAgreement(privateKey, keyPair.getPublic()); - PivTestUtils.ecSignAndVerify(privateKey, keyPair.getPublic()); + KeyPair keyPair = PivTestUtils.loadKey(keyType); + X509Certificate cert = PivTestUtils.createCertificate(keyPair); - //noinspection RedundantCast - ((Destroyable) privateKey).destroy(); + keyStore.setEntry( + alias, + new KeyStore.PrivateKeyEntry(keyPair.getPrivate(), new Certificate[] {cert}), + new PivKeyStoreKeyParameters(PinPolicy.DEFAULT, TouchPolicy.DEFAULT)); + PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, state.pin); - Assert.assertEquals(cert, keyStore.getCertificate(keyStore.getCertificateAlias(cert))); - } + PivTestUtils.ecKeyAgreement(privateKey, keyPair.getPublic()); + PivTestUtils.ecSignAndVerify(privateKey, keyPair.getPublic()); - if (piv.supports(FEATURE_CV25519)) { - for (KeyType keyType : Arrays.asList(KeyType.ED25519, KeyType.X25519)) { + //noinspection RedundantCast + ((Destroyable) privateKey).destroy(); - if (state.isInvalidKeyType(keyType)) { - continue; - } + Assert.assertEquals(cert, keyStore.getCertificate(keyStore.getCertificateAlias(cert))); + } - String alias = Slot.SIGNATURE.getStringAlias(); + if (piv.supports(FEATURE_CV25519)) { + for (KeyType keyType : Arrays.asList(KeyType.ED25519, KeyType.X25519)) { - KeyPair keyPair = PivTestUtils.loadKey(keyType); - X509Certificate cert = PivTestUtils.createCertificate(keyPair); + if (state.isInvalidKeyType(keyType)) { + continue; + } - keyStore.setEntry(alias, new KeyStore.PrivateKeyEntry(keyPair.getPrivate(), new Certificate[]{cert}), new PivKeyStoreKeyParameters(PinPolicy.DEFAULT, TouchPolicy.DEFAULT)); - PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, state.pin); + String alias = Slot.SIGNATURE.getStringAlias(); - if (keyType == KeyType.X25519) { - PivTestUtils.x25519KeyAgreement(privateKey, keyPair.getPublic()); - } + KeyPair keyPair = PivTestUtils.loadKey(keyType); + X509Certificate cert = PivTestUtils.createCertificate(keyPair); - if (keyType == KeyType.ED25519) { - PivTestUtils.ed25519SignAndVerify(privateKey, keyPair.getPublic()); - } + keyStore.setEntry( + alias, + new KeyStore.PrivateKeyEntry(keyPair.getPrivate(), new Certificate[] {cert}), + new PivKeyStoreKeyParameters(PinPolicy.DEFAULT, TouchPolicy.DEFAULT)); + PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, state.pin); - //noinspection RedundantCast - ((Destroyable) privateKey).destroy(); + if (keyType == KeyType.X25519) { + PivTestUtils.x25519KeyAgreement(privateKey, keyPair.getPublic()); + } - Assert.assertEquals(cert, keyStore.getCertificate(keyStore.getCertificateAlias(cert))); - } + if (keyType == KeyType.ED25519) { + PivTestUtils.ed25519SignAndVerify(privateKey, keyPair.getPublic()); } - tearDownJca(); - } + //noinspection RedundantCast + ((Destroyable) privateKey).destroy(); - public static void testGenerateKeys(PivSession piv, PivTestState state) throws Exception { - setupJca(piv); - generateKeys(piv, state); - tearDownJca(); + Assert.assertEquals(cert, keyStore.getCertificate(keyStore.getCertificateAlias(cert))); + } } - public static void testGenerateKeysPreferBC(PivSession piv, PivTestState state) throws Exception { - // following is an alternate version of setupJca method - // the Bouncy Castle provider is set on second position and will provide Ed25519 and X25519 - // cryptographic services on the host. - Security.removeProvider("BC"); - Security.insertProviderAt(new BouncyCastleProvider(), 1); - Security.insertProviderAt(new PivProvider(piv), 1); - - generateKeys(piv, state); - tearDownJca(); + tearDownJca(); + } + + public static void testGenerateKeys(PivSession piv, PivTestState state) throws Exception { + setupJca(piv); + generateKeys(piv, state); + tearDownJca(); + } + + public static void testGenerateKeysPreferBC(PivSession piv, PivTestState state) throws Exception { + // following is an alternate version of setupJca method + // the Bouncy Castle provider is set on second position and will provide Ed25519 and X25519 + // cryptographic services on the host. + Security.removeProvider("BC"); + Security.insertProviderAt(new BouncyCastleProvider(), 1); + Security.insertProviderAt(new PivProvider(piv), 1); + + generateKeys(piv, state); + tearDownJca(); + } + + private static void generateKeys(PivSession piv, PivTestState state) throws Exception { + piv.authenticate(state.managementKey); + + KeyPairGenerator ecGen = KeyPairGenerator.getInstance("YKPivEC"); + for (KeyType keyType : + Arrays.asList(KeyType.ECCP256, KeyType.ECCP384, KeyType.ED25519, KeyType.X25519)) { + + if (state.isInvalidKeyType(keyType)) { + continue; + } + + if (!piv.supports(FEATURE_P384) && keyType == KeyType.ECCP384) { + continue; + } + + if (!piv.supports(FEATURE_CV25519) + && (keyType == KeyType.ED25519 || keyType == KeyType.X25519)) { + continue; + } + + ecGen.initialize( + new PivAlgorithmParameterSpec(Slot.AUTHENTICATION, keyType, null, null, state.pin)); + KeyPair keyPair = ecGen.generateKeyPair(); + + if (keyType == KeyType.ED25519) { + PivTestUtils.ed25519SignAndVerify(keyPair.getPrivate(), keyPair.getPublic()); + continue; + } + + if (keyType != KeyType.X25519) { + PivTestUtils.ecSignAndVerify(keyPair.getPrivate(), keyPair.getPublic()); + } + + if (keyType != KeyType.X25519) { + PivTestUtils.ecKeyAgreement(keyPair.getPrivate(), keyPair.getPublic()); + } else { + PivTestUtils.x25519KeyAgreement(keyPair.getPrivate(), keyPair.getPublic()); + } + // TODO: Test with key loaded from KeyStore } - private static void generateKeys(PivSession piv, PivTestState state) throws Exception { - piv.authenticate(state.managementKey); - - KeyPairGenerator ecGen = KeyPairGenerator.getInstance("YKPivEC"); - for (KeyType keyType : Arrays.asList(KeyType.ECCP256, KeyType.ECCP384, KeyType.ED25519, KeyType.X25519)) { - - if (state.isInvalidKeyType(keyType)) { - continue; - } - - if (!piv.supports(FEATURE_P384) && keyType == KeyType.ECCP384) { - continue; - } - - if (!piv.supports(FEATURE_CV25519) && (keyType == KeyType.ED25519 || keyType == KeyType.X25519)) { - continue; - } - - ecGen.initialize(new PivAlgorithmParameterSpec(Slot.AUTHENTICATION, keyType, null, null, state.pin)); - KeyPair keyPair = ecGen.generateKeyPair(); - - if (keyType == KeyType.ED25519) { - PivTestUtils.ed25519SignAndVerify(keyPair.getPrivate(), keyPair.getPublic()); - continue; - } - - if (keyType != KeyType.X25519) { - PivTestUtils.ecSignAndVerify(keyPair.getPrivate(), keyPair.getPublic()); - } - - if (keyType != KeyType.X25519) { - PivTestUtils.ecKeyAgreement(keyPair.getPrivate(), keyPair.getPublic()); - } else { - PivTestUtils.x25519KeyAgreement(keyPair.getPrivate(), keyPair.getPublic()); - } - //TODO: Test with key loaded from KeyStore - } - - KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("YKPivRSA"); - for (KeyType keyType : Arrays.asList(KeyType.RSA1024, KeyType.RSA2048, KeyType.RSA3072, KeyType.RSA4096)) { - - if (state.isInvalidKeyType(keyType)) { - continue; - } - - if (!piv.supports(FEATURE_RSA3072_RSA4096) && (keyType == KeyType.RSA3072 || keyType == KeyType.RSA4096)) { - continue; - } - - rsaGen.initialize(new PivAlgorithmParameterSpec(Slot.AUTHENTICATION, keyType, null, null, state.pin)); - KeyPair keyPair = rsaGen.generateKeyPair(); - PivTestUtils.rsaEncryptAndDecrypt(keyPair.getPrivate(), keyPair.getPublic()); - PivTestUtils.rsaSignAndVerify(keyPair.getPrivate(), keyPair.getPublic()); - //TODO: Test with key loaded from KeyStore - } + KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("YKPivRSA"); + for (KeyType keyType : + Arrays.asList(KeyType.RSA1024, KeyType.RSA2048, KeyType.RSA3072, KeyType.RSA4096)) { + + if (state.isInvalidKeyType(keyType)) { + continue; + } + + if (!piv.supports(FEATURE_RSA3072_RSA4096) + && (keyType == KeyType.RSA3072 || keyType == KeyType.RSA4096)) { + continue; + } + + rsaGen.initialize( + new PivAlgorithmParameterSpec(Slot.AUTHENTICATION, keyType, null, null, state.pin)); + KeyPair keyPair = rsaGen.generateKeyPair(); + PivTestUtils.rsaEncryptAndDecrypt(keyPair.getPrivate(), keyPair.getPublic()); + PivTestUtils.rsaSignAndVerify(keyPair.getPrivate(), keyPair.getPublic()); + // TODO: Test with key loaded from KeyStore } + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaSigningTests.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaSigningTests.java index 2cb2f033..6dbcaa3f 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaSigningTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaSigningTests.java @@ -16,8 +16,8 @@ package com.yubico.yubikit.testing.piv; -import static com.yubico.yubikit.piv.PivSession.FEATURE_RSA3072_RSA4096; import static com.yubico.yubikit.piv.PivSession.FEATURE_CV25519; +import static com.yubico.yubikit.piv.PivSession.FEATURE_RSA3072_RSA4096; import static com.yubico.yubikit.testing.piv.PivJcaUtils.setupJca; import static com.yubico.yubikit.testing.piv.PivJcaUtils.tearDownJca; @@ -29,12 +29,6 @@ import com.yubico.yubikit.piv.Slot; import com.yubico.yubikit.piv.TouchPolicy; import com.yubico.yubikit.piv.jca.PivAlgorithmParameterSpec; - -import org.junit.Assert; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; @@ -51,175 +45,234 @@ import java.security.spec.PSSParameterSpec; import java.util.HashSet; import java.util.Set; +import org.junit.Assert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class PivJcaSigningTests { - private static final Logger logger = LoggerFactory.getLogger(PivJcaSigningTests.class); + private static final Logger logger = LoggerFactory.getLogger(PivJcaSigningTests.class); - private static Set signatureAlgorithmsWithPss = new HashSet<>(); + private static Set signatureAlgorithmsWithPss = new HashSet<>(); - public static void testSign(PivSession piv, PivTestState state) throws NoSuchAlgorithmException, IOException, ApduException, InvalidKeyException, BadResponseException, InvalidAlgorithmParameterException, SignatureException { - setupJca(piv); - for (KeyType keyType : KeyType.values()) { - if (((keyType == KeyType.RSA3072 || keyType == KeyType.RSA4096) && !piv.supports(FEATURE_RSA3072_RSA4096))) { - continue; // Run only on compatible keys - } - testSign(piv, state, keyType); - } - tearDownJca(); + public static void testSign(PivSession piv, PivTestState state) + throws NoSuchAlgorithmException, + IOException, + ApduException, + InvalidKeyException, + BadResponseException, + InvalidAlgorithmParameterException, + SignatureException { + setupJca(piv); + for (KeyType keyType : KeyType.values()) { + if (((keyType == KeyType.RSA3072 || keyType == KeyType.RSA4096) + && !piv.supports(FEATURE_RSA3072_RSA4096))) { + continue; // Run only on compatible keys + } + testSign(piv, state, keyType); + } + tearDownJca(); + } + + public static void testSign(PivSession piv, PivTestState state, KeyType keyType) + throws NoSuchAlgorithmException, + IOException, + ApduException, + InvalidKeyException, + BadResponseException, + InvalidAlgorithmParameterException, + SignatureException { + if (!piv.supports(FEATURE_CV25519) + && (keyType == KeyType.ED25519 || keyType == KeyType.X25519)) { + return; + } + + if (state.isInvalidKeyType(keyType)) { + return; + } + + if (keyType == KeyType.X25519) { + logger.debug("Ignoring keyType: {}", keyType); + return; } + piv.authenticate(state.managementKey); + logger.debug("Generate key: {}", keyType); - public static void testSign(PivSession piv, PivTestState state, KeyType keyType) throws NoSuchAlgorithmException, IOException, ApduException, InvalidKeyException, BadResponseException, InvalidAlgorithmParameterException, SignatureException { - if (!piv.supports(FEATURE_CV25519) && (keyType == KeyType.ED25519 || keyType == KeyType.X25519)) { - return; + KeyPairGenerator kpg = KeyPairGenerator.getInstance("YKPiv" + keyType.params.algorithm.name()); + kpg.initialize( + new PivAlgorithmParameterSpec( + Slot.SIGNATURE, keyType, PinPolicy.DEFAULT, TouchPolicy.DEFAULT, state.pin)); + KeyPair keyPair = kpg.generateKeyPair(); + + signatureAlgorithmsWithPss = getAllSignatureAlgorithmsWithPSS(); + + switch (keyType.params.algorithm) { + case EC: + if (keyType != KeyType.ED25519) { + testSign(keyPair, "SHA1withECDSA", null); + testSign(keyPair, "SHA256withECDSA", null); + //noinspection SpellCheckingInspection + testSign(keyPair, "NONEwithECDSA", null); + testSign(keyPair, "SHA3-256withECDSA", null); + } else { + testSign(keyPair, "ED25519", null); } + break; + case RSA: + testSign(keyPair, "SHA1withRSA", null); + testSign(keyPair, "SHA256withRSA", null); - if (state.isInvalidKeyType(keyType)) { - return; + String signatureAlgorithm = "SHA1WITHRSA/PSS"; + if (signatureAlgorithmsWithPss.contains(signatureAlgorithm)) { + testSign( + keyPair, + signatureAlgorithm, + new PSSParameterSpec("SHA-1", "MGF1", MGF1ParameterSpec.SHA1, 8, 1)); + PSSParameterSpec param = + new PSSParameterSpec("SHA-1", "MGF1", MGF1ParameterSpec.SHA1, 0, 1); + byte[] sig1 = testSign(keyPair, signatureAlgorithm, param); + byte[] sig2 = testSign(keyPair, signatureAlgorithm, param); + Assert.assertArrayEquals( + "PSS parameters not used, signatures are not identical!", sig1, sig2); } - if (keyType == KeyType.X25519) { - logger.debug("Ignoring keyType: {}", keyType); - return; + try { + signatureAlgorithm = "SHA224WITHRSA/PSS"; + if (signatureAlgorithmsWithPss.contains(signatureAlgorithm)) { + @SuppressWarnings("NewApi") + PSSParameterSpec saltedParam = + new PSSParameterSpec("SHA-224", "MGF1", MGF1ParameterSpec.SHA224, 8, 1); + testSign(keyPair, signatureAlgorithm, saltedParam); + + @SuppressWarnings("NewApi") + PSSParameterSpec param = + new PSSParameterSpec("SHA-224", "MGF1", MGF1ParameterSpec.SHA224, 0, 1); + byte[] sig1 = testSign(keyPair, signatureAlgorithm, param); + byte[] sig2 = testSign(keyPair, signatureAlgorithm, param); + Assert.assertArrayEquals( + "PSS parameters not used, signatures are not identical!", sig1, sig2); + } + } catch (NoSuchFieldError noSuchFieldError) { + // MGF1ParameterSpec.SHA224 is supported from Android API 26 + logger.debug("Ignoring following error: {}", noSuchFieldError.getMessage()); } - piv.authenticate(state.managementKey); - logger.debug("Generate key: {}", keyType); - - KeyPairGenerator kpg = KeyPairGenerator.getInstance("YKPiv" + keyType.params.algorithm.name()); - kpg.initialize(new PivAlgorithmParameterSpec(Slot.SIGNATURE, keyType, PinPolicy.DEFAULT, TouchPolicy.DEFAULT, state.pin)); - KeyPair keyPair = kpg.generateKeyPair(); - - signatureAlgorithmsWithPss = getAllSignatureAlgorithmsWithPSS(); - - switch (keyType.params.algorithm) { - case EC: - if (keyType != KeyType.ED25519) { - testSign(keyPair, "SHA1withECDSA", null); - testSign(keyPair, "SHA256withECDSA", null); - //noinspection SpellCheckingInspection - testSign(keyPair, "NONEwithECDSA", null); - testSign(keyPair, "SHA3-256withECDSA", null); - } else { - testSign(keyPair, "ED25519", null); - } - break; - case RSA: - testSign(keyPair, "SHA1withRSA", null); - testSign(keyPair, "SHA256withRSA", null); - - String signatureAlgorithm = "SHA1WITHRSA/PSS"; - if (signatureAlgorithmsWithPss.contains(signatureAlgorithm)) { - testSign(keyPair, signatureAlgorithm, new PSSParameterSpec("SHA-1", "MGF1", MGF1ParameterSpec.SHA1, 8, 1)); - PSSParameterSpec param = new PSSParameterSpec("SHA-1", "MGF1", MGF1ParameterSpec.SHA1, 0, 1); - byte[] sig1 = testSign(keyPair, signatureAlgorithm, param); - byte[] sig2 = testSign(keyPair, signatureAlgorithm, param); - Assert.assertArrayEquals("PSS parameters not used, signatures are not identical!", sig1, sig2); - } - - try { - signatureAlgorithm = "SHA224WITHRSA/PSS"; - if (signatureAlgorithmsWithPss.contains(signatureAlgorithm)) { - @SuppressWarnings("NewApi") - PSSParameterSpec saltedParam = new PSSParameterSpec("SHA-224", "MGF1", MGF1ParameterSpec.SHA224, 8, 1); - testSign(keyPair, signatureAlgorithm, saltedParam); - - @SuppressWarnings("NewApi") - PSSParameterSpec param = new PSSParameterSpec("SHA-224", "MGF1", MGF1ParameterSpec.SHA224, 0, 1); - byte[] sig1 = testSign(keyPair, signatureAlgorithm, param); - byte[] sig2 = testSign(keyPair, signatureAlgorithm, param); - Assert.assertArrayEquals("PSS parameters not used, signatures are not identical!", sig1, sig2); - } - } catch (NoSuchFieldError noSuchFieldError) { - // MGF1ParameterSpec.SHA224 is supported from Android API 26 - logger.debug("Ignoring following error: {}", noSuchFieldError.getMessage()); - } - - signatureAlgorithm = "SHA256WITHRSA/PSS"; - if (signatureAlgorithmsWithPss.contains(signatureAlgorithm)) { - testSign(keyPair, signatureAlgorithm, new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 8, 1)); - PSSParameterSpec param = new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 0, 1); - byte[] sig1 = testSign(keyPair, signatureAlgorithm, param); - byte[] sig2 = testSign(keyPair, signatureAlgorithm, param); - Assert.assertArrayEquals("PSS parameters not used, signatures are not identical!", sig1, sig2); - } - - signatureAlgorithm = "SHA384WITHRSA/PSS"; - if (signatureAlgorithmsWithPss.contains(signatureAlgorithm)) { - testSign(keyPair, signatureAlgorithm, new PSSParameterSpec("SHA-384", "MGF1", MGF1ParameterSpec.SHA384, 8, 1)); - PSSParameterSpec param = new PSSParameterSpec("SHA-384", "MGF1", MGF1ParameterSpec.SHA384, 0, 1); - byte[] sig1 = testSign(keyPair, signatureAlgorithm, param); - byte[] sig2 = testSign(keyPair, signatureAlgorithm, param); - Assert.assertArrayEquals("PSS parameters not used, signatures are not identical!", sig1, sig2); - } - - // RSA1024 is too small for SHA512WITHRSA/PSS - if (keyType != KeyType.RSA1024) { - signatureAlgorithm = "SHA512WITHRSA/PSS"; - if (signatureAlgorithmsWithPss.contains(signatureAlgorithm)) { - testSign(keyPair, signatureAlgorithm, new PSSParameterSpec("SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 8, 1)); - PSSParameterSpec param = new PSSParameterSpec("SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 0, 1); - byte[] sig1 = testSign(keyPair, signatureAlgorithm, param); - byte[] sig2 = testSign(keyPair, signatureAlgorithm, param); - Assert.assertArrayEquals("PSS parameters not used, signatures are not identical!", sig1, sig2); - } - } - - signatureAlgorithm = "RAWRSASSA-PSS"; - if (signatureAlgorithmsWithPss.contains(signatureAlgorithm)) { - testSign(keyPair, signatureAlgorithm, new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 8, 1)); - PSSParameterSpec param = new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 0, 1); - byte[] sig1 = testSign(keyPair, signatureAlgorithm, param); - byte[] sig2 = testSign(keyPair, signatureAlgorithm, param); - Assert.assertArrayEquals("PSS parameters not used, signatures are not identical!", sig1, sig2); - } - - signatureAlgorithm = "RSASSA-PSS"; - if (signatureAlgorithmsWithPss.contains(signatureAlgorithm)) { - testSign(keyPair, signatureAlgorithm, new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 8, 1)); - PSSParameterSpec param = new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 0, 1); - byte[] sig1 = testSign(keyPair, signatureAlgorithm, param); - byte[] sig2 = testSign(keyPair, signatureAlgorithm, param); - Assert.assertArrayEquals("PSS parameters not used, signatures are not identical!", sig1, sig2); - } - - break; + + signatureAlgorithm = "SHA256WITHRSA/PSS"; + if (signatureAlgorithmsWithPss.contains(signatureAlgorithm)) { + testSign( + keyPair, + signatureAlgorithm, + new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 8, 1)); + PSSParameterSpec param = + new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 0, 1); + byte[] sig1 = testSign(keyPair, signatureAlgorithm, param); + byte[] sig2 = testSign(keyPair, signatureAlgorithm, param); + Assert.assertArrayEquals( + "PSS parameters not used, signatures are not identical!", sig1, sig2); } - } - public static byte[] testSign(KeyPair keyPair, String signatureAlgorithm, AlgorithmParameterSpec param) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, SignatureException { - byte[] message = "Hello world!".getBytes(StandardCharsets.UTF_8); + signatureAlgorithm = "SHA384WITHRSA/PSS"; + if (signatureAlgorithmsWithPss.contains(signatureAlgorithm)) { + testSign( + keyPair, + signatureAlgorithm, + new PSSParameterSpec("SHA-384", "MGF1", MGF1ParameterSpec.SHA384, 8, 1)); + PSSParameterSpec param = + new PSSParameterSpec("SHA-384", "MGF1", MGF1ParameterSpec.SHA384, 0, 1); + byte[] sig1 = testSign(keyPair, signatureAlgorithm, param); + byte[] sig2 = testSign(keyPair, signatureAlgorithm, param); + Assert.assertArrayEquals( + "PSS parameters not used, signatures are not identical!", sig1, sig2); + } - logger.debug("Create signature using {}", signatureAlgorithm); - Signature signer = Signature.getInstance(signatureAlgorithm); - signer.initSign(keyPair.getPrivate()); - if (param != null) signer.setParameter(param); - signer.update(message); - byte[] signature = signer.sign(); + // RSA1024 is too small for SHA512WITHRSA/PSS + if (keyType != KeyType.RSA1024) { + signatureAlgorithm = "SHA512WITHRSA/PSS"; + if (signatureAlgorithmsWithPss.contains(signatureAlgorithm)) { + testSign( + keyPair, + signatureAlgorithm, + new PSSParameterSpec("SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 8, 1)); + PSSParameterSpec param = + new PSSParameterSpec("SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 0, 1); + byte[] sig1 = testSign(keyPair, signatureAlgorithm, param); + byte[] sig2 = testSign(keyPair, signatureAlgorithm, param); + Assert.assertArrayEquals( + "PSS parameters not used, signatures are not identical!", sig1, sig2); + } + } - try { - Signature verifier = Signature.getInstance(signatureAlgorithm); - verifier.initVerify(keyPair.getPublic()); - if (param != null) verifier.setParameter(param); - verifier.update(message); - Assert.assertTrue("Verify signature", verifier.verify(signature)); - logger.debug("Signature verified for: {}", signatureAlgorithm); - return signature; - } catch (InvalidKeyException | SignatureException e) { - throw new RuntimeException(e); + signatureAlgorithm = "RAWRSASSA-PSS"; + if (signatureAlgorithmsWithPss.contains(signatureAlgorithm)) { + testSign( + keyPair, + signatureAlgorithm, + new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 8, 1)); + PSSParameterSpec param = + new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 0, 1); + byte[] sig1 = testSign(keyPair, signatureAlgorithm, param); + byte[] sig2 = testSign(keyPair, signatureAlgorithm, param); + Assert.assertArrayEquals( + "PSS parameters not used, signatures are not identical!", sig1, sig2); } - } - public static Set getAllSignatureAlgorithmsWithPSS() { - signatureAlgorithmsWithPss.clear(); - Set pssSignatures = new HashSet<>(); - Provider provider = Security.getProvider("YKPiv"); - Set allServices = provider.getServices(); - for (Provider.Service service : allServices) { - if (service.getType().equals("Signature") && service.getAlgorithm().endsWith("PSS")) { - pssSignatures.add(service.getAlgorithm()); - } + signatureAlgorithm = "RSASSA-PSS"; + if (signatureAlgorithmsWithPss.contains(signatureAlgorithm)) { + testSign( + keyPair, + signatureAlgorithm, + new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 8, 1)); + PSSParameterSpec param = + new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 0, 1); + byte[] sig1 = testSign(keyPair, signatureAlgorithm, param); + byte[] sig2 = testSign(keyPair, signatureAlgorithm, param); + Assert.assertArrayEquals( + "PSS parameters not used, signatures are not identical!", sig1, sig2); } - return pssSignatures; + break; + } + } + + public static byte[] testSign( + KeyPair keyPair, String signatureAlgorithm, AlgorithmParameterSpec param) + throws NoSuchAlgorithmException, + InvalidAlgorithmParameterException, + InvalidKeyException, + SignatureException { + byte[] message = "Hello world!".getBytes(StandardCharsets.UTF_8); + + logger.debug("Create signature using {}", signatureAlgorithm); + Signature signer = Signature.getInstance(signatureAlgorithm); + signer.initSign(keyPair.getPrivate()); + if (param != null) signer.setParameter(param); + signer.update(message); + byte[] signature = signer.sign(); + + try { + Signature verifier = Signature.getInstance(signatureAlgorithm); + verifier.initVerify(keyPair.getPublic()); + if (param != null) verifier.setParameter(param); + verifier.update(message); + Assert.assertTrue("Verify signature", verifier.verify(signature)); + logger.debug("Signature verified for: {}", signatureAlgorithm); + return signature; + } catch (InvalidKeyException | SignatureException e) { + throw new RuntimeException(e); + } + } + + public static Set getAllSignatureAlgorithmsWithPSS() { + signatureAlgorithmsWithPss.clear(); + Set pssSignatures = new HashSet<>(); + Provider provider = Security.getProvider("YKPiv"); + Set allServices = provider.getServices(); + for (Provider.Service service : allServices) { + if (service.getType().equals("Signature") && service.getAlgorithm().endsWith("PSS")) { + pssSignatures.add(service.getAlgorithm()); + } } + + return pssSignatures; + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaUtils.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaUtils.java index 4cbe50be..2fd6aa69 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaUtils.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivJcaUtils.java @@ -18,41 +18,38 @@ import com.yubico.yubikit.piv.PivSession; import com.yubico.yubikit.piv.jca.PivProvider; - -import org.bouncycastle.jce.provider.BouncyCastleProvider; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.security.Provider; import java.security.Security; import java.util.Set; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class PivJcaUtils { - private static final Logger logger = LoggerFactory.getLogger(PivJcaUtils.class); - - public static void setupJca(PivSession piv) { - Security.removeProvider("BC"); - Security.addProvider(new BouncyCastleProvider()); - Security.insertProviderAt(new PivProvider(piv), 1); - listJcaProviders(); - } - - public static void tearDownJca() { - Security.removeProvider("YKPiv"); - } - - public static void listJcaProviders() { - Provider[] providers = Security.getProviders(); - - for (Provider p : providers) { - @SuppressWarnings("deprecation") - double version = p.getVersion(); - logger.debug("{}/{}/{}", p.getName(), p.getInfo(), version); - Set services = p.getServices(); - for (Provider.Service s : services) { - logger.debug("\t{}: {} -> {}", s.getType(), s.getAlgorithm(), s.getClassName()); - } - } + private static final Logger logger = LoggerFactory.getLogger(PivJcaUtils.class); + + public static void setupJca(PivSession piv) { + Security.removeProvider("BC"); + Security.addProvider(new BouncyCastleProvider()); + Security.insertProviderAt(new PivProvider(piv), 1); + listJcaProviders(); + } + + public static void tearDownJca() { + Security.removeProvider("YKPiv"); + } + + public static void listJcaProviders() { + Provider[] providers = Security.getProviders(); + + for (Provider p : providers) { + @SuppressWarnings("deprecation") + double version = p.getVersion(); + logger.debug("{}/{}/{}", p.getName(), p.getInfo(), version); + Set services = p.getServices(); + for (Provider.Service s : services) { + logger.debug("\t{}: {} -> {}", s.getType(), s.getAlgorithm(), s.getClassName()); + } } + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivMoveKeyTests.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivMoveKeyTests.java index 432cdca1..c3e954d8 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivMoveKeyTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivMoveKeyTests.java @@ -32,10 +32,6 @@ import com.yubico.yubikit.piv.PivSession; import com.yubico.yubikit.piv.Slot; import com.yubico.yubikit.piv.TouchPolicy; - -import org.junit.Assert; -import org.junit.Assume; - import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; @@ -49,72 +45,87 @@ import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.Arrays; +import org.junit.Assert; +import org.junit.Assume; public class PivMoveKeyTests { - static void moveKey(PivSession piv, PivTestState state) - throws IOException, ApduException, BadResponseException, NoSuchAlgorithmException { - Assume.assumeTrue("Key does not support move instruction", piv.supports(FEATURE_MOVE_KEY)); - setupJca(piv); - Slot srcSlot = Slot.RETIRED1; - Slot dstSlot = Slot.RETIRED2; - - piv.authenticate(state.managementKey); - - for (KeyType keyType : Arrays.asList(KeyType.ECCP256, KeyType.ECCP384, KeyType.RSA1024, KeyType.RSA2048, KeyType.ED25519, KeyType.X25519)) { - - if (state.isInvalidKeyType(keyType)) { - continue; - } - - if (!piv.supports(FEATURE_CV25519) && (keyType == KeyType.ED25519 || keyType == KeyType.X25519)) { - continue; - } - - KeyPair keyPair = PivTestUtils.loadKey(keyType); - PrivateKeyValues privateKeyValues = PrivateKeyValues.fromPrivateKey(keyPair.getPrivate()); - piv.putKey(srcSlot, privateKeyValues, PinPolicy.DEFAULT, TouchPolicy.DEFAULT); - - if (hasKey(piv, dstSlot)) { - piv.deleteKey(dstSlot); - } - - piv.moveKey(srcSlot, dstSlot); - Assert.assertFalse("Key in srcSlot still exists", hasKey(piv, srcSlot)); - - try { - KeyStore keyStore = KeyStore.getInstance("YKPiv"); - keyStore.load(null); - - PublicKey publicKey = keyPair.getPublic(); - PrivateKey privateKey = (PrivateKey) keyStore.getKey(dstSlot.getStringAlias(), state.pin); - KeyPair signingKeyPair = new KeyPair(publicKey, privateKey); - - if (keyType != KeyType.X25519) { - testSign(signingKeyPair, keyType.params.algorithm == EC - ? keyType == KeyType.ED25519 ? "ED25519" : "SHA256withECDSA" - : "SHA256withRSA", null); - } - - } catch (KeyStoreException | UnrecoverableKeyException | - InvalidAlgorithmParameterException | InvalidKeyException | - SignatureException | CertificateException e) { - throw new RuntimeException(e); - } - + static void moveKey(PivSession piv, PivTestState state) + throws IOException, ApduException, BadResponseException, NoSuchAlgorithmException { + Assume.assumeTrue("Key does not support move instruction", piv.supports(FEATURE_MOVE_KEY)); + setupJca(piv); + Slot srcSlot = Slot.RETIRED1; + Slot dstSlot = Slot.RETIRED2; + + piv.authenticate(state.managementKey); + + for (KeyType keyType : + Arrays.asList( + KeyType.ECCP256, + KeyType.ECCP384, + KeyType.RSA1024, + KeyType.RSA2048, + KeyType.ED25519, + KeyType.X25519)) { + + if (state.isInvalidKeyType(keyType)) { + continue; + } + + if (!piv.supports(FEATURE_CV25519) + && (keyType == KeyType.ED25519 || keyType == KeyType.X25519)) { + continue; + } + + KeyPair keyPair = PivTestUtils.loadKey(keyType); + PrivateKeyValues privateKeyValues = PrivateKeyValues.fromPrivateKey(keyPair.getPrivate()); + piv.putKey(srcSlot, privateKeyValues, PinPolicy.DEFAULT, TouchPolicy.DEFAULT); + + if (hasKey(piv, dstSlot)) { + piv.deleteKey(dstSlot); + } + + piv.moveKey(srcSlot, dstSlot); + Assert.assertFalse("Key in srcSlot still exists", hasKey(piv, srcSlot)); + + try { + KeyStore keyStore = KeyStore.getInstance("YKPiv"); + keyStore.load(null); + + PublicKey publicKey = keyPair.getPublic(); + PrivateKey privateKey = (PrivateKey) keyStore.getKey(dstSlot.getStringAlias(), state.pin); + KeyPair signingKeyPair = new KeyPair(publicKey, privateKey); + + if (keyType != KeyType.X25519) { + testSign( + signingKeyPair, + keyType.params.algorithm == EC + ? keyType == KeyType.ED25519 ? "ED25519" : "SHA256withECDSA" + : "SHA256withRSA", + null); } - tearDownJca(); - } - private static boolean hasKey(PivSession piv, Slot slot) throws IOException, ApduException { - try { - piv.getSlotMetadata(slot); - } catch (ApduException apduException) { - if (apduException.getSw() == REFERENCED_DATA_NOT_FOUND) { - return false; - } - throw apduException; - } - return true; + } catch (KeyStoreException + | UnrecoverableKeyException + | InvalidAlgorithmParameterException + | InvalidKeyException + | SignatureException + | CertificateException e) { + throw new RuntimeException(e); + } + } + tearDownJca(); + } + + private static boolean hasKey(PivSession piv, Slot slot) throws IOException, ApduException { + try { + piv.getSlotMetadata(slot); + } catch (ApduException apduException) { + if (apduException.getSw() == REFERENCED_DATA_NOT_FOUND) { + return false; + } + throw apduException; } + return true; + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivPinComplexityDeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivPinComplexityDeviceTests.java index 39ed4d29..58b0723d 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivPinComplexityDeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivPinComplexityDeviceTests.java @@ -21,61 +21,60 @@ import com.yubico.yubikit.core.smartcard.ApduException; import com.yubico.yubikit.management.DeviceInfo; import com.yubico.yubikit.piv.PivSession; - import org.hamcrest.CoreMatchers; import org.hamcrest.MatcherAssert; import org.junit.Assert; public class PivPinComplexityDeviceTests { - /** - * For this test, one needs a key with PIN complexity set on. The test will change PINs. - *

- * The test will verify that trying to set a weak PIN for PIV produces expected exceptions. - * - * @see DeviceInfo#getPinComplexity() - */ - static void testPinComplexity(PivSession piv, PivTestState state) throws Throwable { + /** + * For this test, one needs a key with PIN complexity set on. The test will change PINs. + * + *

The test will verify that trying to set a weak PIN for PIV produces expected exceptions. + * + * @see DeviceInfo#getPinComplexity() + */ + static void testPinComplexity(PivSession piv, PivTestState state) throws Throwable { - final DeviceInfo deviceInfo = state.getDeviceInfo(); - assumeTrue("Device does not support PIN complexity", deviceInfo != null); - assumeTrue("Device does not require PIN complexity", deviceInfo.getPinComplexity()); + final DeviceInfo deviceInfo = state.getDeviceInfo(); + assumeTrue("Device does not support PIN complexity", deviceInfo != null); + assumeTrue("Device does not require PIN complexity", deviceInfo.getPinComplexity()); - piv.reset(); + piv.reset(); - // the values in state are not valid after reset and we need to use - // the default values - piv.authenticate(PivTestState.DEFAULT_MANAGEMENT_KEY); - piv.verifyPin(PivTestState.DEFAULT_PIN); + // the values in state are not valid after reset and we need to use + // the default values + piv.authenticate(PivTestState.DEFAULT_MANAGEMENT_KEY); + piv.verifyPin(PivTestState.DEFAULT_PIN); - MatcherAssert.assertThat(piv.getPinAttempts(), CoreMatchers.equalTo(3)); + MatcherAssert.assertThat(piv.getPinAttempts(), CoreMatchers.equalTo(3)); - // try to change to pin which breaks PIN complexity - char[] weakPin = "33333333".toCharArray(); - try { - piv.changePin(PivTestState.DEFAULT_PIN, weakPin); - Assert.fail("Set weak PIN"); - } catch (ApduException apduException) { - if (apduException.getSw() != CONDITIONS_NOT_SATISFIED) { - Assert.fail("Unexpected exception:" + apduException.getMessage()); - } - } catch (Exception e) { - Assert.fail("Unexpected exception:" + e.getMessage()); - } + // try to change to pin which breaks PIN complexity + char[] weakPin = "33333333".toCharArray(); + try { + piv.changePin(PivTestState.DEFAULT_PIN, weakPin); + Assert.fail("Set weak PIN"); + } catch (ApduException apduException) { + if (apduException.getSw() != CONDITIONS_NOT_SATISFIED) { + Assert.fail("Unexpected exception:" + apduException.getMessage()); + } + } catch (Exception e) { + Assert.fail("Unexpected exception:" + e.getMessage()); + } - piv.verifyPin(PivTestState.DEFAULT_PIN); + piv.verifyPin(PivTestState.DEFAULT_PIN); - // change to complex pin - char[] complexPin = "CMPLXPIN".toCharArray(); - try { - piv.changePin(PivTestState.DEFAULT_PIN, complexPin); - } catch (Exception e) { - Assert.fail("Unexpected exception:" + e.getMessage()); - } + // change to complex pin + char[] complexPin = "CMPLXPIN".toCharArray(); + try { + piv.changePin(PivTestState.DEFAULT_PIN, complexPin); + } catch (Exception e) { + Assert.fail("Unexpected exception:" + e.getMessage()); + } - piv.verifyPin(complexPin); + piv.verifyPin(complexPin); - // the value of default PIN in the state is correct for pin complexity settings - piv.changePin(complexPin, state.pin); - } + // the value of default PIN in the state is correct for pin complexity settings + piv.changePin(complexPin, state.pin); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestState.java index 11a30d77..5d501c90 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestState.java @@ -33,135 +33,131 @@ import com.yubico.yubikit.piv.PivSession; import com.yubico.yubikit.testing.ScpParameters; import com.yubico.yubikit.testing.TestState; - import java.io.IOException; - import javax.annotation.Nullable; public class PivTestState extends TestState { - static final char[] DEFAULT_PIN = "123456".toCharArray(); - static final char[] DEFAULT_PUK = "12345678".toCharArray(); - static final byte[] DEFAULT_MANAGEMENT_KEY = new byte[]{ - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, - }; - - private static final char[] COMPLEX_PIN = "11234567".toCharArray(); - private static final char[] COMPLEX_PUK = "11234567".toCharArray(); - private static final byte[] COMPLEX_MANAGEMENT_KEY = new byte[]{ - 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - }; - - public final boolean isFipsApproved; - public char[] pin; - public char[] puk; - public byte[] managementKey; - - public static class Builder extends TestState.Builder { - - public Builder(YubiKeyDevice device, UsbPid usbPid) { - super(device, usbPid); - } - - @Override - public Builder getThis() { - return this; - } - - public PivTestState build() throws Throwable { - return new PivTestState(this); - } + static final char[] DEFAULT_PIN = "123456".toCharArray(); + static final char[] DEFAULT_PUK = "12345678".toCharArray(); + static final byte[] DEFAULT_MANAGEMENT_KEY = + new byte[] { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + }; + + private static final char[] COMPLEX_PIN = "11234567".toCharArray(); + private static final char[] COMPLEX_PUK = "11234567".toCharArray(); + private static final byte[] COMPLEX_MANAGEMENT_KEY = + new byte[] { + 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + }; + + public final boolean isFipsApproved; + public char[] pin; + public char[] puk; + public byte[] managementKey; + + public static class Builder extends TestState.Builder { + + public Builder(YubiKeyDevice device, UsbPid usbPid) { + super(device, usbPid); } - protected PivTestState(Builder builder) throws Throwable { - super(builder); + @Override + public Builder getThis() { + return this; + } - pin = DEFAULT_PIN; - puk = DEFAULT_PUK; - managementKey = DEFAULT_MANAGEMENT_KEY; + public PivTestState build() throws Throwable { + return new PivTestState(this); + } + } - assumeTrue("No SmartCard support", currentDevice.supportsConnection(SmartCardConnection.class)); + protected PivTestState(Builder builder) throws Throwable { + super(builder); - DeviceInfo deviceInfo = getDeviceInfo(); + pin = DEFAULT_PIN; + puk = DEFAULT_PUK; + managementKey = DEFAULT_MANAGEMENT_KEY; - // skip MPE devices - assumeFalse("Ignoring MPE device", isMpe(deviceInfo)); + assumeTrue("No SmartCard support", currentDevice.supportsConnection(SmartCardConnection.class)); - boolean isPivFipsCapable = isFipsCapable(deviceInfo, Capability.PIV); - boolean hasPinComplexity = deviceInfo != null && deviceInfo.getPinComplexity(); + DeviceInfo deviceInfo = getDeviceInfo(); - if (scpParameters.getKid() == null && isPivFipsCapable) { - assumeTrue("Trying to use PIV FIPS capable device over NFC without SCP", isUsbTransport()); - } + // skip MPE devices + assumeFalse("Ignoring MPE device", isMpe(deviceInfo)); - if (scpParameters.getKid() != null) { - // skip the test if the connected key does not provide matching SCP keys - assumeTrue( - "No matching key params found for required kid", - scpParameters.getKeyParams() != null - ); - } + boolean isPivFipsCapable = isFipsCapable(deviceInfo, Capability.PIV); + boolean hasPinComplexity = deviceInfo != null && deviceInfo.getPinComplexity(); - try (SmartCardConnection connection = openSmartCardConnection()) { - PivSession pivSession = getPivSession(connection, scpParameters); - assumeTrue("PIV not available", pivSession != null); + if (scpParameters.getKid() == null && isPivFipsCapable) { + assumeTrue("Trying to use PIV FIPS capable device over NFC without SCP", isUsbTransport()); + } - try { - pivSession.reset(); - } catch (Exception ignored) { - } + if (scpParameters.getKid() != null) { + // skip the test if the connected key does not provide matching SCP keys + assumeTrue( + "No matching key params found for required kid", scpParameters.getKeyParams() != null); + } - if (hasPinComplexity) { - // only use complex pins if pin complexity is required - pivSession.changePin(pin, COMPLEX_PIN); - pivSession.changePuk(puk, COMPLEX_PUK); - pivSession.authenticate(managementKey); + try (SmartCardConnection connection = openSmartCardConnection()) { + PivSession pivSession = getPivSession(connection, scpParameters); + assumeTrue("PIV not available", pivSession != null); - pivSession.setManagementKey(ManagementKeyType.AES192, COMPLEX_MANAGEMENT_KEY, false); + try { + pivSession.reset(); + } catch (Exception ignored) { + } - pin = COMPLEX_PIN; - puk = COMPLEX_PUK; - managementKey = COMPLEX_MANAGEMENT_KEY; - } - } + if (hasPinComplexity) { + // only use complex pins if pin complexity is required + pivSession.changePin(pin, COMPLEX_PIN); + pivSession.changePuk(puk, COMPLEX_PUK); + pivSession.authenticate(managementKey); - deviceInfo = getDeviceInfo(); - isFipsApproved = isFipsApproved(deviceInfo, Capability.PIV); + pivSession.setManagementKey(ManagementKeyType.AES192, COMPLEX_MANAGEMENT_KEY, false); - // after changing PIN, PUK and management key, we expect a FIPS capable device - // to be FIPS approved - if (isPivFipsCapable) { - assertNotNull(deviceInfo); - assertTrue("Device not PIV FIPS approved as expected", isFipsApproved); - } + pin = COMPLEX_PIN; + puk = COMPLEX_PUK; + managementKey = COMPLEX_MANAGEMENT_KEY; + } } - boolean isInvalidKeyType(KeyType keyType) { - return isFipsApproved && (keyType == KeyType.RSA1024 || keyType == KeyType.X25519); - } + deviceInfo = getDeviceInfo(); + isFipsApproved = isFipsApproved(deviceInfo, Capability.PIV); - public void withPiv(StatefulSessionCallback callback) - throws Throwable { - try (SmartCardConnection connection = openSmartCardConnection()) { - final PivSession piv = getPivSession(connection, scpParameters); - assumeTrue("No PIV support", piv != null); - callback.invoke(piv, this); - } - reconnect(); + // after changing PIN, PUK and management key, we expect a FIPS capable device + // to be FIPS approved + if (isPivFipsCapable) { + assertNotNull(deviceInfo); + assertTrue("Device not PIV FIPS approved as expected", isFipsApproved); } + } - @Nullable - public static PivSession getPivSession(SmartCardConnection connection, ScpParameters scpParameters) - throws IOException { - try { - return new PivSession(connection, scpParameters.getKeyParams()); - } catch (ApplicationNotAvailableException | ApduException ignored) { - // no PIV support - } - return null; + boolean isInvalidKeyType(KeyType keyType) { + return isFipsApproved && (keyType == KeyType.RSA1024 || keyType == KeyType.X25519); + } + + public void withPiv(StatefulSessionCallback callback) throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + final PivSession piv = getPivSession(connection, scpParameters); + assumeTrue("No PIV support", piv != null); + callback.invoke(piv, this); + } + reconnect(); + } + + @Nullable public static PivSession getPivSession( + SmartCardConnection connection, ScpParameters scpParameters) throws IOException { + try { + return new PivSession(connection, scpParameters.getKeyParams()); + } catch (ApplicationNotAvailableException | ApduException ignored) { + // no PIV support } + return null; + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestUtils.java b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestUtils.java index fb3aa176..e06d89c1 100755 --- a/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestUtils.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/piv/PivTestUtils.java @@ -17,17 +17,6 @@ import com.yubico.yubikit.core.internal.codec.Base64; import com.yubico.yubikit.piv.KeyType; - -import org.bouncycastle.asn1.ASN1Sequence; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; -import org.bouncycastle.cert.X509CertificateHolder; -import org.bouncycastle.cert.X509v3CertificateBuilder; -import org.bouncycastle.operator.ContentSigner; -import org.bouncycastle.operator.OperatorCreationException; -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; -import org.junit.Assert; - import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -52,439 +41,472 @@ import java.security.spec.X509EncodedKeySpec; import java.util.Date; import java.util.Objects; - import javax.crypto.Cipher; import javax.crypto.KeyAgreement; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.Assert; @SuppressWarnings("SpellCheckingInspection") public class PivTestUtils { - private static final SecureRandom secureRandom = new SecureRandom(); - - private enum StaticKey { - RSA1024( - KeyType.RSA1024, "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBALWeZ0E5O2l_iH" + - "fck9mokf1iWH2eZDWQoJoQKUOAeVoKUecNp250J5tL3EHONqWoF6VLO-B-6jTET4Iz97BeUj7gOJHmE" + - "w-nqFfguTVmNeeiZ711TNYNpF7kwW7yWghWG-Q7iQEoMXfY3x4BL33H2gKRWtMHK66GJViL1l9s3qDX" + - "AgMBAAECgYBO753pFzrfS3LAxbns6_snqcrULjdXoJhs3YFRuVEE9V9LkP-oXguoz3vXjgzqSvib-ur" + - "3U7HvZTM5X-TTXutXdQ5CyORLLtXEZcyCKQI9ihH5fSNJRWRbJ3xe-xi5NANRkRDkro7tm4a5ZD4PYv" + - "O4r29yVB5PXlMkOTLoxNSwwQJBAN5lW93Agi9Ge5B2-B2EnKSlUvj0-jJBkHYAFTiHyTZVEj6baeHBv" + - "JklhVczpWvTXb6Nr8cjAKVshFbdQoBwHmkCQQDRD7djZGIWH1Lz0rkL01nDj4z4QYMgUs3AQhnrXPBj" + - "EgNzphtJ2u7QrCSOBQQHlmAPBDJ_MTxFJMzDIJGDA10_AkATJjEZz_ilr3D2SHgmuoNuXdneG-HrL-A" + - "LeQhavL5jkkGm6GTejnr5yNRJZOYKecGppbOL9wSYOdbPT-_o9T55AkATXCY6cRBYRhxTcf8q5i6Y2p" + - "FOaBqxgpmFJVnrHtcwBXoGWqqKQ1j8QAS-lh5SaY2JtnTKrI-NQ6Qmqbxv6n7XAkBkhLO7pplInVh2W" + - "jqXOV4ZAoOAAJlfpG5-z6mWzCZ9-286OJQLr6OVVQMcYExUO9yVocZQX-4XqEIF0qAB7m31", "MIGf" + - "MA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC1nmdBOTtpf4h33JPZqJH9Ylh9nmQ1kKCaEClDgHlaClH" + - "nDadudCebS9xBzjalqBelSzvgfuo0xE-CM_ewXlI-4DiR5hMPp6hX4Lk1ZjXnome9dUzWDaRe5MFu8l" + - "oIVhvkO4kBKDF32N8eAS99x9oCkVrTByuuhiVYi9ZfbN6g1wIDAQAB" - ), - RSA2048( - KeyType.RSA2048, "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC0G266KNssen" + - "UQwsqN3-f3ysmiHgp4345wsaiDcxXryXX3pXr3vYdiJFQ6HiiMbfdpm4FeulLYCOdBghKHIh_MnxTuw" + - "q6mPrxzLFxqGfHinvORc4Y-mZSiicN_Ajo-uQdgH5LrhlHJ0g7ae26RWW3Z4pOel_SeXWJgKm4prhKz" + - "i6Or3NZ1l4Wpg4C_lrLD9_bhL6XdUmr_kXc2UoldUz1ZyTNmDqr0oyix52jX-Tpxp7WsPUmXUoapxVp" + - "ugOQKlkCGFltb5jnaK8VYrlBfN0a7N0o-HCSIThjBLbr65qKXOmUYgS-q5OmidyeCz_1AJ5OLwSf63M" + - "71NXMtZoJjLdMBAgMBAAECggEAT6Z-HnfpDc-OK_5pQ7sMxCn7Z-WvLet3--ClrJRd0mvC7uVQ73TzB" + - "XUZhqZFumz7aMnrua_e6UlutCrI9NgjhgOoZzrTsBO4lZq9t_KHZXh0MRQM_2w-Lm-MdIPQrGJ5n4n3" + - "GI_LZdyu0vKZYFBTY3NvY0jCVrLnya2aEHa6MIpHsDyJa0EpjZRMHscPAP4C9h0EE_kXdFuu8Q4I-RU" + - "hnWAEAox9wGq05cbWAnzz6f5WWWHUL2CfPvSLHx7jjCXOmXf035pj91IfHghVoQyU0UW29xKSqfJv7n" + - "JwqV67C0cbkd2MeNARiFi7z4kp6ziLU6gPeLQq3iyWy35hTYPl3QKBgQDdlznGc4YkeomH3W22nHol3" + - "BUL96gOrBSZnziNM19hvKQLkRhyIlikQaS7RWlzKbKtDTFhPDixWhKEHDWZ1DRs9th8LLZHXMP-oUyJ" + - "PkFCX28syP7D4cpXNMbRk5yJXcuF72sYMs4dldjUQVa29DaEDkaVFOEAdIVOPNmvmE7MDwKBgQDQEyI" + - "mwRkHzpp-IAFqhy06DJpmlnOlkD0AhrDAT-EpXTwJssZK8DHcwMhEQbBt-3jXjIXLdko0bR9UUKIpvi" + - "yF3TZg7IGlMCT4XSs_UlWUct2n9QRrIV5ivRN5-tZZr4-mxbm5d7aa73oQuZl70d5mn6P4y5OsEc5sX" + - "FNwUSCf7wKBgDo5NhES4bhMCj8My3sj-mRgQ5d1Z08ToAYNdAqF6RYBPwlbApVauPfP17ztLBv6ZNxb" + - "jxIBhNP02tCjqOHWhD_tTEy0YuC1WzpYn4egN_18nfWiim5lsYjgcS04H_VoE8YJdpZRIx9a9DIxSNu" + - "hp4FjTuB1L_mypCQ-kOQ2nN25AoGBAJlw0qlzkorQT9ucrI6rWq3JJ39piaTZRjMCIIvhHDENwT2BqX" + - "sPwCWDwOuc6Ydhf86soOnWtIgOxKC_yaYwyNJ6vCQjpMN1Sn4g7siGZffP8Sdvpy99bwYvWpKEaNfAg" + - "JXCj-B2qKF-4iw9QjMuI-zX4uqQ7bhhdTExsJJOMVnfAoGABSbxwvLPglJ6cpoqyGL5Ihg1LS4qog29" + - "HVmnX4o_HLXtTCO169yQP5lBWIGRO_yUcgouglJpeikcJSPJROWPLs4b2aPv5hhSx47MGZbVAIhSbls" + - "5zOZXDZm4wdfQE5J-4kAVlYF73ZCrH24ZbqqyMF_0wDt_NExsv6FMUwSKfyY=", "MIIBIjANBgkqhk" + - "iG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtBtuuijbLHp1EMLKjd_n98rJoh4KeN-OcLGog3MV68l196V69" + - "72HYiRUOh4ojG33aZuBXrpS2AjnQYIShyIfzJ8U7sKupj68cyxcahnx4p7zkXOGPpmUoonDfwI6PrkH" + - "YB-S64ZRydIO2ntukVlt2eKTnpf0nl1iYCpuKa4Ss4ujq9zWdZeFqYOAv5ayw_f24S-l3VJq_5F3NlK" + - "JXVM9WckzZg6q9KMosedo1_k6cae1rD1Jl1KGqcVaboDkCpZAhhZbW-Y52ivFWK5QXzdGuzdKPhwkiE" + - "4YwS26-uailzplGIEvquTponcngs_9QCeTi8En-tzO9TVzLWaCYy3TAQIDAQAB" - ), - RSA3072( - KeyType.RSA3072, "MIIG_QIBADANBgkqhkiG9w0BAQEFAASCBucwggbjAgEAAoIBgQCe4Vci7OxOOx" + - "ek5ZJ_A7uklWpkiRvwAqguotUwDwXybJqvz2sf0TMgYbSweBLrnt253-w_cJUkXeRS6JgYJNPM5R7zt" + - "aWL8Jsl3ETZVwpWJYZ9cRXmI71kB3ys0ROQwP8Srqw0vT3R6d8DOJ6SbQveScG4Bu5QG_OpFvfwS9qx" + - "v7OJGVbTV1w5lPBzQGZ4AFz0annGgz2FVZ6q61LgabJfjMygxWPNQjbY8HdrsGz3T8RMjb3rRhNKxlZ" + - "mVZob3qGpHxcDqvWMSz9aPgSzFTBWyIKv16_rzMJWcISfm-Ye_jpRAFV1quKHau7XPJJoMDdOF0FPyJ" + - "SLckt97aRjGBG88OwdW-Aj8xMA25_Zrx4Ja_SmHTBX4RGQJgM4qwhZV-XeHfNX5uML0tZgMIi6PzzbX" + - "aF6VIpDF7DsQEkMNheSXcWoD7Mf_gHS_coj0DP8VEuaEd_u-L4EvJHrbfVZ1cwhlIlhE7MrfkqKWZAr" + - "ypLarrZ7V7shU5pZZe0GEQkc6i0CAwEAAQKCAYALPBd_jH3cPVD7c9FjYmXxKjCjCc_8LY_rdk-5bYK" + - "H5TaeAn6Kei5Rlp8ikGcUGsCGjYdE6CbfsrT7yN9Ca84_UZ9Z7-kUJ3AtIfGLZdyBAXfMZIP-KV5R1b" + - "ay-LjXtlIDJe9e7lfSAWXn8ifCZmwdrJ0CcJkG-KcG-K0RJKgDBDGDmxNY7_dBSh8jozTrvOjVzrasO" + - "ykNRePppajPXiIDIC9zGeooAEvlFMH5DzlxVoZkvGwm2CZylepTo695U4mB6FbzGxHDCXjYVAsQDULx" + - "-j670TjnWkmuqXNGEN_SGgAdsEA51gJv8ar1at3nJf2LleSYNAL6sHOski9uEYXJ3CacWvnZRzT_YNT" + - "7WKujhmIS3EwOZgbrYI3QreOls9Vy-Xo53oCwiAii5zvxDMkZmG6oOgblRsV4slXtNEaL1mN2vM2_a4" + - "_Kg_yRmzFMY9znYp46qDj5QbmQ5MhEKgC-_DOrX0ih09eNlrgK6nb9VxlI046zthxY3beq47Z59mkCg" + - "cEAy0KHoX4vA-llBohsGiB_mCa6vvpdYk-uKqUcnEBG9u7UFEXKFn3n7ern2v6D1Om69u4S4_PIw2uS" + - "MBZ89jhNpo2WbX5GI_zQ-DCct9HJEN7vHM9W9AVuWLdn6_HM8PzTvrke1ZKGXorug-IWww6uaaBj-9r" + - "HJTTq_Bg0oUk1jv2jianp2Ct5ScFm4--kIjmb1AuLebywCvMSxZ5VtYHfb4VRgCM-nAIOdv9s27FNky" + - "VKHCgjK0dkLCMB0DgoTjGlAoHBAMga6ETh49iQtCAJpq28cQnqLSOCUI7dLqJcnEitUL5PtyCFkrpS7" + - "ZfLr15cHi4Ud9ycxtbaxAlaNtLo31l8tpLgbeVck0Ke67OUMRGrq9ONVXTYMNNX3tq2M7XO2slKa82O" + - "_tUDMjVlVv6v7R-LXTeqXItgxdISg-ZAYQHG550n6Ldterc2WxGOFZNjD1fkgbdwnu9f9DT-jS-m_C_" + - "oGXD5iqAaV2ovcVFb19iubRrltcqDi1nyhXnyKyjYNXff6QKBwQCBdsPLAgNSO4PEkHpCffanY-vInt" + - "GCP_xQX1CE2ZAZ0m805mrcvp9OdDPv0fMIV0Nl6qgPl4SFrGu1w20eqygScNaisS5d17cGjngTwUSPQ" + - "WAN-qaI0TjCuzcvGpmN2YvJTEIuiKCbcWSQjh4vaZd_4dAtZ-E2eqk9nvFO1cGObVGP6rDupmofp1cw" + - "0b-6qPTvL9dL1_pNTxvi0YIIFUvKzaDmvAwx9EFgXDrrB9jAY5z7qDkWZOeSEU4jYNGTVJ0CgcBlfTg" + - "mj4b29NVWlm6CGVwfkjTYmKRxAP9A-8WMGtMj4txXU0fK1nqIjZbhPclUx67PJni2yfe5YpcBu3hkM5" + - "uJvOgf9yb9GAslZljIxI_-WOVpwKhq2FtABD8Py90tUGCCvi7DLL7PVBmeTO3wHMfnjrEnQ6qxVBCvv" + - "CE3PIGGNJKUTaN6vsfLjIum2AwVIOElf6oscDc0lZJYA9JOHeKhaP8FGrcRNQS9Jd7AmB7gEHd2Qedw" + - "dE98PPXk3lun89kCgcATb1Hcac_ASVlLfyNvhEWYS7yHfALvGlCSTv-zdsIhnZ0oeN8ILSmzM0QaRB6" + - "matSnt-ocYJmJYuVwuzHFwZ-eitjnEmVMuMAMykUV40flaATPqERZvJmVmZbbCBv5qdb48zFkXQtR4q" + - "USvg6GjGUL7CAlBEjqW479l3KtiUuUF7WlIfU-iHhbA7HfkSbN8gtTorX5_t3OwZeFf-OGki5Iaf92u" + - "oq716yEBvD45dbPnkJG5sQTWDIHIKmRcwQ9ibw=", "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB" + - "igKCAYEAnuFXIuzsTjsXpOWSfwO7pJVqZIkb8AKoLqLVMA8F8myar89rH9EzIGG0sHgS657dud_sP3C" + - "VJF3kUuiYGCTTzOUe87Wli_CbJdxE2VcKViWGfXEV5iO9ZAd8rNETkMD_Eq6sNL090enfAziekm0L3k" + - "nBuAbuUBvzqRb38Evasb-ziRlW01dcOZTwc0BmeABc9Gp5xoM9hVWequtS4GmyX4zMoMVjzUI22PB3a" + - "7Bs90_ETI2960YTSsZWZlWaG96hqR8XA6r1jEs_Wj4EsxUwVsiCr9ev68zCVnCEn5vmHv46UQBVdari" + - "h2ru1zySaDA3ThdBT8iUi3JLfe2kYxgRvPDsHVvgI_MTANuf2a8eCWv0ph0wV-ERkCYDOKsIWVfl3h3" + - "zV-bjC9LWYDCIuj88212helSKQxew7EBJDDYXkl3FqA-zH_4B0v3KI9Az_FRLmhHf7vi-BLyR6231Wd" + - "XMIZSJYROzK35KilmQK8qS2q62e1e7IVOaWWXtBhEJHOotAgMBAAE=" - ), - RSA4096( - KeyType.RSA4096, "MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDLqdGZtBymZ0yz" + - "MPApteDlEc0hejP3Vee7i9L8V23HytZyQak6RrlsDPSLgpkn3p9IUZ6fpMN6ND5T" + - "PCvYiGu69Um1MUHlE9n3iZEPLHzu1EhgUHpZMlqAwJYPq1B_CA1yuiyjdafvb8t9" + - "CkKeVIg-ud9WZyQDc-NK1tneOpx87fKZaoqhrUoG4RySv3NwPJYiNgjIIzfZiNZ5" + - "hyGHzTV3zi8bXt7Qt-pzIDn0QBciWHHpxGBGpxdQ74xu0K2mctwsnmJSi69FqNWh" + - "Kekby0M-ZmBRw5d92ZfOWbYOslSUmI2dyDvNvojyNDPUFeWq1T8OaMYoAKe-CvXu" + - "pZpLSGTzgDUZ23kG5aiKFYCiePGBeeYdhCuZq_ybwM0CI-kCfmfZW1L2o-nZshTu" + - "SJxpnTcOVwzX_fkFVlN5dLwXCwLxhJNTJ7Vi5NnzS4Yp8NfY9iPkD97m76zLOMfI" + - "nt50qhxoyEjRK_r-Afjcv-hzrqyp9Q_dontQMVM-y0ck1MKhwl7N4-3k29hy_T9O" + - "fTF7qpV02RF36kVEeHZBLZN6ZlEkA89rib_IUj7wM6qWH3_m3NAELPCUhQ7jCBmn" + - "CzTssElwwYqy7jbpmImdWq6JCRyIv_LUubGYmrUrYKpEfNKTMHjGMhNdOJlkjwDZ" + - "ZqlPl6KvNcKe8xibAkHn1KCQxBH0SwIDAQABAoICAEaKicY2m8vJMMhKFY0k6YH-" + - "EfJb_N7Y2txbWFc9wxD2ASPu-TntoDC8JgGiWQD1u27Vkl8SNwC_Uq0KxNcJnWLS" + - "rdZ7-qppH1B9TgiW7KjeTzlI9q-pYK6CxhckS8vMErhforF1QZcNvkoPgTaM6ens" + - "AF7Rd6hYfewAkdLGs0gUNLiNrfnE50SNuRNdC5Ne8NNlqtIDXMdUfZD3TJZYbgIS" + - "oL9Ws09QvHxmt5wRjNHBF6eT9JLUMh--8QG69sKVuwwbScv0hN9YVLIvLYYKd76H" + - "wCnh2Llm4g0_hm79to0Hb9msLoabTZyylxcJBJUQqngHs0bMv0z0R-2CX3he2VPd" + - "rfrXEpX3UCpNu-BCmXxNNlfB65o8NtG_ro77RZrMAhMnr1H2Xrj-FdTE-Q5gs5oO" + - "qp21wThoZs9SjDF1S6SiOyES47EfRE1a8gVTCAC7HAgtLNauj4ik_qDTuah1ucUr" + - "gqu3f2ZaqWKxSVlYgewPPu9Xuwxgh2h0zv0a1dhT7OA7OC_Wn63KNgN7W2tr2CLN" + - "G5jWY7L723nVnJjorWmc2FynpOhi6k2IQoJ-DPVpKpttAI6f-rCvAIuPe_EIyKG1" + - "Pu_Em0To796_QLY-VstJoYscZwBd2ZH6ZTr1bwNSWHkp5nR9k05u108-WD03wtip" + - "6cl8-iGcaIpqI67Oo-bdAoIBAQDnX-RxftNxrMjiOF54Qtj-9Qt25lR72C74s5aJ" + - "Csci0jCBnhGyvrIQPYTufvgo5m2mqyEKjVJcOfprN9VpL3gZjfaDAq6Vl5WE6-Q-" + - "cxF2B9rpX9Tuzo4Fmj83ReElidel9w7am4U3TbToyZ1h0pSOJftr3BhnmMcOlOIi" + - "aplDawJ6THb03lkvV5JAwiAZR64TPGNv3njhkr295Hn1kIhMetZSzPGnEfbAbRmf" + - "hwFLfobcE5xdIlK2EhpJdMS5ovKINVA6Fw2xEEaC6CYn4xPkVJTPafGVK8TZZie-" + - "QFkAVsIk311oiftMiMK1K57d5RcE1hdXSdYUFpeVYmI30OT3AoIBAQDhVuWzt8wG" + - "kg7ZVwc_4VMd0cT1qA_XMCvQ4wl6mIMITLUYXFEjkpFnLSntkAy34tGTPOqpM9De" + - "Cbumtjj3HPd893ERNEkxlpfR2oY9L4XM6fQajCGxIbv5H1cLnsblJa_P5cKtRjYW" + - "qrY4adU2qOrX6VGaJkvckJC3BjO4xGNlvRE_y-xfmShwQEfW7L8ehgdEhb8_F14G" + - "bpyFz7ZzD4nGonecQwdkAsBBEgwPSnVF-4d7R3J1GwyGrtDHSEdaRsL_S-O3Hkep" + - "PADApntAIGVUR_MI_mCtmsUtc_5mUqg-bJQqh5dnFsIwXZLZQyQaVvewPUedkkHg" + - "Xn54_s0h5BpNAoIBAQCByT6Bk5zUFRISI4CKgSTrz1UA-y7E0X13sHVupgcSN0lS" + - "S_Kti16i0X9xsPNPLgKwDSpZmvBqH3OjFQy3FhOOch2nW6fG7eLHTvMXPMC8rqdT" + - "ZZgx5NexuNZhEOe8gNfglvdUFQzi-snSEtYfe1otaozf8fQWmJKAUW-P0q_qK2qW" + - "Y7IOpXLtpXe6r6oFxDmXPLail-7Cyed5T2JCJzLtg7IZfDDJgMAjLI_E9pv5Vx4a" + - "8T0y2QAAdaMdNUzsvMTDNvSrwSbC_dgvsj1E_pG38OIQfuMuxACF2lHM3JeQIxqA" + - "SHNDIrM-OTDPI4rX-Zux8M3i_t4BIrMg7rEdkiX9AoIBACapLgfDhPGrpXiMgeXn" + - "1sbK8qvjBbS5wwq3qSyrde-6mWdwj0s3HlNBYGwtxsDV3XcRgIE_LpqpuNRFd0iO" + - "Y7fBDFkTS2uCltGeWGGvAZnCmerkF_O4AfQf-GM5_o3aBWv504i-_xCsgU70eWxD" + - "VudsVF_KKkHRW8LLAZy1tQgDhC4Z4pgUQuffX3P0cmXeQOj0uXctnygjWh9rH7Zl" + - "-BFoVnUs2tvBzRJc8ky9TZmQKhJwk6ab2W5SF-fY8sT-Vv5OGueT_l9-t_JVndfG" + - "txvarEviuNuQLjw6Jm-PxuXO4yzYzpUVRoPdyhAUgOE0ApLuMJdMPJkuHSzNKoyi" + - "AhECggEBAJgn1SBkJqfa6cgx7W2eEn8kp4wBpIuPjkFzJcJJ7LBbfmzCMKX8SBZR" + - "cF2kOIOuM8TCFxkW5q8wRUTeqP2awgAMnYWeuxhJM0rzbVg3l6I1Lr1HIOxEExxE" + - "Rr11Qtz3tXM6I2UVJyEF4hu3P1CsZeXJp8458jySwhC4J0Tir5sQ8fj2M0wOiwHY" + - "XDY1A01EpicaeFv7882RHdlPUXXyHORvZ6vA8IVga_PyUovzmvmueOFp2HFmoqpP" + - "Lbtiz8UgfYVsAyGAPiJpTOXT2YipLMeUb7MXdAon100pIxjrfatE7iJRs6jW0o4t" + - "M7ctAhZ7IXQWuolas4ly8L0MABx9258=", "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAy6nRmbQcpmdMszDwKbXg" + - "5RHNIXoz91Xnu4vS_Fdtx8rWckGpOka5bAz0i4KZJ96fSFGen6TDejQ-Uzwr2Ihr" + - "uvVJtTFB5RPZ94mRDyx87tRIYFB6WTJagMCWD6tQfwgNcroso3Wn72_LfQpCnlSI" + - "PrnfVmckA3PjStbZ3jqcfO3ymWqKoa1KBuEckr9zcDyWIjYIyCM32YjWeYchh801" + - "d84vG17e0LfqcyA59EAXIlhx6cRgRqcXUO-MbtCtpnLcLJ5iUouvRajVoSnpG8tD" + - "PmZgUcOXfdmXzlm2DrJUlJiNncg7zb6I8jQz1BXlqtU_DmjGKACnvgr17qWaS0hk" + - "84A1Gdt5BuWoihWAonjxgXnmHYQrmav8m8DNAiPpAn5n2VtS9qPp2bIU7kicaZ03" + - "DlcM1_35BVZTeXS8FwsC8YSTUye1YuTZ80uGKfDX2PYj5A_e5u-syzjHyJ7edKoc" + - "aMhI0Sv6_gH43L_oc66sqfUP3aJ7UDFTPstHJNTCocJezePt5NvYcv0_Tn0xe6qV" + - "dNkRd-pFRHh2QS2TemZRJAPPa4m_yFI-8DOqlh9_5tzQBCzwlIUO4wgZpws07LBJ" + - "cMGKsu426ZiJnVquiQkciL_y1LmxmJq1K2CqRHzSkzB4xjITXTiZZI8A2WapT5ei" + - "rzXCnvMYmwJB59SgkMQR9EsCAwEAAQ==" - ), - ECCP256( - KeyType.ECCP256, "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgaEygF-BBlaq6Mk" + - "mJuN4CTGYo2QPJZYadPjRhKPodCdyhRANCAAQA9NDknDc4Mor6mWKaW0zo3BLSwF8d1yNf4HCLn_zbw" + - "vEkjuXo7-tob8faiZrixXoK7zuxip8yh86r-f0x1bFG", "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD" + - "QgAEAPTQ5Jw3ODKK-plimltM6NwS0sBfHdcjX-Bwi5_828LxJI7l6O_raG_H2oma4sV6Cu87sYqfMof" + - "Oq_n9MdWxRg" - ), - ECCP384( - KeyType.ECCP384, "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCqCSz-IHpchR9ffO" + - "4TJKkxNiBg5Wlg2AK7u4ge_egQZC_qQdTxFZZp8wTHDMNzeaOhZANiAAQ9p9ePq4YY_MfPRQUfx_OPx" + - "i1Ch6e4uIhgVYRUJYgW_kfZhyGRqlEnxXxbdBiCigPDHTWg0botpzmhGWfAmQ63v_2gluvB1sepqojT" + - "TzKlvkGLYui_UZR0GVzyM1KSMww", "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEPafXj6uGGPzHz0UF" + - "H8fzj8YtQoenuLiIYFWEVCWIFv5H2YchkapRJ8V8W3QYgooDwx01oNG6Lac5oRlnwJkOt7_9oJbrwdb" + - "HqaqI008ypb5Bi2Lov1GUdBlc8jNSkjMM" - ), - ED25519( - KeyType.ED25519, "MC4CAQAwBQYDK2VwBCIEIO_yEBZ291rK6lY8BH3RVtO61LnzLv78VxVxBZDj3uvi", - "MCowBQYDK2VwAyEA7m2UD-6mR8vVSpGFFYCnsDgXTuFRT5_M7yVOMM_7uHw=" - ), - X25519( - KeyType.X25519, "MC4CAQAwBQYDK2VuBCIEIJjvGxF_sesDPC6uoIanoMQU-O4HGMpCqyBssnhc8yBS", - "MCowBQYDK2VuAyEAq6Ws-klOFZ_Kbnf4TPqR45T9szGWeKz-5udDURxOeS4" - ); - - private final KeyType keyType; - private final String privateKey; - private final String publicKey; - - StaticKey(KeyType keyType, String privateKey, String publicKey) { - this.keyType = keyType; - this.privateKey = privateKey; - this.publicKey = publicKey; - } - - private KeyPair getKeyPair() { - try { - KeyFactory kf = KeyFactory.getInstance( - (keyType == KeyType.ED25519 || keyType == KeyType.X25519) - ? keyType.name() - : keyType.params.algorithm.name()); - return new KeyPair( - kf.generatePublic(new X509EncodedKeySpec(Base64.fromUrlSafeString(publicKey))), - kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.fromUrlSafeString(privateKey))) - ); - } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { - throw new IllegalStateException(e); - } - } + private static final SecureRandom secureRandom = new SecureRandom(); + + private enum StaticKey { + RSA1024( + KeyType.RSA1024, + "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBALWeZ0E5O2l_iHfck9mokf1iWH2eZDWQoJoQKUOAe" + + "VoKUecNp250J5tL3EHONqWoF6VLO-B-6jTET4Iz97BeUj7gOJHmEw-nqFfguTVmNeeiZ711TNYNpF7kwW7y" + + "WghWG-Q7iQEoMXfY3x4BL33H2gKRWtMHK66GJViL1l9s3qDXAgMBAAECgYBO753pFzrfS3LAxbns6_snqcr" + + "ULjdXoJhs3YFRuVEE9V9LkP-oXguoz3vXjgzqSvib-ur3U7HvZTM5X-TTXutXdQ5CyORLLtXEZcyCKQI9ih" + + "H5fSNJRWRbJ3xe-xi5NANRkRDkro7tm4a5ZD4PYvO4r29yVB5PXlMkOTLoxNSwwQJBAN5lW93Agi9Ge5B2-" + + "B2EnKSlUvj0-jJBkHYAFTiHyTZVEj6baeHBvJklhVczpWvTXb6Nr8cjAKVshFbdQoBwHmkCQQDRD7djZGIW" + + "H1Lz0rkL01nDj4z4QYMgUs3AQhnrXPBjEgNzphtJ2u7QrCSOBQQHlmAPBDJ_MTxFJMzDIJGDA10_AkATJjE" + + "Zz_ilr3D2SHgmuoNuXdneG-HrL-ALeQhavL5jkkGm6GTejnr5yNRJZOYKecGppbOL9wSYOdbPT-_o9T55Ak" + + "ATXCY6cRBYRhxTcf8q5i6Y2pFOaBqxgpmFJVnrHtcwBXoGWqqKQ1j8QAS-lh5SaY2JtnTKrI-NQ6Qmqbxv6" + + "n7XAkBkhLO7pplInVh2WjqXOV4ZAoOAAJlfpG5-z6mWzCZ9-286OJQLr6OVVQMcYExUO9yVocZQX-4XqEIF" + + "0qAB7m31", + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC1nmdBOTtpf4h33JPZqJH9Ylh9nmQ1kKCaEClDgHlaClHnDadud" + + "CebS9xBzjalqBelSzvgfuo0xE-CM_ewXlI-4DiR5hMPp6hX4Lk1ZjXnome9dUzWDaRe5MFu8loIVhvkO4kB" + + "KDF32N8eAS99x9oCkVrTByuuhiVYi9ZfbN6g1wIDAQAB"), + RSA2048( + KeyType.RSA2048, + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC0G266KNssenUQwsqN3-f3ysmiHgp4345wsaiDc" + + "xXryXX3pXr3vYdiJFQ6HiiMbfdpm4FeulLYCOdBghKHIh_MnxTuwq6mPrxzLFxqGfHinvORc4Y-mZSiicN_" + + "Ajo-uQdgH5LrhlHJ0g7ae26RWW3Z4pOel_SeXWJgKm4prhKzi6Or3NZ1l4Wpg4C_lrLD9_bhL6XdUmr_kXc" + + "2UoldUz1ZyTNmDqr0oyix52jX-Tpxp7WsPUmXUoapxVpugOQKlkCGFltb5jnaK8VYrlBfN0a7N0o-HCSITh" + + "jBLbr65qKXOmUYgS-q5OmidyeCz_1AJ5OLwSf63M71NXMtZoJjLdMBAgMBAAECggEAT6Z-HnfpDc-OK_5pQ" + + "7sMxCn7Z-WvLet3--ClrJRd0mvC7uVQ73TzBXUZhqZFumz7aMnrua_e6UlutCrI9NgjhgOoZzrTsBO4lZq9" + + "t_KHZXh0MRQM_2w-Lm-MdIPQrGJ5n4n3GI_LZdyu0vKZYFBTY3NvY0jCVrLnya2aEHa6MIpHsDyJa0EpjZR" + + "MHscPAP4C9h0EE_kXdFuu8Q4I-RUhnWAEAox9wGq05cbWAnzz6f5WWWHUL2CfPvSLHx7jjCXOmXf035pj91" + + "IfHghVoQyU0UW29xKSqfJv7nJwqV67C0cbkd2MeNARiFi7z4kp6ziLU6gPeLQq3iyWy35hTYPl3QKBgQDdl" + + "znGc4YkeomH3W22nHol3BUL96gOrBSZnziNM19hvKQLkRhyIlikQaS7RWlzKbKtDTFhPDixWhKEHDWZ1DRs" + + "9th8LLZHXMP-oUyJPkFCX28syP7D4cpXNMbRk5yJXcuF72sYMs4dldjUQVa29DaEDkaVFOEAdIVOPNmvmE7" + + "MDwKBgQDQEyImwRkHzpp-IAFqhy06DJpmlnOlkD0AhrDAT-EpXTwJssZK8DHcwMhEQbBt-3jXjIXLdko0bR" + + "9UUKIpviyF3TZg7IGlMCT4XSs_UlWUct2n9QRrIV5ivRN5-tZZr4-mxbm5d7aa73oQuZl70d5mn6P4y5OsE" + + "c5sXFNwUSCf7wKBgDo5NhES4bhMCj8My3sj-mRgQ5d1Z08ToAYNdAqF6RYBPwlbApVauPfP17ztLBv6ZNxb" + + "jxIBhNP02tCjqOHWhD_tTEy0YuC1WzpYn4egN_18nfWiim5lsYjgcS04H_VoE8YJdpZRIx9a9DIxSNuhp4F" + + "jTuB1L_mypCQ-kOQ2nN25AoGBAJlw0qlzkorQT9ucrI6rWq3JJ39piaTZRjMCIIvhHDENwT2BqXsPwCWDwO" + + "uc6Ydhf86soOnWtIgOxKC_yaYwyNJ6vCQjpMN1Sn4g7siGZffP8Sdvpy99bwYvWpKEaNfAgJXCj-B2qKF-4" + + "iw9QjMuI-zX4uqQ7bhhdTExsJJOMVnfAoGABSbxwvLPglJ6cpoqyGL5Ihg1LS4qog29HVmnX4o_HLXtTCO1" + + "69yQP5lBWIGRO_yUcgouglJpeikcJSPJROWPLs4b2aPv5hhSx47MGZbVAIhSbls5zOZXDZm4wdfQE5J-4kA" + + "VlYF73ZCrH24ZbqqyMF_0wDt_NExsv6FMUwSKfyY=", + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtBtuuijbLHp1EMLKjd_n98rJoh4KeN-OcLGog3MV68l19" + + "6V6972HYiRUOh4ojG33aZuBXrpS2AjnQYIShyIfzJ8U7sKupj68cyxcahnx4p7zkXOGPpmUoonDfwI6PrkH" + + "YB-S64ZRydIO2ntukVlt2eKTnpf0nl1iYCpuKa4Ss4ujq9zWdZeFqYOAv5ayw_f24S-l3VJq_5F3NlKJXVM" + + "9WckzZg6q9KMosedo1_k6cae1rD1Jl1KGqcVaboDkCpZAhhZbW-Y52ivFWK5QXzdGuzdKPhwkiE4YwS26-u" + + "ailzplGIEvquTponcngs_9QCeTi8En-tzO9TVzLWaCYy3TAQIDAQAB"), + RSA3072( + KeyType.RSA3072, + "MIIG_QIBADANBgkqhkiG9w0BAQEFAASCBucwggbjAgEAAoIBgQCe4Vci7OxOOxek5ZJ_A7uklWpkiRvwAqguotUwD" + + "wXybJqvz2sf0TMgYbSweBLrnt253-w_cJUkXeRS6JgYJNPM5R7ztaWL8Jsl3ETZVwpWJYZ9cRXmI71kB3ys" + + "0ROQwP8Srqw0vT3R6d8DOJ6SbQveScG4Bu5QG_OpFvfwS9qxv7OJGVbTV1w5lPBzQGZ4AFz0annGgz2FVZ6" + + "q61LgabJfjMygxWPNQjbY8HdrsGz3T8RMjb3rRhNKxlZmVZob3qGpHxcDqvWMSz9aPgSzFTBWyIKv16_rzM" + + "JWcISfm-Ye_jpRAFV1quKHau7XPJJoMDdOF0FPyJSLckt97aRjGBG88OwdW-Aj8xMA25_Zrx4Ja_SmHTBX4" + + "RGQJgM4qwhZV-XeHfNX5uML0tZgMIi6PzzbXaF6VIpDF7DsQEkMNheSXcWoD7Mf_gHS_coj0DP8VEuaEd_u" + + "-L4EvJHrbfVZ1cwhlIlhE7MrfkqKWZArypLarrZ7V7shU5pZZe0GEQkc6i0CAwEAAQKCAYALPBd_jH3cPVD" + + "7c9FjYmXxKjCjCc_8LY_rdk-5bYKH5TaeAn6Kei5Rlp8ikGcUGsCGjYdE6CbfsrT7yN9Ca84_UZ9Z7-kUJ3" + + "AtIfGLZdyBAXfMZIP-KV5R1bay-LjXtlIDJe9e7lfSAWXn8ifCZmwdrJ0CcJkG-KcG-K0RJKgDBDGDmxNY7" + + "_dBSh8jozTrvOjVzrasOykNRePppajPXiIDIC9zGeooAEvlFMH5DzlxVoZkvGwm2CZylepTo695U4mB6Fbz" + + "GxHDCXjYVAsQDULx-j670TjnWkmuqXNGEN_SGgAdsEA51gJv8ar1at3nJf2LleSYNAL6sHOski9uEYXJ3Ca" + + "cWvnZRzT_YNT7WKujhmIS3EwOZgbrYI3QreOls9Vy-Xo53oCwiAii5zvxDMkZmG6oOgblRsV4slXtNEaL1m" + + "N2vM2_a4_Kg_yRmzFMY9znYp46qDj5QbmQ5MhEKgC-_DOrX0ih09eNlrgK6nb9VxlI046zthxY3beq47Z59" + + "mkCgcEAy0KHoX4vA-llBohsGiB_mCa6vvpdYk-uKqUcnEBG9u7UFEXKFn3n7ern2v6D1Om69u4S4_PIw2uS" + + "MBZ89jhNpo2WbX5GI_zQ-DCct9HJEN7vHM9W9AVuWLdn6_HM8PzTvrke1ZKGXorug-IWww6uaaBj-9rHJTT" + + "q_Bg0oUk1jv2jianp2Ct5ScFm4--kIjmb1AuLebywCvMSxZ5VtYHfb4VRgCM-nAIOdv9s27FNkyVKHCgjK0" + + "dkLCMB0DgoTjGlAoHBAMga6ETh49iQtCAJpq28cQnqLSOCUI7dLqJcnEitUL5PtyCFkrpS7ZfLr15cHi4Ud" + + "9ycxtbaxAlaNtLo31l8tpLgbeVck0Ke67OUMRGrq9ONVXTYMNNX3tq2M7XO2slKa82O_tUDMjVlVv6v7R-L" + + "XTeqXItgxdISg-ZAYQHG550n6Ldterc2WxGOFZNjD1fkgbdwnu9f9DT-jS-m_C_oGXD5iqAaV2ovcVFb19i" + + "ubRrltcqDi1nyhXnyKyjYNXff6QKBwQCBdsPLAgNSO4PEkHpCffanY-vIntGCP_xQX1CE2ZAZ0m805mrcvp" + + "9OdDPv0fMIV0Nl6qgPl4SFrGu1w20eqygScNaisS5d17cGjngTwUSPQWAN-qaI0TjCuzcvGpmN2YvJTEIui" + + "KCbcWSQjh4vaZd_4dAtZ-E2eqk9nvFO1cGObVGP6rDupmofp1cw0b-6qPTvL9dL1_pNTxvi0YIIFUvKzaDm" + + "vAwx9EFgXDrrB9jAY5z7qDkWZOeSEU4jYNGTVJ0CgcBlfTgmj4b29NVWlm6CGVwfkjTYmKRxAP9A-8WMGtM" + + "j4txXU0fK1nqIjZbhPclUx67PJni2yfe5YpcBu3hkM5uJvOgf9yb9GAslZljIxI_-WOVpwKhq2FtABD8Py9" + + "0tUGCCvi7DLL7PVBmeTO3wHMfnjrEnQ6qxVBCvvCE3PIGGNJKUTaN6vsfLjIum2AwVIOElf6oscDc0lZJYA" + + "9JOHeKhaP8FGrcRNQS9Jd7AmB7gEHd2QedwdE98PPXk3lun89kCgcATb1Hcac_ASVlLfyNvhEWYS7yHfALv" + + "GlCSTv-zdsIhnZ0oeN8ILSmzM0QaRB6matSnt-ocYJmJYuVwuzHFwZ-eitjnEmVMuMAMykUV40flaATPqER" + + "ZvJmVmZbbCBv5qdb48zFkXQtR4qUSvg6GjGUL7CAlBEjqW479l3KtiUuUF7WlIfU-iHhbA7HfkSbN8gtTor" + + "X5_t3OwZeFf-OGki5Iaf92uoq716yEBvD45dbPnkJG5sQTWDIHIKmRcwQ9ibw=", + "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAnuFXIuzsTjsXpOWSfwO7pJVqZIkb8AKoLqLVMA8F8myar" + + "89rH9EzIGG0sHgS657dud_sP3CVJF3kUuiYGCTTzOUe87Wli_CbJdxE2VcKViWGfXEV5iO9ZAd8rNETkMD_" + + "Eq6sNL090enfAziekm0L3knBuAbuUBvzqRb38Evasb-ziRlW01dcOZTwc0BmeABc9Gp5xoM9hVWequtS4Gm" + + "yX4zMoMVjzUI22PB3a7Bs90_ETI2960YTSsZWZlWaG96hqR8XA6r1jEs_Wj4EsxUwVsiCr9ev68zCVnCEn5" + + "vmHv46UQBVdarih2ru1zySaDA3ThdBT8iUi3JLfe2kYxgRvPDsHVvgI_MTANuf2a8eCWv0ph0wV-ERkCYDO" + + "KsIWVfl3h3zV-bjC9LWYDCIuj88212helSKQxew7EBJDDYXkl3FqA-zH_4B0v3KI9Az_FRLmhHf7vi-BLyR" + + "6231WdXMIZSJYROzK35KilmQK8qS2q62e1e7IVOaWWXtBhEJHOotAgMBAAE="), + RSA4096( + KeyType.RSA4096, + "MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDLqdGZtBymZ0yzMPApteDlEc0hejP3Vee7i9L8V" + + "23HytZyQak6RrlsDPSLgpkn3p9IUZ6fpMN6ND5TPCvYiGu69Um1MUHlE9n3iZEPLHzu1EhgUHpZMlqAwJYP" + + "q1B_CA1yuiyjdafvb8t9CkKeVIg-ud9WZyQDc-NK1tneOpx87fKZaoqhrUoG4RySv3NwPJYiNgjIIzfZiNZ" + + "5hyGHzTV3zi8bXt7Qt-pzIDn0QBciWHHpxGBGpxdQ74xu0K2mctwsnmJSi69FqNWhKekby0M-ZmBRw5d92Z" + + "fOWbYOslSUmI2dyDvNvojyNDPUFeWq1T8OaMYoAKe-CvXupZpLSGTzgDUZ23kG5aiKFYCiePGBeeYdhCuZq" + + "_ybwM0CI-kCfmfZW1L2o-nZshTuSJxpnTcOVwzX_fkFVlN5dLwXCwLxhJNTJ7Vi5NnzS4Yp8NfY9iPkD97m" + + "76zLOMfInt50qhxoyEjRK_r-Afjcv-hzrqyp9Q_dontQMVM-y0ck1MKhwl7N4-3k29hy_T9OfTF7qpV02RF" + + "36kVEeHZBLZN6ZlEkA89rib_IUj7wM6qWH3_m3NAELPCUhQ7jCBmnCzTssElwwYqy7jbpmImdWq6JCRyIv_" + + "LUubGYmrUrYKpEfNKTMHjGMhNdOJlkjwDZZqlPl6KvNcKe8xibAkHn1KCQxBH0SwIDAQABAoICAEaKicY2m" + + "8vJMMhKFY0k6YH-EfJb_N7Y2txbWFc9wxD2ASPu-TntoDC8JgGiWQD1u27Vkl8SNwC_Uq0KxNcJnWLSrdZ7" + + "-qppH1B9TgiW7KjeTzlI9q-pYK6CxhckS8vMErhforF1QZcNvkoPgTaM6ensAF7Rd6hYfewAkdLGs0gUNLi" + + "NrfnE50SNuRNdC5Ne8NNlqtIDXMdUfZD3TJZYbgISoL9Ws09QvHxmt5wRjNHBF6eT9JLUMh--8QG69sKVuw" + + "wbScv0hN9YVLIvLYYKd76HwCnh2Llm4g0_hm79to0Hb9msLoabTZyylxcJBJUQqngHs0bMv0z0R-2CX3he2" + + "VPdrfrXEpX3UCpNu-BCmXxNNlfB65o8NtG_ro77RZrMAhMnr1H2Xrj-FdTE-Q5gs5oOqp21wThoZs9SjDF1" + + "S6SiOyES47EfRE1a8gVTCAC7HAgtLNauj4ik_qDTuah1ucUrgqu3f2ZaqWKxSVlYgewPPu9Xuwxgh2h0zv0" + + "a1dhT7OA7OC_Wn63KNgN7W2tr2CLNG5jWY7L723nVnJjorWmc2FynpOhi6k2IQoJ-DPVpKpttAI6f-rCvAI" + + "uPe_EIyKG1Pu_Em0To796_QLY-VstJoYscZwBd2ZH6ZTr1bwNSWHkp5nR9k05u108-WD03wtip6cl8-iGca" + + "IpqI67Oo-bdAoIBAQDnX-RxftNxrMjiOF54Qtj-9Qt25lR72C74s5aJCsci0jCBnhGyvrIQPYTufvgo5m2m" + + "qyEKjVJcOfprN9VpL3gZjfaDAq6Vl5WE6-Q-cxF2B9rpX9Tuzo4Fmj83ReElidel9w7am4U3TbToyZ1h0pS" + + "OJftr3BhnmMcOlOIiaplDawJ6THb03lkvV5JAwiAZR64TPGNv3njhkr295Hn1kIhMetZSzPGnEfbAbRmfhw" + + "FLfobcE5xdIlK2EhpJdMS5ovKINVA6Fw2xEEaC6CYn4xPkVJTPafGVK8TZZie-QFkAVsIk311oiftMiMK1K" + + "57d5RcE1hdXSdYUFpeVYmI30OT3AoIBAQDhVuWzt8wGkg7ZVwc_4VMd0cT1qA_XMCvQ4wl6mIMITLUYXFEj" + + "kpFnLSntkAy34tGTPOqpM9DeCbumtjj3HPd893ERNEkxlpfR2oY9L4XM6fQajCGxIbv5H1cLnsblJa_P5cK" + + "tRjYWqrY4adU2qOrX6VGaJkvckJC3BjO4xGNlvRE_y-xfmShwQEfW7L8ehgdEhb8_F14GbpyFz7ZzD4nGon" + + "ecQwdkAsBBEgwPSnVF-4d7R3J1GwyGrtDHSEdaRsL_S-O3HkepPADApntAIGVUR_MI_mCtmsUtc_5mUqg-b" + + "JQqh5dnFsIwXZLZQyQaVvewPUedkkHgXn54_s0h5BpNAoIBAQCByT6Bk5zUFRISI4CKgSTrz1UA-y7E0X13" + + "sHVupgcSN0lSS_Kti16i0X9xsPNPLgKwDSpZmvBqH3OjFQy3FhOOch2nW6fG7eLHTvMXPMC8rqdTZZgx5Ne" + + "xuNZhEOe8gNfglvdUFQzi-snSEtYfe1otaozf8fQWmJKAUW-P0q_qK2qWY7IOpXLtpXe6r6oFxDmXPLail-" + + "7Cyed5T2JCJzLtg7IZfDDJgMAjLI_E9pv5Vx4a8T0y2QAAdaMdNUzsvMTDNvSrwSbC_dgvsj1E_pG38OIQf" + + "uMuxACF2lHM3JeQIxqASHNDIrM-OTDPI4rX-Zux8M3i_t4BIrMg7rEdkiX9AoIBACapLgfDhPGrpXiMgeXn" + + "1sbK8qvjBbS5wwq3qSyrde-6mWdwj0s3HlNBYGwtxsDV3XcRgIE_LpqpuNRFd0iOY7fBDFkTS2uCltGeWGG" + + "vAZnCmerkF_O4AfQf-GM5_o3aBWv504i-_xCsgU70eWxDVudsVF_KKkHRW8LLAZy1tQgDhC4Z4pgUQuffX3" + + "P0cmXeQOj0uXctnygjWh9rH7Zl-BFoVnUs2tvBzRJc8ky9TZmQKhJwk6ab2W5SF-fY8sT-Vv5OGueT_l9-t" + + "_JVndfGtxvarEviuNuQLjw6Jm-PxuXO4yzYzpUVRoPdyhAUgOE0ApLuMJdMPJkuHSzNKoyiAhECggEBAJgn" + + "1SBkJqfa6cgx7W2eEn8kp4wBpIuPjkFzJcJJ7LBbfmzCMKX8SBZRcF2kOIOuM8TCFxkW5q8wRUTeqP2awgA" + + "MnYWeuxhJM0rzbVg3l6I1Lr1HIOxEExxERr11Qtz3tXM6I2UVJyEF4hu3P1CsZeXJp8458jySwhC4J0Tir5" + + "sQ8fj2M0wOiwHYXDY1A01EpicaeFv7882RHdlPUXXyHORvZ6vA8IVga_PyUovzmvmueOFp2HFmoqpPLbtiz" + + "8UgfYVsAyGAPiJpTOXT2YipLMeUb7MXdAon100pIxjrfatE7iJRs6jW0o4tM7ctAhZ7IXQWuolas4ly8L0M" + + "ABx9258=", + "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAy6nRmbQcpmdMszDwKbXg5RHNIXoz91Xnu4vS_Fdtx8rWc" + + "kGpOka5bAz0i4KZJ96fSFGen6TDejQ-Uzwr2IhruvVJtTFB5RPZ94mRDyx87tRIYFB6WTJagMCWD6tQfwgN" + + "croso3Wn72_LfQpCnlSIPrnfVmckA3PjStbZ3jqcfO3ymWqKoa1KBuEckr9zcDyWIjYIyCM32YjWeYchh80" + + "1d84vG17e0LfqcyA59EAXIlhx6cRgRqcXUO-MbtCtpnLcLJ5iUouvRajVoSnpG8tDPmZgUcOXfdmXzlm2Dr" + + "JUlJiNncg7zb6I8jQz1BXlqtU_DmjGKACnvgr17qWaS0hk84A1Gdt5BuWoihWAonjxgXnmHYQrmav8m8DNA" + + "iPpAn5n2VtS9qPp2bIU7kicaZ03DlcM1_35BVZTeXS8FwsC8YSTUye1YuTZ80uGKfDX2PYj5A_e5u-syzjH" + + "yJ7edKocaMhI0Sv6_gH43L_oc66sqfUP3aJ7UDFTPstHJNTCocJezePt5NvYcv0_Tn0xe6qVdNkRd-pFRHh" + + "2QS2TemZRJAPPa4m_yFI-8DOqlh9_5tzQBCzwlIUO4wgZpws07LBJcMGKsu426ZiJnVquiQkciL_y1LmxmJ" + + "q1K2CqRHzSkzB4xjITXTiZZI8A2WapT5eirzXCnvMYmwJB59SgkMQR9EsCAwEAAQ=="), + ECCP256( + KeyType.ECCP256, + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgaEygF-BBlaq6MkmJuN4CTGYo2QPJZYadPjRhKPodC" + + "dyhRANCAAQA9NDknDc4Mor6mWKaW0zo3BLSwF8d1yNf4HCLn_zbwvEkjuXo7-tob8faiZrixXoK7zuxip8y" + + "h86r-f0x1bFG", + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAPTQ5Jw3ODKK-plimltM6NwS0sBfHdcjX-Bwi5_828LxJI7l6O_ra" + + "G_H2oma4sV6Cu87sYqfMofOq_n9MdWxRg"), + ECCP384( + KeyType.ECCP384, + "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCqCSz-IHpchR9ffO4TJKkxNiBg5Wlg2AK7u4ge_egQZ" + + "C_qQdTxFZZp8wTHDMNzeaOhZANiAAQ9p9ePq4YY_MfPRQUfx_OPxi1Ch6e4uIhgVYRUJYgW_kfZhyGRqlEn" + + "xXxbdBiCigPDHTWg0botpzmhGWfAmQ63v_2gluvB1sepqojTTzKlvkGLYui_UZR0GVzyM1KSMww", + "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEPafXj6uGGPzHz0UFH8fzj8YtQoenuLiIYFWEVCWIFv5H2YchkapRJ8V8W" + + "3QYgooDwx01oNG6Lac5oRlnwJkOt7_9oJbrwdbHqaqI008ypb5Bi2Lov1GUdBlc8jNSkjMM"), + ED25519( + KeyType.ED25519, + "MC4CAQAwBQYDK2VwBCIEIO_yEBZ291rK6lY8BH3RVtO61LnzLv78VxVxBZDj3uvi", + "MCowBQYDK2VwAyEA7m2UD-6mR8vVSpGFFYCnsDgXTuFRT5_M7yVOMM_7uHw="), + X25519( + KeyType.X25519, + "MC4CAQAwBQYDK2VuBCIEIJjvGxF_sesDPC6uoIanoMQU-O4HGMpCqyBssnhc8yBS", + "MCowBQYDK2VuAyEAq6Ws-klOFZ_Kbnf4TPqR45T9szGWeKz-5udDURxOeS4"); + + private final KeyType keyType; + private final String privateKey; + private final String publicKey; + + StaticKey(KeyType keyType, String privateKey, String publicKey) { + this.keyType = keyType; + this.privateKey = privateKey; + this.publicKey = publicKey; } - private static final String[] EC_SIGNATURE_ALGORITHMS = new String[]{"NONEwithECDSA", "SHA1withECDSA", "SHA224withECDSA", "SHA256withECDSA", "SHA384withECDSA", "SHA512withECDSA"}; - private static final String[] RSA_SIGNATURE_ALGORITHMS = new String[]{"NONEwithRSA", "MD5withRSA", "SHA1withRSA", "SHA224withRSA", "SHA256withRSA", "SHA384withRSA", "SHA512withRSA"}; - private static final String[] RSA_CIPHER_ALGORITHMS = new String[]{"RSA/ECB/PKCS1Padding", "RSA/ECB/OAEPWithSHA-1AndMGF1Padding", "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"}; - - public static KeyPair generateKey(KeyType keyType) { - switch (keyType) { - case ECCP256: - return generateEcKey("secp256r1"); - case ECCP384: - return generateEcKey("secp384r1"); - case ED25519: - return generateCv25519Key("ED25519"); - case X25519: - return generateCv25519Key("X25519"); - case RSA1024: - case RSA2048: - case RSA3072: - case RSA4096: - return generateRsaKey(keyType.params.bitLength); - } - throw new IllegalArgumentException("Invalid algorithm"); + private KeyPair getKeyPair() { + try { + KeyFactory kf = + KeyFactory.getInstance( + (keyType == KeyType.ED25519 || keyType == KeyType.X25519) + ? keyType.name() + : keyType.params.algorithm.name()); + return new KeyPair( + kf.generatePublic(new X509EncodedKeySpec(Base64.fromUrlSafeString(publicKey))), + kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.fromUrlSafeString(privateKey)))); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new IllegalStateException(e); + } } - - private static KeyPair generateEcKey(String curve) { - try { - KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyType.Algorithm.EC.name()); - kpg.initialize(new ECGenParameterSpec(curve), secureRandom); - return kpg.generateKeyPair(); - } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { - throw new IllegalStateException(e); - } + } + + private static final String[] EC_SIGNATURE_ALGORITHMS = + new String[] { + "NONEwithECDSA", + "SHA1withECDSA", + "SHA224withECDSA", + "SHA256withECDSA", + "SHA384withECDSA", + "SHA512withECDSA" + }; + private static final String[] RSA_SIGNATURE_ALGORITHMS = + new String[] { + "NONEwithRSA", + "MD5withRSA", + "SHA1withRSA", + "SHA224withRSA", + "SHA256withRSA", + "SHA384withRSA", + "SHA512withRSA" + }; + private static final String[] RSA_CIPHER_ALGORITHMS = + new String[] { + "RSA/ECB/PKCS1Padding", + "RSA/ECB/OAEPWithSHA-1AndMGF1Padding", + "RSA/ECB/OAEPWithSHA-256AndMGF1Padding" + }; + + public static KeyPair generateKey(KeyType keyType) { + switch (keyType) { + case ECCP256: + return generateEcKey("secp256r1"); + case ECCP384: + return generateEcKey("secp384r1"); + case ED25519: + return generateCv25519Key("ED25519"); + case X25519: + return generateCv25519Key("X25519"); + case RSA1024: + case RSA2048: + case RSA3072: + case RSA4096: + return generateRsaKey(keyType.params.bitLength); } - - private static KeyPair generateCv25519Key(String keyType) { - if (!Objects.equals(keyType, "ED25519") && !Objects.equals(keyType, "X25519")) { - throw new IllegalArgumentException("Invalid key keyType"); - } - try { - KeyPairGenerator kpg = KeyPairGenerator.getInstance(keyType); - return kpg.generateKeyPair(); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } + throw new IllegalArgumentException("Invalid algorithm"); + } + + private static KeyPair generateEcKey(String curve) { + try { + KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyType.Algorithm.EC.name()); + kpg.initialize(new ECGenParameterSpec(curve), secureRandom); + return kpg.generateKeyPair(); + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { + throw new IllegalStateException(e); } + } - private static KeyPair generateRsaKey(int keySize) { - try { - KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyType.Algorithm.RSA.name()); - kpg.initialize(keySize); - return kpg.generateKeyPair(); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } + private static KeyPair generateCv25519Key(String keyType) { + if (!Objects.equals(keyType, "ED25519") && !Objects.equals(keyType, "X25519")) { + throw new IllegalArgumentException("Invalid key keyType"); } - - public static KeyPair loadKey(KeyType keyType) { - for (StaticKey staticKey : StaticKey.values()) { - if (keyType == staticKey.keyType) { - return staticKey.getKeyPair(); - } - } - throw new IllegalArgumentException("Unknown algorithm"); + try { + KeyPairGenerator kpg = KeyPairGenerator.getInstance(keyType); + return kpg.generateKeyPair(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); } - - public static X509Certificate createCertificate(KeyPair keyPair) throws IOException, CertificateException { - X500Name name = new X500Name("CN=Example"); - X509v3CertificateBuilder serverCertGen = new X509v3CertificateBuilder( - name, - new BigInteger("123456789"), - new Date(), - new Date(), - name, - SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(keyPair.getPublic() - .getEncoded())) - ); - - String algorithm; - KeyType keyType = KeyType.fromKey(keyPair.getPrivate()); - switch (keyType.params.algorithm) { - case EC: - algorithm = keyType == KeyType.ED25519 - ? "ED25519" - : "SHA256WithECDSA"; - break; - case RSA: - algorithm = "SHA256WithRSA"; - break; - default: - throw new IllegalStateException(); - } - try { - - // for X25519 we sign with a temporary key - PrivateKey pk = null; - if (keyType == KeyType.X25519) { - try { - KeyPairGenerator kpg = KeyPairGenerator.getInstance("ED25519"); - kpg.initialize(255); - KeyPair tempKeyPair = kpg.generateKeyPair(); - pk = tempKeyPair.getPrivate(); - algorithm = "ED25519"; - } catch (Exception e) { - // ignored - } - } else { - pk = keyPair.getPrivate(); - } - - ContentSigner contentSigner = new JcaContentSignerBuilder(algorithm).build(pk); - X509CertificateHolder holder = serverCertGen.build(contentSigner); - - InputStream stream = new ByteArrayInputStream(holder.getEncoded()); - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - return (X509Certificate) cf.generateCertificate(stream); - } catch (OperatorCreationException e) { - throw new RuntimeException(e); - } + } + + private static KeyPair generateRsaKey(int keySize) { + try { + KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyType.Algorithm.RSA.name()); + kpg.initialize(keySize); + return kpg.generateKeyPair(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); } + } - public static byte[] sign(PrivateKey privateKey, Signature algorithm) throws Exception { - byte[] message = "Hello world".getBytes(StandardCharsets.UTF_8); - algorithm.initSign(privateKey); - algorithm.update(message); - return algorithm.sign(); + public static KeyPair loadKey(KeyType keyType) { + for (StaticKey staticKey : StaticKey.values()) { + if (keyType == staticKey.keyType) { + return staticKey.getKeyPair(); + } } - - public static void verify(PublicKey publicKey, Signature algorithm, byte[] signature) throws Exception { - byte[] message = "Hello world".getBytes(StandardCharsets.UTF_8); - algorithm.initVerify(publicKey); - algorithm.update(message); - boolean result = algorithm.verify(signature); - Assert.assertTrue("Signature mismatch for " + algorithm.getAlgorithm(), result); + throw new IllegalArgumentException("Unknown algorithm"); + } + + public static X509Certificate createCertificate(KeyPair keyPair) + throws IOException, CertificateException { + X500Name name = new X500Name("CN=Example"); + X509v3CertificateBuilder serverCertGen = + new X509v3CertificateBuilder( + name, + new BigInteger("123456789"), + new Date(), + new Date(), + name, + SubjectPublicKeyInfo.getInstance( + ASN1Sequence.getInstance(keyPair.getPublic().getEncoded()))); + + String algorithm; + KeyType keyType = KeyType.fromKey(keyPair.getPrivate()); + switch (keyType.params.algorithm) { + case EC: + algorithm = keyType == KeyType.ED25519 ? "ED25519" : "SHA256WithECDSA"; + break; + case RSA: + algorithm = "SHA256WithRSA"; + break; + default: + throw new IllegalStateException(); } + try { - public static void rsaSignAndVerify(PrivateKey privateKey, PublicKey publicKey) throws Exception { - for (String algorithm : RSA_SIGNATURE_ALGORITHMS) { - verify(publicKey, Signature.getInstance(algorithm), sign(privateKey, Signature.getInstance(algorithm))); + // for X25519 we sign with a temporary key + PrivateKey pk = null; + if (keyType == KeyType.X25519) { + try { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("ED25519"); + kpg.initialize(255); + KeyPair tempKeyPair = kpg.generateKeyPair(); + pk = tempKeyPair.getPrivate(); + algorithm = "ED25519"; + } catch (Exception e) { + // ignored } + } else { + pk = keyPair.getPrivate(); + } + + ContentSigner contentSigner = new JcaContentSignerBuilder(algorithm).build(pk); + X509CertificateHolder holder = serverCertGen.build(contentSigner); + + InputStream stream = new ByteArrayInputStream(holder.getEncoded()); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(stream); + } catch (OperatorCreationException e) { + throw new RuntimeException(e); + } + } + + public static byte[] sign(PrivateKey privateKey, Signature algorithm) throws Exception { + byte[] message = "Hello world".getBytes(StandardCharsets.UTF_8); + algorithm.initSign(privateKey); + algorithm.update(message); + return algorithm.sign(); + } + + public static void verify(PublicKey publicKey, Signature algorithm, byte[] signature) + throws Exception { + byte[] message = "Hello world".getBytes(StandardCharsets.UTF_8); + algorithm.initVerify(publicKey); + algorithm.update(message); + boolean result = algorithm.verify(signature); + Assert.assertTrue("Signature mismatch for " + algorithm.getAlgorithm(), result); + } + + public static void rsaSignAndVerify(PrivateKey privateKey, PublicKey publicKey) throws Exception { + for (String algorithm : RSA_SIGNATURE_ALGORITHMS) { + verify( + publicKey, + Signature.getInstance(algorithm), + sign(privateKey, Signature.getInstance(algorithm))); } + } - public static void encryptAndDecrypt(PrivateKey privateKey, PublicKey publicKey, Cipher algorithm) throws Exception { - byte[] message = "Hello world".getBytes(StandardCharsets.UTF_8); + public static void encryptAndDecrypt(PrivateKey privateKey, PublicKey publicKey, Cipher algorithm) + throws Exception { + byte[] message = "Hello world".getBytes(StandardCharsets.UTF_8); - algorithm.init(Cipher.ENCRYPT_MODE, publicKey); - byte[] encrypted = algorithm.doFinal(message); + algorithm.init(Cipher.ENCRYPT_MODE, publicKey); + byte[] encrypted = algorithm.doFinal(message); - algorithm = Cipher.getInstance(algorithm.getAlgorithm()); - algorithm.init(Cipher.DECRYPT_MODE, privateKey); - byte[] decrypted = algorithm.doFinal(encrypted); + algorithm = Cipher.getInstance(algorithm.getAlgorithm()); + algorithm.init(Cipher.DECRYPT_MODE, privateKey); + byte[] decrypted = algorithm.doFinal(encrypted); - Assert.assertArrayEquals("Decrypted mismatch", decrypted, message); - } + Assert.assertArrayEquals("Decrypted mismatch", decrypted, message); + } - public static void rsaEncryptAndDecrypt(PrivateKey privateKey, PublicKey publicKey) throws Exception { - for (String algorithm : RSA_CIPHER_ALGORITHMS) { - encryptAndDecrypt(privateKey, publicKey, Cipher.getInstance(algorithm)); - } - } - - public static void rsaTests() throws Exception { - for (KeyPair keyPair : new KeyPair[]{generateKey(KeyType.RSA1024), generateKey(KeyType.RSA2048), generateKey(KeyType.RSA3072), generateKey(KeyType.RSA4096)}) { - rsaEncryptAndDecrypt(keyPair.getPrivate(), keyPair.getPublic()); - rsaSignAndVerify(keyPair.getPrivate(), keyPair.getPublic()); - } + public static void rsaEncryptAndDecrypt(PrivateKey privateKey, PublicKey publicKey) + throws Exception { + for (String algorithm : RSA_CIPHER_ALGORITHMS) { + encryptAndDecrypt(privateKey, publicKey, Cipher.getInstance(algorithm)); } - - public static void ecTests() throws Exception { - for (KeyPair keyPair : new KeyPair[]{generateKey(KeyType.ECCP256), generateKey(KeyType.ECCP384)}) { - ecSignAndVerify(keyPair.getPrivate(), keyPair.getPublic()); - ecKeyAgreement(keyPair.getPrivate(), keyPair.getPublic()); - } + } + + public static void rsaTests() throws Exception { + for (KeyPair keyPair : + new KeyPair[] { + generateKey(KeyType.RSA1024), + generateKey(KeyType.RSA2048), + generateKey(KeyType.RSA3072), + generateKey(KeyType.RSA4096) + }) { + rsaEncryptAndDecrypt(keyPair.getPrivate(), keyPair.getPublic()); + rsaSignAndVerify(keyPair.getPrivate(), keyPair.getPublic()); } + } - public static void cv25519Tests() throws Exception { - KeyPair ed25519KeyPair = generateKey(KeyType.ED25519); - ed25519SignAndVerify(ed25519KeyPair.getPrivate(), ed25519KeyPair.getPublic()); + public static void ecTests() throws Exception { + for (KeyPair keyPair : + new KeyPair[] {generateKey(KeyType.ECCP256), generateKey(KeyType.ECCP384)}) { + ecSignAndVerify(keyPair.getPrivate(), keyPair.getPublic()); + ecKeyAgreement(keyPair.getPrivate(), keyPair.getPublic()); } - - public static void ecSignAndVerify(PrivateKey privateKey, PublicKey publicKey) throws Exception { - for (String algorithm : EC_SIGNATURE_ALGORITHMS) { - verify(publicKey, Signature.getInstance(algorithm), sign(privateKey, Signature.getInstance(algorithm))); - } + } + + public static void cv25519Tests() throws Exception { + KeyPair ed25519KeyPair = generateKey(KeyType.ED25519); + ed25519SignAndVerify(ed25519KeyPair.getPrivate(), ed25519KeyPair.getPublic()); + } + + public static void ecSignAndVerify(PrivateKey privateKey, PublicKey publicKey) throws Exception { + for (String algorithm : EC_SIGNATURE_ALGORITHMS) { + verify( + publicKey, + Signature.getInstance(algorithm), + sign(privateKey, Signature.getInstance(algorithm))); } + } - public static void ed25519SignAndVerify(PrivateKey privateKey, PublicKey publicKey) throws Exception { - String algorithm = "ED25519"; - verify(publicKey, Signature.getInstance(algorithm), sign(privateKey, Signature.getInstance(algorithm))); - } + public static void ed25519SignAndVerify(PrivateKey privateKey, PublicKey publicKey) + throws Exception { + String algorithm = "ED25519"; + verify( + publicKey, + Signature.getInstance(algorithm), + sign(privateKey, Signature.getInstance(algorithm))); + } - public static void ecKeyAgreement(PrivateKey privateKey, PublicKey publicKey) throws Exception { - KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); - kpg.initialize(((ECKey) publicKey).getParams()); + public static void ecKeyAgreement(PrivateKey privateKey, PublicKey publicKey) throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); + kpg.initialize(((ECKey) publicKey).getParams()); - KeyPair peerPair = kpg.generateKeyPair(); + KeyPair peerPair = kpg.generateKeyPair(); - KeyAgreement ka = KeyAgreement.getInstance("ECDH"); + KeyAgreement ka = KeyAgreement.getInstance("ECDH"); - ka.init(privateKey); - ka.doPhase(peerPair.getPublic(), true); - byte[] secret = ka.generateSecret(); + ka.init(privateKey); + ka.doPhase(peerPair.getPublic(), true); + byte[] secret = ka.generateSecret(); - ka = KeyAgreement.getInstance("ECDH"); - ka.init(peerPair.getPrivate()); - ka.doPhase(publicKey, true); - byte[] peerSecret = ka.generateSecret(); + ka = KeyAgreement.getInstance("ECDH"); + ka.init(peerPair.getPrivate()); + ka.doPhase(publicKey, true); + byte[] peerSecret = ka.generateSecret(); - Assert.assertArrayEquals("Secret mismatch", secret, peerSecret); - } + Assert.assertArrayEquals("Secret mismatch", secret, peerSecret); + } - public static void x25519KeyAgreement(PrivateKey privateKey, PublicKey publicKey) throws Exception { - KeyPairGenerator kpg = KeyPairGenerator.getInstance("X25519"); - kpg.initialize(255); + public static void x25519KeyAgreement(PrivateKey privateKey, PublicKey publicKey) + throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("X25519"); + kpg.initialize(255); - KeyPair peerPair = kpg.generateKeyPair(); + KeyPair peerPair = kpg.generateKeyPair(); - KeyAgreement ka = KeyAgreement.getInstance("X25519"); + KeyAgreement ka = KeyAgreement.getInstance("X25519"); - ka.init(privateKey); - ka.doPhase(peerPair.getPublic(), true); - byte[] secret = ka.generateSecret(); + ka.init(privateKey); + ka.doPhase(peerPair.getPublic(), true); + byte[] secret = ka.generateSecret(); - ka = KeyAgreement.getInstance("X25519"); - ka.init(peerPair.getPrivate()); - ka.doPhase(publicKey, true); - byte[] peerSecret = ka.generateSecret(); + ka = KeyAgreement.getInstance("X25519"); + ka.init(peerPair.getPrivate()); + ka.doPhase(publicKey, true); + byte[] peerSecret = ka.generateSecret(); - Assert.assertArrayEquals("Secret mismatch", secret, peerSecret); - } + Assert.assertArrayEquals("Secret mismatch", secret, peerSecret); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java index 9250b1b8..416c5c2e 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp03DeviceTests.java @@ -28,158 +28,177 @@ import com.yubico.yubikit.core.smartcard.scp.SecurityDomainSession; import com.yubico.yubikit.core.smartcard.scp.StaticKeys; import com.yubico.yubikit.core.util.RandomUtils; - import java.io.IOException; public class Scp03DeviceTests { - static final KeyRef DEFAULT_KEY = new KeyRef((byte) 0x01, (byte) 0xff); - static final ScpKeyParams defaultRef = new Scp03KeyParams(DEFAULT_KEY, StaticKeys.getDefaultKeys()); + static final KeyRef DEFAULT_KEY = new KeyRef((byte) 0x01, (byte) 0xff); + static final ScpKeyParams defaultRef = + new Scp03KeyParams(DEFAULT_KEY, StaticKeys.getDefaultKeys()); - public static void before(SecurityDomainTestState state) throws Throwable { - assumeFalse("SCP03 not supported over NFC on FIPS capable devices", - state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); - state.withSecurityDomain(SecurityDomainSession::reset); - } + public static void before(SecurityDomainTestState state) throws Throwable { + assumeFalse( + "SCP03 not supported over NFC on FIPS capable devices", + state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); + state.withSecurityDomain(SecurityDomainSession::reset); + } - public static void testImportKey(SecurityDomainTestState state) throws Throwable { + public static void testImportKey(SecurityDomainTestState state) throws Throwable { - final byte[] sk = new byte[]{ - 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, - 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + final byte[] sk = + new byte[] { + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, }; - final StaticKeys staticKeys = new StaticKeys(sk, sk, sk); - final KeyRef ref = new KeyRef((byte) 0x01, (byte) 0x01); - final ScpKeyParams params = new Scp03KeyParams(ref, staticKeys); + final StaticKeys staticKeys = new StaticKeys(sk, sk, sk); + final KeyRef ref = new KeyRef((byte) 0x01, (byte) 0x01); + final ScpKeyParams params = new Scp03KeyParams(ref, staticKeys); - assumeFalse("SCP03 not supported over NFC on FIPS capable devices", - state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); + assumeFalse( + "SCP03 not supported over NFC on FIPS capable devices", + state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); - state.withSecurityDomain(session -> { - session.authenticate(defaultRef); - session.putKey(ref, staticKeys, 0); + state.withSecurityDomain( + session -> { + session.authenticate(defaultRef); + session.putKey(ref, staticKeys, 0); }); - state.withSecurityDomain(session -> { - session.authenticate(params); + state.withSecurityDomain( + session -> { + session.authenticate(params); }); - state.withSecurityDomain(session -> { - // cannot use default key to authenticate - assertThrows(ApduException.class, () -> session.authenticate(defaultRef)); + state.withSecurityDomain( + session -> { + // cannot use default key to authenticate + assertThrows(ApduException.class, () -> session.authenticate(defaultRef)); }); + } - } - - public static void testDeleteKey(SecurityDomainTestState state) throws Throwable { - final StaticKeys staticKeys1 = randomStaticKeys(); - final StaticKeys staticKeys2 = randomStaticKeys(); - final KeyRef keyRef1 = new KeyRef((byte) 0x01, (byte) 0x10); - final KeyRef keyRef2 = new KeyRef((byte) 0x01, (byte) 0x55); - final Scp03KeyParams ref1 = new Scp03KeyParams(keyRef1, staticKeys1); - final ScpKeyParams ref2 = new Scp03KeyParams(keyRef2, staticKeys2); + public static void testDeleteKey(SecurityDomainTestState state) throws Throwable { + final StaticKeys staticKeys1 = randomStaticKeys(); + final StaticKeys staticKeys2 = randomStaticKeys(); + final KeyRef keyRef1 = new KeyRef((byte) 0x01, (byte) 0x10); + final KeyRef keyRef2 = new KeyRef((byte) 0x01, (byte) 0x55); + final Scp03KeyParams ref1 = new Scp03KeyParams(keyRef1, staticKeys1); + final ScpKeyParams ref2 = new Scp03KeyParams(keyRef2, staticKeys2); - state.withSecurityDomain(session -> { - session.authenticate(defaultRef); - session.putKey(keyRef1, staticKeys1, 0); + state.withSecurityDomain( + session -> { + session.authenticate(defaultRef); + session.putKey(keyRef1, staticKeys1, 0); }); - // authenticate with the new key and put the second - state.withSecurityDomain(session -> { - session.authenticate(ref1); - session.putKey(keyRef2, staticKeys2, 0); + // authenticate with the new key and put the second + state.withSecurityDomain( + session -> { + session.authenticate(ref1); + session.putKey(keyRef2, staticKeys2, 0); }); - state.withSecurityDomain(session -> { - session.authenticate(ref1); + state.withSecurityDomain( + session -> { + session.authenticate(ref1); }); - state.withSecurityDomain(session -> { - session.authenticate(ref2); + state.withSecurityDomain( + session -> { + session.authenticate(ref2); }); - // delete first key - state.withSecurityDomain(session -> { - session.authenticate(ref2); - session.deleteKey(keyRef1, false); + // delete first key + state.withSecurityDomain( + session -> { + session.authenticate(ref2); + session.deleteKey(keyRef1, false); }); - state.withSecurityDomain(session -> { - assertThrows(ApduException.class, () -> session.authenticate(ref1)); + state.withSecurityDomain( + session -> { + assertThrows(ApduException.class, () -> session.authenticate(ref1)); }); - state.withSecurityDomain(session -> { - session.authenticate(ref2); + state.withSecurityDomain( + session -> { + session.authenticate(ref2); }); - // delete the second key - state.withSecurityDomain(session -> { - session.authenticate(ref2); - session.deleteKey(keyRef2, true); // the last key + // delete the second key + state.withSecurityDomain( + session -> { + session.authenticate(ref2); + session.deleteKey(keyRef2, true); // the last key }); - state.withSecurityDomain(session -> { - assertThrows(ApduException.class, () -> session.authenticate(ref2)); + state.withSecurityDomain( + session -> { + assertThrows(ApduException.class, () -> session.authenticate(ref2)); }); - } + } - public static void testReplaceKey(SecurityDomainTestState state) throws Throwable { - final StaticKeys staticKeys1 = randomStaticKeys(); - final StaticKeys staticKeys2 = randomStaticKeys(); + public static void testReplaceKey(SecurityDomainTestState state) throws Throwable { + final StaticKeys staticKeys1 = randomStaticKeys(); + final StaticKeys staticKeys2 = randomStaticKeys(); - final KeyRef keyRef1 = new KeyRef((byte) 0x01, (byte) 0x10); - final KeyRef keyRef2 = new KeyRef((byte) 0x01, (byte) 0x55); + final KeyRef keyRef1 = new KeyRef((byte) 0x01, (byte) 0x10); + final KeyRef keyRef2 = new KeyRef((byte) 0x01, (byte) 0x55); - final ScpKeyParams ref1 = new Scp03KeyParams(keyRef1, staticKeys1); - final ScpKeyParams ref2 = new Scp03KeyParams(keyRef2, staticKeys2); + final ScpKeyParams ref1 = new Scp03KeyParams(keyRef1, staticKeys1); + final ScpKeyParams ref2 = new Scp03KeyParams(keyRef2, staticKeys2); - state.withSecurityDomain(session -> { - session.authenticate(defaultRef); - session.putKey(keyRef1, staticKeys1, 0); + state.withSecurityDomain( + session -> { + session.authenticate(defaultRef); + session.putKey(keyRef1, staticKeys1, 0); }); - // authenticate with the new key and replace it with the second - state.withSecurityDomain(session -> { - session.authenticate(ref1); - session.putKey(keyRef2, staticKeys2, keyRef1.getKvn()); + // authenticate with the new key and replace it with the second + state.withSecurityDomain( + session -> { + session.authenticate(ref1); + session.putKey(keyRef2, staticKeys2, keyRef1.getKvn()); }); - state.withSecurityDomain(session -> { - assertThrows(ApduException.class, () -> session.authenticate(ref1)); + state.withSecurityDomain( + session -> { + assertThrows(ApduException.class, () -> session.authenticate(ref1)); }); - state.withSecurityDomain(session -> { - session.authenticate(ref2); + state.withSecurityDomain( + session -> { + session.authenticate(ref2); }); - } + } - public static void testWrongKey(SecurityDomainTestState state) throws Throwable { - final StaticKeys staticKeys = randomStaticKeys(); - final KeyRef ref = new KeyRef((byte) 0x01, (byte) 0x01); - final ScpKeyParams params = new Scp03KeyParams(ref, staticKeys); + public static void testWrongKey(SecurityDomainTestState state) throws Throwable { + final StaticKeys staticKeys = randomStaticKeys(); + final KeyRef ref = new KeyRef((byte) 0x01, (byte) 0x01); + final ScpKeyParams params = new Scp03KeyParams(ref, staticKeys); - state.withSecurityDomain(session -> { - assertThrows(ApduException.class, () -> session.authenticate(params)); - assertThrows(ApduException.class, () -> verifyAuth(session)); + state.withSecurityDomain( + session -> { + assertThrows(ApduException.class, () -> session.authenticate(params)); + assertThrows(ApduException.class, () -> verifyAuth(session)); }); - state.withSecurityDomain(session -> { - session.authenticate(defaultRef); + state.withSecurityDomain( + session -> { + session.authenticate(defaultRef); }); - } + } - private static StaticKeys randomStaticKeys() { - return new StaticKeys( - RandomUtils.getRandomBytes(16), - RandomUtils.getRandomBytes(16), - RandomUtils.getRandomBytes(16) - ); - } + private static StaticKeys randomStaticKeys() { + return new StaticKeys( + RandomUtils.getRandomBytes(16), + RandomUtils.getRandomBytes(16), + RandomUtils.getRandomBytes(16)); + } - private static void verifyAuth(SecurityDomainSession session) - throws BadResponseException, ApduException, IOException { - KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x7f); - session.generateEcKey(ref, 0); - session.deleteKey(ref, false); - } + private static void verifyAuth(SecurityDomainSession session) + throws BadResponseException, ApduException, IOException { + KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x7f); + session.generateEcKey(ref, 0); + session.deleteKey(ref, false); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java index 65d35ac1..b3d6354a 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11DeviceTests.java @@ -39,7 +39,6 @@ import com.yubico.yubikit.core.smartcard.scp.StaticKeys; import com.yubico.yubikit.core.util.RandomUtils; import com.yubico.yubikit.core.util.Tlv; - import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -61,273 +60,311 @@ import java.util.List; public class Scp11DeviceTests { - private static final ScpKeyParams defaultKeyParams = - new Scp03KeyParams(new KeyRef((byte) 0x01, (byte) 0xff), StaticKeys.getDefaultKeys()); - - private static final byte OCE_KID = 0x010; - - public static void before(SecurityDomainTestState state) throws Throwable { - assumeTrue("Device does not support SCP11a", - state.getDeviceInfo().getVersion().isAtLeast(5, 7, 2)); - assumeFalse("SCP03 authentication not supported over NFC on FIPS capable devices", - state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); - state.withSecurityDomain(SecurityDomainSession::reset); - } - - public static void testScp11aAuthenticate(SecurityDomainTestState state) throws Throwable { - final byte kvn = 0x03; - - ScpKeyParams keyParams = state.withSecurityDomain(defaultKeyParams, session -> { - return loadKeys(session, ScpKid.SCP11a, kvn); - }); - - state.withSecurityDomain(keyParams, session -> { - session.deleteKey(new KeyRef(ScpKid.SCP11a, kvn), false); - }); - } - - public static void testScp11aAllowList(SecurityDomainTestState state) throws Throwable { - final byte kvn = 0x05; - final KeyRef oceKeyRef = new KeyRef(OCE_KID, kvn); - - ScpKeyParams keyParams = state.withSecurityDomain(defaultKeyParams, session -> { - return loadKeys(session, ScpKid.SCP11a, kvn); + private static final ScpKeyParams defaultKeyParams = + new Scp03KeyParams(new KeyRef((byte) 0x01, (byte) 0xff), StaticKeys.getDefaultKeys()); + + private static final byte OCE_KID = 0x010; + + public static void before(SecurityDomainTestState state) throws Throwable { + assumeTrue( + "Device does not support SCP11a", state.getDeviceInfo().getVersion().isAtLeast(5, 7, 2)); + assumeFalse( + "SCP03 authentication not supported over NFC on FIPS capable devices", + state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); + state.withSecurityDomain(SecurityDomainSession::reset); + } + + public static void testScp11aAuthenticate(SecurityDomainTestState state) throws Throwable { + final byte kvn = 0x03; + + ScpKeyParams keyParams = + state.withSecurityDomain( + defaultKeyParams, + session -> { + return loadKeys(session, ScpKid.SCP11a, kvn); + }); + + state.withSecurityDomain( + keyParams, + session -> { + session.deleteKey(new KeyRef(ScpKid.SCP11a, kvn), false); }); - - state.withSecurityDomain(keyParams, session -> { - final List serials = Arrays.asList( - // serial numbers from OCE - new BigInteger("7f4971b0ad51f84c9da9928b2d5fef5e16b2920a", 16), - new BigInteger("6b90028800909f9ffcd641346933242748fbe9ad", 16) - ); - session.storeAllowlist(oceKeyRef, serials); + } + + public static void testScp11aAllowList(SecurityDomainTestState state) throws Throwable { + final byte kvn = 0x05; + final KeyRef oceKeyRef = new KeyRef(OCE_KID, kvn); + + ScpKeyParams keyParams = + state.withSecurityDomain( + defaultKeyParams, + session -> { + return loadKeys(session, ScpKid.SCP11a, kvn); + }); + + state.withSecurityDomain( + keyParams, + session -> { + final List serials = + Arrays.asList( + // serial numbers from OCE + new BigInteger("7f4971b0ad51f84c9da9928b2d5fef5e16b2920a", 16), + new BigInteger("6b90028800909f9ffcd641346933242748fbe9ad", 16)); + session.storeAllowlist(oceKeyRef, serials); }); - state.withSecurityDomain(keyParams, session -> { - session.deleteKey(new KeyRef(ScpKid.SCP11a, kvn), false); + state.withSecurityDomain( + keyParams, + session -> { + session.deleteKey(new KeyRef(ScpKid.SCP11a, kvn), false); }); - } + } - public static void testScp11aAllowListBlocked(SecurityDomainTestState state) throws Throwable { - final byte kvn = 0x03; - final KeyRef oceKeyRef = new KeyRef(OCE_KID, kvn); + public static void testScp11aAllowListBlocked(SecurityDomainTestState state) throws Throwable { + final byte kvn = 0x03; + final KeyRef oceKeyRef = new KeyRef(OCE_KID, kvn); - ScpKeyParams scp03KeyParams = importScp03Key(state); + ScpKeyParams scp03KeyParams = importScp03Key(state); - Scp11KeyParams keyParams = state.withSecurityDomain(scp03KeyParams, session -> { - // make space for new key - session.deleteKey(new KeyRef(ScpKid.SCP11b, (byte) 1), false); + Scp11KeyParams keyParams = + state.withSecurityDomain( + scp03KeyParams, + session -> { + // make space for new key + session.deleteKey(new KeyRef(ScpKid.SCP11b, (byte) 1), false); - Scp11KeyParams scp11KeyParams = loadKeys(session, ScpKid.SCP11a, kvn); + Scp11KeyParams scp11KeyParams = loadKeys(session, ScpKid.SCP11a, kvn); - final List serials = Arrays.asList( - BigInteger.valueOf(1), BigInteger.valueOf(2), BigInteger.valueOf(3), - BigInteger.valueOf(4), BigInteger.valueOf(5)); + final List serials = + Arrays.asList( + BigInteger.valueOf(1), + BigInteger.valueOf(2), + BigInteger.valueOf(3), + BigInteger.valueOf(4), + BigInteger.valueOf(5)); - session.storeAllowlist(oceKeyRef, serials); + session.storeAllowlist(oceKeyRef, serials); - return scp11KeyParams; - }); + return scp11KeyParams; + }); - // authenticate with scp11a will throw - state.withSecurityDomain(session -> { - assertThrows(ApduException.class, () -> session.authenticate(keyParams)); + // authenticate with scp11a will throw + state.withSecurityDomain( + session -> { + assertThrows(ApduException.class, () -> session.authenticate(keyParams)); }); - // reset the allow list - state.withSecurityDomain(scp03KeyParams, session -> { - session.storeAllowlist(oceKeyRef, new ArrayList<>()); + // reset the allow list + state.withSecurityDomain( + scp03KeyParams, + session -> { + session.storeAllowlist(oceKeyRef, new ArrayList<>()); }); - // authenticate with scp11a will not throw - state.withSecurityDomain(session -> { - session.authenticate(keyParams); + // authenticate with scp11a will not throw + state.withSecurityDomain( + session -> { + session.authenticate(keyParams); }); - } + } - public static void testScp11bAuthenticate(SecurityDomainTestState state) throws Throwable { - final KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x1); + public static void testScp11bAuthenticate(SecurityDomainTestState state) throws Throwable { + final KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x1); - List chain = state.withSecurityDomain(defaultKeyParams, session -> { - return session.getCertificateBundle(ref); - }); + List chain = + state.withSecurityDomain( + defaultKeyParams, + session -> { + return session.getCertificateBundle(ref); + }); - X509Certificate leaf = chain.get(chain.size() - 1); - Scp11KeyParams params = new Scp11KeyParams(ref, leaf.getPublicKey()); + X509Certificate leaf = chain.get(chain.size() - 1); + Scp11KeyParams params = new Scp11KeyParams(ref, leaf.getPublicKey()); - state.withSecurityDomain(params, session -> { - assertThrows(ApduException.class, () -> verifyScp11bAuth(session)); + state.withSecurityDomain( + params, + session -> { + assertThrows(ApduException.class, () -> verifyScp11bAuth(session)); }); - } - - public static void testScp11bWrongPubKey(SecurityDomainTestState state) throws Throwable { - final KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x1); - - List chain = state.withSecurityDomain(defaultKeyParams, session -> { - return session.getCertificateBundle(ref); + } + + public static void testScp11bWrongPubKey(SecurityDomainTestState state) throws Throwable { + final KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x1); + + List chain = + state.withSecurityDomain( + defaultKeyParams, + session -> { + return session.getCertificateBundle(ref); + }); + + X509Certificate cert = chain.get(0); + Scp11KeyParams params = new Scp11KeyParams(ref, cert.getPublicKey()); + + state.withSecurityDomain( + session -> { + BadResponseException e = + assertThrows(BadResponseException.class, () -> session.authenticate(params)); + assertEquals("Receipt does not match", e.getMessage()); }); - - X509Certificate cert = chain.get(0); - Scp11KeyParams params = new Scp11KeyParams(ref, cert.getPublicKey()); - - state.withSecurityDomain(session -> { - BadResponseException e = - assertThrows(BadResponseException.class, () -> session.authenticate(params)); - assertEquals("Receipt does not match", e.getMessage()); - }); - } - - public static void testScp11bImport(SecurityDomainTestState state) throws Throwable { - final KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x2); - - ScpKeyParams keyParams = state.withSecurityDomain(session -> { - session.authenticate(defaultKeyParams); - KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); - ECGenParameterSpec ecParams = new ECGenParameterSpec("secp256r1"); - kpg.initialize(ecParams); - KeyPair keyPair = kpg.generateKeyPair(); - session.putKey(ref, PrivateKeyValues.fromPrivateKey(keyPair.getPrivate()), 0); - return new Scp11KeyParams(ref, keyPair.getPublic()); + } + + public static void testScp11bImport(SecurityDomainTestState state) throws Throwable { + final KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x2); + + ScpKeyParams keyParams = + state.withSecurityDomain( + session -> { + session.authenticate(defaultKeyParams); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); + ECGenParameterSpec ecParams = new ECGenParameterSpec("secp256r1"); + kpg.initialize(ecParams); + KeyPair keyPair = kpg.generateKeyPair(); + session.putKey(ref, PrivateKeyValues.fromPrivateKey(keyPair.getPrivate()), 0); + return new Scp11KeyParams(ref, keyPair.getPublic()); + }); + + state.withSecurityDomain( + session -> { + session.authenticate(keyParams); }); - - state.withSecurityDomain(session -> { - session.authenticate(keyParams); + } + + private static void verifyScp11bAuth(SecurityDomainSession session) + throws BadResponseException, ApduException, IOException { + KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x7f); + session.generateEcKey(ref, 0); + session.deleteKey(ref, false); + } + + public static void testScp11cAuthenticate(SecurityDomainTestState state) throws Throwable { + final byte kvn = 0x03; + + ScpKeyParams keyParams = + state.withSecurityDomain( + defaultKeyParams, + session -> { + return loadKeys(session, ScpKid.SCP11c, kvn); + }); + + state.withSecurityDomain( + keyParams, + session -> { + assertThrows( + ApduException.class, () -> session.deleteKey(new KeyRef(ScpKid.SCP11c, kvn), false)); }); - } + } - private static void verifyScp11bAuth(SecurityDomainSession session) - throws BadResponseException, ApduException, IOException { - KeyRef ref = new KeyRef(ScpKid.SCP11b, (byte) 0x7f); - session.generateEcKey(ref, 0); - session.deleteKey(ref, false); - } + private static ScpKeyParams importScp03Key(SecurityDomainTestState state) throws Throwable { + assumeFalse( + "SCP03 management not supported over NFC on FIPS capable devices", + state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); - public static void testScp11cAuthenticate(SecurityDomainTestState state) throws Throwable { - final byte kvn = 0x03; + final KeyRef scp03Ref = new KeyRef((byte) 0x01, (byte) 0x01); - ScpKeyParams keyParams = state.withSecurityDomain(defaultKeyParams, session -> { - return loadKeys(session, ScpKid.SCP11c, kvn); - }); + final StaticKeys staticKeys = + new StaticKeys( + RandomUtils.getRandomBytes(16), + RandomUtils.getRandomBytes(16), + RandomUtils.getRandomBytes(16)); - state.withSecurityDomain(keyParams, session -> { - assertThrows(ApduException.class, () -> - session.deleteKey(new KeyRef(ScpKid.SCP11c, kvn), false)); + state.withSecurityDomain( + session -> { + session.authenticate(defaultKeyParams); + session.putKey(scp03Ref, staticKeys, 0); }); - } - private static ScpKeyParams importScp03Key(SecurityDomainTestState state) throws Throwable { - assumeFalse("SCP03 management not supported over NFC on FIPS capable devices", - state.getDeviceInfo().getFipsCapable() != 0 && !state.isUsbTransport()); + return new Scp03KeyParams(scp03Ref, staticKeys); + } - final KeyRef scp03Ref = new KeyRef((byte) 0x01, (byte) 0x01); - - final StaticKeys staticKeys = new StaticKeys( - RandomUtils.getRandomBytes(16), - RandomUtils.getRandomBytes(16), - RandomUtils.getRandomBytes(16) - ); - - state.withSecurityDomain(session -> { - session.authenticate(defaultKeyParams); - session.putKey(scp03Ref, staticKeys, 0); - }); - - return new Scp03KeyParams(scp03Ref, staticKeys); + @SuppressWarnings({"unchecked", "SameParameterValue"}) + private static ScpCertificates getOceCertificates(byte[] pem) + throws CertificateException, IOException { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + try (InputStream is = new ByteArrayInputStream(pem)) { + return ScpCertificates.from( + (List) certificateFactory.generateCertificates(is)); } + } - @SuppressWarnings({"unchecked", "SameParameterValue"}) - private static ScpCertificates getOceCertificates(byte[] pem) - throws CertificateException, IOException { - CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - try (InputStream is = new ByteArrayInputStream(pem)) { - return ScpCertificates.from((List) certificateFactory.generateCertificates(is)); - } + private static byte[] getSki(X509Certificate certificate) { + byte[] skiExtensionValue = certificate.getExtensionValue("2.5.29.14"); + if (skiExtensionValue == null) { + return null; } - - private static byte[] getSki(X509Certificate certificate) { - byte[] skiExtensionValue = certificate.getExtensionValue("2.5.29.14"); - if (skiExtensionValue == null) { - return null; - } - assertNotNull("Missing Subject Key Identifier", skiExtensionValue); - Tlv tlv = Tlv.parse(skiExtensionValue); - assertEquals("Invalid extension value", 0x04, tlv.getTag()); - Tlv digest = Tlv.parse(tlv.getValue()); - assertEquals("Invalid Subject Key Identifier", 0x04, digest.getTag()); - return digest.getValue(); + assertNotNull("Missing Subject Key Identifier", skiExtensionValue); + Tlv tlv = Tlv.parse(skiExtensionValue); + assertEquals("Invalid extension value", 0x04, tlv.getTag()); + Tlv digest = Tlv.parse(tlv.getValue()); + assertEquals("Invalid Subject Key Identifier", 0x04, digest.getTag()); + return digest.getValue(); + } + + private static List getCertificateChain(KeyStore keyStore, String alias) + throws KeyStoreException { + Certificate[] chain = keyStore.getCertificateChain(alias); + final List certificateChain = new ArrayList<>(); + for (Certificate cert : chain) { + if (cert instanceof X509Certificate) { + certificateChain.add((X509Certificate) cert); + } } + return certificateChain; + } - private static List getCertificateChain(KeyStore keyStore, String alias) - throws KeyStoreException { - Certificate[] chain = keyStore.getCertificateChain(alias); - final List certificateChain = new ArrayList<>(); - for (Certificate cert : chain) { - if (cert instanceof X509Certificate) { - certificateChain.add((X509Certificate) cert); - } - } - return certificateChain; - } - - private static Scp11KeyParams loadKeys(SecurityDomainSession session, byte kid, byte kvn) - throws Throwable { - KeyRef sessionRef = new KeyRef(kid, kvn); - KeyRef oceRef = new KeyRef(OCE_KID, kvn); + private static Scp11KeyParams loadKeys(SecurityDomainSession session, byte kid, byte kvn) + throws Throwable { + KeyRef sessionRef = new KeyRef(kid, kvn); + KeyRef oceRef = new KeyRef(OCE_KID, kvn); - PublicKeyValues publicKeyValues = session.generateEcKey(sessionRef, 0); + PublicKeyValues publicKeyValues = session.generateEcKey(sessionRef, 0); - ScpCertificates oceCerts = getOceCertificates(OCE_CERTS); - assertNotNull("Missing CA", oceCerts.ca); - session.putKey(oceRef, PublicKeyValues.fromPublicKey(oceCerts.ca.getPublicKey()), 0); + ScpCertificates oceCerts = getOceCertificates(OCE_CERTS); + assertNotNull("Missing CA", oceCerts.ca); + session.putKey(oceRef, PublicKeyValues.fromPublicKey(oceCerts.ca.getPublicKey()), 0); - byte[] ski = getSki(oceCerts.ca); - assertNotNull("CA certificate missing Subject Key Identifier", ski); - session.storeCaIssuer(oceRef, ski); + byte[] ski = getSki(oceCerts.ca); + assertNotNull("CA certificate missing Subject Key Identifier", ski); + session.storeCaIssuer(oceRef, ski); - KeyStore keyStore = KeyStore.getInstance("PKCS12"); + KeyStore keyStore = KeyStore.getInstance("PKCS12"); - try (InputStream is = new ByteArrayInputStream(OCE)) { - keyStore.load(is, OCE_PASSWORD); + try (InputStream is = new ByteArrayInputStream(OCE)) { + keyStore.load(is, OCE_PASSWORD); - PrivateKey sk = getPrivateKey(keyStore); - ScpCertificates certs = getCertificates(keyStore); + PrivateKey sk = getPrivateKey(keyStore); + ScpCertificates certs = getCertificates(keyStore); - List certChain = new ArrayList<>(certs.bundle); - if (certs.leaf != null) { - certChain.add(certs.leaf); - } + List certChain = new ArrayList<>(certs.bundle); + if (certs.leaf != null) { + certChain.add(certs.leaf); + } - return new Scp11KeyParams( - sessionRef, - publicKeyValues.toPublicKey(), - oceRef, - sk, - certChain - ); - } + return new Scp11KeyParams(sessionRef, publicKeyValues.toPublicKey(), oceRef, sk, certChain); } + } - static PrivateKey getPrivateKey(KeyStore keyStore) throws Throwable { - final Enumeration aliases = keyStore.aliases(); - assertTrue(aliases.hasMoreElements()); - String alias = keyStore.aliases().nextElement(); - assertTrue(keyStore.isKeyEntry(alias)); + static PrivateKey getPrivateKey(KeyStore keyStore) throws Throwable { + final Enumeration aliases = keyStore.aliases(); + assertTrue(aliases.hasMoreElements()); + String alias = keyStore.aliases().nextElement(); + assertTrue(keyStore.isKeyEntry(alias)); - Key sk = keyStore.getKey(keyStore.aliases().nextElement(), OCE_PASSWORD); - assertTrue("No private key in pkcs12", sk instanceof PrivateKey); + Key sk = keyStore.getKey(keyStore.aliases().nextElement(), OCE_PASSWORD); + assertTrue("No private key in pkcs12", sk instanceof PrivateKey); - return (PrivateKey) sk; - } + return (PrivateKey) sk; + } - static ScpCertificates getCertificates(KeyStore keyStore) throws Throwable { - final Enumeration aliases = keyStore.aliases(); - assertTrue(aliases.hasMoreElements()); - String alias = keyStore.aliases().nextElement(); - assertTrue(keyStore.isKeyEntry(alias)); + static ScpCertificates getCertificates(KeyStore keyStore) throws Throwable { + final Enumeration aliases = keyStore.aliases(); + assertTrue(aliases.hasMoreElements()); + String alias = keyStore.aliases().nextElement(); + assertTrue(keyStore.isKeyEntry(alias)); - Key sk = keyStore.getKey(keyStore.aliases().nextElement(), OCE_PASSWORD); - assertTrue("No private key in pkcs12", sk instanceof PrivateKey); + Key sk = keyStore.getKey(keyStore.aliases().nextElement(), OCE_PASSWORD); + assertTrue("No private key in pkcs12", sk instanceof PrivateKey); - return ScpCertificates.from(getCertificateChain(keyStore, alias)); - } + return ScpCertificates.from(getCertificateChain(keyStore, alias)); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11TestData.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11TestData.java index 40546cb0..8e36e627 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11TestData.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/Scp11TestData.java @@ -17,74 +17,78 @@ package com.yubico.yubikit.testing.sd; import com.yubico.yubikit.core.internal.codec.Base64; - import java.nio.charset.StandardCharsets; class Scp11TestData { - @SuppressWarnings("SpellCheckingInspection") - static final byte[] OCE_CERTS = ("-----BEGIN CERTIFICATE-----\n" + - "MIIB8DCCAZegAwIBAgIUf0lxsK1R+EydqZKLLV/vXhaykgowCgYIKoZIzj0EAwIw\n" + - "KjEoMCYGA1UEAwwfRXhhbXBsZSBPQ0UgUm9vdCBDQSBDZXJ0aWZpY2F0ZTAeFw0y\n" + - "NDA1MjgwOTIyMDlaFw0yNDA4MjYwOTIyMDlaMC8xLTArBgNVBAMMJEV4YW1wbGUg\n" + - "T0NFIEludGVybWVkaWF0ZSBDZXJ0aWZpY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49\n" + - "AwEHA0IABMXbjb+Y33+GP8qUznrdZSJX9b2qC0VUS1WDhuTlQUfg/RBNFXb2/qWt\n" + - "h/a+Ag406fV7wZW2e4PPH+Le7EwS1nyjgZUwgZIwHQYDVR0OBBYEFJzdQCINVBES\n" + - "R4yZBN2l5CXyzlWsMB8GA1UdIwQYMBaAFDGqVWafYGfoHzPc/QT+3nPlcZ89MBIG\n" + - "A1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgIEMCwGA1UdIAEB/wQiMCAw\n" + - "DgYMKoZIhvxrZAAKAgEoMA4GDCqGSIb8a2QACgIBADAKBggqhkjOPQQDAgNHADBE\n" + - "AiBE5SpNEKDW3OehDhvTKT9g1cuuIyPdaXGLZ3iX0x0VcwIgdnIirhlKocOKGXf9\n" + - "ijkE8e+9dTazSPLf24lSIf0IGC8=\n" + - "-----END CERTIFICATE-----\n" + - "-----BEGIN CERTIFICATE-----\n" + - "MIIB2zCCAYGgAwIBAgIUSf59wIpCKOrNGNc5FMPTD9zDGVAwCgYIKoZIzj0EAwIw\n" + - "KjEoMCYGA1UEAwwfRXhhbXBsZSBPQ0UgUm9vdCBDQSBDZXJ0aWZpY2F0ZTAeFw0y\n" + - "NDA1MjgwOTIyMDlaFw0yNDA2MjcwOTIyMDlaMCoxKDAmBgNVBAMMH0V4YW1wbGUg\n" + - "T0NFIFJvb3QgQ0EgQ2VydGlmaWNhdGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC\n" + - "AASPrxfpSB/AvuvLKaCz1YTx68Xbtx8S9xAMfRGwzp5cXMdF8c7AWpUfeM3BQ26M\n" + - "h0WPvyBJKhCdeK8iVCaHyr5Jo4GEMIGBMB0GA1UdDgQWBBQxqlVmn2Bn6B8z3P0E\n" + - "/t5z5XGfPTASBgNVHRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIBBjA8BgNV\n" + - "HSABAf8EMjAwMA4GDCqGSIb8a2QACgIBFDAOBgwqhkiG/GtkAAoCASgwDgYMKoZI\n" + - "hvxrZAAKAgEAMAoGCCqGSM49BAMCA0gAMEUCIHv8cgOzxq2n1uZktL9gCXSR85mk\n" + - "TieYeSoKZn6MM4rOAiEA1S/+7ez/gxDl01ztKeoHiUiW4FbEG4JUCzIITaGxVvM=\n" + - "-----END CERTIFICATE-----").getBytes(StandardCharsets.UTF_8); + @SuppressWarnings("SpellCheckingInspection") + static final byte[] OCE_CERTS = + ("-----BEGIN CERTIFICATE-----\n" + + "MIIB8DCCAZegAwIBAgIUf0lxsK1R+EydqZKLLV/vXhaykgowCgYIKoZIzj0EAwIw\n" + + "KjEoMCYGA1UEAwwfRXhhbXBsZSBPQ0UgUm9vdCBDQSBDZXJ0aWZpY2F0ZTAeFw0y\n" + + "NDA1MjgwOTIyMDlaFw0yNDA4MjYwOTIyMDlaMC8xLTArBgNVBAMMJEV4YW1wbGUg\n" + + "T0NFIEludGVybWVkaWF0ZSBDZXJ0aWZpY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49\n" + + "AwEHA0IABMXbjb+Y33+GP8qUznrdZSJX9b2qC0VUS1WDhuTlQUfg/RBNFXb2/qWt\n" + + "h/a+Ag406fV7wZW2e4PPH+Le7EwS1nyjgZUwgZIwHQYDVR0OBBYEFJzdQCINVBES\n" + + "R4yZBN2l5CXyzlWsMB8GA1UdIwQYMBaAFDGqVWafYGfoHzPc/QT+3nPlcZ89MBIG\n" + + "A1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgIEMCwGA1UdIAEB/wQiMCAw\n" + + "DgYMKoZIhvxrZAAKAgEoMA4GDCqGSIb8a2QACgIBADAKBggqhkjOPQQDAgNHADBE\n" + + "AiBE5SpNEKDW3OehDhvTKT9g1cuuIyPdaXGLZ3iX0x0VcwIgdnIirhlKocOKGXf9\n" + + "ijkE8e+9dTazSPLf24lSIf0IGC8=\n" + + "-----END CERTIFICATE-----\n" + + "-----BEGIN CERTIFICATE-----\n" + + "MIIB2zCCAYGgAwIBAgIUSf59wIpCKOrNGNc5FMPTD9zDGVAwCgYIKoZIzj0EAwIw\n" + + "KjEoMCYGA1UEAwwfRXhhbXBsZSBPQ0UgUm9vdCBDQSBDZXJ0aWZpY2F0ZTAeFw0y\n" + + "NDA1MjgwOTIyMDlaFw0yNDA2MjcwOTIyMDlaMCoxKDAmBgNVBAMMH0V4YW1wbGUg\n" + + "T0NFIFJvb3QgQ0EgQ2VydGlmaWNhdGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC\n" + + "AASPrxfpSB/AvuvLKaCz1YTx68Xbtx8S9xAMfRGwzp5cXMdF8c7AWpUfeM3BQ26M\n" + + "h0WPvyBJKhCdeK8iVCaHyr5Jo4GEMIGBMB0GA1UdDgQWBBQxqlVmn2Bn6B8z3P0E\n" + + "/t5z5XGfPTASBgNVHRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIBBjA8BgNV\n" + + "HSABAf8EMjAwMA4GDCqGSIb8a2QACgIBFDAOBgwqhkiG/GtkAAoCASgwDgYMKoZI\n" + + "hvxrZAAKAgEAMAoGCCqGSM49BAMCA0gAMEUCIHv8cgOzxq2n1uZktL9gCXSR85mk\n" + + "TieYeSoKZn6MM4rOAiEA1S/+7ez/gxDl01ztKeoHiUiW4FbEG4JUCzIITaGxVvM=\n" + + "-----END CERTIFICATE-----") + .getBytes(StandardCharsets.UTF_8); + + // PKCS12 certificate with a private key and full certificate chain + @SuppressWarnings("SpellCheckingInspection") + static byte[] OCE = + Base64.fromUrlSafeString( + "MIIIfAIBAzCCCDIGCSqGSIb3DQEHAaCCCCMEgggfMIIIGzCCBtIGCSqGSIb3DQEHBqCCBsMwgga_AgEAMIIGuAY" + + "JKoZIhvcNAQcBMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAg8IcJO44iSgAICCAAwDAYIKoZI" + + "hvcNAgkFADAdBglghkgBZQMEASoEEAllIHdoQx_USA3jmRMeciiAggZQAHCPJ5lzPV0Z5tnssXZZ1AWm8" + + "AcKEq28gWUTVqVxc-0EcbKQHig1Jx7rqC3q4G4sboIRw1vDH6q5O8eGsbkeNuYBim8fZ08JrsjeJABJoE" + + "iJrPqplMWA7H6a7athg3YSu1v4OR3UKN5Gyzn3s0Yx5yMm_xzw204TEK5_1LpK8AMcUliFSq7jw3Xl1RY" + + "0zjMSWyQjX0KmB9IdubqQCfhy8zkKluAQADtHsEYAn0F3LoMETQytyUSkIvGMZoFemkCWV7zZ5n5IPhXL" + + "7gvnTu0WS8UxEnz_-FYdF43cjmwGfSb3OpaxOND4PBCpwzbFfVCLa6mUBlwq1KQWRm1-PFm4LnL-3s2mx" + + "fjJAsVYP4U722_FHpW8rdTsyvdift9lsQjas2jIjCu8PFClFZJLQldu5FxOhKzx2gsjYS_aeTdefwjlRi" + + "GtEFSrE1snKBbnBeRYFocBjhTD_sy3Vj0i5sbWwTx7iq67joWydWAMp_lGSZ6akWRsyku_282jlwYsc3p" + + "R05qCHkbV0TzJcZofhXBwRgH5NKfulnJ1gH-i3e3RT3TauAKlqCeAfvDvA3-jxEDy_puPncod7WH0m9P4" + + "OmXjZ0s5EI4U-v6bKPgL7LlTCEI6yj15P7kxmruoxZlDAmhixVmlwJ8ZbVxD6Q-AOhXYPg-il3AYaRAS-" + + "VyJla0K-ac6hpYVAnbZCPzgHVkKC6iq4a_azf2b4uq9ks109jjnryAChdBsGdmStpZaPW4koMSAIJf12v" + + "GRp5jNjSaxaIL5QxTn0WCO8FHi1oqTmlTSWvR8wwZLiBmqQtnNTpewiLL7C22lerUT7pYvKLCq_nnPYtb" + + "5UrSTHrmTNOUzEGVOSAGUWV293S4yiPGIwxT3dPE5_UaU_yKq1RonMRaPhOZEESZEwLKVCqyDVEbAt7Hd" + + "ahp-Ex0FVrC5JQhpVQ0Wn6uCptF2Jup70u-P2kVWjxrGBuRrlgEkKuHcohWoO9EMX_bLK9KcY4s1ofnfg" + + "SNagsAyX7N51Bmahgz1MCFOEcuFa375QYQhqkyLO2ZkNTpFQtjHjX0izZWO55LN3rNpcD9-fZt6ldoZCp" + + "g-t6y5xqHy-7soH0BpxF1oGIHAUkYSuXpLY0M7Pt3qqvsJ4_ycmFUEyoGv8Ib_ieUBbebPz0Uhn-jaTpj" + + "gtKCyym7nBxVCuUv39vZ31nhNr4WaFsjdB_FOJh1s4KI6kQgzCSObrIVXBcLCTXPfZ3jWxspKIREHn-zN" + + "uW7jIkbugSRiNFfVArcc7cmU4av9JPSmFiZzeyA0gkrkESTg8DVPT16u7W5HREX4CwmKu-12R6iYQ_po9" + + "Hcy6NJ8ShLdAzU0-q_BzgH7Cb8qimjgfGBA3Mesc-P98FlCzAjB2EgucRuXuehM_FemmZyNl0qI1Mj9qO" + + "gx_HeYaJaYD-yXwojApmetFGtDtMJsDxwL0zK7eGXeHHa7pd7OybKdSjDq25CCTOZvfR0DD55FDIGCy0F" + + "sJTcferzPFlkz_Q45vEwuGfEBnXXS9IhH4ySvJmDmyfLMGiHW6t-9gjyEEg-dwSOq9yXYScfCsefRl7-o" + + "_9nDoNQ8s_XS7LKlJ72ZEBaKeAxcm6q4wVwUWITNNl1R3EYAsFBWzYt4Ka9Ob3igVaNfeG9K4pfQqMWcP" + + "pqVp4FuIsEpDWZYuv71s-WMYCs1JMfHbHDUczdRet1Ir2vLDGeWwvci70AzeKvvQ9OwBVESRec6cVrgt3" + + "EJWLey5sXY01WpMm526fwtLolSMpCf-dNePT97nXemQCcr3QXimagHTSGPngG3577FPrSQJl-lCJDYxBF" + + "Ftnd6hq4OcVr5HiNAbLnSjBWbzqxhHMmgoojy4rwtHmrfyVYKXyl-98r-Lobitv2tpnBqmjL6dMPRBOJv" + + "Ql8-Wp4MGBsi1gvTgW_-pLlMXT--1iYyxBeK9_AN5hfjtrivewE3JY531jwkrl3rUl50MKwBJMMAtQQIY" + + "rDg7DAg_-QcOi-2mgo9zJPzR2jIXF0wP-9FA4-MITa2v78QVXcesh63agcFJCayGAL1StnbSBvvDqK5vE" + + "ei3uGZbeJEpU1hikQx57w3UzS9O7OSQMFvRBOrFBQsYC4JzfF0soIweGNpJxpm-UNYz-hB9vCb8-3OHA0" + + "69M0CAlJVOTF9uEpLVRzK-1kwggFBBgkqhkiG9w0BBwGgggEyBIIBLjCCASowggEmBgsqhkiG9w0BDAoB" + + "AqCB7zCB7DBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIexxrwNlHM34CAggAMAwGCCqGSIb3D" + + "QIJBQAwHQYJYIZIAWUDBAEqBBAkK96h6gHJglyJl1_yEylvBIGQh62z7u5RoQ9y5wIXbE3_oMQTKVfCSr" + + "tqGUmj38sxDY7yIoTVQq7sw0MPNeYHROgGUAzawU0DlXMGuOWrbgzYeURZs0_HZ2Cqk8qhVnD8TgpB2n0" + + "U0NB7aJRHlkzTl5MLFAwn3NE49CSzb891lGwfLYXYCfNfqltD7xZ7uvz6JAo_y6UtY8892wrRv4Udejyf" + + "MSUwIwYJKoZIhvcNAQkVMRYEFJBU0s1_6SLbIRbyeq65gLWqClWNMEEwMTANBglghkgBZQMEAgEFAAQgq" + + "kOJRTcBlnx5yn57k23PH-qUXUGPEuYkrGy-DzEQiikECB0BXjHOZZhuAgIIAA=="); - // PKCS12 certificate with a private key and full certificate chain - @SuppressWarnings("SpellCheckingInspection") - static byte[] OCE = Base64.fromUrlSafeString("MIIIfAIBAzCCCDIGCSqGSIb3DQEHAaCCCCMEgggfMIIIGz" + - "CCBtIGCSqGSIb3DQEHBqCCBsMwgga_AgEAMIIGuAYJKoZIhvcNAQcBMFcGCSqGSIb3DQEFDTBKMCkGCSqGS" + - "Ib3DQEFDDAcBAg8IcJO44iSgAICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEAllIHdoQx_USA3j" + - "mRMeciiAggZQAHCPJ5lzPV0Z5tnssXZZ1AWm8AcKEq28gWUTVqVxc-0EcbKQHig1Jx7rqC3q4G4sboIRw1v" + - "DH6q5O8eGsbkeNuYBim8fZ08JrsjeJABJoEiJrPqplMWA7H6a7athg3YSu1v4OR3UKN5Gyzn3s0Yx5yMm_x" + - "zw204TEK5_1LpK8AMcUliFSq7jw3Xl1RY0zjMSWyQjX0KmB9IdubqQCfhy8zkKluAQADtHsEYAn0F3LoMET" + - "QytyUSkIvGMZoFemkCWV7zZ5n5IPhXL7gvnTu0WS8UxEnz_-FYdF43cjmwGfSb3OpaxOND4PBCpwzbFfVCL" + - "a6mUBlwq1KQWRm1-PFm4LnL-3s2mxfjJAsVYP4U722_FHpW8rdTsyvdift9lsQjas2jIjCu8PFClFZJLQld" + - "u5FxOhKzx2gsjYS_aeTdefwjlRiGtEFSrE1snKBbnBeRYFocBjhTD_sy3Vj0i5sbWwTx7iq67joWydWAMp_" + - "lGSZ6akWRsyku_282jlwYsc3pR05qCHkbV0TzJcZofhXBwRgH5NKfulnJ1gH-i3e3RT3TauAKlqCeAfvDvA" + - "3-jxEDy_puPncod7WH0m9P4OmXjZ0s5EI4U-v6bKPgL7LlTCEI6yj15P7kxmruoxZlDAmhixVmlwJ8ZbVxD" + - "6Q-AOhXYPg-il3AYaRAS-VyJla0K-ac6hpYVAnbZCPzgHVkKC6iq4a_azf2b4uq9ks109jjnryAChdBsGdm" + - "StpZaPW4koMSAIJf12vGRp5jNjSaxaIL5QxTn0WCO8FHi1oqTmlTSWvR8wwZLiBmqQtnNTpewiLL7C22ler" + - "UT7pYvKLCq_nnPYtb5UrSTHrmTNOUzEGVOSAGUWV293S4yiPGIwxT3dPE5_UaU_yKq1RonMRaPhOZEESZEw" + - "LKVCqyDVEbAt7Hdahp-Ex0FVrC5JQhpVQ0Wn6uCptF2Jup70u-P2kVWjxrGBuRrlgEkKuHcohWoO9EMX_bL" + - "K9KcY4s1ofnfgSNagsAyX7N51Bmahgz1MCFOEcuFa375QYQhqkyLO2ZkNTpFQtjHjX0izZWO55LN3rNpcD9" + - "-fZt6ldoZCpg-t6y5xqHy-7soH0BpxF1oGIHAUkYSuXpLY0M7Pt3qqvsJ4_ycmFUEyoGv8Ib_ieUBbebPz0" + - "Uhn-jaTpjgtKCyym7nBxVCuUv39vZ31nhNr4WaFsjdB_FOJh1s4KI6kQgzCSObrIVXBcLCTXPfZ3jWxspKI" + - "REHn-zNuW7jIkbugSRiNFfVArcc7cmU4av9JPSmFiZzeyA0gkrkESTg8DVPT16u7W5HREX4CwmKu-12R6iY" + - "Q_po9Hcy6NJ8ShLdAzU0-q_BzgH7Cb8qimjgfGBA3Mesc-P98FlCzAjB2EgucRuXuehM_FemmZyNl0qI1Mj" + - "9qOgx_HeYaJaYD-yXwojApmetFGtDtMJsDxwL0zK7eGXeHHa7pd7OybKdSjDq25CCTOZvfR0DD55FDIGCy0" + - "FsJTcferzPFlkz_Q45vEwuGfEBnXXS9IhH4ySvJmDmyfLMGiHW6t-9gjyEEg-dwSOq9yXYScfCsefRl7-o_" + - "9nDoNQ8s_XS7LKlJ72ZEBaKeAxcm6q4wVwUWITNNl1R3EYAsFBWzYt4Ka9Ob3igVaNfeG9K4pfQqMWcPpqV" + - "p4FuIsEpDWZYuv71s-WMYCs1JMfHbHDUczdRet1Ir2vLDGeWwvci70AzeKvvQ9OwBVESRec6cVrgt3EJWLe" + - "y5sXY01WpMm526fwtLolSMpCf-dNePT97nXemQCcr3QXimagHTSGPngG3577FPrSQJl-lCJDYxBFFtnd6hq" + - "4OcVr5HiNAbLnSjBWbzqxhHMmgoojy4rwtHmrfyVYKXyl-98r-Lobitv2tpnBqmjL6dMPRBOJvQl8-Wp4MG" + - "Bsi1gvTgW_-pLlMXT--1iYyxBeK9_AN5hfjtrivewE3JY531jwkrl3rUl50MKwBJMMAtQQIYrDg7DAg_-Qc" + - "Oi-2mgo9zJPzR2jIXF0wP-9FA4-MITa2v78QVXcesh63agcFJCayGAL1StnbSBvvDqK5vEei3uGZbeJEpU1" + - "hikQx57w3UzS9O7OSQMFvRBOrFBQsYC4JzfF0soIweGNpJxpm-UNYz-hB9vCb8-3OHA069M0CAlJVOTF9uE" + - "pLVRzK-1kwggFBBgkqhkiG9w0BBwGgggEyBIIBLjCCASowggEmBgsqhkiG9w0BDAoBAqCB7zCB7DBXBgkqh" + - "kiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIexxrwNlHM34CAggAMAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUD" + - "BAEqBBAkK96h6gHJglyJl1_yEylvBIGQh62z7u5RoQ9y5wIXbE3_oMQTKVfCSrtqGUmj38sxDY7yIoTVQq7" + - "sw0MPNeYHROgGUAzawU0DlXMGuOWrbgzYeURZs0_HZ2Cqk8qhVnD8TgpB2n0U0NB7aJRHlkzTl5MLFAwn3N" + - "E49CSzb891lGwfLYXYCfNfqltD7xZ7uvz6JAo_y6UtY8892wrRv4UdejyfMSUwIwYJKoZIhvcNAQkVMRYEF" + - "JBU0s1_6SLbIRbyeq65gLWqClWNMEEwMTANBglghkgBZQMEAgEFAAQgqkOJRTcBlnx5yn57k23PH-qUXUGP" + - "EuYkrGy-DzEQiikECB0BXjHOZZhuAgIIAA=="); - static char[] OCE_PASSWORD = "password".toCharArray(); + static char[] OCE_PASSWORD = "password".toCharArray(); } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/ScpCertificates.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/ScpCertificates.java index c300f379..44d7dd9b 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/sd/ScpCertificates.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/ScpCertificates.java @@ -23,89 +23,89 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; - import javax.annotation.Nullable; class ScpCertificates { - @Nullable final X509Certificate ca; - final List bundle; - @Nullable final X509Certificate leaf; - - ScpCertificates(@Nullable X509Certificate ca, List bundle, @Nullable X509Certificate leaf) { - this.ca = ca; - this.bundle = bundle; - this.leaf = leaf; + @Nullable final X509Certificate ca; + final List bundle; + @Nullable final X509Certificate leaf; + + ScpCertificates( + @Nullable X509Certificate ca, List bundle, @Nullable X509Certificate leaf) { + this.ca = ca; + this.bundle = bundle; + this.leaf = leaf; + } + + static ScpCertificates from(@Nullable List certificates) { + if (certificates == null) { + return new ScpCertificates(null, Collections.emptyList(), null); + } + + X509Certificate ca = null; + BigInteger seenSerial = null; + + // order certificates with the Root CA on top + List ordered = new ArrayList<>(); + ordered.add(certificates.get(0)); + certificates.remove(0); + + while (!certificates.isEmpty()) { + X509Certificate head = ordered.get(0); + X509Certificate tail = ordered.get(ordered.size() - 1); + X509Certificate cert = certificates.get(0); + certificates.remove(0); + + if (isIssuedBy(cert, cert)) { + ordered.add(0, cert); + ca = ordered.get(0); + continue; + } + + if (isIssuedBy(cert, tail)) { + ordered.add(cert); + continue; + } + + if (isIssuedBy(head, cert)) { + ordered.add(0, cert); + continue; + } + + if (seenSerial != null && cert.getSerialNumber().equals(seenSerial)) { + fail("Cannot decide the order of " + cert + " in " + ordered); + } + + // this cert could not be ordered, try to process rest of certificates + // but if you see this cert again fail because the cert chain is not complete + certificates.add(cert); + seenSerial = cert.getSerialNumber(); } - static ScpCertificates from(@Nullable List certificates) { - if (certificates == null) { - return new ScpCertificates(null, Collections.emptyList(), null); - } - - X509Certificate ca = null; - BigInteger seenSerial = null; - - // order certificates with the Root CA on top - List ordered = new ArrayList<>(); - ordered.add(certificates.get(0)); - certificates.remove(0); - - while (!certificates.isEmpty()) { - X509Certificate head = ordered.get(0); - X509Certificate tail = ordered.get(ordered.size() - 1); - X509Certificate cert = certificates.get(0); - certificates.remove(0); - - if (isIssuedBy(cert, cert)) { - ordered.add(0, cert); - ca = ordered.get(0); - continue; - } - - if (isIssuedBy(cert, tail)) { - ordered.add(cert); - continue; - } - - if (isIssuedBy(head, cert)) { - ordered.add(0, cert); - continue; - } - - if (seenSerial != null && cert.getSerialNumber().equals(seenSerial)) { - fail("Cannot decide the order of " + cert + " in " + ordered); - } - - // this cert could not be ordered, try to process rest of certificates - // but if you see this cert again fail because the cert chain is not complete - certificates.add(cert); - seenSerial = cert.getSerialNumber(); - } - - // find ca and leaf - if (ca != null) { - ordered.remove(0); - } - - X509Certificate leaf = null; - if (!ordered.isEmpty()) { - X509Certificate lastCert = ordered.get(ordered.size() - 1); - final boolean[] keyUsage = lastCert.getKeyUsage(); - if (keyUsage != null && keyUsage[4]) { - leaf = lastCert; - ordered.remove(leaf); - } - } - - return new ScpCertificates(ca, ordered, leaf); + // find ca and leaf + if (ca != null) { + ordered.remove(0); } - /** - * @param subjectCert the certificate which we test if it is issued by the issuerCert - * @param issuerCert the certificate which should issue the subjectCertificate - * @return true if the subject certificate is issued by the issuer certificate - */ - private static boolean isIssuedBy(X509Certificate subjectCert, X509Certificate issuerCert) { - return subjectCert.getIssuerX500Principal().equals(issuerCert.getSubjectX500Principal()); + X509Certificate leaf = null; + if (!ordered.isEmpty()) { + X509Certificate lastCert = ordered.get(ordered.size() - 1); + final boolean[] keyUsage = lastCert.getKeyUsage(); + if (keyUsage != null && keyUsage[4]) { + leaf = lastCert; + ordered.remove(leaf); + } } + + return new ScpCertificates(ca, ordered, leaf); + } + + /** + * @param subjectCert the certificate which we test if it is issued by the issuerCert + * @param issuerCert the certificate which should issue the subjectCertificate + * @return true if the subject certificate is issued by the issuer certificate + */ + private static boolean isIssuedBy(X509Certificate subjectCert, X509Certificate issuerCert) { + return subjectCert.getIssuerX500Principal().equals(issuerCert.getSubjectX500Principal()); + } } diff --git a/testing/src/main/java/com/yubico/yubikit/testing/sd/SecurityDomainTestState.java b/testing/src/main/java/com/yubico/yubikit/testing/sd/SecurityDomainTestState.java index 2c50e9ba..d4a48f74 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/sd/SecurityDomainTestState.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/sd/SecurityDomainTestState.java @@ -26,96 +26,96 @@ import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; import com.yubico.yubikit.core.smartcard.scp.SecurityDomainSession; import com.yubico.yubikit.testing.TestState; - import java.io.IOException; - import javax.annotation.Nullable; public class SecurityDomainTestState extends TestState { - public static class Builder extends TestState.Builder { - - public Builder(YubiKeyDevice device, UsbPid usbPid) { - super(device, usbPid); - } + public static class Builder extends TestState.Builder { - @Override - public Builder getThis() { - return this; - } + public Builder(YubiKeyDevice device, UsbPid usbPid) { + super(device, usbPid); + } - public SecurityDomainTestState build() throws Throwable { - return new SecurityDomainTestState(this); - } + @Override + public Builder getThis() { + return this; } - protected SecurityDomainTestState(Builder builder) throws Throwable { - super(builder); + public SecurityDomainTestState build() throws Throwable { + return new SecurityDomainTestState(this); + } + } - try (SmartCardConnection connection = openSmartCardConnection()) { - assumeTrue("Key does not support smart card connection", connection != null); - SecurityDomainSession sd = getSecurityDomainSession(connection); - assumeTrue("Security domain not supported", sd != null); - assertNull("These tests expect kid to be null", scpParameters.getKid()); - } + protected SecurityDomainTestState(Builder builder) throws Throwable { + super(builder); + try (SmartCardConnection connection = openSmartCardConnection()) { + assumeTrue("Key does not support smart card connection", connection != null); + SecurityDomainSession sd = getSecurityDomainSession(connection); + assumeTrue("Security domain not supported", sd != null); + assertNull("These tests expect kid to be null", scpParameters.getKid()); } + } - public void withDeviceCallback(StatefulDeviceCallback callback) throws Throwable { - callback.invoke(this); - } + public void withDeviceCallback(StatefulDeviceCallback callback) + throws Throwable { + callback.invoke(this); + } - public void withSecurityDomain(SessionCallback callback) throws Throwable { - try (SmartCardConnection connection = openSmartCardConnection()) { - callback.invoke(getSecurityDomainSession(connection)); - } - reconnect(); + public void withSecurityDomain(SessionCallback callback) throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + callback.invoke(getSecurityDomainSession(connection)); } - - public R withSecurityDomain(SessionCallbackT callback) throws Throwable { - R result; - try (SmartCardConnection connection = openSmartCardConnection()) { - result = callback.invoke(getSecurityDomainSession(connection)); - } - reconnect(); - return result; + reconnect(); + } + + public R withSecurityDomain(SessionCallbackT callback) + throws Throwable { + R result; + try (SmartCardConnection connection = openSmartCardConnection()) { + result = callback.invoke(getSecurityDomainSession(connection)); } - - public void withSecurityDomain(ScpKeyParams scpKeyParams, SessionCallback callback) throws Throwable { - try (SmartCardConnection connection = openSmartCardConnection()) { - callback.invoke(getSecurityDomainSession(scpKeyParams, connection)); - } - reconnect(); + reconnect(); + return result; + } + + public void withSecurityDomain( + ScpKeyParams scpKeyParams, SessionCallback callback) throws Throwable { + try (SmartCardConnection connection = openSmartCardConnection()) { + callback.invoke(getSecurityDomainSession(scpKeyParams, connection)); } - - public R withSecurityDomain(ScpKeyParams scpKeyParams, SessionCallbackT callback) throws Throwable { - R result; - try (SmartCardConnection connection = openSmartCardConnection()) { - result = callback.invoke(getSecurityDomainSession(scpKeyParams, connection)); - } - reconnect(); - return result; + reconnect(); + } + + public R withSecurityDomain( + ScpKeyParams scpKeyParams, SessionCallbackT callback) + throws Throwable { + R result; + try (SmartCardConnection connection = openSmartCardConnection()) { + result = callback.invoke(getSecurityDomainSession(scpKeyParams, connection)); } - - @Nullable - protected SecurityDomainSession getSecurityDomainSession(SmartCardConnection connection) - throws IOException { - try { - return new SecurityDomainSession(connection, scpParameters.getKeyParams()); - } catch (ApplicationNotAvailableException ignored) { - // no Security Domain support - } - return null; + reconnect(); + return result; + } + + @Nullable protected SecurityDomainSession getSecurityDomainSession(SmartCardConnection connection) + throws IOException { + try { + return new SecurityDomainSession(connection, scpParameters.getKeyParams()); + } catch (ApplicationNotAvailableException ignored) { + // no Security Domain support } - - @Nullable - public static SecurityDomainSession getSecurityDomainSession(ScpKeyParams scpKeyParams, SmartCardConnection connection) - throws IOException { - try { - return new SecurityDomainSession(connection, scpKeyParams); - } catch (ApplicationNotAvailableException ignored) { - // no Security Domain support - } - return null; + return null; + } + + @Nullable public static SecurityDomainSession getSecurityDomainSession( + ScpKeyParams scpKeyParams, SmartCardConnection connection) throws IOException { + try { + return new SecurityDomainSession(connection, scpKeyParams); + } catch (ApplicationNotAvailableException ignored) { + // no Security Domain support } + return null; + } } diff --git a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/BaseSlotConfiguration.java b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/BaseSlotConfiguration.java index faa8a39e..2346fd18 100755 --- a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/BaseSlotConfiguration.java +++ b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/BaseSlotConfiguration.java @@ -18,7 +18,6 @@ import com.yubico.yubikit.core.Version; import com.yubico.yubikit.core.otp.ChecksumUtils; - import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; @@ -26,141 +25,157 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; - import javax.annotation.Nullable; -abstract class BaseSlotConfiguration> implements SlotConfiguration { - // Config structure - protected static final int FIXED_SIZE = 16; // Max size of fixed field - protected static final int UID_SIZE = 6; // Size of secret ID field - protected static final int KEY_SIZE = 16; // Size of AES key - - private static final int ACC_CODE_SIZE = 6; // Size of access code to re-program device - private static final int CONFIG_SIZE = 52; // Size of config struct (excluding current access code) +abstract class BaseSlotConfiguration> + implements SlotConfiguration { + // Config structure + protected static final int FIXED_SIZE = 16; // Max size of fixed field + protected static final int UID_SIZE = 6; // Size of secret ID field + protected static final int KEY_SIZE = 16; // Size of AES key - protected byte[] fixed = new byte[0]; - protected final byte[] uid = new byte[UID_SIZE]; - protected final byte[] key = new byte[KEY_SIZE]; + private static final int ACC_CODE_SIZE = 6; // Size of access code to re-program device + private static final int CONFIG_SIZE = + 52; // Size of config struct (excluding current access code) - private final Map> flags = new HashMap<>(); + protected byte[] fixed = new byte[0]; + protected final byte[] uid = new byte[UID_SIZE]; + protected final byte[] key = new byte[KEY_SIZE]; - protected BaseSlotConfiguration() { - for (FlagType type : FlagType.values()) { - flags.put(type, new HashSet<>()); - } + private final Map> flags = new HashMap<>(); - updateFlags(EXTFLAG_SERIAL_API_VISIBLE, true); - updateFlags(EXTFLAG_ALLOW_UPDATE, true); + protected BaseSlotConfiguration() { + for (FlagType type : FlagType.values()) { + flags.put(type, new HashSet<>()); } - protected abstract T getThis(); - - protected final byte getFlags(FlagType type) { - byte bits = 0; - for (Flag flag : flags.get(type)) { - bits |= flag.bit; - } - return bits; - } + updateFlags(EXTFLAG_SERIAL_API_VISIBLE, true); + updateFlags(EXTFLAG_ALLOW_UPDATE, true); + } - protected T updateFlags(Flag flag, boolean value) { - Set set = flags.get(flag.type); - if (value) { - set.add(flag); - } else { - set.remove(flag); - } - return getThis(); - } + protected abstract T getThis(); - @Override - public boolean isSupportedBy(Version version) { - if (version.major == 0) { - return true; - } - for (Set flagsOfType : flags.values()) { - for (Flag flag : flagsOfType) { - if (!(flag instanceof NonFailingFlag) && !flag.isSupportedBy(version)) { - return false; - } - } - } - return true; + protected final byte getFlags(FlagType type) { + byte bits = 0; + for (Flag flag : flags.get(type)) { + bits |= flag.bit; } - - @Override - public byte[] getConfig(@Nullable byte[] accCode) { - return buildConfig(fixed, uid, key, getFlags(FlagType.EXT), getFlags(FlagType.TKT), getFlags(FlagType.CFG), accCode); + return bits; + } + + protected T updateFlags(Flag flag, boolean value) { + Set set = flags.get(flag.type); + if (value) { + set.add(flag); + } else { + set.remove(flag); } + return getThis(); + } - public T serialApiVisible(boolean serialApiVisible) { - return updateFlags(EXTFLAG_SERIAL_API_VISIBLE, serialApiVisible); + @Override + public boolean isSupportedBy(Version version) { + if (version.major == 0) { + return true; } - - public T serialUsbVisible(boolean serialUsbVisible) { - return updateFlags(EXTFLAG_SERIAL_USB_VISIBLE, serialUsbVisible); + for (Set flagsOfType : flags.values()) { + for (Flag flag : flagsOfType) { + if (!(flag instanceof NonFailingFlag) && !flag.isSupportedBy(version)) { + return false; + } + } } - - public T allowUpdate(boolean allowUpdate) { - return updateFlags(EXTFLAG_ALLOW_UPDATE, allowUpdate); + return true; + } + + @Override + public byte[] getConfig(@Nullable byte[] accCode) { + return buildConfig( + fixed, + uid, + key, + getFlags(FlagType.EXT), + getFlags(FlagType.TKT), + getFlags(FlagType.CFG), + accCode); + } + + public T serialApiVisible(boolean serialApiVisible) { + return updateFlags(EXTFLAG_SERIAL_API_VISIBLE, serialApiVisible); + } + + public T serialUsbVisible(boolean serialUsbVisible) { + return updateFlags(EXTFLAG_SERIAL_USB_VISIBLE, serialUsbVisible); + } + + public T allowUpdate(boolean allowUpdate) { + return updateFlags(EXTFLAG_ALLOW_UPDATE, allowUpdate); + } + + /** + * Makes the configuration dormant (hidden from use). A dormant configuration needs to be updated + * and the dormant bit removed to be used. + * + * @param dormant if true, the configuration cannot be used + * @return the configuration for chaining + */ + public T dormant(boolean dormant) { + return updateFlags(EXTFLAG_DORMANT, dormant); + } + + /** + * Inverts the behaviour of the led on the YubiKey. + * + * @param invertLed if true, the LED behavior is inverted + * @return the configuration for chaining + */ + public T invertLed(boolean invertLed) { + return updateFlags(EXTFLAG_LED_INV, invertLed); + } + + /** + * When set for slot 1, access to modify slot 2 is blocked (even if slot 2 is empty). + * + * @param protectSlot2 If true, slot 2 cannot be modified. + * @return the configuration for chaining + */ + public T protectSlot2(boolean protectSlot2) { + return updateFlags(TKTFLAG_PROTECT_CFG2, protectSlot2); + } + + static byte[] buildConfig( + byte[] fixed, + byte[] uid, + byte[] key, + byte extFlags, + byte tktFlags, + byte cfgFlags, + @Nullable byte[] accCode) { + if (fixed.length > FIXED_SIZE) { + throw new IllegalArgumentException("Incorrect length for fixed"); } - - /** - * Makes the configuration dormant (hidden from use). A dormant configuration needs to be updated and the dormant - * bit removed to be used. - * - * @param dormant if true, the configuration cannot be used - * @return the configuration for chaining - */ - public T dormant(boolean dormant) { - return updateFlags(EXTFLAG_DORMANT, dormant); + if (uid.length != UID_SIZE) { + throw new IllegalArgumentException("Incorrect length for uid"); } - - /** - * Inverts the behaviour of the led on the YubiKey. - * - * @param invertLed if true, the LED behavior is inverted - * @return the configuration for chaining - */ - public T invertLed(boolean invertLed) { - return updateFlags(EXTFLAG_LED_INV, invertLed); + if (key.length != KEY_SIZE) { + throw new IllegalArgumentException("Incorrect length for key"); } - - /** - * When set for slot 1, access to modify slot 2 is blocked (even if slot 2 is empty). - * - * @param protectSlot2 If true, slot 2 cannot be modified. - * @return the configuration for chaining - */ - public T protectSlot2(boolean protectSlot2) { - return updateFlags(TKTFLAG_PROTECT_CFG2, protectSlot2); + if (accCode != null && accCode.length != ACC_CODE_SIZE) { + throw new IllegalArgumentException("Incorrect length for access code"); } - static byte[] buildConfig(byte[] fixed, byte[] uid, byte[] key, byte extFlags, byte tktFlags, byte cfgFlags, @Nullable byte[] accCode) { - if (fixed.length > FIXED_SIZE) { - throw new IllegalArgumentException("Incorrect length for fixed"); - } - if (uid.length != UID_SIZE) { - throw new IllegalArgumentException("Incorrect length for uid"); - } - if (key.length != KEY_SIZE) { - throw new IllegalArgumentException("Incorrect length for key"); - } - if (accCode != null && accCode.length != ACC_CODE_SIZE) { - throw new IllegalArgumentException("Incorrect length for access code"); - } - - ByteBuffer config = ByteBuffer.allocate(CONFIG_SIZE).order(ByteOrder.LITTLE_ENDIAN); - return config.put(Arrays.copyOf(fixed, FIXED_SIZE)) - .put(uid) - .put(key) - .put(accCode == null ? new byte[ACC_CODE_SIZE] : accCode) - .put((byte) fixed.length) - .put(extFlags) - .put(tktFlags) - .put(cfgFlags) - .putShort((short) 0) // 2 bytes RFU - .putShort((short) ~ChecksumUtils.calculateCrc(config.array(), config.position())) - .array(); - } + ByteBuffer config = ByteBuffer.allocate(CONFIG_SIZE).order(ByteOrder.LITTLE_ENDIAN); + return config + .put(Arrays.copyOf(fixed, FIXED_SIZE)) + .put(uid) + .put(key) + .put(accCode == null ? new byte[ACC_CODE_SIZE] : accCode) + .put((byte) fixed.length) + .put(extFlags) + .put(tktFlags) + .put(cfgFlags) + .putShort((short) 0) // 2 bytes RFU + .putShort((short) ~ChecksumUtils.calculateCrc(config.array(), config.position())) + .array(); + } } diff --git a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/ConfigurationState.java b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/ConfigurationState.java index d6f19bfb..5d71a9fe 100755 --- a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/ConfigurationState.java +++ b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/ConfigurationState.java @@ -18,63 +18,72 @@ import com.yubico.yubikit.core.Version; -/** - * Data object containing the state of slot programming for a YubiKey. - */ +/** Data object containing the state of slot programming for a YubiKey. */ public class ConfigurationState { - private static final byte CONFIG1_VALID = 0x01; /* Bit in touchLevel indicating that configuration 1 is valid (from firmware 2.1) */ - private static final byte CONFIG2_VALID = 0x02; /* Bit in touchLevel indicating that configuration 2 is valid (from firmware 2.1) */ - private static final byte CONFIG1_TOUCH = 0x04; /* Bit in touchLevel indicating that configuration 1 requires touch (from firmware 3.0) */ - private static final byte CONFIG2_TOUCH = 0x08; /* Bit in touchLevel indicating that configuration 2 requires touch (from firmware 3.0) */ - private static final byte CONFIG_LED_INV = 0x10; /* Bit in touchLevel indicating that LED behavior is inverted (EXTFLAG_LED_INV mirror) */ - private static final byte CONFIG_STATUS_MASK = 0x1f; /* Mask for status bits */ + private static final byte CONFIG1_VALID = + 0x01; /* Bit in touchLevel indicating that configuration 1 is valid (from firmware 2.1) */ + private static final byte CONFIG2_VALID = + 0x02; /* Bit in touchLevel indicating that configuration 2 is valid (from firmware 2.1) */ + private static final byte CONFIG1_TOUCH = + 0x04; /* Bit in touchLevel indicating that configuration 1 requires touch (from firmware 3.0) */ + private static final byte CONFIG2_TOUCH = + 0x08; /* Bit in touchLevel indicating that configuration 2 requires touch (from firmware 3.0) */ + private static final byte CONFIG_LED_INV = + 0x10; /* Bit in touchLevel indicating that LED behavior is inverted (EXTFLAG_LED_INV mirror) */ + private static final byte CONFIG_STATUS_MASK = 0x1f; /* Mask for status bits */ - private final Version version; - private final byte flags; + private final Version version; + private final byte flags; - ConfigurationState(Version version, short touchLevel) { - this.version = version; - this.flags = (byte) (CONFIG_STATUS_MASK & touchLevel); - } + ConfigurationState(Version version, short touchLevel) { + this.version = version; + this.flags = (byte) (CONFIG_STATUS_MASK & touchLevel); + } - /** - * Checks if a slot is configured or empty - *

- * This functionality requires support for {@link YubiOtpSession#FEATURE_CHECK_CONFIGURED}, available on YubiKey 2.1 or later. - * - * @param slot the slot to check - * @return true if the slot holds configuration, false if empty - */ - public boolean isConfigured(Slot slot) { - if (YubiOtpSession.FEATURE_CHECK_CONFIGURED.isSupportedBy(version)) { - return (flags & slot.map(CONFIG1_VALID, CONFIG2_VALID)) != 0; - } - throw new UnsupportedOperationException("Checking if a slot is configured is not supported on this YubiKey."); + /** + * Checks if a slot is configured or empty + * + *

This functionality requires support for {@link YubiOtpSession#FEATURE_CHECK_CONFIGURED}, + * available on YubiKey 2.1 or later. + * + * @param slot the slot to check + * @return true if the slot holds configuration, false if empty + */ + public boolean isConfigured(Slot slot) { + if (YubiOtpSession.FEATURE_CHECK_CONFIGURED.isSupportedBy(version)) { + return (flags & slot.map(CONFIG1_VALID, CONFIG2_VALID)) != 0; } + throw new UnsupportedOperationException( + "Checking if a slot is configured is not supported on this YubiKey."); + } - /** - * Checks if a configured slot is triggered by touch or not. - *

- * This functionality requires support for {@link YubiOtpSession#FEATURE_CHECK_TOUCH_TRIGGERED}, available on YubiKey 3.0 or later. - * A slot is triggered by touch if a user-initiated touch of the sensor causes it to output a payload over the keyboard interface. - * Only HMAC-SHA1 challenge-response credentials are not triggered by touch. - * - * @param slot the slot to check - * @return true of the slot is triggered by touch, false if not (or if checking isn't supported) - */ - public boolean isTouchTriggered(Slot slot) { - if (YubiOtpSession.FEATURE_CHECK_TOUCH_TRIGGERED.isSupportedBy(version)) { - return (flags & slot.map(CONFIG1_TOUCH, CONFIG2_TOUCH)) != 0; - } - throw new UnsupportedOperationException("Checking if a slot is triggered by touch is not supported on this YubiKey."); + /** + * Checks if a configured slot is triggered by touch or not. + * + *

This functionality requires support for {@link + * YubiOtpSession#FEATURE_CHECK_TOUCH_TRIGGERED}, available on YubiKey 3.0 or later. A slot is + * triggered by touch if a user-initiated touch of the sensor causes it to output a payload over + * the keyboard interface. Only HMAC-SHA1 challenge-response credentials are not triggered by + * touch. + * + * @param slot the slot to check + * @return true of the slot is triggered by touch, false if not (or if checking isn't supported) + */ + public boolean isTouchTriggered(Slot slot) { + if (YubiOtpSession.FEATURE_CHECK_TOUCH_TRIGGERED.isSupportedBy(version)) { + return (flags & slot.map(CONFIG1_TOUCH, CONFIG2_TOUCH)) != 0; } + throw new UnsupportedOperationException( + "Checking if a slot is triggered by touch is not supported on this YubiKey."); + } - /** - * Checks if the LED has been configured to be inverted. - * - * @return true if inverted, false if not - */ - public boolean isLedInverted() { - return SlotConfiguration.EXTFLAG_LED_INV.isSupportedBy(version) && (flags & CONFIG_LED_INV) != 0; - } + /** + * Checks if the LED has been configured to be inverted. + * + * @return true if inverted, false if not + */ + public boolean isLedInverted() { + return SlotConfiguration.EXTFLAG_LED_INV.isSupportedBy(version) + && (flags & CONFIG_LED_INV) != 0; + } } diff --git a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/HmacSha1SlotConfiguration.java b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/HmacSha1SlotConfiguration.java index e201c8ec..b7eb347f 100755 --- a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/HmacSha1SlotConfiguration.java +++ b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/HmacSha1SlotConfiguration.java @@ -16,70 +16,74 @@ package com.yubico.yubikit.yubiotp; import com.yubico.yubikit.core.application.CommandState; - import java.nio.ByteBuffer; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** - * Configures HMAC-SHA1 challenge response secret on YubiKey - * ({@link YubiOtpSession#calculateHmacSha1(Slot, byte[], CommandState)} how to use it after configuration) + * Configures HMAC-SHA1 challenge response secret on YubiKey ({@link + * YubiOtpSession#calculateHmacSha1(Slot, byte[], CommandState)} how to use it after configuration) */ public class HmacSha1SlotConfiguration extends BaseSlotConfiguration { - private static final int HMAC_KEY_SIZE = 20; // Size of OATH-HOTP key (key field + first 4 of UID field) + private static final int HMAC_KEY_SIZE = + 20; // Size of OATH-HOTP key (key field + first 4 of UID field) - static byte[] shortenHmacSha1Key(byte[] key) { - if (key.length > 64) { - // As per HMAC specification, shorten keys longer than BLOCKSIZE by hashing them. - try { - return MessageDigest.getInstance("SHA1").digest(key); - } catch (NoSuchAlgorithmException e) { - // Shouldn't happen - throw new IllegalStateException(); - } - } - if (key.length > HMAC_KEY_SIZE) { - throw new UnsupportedOperationException("HMAC-SHA1 key lengths >20 bytes are not supported"); - } - return key; + static byte[] shortenHmacSha1Key(byte[] key) { + if (key.length > 64) { + // As per HMAC specification, shorten keys longer than BLOCKSIZE by hashing them. + try { + return MessageDigest.getInstance("SHA1").digest(key); + } catch (NoSuchAlgorithmException e) { + // Shouldn't happen + throw new IllegalStateException(); + } + } + if (key.length > HMAC_KEY_SIZE) { + throw new UnsupportedOperationException("HMAC-SHA1 key lengths >20 bytes are not supported"); } + return key; + } - /** - * Creates a HMAC-SHA1 challenge-response configuration with default settings. - * - * @param secret the 20 bytes HMAC key to store - */ - public HmacSha1SlotConfiguration(byte[] secret) { - // Secret is packed into key and uid - ByteBuffer.wrap(ByteBuffer.allocate(KEY_SIZE + UID_SIZE).put(shortenHmacSha1Key(secret)).array()).get(key).get(uid); + /** + * Creates a HMAC-SHA1 challenge-response configuration with default settings. + * + * @param secret the 20 bytes HMAC key to store + */ + public HmacSha1SlotConfiguration(byte[] secret) { + // Secret is packed into key and uid + ByteBuffer.wrap( + ByteBuffer.allocate(KEY_SIZE + UID_SIZE).put(shortenHmacSha1Key(secret)).array()) + .get(key) + .get(uid); - updateFlags(TKTFLAG_CHAL_RESP, true); - updateFlags(CFGFLAG_CHAL_HMAC, true); - updateFlags(CFGFLAG_HMAC_LT64, true); - } + updateFlags(TKTFLAG_CHAL_RESP, true); + updateFlags(CFGFLAG_CHAL_HMAC, true); + updateFlags(CFGFLAG_HMAC_LT64, true); + } - @Override - protected HmacSha1SlotConfiguration getThis() { - return this; - } + @Override + protected HmacSha1SlotConfiguration getThis() { + return this; + } - /** - * Whether or not to require a user presence check for calculating the response. - * - * @param requireTouch if true, any attempt to calculate a response will cause the YubiKey to require touch (default: false) - * @return the configuration for chaining - */ - public HmacSha1SlotConfiguration requireTouch(boolean requireTouch) { - return updateFlags(CFGFLAG_CHAL_BTN_TRIG, requireTouch); - } + /** + * Whether or not to require a user presence check for calculating the response. + * + * @param requireTouch if true, any attempt to calculate a response will cause the YubiKey to + * require touch (default: false) + * @return the configuration for chaining + */ + public HmacSha1SlotConfiguration requireTouch(boolean requireTouch) { + return updateFlags(CFGFLAG_CHAL_BTN_TRIG, requireTouch); + } - /** - * Whether or not challenges sent to this slot are less than 64 bytes long or not. - * - * @param lt64 if false, all challenges must be exactly 64 bytes long (default: true) - * @return the configuration for chaining - */ - public HmacSha1SlotConfiguration lt64(boolean lt64) { - return updateFlags(CFGFLAG_HMAC_LT64, lt64); - } + /** + * Whether or not challenges sent to this slot are less than 64 bytes long or not. + * + * @param lt64 if false, all challenges must be exactly 64 bytes long (default: true) + * @return the configuration for chaining + */ + public HmacSha1SlotConfiguration lt64(boolean lt64) { + return updateFlags(CFGFLAG_HMAC_LT64, lt64); + } } diff --git a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/HotpSlotConfiguration.java b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/HotpSlotConfiguration.java index e492d617..96ee0234 100755 --- a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/HotpSlotConfiguration.java +++ b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/HotpSlotConfiguration.java @@ -15,76 +15,81 @@ */ package com.yubico.yubikit.yubiotp; +import static com.yubico.yubikit.yubiotp.HmacSha1SlotConfiguration.shortenHmacSha1Key; + import java.nio.ByteBuffer; import java.util.Arrays; -import static com.yubico.yubikit.yubiotp.HmacSha1SlotConfiguration.shortenHmacSha1Key; - -/** - * Configures the YubiKey to return an OATH-HOTP code on touch - */ +/** Configures the YubiKey to return an OATH-HOTP code on touch */ public class HotpSlotConfiguration extends KeyboardSlotConfiguration { - /** - * Creates an OATH-HOTP configuration with default settings. - * - * @param secret the shared secret for the OATH-TOTP credential - */ - public HotpSlotConfiguration(byte[] secret) { - // Secret is packed into key and uid - ByteBuffer.wrap(ByteBuffer.allocate(KEY_SIZE + UID_SIZE).put(shortenHmacSha1Key(secret)).array()).get(key).get(uid); + /** + * Creates an OATH-HOTP configuration with default settings. + * + * @param secret the shared secret for the OATH-TOTP credential + */ + public HotpSlotConfiguration(byte[] secret) { + // Secret is packed into key and uid + ByteBuffer.wrap( + ByteBuffer.allocate(KEY_SIZE + UID_SIZE).put(shortenHmacSha1Key(secret)).array()) + .get(key) + .get(uid); - updateFlags(TKTFLAG_OATH_HOTP, true); - } + updateFlags(TKTFLAG_OATH_HOTP, true); + } - @Override - protected HotpSlotConfiguration getThis() { - return this; - } + @Override + protected HotpSlotConfiguration getThis() { + return this; + } - /** - * If set, output an 8 digit OATH-HOTP code instead of a 6 digit code. - * - * @param digits8 true to use 8 digits of code output. - * @return the configuration for chaining - */ - public HotpSlotConfiguration digits8(boolean digits8) { - return updateFlags(CFGFLAG_OATH_HOTP8, digits8); - } + /** + * If set, output an 8 digit OATH-HOTP code instead of a 6 digit code. + * + * @param digits8 true to use 8 digits of code output. + * @return the configuration for chaining + */ + public HotpSlotConfiguration digits8(boolean digits8) { + return updateFlags(CFGFLAG_OATH_HOTP8, digits8); + } - /** - * Configure OATH token id with a provided value. - * The standard OATH token id for a Yubico YubiKey is (MODHEX) OO=ub, TT=he, (BCD) UUUUUUUU=serial number. - *

- * The reason for the decimal serial number is to make it easy for humans to correlate the serial number on the back of the YubiKey to an entry in a list of associated tokens for example. - *

- * NOTE: If fixedModhex1 and fixedModhex2 are BOTH set, the entire token id will be output in MODHEX. - * - * @param tokenId the raw token ID value - * @param fixedModhex1 output the first byte of the token ID as MODHEX - * @param fixedModhex2 output the first two bytes of the token ID as MODHEX - * @return the configuration for chaining - */ - public HotpSlotConfiguration tokenId(byte[] tokenId, boolean fixedModhex1, boolean fixedModhex2) { - if (tokenId.length > FIXED_SIZE) { - throw new IllegalArgumentException("Token ID must be <= 16 bytes"); - } - fixed = Arrays.copyOf(tokenId, tokenId.length); - updateFlags(CFGFLAG_OATH_FIXED_MODHEX1, fixedModhex1); - return updateFlags(CFGFLAG_OATH_FIXED_MODHEX2, fixedModhex2); + /** + * Configure OATH token id with a provided value. The standard OATH token id for a Yubico YubiKey + * is (MODHEX) OO=ub, TT=he, (BCD) UUUUUUUU=serial number. + * + *

The reason for the decimal serial number is to make it easy for humans to correlate the + * serial number on the back of the YubiKey to an entry in a list of associated tokens for + * example. + * + *

NOTE: If fixedModhex1 and fixedModhex2 are BOTH set, the entire token id will be output in + * MODHEX. + * + * @param tokenId the raw token ID value + * @param fixedModhex1 output the first byte of the token ID as MODHEX + * @param fixedModhex2 output the first two bytes of the token ID as MODHEX + * @return the configuration for chaining + */ + public HotpSlotConfiguration tokenId(byte[] tokenId, boolean fixedModhex1, boolean fixedModhex2) { + if (tokenId.length > FIXED_SIZE) { + throw new IllegalArgumentException("Token ID must be <= 16 bytes"); } + fixed = Arrays.copyOf(tokenId, tokenId.length); + updateFlags(CFGFLAG_OATH_FIXED_MODHEX1, fixedModhex1); + return updateFlags(CFGFLAG_OATH_FIXED_MODHEX2, fixedModhex2); + } - /** - * Set OATH Initial Moving Factor. - * This is the initial counter value for the YubiKey. This should be a value between 0 and 1048560, evenly dividable by 16. - * - * @param imf the initial counter value for the credential - * @return the configuration for chaining - */ - public HotpSlotConfiguration imf(int imf) { - if (imf % 16 != 0 || imf > 0xffff0 || imf < 0) { - throw new IllegalArgumentException("imf should be between 0 and 1048560, evenly dividable by 16"); - } - ByteBuffer.wrap(uid, 4, 2).putShort((short) (imf >> 4)); - return getThis(); + /** + * Set OATH Initial Moving Factor. This is the initial counter value for the YubiKey. This should + * be a value between 0 and 1048560, evenly dividable by 16. + * + * @param imf the initial counter value for the credential + * @return the configuration for chaining + */ + public HotpSlotConfiguration imf(int imf) { + if (imf % 16 != 0 || imf > 0xffff0 || imf < 0) { + throw new IllegalArgumentException( + "imf should be between 0 and 1048560, evenly dividable by 16"); } + ByteBuffer.wrap(uid, 4, 2).putShort((short) (imf >> 4)); + return getThis(); + } } diff --git a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/KeyboardSlotConfiguration.java b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/KeyboardSlotConfiguration.java index 5c813841..2de9c506 100755 --- a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/KeyboardSlotConfiguration.java +++ b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/KeyboardSlotConfiguration.java @@ -15,56 +15,59 @@ */ package com.yubico.yubikit.yubiotp; -abstract class KeyboardSlotConfiguration> extends BaseSlotConfiguration { - protected KeyboardSlotConfiguration() { - // Unchecked defaults, ignored if not supported - updateFlags(TKTFLAG_APPEND_CR, true); - updateFlags(EXTFLAG_FAST_TRIG, true); - } +abstract class KeyboardSlotConfiguration> + extends BaseSlotConfiguration { + protected KeyboardSlotConfiguration() { + // Unchecked defaults, ignored if not supported + updateFlags(TKTFLAG_APPEND_CR, true); + updateFlags(EXTFLAG_FAST_TRIG, true); + } - /** - * Appends a Carriage Return (Enter key press) at the end of the output. - * - * @param appendCr if true, the output of the slot will end with a CR (default: true) - * @return the configuration for chaining - */ - public T appendCr(boolean appendCr) { - return updateFlags(TKTFLAG_APPEND_CR, appendCr); - } + /** + * Appends a Carriage Return (Enter key press) at the end of the output. + * + * @param appendCr if true, the output of the slot will end with a CR (default: true) + * @return the configuration for chaining + */ + public T appendCr(boolean appendCr) { + return updateFlags(TKTFLAG_APPEND_CR, appendCr); + } - /** - * Faster triggering when only slot 1 is configured. - * This option is always in effect on firmware versions 3.0 and above. - * - * @param fastTrigger if true, trigger slot 1 quicker when slot 2 is not configured (default: true) - * @return the configuration for chaining - */ - public T fastTrigger(boolean fastTrigger) { - return updateFlags(EXTFLAG_FAST_TRIG, fastTrigger); - } + /** + * Faster triggering when only slot 1 is configured. This option is always in effect on firmware + * versions 3.0 and above. + * + * @param fastTrigger if true, trigger slot 1 quicker when slot 2 is not configured (default: + * true) + * @return the configuration for chaining + */ + public T fastTrigger(boolean fastTrigger) { + return updateFlags(EXTFLAG_FAST_TRIG, fastTrigger); + } - /** - * Adds a delay between each key press when sending output. - * This may sometimes be needed if the host system isn't able to handle the default speed at which keystrokes are sent. - *

- * NOTE: These two flags can be combined to maximize the delay. - * - * @param pacing10Ms Adds a ~10ms delay between keystrokes (default: false) - * @param pacing20Ms Adds a ~20ms delay between keystrokes (default: false) - * @return the configuration for chaining - */ - public T pacing(boolean pacing10Ms, boolean pacing20Ms) { - updateFlags(CFGFLAG_PACING_10MS, pacing10Ms); - return updateFlags(CFGFLAG_PACING_20MS, pacing20Ms); - } + /** + * Adds a delay between each key press when sending output. This may sometimes be needed if the + * host system isn't able to handle the default speed at which keystrokes are sent. + * + *

NOTE: These two flags can be combined to maximize the delay. + * + * @param pacing10Ms Adds a ~10ms delay between keystrokes (default: false) + * @param pacing20Ms Adds a ~20ms delay between keystrokes (default: false) + * @return the configuration for chaining + */ + public T pacing(boolean pacing10Ms, boolean pacing20Ms) { + updateFlags(CFGFLAG_PACING_10MS, pacing10Ms); + return updateFlags(CFGFLAG_PACING_20MS, pacing20Ms); + } - /** - * Send scancodes for numeric keypad key presses when sending digits - helps with some keyboard layouts. - * - * @param useNumeric true to use the numeric keypad (default: false) - * @return the configuration for chaining - */ - public T useNumeric(boolean useNumeric) { - return updateFlags(EXTFLAG_USE_NUMERIC_KEYPAD, useNumeric); - } + /** + * Send scancodes for numeric keypad key presses when sending digits - helps with some keyboard + * layouts. + * + * @param useNumeric true to use the numeric keypad (default: false) + * @return the configuration for chaining + */ + public T useNumeric(boolean useNumeric) { + return updateFlags(EXTFLAG_USE_NUMERIC_KEYPAD, useNumeric); + } } diff --git a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/Slot.java b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/Slot.java index 6ad8eb48..114fbc66 100755 --- a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/Slot.java +++ b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/Slot.java @@ -16,33 +16,27 @@ package com.yubico.yubikit.yubiotp; -/** - * Slots on YubiKey (Yubico OTP/YubiKey/Configuration interface). - */ +/** Slots on YubiKey (Yubico OTP/YubiKey/Configuration interface). */ public enum Slot { - /** - * Slot one (short touch of YubiKey sensor) - */ - ONE, - /** - * Slot two (long touch of YubiKey sensor) - */ - TWO; + /** Slot one (short touch of YubiKey sensor) */ + ONE, + /** Slot two (long touch of YubiKey sensor) */ + TWO; - /** - * Maps a Slot value to one of two byte values. - * - * @param one the value to use for slot 1 - * @param two the value to use for slot 2 - * @return either one or two, depending on the slot. - */ - byte map(byte one, byte two) { - switch (this) { - case ONE: - return one; - case TWO: - return two; - } - throw new IllegalStateException("Invalid enum value"); + /** + * Maps a Slot value to one of two byte values. + * + * @param one the value to use for slot 1 + * @param two the value to use for slot 2 + * @return either one or two, depending on the slot. + */ + byte map(byte one, byte two) { + switch (this) { + case ONE: + return one; + case TWO: + return two; } + throw new IllegalStateException("Invalid enum value"); + } } diff --git a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/SlotConfiguration.java b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/SlotConfiguration.java index 2cec4c0a..c5dc645f 100755 --- a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/SlotConfiguration.java +++ b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/SlotConfiguration.java @@ -17,101 +17,187 @@ import com.yubico.yubikit.core.Version; import com.yubico.yubikit.core.application.Feature; - import javax.annotation.Nullable; public interface SlotConfiguration { - // Constants in this file come from https://github.com/Yubico/yubikey-personalization/blob/master/ykcore/ykdef.h - - // Yubikey 1 and above - Flag TKTFLAG_TAB_FIRST = new Flag(FlagType.TKT, "TAB_FIRST", 0x01, 1, 0); // Send TAB before first part - Flag TKTFLAG_APPEND_TAB1 = new Flag(FlagType.TKT, "APPEND_TAB1", 0x02, 1, 0); // Send TAB after first part - Flag TKTFLAG_APPEND_TAB2 = new Flag(FlagType.TKT, "APPEND_TAB2", 0x04, 1, 0); // Send TAB after second part - Flag TKTFLAG_APPEND_DELAY1 = new Flag(FlagType.TKT, "APPEND_DELAY1", 0x08, 1, 0); // Add 0.5s delay after first part - Flag TKTFLAG_APPEND_DELAY2 = new Flag(FlagType.TKT, "APPEND_DELAY2", 0x10, 1, 0); // Add 0.5s delay after second part - Flag TKTFLAG_APPEND_CR = new Flag(FlagType.TKT, "APPEND_CR", 0x20, 1, 0); // Append CR as final character - - // Yubikey 2 and above - Flag TKTFLAG_PROTECT_CFG2 = new Flag(FlagType.TKT, "PROTECT_CFG2", 0x80, 2, 0); // Block update of config 2 unless config 2 is configured and has this bit set - - // Configuration flags - - // Yubikey 1 and above - Flag CFGFLAG_SEND_REF = new Flag(FlagType.CFG, "SEND_REF", 0x01, 1, 0); // Send reference string (0..F) before data - Flag CFGFLAG_PACING_10MS = new Flag(FlagType.CFG, "PACING_10MS", 0x04, 1, 0); // Add 10ms intra-key pacing - Flag CFGFLAG_PACING_20MS = new Flag(FlagType.CFG, "PACING_20MS", 0x08, 1, 0); // Add 20ms intra-key pacing - Flag CFGFLAG_STATIC_TICKET = new Flag(FlagType.CFG, "STATIC_TICKET", 0x20, 1, 0); // Static ticket generation - - // Yubikey 1 only - Flag CFGFLAG_TICKET_FIRST = new Flag(FlagType.CFG, "TICKET_FIRST", 0x02, 1, 0); // Send ticket first (default is fixed part) - Flag CFGFLAG_ALLOW_HIDTRIG = new Flag(FlagType.CFG, "ALLOW_HIDTRIG", 0x10, 1, 0); // Allow trigger through HID/keyboard - - // Yubikey 2 and above - Flag CFGFLAG_SHORT_TICKET = new Flag(FlagType.CFG, "SHORT_TICKET", 0x02, 2, 0); // Send truncated ticket (half length) - Flag CFGFLAG_STRONG_PW1 = new Flag(FlagType.CFG, "STRONG_PW1", 0x10, 2, 0); // Strong password policy flag #1 (mixed case) - Flag CFGFLAG_STRONG_PW2 = new Flag(FlagType.CFG, "STRONG_PW2", 0x40, 2, 0); // Strong password policy flag #2 (substitute 0..7 to digits) - Flag CFGFLAG_MAN_UPDATE = new Flag(FlagType.CFG, "MAN_UPDATE", 0x80, 2, 0); // Allow manual (local) update of static OTP - - // Yubikey 2.1 and above - Flag TKTFLAG_OATH_HOTP = new Flag(FlagType.TKT, "OATH_HOTP", 0x40, 2, 1); // OATH HOTP mode - Flag CFGFLAG_OATH_HOTP8 = new Flag(FlagType.CFG, "OATH_HOTP8", 0x02, 2, 1); // Generate 8 digits HOTP rather than 6 digits - Flag CFGFLAG_OATH_FIXED_MODHEX1 = new Flag(FlagType.CFG, "OATH_FIXED_MODHEX1", 0x10, 2, 1); // First byte in fixed part sent as modhex - Flag CFGFLAG_OATH_FIXED_MODHEX2 = new Flag(FlagType.CFG, "OATH_FIXED_MODHEX2", 0x40, 2, 1); // First two bytes in fixed part sent as modhex (if paired with OATH_FIXED_MODHEX1, all of fixed part is sent as modhex) - - // Yubikey 2.2 and above - Flag TKTFLAG_CHAL_RESP = new Flag(FlagType.TKT, "CHAL_RESP", 0x40, 2, 2); // Challenge-response enabled (both must be set) - Flag CFGFLAG_CHAL_YUBICO = new Flag(FlagType.CFG, "CHAL_YUBICO", 0x20, 2, 2); // Challenge-response enabled - Yubico OTP mode - Flag CFGFLAG_CHAL_HMAC = new Flag(FlagType.CFG, "CHAL_HMAC", 0x22, 2, 2); // Challenge-response enabled - HMAC-SHA1 - Flag CFGFLAG_HMAC_LT64 = new Flag(FlagType.CFG, "CHAL_LT64", 0x04, 2, 2); // Set when HMAC message is less than 64 bytes - Flag CFGFLAG_CHAL_BTN_TRIG = new Flag(FlagType.CFG, "CHAL_BTN_TRIG", 0x08, 2, 2); // Challenge-response operation requires button press - - Flag EXTFLAG_SERIAL_BTN_VISIBLE = new Flag(FlagType.EXT, "SERIAL_BTN_VISIBLE", 0x01, 2, 2); // Serial number visible at startup (button press) - Flag EXTFLAG_SERIAL_USB_VISIBLE = new Flag(FlagType.EXT, "SERIAL_USB_VISIBLE", 0x02, 2, 2); // Serial number visible in USB iSerial field - Flag EXTFLAG_SERIAL_API_VISIBLE = new NonFailingFlag(FlagType.EXT, "SERIAL_API_VISIBLE", 0x04, 2, 2); // Serial number visible via API call - - // YubiKey 2.3 and above - Flag EXTFLAG_USE_NUMERIC_KEYPAD = new Flag(FlagType.EXT, "USE_NUMERIC_KEYPAD", 0x08, 2, 3); // Use numeric keypad for digits - Flag EXTFLAG_FAST_TRIG = new NonFailingFlag(FlagType.EXT, "FAST_TRIG", 0x10, 2, 3); // Use fast trig if only cfg1 set - Flag EXTFLAG_ALLOW_UPDATE = new NonFailingFlag(FlagType.EXT, "ALLOW_UPDATE", 0x20, 2, 3); // Allow update of existing configuration (selected flags + access code) - Flag EXTFLAG_DORMANT = new Flag(FlagType.EXT, "DORMANT", 0x40, 2, 3); // Dormant configuration (can be woken up and flag removed = requires update flag) - - // YubiKey 2.4 and 3.1 and above (not 3.0) - Flag EXTFLAG_LED_INV = new Flag(FlagType.EXT, "LED_INV", 0x80, 2, 4); // LED idle state is off rather than on - - /** - * Checks the configuration against a YubiKey firmware version to see if it is supported - * - * @param version the firmware version to check against - * @return true if the given YubiKey version supports this configuration - */ - boolean isSupportedBy(Version version); - - byte[] getConfig(@Nullable byte[] accCode); - - enum FlagType { - TKT, CFG, EXT - } - - /** - * A flag used for slot configuration. - */ - class Flag extends Feature.Versioned { - public final FlagType type; - public final byte bit; - - Flag(FlagType type, String name, int bit, int major, int minor) { - super(type.name() + "FLAG_" + name, major, minor, 0); - this.type = type; - this.bit = (byte) bit; - } + // Constants in this file come from + // https://github.com/Yubico/yubikey-personalization/blob/master/ykcore/ykdef.h + + // Yubikey 1 and above + Flag TKTFLAG_TAB_FIRST = + new Flag(FlagType.TKT, "TAB_FIRST", 0x01, 1, 0); // Send TAB before first part + Flag TKTFLAG_APPEND_TAB1 = + new Flag(FlagType.TKT, "APPEND_TAB1", 0x02, 1, 0); // Send TAB after first part + Flag TKTFLAG_APPEND_TAB2 = + new Flag(FlagType.TKT, "APPEND_TAB2", 0x04, 1, 0); // Send TAB after second part + Flag TKTFLAG_APPEND_DELAY1 = + new Flag(FlagType.TKT, "APPEND_DELAY1", 0x08, 1, 0); // Add 0.5s delay after first part + Flag TKTFLAG_APPEND_DELAY2 = + new Flag(FlagType.TKT, "APPEND_DELAY2", 0x10, 1, 0); // Add 0.5s delay after second part + Flag TKTFLAG_APPEND_CR = + new Flag(FlagType.TKT, "APPEND_CR", 0x20, 1, 0); // Append CR as final character + + // Yubikey 2 and above + Flag TKTFLAG_PROTECT_CFG2 = + new Flag( + FlagType.TKT, + "PROTECT_CFG2", + 0x80, + 2, + 0); // Block update of config 2 unless config 2 is configured and has this bit set + + // Configuration flags + + // Yubikey 1 and above + Flag CFGFLAG_SEND_REF = + new Flag(FlagType.CFG, "SEND_REF", 0x01, 1, 0); // Send reference string (0..F) before data + Flag CFGFLAG_PACING_10MS = + new Flag(FlagType.CFG, "PACING_10MS", 0x04, 1, 0); // Add 10ms intra-key pacing + Flag CFGFLAG_PACING_20MS = + new Flag(FlagType.CFG, "PACING_20MS", 0x08, 1, 0); // Add 20ms intra-key pacing + Flag CFGFLAG_STATIC_TICKET = + new Flag(FlagType.CFG, "STATIC_TICKET", 0x20, 1, 0); // Static ticket generation + + // Yubikey 1 only + Flag CFGFLAG_TICKET_FIRST = + new Flag( + FlagType.CFG, "TICKET_FIRST", 0x02, 1, 0); // Send ticket first (default is fixed part) + Flag CFGFLAG_ALLOW_HIDTRIG = + new Flag(FlagType.CFG, "ALLOW_HIDTRIG", 0x10, 1, 0); // Allow trigger through HID/keyboard + + // Yubikey 2 and above + Flag CFGFLAG_SHORT_TICKET = + new Flag(FlagType.CFG, "SHORT_TICKET", 0x02, 2, 0); // Send truncated ticket (half length) + Flag CFGFLAG_STRONG_PW1 = + new Flag( + FlagType.CFG, "STRONG_PW1", 0x10, 2, 0); // Strong password policy flag #1 (mixed case) + Flag CFGFLAG_STRONG_PW2 = + new Flag( + FlagType.CFG, + "STRONG_PW2", + 0x40, + 2, + 0); // Strong password policy flag #2 (substitute 0..7 to digits) + Flag CFGFLAG_MAN_UPDATE = + new Flag(FlagType.CFG, "MAN_UPDATE", 0x80, 2, 0); // Allow manual (local) update of static OTP + + // Yubikey 2.1 and above + Flag TKTFLAG_OATH_HOTP = new Flag(FlagType.TKT, "OATH_HOTP", 0x40, 2, 1); // OATH HOTP mode + Flag CFGFLAG_OATH_HOTP8 = + new Flag( + FlagType.CFG, "OATH_HOTP8", 0x02, 2, 1); // Generate 8 digits HOTP rather than 6 digits + Flag CFGFLAG_OATH_FIXED_MODHEX1 = + new Flag( + FlagType.CFG, + "OATH_FIXED_MODHEX1", + 0x10, + 2, + 1); // First byte in fixed part sent as modhex + Flag CFGFLAG_OATH_FIXED_MODHEX2 = + new Flag( + FlagType.CFG, + "OATH_FIXED_MODHEX2", + 0x40, + 2, + 1); // First two bytes in fixed part sent as modhex (if paired with OATH_FIXED_MODHEX1, + // all of fixed part is sent as modhex) + + // Yubikey 2.2 and above + Flag TKTFLAG_CHAL_RESP = + new Flag( + FlagType.TKT, "CHAL_RESP", 0x40, 2, 2); // Challenge-response enabled (both must be set) + Flag CFGFLAG_CHAL_YUBICO = + new Flag( + FlagType.CFG, "CHAL_YUBICO", 0x20, 2, 2); // Challenge-response enabled - Yubico OTP mode + Flag CFGFLAG_CHAL_HMAC = + new Flag(FlagType.CFG, "CHAL_HMAC", 0x22, 2, 2); // Challenge-response enabled - HMAC-SHA1 + Flag CFGFLAG_HMAC_LT64 = + new Flag( + FlagType.CFG, "CHAL_LT64", 0x04, 2, 2); // Set when HMAC message is less than 64 bytes + Flag CFGFLAG_CHAL_BTN_TRIG = + new Flag( + FlagType.CFG, + "CHAL_BTN_TRIG", + 0x08, + 2, + 2); // Challenge-response operation requires button press + + Flag EXTFLAG_SERIAL_BTN_VISIBLE = + new Flag( + FlagType.EXT, + "SERIAL_BTN_VISIBLE", + 0x01, + 2, + 2); // Serial number visible at startup (button press) + Flag EXTFLAG_SERIAL_USB_VISIBLE = + new Flag( + FlagType.EXT, + "SERIAL_USB_VISIBLE", + 0x02, + 2, + 2); // Serial number visible in USB iSerial field + Flag EXTFLAG_SERIAL_API_VISIBLE = + new NonFailingFlag( + FlagType.EXT, "SERIAL_API_VISIBLE", 0x04, 2, 2); // Serial number visible via API call + + // YubiKey 2.3 and above + Flag EXTFLAG_USE_NUMERIC_KEYPAD = + new Flag(FlagType.EXT, "USE_NUMERIC_KEYPAD", 0x08, 2, 3); // Use numeric keypad for digits + Flag EXTFLAG_FAST_TRIG = + new NonFailingFlag(FlagType.EXT, "FAST_TRIG", 0x10, 2, 3); // Use fast trig if only cfg1 set + Flag EXTFLAG_ALLOW_UPDATE = + new NonFailingFlag( + FlagType.EXT, + "ALLOW_UPDATE", + 0x20, + 2, + 3); // Allow update of existing configuration (selected flags + access code) + Flag EXTFLAG_DORMANT = + new Flag( + FlagType.EXT, + "DORMANT", + 0x40, + 2, + 3); // Dormant configuration (can be woken up and flag removed = requires update flag) + + // YubiKey 2.4 and 3.1 and above (not 3.0) + Flag EXTFLAG_LED_INV = + new Flag(FlagType.EXT, "LED_INV", 0x80, 2, 4); // LED idle state is off rather than on + + /** + * Checks the configuration against a YubiKey firmware version to see if it is supported + * + * @param version the firmware version to check against + * @return true if the given YubiKey version supports this configuration + */ + boolean isSupportedBy(Version version); + + byte[] getConfig(@Nullable byte[] accCode); + + enum FlagType { + TKT, + CFG, + EXT + } + + /** A flag used for slot configuration. */ + class Flag extends Feature.Versioned { + public final FlagType type; + public final byte bit; + + Flag(FlagType type, String name, int bit, int major, int minor) { + super(type.name() + "FLAG_" + name, major, minor, 0); + this.type = type; + this.bit = (byte) bit; } - - /** - * Flag which should not cause a SlotConfiguration to fail, even if required version is not met. - */ - class NonFailingFlag extends Flag { - NonFailingFlag(FlagType type, String name, int bit, int major, int minor) { - super(type, name, bit, major, minor); - } + } + + /** + * Flag which should not cause a SlotConfiguration to fail, even if required version is not met. + */ + class NonFailingFlag extends Flag { + NonFailingFlag(FlagType type, String name, int bit, int major, int minor) { + super(type, name, bit, major, minor); } -} \ No newline at end of file + } +} diff --git a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/StaticPasswordSlotConfiguration.java b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/StaticPasswordSlotConfiguration.java index 9555b8a7..b5bba916 100755 --- a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/StaticPasswordSlotConfiguration.java +++ b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/StaticPasswordSlotConfiguration.java @@ -17,32 +17,34 @@ import java.nio.ByteBuffer; -/** - * Configures YubiKey to return static password on touch. - */ -public class StaticPasswordSlotConfiguration extends KeyboardSlotConfiguration { - private static final int SCAN_CODES_SIZE = FIXED_SIZE + UID_SIZE + KEY_SIZE; +/** Configures YubiKey to return static password on touch. */ +public class StaticPasswordSlotConfiguration + extends KeyboardSlotConfiguration { + private static final int SCAN_CODES_SIZE = FIXED_SIZE + UID_SIZE + KEY_SIZE; - /** - * Creates a Static Password configuration with default settings. - * - * @param scanCodes the password to store on YubiKey as an array of keyboard scan codes. - */ - public StaticPasswordSlotConfiguration(byte[] scanCodes) { - if (scanCodes.length > SCAN_CODES_SIZE) { - throw new UnsupportedOperationException("Password is too long"); - } + /** + * Creates a Static Password configuration with default settings. + * + * @param scanCodes the password to store on YubiKey as an array of keyboard scan codes. + */ + public StaticPasswordSlotConfiguration(byte[] scanCodes) { + if (scanCodes.length > SCAN_CODES_SIZE) { + throw new UnsupportedOperationException("Password is too long"); + } - // Scan codes are packed into fixed, uid, and key, and zero padded. - fixed = new byte[FIXED_SIZE]; - // NB: rewind() doesn't return a ByteBuffer before Java 9. - ByteBuffer.wrap(ByteBuffer.allocate(SCAN_CODES_SIZE).put(scanCodes).array()).get(fixed).get(uid).get(key); + // Scan codes are packed into fixed, uid, and key, and zero padded. + fixed = new byte[FIXED_SIZE]; + // NB: rewind() doesn't return a ByteBuffer before Java 9. + ByteBuffer.wrap(ByteBuffer.allocate(SCAN_CODES_SIZE).put(scanCodes).array()) + .get(fixed) + .get(uid) + .get(key); - updateFlags(CFGFLAG_SHORT_TICKET, true); - } + updateFlags(CFGFLAG_SHORT_TICKET, true); + } - @Override - protected StaticPasswordSlotConfiguration getThis() { - return this; - } + @Override + protected StaticPasswordSlotConfiguration getThis() { + return this; + } } diff --git a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/StaticTicketSlotConfiguration.java b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/StaticTicketSlotConfiguration.java index 6af366a8..4af86307 100755 --- a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/StaticTicketSlotConfiguration.java +++ b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/StaticTicketSlotConfiguration.java @@ -19,72 +19,79 @@ /** * Configures the YubiKey to output a Static Ticket. - * NOTE: {@link StaticPasswordSlotConfiguration} is a better choice in most cases! - *

- * A Static Ticket behaves like a Yubico OTP, but with all changing state removed. + * + *

NOTE: {@link StaticPasswordSlotConfiguration} is a better choice in most cases! + * + *

A Static Ticket behaves like a Yubico OTP, but with all changing state removed. */ -public class StaticTicketSlotConfiguration extends KeyboardSlotConfiguration { - /** - * Creates a Static Ticket configuration with default settings. - * - * @param fixed data to use for the fixed portion of the ticket - * @param uid uid value (corresponds to a Yubico OTP private ID) - * @param key AES key used to generate the "dynamic" part of the ticket - */ - public StaticTicketSlotConfiguration(byte[] fixed, byte[] uid, byte[] key) { - if (fixed.length > FIXED_SIZE) { - throw new IllegalArgumentException("Public ID must be <= 16 bytes"); - } +public class StaticTicketSlotConfiguration + extends KeyboardSlotConfiguration { + /** + * Creates a Static Ticket configuration with default settings. + * + * @param fixed data to use for the fixed portion of the ticket + * @param uid uid value (corresponds to a Yubico OTP private ID) + * @param key AES key used to generate the "dynamic" part of the ticket + */ + public StaticTicketSlotConfiguration(byte[] fixed, byte[] uid, byte[] key) { + if (fixed.length > FIXED_SIZE) { + throw new IllegalArgumentException("Public ID must be <= 16 bytes"); + } - this.fixed = Arrays.copyOf(fixed, fixed.length); - System.arraycopy(uid, 0, this.uid, 0, uid.length); - System.arraycopy(key, 0, this.key, 0, key.length); + this.fixed = Arrays.copyOf(fixed, fixed.length); + System.arraycopy(uid, 0, this.uid, 0, uid.length); + System.arraycopy(key, 0, this.key, 0, key.length); - updateFlags(CFGFLAG_STATIC_TICKET, true); - } + updateFlags(CFGFLAG_STATIC_TICKET, true); + } - @Override - protected StaticTicketSlotConfiguration getThis() { - return this; - } + @Override + protected StaticTicketSlotConfiguration getThis() { + return this; + } - /** - * Truncate the OTP-portion of the ticket to 16 characters. - * - * @param shortTicket if true, the OTP is truncated to 16 characters (default: false) - * @return the configuration for chaining - */ - public StaticTicketSlotConfiguration shortTicket(boolean shortTicket) { - return updateFlags(CFGFLAG_SHORT_TICKET, shortTicket); - } + /** + * Truncate the OTP-portion of the ticket to 16 characters. + * + * @param shortTicket if true, the OTP is truncated to 16 characters (default: false) + * @return the configuration for chaining + */ + public StaticTicketSlotConfiguration shortTicket(boolean shortTicket) { + return updateFlags(CFGFLAG_SHORT_TICKET, shortTicket); + } - /** - * Modifier flags to alter the output string to conform to password validation rules. - *

- * NOTE: special=true implies digits=true, and cannot be used without it. - * - * @param upperCase if true the two first letters of the output string are upper-cased (default: false) - * @param digit if true the first eight characters of the modhex alphabet are replaced with the numbers 0 to 7 (default: false) - * @param special if true a ! is sent as the very first character, and digits is implied (default: false) - * @return the configuration for chaining - */ - public StaticTicketSlotConfiguration strongPassword(boolean upperCase, boolean digit, boolean special) { - updateFlags(CFGFLAG_STRONG_PW1, upperCase); - updateFlags(CFGFLAG_STRONG_PW2, digit || special); - return updateFlags(CFGFLAG_SEND_REF, special); - } + /** + * Modifier flags to alter the output string to conform to password validation rules. + * + *

NOTE: special=true implies digits=true, and cannot be used without it. + * + * @param upperCase if true the two first letters of the output string are upper-cased (default: + * false) + * @param digit if true the first eight characters of the modhex alphabet are replaced with the + * numbers 0 to 7 (default: false) + * @param special if true a ! is sent as the very first character, and digits is implied (default: + * false) + * @return the configuration for chaining + */ + public StaticTicketSlotConfiguration strongPassword( + boolean upperCase, boolean digit, boolean special) { + updateFlags(CFGFLAG_STRONG_PW1, upperCase); + updateFlags(CFGFLAG_STRONG_PW2, digit || special); + return updateFlags(CFGFLAG_SEND_REF, special); + } - /** - * Enabled Manual Update of the static ticket. - * NOTE: This feature is ONLY supported on YubiKey 2.x - *

- * Manual update is triggered by the user by holding the sensor pressed for 8-15 seconds. - * This will generate a new random static ticket to be used, until manual update is again invoked. - * - * @param manualUpdate if true, enable user-initiated manual update (default: false) - * @return the configuration for chaining - */ - public StaticTicketSlotConfiguration manualUpdate(boolean manualUpdate) { - return updateFlags(CFGFLAG_MAN_UPDATE, manualUpdate); - } + /** + * Enabled Manual Update of the static ticket. + * + *

NOTE: This feature is ONLY supported on YubiKey 2.x + * + *

Manual update is triggered by the user by holding the sensor pressed for 8-15 seconds. This + * will generate a new random static ticket to be used, until manual update is again invoked. + * + * @param manualUpdate if true, enable user-initiated manual update (default: false) + * @return the configuration for chaining + */ + public StaticTicketSlotConfiguration manualUpdate(boolean manualUpdate) { + return updateFlags(CFGFLAG_MAN_UPDATE, manualUpdate); + } } diff --git a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/UpdateConfiguration.java b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/UpdateConfiguration.java index 702ecf35..41a0db05 100755 --- a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/UpdateConfiguration.java +++ b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/UpdateConfiguration.java @@ -17,75 +17,91 @@ package com.yubico.yubikit.yubiotp; import com.yubico.yubikit.core.Version; - import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Set; public class UpdateConfiguration extends KeyboardSlotConfiguration { - private static final Set UPDATE_FLAGS; + private static final Set UPDATE_FLAGS; - static { - Set allowed = new HashSet<>(); - allowed.addAll(Arrays.asList(EXTFLAG_ALLOW_UPDATE, EXTFLAG_DORMANT, EXTFLAG_FAST_TRIG, EXTFLAG_LED_INV, EXTFLAG_SERIAL_API_VISIBLE, EXTFLAG_SERIAL_BTN_VISIBLE, EXTFLAG_SERIAL_USB_VISIBLE, EXTFLAG_USE_NUMERIC_KEYPAD)); - allowed.addAll(Arrays.asList(TKTFLAG_TAB_FIRST, TKTFLAG_APPEND_TAB1, TKTFLAG_APPEND_TAB2, TKTFLAG_APPEND_DELAY1, TKTFLAG_APPEND_DELAY2, TKTFLAG_APPEND_CR)); - allowed.addAll(Arrays.asList(CFGFLAG_PACING_10MS, CFGFLAG_PACING_20MS)); - UPDATE_FLAGS = Collections.unmodifiableSet(allowed); - } + static { + Set allowed = new HashSet<>(); + allowed.addAll( + Arrays.asList( + EXTFLAG_ALLOW_UPDATE, + EXTFLAG_DORMANT, + EXTFLAG_FAST_TRIG, + EXTFLAG_LED_INV, + EXTFLAG_SERIAL_API_VISIBLE, + EXTFLAG_SERIAL_BTN_VISIBLE, + EXTFLAG_SERIAL_USB_VISIBLE, + EXTFLAG_USE_NUMERIC_KEYPAD)); + allowed.addAll( + Arrays.asList( + TKTFLAG_TAB_FIRST, + TKTFLAG_APPEND_TAB1, + TKTFLAG_APPEND_TAB2, + TKTFLAG_APPEND_DELAY1, + TKTFLAG_APPEND_DELAY2, + TKTFLAG_APPEND_CR)); + allowed.addAll(Arrays.asList(CFGFLAG_PACING_10MS, CFGFLAG_PACING_20MS)); + UPDATE_FLAGS = Collections.unmodifiableSet(allowed); + } - @Override - public boolean isSupportedBy(Version version) { - return YubiOtpSession.FEATURE_UPDATE.isSupportedBy(version) && super.isSupportedBy(version); - } + @Override + public boolean isSupportedBy(Version version) { + return YubiOtpSession.FEATURE_UPDATE.isSupportedBy(version) && super.isSupportedBy(version); + } - @Override - protected UpdateConfiguration getThis() { - return this; - } + @Override + protected UpdateConfiguration getThis() { + return this; + } - @Override - protected UpdateConfiguration updateFlags(Flag flag, boolean value) { - if (!UPDATE_FLAGS.contains(flag)) { - throw new IllegalArgumentException("Unsupported TKT flags for update"); - } - return super.updateFlags(flag, value); + @Override + protected UpdateConfiguration updateFlags(Flag flag, boolean value) { + if (!UPDATE_FLAGS.contains(flag)) { + throw new IllegalArgumentException("Unsupported TKT flags for update"); } + return super.updateFlags(flag, value); + } - /** - * This setting cannot be changed for update, and this method will throw an IllegalArgumentException - * - * @param protectSlot2 If true, slot 2 cannot be modified. - * @return this method will not return normally - */ - @Override - public UpdateConfiguration protectSlot2(boolean protectSlot2) { - throw new IllegalArgumentException("protectSlot2 cannot be applied to UpdateConfiguration"); - } + /** + * This setting cannot be changed for update, and this method will throw an + * IllegalArgumentException + * + * @param protectSlot2 If true, slot 2 cannot be modified. + * @return this method will not return normally + */ + @Override + public UpdateConfiguration protectSlot2(boolean protectSlot2) { + throw new IllegalArgumentException("protectSlot2 cannot be applied to UpdateConfiguration"); + } - /** - * Inserts tabs in-between different parts of the OTP. - * - * @param before inserts a tab before any other output (default: false) - * @param afterFirst inserts a tab after the static part of the OTP (default: false) - * @param afterSecond inserts a tab after the end of the OTP (default: false) - * @return the configuration for chaining - */ - public UpdateConfiguration tabs(boolean before, boolean afterFirst, boolean afterSecond) { - updateFlags(TKTFLAG_TAB_FIRST, before); - updateFlags(TKTFLAG_APPEND_TAB1, afterFirst); - return updateFlags(TKTFLAG_APPEND_TAB2, afterSecond); - } + /** + * Inserts tabs in-between different parts of the OTP. + * + * @param before inserts a tab before any other output (default: false) + * @param afterFirst inserts a tab after the static part of the OTP (default: false) + * @param afterSecond inserts a tab after the end of the OTP (default: false) + * @return the configuration for chaining + */ + public UpdateConfiguration tabs(boolean before, boolean afterFirst, boolean afterSecond) { + updateFlags(TKTFLAG_TAB_FIRST, before); + updateFlags(TKTFLAG_APPEND_TAB1, afterFirst); + return updateFlags(TKTFLAG_APPEND_TAB2, afterSecond); + } - /** - * Inserts delays in-between different parts of the OTP. - * - * @param afterFirst inserts a delay after the static part of the OTP (default: false) - * @param afterSecond inserts a delay after the end of the OTP (default: false) - * @return the configuration for chaining - */ - public UpdateConfiguration delay(boolean afterFirst, boolean afterSecond) { - updateFlags(TKTFLAG_APPEND_DELAY1, afterFirst); - return updateFlags(TKTFLAG_APPEND_DELAY2, afterSecond); - } + /** + * Inserts delays in-between different parts of the OTP. + * + * @param afterFirst inserts a delay after the static part of the OTP (default: false) + * @param afterSecond inserts a delay after the end of the OTP (default: false) + * @return the configuration for chaining + */ + public UpdateConfiguration delay(boolean afterFirst, boolean afterSecond) { + updateFlags(TKTFLAG_APPEND_DELAY1, afterFirst); + return updateFlags(TKTFLAG_APPEND_DELAY2, afterSecond); + } } diff --git a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/YubiOtpSession.java b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/YubiOtpSession.java index 52ebaf11..f9a2804f 100755 --- a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/YubiOtpSession.java +++ b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/YubiOtpSession.java @@ -16,551 +16,584 @@ package com.yubico.yubikit.yubiotp; -import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.Transport; import com.yubico.yubikit.core.Version; import com.yubico.yubikit.core.YubiKeyConnection; import com.yubico.yubikit.core.YubiKeyDevice; -import com.yubico.yubikit.core.smartcard.ApduException; -import com.yubico.yubikit.core.smartcard.AppId; import com.yubico.yubikit.core.application.ApplicationNotAvailableException; import com.yubico.yubikit.core.application.ApplicationSession; import com.yubico.yubikit.core.application.BadResponseException; import com.yubico.yubikit.core.application.CommandException; import com.yubico.yubikit.core.application.CommandState; import com.yubico.yubikit.core.application.Feature; +import com.yubico.yubikit.core.internal.Logger; import com.yubico.yubikit.core.otp.ChecksumUtils; import com.yubico.yubikit.core.otp.OtpConnection; import com.yubico.yubikit.core.otp.OtpProtocol; import com.yubico.yubikit.core.smartcard.Apdu; +import com.yubico.yubikit.core.smartcard.ApduException; +import com.yubico.yubikit.core.smartcard.AppId; import com.yubico.yubikit.core.smartcard.SmartCardConnection; import com.yubico.yubikit.core.smartcard.SmartCardProtocol; import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams; import com.yubico.yubikit.core.util.Callback; import com.yubico.yubikit.core.util.Result; - -import org.slf4j.LoggerFactory; - import java.io.Closeable; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import java.util.Arrays; - import javax.annotation.Nullable; +import org.slf4j.LoggerFactory; /** - * Application to use and configure the OTP application of the YubiKey. - * This applications supports configuration of the two YubiKey "OTP slots" which are typically activated by pressing - * the capacitive sensor on the YubiKey for either a short or long press. - *

- * Each slot can be configured with one of the following types of credentials: - * - YubiOTP - a Yubico OTP (One Time Password) credential. - * - OATH-HOTP - a counter based (HOTP) OATH OTP credential (see https://tools.ietf.org/html/rfc4226). - * - Static Password - a static (non-changing) password. - * - Challenge-Response - a HMAC-SHA1 key which can be accessed programmatically. - *

- * Additionally for NFC enabled YubiKeys, one slot can be configured to be output over NDEF as part of a URL payload. + * Application to use and configure the OTP application of the YubiKey. This applications supports + * configuration of the two YubiKey "OTP slots" which are typically activated by pressing the + * capacitive sensor on the YubiKey for either a short or long press. + * + *

Each slot can be configured with one of the following types of credentials: - YubiOTP - a + * Yubico OTP (One Time Password) credential. - OATH-HOTP - a counter based (HOTP) OATH OTP + * credential (see https://tools.ietf.org/html/rfc4226). - Static + * Password - a static (non-changing) password. - Challenge-Response - a HMAC-SHA1 key which can be + * accessed programmatically. + * + *

Additionally for NFC enabled YubiKeys, one slot can be configured to be output over NDEF as + * part of a URL payload. */ public class YubiOtpSession extends ApplicationSession { - public static final String DEFAULT_NDEF_URI = "https://my.yubico.com/yk/#"; - - // Features - /** - * Support for checking if a slot is configured via the ConfigState. - */ - public static final Feature FEATURE_CHECK_CONFIGURED = new Feature.Versioned<>("Check if a slot is configured", 2, 1, 0); - /** - * Support for checking if a configured slot requires touch via the ConfigState. - */ - public static final Feature FEATURE_CHECK_TOUCH_TRIGGERED = new Feature.Versioned<>("Check if a slot is triggered by touch", 3, 0, 0); - /** - * Support for HMAC-SHA1 challenge response functionality. - */ - public static final Feature FEATURE_CHALLENGE_RESPONSE = new Feature.Versioned<>("Challenge-Response", 2, 2, 0); - - /** - * Support for swapping slot configurations. - */ - public static final Feature FEATURE_SWAP = new Feature.Versioned<>("Swap Slots", 2, 3, 0); - /** - * Support for updating an already configured slot. - */ - public static final Feature FEATURE_UPDATE = new Feature.Versioned<>("Update Slot", 2, 3, 0); - /** - * Support for NDEF configuration. - */ - public static final Feature FEATURE_NDEF = new Feature.Versioned<>("NDEF", 3, 0, 0); - - private static final int ACC_CODE_SIZE = 6; // Size of access code to re-program device - private static final int CONFIG_SIZE = 52; // Size of config struct (excluding current access code) - private static final int NDEF_DATA_SIZE = 54; // Size of the NDEF payload data - - private static final byte INS_CONFIG = 0x01; - - private static final int HMAC_CHALLENGE_SIZE = 64; - private static final int HMAC_RESPONSE_SIZE = 20; - - private static final byte CMD_CONFIG_1 = 0x1; - private static final byte CMD_NAV = 0x2; - private static final byte CMD_CONFIG_2 = 0x3; - private static final byte CMD_UPDATE_1 = 0x4; - private static final byte CMD_UPDATE_2 = 0x5; - private static final byte CMD_SWAP = 0x6; - private static final byte CMD_NDEF_1 = 0x8; - private static final byte CMD_NDEF_2 = 0x9; - private static final byte CMD_DEVICE_SERIAL = 0x10; - private static final byte CMD_SCAN_MAP = 0x12; - private static final byte CMD_CHALLENGE_OTP_1 = 0x20; - private static final byte CMD_CHALLENGE_OTP_2 = 0x28; - private static final byte CMD_CHALLENGE_HMAC_1 = 0x30; - private static final byte CMD_CHALLENGE_HMAC_2 = 0x38; - - private final Backend backend; - - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(YubiOtpSession.class); - - /** - * Connects to a YubiKeyDevice and establishes a new session with a YubiKeys OTP application. - *

- * This method will use whichever connection type is available. - * - * @param device A YubiKey device to use - */ - public static void create(YubiKeyDevice device, Callback> callback) { - if (device.supportsConnection(OtpConnection.class)) { - device.requestConnection(OtpConnection.class, value -> callback.invoke(Result.of(() -> new YubiOtpSession(value.getValue())))); - } else if (device.supportsConnection(SmartCardConnection.class)) { - device.requestConnection(SmartCardConnection.class, value -> callback.invoke(Result.of(() -> new YubiOtpSession(value.getValue())))); - } else { - callback.invoke(Result.failure(new ApplicationNotAvailableException("Session does not support any compatible connection type"))); - } + public static final String DEFAULT_NDEF_URI = "https://my.yubico.com/yk/#"; + + // Features + /** Support for checking if a slot is configured via the ConfigState. */ + public static final Feature FEATURE_CHECK_CONFIGURED = + new Feature.Versioned<>("Check if a slot is configured", 2, 1, 0); + + /** Support for checking if a configured slot requires touch via the ConfigState. */ + public static final Feature FEATURE_CHECK_TOUCH_TRIGGERED = + new Feature.Versioned<>("Check if a slot is triggered by touch", 3, 0, 0); + + /** Support for HMAC-SHA1 challenge response functionality. */ + public static final Feature FEATURE_CHALLENGE_RESPONSE = + new Feature.Versioned<>("Challenge-Response", 2, 2, 0); + + /** Support for swapping slot configurations. */ + public static final Feature FEATURE_SWAP = + new Feature.Versioned<>("Swap Slots", 2, 3, 0); + + /** Support for updating an already configured slot. */ + public static final Feature FEATURE_UPDATE = + new Feature.Versioned<>("Update Slot", 2, 3, 0); + + /** Support for NDEF configuration. */ + public static final Feature FEATURE_NDEF = + new Feature.Versioned<>("NDEF", 3, 0, 0); + + private static final int ACC_CODE_SIZE = 6; // Size of access code to re-program device + private static final int CONFIG_SIZE = + 52; // Size of config struct (excluding current access code) + private static final int NDEF_DATA_SIZE = 54; // Size of the NDEF payload data + + private static final byte INS_CONFIG = 0x01; + + private static final int HMAC_CHALLENGE_SIZE = 64; + private static final int HMAC_RESPONSE_SIZE = 20; + + private static final byte CMD_CONFIG_1 = 0x1; + private static final byte CMD_NAV = 0x2; + private static final byte CMD_CONFIG_2 = 0x3; + private static final byte CMD_UPDATE_1 = 0x4; + private static final byte CMD_UPDATE_2 = 0x5; + private static final byte CMD_SWAP = 0x6; + private static final byte CMD_NDEF_1 = 0x8; + private static final byte CMD_NDEF_2 = 0x9; + private static final byte CMD_DEVICE_SERIAL = 0x10; + private static final byte CMD_SCAN_MAP = 0x12; + private static final byte CMD_CHALLENGE_OTP_1 = 0x20; + private static final byte CMD_CHALLENGE_OTP_2 = 0x28; + private static final byte CMD_CHALLENGE_HMAC_1 = 0x30; + private static final byte CMD_CHALLENGE_HMAC_2 = 0x38; + + private final Backend backend; + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(YubiOtpSession.class); + + /** + * Connects to a YubiKeyDevice and establishes a new session with a YubiKeys OTP application. + * + *

This method will use whichever connection type is available. + * + * @param device A YubiKey device to use + */ + public static void create( + YubiKeyDevice device, Callback> callback) { + if (device.supportsConnection(OtpConnection.class)) { + device.requestConnection( + OtpConnection.class, + value -> callback.invoke(Result.of(() -> new YubiOtpSession(value.getValue())))); + } else if (device.supportsConnection(SmartCardConnection.class)) { + device.requestConnection( + SmartCardConnection.class, + value -> callback.invoke(Result.of(() -> new YubiOtpSession(value.getValue())))); + } else { + callback.invoke( + Result.failure( + new ApplicationNotAvailableException( + "Session does not support any compatible connection type"))); } - - /** - * Create new instance of {@link YubiOtpSession} using an {@link SmartCardConnection}. - * NOTE: Not all functionality is available over all transports. Over USB, some functionality may be blocked when - * not using an OtpConnection. - * - * @param connection an Iso7816Connection with a YubiKey - * @throws IOException in case of connection error - * @throws ApplicationNotAvailableException if the application is missing or disabled - */ - public YubiOtpSession(SmartCardConnection connection) throws IOException, ApplicationNotAvailableException { - this(connection, null); + } + + /** + * Create new instance of {@link YubiOtpSession} using an {@link SmartCardConnection}. + * + *

NOTE: Not all functionality is available over all transports. Over USB, some functionality + * may be blocked when not using an OtpConnection. + * + * @param connection an Iso7816Connection with a YubiKey + * @throws IOException in case of connection error + * @throws ApplicationNotAvailableException if the application is missing or disabled + */ + public YubiOtpSession(SmartCardConnection connection) + throws IOException, ApplicationNotAvailableException { + this(connection, null); + } + + /** + * Create new instance of {@link YubiOtpSession} using an {@link SmartCardConnection}. + * + *

NOTE: Not all functionality is available over all transports. Over USB, some functionality + * may be blocked when not using an OtpConnection. + * + * @param connection an Iso7816Connection with a YubiKey + * @throws IOException in case of connection error + * @throws ApplicationNotAvailableException if the application is missing or disabled + */ + public YubiOtpSession(SmartCardConnection connection, @Nullable ScpKeyParams scpKeyParams) + throws IOException, ApplicationNotAvailableException { + Version version = null; + SmartCardProtocol protocol = new SmartCardProtocol(connection); + + if (connection.getTransport() == Transport.NFC) { + // If available, this is more reliable than status.getVersion() over NFC + try { + byte[] response = protocol.select(AppId.MANAGEMENT); + version = Version.parse(new String(response, StandardCharsets.UTF_8)); + } catch (ApplicationNotAvailableException e) { + // NB: YubiKey NEO doesn't support the Management Application over NFC. + // NEO: version will be populated further down. + } } - /** - * Create new instance of {@link YubiOtpSession} using an {@link SmartCardConnection}. - * NOTE: Not all functionality is available over all transports. Over USB, some functionality may be blocked when - * not using an OtpConnection. - * - * @param connection an Iso7816Connection with a YubiKey - * @throws IOException in case of connection error - * @throws ApplicationNotAvailableException if the application is missing or disabled - */ - public YubiOtpSession(SmartCardConnection connection, @Nullable ScpKeyParams scpKeyParams) throws IOException, ApplicationNotAvailableException { - Version version = null; - SmartCardProtocol protocol = new SmartCardProtocol(connection); - - if (connection.getTransport() == Transport.NFC) { - // If available, this is more reliable than status.getVersion() over NFC - try { - byte[] response = protocol.select(AppId.MANAGEMENT); - version = Version.parse(new String(response, StandardCharsets.UTF_8)); - } catch (ApplicationNotAvailableException e) { - // NB: YubiKey NEO doesn't support the Management Application over NFC. - // NEO: version will be populated further down. - } - } - - byte[] statusBytes = protocol.select(AppId.OTP); - if (version == null) { - // We didn't get a version above, get it from the status struct. - version = Version.fromBytes(statusBytes); - } - - if (scpKeyParams != null) { - try { - protocol.initScp(scpKeyParams); - } catch (ApduException | BadResponseException e) { - throw new IOException(e); - } - } - protocol.configure(version); - - backend = new Backend(protocol, version, parseConfigState(version, statusBytes)) { - // 5.0.0-5.2.5 have an issue with status over NFC - private final boolean dummyStatus = connection.getTransport() == Transport.NFC && version.isAtLeast(5, 0, 0) && version.isLessThan(5, 2, 5); + byte[] statusBytes = protocol.select(AppId.OTP); + if (version == null) { + // We didn't get a version above, get it from the status struct. + version = Version.fromBytes(statusBytes); + } - { - if (dummyStatus) { // We can't read the status, so use a dummy with both slots marked as configured. - configurationState = new ConfigurationState(version, (short) 3); - } + if (scpKeyParams != null) { + try { + protocol.initScp(scpKeyParams); + } catch (ApduException | BadResponseException e) { + throw new IOException(e); + } + } + protocol.configure(version); + + backend = + new Backend(protocol, version, parseConfigState(version, statusBytes)) { + // 5.0.0-5.2.5 have an issue with status over NFC + private final boolean dummyStatus = + connection.getTransport() == Transport.NFC + && version.isAtLeast(5, 0, 0) + && version.isLessThan(5, 2, 5); + + { + if (dummyStatus) { // We can't read the status, so use a dummy with both slots marked as + // configured. + configurationState = new ConfigurationState(version, (short) 3); } + } - @Override - void writeToSlot(byte slot, byte[] data) throws IOException, CommandException { - byte[] status = delegate.sendAndReceive(new Apdu(0, INS_CONFIG, slot, 0, data)); - if (!dummyStatus) { - configurationState = parseConfigState(this.version, status); - } + @Override + void writeToSlot(byte slot, byte[] data) throws IOException, CommandException { + byte[] status = delegate.sendAndReceive(new Apdu(0, INS_CONFIG, slot, 0, data)); + if (!dummyStatus) { + configurationState = parseConfigState(this.version, status); } - - @Override - byte[] sendAndReceive(byte slot, byte[] data, int expectedResponseLength, @Nullable CommandState state) throws IOException, CommandException { - byte[] response = delegate.sendAndReceive(new Apdu(0, INS_CONFIG, slot, 0, data)); - if (expectedResponseLength != response.length) { - throw new BadResponseException("Unexpected response length"); - } - return response; + } + + @Override + byte[] sendAndReceive( + byte slot, byte[] data, int expectedResponseLength, @Nullable CommandState state) + throws IOException, CommandException { + byte[] response = delegate.sendAndReceive(new Apdu(0, INS_CONFIG, slot, 0, data)); + if (expectedResponseLength != response.length) { + throw new BadResponseException("Unexpected response length"); } + return response; + } }; - logCtor(connection); - } - - /** - * Create new instance of {@link YubiOtpSession} using an {@link OtpConnection}. - * - * @param connection an OtpConnection with YubiKey - * @throws IOException in case of connection error - */ - public YubiOtpSession(OtpConnection connection) throws IOException { - OtpProtocol protocol = new OtpProtocol(connection); - byte[] statusBytes = protocol.readStatus(); - Version version = protocol.getVersion(); - backend = new Backend(protocol, version, parseConfigState(version, statusBytes)) { - @Override - void writeToSlot(byte slot, byte[] data) throws IOException, CommandException { - configurationState = parseConfigState(version, delegate.sendAndReceive(slot, data, null)); - } - - @Override - byte[] sendAndReceive(byte slot, byte[] data, int expectedResponseLength, @Nullable CommandState state) throws IOException, CommandException { - byte[] response = delegate.sendAndReceive(slot, data, state); - if (ChecksumUtils.checkCrc(response, expectedResponseLength + 2)) { - return Arrays.copyOf(response, expectedResponseLength); - } - throw new IOException("Invalid CRC"); + logCtor(connection); + } + + /** + * Create new instance of {@link YubiOtpSession} using an {@link OtpConnection}. + * + * @param connection an OtpConnection with YubiKey + * @throws IOException in case of connection error + */ + public YubiOtpSession(OtpConnection connection) throws IOException { + OtpProtocol protocol = new OtpProtocol(connection); + byte[] statusBytes = protocol.readStatus(); + Version version = protocol.getVersion(); + backend = + new Backend(protocol, version, parseConfigState(version, statusBytes)) { + @Override + void writeToSlot(byte slot, byte[] data) throws IOException, CommandException { + configurationState = + parseConfigState(version, delegate.sendAndReceive(slot, data, null)); + } + + @Override + byte[] sendAndReceive( + byte slot, byte[] data, int expectedResponseLength, @Nullable CommandState state) + throws IOException, CommandException { + byte[] response = delegate.sendAndReceive(slot, data, state); + if (ChecksumUtils.checkCrc(response, expectedResponseLength + 2)) { + return Arrays.copyOf(response, expectedResponseLength); } + throw new IOException("Invalid CRC"); + } }; - logCtor(connection); + logCtor(connection); + } + + @Override + public void close() throws IOException { + backend.close(); + } + + /** + * Get the configuration state of the application. + * + * @return the current configuration state of the two slots. + */ + public ConfigurationState getConfigurationState() { + return backend.configurationState; + } + + /** + * Get the firmware version of the YubiKey + * + * @return Yubikey firmware version + */ + @Override + public Version getVersion() { + return backend.version; + } + + /** + * Get the serial number of the YubiKey. Note that the EXTFLAG_SERIAL_API_VISIBLE flag must be set + * for this command to work. + * + * @return the serial number + * @throws IOException in case of communication error + * @throws CommandException in case of an error response from the YubiKey + */ + public int getSerialNumber() throws IOException, CommandException { + return ByteBuffer.wrap(backend.sendAndReceive(CMD_DEVICE_SERIAL, new byte[0], 4, null)) + .getInt(); + } + + /** + * Swaps the two slot configurations with each other. + * + * @throws IOException in case of communication error + * @throws CommandException in case of an error response from the YubiKey + */ + public void swapConfigurations() throws IOException, CommandException { + Logger.debug(logger, "Swapping touch slots"); + require(FEATURE_SWAP); + writeConfig(CMD_SWAP, new byte[0], null); + } + + /** + * Delete the contents of a slot. + * + *

NOTE: Attempting to delete an empty slot will under certain circumstances fail, resulting in + * a {@link com.yubico.yubikit.core.otp.CommandRejectedException} being thrown. Prefer to check if + * a slot is configured before calling delete. + * + * @param slot the slot to delete + * @param curAccCode the currently set access code, if needed + * @throws IOException in case of communication error + * @throws CommandException in case of an error response from the YubiKey + */ + public void deleteConfiguration(Slot slot, @Nullable byte[] curAccCode) + throws IOException, CommandException { + Logger.debug(logger, "Deleting slot {}", slot); + writeConfig(slot.map(CMD_CONFIG_1, CMD_CONFIG_2), new byte[CONFIG_SIZE], curAccCode); + } + + /** + * Write a configuration to a slot, overwriting previous configuration (if present). + * + * @param slot the slot to write to + * @param configuration the new configuration to write + * @param accCode the access code to set (or null, to not set an access code) + * @param curAccCode the current access code, if one is set for the target slot + * @throws IOException in case of communication error + * @throws CommandException in case of an error response from the YubiKey + */ + public void putConfiguration( + Slot slot, + SlotConfiguration configuration, + @Nullable byte[] accCode, + @Nullable byte[] curAccCode) + throws IOException, CommandException { + if (!configuration.isSupportedBy(backend.version)) { + throw new UnsupportedOperationException( + "This configuration update is not supported on this YubiKey version"); } - - @Override - public void close() throws IOException { - backend.close(); + Logger.debug( + logger, + "Writing configuration of type {} to slot {}", + configuration.getClass().getSimpleName(), + slot); + writeConfig(slot.map(CMD_CONFIG_1, CMD_CONFIG_2), configuration.getConfig(accCode), curAccCode); + } + + /** + * Update the configuration of a slot, keeping the credential. + * + *

This functionality requires support for {@link #FEATURE_UPDATE}, available on YubiKey 2.3 or + * later. + * + * @param slot the slot to update + * @param configuration the updated flags tp set + * @param accCode the access code to set + * @param curAccCode the current access code, if needed + * @throws IOException in case of communication error + * @throws CommandException in case of an error response from the YubiKey + */ + public void updateConfiguration( + Slot slot, + UpdateConfiguration configuration, + @Nullable byte[] accCode, + @Nullable byte[] curAccCode) + throws IOException, CommandException { + require(FEATURE_UPDATE); + if (!configuration.isSupportedBy(backend.version)) { + throw new UnsupportedOperationException( + "This configuration is not supported on this YubiKey version"); } - - /** - * Get the configuration state of the application. - * - * @return the current configuration state of the two slots. - */ - public ConfigurationState getConfigurationState() { - return backend.configurationState; - } - - /** - * Get the firmware version of the YubiKey - * - * @return Yubikey firmware version - */ - @Override - public Version getVersion() { - return backend.version; + if (!Arrays.equals(accCode, curAccCode) + && getVersion().isAtLeast(4, 3, 2) + && getVersion().isLessThan(4, 3, 6)) { + throw new UnsupportedOperationException( + "The access code cannot be updated on this YubiKey. Instead, delete the slot and" + + " configure it anew."); } - - /** - * Get the serial number of the YubiKey. - * Note that the EXTFLAG_SERIAL_API_VISIBLE flag must be set for this command to work. - * - * @return the serial number - * @throws IOException in case of communication error - * @throws CommandException in case of an error response from the YubiKey - */ - public int getSerialNumber() throws IOException, CommandException { - return ByteBuffer.wrap(backend.sendAndReceive(CMD_DEVICE_SERIAL, new byte[0], 4, null)).getInt(); - } - - /** - * Swaps the two slot configurations with each other. - * - * @throws IOException in case of communication error - * @throws CommandException in case of an error response from the YubiKey - */ - public void swapConfigurations() throws IOException, CommandException { - Logger.debug(logger, "Swapping touch slots"); - require(FEATURE_SWAP); - writeConfig(CMD_SWAP, new byte[0], null); - } - - /** - * Delete the contents of a slot. - *

- * NOTE: Attempting to delete an empty slot will under certain circumstances fail, resulting in - * a {@link com.yubico.yubikit.core.otp.CommandRejectedException} being thrown. Prefer to check - * if a slot is configured before calling delete. - * - * @param slot the slot to delete - * @param curAccCode the currently set access code, if needed - * @throws IOException in case of communication error - * @throws CommandException in case of an error response from the YubiKey - */ - public void deleteConfiguration(Slot slot, @Nullable byte[] curAccCode) throws IOException, CommandException { - Logger.debug(logger, "Deleting slot {}", slot); - writeConfig( - slot.map(CMD_CONFIG_1, CMD_CONFIG_2), - new byte[CONFIG_SIZE], - curAccCode - ); - } - - /** - * Write a configuration to a slot, overwriting previous configuration (if present). - * - * @param slot the slot to write to - * @param configuration the new configuration to write - * @param accCode the access code to set (or null, to not set an access code) - * @param curAccCode the current access code, if one is set for the target slot - * @throws IOException in case of communication error - * @throws CommandException in case of an error response from the YubiKey - */ - public void putConfiguration(Slot slot, SlotConfiguration configuration, @Nullable byte[] accCode, @Nullable byte[] curAccCode) throws IOException, CommandException { - if (!configuration.isSupportedBy(backend.version)) { - throw new UnsupportedOperationException("This configuration update is not supported on this YubiKey version"); - } - Logger.debug(logger, "Writing configuration of type {} to slot {}", - configuration.getClass().getSimpleName(), - slot); - writeConfig( - slot.map(CMD_CONFIG_1, CMD_CONFIG_2), - configuration.getConfig(accCode), - curAccCode - ); - } - - /** - * Update the configuration of a slot, keeping the credential. - *

- * This functionality requires support for {@link #FEATURE_UPDATE}, available on YubiKey 2.3 or later. - * - * @param slot the slot to update - * @param configuration the updated flags tp set - * @param accCode the access code to set - * @param curAccCode the current access code, if needed - * @throws IOException in case of communication error - * @throws CommandException in case of an error response from the YubiKey - */ - public void updateConfiguration(Slot slot, UpdateConfiguration configuration, @Nullable byte[] accCode, @Nullable byte[] curAccCode) throws IOException, CommandException { - require(FEATURE_UPDATE); - if (!configuration.isSupportedBy(backend.version)) { - throw new UnsupportedOperationException("This configuration is not supported on this YubiKey version"); - } - if (!Arrays.equals(accCode, curAccCode) && getVersion().isAtLeast(4, 3, 2) && getVersion().isLessThan(4, 3, 6)) { - throw new UnsupportedOperationException("The access code cannot be updated on this YubiKey. Instead, delete the slot and configure it anew."); - } - Logger.debug(logger, "Writing configuration update to slot {}", slot); - writeConfig( - slot.map(CMD_UPDATE_1, CMD_UPDATE_2), - configuration.getConfig(accCode), - curAccCode - ); + Logger.debug(logger, "Writing configuration update to slot {}", slot); + writeConfig(slot.map(CMD_UPDATE_1, CMD_UPDATE_2), configuration.getConfig(accCode), curAccCode); + } + + /** + * Configure the NFC NDEF payload, and which slot to use. + * + *

This functionality requires support for {@link #FEATURE_NDEF}, available on YubiKey 3 or + * later. + * + * @param slot the YubiKey slot to append to the uri payload + * @param uri the URI prefix (if null, the default "https://my.yubico.com/yk/#" will be used) + * @param curAccCode the current access code, if needed + * @throws IOException in case of communication error + * @throws CommandException in case of an error response from the YubiKey + */ + @SuppressWarnings("JavadocLinkAsPlainText") + public void setNdefConfiguration(Slot slot, @Nullable String uri, @Nullable byte[] curAccCode) + throws IOException, CommandException { + Logger.debug(logger, "Writing NDEF configuration for slot {} ", slot); + require(FEATURE_NDEF); + writeConfig( + slot.map(CMD_NDEF_1, CMD_NDEF_2), + buildNdefConfig(uri == null ? DEFAULT_NDEF_URI : uri), + curAccCode); + } + + /** + * Calculates HMAC-SHA1 on given challenge (using secret that configured/programmed on YubiKey) + * + *

This functionality requires support for {@link #FEATURE_CHALLENGE_RESPONSE}, available on + * YubiKey 2.2 or later. + * + * @param slot the slot on YubiKey that configured with challenge response secret + * @param challenge generated challenge that will be sent + * @param state if false, the command will be aborted in case the credential requires user touch + * @return response on challenge returned from YubiKey + * @throws IOException in case of communication error, or no key configured in slot + * @throws CommandException in case of an error response from the YubiKey + */ + public byte[] calculateHmacSha1(Slot slot, byte[] challenge, @Nullable CommandState state) + throws IOException, CommandException { + Logger.debug(logger, "Calculating response for slog {}", slot); + require(FEATURE_CHALLENGE_RESPONSE); + + // Pad challenge with byte different from last. + byte[] padded = new byte[HMAC_CHALLENGE_SIZE]; + Arrays.fill(padded, (byte) (challenge[challenge.length - 1] == 0 ? 1 : 0)); + System.arraycopy(challenge, 0, padded, 0, challenge.length); + + // response for HMAC-SHA1 challenge response is always 20 bytes + return backend.sendAndReceive( + slot.map(CMD_CHALLENGE_HMAC_1, CMD_CHALLENGE_HMAC_2), padded, HMAC_RESPONSE_SIZE, state); + } + + private void writeConfig(byte commandSlot, byte[] config, @Nullable byte[] curAccCode) + throws IOException, CommandException { + Logger.debug( + logger, + "Writing configuration to slot {}, access code: {}", + commandSlot, + curAccCode != null); + backend.writeToSlot( + commandSlot, + ByteBuffer.allocate(config.length + ACC_CODE_SIZE) + .put(config) + .put(curAccCode == null ? new byte[ACC_CODE_SIZE] : curAccCode) + .array()); + Logger.info(logger, "Configuration written"); + } + + private static ConfigurationState parseConfigState(Version version, byte[] status) { + return new ConfigurationState( + version, ByteBuffer.wrap(status, 4, 2).order(ByteOrder.LITTLE_ENDIAN).getShort()); + } + + // From nfcforum-ts-rtd-uri-1.0.pdf + private static final String[] NDEF_URL_PREFIXES = { + "http://www.", + "https://www.", + "http://", + "https://", + "tel:", + "mailto:", + "ftp://anonymous:anonymous@", + "ftp://ftp.", + "ftps://", + "sftp://", + "smb://", + "nfs://", + "ftp://", + "dav://", + "news:", + "telnet://", + "imap:", + "rtsp://", + "urn:", + "pop:", + "sip:", + "sips:", + "tftp:", + "btspp://", + "btl2cap://", + "btgoep://", + "tcpobex://", + "irdaobex://", + "file://", + "urn:epc:id:", + "urn:epc:tag:", + "urn:epc:pat:", + "urn:epc:raw:", + "urn:epc:", + "urn:nfc:" + }; + + private static byte[] buildNdefConfig(String uri) { + byte idCode = 0; + for (int i = 0; i < NDEF_URL_PREFIXES.length; i++) { + String prefix = NDEF_URL_PREFIXES[i]; + if (uri.startsWith(prefix)) { + idCode = (byte) (i + 1); + uri = uri.substring(prefix.length()); + break; + } } - - /** - * Configure the NFC NDEF payload, and which slot to use. - *

- * This functionality requires support for {@link #FEATURE_NDEF}, available on YubiKey 3 or later. - * - * @param slot the YubiKey slot to append to the uri payload - * @param uri the URI prefix (if null, the default "https://my.yubico.com/yk/#" will be used) - * @param curAccCode the current access code, if needed - * @throws IOException in case of communication error - * @throws CommandException in case of an error response from the YubiKey - */ - @SuppressWarnings("JavadocLinkAsPlainText") - public void setNdefConfiguration(Slot slot, @Nullable String uri, @Nullable byte[] curAccCode) throws IOException, CommandException { - Logger.debug(logger, "Writing NDEF configuration for slot {} ", slot); - require(FEATURE_NDEF); - writeConfig( - slot.map(CMD_NDEF_1, CMD_NDEF_2), - buildNdefConfig(uri == null ? DEFAULT_NDEF_URI : uri), - curAccCode - ); + byte[] uriBytes = uri.getBytes(StandardCharsets.UTF_8); + int dataLength = 1 + uriBytes.length; + if (dataLength > NDEF_DATA_SIZE) { + throw new IllegalArgumentException("URI payload too large"); } - - - /** - * Calculates HMAC-SHA1 on given challenge (using secret that configured/programmed on YubiKey) - *

- * This functionality requires support for {@link #FEATURE_CHALLENGE_RESPONSE}, available on YubiKey 2.2 or later. - * - * @param slot the slot on YubiKey that configured with challenge response secret - * @param challenge generated challenge that will be sent - * @param state if false, the command will be aborted in case the credential requires user touch - * @return response on challenge returned from YubiKey - * @throws IOException in case of communication error, or no key configured in slot - * @throws CommandException in case of an error response from the YubiKey - */ - public byte[] calculateHmacSha1(Slot slot, byte[] challenge, @Nullable CommandState state) throws IOException, CommandException { - Logger.debug(logger, "Calculating response for slog {}", slot); - require(FEATURE_CHALLENGE_RESPONSE); - - // Pad challenge with byte different from last. - byte[] padded = new byte[HMAC_CHALLENGE_SIZE]; - Arrays.fill(padded, (byte) (challenge[challenge.length - 1] == 0 ? 1 : 0)); - System.arraycopy(challenge, 0, padded, 0, challenge.length); - - // response for HMAC-SHA1 challenge response is always 20 bytes - return backend.sendAndReceive( - slot.map(CMD_CHALLENGE_HMAC_1, CMD_CHALLENGE_HMAC_2), - padded, - HMAC_RESPONSE_SIZE, - state - ); + return ByteBuffer.allocate(2 + NDEF_DATA_SIZE) + .put((byte) dataLength) + .put((byte) 'U') + .put(idCode) + .put(uriBytes) + .array(); + } + + private abstract static class Backend implements Closeable { + protected final T delegate; + protected final Version version; + protected ConfigurationState configurationState; + + private Backend(T delegate, Version version, ConfigurationState configurationState) { + this.version = version; + this.delegate = delegate; + this.configurationState = configurationState; } - private void writeConfig(byte commandSlot, byte[] config, @Nullable byte[] curAccCode) throws IOException, CommandException { - Logger.debug(logger, "Writing configuration to slot {}, access code: {}", - commandSlot, curAccCode != null); - backend.writeToSlot( - commandSlot, - ByteBuffer.allocate(config.length + ACC_CODE_SIZE) - .put(config) - .put(curAccCode == null ? new byte[ACC_CODE_SIZE] : curAccCode) - .array() - ); - Logger.info(logger, "Configuration written"); - } + abstract void writeToSlot(byte slot, byte[] data) throws IOException, CommandException; - private static ConfigurationState parseConfigState(Version version, byte[] status) { - return new ConfigurationState(version, ByteBuffer.wrap(status, 4, 2).order(ByteOrder.LITTLE_ENDIAN).getShort()); - } + abstract byte[] sendAndReceive( + byte slot, byte[] data, int expectedResponseLength, @Nullable CommandState state) + throws IOException, CommandException; - // From nfcforum-ts-rtd-uri-1.0.pdf - private static final String[] NDEF_URL_PREFIXES = { - "http://www.", - "https://www.", - "http://", - "https://", - "tel:", - "mailto:", - "ftp://anonymous:anonymous@", - "ftp://ftp.", - "ftps://", - "sftp://", - "smb://", - "nfs://", - "ftp://", - "dav://", - "news:", - "telnet://", - "imap:", - "rtsp://", - "urn:", - "pop:", - "sip:", - "sips:", - "tftp:", - "btspp://", - "btl2cap://", - "btgoep://", - "tcpobex://", - "irdaobex://", - "file://", - "urn:epc:id:", - "urn:epc:tag:", - "urn:epc:pat:", - "urn:epc:raw:", - "urn:epc:", - "urn:nfc:" - }; - - private static byte[] buildNdefConfig(String uri) { - byte idCode = 0; - for (int i = 0; i < NDEF_URL_PREFIXES.length; i++) { - String prefix = NDEF_URL_PREFIXES[i]; - if (uri.startsWith(prefix)) { - idCode = (byte) (i + 1); - uri = uri.substring(prefix.length()); - break; - } - } - byte[] uriBytes = uri.getBytes(StandardCharsets.UTF_8); - int dataLength = 1 + uriBytes.length; - if (dataLength > NDEF_DATA_SIZE) { - throw new IllegalArgumentException("URI payload too large"); - } - return ByteBuffer.allocate(2 + NDEF_DATA_SIZE) - .put((byte) dataLength) - .put((byte) 'U') - .put(idCode) - .put(uriBytes) - .array(); + @Override + public void close() throws IOException { + delegate.close(); } - - private static abstract class Backend implements Closeable { - protected final T delegate; - protected final Version version; - protected ConfigurationState configurationState; - - private Backend(T delegate, Version version, ConfigurationState configurationState) { - this.version = version; - this.delegate = delegate; - this.configurationState = configurationState; - } - - abstract void writeToSlot(byte slot, byte[] data) throws IOException, CommandException; - - abstract byte[] sendAndReceive(byte slot, byte[] data, int expectedResponseLength, @Nullable CommandState state) throws IOException, CommandException; - - @Override - public void close() throws IOException { - delegate.close(); - } + } + + private void logCtor(YubiKeyConnection connection) { + final Version version = getVersion(); + Logger.debug( + logger, + "YubiOTP session initialized for connection={}, version={}, ledInverted={}", + connection.getClass().getSimpleName(), + version, + backend.configurationState.isLedInverted()); + + Boolean slotOneConfigured = null; + Boolean slotTwoConfigured = null; + if (FEATURE_CHECK_CONFIGURED.isSupportedBy(version)) { + slotOneConfigured = backend.configurationState.isConfigured(Slot.ONE); + slotTwoConfigured = backend.configurationState.isConfigured(Slot.TWO); + } else { + Logger.debug( + logger, "This YubiKey does not support checking whether OTP slot " + "is configured"); } - private void logCtor(YubiKeyConnection connection) { - final Version version = getVersion(); - Logger.debug(logger, "YubiOTP session initialized for connection={}, version={}, ledInverted={}", - connection.getClass().getSimpleName(), - version, - backend.configurationState.isLedInverted()); - - Boolean slotOneConfigured = null; - Boolean slotTwoConfigured = null; - if (FEATURE_CHECK_CONFIGURED.isSupportedBy(version)) { - slotOneConfigured = backend.configurationState.isConfigured(Slot.ONE); - slotTwoConfigured = backend.configurationState.isConfigured(Slot.TWO); - } else { - Logger.debug(logger, "This YubiKey does not support checking whether OTP slot " + - "is configured"); - } - - Boolean slotOneTouchTriggered = null; - Boolean slotTwoTouchTriggered = null; - if (FEATURE_CHECK_TOUCH_TRIGGERED.isSupportedBy(version)) { - slotOneTouchTriggered = backend.configurationState.isTouchTriggered(Slot.ONE); - slotTwoTouchTriggered = backend.configurationState.isTouchTriggered(Slot.TWO); - } else { - Logger.debug(logger, "This YubiKey does not support checking whether OTP slot is " + - "touch triggered"); - } - Logger.debug(logger, "Configuration slot 1: configured={}, touchTriggered={}", - slotOneConfigured != null - ? slotOneConfigured - : "?", - slotOneTouchTriggered != null - ? slotOneTouchTriggered - : "?"); - Logger.debug(logger, "Configuration slot 2: configured={}, touchTriggered={}", - slotTwoConfigured != null - ? slotTwoConfigured - : "?", - slotTwoTouchTriggered != null - ? slotTwoTouchTriggered - : "?"); + Boolean slotOneTouchTriggered = null; + Boolean slotTwoTouchTriggered = null; + if (FEATURE_CHECK_TOUCH_TRIGGERED.isSupportedBy(version)) { + slotOneTouchTriggered = backend.configurationState.isTouchTriggered(Slot.ONE); + slotTwoTouchTriggered = backend.configurationState.isTouchTriggered(Slot.TWO); + } else { + Logger.debug( + logger, + "This YubiKey does not support checking whether OTP slot is " + "touch triggered"); } -} \ No newline at end of file + Logger.debug( + logger, + "Configuration slot 1: configured={}, touchTriggered={}", + slotOneConfigured != null ? slotOneConfigured : "?", + slotOneTouchTriggered != null ? slotOneTouchTriggered : "?"); + Logger.debug( + logger, + "Configuration slot 2: configured={}, touchTriggered={}", + slotTwoConfigured != null ? slotTwoConfigured : "?", + slotTwoTouchTriggered != null ? slotTwoTouchTriggered : "?"); + } +} diff --git a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/YubiOtpSlotConfiguration.java b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/YubiOtpSlotConfiguration.java index 208c6983..1d3ed095 100755 --- a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/YubiOtpSlotConfiguration.java +++ b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/YubiOtpSlotConfiguration.java @@ -17,65 +17,63 @@ import java.util.Arrays; -/** - * Configures the YubiKey to return YubiOTP (one-time password) on touch. - */ +/** Configures the YubiKey to return YubiOTP (one-time password) on touch. */ public class YubiOtpSlotConfiguration extends KeyboardSlotConfiguration { - /** - * Creates a Yubico OTP configuration with default settings. - * - * @param fixed "public ID" static portion of each OTP (0-16 bytes) - * @param uid "private id" internal to the credential (6 bytes) - * @param key the AES key to encrypt the OTP payload (20 bytes) - */ - public YubiOtpSlotConfiguration(byte[] fixed, byte[] uid, byte[] key) { - if (fixed.length > FIXED_SIZE) { - throw new IllegalArgumentException("Public ID must be <= 16 bytes"); - } - - this.fixed = Arrays.copyOf(fixed, fixed.length); - System.arraycopy(uid, 0, this.uid, 0, uid.length); - System.arraycopy(key, 0, this.key, 0, key.length); + /** + * Creates a Yubico OTP configuration with default settings. + * + * @param fixed "public ID" static portion of each OTP (0-16 bytes) + * @param uid "private id" internal to the credential (6 bytes) + * @param key the AES key to encrypt the OTP payload (20 bytes) + */ + public YubiOtpSlotConfiguration(byte[] fixed, byte[] uid, byte[] key) { + if (fixed.length > FIXED_SIZE) { + throw new IllegalArgumentException("Public ID must be <= 16 bytes"); } - @Override - protected YubiOtpSlotConfiguration getThis() { - return this; - } + this.fixed = Arrays.copyOf(fixed, fixed.length); + System.arraycopy(uid, 0, this.uid, 0, uid.length); + System.arraycopy(key, 0, this.key, 0, key.length); + } - /** - * Inserts tabs in-between different parts of the OTP. - * - * @param before inserts a tab before any other output (default: false) - * @param afterFirst inserts a tab after the static part of the OTP (default: false) - * @param afterSecond inserts a tab after the end of the OTP (default: false) - * @return the configuration for chaining - */ - public YubiOtpSlotConfiguration tabs(boolean before, boolean afterFirst, boolean afterSecond) { - updateFlags(TKTFLAG_TAB_FIRST, before); - updateFlags(TKTFLAG_APPEND_TAB1, afterFirst); - return updateFlags(TKTFLAG_APPEND_TAB2, afterSecond); - } + @Override + protected YubiOtpSlotConfiguration getThis() { + return this; + } - /** - * Inserts delays in-between different parts of the OTP. - * - * @param afterFirst inserts a delay after the static part of the OTP (default: false) - * @param afterSecond inserts a delay after the end of the OTP (default: false) - * @return the configuration for chaining - */ - public YubiOtpSlotConfiguration delay(boolean afterFirst, boolean afterSecond) { - updateFlags(TKTFLAG_APPEND_DELAY1, afterFirst); - return updateFlags(TKTFLAG_APPEND_DELAY2, afterSecond); - } + /** + * Inserts tabs in-between different parts of the OTP. + * + * @param before inserts a tab before any other output (default: false) + * @param afterFirst inserts a tab after the static part of the OTP (default: false) + * @param afterSecond inserts a tab after the end of the OTP (default: false) + * @return the configuration for chaining + */ + public YubiOtpSlotConfiguration tabs(boolean before, boolean afterFirst, boolean afterSecond) { + updateFlags(TKTFLAG_TAB_FIRST, before); + updateFlags(TKTFLAG_APPEND_TAB1, afterFirst); + return updateFlags(TKTFLAG_APPEND_TAB2, afterSecond); + } - /** - * Send a reference string of all 16 modhex characters before the OTP. - * - * @param sendReference if true, sends the reference string (default: false) - * @return the configuration for chaining - */ - public YubiOtpSlotConfiguration sendReference(boolean sendReference) { - return updateFlags(CFGFLAG_SEND_REF, sendReference); - } + /** + * Inserts delays in-between different parts of the OTP. + * + * @param afterFirst inserts a delay after the static part of the OTP (default: false) + * @param afterSecond inserts a delay after the end of the OTP (default: false) + * @return the configuration for chaining + */ + public YubiOtpSlotConfiguration delay(boolean afterFirst, boolean afterSecond) { + updateFlags(TKTFLAG_APPEND_DELAY1, afterFirst); + return updateFlags(TKTFLAG_APPEND_DELAY2, afterSecond); + } + + /** + * Send a reference string of all 16 modhex characters before the OTP. + * + * @param sendReference if true, sends the reference string (default: false) + * @return the configuration for chaining + */ + public YubiOtpSlotConfiguration sendReference(boolean sendReference) { + return updateFlags(CFGFLAG_SEND_REF, sendReference); + } } diff --git a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/package-info.java b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/package-info.java index 3b8740a8..723bd03f 100755 --- a/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/package-info.java +++ b/yubiotp/src/main/java/com/yubico/yubikit/yubiotp/package-info.java @@ -16,4 +16,4 @@ @PackageNonnullByDefault package com.yubico.yubikit.yubiotp; -import com.yubico.yubikit.core.PackageNonnullByDefault; \ No newline at end of file +import com.yubico.yubikit.core.PackageNonnullByDefault; diff --git a/yubiotp/src/test/java/com/yubico/yubikit/yubiotp/BaseSlotConfigurationTest.java b/yubiotp/src/test/java/com/yubico/yubikit/yubiotp/BaseSlotConfigurationTest.java index 00fe8df1..0836f602 100755 --- a/yubiotp/src/test/java/com/yubico/yubikit/yubiotp/BaseSlotConfigurationTest.java +++ b/yubiotp/src/test/java/com/yubico/yubikit/yubiotp/BaseSlotConfigurationTest.java @@ -16,31 +16,25 @@ package com.yubico.yubikit.yubiotp; -import org.junit.Test; - import static org.junit.Assert.assertEquals; +import org.junit.Test; + public class BaseSlotConfigurationTest { - @Test - public void testBuildConfig() { - byte[] fixed = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}; - byte[] uid = {0x11, 0x12, 0x13, 0x14, 0x15, 0x16}; - byte[] key = {0x20, 0x21, 0x22, 0x23, 0x24, 0x25, - 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f}; - byte tktFlags = SlotConfiguration.TKTFLAG_APPEND_CR.bit; - byte[] config = BaseSlotConfiguration.buildConfig( - fixed, - uid, - key, - (byte) 0, - tktFlags, - (byte) 0, - null - ); + @Test + public void testBuildConfig() { + byte[] fixed = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}; + byte[] uid = {0x11, 0x12, 0x13, 0x14, 0x15, 0x16}; + byte[] key = { + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f + }; + byte tktFlags = SlotConfiguration.TKTFLAG_APPEND_CR.bit; + byte[] config = + BaseSlotConfiguration.buildConfig(fixed, uid, key, (byte) 0, tktFlags, (byte) 0, null); - assertEquals(52, config.length); - assertEquals(fixed.length, config[44]); - assertEquals(tktFlags, config[46]); - } -} \ No newline at end of file + assertEquals(52, config.length); + assertEquals(fixed.length, config[44]); + assertEquals(tktFlags, config[46]); + } +}