Commit d4258765 authored by 0Tyler's avatar 0Tyler

Initial commit

parents
Pipeline #1672 canceled with stages
target/
.settings/
.classpath
.project
src/main/java/META-INF/
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="gaedemo" />
</profile>
</annotationProcessing>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8.0_221" project-jdk-type="JavaSDK" />
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
</set>
</option>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
\ No newline at end of file
# How to Contribute
We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.
## Contributor License Agreement
Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution,
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to <https://cla.developers.google.com/> to see
your current agreements on file or to sign a new one.
You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.
## Code reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.
This diff is collapsed.
# WebAuthnDemo
An example Java Relying Party implementation of the [WebAuthn
specification](https://w3c.github.io/webauthn/).
### Install
The demo is written on top of Google App Engine. Run
```sh
$ mvn appengine:devserver
```
for a local development server instance.
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="FacetManager">
<facet type="google-app-engine" name="Google App Engine">
<configuration />
</facet>
<facet type="app-engine-standard" name="Google App Engine Standard">
<configuration />
</facet>
</component>
</module>
\ No newline at end of file
This diff is collapsed.
mvn appengine:devserver
\ No newline at end of file
/*
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.google.webauthn.gaedemo.crypto;
import com.google.webauthn.gaedemo.objects.Algorithm;
import org.jose4j.jws.BaseSignatureAlgorithm;
import org.jose4j.jws.EcdsaUsingShaAlgorithm;
import org.jose4j.jws.RsaUsingShaAlgorithm;
import java.util.HashMap;
import java.util.Map;
public class AlgorithmIdentifierMapper {
private static final Map<Algorithm, BaseSignatureAlgorithm> map = new HashMap<>();
static {
map.put(Algorithm.ES256, new EcdsaUsingShaAlgorithm.EcdsaP256UsingSha256());
map.put(Algorithm.ES384, new EcdsaUsingShaAlgorithm.EcdsaP384UsingSha384());
map.put(Algorithm.ES512, new EcdsaUsingShaAlgorithm.EcdsaP521UsingSha512());
map.put(Algorithm.RS256, new RsaUsingShaAlgorithm.RsaSha256());
map.put(Algorithm.RS384, new RsaUsingShaAlgorithm.RsaSha384());
map.put(Algorithm.RS512, new RsaUsingShaAlgorithm.RsaSha512());
map.put(Algorithm.PS256, new RsaUsingShaAlgorithm.RsaPssSha256());
map.put(Algorithm.PS384, new RsaUsingShaAlgorithm.RsaPssSha384());
map.put(Algorithm.PS512, new RsaUsingShaAlgorithm.RsaPssSha512());
}
public static BaseSignatureAlgorithm get(Algorithm algorithm) {
return map.get(algorithm);
}
}
/*
* Copyright 2018 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.google.webauthn.gaedemo.crypto;
import com.google.webauthn.gaedemo.objects.CablePairingData;
import com.google.webauthn.gaedemo.objects.CableSessionData;
import org.bouncycastle.util.Arrays;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Random;
public class Cable {
private static final byte[] HMAC_TAG_CLIENT_EID =
"client".getBytes(StandardCharsets.UTF_8);
private static final byte[] HMAC_TAG_AUTHENTICATOR_EID =
"authenticator".getBytes(StandardCharsets.UTF_8);
private static final byte[] HKDF_INFO_SESSION_PRE_KEY =
"FIDO caBLE v1 sessionPreKey".getBytes(StandardCharsets.UTF_8);
private final Random random;
public Cable() {
this(new SecureRandom());
}
Cable(Random random) {
this.random = random;
}
public CableSessionData generateSessionData(CablePairingData pairingData) {
byte[] nonce = new byte[8];
random.nextBytes(nonce);
byte[] clientEidHash = Crypto.hmacSha256(pairingData.irk,
Arrays.concatenate(nonce, HMAC_TAG_CLIENT_EID), 8);
byte[] clientEid = Arrays.concatenate(nonce, clientEidHash);
byte[] authenticatorEid = Crypto.hmacSha256(pairingData.irk,
Arrays.concatenate(clientEid, HMAC_TAG_AUTHENTICATOR_EID), 16);
byte[] sessionPreKey = Crypto.hkdfSha256(pairingData.lk, nonce,
HKDF_INFO_SESSION_PRE_KEY, 32);
return new CableSessionData(pairingData.version, clientEid, authenticatorEid, sessionPreKey);
}
}
This diff is collapsed.
/*
* Copyright 2016 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*
*/
package com.google.webauthn.gaedemo.crypto;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.json.webtoken.JsonWebSignature;
import com.google.api.client.util.Base64;
import com.google.api.client.util.Key;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import javax.net.ssl.SSLException;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
/**
* Sample code to verify the device attestation statement offline.
*/
public class OfflineVerify {
private static final DefaultHostnameVerifier HOSTNAME_VERIFIER = new DefaultHostnameVerifier();
/**
* Class for parsing the JSON data.
*/
public static class AttestationStatement extends JsonWebSignature.Payload {
@Key
private String nonce;
@Key
private long timestampMs;
@Key
private String apkPackageName;
@Key
private String apkDigestSha256;
@Key
private boolean ctsProfileMatch;
public byte[] getNonce() {
return Base64.decodeBase64(nonce);
}
public long getTimestampMs() {
return timestampMs;
}
public String getApkPackageName() {
return apkPackageName;
}
public byte[] getApkDigestSha256() {
return Base64.decodeBase64(apkDigestSha256);
}
public boolean isCtsProfileMatch() {
return ctsProfileMatch;
}
}
public static AttestationStatement parseAndVerify(String signedAttestationStatement) {
// Parse JSON Web Signature format.
JsonWebSignature jws;
try {
jws = JsonWebSignature.parser(JacksonFactory.getDefaultInstance())
.setPayloadClass(AttestationStatement.class).parse(signedAttestationStatement);
} catch (IOException e) {
System.err
.println("Failure: " + signedAttestationStatement + " is not valid JWS " + "format.");
return null;
}
// Verify the signature of the JWS and retrieve the signature certificate.
X509Certificate cert;
try {
cert = jws.verifySignature();
if (cert == null) {
System.err.println("Failure: Signature verification failed.");
return null;
}
} catch (GeneralSecurityException e) {
System.err.println("Failure: Error during cryptographic verification of the JWS signature.");
return null;
}
// Verify the hostname of the certificate.
if (!verifyHostname("attest.android.com", cert)) {
System.err
.println("Failure: Certificate isn't issued for the hostname attest.android" + ".com.");
return null;
}
// Extract and use the payload data.
AttestationStatement stmt = (AttestationStatement) jws.getPayload();
return stmt;
}
/**
* Verifies that the certificate matches the specified hostname. Uses the
* {@link DefaultHostnameVerifier} from the Apache HttpClient library to confirm that the hostname
* matches the certificate.
*
* @param hostname
* @param leafCert
* @return
*/
private static boolean verifyHostname(String hostname, X509Certificate leafCert) {
try {
// Check that the hostname matches the certificate. This method throws an exception if
// the cert could not be verified.
HOSTNAME_VERIFIER.verify(hostname, leafCert);
return true;
} catch (SSLException e) {
return false;
}
}
private static void process(String signedAttestationStatement) {
AttestationStatement stmt = parseAndVerify(signedAttestationStatement);
if (stmt == null) {
System.err.println("Failure: Failed to parse and verify the attestation statement.");
return;
}
System.out.println("Successfully verified the attestation statement. The content is:");
System.out.println("Nonce: " + Arrays.toString(stmt.getNonce()));
System.out.println("Timestamp: " + stmt.getTimestampMs() + " ms");
System.out.println("APK package name: " + stmt.getApkPackageName());
System.out.println("APK digest SHA256: " + Arrays.toString(stmt.getApkDigestSha256()));
System.out.println("CTS profile match: " + stmt.isCtsProfileMatch());
System.out.println("\n** This sample only shows how to verify the authenticity of an "
+ "attestation response. Next, you must check that the server response matches the "
+ "request by comparing the nonce, package name, timestamp and digest.");
}
public static void main(String[] args) {
if (args.length != 1) {
System.err.println("Usage: OfflineVerify <signed attestation statement>");
return;
}
process(args[0]);
}
}
package com.google.webauthn.gaedemo.endpoints;
/**
* Contains the client IDs and scopes for allowed clients consuming FIDO2 API.
*/
public class Constants {
// Scopes: https://developers.google.com/identity/protocols/googlescopes
public static final String EMAIL_SCOPE = "https://www.googleapis.com/auth/userinfo.email";
public static final String OPENID_SCOPE = "openid";
// TODO: configure Client IDs for android and web clients.
// ClientIds:
// https://cloud.google.com/endpoints/docs/frameworks/java/creating-client-ids
public static final String WEB_CLIENT_ID =
"762961289381-hbbpkaqgi1kelev5mquj4dg4n8glr59p.apps.googleusercontent.com";
public static final String ANDROID_CLIENT_ID =
"762961289381-d27cfmh1st5m681lo2r7837hg0a6b0pm.apps.googleusercontent.com";
public static final String ANDROID_CLIENT_ID2 =
"762961289381-87bpacb39s1pi3tsi2ahlsqvsmuh3odr.apps.googleusercontent.com";
public static final String ANDROID_AUDIENCE = WEB_CLIENT_ID;
public static final String APP_ID = "webauthndemo.appspot.com";
}
package com.google.webauthn.gaedemo.endpoints;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Named;
import javax.servlet.ServletException;
import com.google.api.server.spi.config.Api;
import com.google.api.server.spi.config.ApiMethod;
import com.google.api.server.spi.config.ApiNamespace;
import com.google.appengine.api.oauth.OAuthRequestException;
import com.google.appengine.api.users.User;
import com.google.common.io.BaseEncoding;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.webauthn.gaedemo.exceptions.ResponseException;
import com.google.webauthn.gaedemo.objects.AndroidSafetyNetAttestationStatement;
import com.google.webauthn.gaedemo.objects.AttestationObject;
import com.google.webauthn.gaedemo.objects.AuthenticatorAssertionResponse;
import com.google.webauthn.gaedemo.objects.AuthenticatorAttestationResponse;
import com.google.webauthn.gaedemo.objects.FidoU2fAttestationStatement;
import com.google.webauthn.gaedemo.objects.PublicKeyCredential;
import com.google.webauthn.gaedemo.objects.PublicKeyCredentialCreationOptions;
import com.google.webauthn.gaedemo.objects.PublicKeyCredentialRequestOptions;
import com.google.webauthn.gaedemo.server.AndroidSafetyNetServer;
import com.google.webauthn.gaedemo.server.PackedServer;
import com.google.webauthn.gaedemo.server.Server;
import com.google.webauthn.gaedemo.server.U2fServer;
import com.google.webauthn.gaedemo.storage.Credential;
import com.google.webauthn.gaedemo.storage.SessionData;
/**
* An endpoint class for handling FIDO2 requests.
*
* Google Cloud Endpoints generate APIs and client libraries from API backend, to simplify client
* access to data from other applications.
* https://cloud.google.com/endpoints/docs/frameworks/java/get-started-frameworks-java
*/
@Api(
name = "fido2RequestHandler",
version = "v1",
scopes = {Constants.EMAIL_SCOPE, Constants.OPENID_SCOPE},
clientIds = {Constants.WEB_CLIENT_ID, Constants.ANDROID_CLIENT_ID,
Constants.ANDROID_CLIENT_ID2},
audiences = {Constants.ANDROID_AUDIENCE},
namespace = @ApiNamespace(
ownerName = "gaedemo.webauthn.google.com", ownerDomain = "gaedemo.webauthn.google.com")
)
public class Fido2RequestHandler {
@ApiMethod(name = "getRegistrationRequest", path="get/register")
public List<String> getRegistrationRequest(User user) throws OAuthRequestException {
if (user == null) {
throw new OAuthRequestException("User is not authenticated");
}
PublicKeyCredentialCreationOptions options = new PublicKeyCredentialCreationOptions(
user.getNickname() /* userName */, user.getEmail() /* userId */,
Constants.APP_ID /* rpId */, Constants.APP_ID /* rpName */);
SessionData session = new SessionData(options.challenge, Constants.APP_ID);
session.save(user.getEmail());
JsonObject sessionJson = session.getJsonObject();
JsonObject optionsJson = options.getJsonObject();
optionsJson.add("session", sessionJson);
List<String> resultList = new ArrayList<String>();
resultList.add(optionsJson.toString());
return resultList;
}
@ApiMethod(name = "processRegistrationResponse")
public List<String> processRegistrationResponse(
@Named("responseData") String responseData, User user)
throws OAuthRequestException, ResponseException, ServletException {
if (user == null) {
throw new OAuthRequestException("User is not authenticated");
}
Gson gson = new Gson();
JsonElement element = gson.fromJson(responseData, JsonElement.class);
JsonObject object = element.getAsJsonObject();
String clientDataJSON = object.get("clientDataJSON").getAsString();
String attestationObject = object.get("attestationObject").getAsString();
AuthenticatorAttestationResponse attestation =
new AuthenticatorAttestationResponse(clientDataJSON, attestationObject);
// TODO
String credentialId = BaseEncoding.base64Url().encode(
attestation.getAttestationObject().getAuthenticatorData().getAttData().getCredentialId());
String type = null;
String session = null;
PublicKeyCredential cred = new PublicKeyCredential(credentialId, type,
BaseEncoding.base64Url().decode(credentialId), attestation);
switch (cred.getAttestationType()) {
case FIDOU2F:
U2fServer.registerCredential(cred, user.getEmail(),
session, Constants.APP_ID, Constants.APP_ID);
break;
case ANDROIDSAFETYNET:
AndroidSafetyNetServer.registerCredential(
cred, user.getEmail(), session, Constants.APP_ID);
break;
case PACKED:
PackedServer.registerCredential(cred, user.getEmail(), session, Constants.APP_ID);
break;
default:
// This should never happen.
}
Credential credential = new Credential(cred);
credential.save(user.getEmail());
List<String> resultList = new ArrayList<String>();
resultList.add(credential.toJson());
return resultList;
}
@ApiMethod(name = "getSignRequest", path="get/sign")
public List<String> getSignRequest(User user) throws OAuthRequestException {
if (user == null) {
throw new OAuthRequestException("User is not authenticated");
}
PublicKeyCredentialRequestOptions assertion =
new PublicKeyCredentialRequestOptions(Constants.APP_ID);
SessionData session = new SessionData(assertion.challenge, Constants.APP_ID);
session.save(user.getEmail());
assertion.populateAllowList(user.getEmail());
JsonObject assertionJson = assertion.getJsonObject();
JsonObject sessionJson = session.getJsonObject();
assertionJson.add("session", sessionJson);
List<String> resultList = new ArrayList<String>();
resultList.add(assertionJson.toString());
return resultList;
}
@ApiMethod(name = "processSignResponse")
public List<String> processSignResponse(
@Named("responseData") String responseData, User user)
throws OAuthRequestException, ResponseException, ServletException {
if (user == null) {
throw new OAuthRequestException("User is not authenticated");
}
Gson gson = new Gson();
JsonElement element = gson.fromJson(responseData, JsonElement.class);
JsonObject object = element.getAsJsonObject();
String clientDataJSON = object.get("clientDataJSON").getAsString();
String authenticatorData = object.get("authenticatorData").getAsString();
String credentialId = object.get("credentialId").getAsString();
String signature = object.get("signature").getAsString();
AuthenticatorAssertionResponse assertion =
new AuthenticatorAssertionResponse(clientDataJSON, authenticatorData, signature);
// TODO
String type = null;
String session = null;
PublicKeyCredential cred = new PublicKeyCredential(credentialId, type,
BaseEncoding.base64Url().decode(credentialId), assertion);
Credential savedCredential;
try {
savedCredential = Server.validateAndFindCredential(cred, user.getEmail(), session);
} catch (ResponseException e) {
throw new ServletException("Unable to validate assertion", e);
}
Server.verifyAssertion(cred, user.getEmail(), session, savedCredential);
List<String> resultList = new ArrayList<String>();
resultList.add(savedCredential.toJson());
return resultList;
}
@ApiMethod(name = "getAllSecurityKeys", path = "getAllSecurityKeys")
public String[] getAllSecurityKeys(User user) throws OAuthRequestException {
if (user == null) {
throw new OAuthRequestException("User is not authenticated");
}
List<Credential> savedCreds = Credential.load(user.getEmail());
JsonArray result = new JsonArray();
for (Credential c : savedCreds) {
JsonObject cJson = new JsonObject();
cJson.addProperty("handle", BaseEncoding.base64Url().encode(c.getCredential().rawId));
// TODO
/*
try {
cJson.addProperty("publicKey", Integer.toHexString(
Crypto.decodePublicKey(ecc.getX(), ecc.getY()).hashCode()));
} catch (WebAuthnException e) {
e.printStackTrace();
continue;
}
*/
AttestationObject attObj =
((AuthenticatorAttestationResponse) c.getCredential().getResponse())
.getAttestationObject();
if (attObj.getAttestationStatement() instanceof FidoU2fAttestationStatement) {
cJson.addProperty("name", "FIDO U2F Authenticator");
} else if (attObj.getAttestationStatement() instanceof AndroidSafetyNetAttestationStatement) {
cJson.addProperty("name", "Android SafetyNet");
}
cJson.addProperty("date", c.getDate().toString());
cJson.addProperty("id", c.id);
result.add(cJson);
}
return new String[] {result.toString()};
}
@ApiMethod(name = "removeSecurityKey")
public String[] removeSecurityKey(User user, @Named("publicKey") String publicKey)
throws OAuthRequestException {
if (user == null) {
throw new OAuthRequestException("User is not authenticated");
}
Credential.remove(user.getEmail(), publicKey);
return new String[] {"OK"};
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.exceptions;
public class ResponseException extends Exception {
/**
*
*/
private static final long serialVersionUID = 6341176597489797230L;
/**
* @param string
*/
public ResponseException(String string) {
super(string);
}
/**
* @param string
*/
public ResponseException(String string, Throwable cause) {
super(string, cause);
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.exceptions;
public class WebAuthnException extends Exception {
/**
*
*/
private static final long serialVersionUID = 3285394858024616570L;
/**
* @param string
*/
public WebAuthnException(String string) {
super(string);
}
public WebAuthnException(String message, Throwable cause) {
super(message, cause);
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
/**
* Algorithm enum differentiating between the supported asymmetric key algorithms
*/
public enum Algorithm {
ES256("ES256"), ES384("ES384"), ES512("ES512"), RS256("RS256"), RS384("RS384"), RS512(
"RS512"), PS256("PS256"), PS384("PS384"), PS512("PS512"),
UNDEFINED("undefined");
final private String name;
/**
* @param name The string representation of the algorithm name
*/
private Algorithm(String name) {
this.name = name;
}
/**
* @param alg The Algorithm to check
* @return If the Algorithm is an ECC Algorithm
*/
public static boolean isEccAlgorithm(Algorithm alg) {
return alg == ES256 || alg == ES384 || alg == ES512;
}
/**
* @param alg The Algorithm to check
* @return If the Algorithm is an RSA Algorithm
*/
public static boolean isRsaAlgorithm(Algorithm alg) {
return alg == RS256 || alg == RS384 || alg == RS512 || alg == PS256 || alg == PS384
|| alg == PS512;
}
/**
* @param s Input string to decode
* @return Transport corresponding to the input string
*/
public static Algorithm decode(String s) {
for (Algorithm t : Algorithm.values()) {
if (t.name.equals(s)) {
return t;
}
}
// COSE Algorithm Identifiers
if (s.equals("-7")) {
return ES256;
}
throw new IllegalArgumentException(s + " not a valid Algorithm");
}
/**
* @param alg Input integer to decode
* @return Transport corresponding to the input string
*/
public static Algorithm decode(int alg) {
switch (alg) {
case -7:
return ES256;
case -35:
return ES384;
case -36:
return ES512;
case -37:
return PS256;
case -38:
return PS384;
case -39:
return PS512;
case -257:
return RS256;
case -258:
return RS384;
case -259:
return RS512;
case -260:
return ES256;
case -261:
return ES512;
}
return Algorithm.UNDEFINED;
}
public int encodeToInt() {
switch (this) {
case ES256:
return -7;
case ES384:
return -35;
case ES512:
return -36;
case PS256:
return -37;
case PS384:
return -38;
case PS512:
return -39;
case RS256:
return -257;
case RS384:
return -258;
case RS512:
return -259;
default:
}
return -1;
}
@Override
public String toString() {
return name;
}
public Object toReadableString() {
return name;
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import java.util.Arrays;
import java.util.Objects;
import com.google.webauthn.gaedemo.exceptions.ResponseException;
import com.googlecode.objectify.annotation.Subclass;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.model.ByteString;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.Map;
import co.nstant.in.cbor.model.UnicodeString;
/**
* Object representation of the Android SafetyNet attestation statement
*/
@Subclass
public class AndroidSafetyNetAttestationStatement extends AttestationStatement {
String ver;
byte[] response;
public AndroidSafetyNetAttestationStatement() {
this.ver = null;
this.response = null;
}
/**
* Decodes a cbor representation of an AndroidSafetyNetAttestationStatement into the object
* representation
*
* @param attStmt Cbor DataItem representation of the attestation statement to decode
* @return Decoded AndroidSafetyNetAttestationStatement
* @throws ResponseException Input was not a valid AndroidSafetyNetAttestationStatement DataItem
*/
public static AndroidSafetyNetAttestationStatement decode(DataItem attStmt)
throws ResponseException {
AndroidSafetyNetAttestationStatement result = new AndroidSafetyNetAttestationStatement();
Map given = (Map) attStmt;
for (DataItem data : given.getKeys()) {
if (data instanceof UnicodeString) {
if (((UnicodeString) data).getString().equals("ver")) {
UnicodeString version = (UnicodeString) given.get(data);
result.ver = version.getString();
} else if (((UnicodeString) data).getString().equals("response")) {
result.response = ((ByteString) (given.get(data))).getBytes();
}
}
}
if (result.response == null || result.ver == null)
throw new ResponseException("Invalid JWT Cbor");
return result;
}
@Override
DataItem encode() throws CborException {
Map map = new Map();
map.put(new UnicodeString("ver"), new UnicodeString(ver));
map.put(new UnicodeString("response"), new ByteString(response));
return map;
}
@Override
public int hashCode() {
return Objects.hash(ver, Arrays.hashCode(response));
}
@Override
public boolean equals(Object obj) {
if (obj instanceof AndroidSafetyNetAttestationStatement) {
AndroidSafetyNetAttestationStatement other = (AndroidSafetyNetAttestationStatement) obj;
try {
if (!ver.equals(other.ver)) {
return false;
}
} catch (NullPointerException e) {
return false;
}
return Arrays.equals(response, other.response);
}
return false;
}
/**
* Return the Google Play Services version used to create the SafetyNet attestation
*
* @return the version
*/
public String getVer() {
return ver;
}
/**
* @return the response bytes
*/
public byte[] getResponse() {
return response;
}
@Override
public String getName() {
return "Android SafetyNet";
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
public enum Attachment {
PLATFORM("platform"), CROSS_PLATFORM("cross-platform");
final String name;
private Attachment(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
public enum AttestationConveyancePreference {
NONE("none"), INDIRECT("indirect"), DIRECT("direct");
private final String val;
AttestationConveyancePreference(String s) {
this.val = s;
}
/**
* @param s
* @return AuthenticatorAttachment corresponding to the input string
*/
public static AttestationConveyancePreference decode(String s) {
for (AttestationConveyancePreference a : AttestationConveyancePreference.values()) {
if (a.val.equals(s)) {
return a;
}
}
throw new IllegalArgumentException(s + " not a valid AttestationConveyancePreference");
}
@Override
public String toString() {
return this.val;
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import co.nstant.in.cbor.CborException;
import com.google.common.primitives.Bytes;
import com.google.webauthn.gaedemo.exceptions.ResponseException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Objects;
/**
* Object representation of the attestation data
*/
public class AttestationData {
byte[] aaguid;
byte[] credentialId;
CredentialPublicKey publicKey;
public AttestationData() {
aaguid = new byte[16];
credentialId = new byte[] {};
publicKey = new EccKey();
}
/**
* @param aaguid Authenticator Attestation GUID
* @param credentialId Credential ID
* @param publicKey CredentialPublicKey
*/
public AttestationData(byte[] aaguid, byte[] credentialId, CredentialPublicKey publicKey) {
this.aaguid = aaguid;
this.credentialId = credentialId;
this.publicKey = publicKey;
}
/**
* Decodes the input byte array into the AttestationData object.
*
* @param data
* @return AttestationData object created from the byte sequence
* @throws ResponseException
* @throws CborException
*/
public static AttestationData decode(byte[] data) throws ResponseException, CborException {
AttestationData result = new AttestationData();
// The attested credential data is formatted as follows:
// [16 bytes] AAGUID
// [2 bytes] Credential ID length (L)
// [L bytes] Credential ID
// [Remaining bytes] Credential (COSE) Public Key
int index = 0;
if (data.length < 18) {
throw new ResponseException("Invalid input");
}
System.arraycopy(data, 0, result.aaguid, 0, 16);
index += 16;
int length = (data[index++] & 0xFF) << 8;
length += data[index++] & 0xFF;
result.credentialId = new byte[length];
System.arraycopy(data, index, result.credentialId, 0, length);
index += length;
byte[] cbor = new byte[data.length - index];
System.arraycopy(data, index, cbor, 0, data.length - index);
result.publicKey = CredentialPublicKey.decode(cbor);
return result;
}
/**
* @return Encoded byte array created from AttestationData object
* @throws CborException
*/
public byte[] encode() throws CborException {
return Bytes.concat(aaguid, ByteBuffer.allocate(2).putShort((short) credentialId.length).array(), credentialId,
publicKey.encode());
}
/**
*
*/
@Override
public boolean equals(Object obj) {
try {
AttestationData other = (AttestationData) obj;
return Arrays.equals(encode(), other.encode());
} catch (NullPointerException | CborException | ClassCastException e) {
return false;
}
}
@Override
public int hashCode() {
return Objects.hash(Arrays.hashCode(aaguid), Arrays.hashCode(credentialId), publicKey);
}
/**
* @return the aaguid
*/
public byte[] getAaguid() {
return aaguid;
}
/**
* @return the credentialId
*/
public byte[] getCredentialId() {
return credentialId;
}
/**
* @return the publicKey
*/
public CredentialPublicKey getPublicKey() {
return publicKey;
}
}
package com.google.webauthn.gaedemo.objects;
public interface AttestationExtension {
public enum Type {
CABLE,
}
public Type getType();
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import co.nstant.in.cbor.CborBuilder;
import co.nstant.in.cbor.CborDecoder;
import co.nstant.in.cbor.CborEncoder;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.model.ByteString;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.Map;
import co.nstant.in.cbor.model.UnicodeString;
import com.google.webauthn.gaedemo.exceptions.ResponseException;
import java.io.ByteArrayOutputStream;
import java.util.List;
public class AttestationObject {
AuthenticatorData authData;
String fmt;
AttestationStatement attStmt;
public AttestationObject() {
}
/**
* @param authData
* @param fmt
* @param attStmt
*/
public AttestationObject(AuthenticatorData authData, String fmt, AttestationStatement attStmt) {
this.authData = authData;
this.fmt = fmt;
this.attStmt = attStmt;
}
/**
* @param attestationObject
* @return AttestationObject created from the provided byte array
* @throws CborException
* @throws ResponseException
*/
public static AttestationObject decode(byte[] attestationObject)
throws CborException, ResponseException {
AttestationObject result = new AttestationObject();
List<DataItem> dataItems = CborDecoder.decode(attestationObject);
if (dataItems.size() == 1 && dataItems.get(0) instanceof Map) {
DataItem attStmt = null;
Map attObjMap = (Map) dataItems.get(0);
for (DataItem key : attObjMap.getKeys()) {
if (key instanceof UnicodeString) {
switch(((UnicodeString) key).getString()) {
case "fmt":
UnicodeString value = (UnicodeString) attObjMap.get(key);
result.fmt = value.getString();
break;
case "authData":
byte[] authData = ((ByteString) attObjMap.get(key)).getBytes();
result.authData = AuthenticatorData.decode(authData);
break;
case "attStmt":
attStmt = attObjMap.get(key);
break;
}
}
}
if (attStmt != null) {
result.attStmt = AttestationStatement.decode(result.fmt, attStmt);
}
}
return result;
}
/**
* @return Encoded byte array containing AttestationObject data
* @throws CborException
*/
public byte[] encode() throws CborException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
List<DataItem> cbor =
new CborBuilder().addMap().put("fmt", fmt).put("authData", authData.encode())
.put(new UnicodeString("attStmt"), attStmt.encode()).end().build();
new CborEncoder(output).encode(cbor);
return output.toByteArray();
}
/**
* @return the authData
*/
public AuthenticatorData getAuthenticatorData() {
return authData;
}
/**
* @return the fmt
*/
public String getFormat() {
return fmt;
}
/**
* @return the attStmt
*/
public AttestationStatement getAttestationStatement() {
return attStmt;
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.model.DataItem;
import com.google.webauthn.gaedemo.exceptions.ResponseException;
public abstract class AttestationStatement {
/**
* @param fmt
* @param attStmt
* @return Attestation statement of provided format
*/
public static AttestationStatement decode(String fmt, DataItem attStmt) {
switch (fmt) {
case "fido-u2f":
return FidoU2fAttestationStatement.decode(attStmt);
case "packed":
return PackedAttestationStatement.decode(attStmt);
case "android-safetynet":
try {
return AndroidSafetyNetAttestationStatement.decode(attStmt);
} catch (ResponseException e) {
break;
}
case "none":
return new NoneAttestationStatement();
}
return null;
}
/**
* @return Encoded AttestationStatement
* @throws CborException
*/
abstract DataItem encode() throws CborException;
public abstract String getName();
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
public enum AttestationStatementEnum {
FIDOU2F, ANDROIDSAFETYNET, PACKED, NONE;
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import java.security.KeyPair;
import java.security.interfaces.ECPublicKey;
import java.util.ArrayList;
import java.util.List;
import com.google.common.io.BaseEncoding;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.webauthn.gaedemo.crypto.Crypto;
public class AuthenticationExtensionsClientInputs {
public List<CableSessionData> cableAuthentication;
public KeyPair rpPublicKey;
JsonObject registrationExtensions;
/**
* @param parameter
* @return
*/
public static AuthenticationExtensionsClientInputs parse(String parameter) {
Gson gson = new Gson();
return gson.fromJson(parameter, AuthenticationExtensionsClientInputs.class);
}
/**
* Add Cable Session Data extension.
* @param cableSessionData
*/
public void addCableSessionData(CableSessionData cableSessionData) {
if (cableAuthentication == null) {
cableAuthentication = new ArrayList<>();
}
cableAuthentication.add(cableSessionData);
}
/**
* @return JSONObject formatted extension object.
*/
public JsonObject getJsonObject() {
JsonObject result = new JsonObject();
if (cableAuthentication != null) {
JsonArray cableSessionDatas = new JsonArray();
for (CableSessionData sessionData : cableAuthentication) {
cableSessionDatas.add(sessionData.getJsonObject());
}
result.add("cableAuthentication", cableSessionDatas);
}
return result;
}
/**
* Add cable registration data extension.
* @return
*/
public KeyPair addCableRegistrationData() {
if (registrationExtensions == null) {
registrationExtensions = new JsonObject();
}
KeyPair keyPair = Crypto.generateKeyPair();
JsonObject cableRegistration = new JsonObject();
JsonArray versionArray = new JsonArray();
versionArray.add(1L);
cableRegistration.add("versions", versionArray);
cableRegistration.addProperty("rpPublicKey", BaseEncoding.base64()
.encode(Crypto.compressECPublicKey((ECPublicKey) keyPair.getPublic())));
registrationExtensions.add("cableRegistration", cableRegistration);
return keyPair;
}
/**
* @return registration extensions.
*/
public JsonObject getRegistrationExtensions() {
return registrationExtensions;
}
}
package com.google.webauthn.gaedemo.objects;
import com.google.gson.JsonElement;
public class AuthenticationExtensionsClientOutputs {
public AuthenticationExtensionsClientOutputs() {}
public void parseExtensions(JsonElement json) {}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import java.nio.charset.StandardCharsets;
import com.google.common.io.BaseEncoding;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonSyntaxException;
import com.google.webauthn.gaedemo.exceptions.ResponseException;
import com.googlecode.objectify.annotation.Subclass;
@Subclass
public class AuthenticatorAssertionResponse extends AuthenticatorResponse {
private static class AssertionResponseJson {
String clientDataJSON;
String authenticatorData;
String signature;
String userHandle;
}
byte[] authDataBytes;
AuthenticatorData authData;
byte[] signature;
byte[] userHandle;
public AuthenticatorAssertionResponse() {}
public AuthenticatorAssertionResponse(String clientDataJSON, String authenticatorData,
String signatureString) throws ResponseException {
clientData = CollectedClientData.decode(clientDataJSON);
clientDataBytes = clientDataJSON.getBytes(StandardCharsets.UTF_8);
authData = AuthenticatorData.decode(BaseEncoding.base64().decode(authenticatorData));
signature = BaseEncoding.base64().decode(signatureString);
}
/**
* @param data
* @throws ResponseException
*/
public AuthenticatorAssertionResponse(JsonElement data) throws ResponseException {
Gson gson = new Gson();
try {
AssertionResponseJson parsedObject = gson.fromJson(data, AssertionResponseJson.class);
clientDataBytes = BaseEncoding.base64().decode(parsedObject.clientDataJSON);
clientData = gson.fromJson(new String(clientDataBytes, StandardCharsets.UTF_8),
CollectedClientData.class);
authDataBytes = BaseEncoding.base64().decode(parsedObject.authenticatorData);
authData = AuthenticatorData.decode(authDataBytes);
signature = BaseEncoding.base64().decode(parsedObject.signature);
userHandle = BaseEncoding.base64().decode(parsedObject.userHandle);
} catch (JsonSyntaxException e) {
throw new ResponseException("Response format incorrect");
}
}
/**
* @return byte array of authData
*/
public byte[] getAuthDataBytes() {
return authDataBytes;
}
/**
* @return the authData
*/
public AuthenticatorData getAuthenticatorData() {
return authData;
}
/**
* @return the signature
*/
public byte[] getSignature() {
return signature;
}
/**
* @return the userHandle
*/
public byte[] getUserHandle() {
return userHandle;
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
/**
*
*/
public enum AuthenticatorAttachment {
PLATFORM("platform"), CROSS_PLATFORM("cross-platform");
private final String name;
/**
* @param name
*/
private AuthenticatorAttachment(String name) {
this.name = name;
}
/**
* @param s
* @return AuthenticatorAttachment corresponding to the input string
*/
public static AuthenticatorAttachment decode(String s) {
for (AuthenticatorAttachment a : AuthenticatorAttachment.values()) {
if (a.name.equals(s)) {
return a;
}
}
throw new IllegalArgumentException(s + " not a valid AuthenticatorAttachment");
}
@Override
public String toString() {
return name;
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import java.nio.charset.StandardCharsets;
import com.google.common.io.BaseEncoding;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.webauthn.gaedemo.exceptions.ResponseException;
import com.googlecode.objectify.annotation.Subclass;
import co.nstant.in.cbor.CborException;
@Subclass
public class AuthenticatorAttestationResponse extends AuthenticatorResponse {
private static class AttestationResponseJson {
String clientDataJSON;
String attestationObject;
String[] transports;
}
public AttestationObject decodedObject;
public String[] transports;
/**
*
*/
public AuthenticatorAttestationResponse() {}
/**
* Create AuthenticatorAttestationResponse from member objects.
* @param clientDataJSON
* @param attestationObject
* @throws ResponseException
*/
public AuthenticatorAttestationResponse(String clientDataJSON, String attestationObject)
throws ResponseException {
clientData = CollectedClientData.decode(clientDataJSON);
clientDataBytes = clientDataJSON.getBytes(StandardCharsets.UTF_8);
try {
decodedObject = AttestationObject.decode(BaseEncoding.base64().decode(attestationObject));
} catch (CborException e) {
throw new ResponseException("Cannot decode attestation object");
}
}
/**
* @param data
* @throws ResponseException
*/
public AuthenticatorAttestationResponse(JsonElement data) throws ResponseException {
Gson gson = new Gson();
AttestationResponseJson parsedObject = gson.fromJson(data, AttestationResponseJson.class);
clientDataBytes = BaseEncoding.base64().decode(parsedObject.clientDataJSON);
byte[] attestationObject = BaseEncoding.base64().decode(parsedObject.attestationObject);
try {
decodedObject = AttestationObject.decode(attestationObject);
} catch (CborException e) {
throw new ResponseException("Cannot decode attestation object");
}
clientData = gson.fromJson(new String(clientDataBytes, StandardCharsets.UTF_8),
CollectedClientData.class);
transports = parsedObject.transports;
}
/**
* @return json encoded representation of the AuthenticatorAttestationResponse
*/
public String encode() {
JsonObject json = new JsonObject();
json.addProperty("clientDataJSON", BaseEncoding.base64().encode(clientDataBytes));
try {
json.addProperty("attestationObject", BaseEncoding.base64().encode(decodedObject.encode()));
} catch (CborException e) {
return null;
}
return json.getAsString();
}
/**
* @return the decodedObject
*/
public AttestationObject getAttestationObject() {
return decodedObject;
}
/**
* @return the list of transports supported by the credential
*/
public String[] getTransports() {
return transports;
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Ints;
import com.google.webauthn.gaedemo.exceptions.ResponseException;
import co.nstant.in.cbor.CborDecoder;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.Map;
import co.nstant.in.cbor.model.UnicodeString;
public class AuthenticatorData {
private byte[] rpIdHash;
private byte flags;
private int signCount;
// optional
AttestationData attData;
private byte[] extensions;
/**
* @param rpIdHash
* @param flags
* @param signCount
* @param attData
*/
public AuthenticatorData(byte[] rpIdHash, byte flags, int signCount, AttestationData attData, byte[] extensions) {
this.rpIdHash = rpIdHash;
this.flags = flags;
this.signCount = signCount;
this.attData = attData;
this.extensions = extensions;
}
AuthenticatorData() {
rpIdHash = new byte[32];
attData = new AttestationData();
this.extensions = null;
}
AuthenticatorData(byte[] rpIdHash, byte flags, int signCount) {
this.rpIdHash = rpIdHash;
this.flags = flags;
this.signCount = signCount;
this.attData = null;
this.extensions = null;
}
/**
* @return the rpIdHash
*/
public byte[] getRpIdHash() {
return rpIdHash;
}
/**
* @return the flags
*/
public byte getFlags() {
return flags;
}
/**
* @return the UP bit of the flags
*/
public boolean isUP() {
return (flags & 1) != 0;
}
/**
* @return the UV bit of the flags
*/
public boolean isUV() {
return (flags & 1 << 2) != 0;
}
/**
* @return the AT bit of the flags
*/
public boolean hasAttestationData() {
return (flags & 1 << 6) != 0;
}
/**
* @return the ED bit of the flags
*/
public boolean hasExtensionData() {
return (flags & 1 << 7) != 0;
}
/**
* @return the Attestation extensions
*/
public HashMap<String, AttestationExtension> getExtensionData() {
return parseExtensions(this.extensions);
}
/**
* @return the signCount
*/
public int getSignCount() {
return signCount;
}
/**
* @return the attData
*/
public AttestationData getAttData() {
return attData;
}
/**
* @param authData
* @return Decoded AuthenticatorData object
* @throws ResponseException
*/
public static AuthenticatorData decode(byte[] authData) throws ResponseException {
if (authData.length < 37) {
throw new ResponseException("Invalid input");
}
int index = 0;
byte[] rpIdHash = new byte[32];
System.arraycopy(authData, 0, rpIdHash, 0, 32);
index += 32;
byte flags = authData[index++];
int signCount = Ints.fromBytes(authData[index++], authData[index++], authData[index++], authData[index++]);
int definedIndex = index;
AttestationData attData = null;
// Bit 6 determines whether attestation data was included
if ((flags & 1 << 6) != 0) {
byte[] remainder = new byte[authData.length - index];
System.arraycopy(authData, index, remainder, 0, authData.length - index);
try {
attData = AttestationData.decode(remainder);
} catch (CborException e) {
throw new ResponseException("Error decoding");
}
}
byte[] extensions = null;
// Bit 7 determines whether extensions are included.
if ((flags & 1 << 7) != 0) {
try {
int start = definedIndex + attData.encode().length;
if (authData.length > start) {
byte[] remainder = new byte[authData.length - start];
System.arraycopy(authData, start, remainder, 0, authData.length - start);
extensions = remainder;
}
} catch (CborException e) {
throw new ResponseException("Error decoding authenticator extensions");
}
}
return new AuthenticatorData(rpIdHash, flags, signCount, attData, extensions);
}
/**
* Parse Attestation extensions
*
* @return extension map
*/
private HashMap<String, AttestationExtension> parseExtensions(byte[] extensions) {
HashMap<String, AttestationExtension> extensionMap = new HashMap<>();
try {
List<DataItem> dataItems = CborDecoder.decode(extensions);
if (dataItems.size() < 1 || !(dataItems.get(0) instanceof Map)) {
return extensionMap;
}
Map map = (Map) dataItems.get(0);
for (DataItem data : map.getKeys()) {
if (data instanceof UnicodeString) {
if (((UnicodeString) data).getString().equals(CableRegistrationData.KEY)) {
CableRegistrationData decodedCableData = CableRegistrationData.parseFromCbor(map.get(data));
extensionMap.put(CableRegistrationData.KEY, decodedCableData);
}
}
}
} catch (CborException e) {
e.printStackTrace();
}
return extensionMap;
}
/**
* @return Encoded byte array
* @throws CborException
*/
public byte[] encode() throws CborException {
byte[] flags = { this.flags };
byte[] signCount = ByteBuffer.allocate(4).putInt(this.signCount).array();
byte[] result;
if (this.attData != null) {
byte[] attData = this.attData.encode();
result = Bytes.concat(rpIdHash, flags, signCount, attData);
} else {
result = Bytes.concat(rpIdHash, flags, signCount);
}
if (this.extensions != null) {
result = Bytes.concat(result, extensions);
}
return result;
}
@Override
public int hashCode() {
return Objects.hash(Arrays.hashCode(rpIdHash), flags, signCount, attData, Arrays.hashCode(extensions));
}
@Override
public boolean equals(Object obj) {
try {
return Arrays.equals(encode(), ((AuthenticatorData) obj).encode());
} catch (CborException | ClassCastException e) {
return false;
}
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
public class AuthenticatorResponse {
protected CollectedClientData clientData;
protected byte[] clientDataBytes;
/**
* @return the clientData
*/
public CollectedClientData getClientData() {
return clientData;
}
/**
* @return the clientData string
*/
public byte[] getClientDataBytes() {
return clientDataBytes;
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import com.google.gson.JsonObject;
public class AuthenticatorSelectionCriteria {
public AuthenticatorAttachment authenticatorAttachment;
public boolean requireResidentKey;
public UserVerificationRequirement userVerification;
public AuthenticatorSelectionCriteria() {
authenticatorAttachment = null;
requireResidentKey = false;
userVerification = UserVerificationRequirement.PREFERRED;
}
public AuthenticatorSelectionCriteria(AuthenticatorAttachment authenticatorAttachment,
boolean requireResidentKey, UserVerificationRequirement userVerification) {
this.authenticatorAttachment = authenticatorAttachment;
this.requireResidentKey = requireResidentKey;
this.userVerification = userVerification;
}
public JsonObject getJsonObject() {
JsonObject result = new JsonObject();
if (authenticatorAttachment != null) {
result.addProperty("authenticatorAttachment", authenticatorAttachment.toString());
}
result.addProperty("requireResidentKey", requireResidentKey);
if (userVerification != null) {
result.addProperty("userVerification", userVerification.toString());
}
return result;
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
public enum AuthenticatorTransport {
USB("usb"), NFC("nfc"), BLE("ble"), INTERNAL("internal"), CABLE("cable"), LIGHTNING("lightning");
final private String name;
/**
* @param name
*/
private AuthenticatorTransport(String name) {
this.name = name;
}
/**
* @param s
* @return Transport corresponding to the input string
*/
public static AuthenticatorTransport decode(String s) {
for (AuthenticatorTransport t : AuthenticatorTransport.values()) {
if (t.name.equals(s)) {
return t;
}
}
throw new IllegalArgumentException(s + " not a valid Transport");
}
@Override
public String toString() {
return name;
}
}
/*
* Copyright 2018 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*
*/
package com.google.webauthn.gaedemo.objects;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.interfaces.ECPublicKey;
import org.bouncycastle.util.Arrays;
import com.google.common.primitives.Bytes;
import com.google.webauthn.gaedemo.crypto.Crypto;
public class CablePairingData {
public int version;
public byte[] irk;
public byte[] lk;
private static int HKDF_SHA_LENGTH = 64;
private static int K_LENGTH = 32;
public CablePairingData(int version, byte[] irk, byte[] lk) {
this.version = version;
this.irk = irk;
this.lk = lk;
}
public CablePairingData() {}
/**
* @param cableData
* @param sessionKeyPair
* @return
*/
public static CablePairingData generatePairingData(CableRegistrationData cableData,
KeyPair sessionKeyPair) {
byte[] sharedSecret = Crypto.getS(sessionKeyPair.getPrivate(), cableData.publicKey);
byte[] info = "FIDO caBLE v1 pairing data".getBytes(StandardCharsets.US_ASCII);
byte[] version = ByteBuffer.allocate(4).putInt(cableData.versions.get(0)).array();
byte[] result = Crypto.hkdfSha256(sharedSecret, Crypto.sha256Digest(Bytes.concat(version,
Crypto.compressECPublicKey((ECPublicKey) sessionKeyPair.getPublic()), cableData.publicKey)),
info, HKDF_SHA_LENGTH);
return new CablePairingData(cableData.versions.get(0), Arrays.copyOf(result, K_LENGTH),
Arrays.copyOfRange(result, K_LENGTH, 2 * K_LENGTH));
}
}
package com.google.webauthn.gaedemo.objects;
import java.util.ArrayList;
import java.util.List;
import co.nstant.in.cbor.model.ByteString;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.Map;
import co.nstant.in.cbor.model.UnicodeString;
import co.nstant.in.cbor.model.UnsignedInteger;
public class CableRegistrationData implements AttestationExtension {
public static final String KEY = "cableRegistration";
List<Integer> versions;
Integer maxVersion;
byte[] publicKey;
public CableRegistrationData() {
}
public static CableRegistrationData parseFromCbor(DataItem cborCableData) {
CableRegistrationData cableData = new CableRegistrationData();
Map cborMap = (Map) cborCableData;
for (DataItem data : cborMap.getKeys()) {
if (data instanceof UnicodeString) {
switch (((UnicodeString) data).getString()) {
case "version":
cableData.versions = new ArrayList<>();
cableData.versions.add(((UnsignedInteger) cborMap.get(data)).getValue().intValue());
break;
case "maxVersion":
cableData.maxVersion = ((UnsignedInteger) cborMap.get(data)).getValue().intValue();
break;
case "authenticatorPublicKey":
cableData.publicKey = ((ByteString) cborMap.get(data)).getBytes();
break;
}
}
}
return cableData;
}
@Override
public Type getType() {
return Type.CABLE;
}
}
/*
* Copyright 2018 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*
*/
package com.google.webauthn.gaedemo.objects;
import com.google.common.io.BaseEncoding;
import com.google.gson.JsonObject;
import java.util.Arrays;
import java.util.Objects;
public class CableSessionData {
public int version;
public byte[] clientEid;
public byte[] authenticatorEid;
public byte[] sessionPreKey;
public CableSessionData() {}
public CableSessionData(int version, byte[] clientEid, byte[] authenticatorEid,
byte[] sessionPreKey) {
this.version = version;
this.clientEid = clientEid;
this.authenticatorEid = authenticatorEid;
this.sessionPreKey = sessionPreKey;
}
public JsonObject getJsonObject() {
JsonObject result = new JsonObject();
result.addProperty("version", version);
result.addProperty("clientEid", BaseEncoding.base64().encode(clientEid));
result.addProperty("authenticatorEid", BaseEncoding.base64().encode(authenticatorEid));
result.addProperty("sessionPreKey", BaseEncoding.base64().encode(sessionPreKey));
return result;
}
@Override
public int hashCode() {
return Objects.hash(version, Arrays.hashCode(clientEid), Arrays.hashCode(authenticatorEid),
Arrays.hashCode(sessionPreKey));
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CableSessionData that = (CableSessionData) o;
return version == that.version && Arrays.equals(clientEid, that.clientEid)
&& Arrays.equals(authenticatorEid, that.authenticatorEid)
&& Arrays.equals(sessionPreKey, that.sessionPreKey);
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.google.webauthn.gaedemo.crypto.Crypto;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;
public class CollectedClientData {
String type;
String challenge;
String origin;
String hashAlgorithm;
String tokenBindingId;
AuthenticationExtensionsClientInputs clientExtensions;
AuthenticationExtensionsClientInputs authenticatorExtensions;
CollectedClientData() {}
/**
* @param json
* @return Decoded CollectedClientData object
*/
public static CollectedClientData decode(String json) {
Gson gson = new Gson();
try {
return gson.fromJson(json, CollectedClientData.class);
} catch (JsonSyntaxException e) {
return null;
}
}
/**
* @return json encoded representation of CollectedClientData
*/
public String encode() {
Gson gson = new Gson();
return gson.toJson(this);
}
public byte[] getHash() {
String json = encode();
try {
return Crypto.digest(json.getBytes(StandardCharsets.UTF_8), hashAlgorithm);
} catch (NoSuchAlgorithmException e) {
return Crypto.sha256Digest(json.getBytes(StandardCharsets.UTF_8));
}
}
/**
* @return the challenge
*/
public String getChallenge() {
return challenge;
}
/**
* @return the origin
*/
public String getOrigin() {
return origin;
}
/**
* @return the hashAlg
*/
public String getHashAlg() {
return hashAlgorithm;
}
/**
* @return the tokenBinding
*/
public String getTokenBinding() {
return tokenBindingId;
}
public String getType() {
return type;
}
public AuthenticationExtensionsClientInputs getClientExtensions() {
return clientExtensions;
}
public AuthenticationExtensionsClientInputs getAuthenticatorExtensions() {
return authenticatorExtensions;
}
@Override
public int hashCode() {
return Objects.hash(type, challenge, origin, hashAlgorithm, tokenBindingId, clientExtensions,
authenticatorExtensions);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof CollectedClientData)) {
return false;
}
return encode().equals(((CollectedClientData)obj).encode());
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import java.security.InvalidParameterException;
import java.util.List;
import co.nstant.in.cbor.CborDecoder;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.model.ByteString;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.Map;
import co.nstant.in.cbor.model.NegativeInteger;
import co.nstant.in.cbor.model.UnsignedInteger;
public abstract class CredentialPublicKey {
Algorithm alg;
int kty;
byte[] cborEncodedKey;
static final int CRV_LABEL = -1;
static final int X_LABEL = -2;
static final int Y_LABEL = -3;
static final int N_LABEL = -1;
static final int E_LABEL = -2;
static final int KTY_LABEL = 1;
static final int ALG_LABEL = 3;
/**
* @return Cbor encoded byte array
* @throws CborException
*/
public abstract byte[] encode() throws CborException;
/**
* @return human-readable hex string of the key
*/
@Override
public abstract String toString();
/**
* Get algorithm info
*
* @return algorithm
*/
public Algorithm getAlg() {
return alg;
}
/**
* @param cbor
* @return CredentialPublicKey object decoded from cbor byte array
* @throws CborException
*/
public static CredentialPublicKey decode(byte[] cbor) throws CborException {
List<DataItem> dataItems = CborDecoder.decode(cbor);
if (dataItems.size() < 1 || !(dataItems.get(0) instanceof Map)) {
return null;
}
Map map = (Map) dataItems.get(0);
// If there are 4 keys in the map, the key should be RSA. If there are 5, then it is ECC.
if (map.getKeys().size() == 4) {
RsaKey rsaKey = new RsaKey();
for (DataItem d : map.getKeys()) {
int tmp = 0;
if (d instanceof NegativeInteger) {
tmp = ((NegativeInteger) d).getValue().intValue();
} else if (d instanceof UnsignedInteger) {
tmp = ((UnsignedInteger) d).getValue().intValue();
}
switch (tmp) {
case N_LABEL:
if (map.get(d) instanceof ByteString) {
rsaKey.n = ((ByteString) map.get(d)).getBytes();
} else {
throw new InvalidParameterException("Public key 'N' invalid type");
}
break;
case E_LABEL:
if (map.get(d) instanceof ByteString) {
rsaKey.e = ((ByteString) map.get(d)).getBytes();
} else {
throw new InvalidParameterException("Public key 'E' invalid type");
}
break;
case KTY_LABEL:
if (map.get(d) instanceof UnsignedInteger) {
rsaKey.kty = ((UnsignedInteger) map.get(d)).getValue().intValue();
} else {
throw new InvalidParameterException("Public key 'KTY' invalid type");
}
break;
case ALG_LABEL:
if (map.get(d) instanceof NegativeInteger) {
rsaKey.alg = Algorithm.decode(((NegativeInteger) map.get(d)).getValue().intValue());
if (!Algorithm.isRsaAlgorithm(rsaKey.alg))
throw new InvalidParameterException("Unsupported RSA algorithm");
} else {
throw new InvalidParameterException("Public key 'ALG' invalid type");
}
break;
}
}
return rsaKey;
} else if (map.getKeys().size() == 5) {
EccKey eccKey = new EccKey();
for (DataItem d : map.getKeys()) {
int tmp = 0;
if (d instanceof NegativeInteger) {
tmp = ((NegativeInteger) d).getValue().intValue();
} else if (d instanceof UnsignedInteger) {
tmp = ((UnsignedInteger) d).getValue().intValue();
}
switch (tmp) {
case CRV_LABEL:
if (map.get(d) instanceof UnsignedInteger) {
eccKey.crv = ((UnsignedInteger) map.get(d)).getValue().intValue();
} else {
throw new InvalidParameterException("Public key 'CRV' invalid type");
}
break;
case X_LABEL:
if (map.get(d) instanceof ByteString) {
eccKey.x = ((ByteString) map.get(d)).getBytes();
} else {
throw new InvalidParameterException("Public key 'X' invalid type");
}
break;
case Y_LABEL:
if (map.get(d) instanceof ByteString) {
eccKey.y = ((ByteString) map.get(d)).getBytes();
} else {
throw new InvalidParameterException("Public key 'Y' invalid type");
}
break;
case KTY_LABEL:
if (map.get(d) instanceof UnsignedInteger) {
eccKey.kty = ((UnsignedInteger) map.get(d)).getValue().intValue();
} else {
throw new InvalidParameterException("Public key 'KTY' invalid type");
}
break;
case ALG_LABEL:
if (map.get(d) instanceof NegativeInteger) {
eccKey.alg = Algorithm.decode(((NegativeInteger) map.get(d)).getValue().intValue());
if (!Algorithm.isEccAlgorithm(eccKey.alg))
throw new InvalidParameterException("Unsupported ECC algorithm");
} else {
throw new InvalidParameterException("Public key 'ALG' invalid type");
}
break;
}
}
return eccKey;
}
throw new InvalidParameterException("Unsupported COSE public key sent");
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import java.util.List;
import javax.xml.bind.DatatypeConverter;
import com.google.common.primitives.Bytes;
import com.googlecode.objectify.annotation.Subclass;
import co.nstant.in.cbor.CborBuilder;
import co.nstant.in.cbor.CborEncoder;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.model.ByteString;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.NegativeInteger;
import co.nstant.in.cbor.model.UnsignedInteger;
@Subclass
public class EccKey extends CredentialPublicKey {
byte[] x, y;
int crv;
EccKey() {
x = null;
y = null;
alg = Algorithm.UNDEFINED;
}
public EccKey(Algorithm alg, byte[] x, byte[] y) {
this.alg = alg;
this.x = x;
this.y = y;
}
/**
* @param x
* @param y
*/
public EccKey(byte[] x, byte[] y) {
super();
this.x = x;
this.y = y;
}
@Override
public int hashCode() {
return Arrays.hashCode(Bytes.concat(x, y));
}
@Override
public boolean equals(Object obj) {
try {
if (obj instanceof EccKey) {
EccKey other = (EccKey) obj;
if (Arrays.equals(x, other.x) && Arrays.equals(y, other.y) && alg == other.alg) {
return true;
}
}
} catch (NullPointerException e) {
}
return false;
}
@Override
public byte[] encode() throws CborException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
List<DataItem> dataItems =
new CborBuilder().addMap().put(new UnsignedInteger(KTY_LABEL), new UnsignedInteger(kty))
.put(new UnsignedInteger(ALG_LABEL), new NegativeInteger(alg.encodeToInt()))
.put(new NegativeInteger(CRV_LABEL), new UnsignedInteger(crv))
.put(new NegativeInteger(X_LABEL), new ByteString(x))
.put(new NegativeInteger(Y_LABEL), new ByteString(y)).end().build();
new CborEncoder(output).encode(dataItems);
return output.toByteArray();
}
/**
* @return the x
*/
public byte[] getX() {
return x;
}
/**
* @return the y
*/
public byte[] getY() {
return y;
}
@Override
public String toString() {
StringBuilder b = new StringBuilder();
b.append("alg:");
b.append(alg.toReadableString());
b.append(" x:");
b.append(DatatypeConverter.printHexBinary(x));
b.append(" y:");
b.append(DatatypeConverter.printHexBinary(y));
return b.toString();
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import co.nstant.in.cbor.CborDecoder;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.model.Array;
import co.nstant.in.cbor.model.ByteString;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.Map;
import co.nstant.in.cbor.model.UnicodeString;
import com.googlecode.objectify.annotation.Subclass;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@Subclass
public class FidoU2fAttestationStatement extends AttestationStatement {
public byte[] sig;
public byte[] attestnCert;
public List<byte[]> caCert;
/**
* @param sig
* @param attestnCert
* @param caCert
*/
public FidoU2fAttestationStatement(byte[] sig, byte[] attestnCert, List<byte[]> caCert) {
super();
this.sig = sig;
this.attestnCert = attestnCert;
this.caCert = caCert;
}
public FidoU2fAttestationStatement() {
}
/**
* @param attStmt
* @return Decoded FidoU2fAttestationStatement
*/
public static FidoU2fAttestationStatement decode(DataItem attStmt) {
FidoU2fAttestationStatement result = new FidoU2fAttestationStatement();
Map given = null;
if (attStmt instanceof ByteString) {
byte[] temp = ((ByteString) attStmt).getBytes();
List<DataItem> dataItems = null;
try {
dataItems = CborDecoder.decode(temp);
} catch (Exception e) {
}
given = (Map) dataItems.get(0);
} else {
given = (Map) attStmt;
}
for (DataItem data : given.getKeys()) {
if (data instanceof UnicodeString) {
if (((UnicodeString) data).getString().equals("x5c")) {
Array array = (Array) given.get(data);
List<DataItem> list = array.getDataItems();
if (list.size() > 0) {
result.attestnCert = ((ByteString) list.get(0)).getBytes();
}
result.caCert = new ArrayList<byte[]>();
for (int i = 1; i < list.size(); i++) {
result.caCert.add(((ByteString) list.get(i)).getBytes());
}
} else if (((UnicodeString) data).getString().equals("sig")) {
result.sig = ((ByteString) (given.get(data))).getBytes();
}
}
}
return result;
}
@Override
DataItem encode() throws CborException {
Map result = new Map();
Array x5c = new Array();
x5c.add(new ByteString(attestnCert));
for (byte[] cert : this.caCert) {
x5c.add(new ByteString(cert));
}
result.put(new UnicodeString("x5c"), x5c);
result.put(new UnicodeString("sig"), new ByteString(sig));
return result;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof FidoU2fAttestationStatement)) {
return false;
}
FidoU2fAttestationStatement other = (FidoU2fAttestationStatement) obj;
if (!Arrays.equals(attestnCert, other.attestnCert)) {
return false;
}
if (!Arrays.equals(sig, other.sig)) {
return false;
}
if (caCert.size() == other.caCert.size()) {
return false;
}
for (int i = 0; i < caCert.size(); i++) {
if (!Arrays.equals(caCert.get(i), other.caCert.get(i))) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
return Objects.hash(Arrays.hashCode(sig), Arrays.hashCode(attestnCert), caCert);
}
@Override
public String getName() {
return "FIDO U2F Authenticator";
}
}
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import com.googlecode.objectify.annotation.Subclass;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.Map;
@Subclass
public final class NoneAttestationStatement extends AttestationStatement {
public NoneAttestationStatement() {}
@Override
DataItem encode() throws CborException {
Map result = new Map();
return result;
}
@Override
public int hashCode() {
return 0;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof NoneAttestationStatement)
return true;
return false;
}
@Override
public String getName() {
return "NONE ATTESTATION";
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import com.googlecode.objectify.annotation.Subclass;
import co.nstant.in.cbor.CborDecoder;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.model.Array;
import co.nstant.in.cbor.model.ByteString;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.Map;
import co.nstant.in.cbor.model.NegativeInteger;
import co.nstant.in.cbor.model.UnicodeString;
@Subclass
public class PackedAttestationStatement extends AttestationStatement {
public byte[] sig;
public byte[] attestnCert;
public List<byte[]> caCert;
public Algorithm alg;
public byte[] ecdaaKeyId;
/**
* @param sig
* @param attestnCert
* @param caCert
*/
public PackedAttestationStatement(byte[] sig, byte[] attestnCert, List<byte[]> caCert, String alg) {
super();
this.sig = sig;
this.attestnCert = attestnCert;
this.caCert = caCert;
this.alg = Algorithm.decode(alg);
this.ecdaaKeyId = null;
}
/**
* @param sig
* @param attestnCert
* @param caCert
*/
public PackedAttestationStatement(byte[] sig, byte[] ecdaaKeyId, String alg) {
super();
this.sig = sig;
this.ecdaaKeyId = ecdaaKeyId;
this.alg = Algorithm.decode(alg);
this.caCert = null;
this.attestnCert = null;
}
public PackedAttestationStatement() {
this.sig = null;
this.attestnCert = null;
this.caCert = null;
this.alg = null;
this.ecdaaKeyId = null;
}
/**
* @param attStmt
* @return Decoded FidoU2fAttestationStatement
*/
public static PackedAttestationStatement decode(DataItem attStmt) {
PackedAttestationStatement result = new PackedAttestationStatement();
Map given = null;
if (attStmt instanceof ByteString) {
byte[] temp = ((ByteString) attStmt).getBytes();
List<DataItem> dataItems = null;
try {
dataItems = CborDecoder.decode(temp);
} catch (Exception e) {
}
given = (Map) dataItems.get(0);
} else {
given = (Map) attStmt;
}
for (DataItem data : given.getKeys()) {
if (data instanceof UnicodeString) {
switch (((UnicodeString) data).getString()) {
case "x5c":
Array array = (Array) given.get(data);
List<DataItem> list = array.getDataItems();
if (list.size() > 0) {
result.attestnCert = ((ByteString) list.get(0)).getBytes();
}
result.caCert = new ArrayList<byte[]>();
for (int i = 1; i < list.size(); i++) {
result.caCert.add(((ByteString) list.get(i)).getBytes());
}
break;
case "sig":
result.sig = ((ByteString) (given.get(data))).getBytes();
break;
case "alg":
int algInt = new BigDecimal(((NegativeInteger) (given.get(data))).getValue()).intValueExact();
result.alg = Algorithm.decode(algInt);
break;
case "ecdaaKeyId":
result.ecdaaKeyId = ((ByteString) (given.get(data))).getBytes();
break;
}
}
}
return result;
}
@Override
DataItem encode() throws CborException {
Map result = new Map();
if (attestnCert != null) {
Array x5c = new Array();
x5c.add(new ByteString(attestnCert));
for (byte[] cert : this.caCert) {
x5c.add(new ByteString(cert));
}
result.put(new UnicodeString("x5c"), x5c);
}
if (ecdaaKeyId != null) {
result.put(new UnicodeString("ecdaaKeyId"), new ByteString(ecdaaKeyId));
}
result.put(new UnicodeString("sig"), new ByteString(sig));
result.put(new UnicodeString("alg"), new UnicodeString(alg.toString()));
return result;
}
@Override
public int hashCode() {
return Objects.hash(Arrays.hashCode(sig), Arrays.hashCode(attestnCert), caCert, alg, Arrays.hashCode(ecdaaKeyId));
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof PackedAttestationStatement)) {
return false;
}
PackedAttestationStatement other = (PackedAttestationStatement) obj;
try {
return encode().equals(other.encode());
} catch (CborException e) {
}
return false;
}
@Override
public String getName() {
return "Packed Attestation";
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import com.google.gson.Gson;
public class PublicKeyCredential {
public String id;
public String type;
public byte[] rawId;
AuthenticatorResponse response;
/**
* @param id
* @param type
* @param rawId
* @param response
*/
public PublicKeyCredential(String id, String type, byte[] rawId, AuthenticatorResponse response) {
this.id = id;
this.type = type;
this.rawId = rawId;
this.response = response;
}
/**
*
*/
public PublicKeyCredential() {}
/**
* @return the id
*/
public String getId() {
return id;
}
/**
* @return the type
*/
public String getType() {
return type;
}
/**
* @return the rawId
*/
public byte[] getRawId() {
return rawId;
}
public AttestationStatementEnum getAttestationType() {
try {
AuthenticatorAttestationResponse attRsp = (AuthenticatorAttestationResponse) response;
AttestationStatement attStmt = attRsp.decodedObject.getAttestationStatement();
if (attStmt instanceof AndroidSafetyNetAttestationStatement) {
return AttestationStatementEnum.ANDROIDSAFETYNET;
} else if (attStmt instanceof FidoU2fAttestationStatement) {
return AttestationStatementEnum.FIDOU2F;
} else if (attStmt instanceof PackedAttestationStatement) {
return AttestationStatementEnum.PACKED;
} else if (attStmt instanceof NoneAttestationStatement) {
return AttestationStatementEnum.NONE;
}
} catch (ClassCastException e) {
return null;
}
return null;
}
/**
* @return the response
*/
public AuthenticatorResponse getResponse() {
return response;
}
/**
* @return json encoded String representation of PublicKeyCredential
*/
public String encode() {
return new Gson().toJson(this);
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import com.google.common.io.BaseEncoding;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
public class PublicKeyCredentialCreationOptions {
private static final int CHALLENGE_LENGTH = 32;
private final SecureRandom random = new SecureRandom();
PublicKeyCredentialEntity rp;
PublicKeyCredentialUserEntity user;
public byte[] challenge;
ArrayList<PublicKeyCredentialParameters> pubKeyCredParams;
long timeout;
ArrayList<PublicKeyCredentialDescriptor> excludeCredentials;
protected AuthenticatorSelectionCriteria authenticatorSelection;
protected AttestationConveyancePreference attestation;
protected AuthenticationExtensionsClientInputs extensions;
/**
*
*/
public PublicKeyCredentialCreationOptions() {
pubKeyCredParams = new ArrayList<PublicKeyCredentialParameters>();
excludeCredentials = new ArrayList<PublicKeyCredentialDescriptor>();
extensions = null;
authenticatorSelection = null;
}
/**
* @param userId
* @param rpId
* @param rpName
*/
public PublicKeyCredentialCreationOptions(String userName, String userId, String rpId,
String rpName) {
pubKeyCredParams = new ArrayList<PublicKeyCredentialParameters>();
excludeCredentials = new ArrayList<PublicKeyCredentialDescriptor>();
rp = new PublicKeyCredentialRpEntity(rpId, rpName, null);
byte[] userIdBytes = new byte[32];
random.nextBytes(userIdBytes);
user = new PublicKeyCredentialUserEntity(userName, userIdBytes);
challenge = new byte[CHALLENGE_LENGTH];
random.nextBytes(challenge);
pubKeyCredParams.add(
new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, Algorithm.ES256));
pubKeyCredParams.add(
new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, Algorithm.ES384));
pubKeyCredParams.add(
new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, Algorithm.ES512));
pubKeyCredParams.add(
new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, Algorithm.RS256));
pubKeyCredParams.add(
new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, Algorithm.RS384));
pubKeyCredParams.add(
new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, Algorithm.RS512));
pubKeyCredParams.add(
new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, Algorithm.PS256));
pubKeyCredParams.add(
new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, Algorithm.PS384));
pubKeyCredParams.add(
new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, Algorithm.PS512));
extensions = null;
}
public void setExtensions(AuthenticationExtensionsClientInputs extensions) {
this.extensions = extensions;
}
public void setCriteria(AuthenticatorSelectionCriteria criteria) {
this.authenticatorSelection = criteria;
}
public void setTimeout(long timeout) {
this.timeout = timeout;
}
public void excludeCredential(PublicKeyCredentialDescriptor credential) {
excludeCredentials.add(credential);
}
public void setExcludeCredentials(Collection<PublicKeyCredentialDescriptor> excludeCredentials) {
this.excludeCredentials.clear();
this.excludeCredentials.addAll(excludeCredentials);
}
public void setAttestationConveyancePreference(AttestationConveyancePreference attestation) {
this.attestation = attestation;
}
/**
* @return Encoded JsonObect representation of MakeCredentialOptions
*/
public JsonObject getJsonObject() {
// Required parameters
JsonObject result = new JsonObject();
result.add("rp", rp.getJsonObject());
result.add("user", user.getJsonObject());
result.addProperty("challenge", BaseEncoding.base64().encode(challenge));
JsonArray params = new JsonArray();
for (PublicKeyCredentialParameters param : pubKeyCredParams)
params.add(param.getJsonObject());
result.add("pubKeyCredParams", params);
// Optional parameters
if (this.timeout > 0) {
result.addProperty("timeout", timeout);
}
if (this.excludeCredentials != null && this.excludeCredentials.size() > 0) {
JsonArray excludeParams = new JsonArray();
for (PublicKeyCredentialDescriptor descriptor : excludeCredentials) {
excludeParams.add(descriptor.getJsonObject());
}
result.add("excludeCredentials", excludeParams);
}
if (this.authenticatorSelection != null) {
result.add("authenticatorSelection", authenticatorSelection.getJsonObject());
}
if (this.attestation != null) {
result.addProperty("attestation", this.attestation.toString());
}
if (extensions != null) {
// TODO
}
return result;
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import com.google.common.io.BaseEncoding;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import java.util.ArrayList;
public class PublicKeyCredentialDescriptor {
/**
* @param type
* @param id
*/
public PublicKeyCredentialDescriptor(PublicKeyCredentialType type, byte[] id) {
this.type = type;
this.id = id;
this.transports = new ArrayList<AuthenticatorTransport>();
}
/**
* @param type
* @param id
* @param transports
*/
public PublicKeyCredentialDescriptor(PublicKeyCredentialType type, byte[] id,
ArrayList<AuthenticatorTransport> transports) {
this.type = type;
this.id = id;
this.transports = transports;
}
private PublicKeyCredentialType type;
private byte[] id;
private ArrayList<AuthenticatorTransport> transports;
/**
* @return Encoded JsonObject representation of PublicKeyCredentialDescriptor
*/
public JsonObject getJsonObject() {
JsonObject result = new JsonObject();
result.addProperty("type", type.toString());
result.addProperty("id", BaseEncoding.base64().encode(id));
JsonArray transports = new JsonArray();
if (this.transports != null) {
for (AuthenticatorTransport t : this.transports) {
JsonPrimitive element = new JsonPrimitive(t.toString());
transports.add(element);
}
if (transports.size() > 0) {
result.add("transports", transports);
}
}
return result;
}
/**
* @param transports the transports to set
*/
public void setTransports(ArrayList<AuthenticatorTransport> transports) {
this.transports = transports;
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import com.google.gson.JsonObject;
public class PublicKeyCredentialEntity {
protected String name;
protected String icon;
/**
* @param name
* @param icon
*/
PublicKeyCredentialEntity(String name, String icon) {
this.name = name;
this.icon = icon;
}
PublicKeyCredentialEntity() {
this.name = null;
this.icon = null;
}
/**
* @return Encoded JsonObject representation of PublicKeyCredentialEntity
*/
public JsonObject getJsonObject() {
JsonObject json = new JsonObject();
json.addProperty("name", name);
if (icon != null)
json.addProperty("icon", icon);
return json;
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import com.google.gson.JsonObject;
public class PublicKeyCredentialParameters {
private PublicKeyCredentialType type;
private Algorithm algorithm;
/**
* @param type
* @param algorithm
*/
public PublicKeyCredentialParameters(PublicKeyCredentialType type, Algorithm algorithm) {
this.type = type;
this.algorithm = algorithm;
}
/**
* @return JsonObject representation of PublicKeyCredentialParameters
*/
public JsonObject getJsonObject() {
JsonObject result = new JsonObject();
result.addProperty("type", type.toString());
result.addProperty("alg", algorithm.encodeToInt());
return result;
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import com.google.common.io.BaseEncoding;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.webauthn.gaedemo.crypto.Cable;
import com.google.webauthn.gaedemo.storage.Credential;
public class PublicKeyCredentialRequestOptions {
private static final int CHALLENGE_LENGTH = 32;
private final SecureRandom random = new SecureRandom();
// Required parameters
public byte[] challenge;
// Optional parameters
public long timeout;
public String rpId;
protected ArrayList<PublicKeyCredentialDescriptor> allowCredentials;
protected UserVerificationRequirement userVerification;
AuthenticationExtensionsClientInputs extensions;
/**
* @param rpId
*/
public PublicKeyCredentialRequestOptions(String rpId) {
challenge = new byte[CHALLENGE_LENGTH];
random.nextBytes(challenge);
allowCredentials = new ArrayList<PublicKeyCredentialDescriptor>();
this.rpId = rpId;
}
/**
* @return JsonObject representation of PublicKeyCredentialRequestOptions
*/
public JsonObject getJsonObject() {
JsonObject result = new JsonObject();
result.addProperty("challenge", BaseEncoding.base64().encode(challenge));
if (timeout > 0) {
result.addProperty("timeout", timeout);
}
result.addProperty("rpId", rpId);
JsonArray allowCredentials = new JsonArray();
for (PublicKeyCredentialDescriptor credential : this.allowCredentials) {
allowCredentials.add(credential.getJsonObject());
}
result.add("allowCredentials", allowCredentials);
if (extensions != null) {
result.add("extensions", extensions.getJsonObject());
}
return result;
}
public List<PublicKeyCredentialDescriptor> getAllowCredentials() {
return allowCredentials;
}
/**
* @param currentUser
*/
public void populateAllowList(String currentUser) {
List<Credential> credentialList = Credential.load(currentUser);
for (Credential c : credentialList) {
PublicKeyCredential storedCred = c.getCredential();
if (storedCred == null)
continue;
PublicKeyCredentialDescriptor pkcd =
new PublicKeyCredentialDescriptor(PublicKeyCredentialType.PUBLIC_KEY, storedCred.rawId);
if (((AuthenticatorAttestationResponse) storedCred.getResponse()).getTransports() != null) {
ArrayList<AuthenticatorTransport> transportList = new ArrayList<>();
for (String transport : ((AuthenticatorAttestationResponse) storedCred.getResponse()).getTransports()) {
transportList.add(AuthenticatorTransport.decode(transport));
}
pkcd.setTransports(transportList);
}
allowCredentials.add(pkcd);
Cable cableCrypto = new Cable();
CablePairingData cablePairingData = c.getCablePairingData();
if (cablePairingData != null) {
if (extensions == null) {
extensions = new AuthenticationExtensionsClientInputs();
}
extensions.addCableSessionData(cableCrypto.generateSessionData(cablePairingData));
}
}
}
}
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
public class PublicKeyCredentialRpEntity extends PublicKeyCredentialEntity {
protected String id;
/**
* @param id
* @param name
* @param icon
*/
PublicKeyCredentialRpEntity(String id, String name, String icon) {
super(name, icon);
this.id = id;
}
PublicKeyCredentialRpEntity() {
super(null, null);
this.id = null;
}
@Override
public JsonObject getJsonObject() {
Gson gson = new Gson();
return (JsonObject) gson.toJsonTree(this);
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
public enum PublicKeyCredentialType {
PUBLIC_KEY("public-key");
final String name;
/**
* @param name
*/
private PublicKeyCredentialType(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import com.google.common.io.BaseEncoding;
import com.google.gson.JsonObject;
public class PublicKeyCredentialUserEntity extends PublicKeyCredentialEntity {
protected String displayName;
protected byte[] id;
/**
* @param displayName
*/
public PublicKeyCredentialUserEntity(String displayName, byte[] id) {
super();
this.displayName = displayName;
this.name = displayName;
this.id = id;
}
/**
* @return
*/
@Override
public JsonObject getJsonObject() {
JsonObject superJson = super.getJsonObject();
superJson.addProperty("displayName", displayName);
superJson.addProperty("id", BaseEncoding.base64().encode(id));
return superJson;
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
import co.nstant.in.cbor.CborBuilder;
import co.nstant.in.cbor.CborEncoder;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.model.ByteString;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.NegativeInteger;
import co.nstant.in.cbor.model.UnsignedInteger;
import com.googlecode.objectify.annotation.Subclass;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import javax.xml.bind.DatatypeConverter;
@Subclass
public class RsaKey extends CredentialPublicKey {
byte[] n, e;
RsaKey() {
n = null;
e = null;
alg = Algorithm.UNDEFINED;
}
public RsaKey(Algorithm alg, byte[] n, byte[] e) {
this.alg = alg;
this.n = n;
this.e = e;
}
@Override
public int hashCode() {
return Objects.hash(Arrays.hashCode(n), Arrays.hashCode(e));
}
@Override
public boolean equals(Object obj) {
try {
if (obj instanceof RsaKey) {
RsaKey other = (RsaKey) obj;
if (Arrays.equals(n, other.n) && Arrays.equals(e, other.e) && alg == other.alg) {
return true;
}
}
} catch (NullPointerException e) {
}
return false;
}
@Override
public byte[] encode() throws CborException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
List<DataItem> dataItems =
new CborBuilder().addMap().put(new UnsignedInteger(KTY_LABEL), new UnsignedInteger(kty))
.put(new UnsignedInteger(ALG_LABEL), new NegativeInteger(alg.encodeToInt()))
.put(new NegativeInteger(N_LABEL), new ByteString(n))
.put(new NegativeInteger(E_LABEL), new ByteString(e)).end().build();
new CborEncoder(output).encode(dataItems);
return output.toByteArray();
}
@Override
public String toString() {
StringBuilder b = new StringBuilder();
b.append("alg:");
b.append(alg.toReadableString());
b.append(" n:");
b.append(DatatypeConverter.printHexBinary(n));
b.append("<br />");
b.append(" e:");
b.append(DatatypeConverter.printHexBinary(e));
return b.toString();
}
public byte[] getN() {
return n;
}
public byte[] getE() {
return e;
}
}
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.objects;
/**
*
*/
public enum UserVerificationRequirement {
REQUIRED("required"), PREFERRED("preferred"), DISCOURAGED("discouraged");
private final String name;
/**
* @param name
*/
private UserVerificationRequirement(String name) {
this.name = name;
}
/**
* @param s
* @return AuthenticatorAttachment corresponding to the input string
*/
public static UserVerificationRequirement decode(String s) {
for (UserVerificationRequirement a : UserVerificationRequirement.values()) {
if (a.name.equals(s)) {
return a;
}
}
throw new IllegalArgumentException(s + " not a valid AuthenticatorAttachment");
}
@Override
public String toString() {
return name;
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.server;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import com.google.common.primitives.Bytes;
import com.google.webauthn.gaedemo.crypto.Crypto;
import com.google.webauthn.gaedemo.crypto.OfflineVerify;
import com.google.webauthn.gaedemo.crypto.OfflineVerify.AttestationStatement;
import com.google.webauthn.gaedemo.exceptions.ResponseException;
import com.google.webauthn.gaedemo.exceptions.WebAuthnException;
import com.google.webauthn.gaedemo.objects.AndroidSafetyNetAttestationStatement;
import com.google.webauthn.gaedemo.objects.AuthenticatorAssertionResponse;
import com.google.webauthn.gaedemo.objects.AuthenticatorAttestationResponse;
import com.google.webauthn.gaedemo.objects.EccKey;
import com.google.webauthn.gaedemo.objects.PublicKeyCredential;
import com.google.webauthn.gaedemo.storage.Credential;
import co.nstant.in.cbor.CborException;
public class AndroidSafetyNetServer extends Server {
private static final Logger Log = Logger.getLogger(AndroidSafetyNetServer.class.getName());
/**
* @param cred
* @param rpId
* @param session
* @param currentUser
* @throws ServletException
*/
public static void registerCredential(PublicKeyCredential cred, String currentUser,
String session, String rpId) throws ServletException {
if (!(cred.getResponse() instanceof AuthenticatorAttestationResponse)) {
throw new ServletException("Invalid response structure");
}
AuthenticatorAttestationResponse attResponse =
(AuthenticatorAttestationResponse) cred.getResponse();
List<Credential> savedCreds = Credential.load(currentUser);
for (Credential c : savedCreds) {
if (c.getCredential().id.equals(cred.id)) {
throw new ServletException("Credential already registered for this user");
}
}
try {
verifySessionAndChallenge(attResponse, currentUser, session);
} catch (ResponseException e1) {
throw new ServletException("Unable to verify session and challenge data");
}
AndroidSafetyNetAttestationStatement attStmt =
(AndroidSafetyNetAttestationStatement) attResponse.decodedObject.getAttestationStatement();
AttestationStatement stmt =
OfflineVerify.parseAndVerify(new String(attStmt.getResponse(), StandardCharsets.UTF_8));
if (stmt == null) {
Log.info("Failure: Failed to parse and verify the attestation statement.");
throw new ServletException("Failed to verify attestation statement");
}
byte[] clientDataHash = Crypto.sha256Digest(attResponse.getClientDataBytes());
try {
// Nonce was changed from [authenticatorData, clientDataHash] to
// sha256 [authenticatorData, clientDataHash]
// https://github.com/w3c/webauthn/pull/869
byte[] expectedNonce = Crypto.sha256Digest(Bytes.concat(
attResponse.getAttestationObject().getAuthenticatorData().encode(), clientDataHash));
if (!Arrays.equals(expectedNonce, stmt.getNonce())) {
expectedNonce = Bytes.concat(
attResponse.getAttestationObject().getAuthenticatorData().encode(), clientDataHash);
if (!Arrays.equals(expectedNonce, stmt.getNonce())) {
throw new ServletException("Nonce does not match");
}
//
}
} catch (CborException e) {
throw new ServletException("Error encoding authdata");
}
/*
* // Test devices won't pass this. if (!stmt.isCtsProfileMatch()) { throw new
* ServletException("No cts profile match"); }
*/
}
// TODO Remove after switch to generic verification
/**
* @param cred
* @param currentUser
* @param sessionId
* @throws ServletException
*/
public static void verifyAssertion(PublicKeyCredential cred, String currentUser, String sessionId,
Credential savedCredential) throws ServletException {
AuthenticatorAssertionResponse assertionResponse =
(AuthenticatorAssertionResponse) cred.getResponse();
Log.info("-- Verifying signature --");
if (!(savedCredential.getCredential()
.getResponse() instanceof AuthenticatorAttestationResponse)) {
throw new ServletException("Stored attestation missing");
}
AuthenticatorAttestationResponse storedAttData =
(AuthenticatorAttestationResponse) savedCredential.getCredential().getResponse();
if (!(storedAttData.decodedObject.getAuthenticatorData().getAttData()
.getPublicKey() instanceof EccKey)) {
throw new ServletException("Ecc key not provided");
}
EccKey publicKey =
(EccKey) storedAttData.decodedObject.getAuthenticatorData().getAttData().getPublicKey();
try {
byte[] clientDataHash = Crypto.sha256Digest(assertionResponse.getClientDataBytes());
byte[] signedBytes =
Bytes.concat(assertionResponse.getAuthenticatorData().encode(), clientDataHash);
if (!Crypto.verifySignature(Crypto.decodePublicKey(publicKey.getX(), publicKey.getY()),
signedBytes, assertionResponse.getSignature())) {
throw new ServletException("Signature invalid");
}
} catch (WebAuthnException e) {
throw new ServletException("Failure while verifying signature", e);
} catch (CborException e) {
throw new ServletException("Failure while verifying authenticator data");
}
if (Integer.compareUnsigned(assertionResponse.getAuthenticatorData().getSignCount(),
savedCredential.getSignCount()) <= 0 && savedCredential.getSignCount() != 0) {
throw new ServletException("Sign count invalid");
}
savedCredential.updateSignCount(assertionResponse.getAuthenticatorData().getSignCount());
Log.info("Signature verified");
}
}
package com.google.webauthn.gaedemo.server;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
public class Datastore {
private static DatastoreService datastore = null;
public static DatastoreService getDatastore() {
if (datastore == null) {
synchronized (Datastore.class) {
if (datastore == null) {
datastore = DatastoreServiceFactory.getDatastoreService();
}
}
}
return datastore;
}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.server;
import com.google.webauthn.gaedemo.objects.AndroidSafetyNetAttestationStatement;
import com.google.webauthn.gaedemo.objects.AuthenticatorAssertionResponse;
import com.google.webauthn.gaedemo.objects.AuthenticatorAttestationResponse;
import com.google.webauthn.gaedemo.objects.EccKey;
import com.google.webauthn.gaedemo.objects.FidoU2fAttestationStatement;
import com.google.webauthn.gaedemo.objects.NoneAttestationStatement;
import com.google.webauthn.gaedemo.objects.PackedAttestationStatement;
import com.google.webauthn.gaedemo.objects.RsaKey;
import com.google.webauthn.gaedemo.storage.AttestationSessionData;
import com.google.webauthn.gaedemo.storage.Credential;
import com.google.webauthn.gaedemo.storage.User;
import com.googlecode.objectify.ObjectifyService;
import com.googlecode.objectify.VoidWork;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
public class OfyHelper implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent event) {
ObjectifyService.run(new VoidWork() {
@Override
public void vrun() {
ObjectifyService.register(User.class);
ObjectifyService.register(Credential.class);
ObjectifyService.register(AttestationSessionData.class);
ObjectifyService.register(AuthenticatorAttestationResponse.class);
ObjectifyService.register(AuthenticatorAssertionResponse.class);
ObjectifyService.register(RsaKey.class);
ObjectifyService.register(EccKey.class);
ObjectifyService.register(FidoU2fAttestationStatement.class);
ObjectifyService.register(PackedAttestationStatement.class);
ObjectifyService.register(AndroidSafetyNetAttestationStatement.class);
ObjectifyService.register(NoneAttestationStatement.class);
}
});
}
@Override
public void contextDestroyed(ServletContextEvent sce) {}
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.server;
import com.google.gson.Gson;
public class PublicKeyCredentialResponse {
protected boolean success;
protected String message;
protected String handle;
public PublicKeyCredentialResponse(boolean success, String message) {
this.success = success;
this.message = message;
}
public PublicKeyCredentialResponse(boolean success, String message, String handle) {
this.success = success;
this.message = message;
this.handle = handle;
}
public String toJson() {
Gson gson = new Gson();
return gson.toJson(this);
}
}
This diff is collapsed.
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.servlets;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Servlet class handling the http traffic for asset links.
*/
public class AssetLinksHttpServlet extends HttpServlet {
/**
*
*/
private static final long serialVersionUID = 4479027193794261437L;
private static final String PATH = "/well-known/assetlinks.json";
public AssetLinksHttpServlet() {}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// Do anything else that needs doing here
if (request.getRequestURI().toLowerCase().contains(".json")) {
response.setContentType("application/json");
}
// Read and return the resource from the non-hidden folder
String respString = readResource(PATH);
// response.getOutputStream().print(respString);
response.getWriter().print(respString);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
private String readResource(String resName) throws IOException {
InputStream in = getServletContext().getResourceAsStream(resName);
return inputStreamToString(in);
}
private static String inputStreamToString(InputStream in) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8"));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
return sb.toString();
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.webauthn.gaedemo.storage;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Parent;
import com.google.appengine.api.datastore.ShortBlob;
import com.googlecode.objectify.Key;
@Entity
public class Authenticator {
@Parent
Key<User> theUser;
@Id
public Long id;
public ShortBlob keyHandle;
public ShortBlob publicKey;
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment