AgileEncryptionConfig.java

/*
Copyright (c) 2026 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;

import java.util.Set;

/**
 * The algorithms agile encryption is written with.
 * <p>
 * Agile carries its parameters in the file, so a reader honours whatever it
 * finds, but ms access writes only two combinations and there is no evidence
 * it opens any other.  {@link #OFFICE_2010} and {@link #OFFICE_2013} are
 * those two, measured from files ms access wrote.  The enums below hold what
 * has been verified rather than everything the format allows, so a
 * configuration built by hand cannot name an algorithm which has never been
 * tested.
 * <p>
 * The block size and the hash size are not settable, because each follows
 * from the algorithm chosen.  A descriptor which disagrees with itself parses
 * and then fails to decrypt, so the file would look written and only ms
 * access would say otherwise.
 *
 * @author James Ahlborn
 */
public class AgileEncryptionConfig
{
  /** The ciphers which can be written, with the name the descriptor uses. */
  public enum Cipher
  {
    AES("AES", 16);

    private final String _xmlName;
    private final int _blockSize;

    private Cipher(String xmlName, int blockSize) {
      _xmlName = xmlName;
      _blockSize = blockSize;
    }

    public String getXmlName() {
      return _xmlName;
    }

    /** Returns the cipher block size, which the descriptor also carries. */
    public int getBlockSize() {
      return _blockSize;
    }
  }

  /** The chaining modes which can be written. */
  public enum Chaining
  {
    CBC("ChainingModeCBC");

    private final String _xmlName;

    private Chaining(String xmlName) {
      _xmlName = xmlName;
    }

    public String getXmlName() {
      return _xmlName;
    }
  }

  /** The hashes which can be written, with the length each produces. */
  public enum Hash
  {
    SHA1("SHA1", 20),
    SHA256("SHA256", 32),
    SHA384("SHA384", 48),
    SHA512("SHA512", 64);

    private final String _xmlName;
    private final int _size;

    private Hash(String xmlName, int size) {
      _xmlName = xmlName;
      _size = size;
    }

    public String getXmlName() {
      return _xmlName;
    }

    /** Returns the hash length, which the descriptor also carries. */
    public int getSize() {
      return _size;
    }
  }

  /**
   * The key sizes which can be written.  192 bits is missing because the key
   * value is encrypted whole and unpadded, and 24 bytes is not a whole number
   * of 16 byte blocks, so the cipher refuses it.  Ms access writes neither
   * 192 bit keys nor a padded key value.
   */
  private static final Set<Integer> KEY_BITS = Set.of(128, 256);

  /** the spin count ms access writes in both of its profiles */
  public static final int DEFAULT_SPIN_COUNT = 100000;

  /** the salt size ms access writes in both of its profiles */
  public static final int DEFAULT_SALT_SIZE = 16;

  /** what ms access writes by default, and still writes today */
  public static final AgileEncryptionConfig OFFICE_2010 =
    new AgileEncryptionConfig(Cipher.AES, Chaining.CBC, 128, Hash.SHA1);

  /** the stronger profile ms access 2013 writes */
  public static final AgileEncryptionConfig OFFICE_2013 =
    new AgileEncryptionConfig(Cipher.AES, Chaining.CBC, 256, Hash.SHA512);

  private final Cipher _cipher;
  private final Chaining _chaining;
  private final int _keyBits;
  private final Hash _hash;
  private final int _spinCount;
  private final int _saltSize;

  /**
   * Uses the spin count and the salt size ms access writes, which are the
   * same in both of its profiles.
   */
  public AgileEncryptionConfig(Cipher cipher, Chaining chaining, int keyBits,
                               Hash hash)
  {
    this(cipher, chaining, keyBits, hash, DEFAULT_SPIN_COUNT,
         DEFAULT_SALT_SIZE);
  }

  public AgileEncryptionConfig(Cipher cipher, Chaining chaining, int keyBits,
                               Hash hash, int spinCount, int saltSize)
  {
    if((cipher == null) || (chaining == null) || (hash == null)) {
      throw new IllegalArgumentException(
          "Cipher, chaining mode and hash are all required");
    }
    if(!KEY_BITS.contains(keyBits)) {
      throw new IllegalArgumentException(
          "Unsupported key size " + keyBits + ", expected one of " + KEY_BITS);
    }
    if(spinCount < 0) {
      throw new IllegalArgumentException(
          "Spin count may not be negative, got " + spinCount);
    }
    // the password salt is the IV the wrapped values are encrypted under,
    // and the read path hands it to the cipher as it stands, which accepts
    // only an IV of exactly the block size
    if(saltSize != cipher.getBlockSize()) {
      throw new IllegalArgumentException(
          "Salt size must be the " + cipher.getBlockSize() +
          " byte block size of " + cipher + ", got " + saltSize);
    }

    // the key is the first bytes of a hash, and a hash shorter than the key
    // is padded out with a fixed byte.  Neither profile ms access writes
    // reaches that padding, so a combination which would is refused rather
    // than written on the strength of the spec alone
    if(hash.getSize() < (keyBits / 8)) {
      throw new IllegalArgumentException(
          "A " + keyBits + " bit key needs a hash longer than " + hash +
          ", which gives " + hash.getSize() + " bytes");
    }

    _cipher = cipher;
    _chaining = chaining;
    _keyBits = keyBits;
    _hash = hash;
    _spinCount = spinCount;
    _saltSize = saltSize;
  }

  public Cipher getCipher() {
    return _cipher;
  }

  public Chaining getChaining() {
    return _chaining;
  }

  public int getKeyBits() {
    return _keyBits;
  }

  public Hash getHash() {
    return _hash;
  }

  public int getSpinCount() {
    return _spinCount;
  }

  public int getSaltSize() {
    return _saltSize;
  }

  /** Returns the cipher block size, which follows from the cipher. */
  public int getBlockSize() {
    return _cipher.getBlockSize();
  }

  /** Returns the hash length, which follows from the hash. */
  public int getHashSize() {
    return _hash.getSize();
  }

  @Override
  public String toString() {
    return "AgileEncryptionConfig[" + _cipher + " " + _chaining + " " +
      _keyBits + " bits, " + _hash + ", spinCount " + _spinCount +
      ", saltSize " + _saltSize + "]";
  }
}