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 60125513..e23c4352 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 @@ -128,4 +128,30 @@ public void deleteCredential(PublicKeyCredentialDescriptor credential) throws IO 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/ctap/CredentialManagement.java b/fido/src/main/java/com/yubico/yubikit/fido/ctap/CredentialManagement.java index 1e25b02f..e371f646 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 @@ -24,6 +24,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -84,11 +85,16 @@ public CredentialManagement( } public static boolean isSupported(Ctap2Session.InfoData info) { - final Map options = info.getOptions(); - if (Boolean.TRUE.equals(options.get("credMgmt"))) { - return true; - } else return info.getVersions().contains("FIDO_2_1_PRE") && - Boolean.TRUE.equals(options.get("credentialMgmtPreview")); + 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( @@ -202,6 +208,36 @@ public void deleteCredential(Map credentialId) throws IOException, Co 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"); + } + + 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. */ diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementInstrumentedTests.java index 7c89d887..d5d51dbe 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/Ctap2CredentialManagementInstrumentedTests.java @@ -44,6 +44,12 @@ public void testReadMetadata() throws Throwable { public void testManagement() throws Throwable { withCtap2Session(Ctap2CredentialManagementTests::testManagement); } + + @Test + @Category(SmokeTest.class) + public void testUpdateUserInformation() throws Throwable { + withCtap2Session(Ctap2CredentialManagementTests::testUpdateUserInformation); + } } @Category(PinUvAuthProtocolV1Test.class) 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 501bfd0f..c2f53b8d 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 @@ -711,6 +711,25 @@ public static void testClientCredentialManagement(FidoTestState state) throws Th 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()); 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 66e821e1..4ef9a858 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 @@ -26,6 +26,7 @@ import com.yubico.yubikit.fido.ctap.ClientPin; import com.yubico.yubikit.fido.ctap.CredentialManagement; 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; @@ -79,17 +80,72 @@ public static void testReadMetadata(Ctap2Session session, FidoTestState state) t public static void testManagement(Ctap2Session session, FidoTestState state) throws Throwable { CredentialManagement credentialManagement = setupCredentialManagement(session, state); + assertThat(credentialManagement.enumerateRps(), empty()); - final SerializationType cborType = SerializationType.CBOR; + 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()); - Map options = new HashMap<>(); - options.put("rk", true); + byte[] pinToken = new ClientPin(session, credentialManagement.getPinUvAuth()).getPinToken( + TestData.PIN, + ClientPin.PIN_PERMISSION_MC, + TestData.RP.getId()); - 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), @@ -97,30 +153,22 @@ public static void testManagement(Ctap2Session session, FidoTestState state) thr Collections.singletonList(TestData.PUB_KEY_CRED_PARAMS_ES256.toMap(cborType)), null, null, - options, + Collections.singletonMap("rk", true), pinAuth, state.getPinUvAuthProtocol().getVersion(), null, null ); + } - - // this sets correct permission for handling credential management commands - credentialManagement = setupCredentialManagement(session, state); - + 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)); - CredentialManagement.CredentialData credData = creds.get(0); - 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); + return creds.get(0); } + }