All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.facebook.delegatedrecovery.RecoveryToken Maven / Gradle / Ivy

The newest version!
/*
 * 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;

  /**
   * 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;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy