AgileEncryptionProvider.java

/*
Copyright (c) 2013 James Ahlborn

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.healthmarketscience.jackcess.crypt.impl.office;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;

import com.healthmarketscience.jackcess.crypt.InvalidCryptoConfigurationException;
import com.healthmarketscience.jackcess.crypt.model.CTEncryption;
import com.healthmarketscience.jackcess.crypt.model.CTKeyData;
import com.healthmarketscience.jackcess.crypt.model.CTKeyEncryptor;
import com.healthmarketscience.jackcess.crypt.model.password.CTPasswordKeyEncryptor;
import com.healthmarketscience.jackcess.crypt.model.password.STPasswordKeyEncryptorUri;
import com.healthmarketscience.jackcess.impl.PageChannel;
import org.bouncycastle.crypto.BlockCipher;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;

/**
 *
 * @author James Ahlborn
 */
public class AgileEncryptionProvider extends BlockCipherProvider
{
  private static final int RESERVED_VAL = 0x40;

  /** the block constants which separate the three wrapped values */
  static final byte[] VERIFIER_INPUT_BLOCK = {
    (byte)0xfe, (byte)0xa7, (byte)0xd2, (byte)0x76,
    (byte)0x3b, (byte)0x4b, (byte)0x9e, (byte)0x79};
  static final byte[] VERIFIER_VALUE_BLOCK = {
    (byte)0xd7, (byte)0xaa, (byte)0x0f, (byte)0x6d,
    (byte)0x30, (byte)0x61, (byte)0x34, (byte)0x4e};
  static final byte[] KEY_VALUE_BLOCK = {
    (byte)0x14, (byte)0x6e, (byte)0x0b, (byte)0xe7,
    (byte)0xab, (byte)0xac, (byte)0xd0, (byte)0xd6};

  /** the byte a hash too short for what it becomes is padded with */
  private static final int PAD_BYTE = 0x36;

  private final CTEncryption _encryptDesc;
  private final CTPasswordKeyEncryptor _pwdKeyEnc;
  private final byte[] _keyValue;

  public AgileEncryptionProvider(PageChannel channel, byte[] encodingKey,
                                 ByteBuffer encProvBuf, byte[] pwdBytes)
    throws IOException
  {
    super(channel, encodingKey);

    // OC: 2.3.4.10
    int reservedVal = encProvBuf.getInt();
    if(reservedVal != RESERVED_VAL) {
      throw new InvalidCryptoConfigurationException("Unexpected reserved value " + reservedVal);
    }

    byte[] xmlBytes = new byte[encProvBuf.remaining()];
    encProvBuf.get(xmlBytes);
    _encryptDesc = XmlEncryptionDescriptor.parseEncryptionDescriptor(xmlBytes);

    // for now we expect a single, password key encryptor
    CTPasswordKeyEncryptor pwdKeyEnc = null;
    if((_encryptDesc.getKeyEncryptors() != null) &&
       (_encryptDesc.getKeyEncryptors().getKeyEncryptor().size() == 1)) {
      CTKeyEncryptor keyEnc = _encryptDesc.getKeyEncryptors()
        .getKeyEncryptor().get(0);
      if(STPasswordKeyEncryptorUri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_PASSWORD.value().equals(keyEnc.getUri())) {
        pwdKeyEnc = (CTPasswordKeyEncryptor)keyEnc.getAny();
      }
    }

    if(pwdKeyEnc == null) {
      throw new InvalidCryptoConfigurationException("Missing or unexpected key encryptor");
    }
    _pwdKeyEnc = pwdKeyEnc;

    _keyValue = decryptKeyValue(pwdBytes);
  }

  @Override
  protected Digest initPwdDigest() {
    return XmlEncryptionDescriptor.initDigest(_pwdKeyEnc.getHashAlgorithm());
  }

  @Override
  protected Digest initCryptDigest() {
    return XmlEncryptionDescriptor.initDigest(
        _encryptDesc.getKeyData().getHashAlgorithm());
  }

  @Override
  protected BlockCipher initPwdCipher() {
    return XmlEncryptionDescriptor.initCipher(
        _pwdKeyEnc.getCipherAlgorithm(), _pwdKeyEnc.getCipherChaining());
  }

  @Override
  protected BlockCipher initCryptCipher() {
    CTKeyData keyData = _encryptDesc.getKeyData();
    return XmlEncryptionDescriptor.initCipher(
        keyData.getCipherAlgorithm(), keyData.getCipherChaining());
  }

  @Override
  protected boolean verifyPassword(byte[] pwdBytes) {

    byte[] verifier = decryptVerifierHashInput(pwdBytes);
    byte[] verifierHash = decryptVerifierHashValue(pwdBytes);


    byte[] testHash = padToBlockSize(
        hash(getDigest(), verifier), (int)_pwdKeyEnc.getBlockSize());

    return Arrays.equals(verifierHash, testHash);
  }

  @Override
  protected ParametersWithIV computeCipherParams(int pageNumber) {
    // OC: 2.3.4.15 says to use the zero-based segment number alone as block
    // key, but this is incorrect for MS Access - when actually decrypting
    // pages, we also incorporate the "encoding key"
    byte[] blockBytes = getEncodingKey(pageNumber);

    CTKeyData keyData = _encryptDesc.getKeyData();
    byte[] iv = deriveIV(getDigest(), keyData.getSaltValue(),
                                         blockBytes,
                                         (int)keyData.getBlockSize());
    return new ParametersWithIV(new KeyParameter(_keyValue), iv);
  }

  private byte[] decryptVerifierHashInput(byte[] pwdBytes) {
    // OC: 2.3.4.13 (part 1)
    byte[] key = cryptDeriveKey(pwdBytes, VERIFIER_INPUT_BLOCK);

    return blockDecryptBytes(key, _pwdKeyEnc.getSaltValue(),
                             _pwdKeyEnc.getEncryptedVerifierHashInput());
  }

  private byte[] decryptVerifierHashValue(byte[] pwdBytes) {
    // OC: 2.3.4.13 (part 2)
    byte[] key = cryptDeriveKey(pwdBytes, VERIFIER_VALUE_BLOCK);

    return blockDecryptBytes(key, _pwdKeyEnc.getSaltValue(),
                             _pwdKeyEnc.getEncryptedVerifierHashValue());
  }

  private byte[] decryptKeyValue(byte[] pwdBytes) {
    // OC: 2.3.4.13 (part 3)
    byte[] key = cryptDeriveKey(pwdBytes, KEY_VALUE_BLOCK);

    return blockDecryptBytes(key, _pwdKeyEnc.getSaltValue(),
                             _pwdKeyEnc.getEncryptedKeyValue());
  }

  private byte[] cryptDeriveKey(byte[] pwdBytes, byte[] blockBytes)
  {
    return deriveKey(
        getDigest(), pwdBytes, _pwdKeyEnc.getSaltValue(),
        (int)_pwdKeyEnc.getSpinCount(), blockBytes,
        bits2bytes((int)_pwdKeyEnc.getKeyBits()));
  }

  /**
   * Builds the key for one block constant out of the password and the salt.
   * OC: 2.3.4.11
   */
  static byte[] deriveKey(Digest digest, byte[] pwdBytes, byte[] salt,
                          int spinCount, byte[] blockBytes, int keyByteLen)
  {
    byte[] baseHash = hash(digest, salt, pwdBytes);
    byte[] iterHash = iterateHash(digest, baseHash,
                                                        spinCount);
    byte[] finalHash = hash(digest, iterHash,
                                                  blockBytes);
    return fixToLength(finalHash, keyByteLen, PAD_BYTE);
  }

  /**
   * Builds the IV for one block constant, or returns the salt itself where
   * there is no block constant.  OC: 2.3.4.12
   */
  static byte[] deriveIV(Digest digest, byte[] salt, byte[] blockBytes,
                         int blockSize)
  {
    byte[] ivBytes = ((blockBytes != null) ?
                      hash(digest, salt, blockBytes) :
                      salt);
    return fixToLength(ivBytes, blockSize, PAD_BYTE);
  }

  /**
   * Rounds the given hash up to a whole number of cipher blocks, which is
   * what the verifier hash value is encrypted as.
   */
  static byte[] padToBlockSize(byte[] bytes, int blockSize)
  {
    if((bytes.length % blockSize) == 0) {
      return bytes;
    }
    int len = ((bytes.length + blockSize - 1) / blockSize) * blockSize;
    return fixToLength(bytes, len);
  }
}