Java tutorial
/* * Copyright (c) 2016-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.delegatedrecovery; import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.DERSequenceGenerator; import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.params.ECPrivateKeyParameters; import org.bouncycastle.crypto.signers.ECDSASigner; import org.bouncycastle.crypto.signers.HMacDSAKCalculator; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.*; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.util.Arrays; import java.util.Base64; /** * Represents the recovery token, and serves as a base class for the * countersigned recovery token, in the delegated account recovery protocol. */ public class RecoveryToken { /** * No options for token options field */ public static final byte NO_OPTIONS = 0x00; /** * Status callbacks requested token options flag. */ public static final byte STATUS_REQUESTED_FLAG = 0x01; /** * Low-friction token recovery requested options flag. */ public static final byte LOW_FRICTION_REQUESTED_FLAG = 0x02; /** * Mandatory version field value. */ public static final byte VERSION = 0x00; /** * Token type field for recovery token. */ public static final byte TYPE_RECOVERY_TOKEN = 0x00; /** * Token type field for countersigned recovery token. */ public static final byte TYPE_COUNTERSIGNED_TOKEN = 0x01; protected byte type; protected byte version; protected byte[] id; protected byte options; protected String issuer; protected String audience; protected String issuedTime; protected byte[] data; protected byte[] binding; protected byte[] signature; protected byte[] decoded; protected String encoded; /** * Construct a RecoveryToken. * * @param privateKey The key to sign this token with. * @param id A unique id for the key. * @param options A set of bit flags setting options on the token * @param issuer The RFC-6454 origin of the recovery service * @param audience The RFC-6454 origin of your service * @param data Additional data to store in the token, can be null. This data will not be encrypted by this method. * @param binding token binding string to verify against, usually null * @throws InvalidOriginException If the issuer or audience is invalid * @throws IOException If signature fails DER encoding */ public RecoveryToken(final ECPrivateKey privateKey, final byte[] id, final byte options, final String issuer, final String audience, final byte[] data, final byte[] binding) throws InvalidOriginException, IOException { if (id == null || id.length != 16) { throw new InvalidParameterException("token id must be byte[16]"); } DelegatedRecoveryUtils.validateOrigin(issuer); DelegatedRecoveryUtils.validateOrigin(audience); this.version = VERSION; this.type = TYPE_RECOVERY_TOKEN; this.id = id.clone(); this.options = options; this.issuer = issuer; this.audience = audience; this.data = data.clone(); this.binding = binding.clone(); this.issuedTime = DelegatedRecoveryUtils.nowISO8601(); final int tokenLength = 1 + // uint8 version 1 + // uint8 type 16 + // byte[16] token_id 1 + // uint8 options 2 + // uint16 issuer_length issuer.length() + // issuer[issuer_length] 2 + // uint16 audience_length audience.length() + // audience[audience_length] 2 + // uint16 issued_time_length issuedTime.length() + // issued_time[isued_time_length] 2 + // uint16 data_length data.length + // data[data_length] 2 + // uint16 binding_length binding.length; // binding[binding_length] final byte[] rawToken = new byte[tokenLength]; final ByteBuffer tokenBuffer = ByteBuffer.wrap(rawToken); tokenBuffer.put(RecoveryToken.VERSION).put(RecoveryToken.TYPE_RECOVERY_TOKEN).put(id).put(options) .putChar((char) issuer.length()).put(issuer.getBytes(StandardCharsets.US_ASCII)) .putChar((char) audience.length()).put(audience.getBytes(StandardCharsets.US_ASCII)) .putChar((char) issuedTime.length()).put(issuedTime.getBytes(StandardCharsets.US_ASCII)) .putChar((char) data.length).put(data).putChar((char) binding.length).put(binding); final byte[] rawArray = rawToken; this.signature = getSignature(rawToken, privateKey); this.decoded = new byte[rawArray.length + signature.length]; System.arraycopy(rawArray, 0, decoded, 0, rawArray.length); System.arraycopy(signature, 0, decoded, rawArray.length, signature.length); this.encoded = Base64.getEncoder().encodeToString(decoded); } /** * Check the signature on a token. * * @param keys they keys to validate * @return whether signature is valid * @throws InvalidKeyException If the keys are invalid * @throws SignatureException If the keys are invalid */ public boolean isSignatureValid(final ECPublicKey[] keys) throws InvalidKeyException, SignatureException { try { final Signature verifier = Signature.getInstance("SHA256withECDSA"); for (final ECPublicKey key : keys) { verifier.initVerify(key); verifier.update(Arrays.copyOfRange(decoded, 0, decoded.length - signature.length)); if (verifier.verify(signature)) { return true; } } return false; } catch (final NoSuchAlgorithmException e) { throw new Error(e.getMessage()); } } /** * Construct a token from an encoded string. This constructor does not * validate the token signature. * * @param encoded Base64 encoded binary token * @throws InvalidOriginException If the issuer or audience in the token are invalid */ public RecoveryToken(final String encoded) throws InvalidOriginException { try { this.encoded = encoded; decoded = Base64.getDecoder().decode(encoded); int offset = 0; version = decoded[offset]; offset += 1; type = decoded[offset]; offset += 1; id = Arrays.copyOfRange(decoded, offset, offset + 16); offset += 16; options = decoded[offset]; offset += 1; final int issuerLength = decoded[offset] << 8 & 0xFF00 | decoded[offset + 1] & 0xFF; offset += 2; issuer = new String(Arrays.copyOfRange(decoded, offset, offset + issuerLength), "US-ASCII"); offset += issuerLength; final int audienceLength = decoded[offset] << 8 & 0xFF00 | decoded[offset + 1] & 0xFF; offset += 2; audience = new String(Arrays.copyOfRange(decoded, offset, offset + audienceLength), "US-ASCII"); offset += audienceLength; final int issuedTimeLength = decoded[offset] << 8 & 0xFF00 | decoded[offset + 1] & 0xFF; offset += 2; issuedTime = new String(Arrays.copyOfRange(decoded, offset, offset + issuedTimeLength), "US-ASCII"); offset += issuedTimeLength; final int dataLength = decoded[offset] << 8 & 0xFF00 | decoded[offset + 1] & 0xFF; offset += 2; data = Arrays.copyOfRange(decoded, offset, offset + dataLength); offset += dataLength; final int bindingLength = decoded[offset] << 8 & 0xFF00 | decoded[offset + 1] & 0xFF; offset += 2; binding = Arrays.copyOfRange(decoded, offset, offset + bindingLength); offset += bindingLength; signature = Arrays.copyOfRange(decoded, offset, decoded.length); commonSanityCheck(); typedSanityCheck(); } catch (final UnsupportedEncodingException e) { throw new Error(e.getMessage()); } } protected void commonSanityCheck() throws InvalidOriginException { if (version != VERSION) { throw new IllegalArgumentException("illegal version"); } DelegatedRecoveryUtils.validateOrigin(issuer); DelegatedRecoveryUtils.validateOrigin(audience); } protected void typedSanityCheck() { if (type != RecoveryToken.TYPE_COUNTERSIGNED_TOKEN) { throw new IllegalArgumentException("illegal token type"); } } private byte[] getSignature(final byte[] rawArray, final ECPrivateKey privateKey) throws IOException { if (this.signature != null) { throw new IllegalStateException("This token already has a signature."); } final BigInteger privatePoint = privateKey.getS(); final SHA256Digest digest = new SHA256Digest(); final byte[] hash = new byte[digest.getByteLength()]; digest.update(rawArray, 0, rawArray.length); digest.doFinal(hash, 0); final ECDSASigner signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest())); signer.init(true, new ECPrivateKeyParameters(privatePoint, DelegatedRecoveryUtils.P256_DOMAIN_PARAMS)); final BigInteger[] signature = signer.generateSignature(hash); final ByteArrayOutputStream s = new ByteArrayOutputStream(); final DERSequenceGenerator seq = new DERSequenceGenerator(s); seq.addObject(new ASN1Integer(signature[0])); seq.addObject(new ASN1Integer(signature[1])); seq.close(); return s.toByteArray(); } public byte getType() { return type; } public byte getVersion() { return version; } public byte[] getId() { return id == null ? null : id.clone(); } public byte getOptions() { return options; } public String getIssuer() { return issuer; } public String getAudience() { return audience; } /** * ISO8601 time string * @return the issued time */ public String getIssuedTime() { if (this.signature == null) { throw new IllegalStateException("This token has not been signed. Call getSigned(privateKey) first."); } return issuedTime; } public byte[] getData() { return data == null ? null : data.clone(); } public byte[] getBinding() { return binding == null ? null : binding.clone(); } public byte[] getSignature() { return signature == null ? null : signature.clone(); } public String getEncoded() throws IllegalStateException { return encoded; } }