EncryptionUtil.java

/*
Copyright (c) 2025 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.io.IOException;
import java.nio.file.Path;

import com.healthmarketscience.jackcess.crypt.impl.DbFileRewriter;
import com.healthmarketscience.jackcess.crypt.impl.JetEncodingHandler;
import com.healthmarketscience.jackcess.crypt.impl.OfficeEncryptionHandler;

/**
 * Adds and removes the encryption on a database.  Reading an encrypted
 * database needs {@link CryptCodecProvider} instead, and the jet database
 * password, which is a different feature, is {@link PasswordUtil}.
 * <p>
 * Every operation rewrites the whole file, so each takes a path rather than an
 * open {@link com.healthmarketscience.jackcess.Database}.  The work builds a
 * separate file and replaces the original only once that file is complete, so
 * an interrupted operation leaves the original database as it was.  The in
 * place forms give the database a new identity on disk, which drops any hard
 * link to it, and the database must not be in use.
 * <p>
 * <b>Jet encoding</b> is what ms access called "Encode/Decode Database" and
 * removed after access 2003.  It applies to {@code .mdb} files, it has no
 * password, and its key is stored in the file, so it stops only a reader which
 * does not know about it.  A current ms access opens an encoded database and
 * cannot add or remove the encoding.
 * <p>
 * <b>Only jet 4 can be altered.</b>  Adding or removing the encoding rewrites
 * the database, and jackcess does not write jet 3.  An encoded jet 3 database
 * still reads through {@link CryptCodecProvider}, which needs no change to the
 * file.
 * <p>
 * Jet encoding and the jet database password are independent, and this class
 * leaves the password alone in both directions.  To have both, call
 * {@link PasswordUtil} as well, in either order.
 *
 * @author James Ahlborn
 */
public class EncryptionUtil
{
  private EncryptionUtil() {}

  /**
   * Encrypts the given {@code .accdb} database with agile encryption,
   * replacing the file with the result.  Agile is what a current ms access
   * writes, and {@link AgileEncryptionConfig#OFFICE_2010} is the profile it
   * chooses.
   *
   * @param dbFile the plain database, not in use
   * @param password the password to encrypt it with, not empty
   * @throws IllegalStateException if it is already encrypted
   */
  public static void addOfficeAgileEncryption(Path dbFile, String password)
    throws IOException
  {
    addOfficeAgileEncryption(dbFile, password,
                             AgileEncryptionConfig.OFFICE_2010);
  }

  /**
   * Encrypts the given {@code .accdb} database with agile encryption under
   * the given algorithms, replacing the file with the result.
   */
  public static void addOfficeAgileEncryption(
      Path dbFile, String password, AgileEncryptionConfig config)
    throws IOException
  {
    DbFileRewriter.rewriteInPlace(
        dbFile, (src, dst) ->
        OfficeEncryptionHandler.addEncryption(src, dst, password, config));
  }

  /**
   * Writes an agile encrypted copy of the given {@code .accdb} database,
   * leaving the source alone.
   */
  public static void addOfficeAgileEncryption(
      Path srcFile, Path dstFile, String password)
    throws IOException
  {
    addOfficeAgileEncryption(srcFile, dstFile, password,
                             AgileEncryptionConfig.OFFICE_2010);
  }

  /**
   * Writes a copy of the given {@code .accdb} database encrypted under the
   * given algorithms, leaving the source alone.
   */
  public static void addOfficeAgileEncryption(
      Path srcFile, Path dstFile, String password,
      AgileEncryptionConfig config)
    throws IOException
  {
    DbFileRewriter.rewriteTo(
        srcFile, dstFile, (src, dst) ->
        OfficeEncryptionHandler.addEncryption(src, dst, password, config));
  }

  /**
   * Removes the encryption from the given {@code .accdb} database, replacing
   * the file with the result.
   * <p>
   * Every scheme {@link CryptCodecProvider} can read is supported, which is
   * agile and the older ones alike.  Extensible encryption is the exception,
   * because it delegates to an external provider and cannot be read at all.
   * <p>
   * The format version is left as it is.  Ms access raises it when it writes
   * agile encryption and does not lower it again when the encryption is
   * removed, so a decrypted database keeps the raised version.
   *
   * @param dbFile the encrypted database, not in use
   * @param password the password it was encrypted with
   * @throws IllegalStateException if it is not encrypted
   * @throws InvalidCredentialsException if the password is wrong
   */
  public static void removeOfficeEncryption(Path dbFile, String password)
    throws IOException
  {
    DbFileRewriter.rewriteInPlace(
        dbFile, (src, dst) ->
        OfficeEncryptionHandler.removeEncryption(src, dst, password));
  }

  /**
   * Writes a decrypted copy of the given {@code .accdb} database, leaving the
   * source alone.
   *
   * @param srcFile the encrypted database
   * @param dstFile where to write it, overwritten if it exists
   * @param password the password the source was encrypted with
   * @throws IllegalStateException if the source is not encrypted
   * @throws InvalidCredentialsException if the password is wrong
   */
  public static void removeOfficeEncryption(Path srcFile, Path dstFile,
                                            String password)
    throws IOException
  {
    DbFileRewriter.rewriteTo(
        srcFile, dstFile, (src, dst) ->
        OfficeEncryptionHandler.removeEncryption(src, dst, password));
  }

  /**
   * Adds jet encoding to the given database, under a freshly generated key,
   * replacing the file with the result.
   *
   * @param dbFile the jet 4 database to encode, not in use
   * @throws IllegalStateException if it is already encoded
   */
  public static void addJetEncoding(Path dbFile) throws IOException
  {
    DbFileRewriter.rewriteInPlace(dbFile, JetEncodingHandler::addEncoding);
  }

  /**
   * Writes an encoded copy of the given database, under a freshly generated
   * key, leaving the source alone.
   *
   * @param srcFile the jet 4 database to encode
   * @param dstFile where to write it, overwritten if it exists
   * @throws IllegalStateException if the source is already encoded
   */
  public static void addJetEncoding(Path srcFile, Path dstFile)
    throws IOException
  {
    DbFileRewriter.rewriteTo(srcFile, dstFile, JetEncodingHandler::addEncoding);
  }

  /**
   * Removes the jet encoding from the given database, replacing the file with
   * the result.
   *
   * @param dbFile the encoded jet 4 database, not in use
   * @throws IllegalStateException if it is not encoded
   */
  public static void removeJetEncoding(Path dbFile) throws IOException
  {
    DbFileRewriter.rewriteInPlace(dbFile, JetEncodingHandler::removeEncoding);
  }

  /**
   * Writes a plain copy of the given encoded database, leaving the source
   * alone.
   *
   * @param srcFile the encoded jet 4 database
   * @param dstFile where to write it, overwritten if it exists
   * @throws IllegalStateException if the source is not encoded
   */
  public static void removeJetEncoding(Path srcFile, Path dstFile)
    throws IOException
  {
    DbFileRewriter.rewriteTo(srcFile, dstFile,
                             JetEncodingHandler::removeEncoding);
  }
}