POI:设置密码以防止更改

POI: Set password to protect from changes

我想使用 Apache POI 对 OOXML 文件启用密码保护。

通过 Office 程序,在保存文件时(pptxxlsx、...)我能够 select Tools > Options 并且我有提示为打开 and/or 更改文件设置密码。

现在我通过 google 搜索了几个小时并阅读了一些 API 页面以找到 POI 方法,但找不到任何东西。

知道这是否已实施或微软的专长,因为他们根本不在乎他们自己的标准化?

编辑: 由于下面的第一条评论指向 Office 2003 文档,我可能会明确指出:我在谈论 XSS* 功能。我想保护 2007 年以来的 OOXML 格式。我在不同的 API 上查找类似的功能,但找不到那些。 HSSWorkBook#writeProtect... 我知道。

尽管 Excel 在保存文件时一步完成此操作,但这是两个步骤。

起初 ReadOnlyRecommended 设置在 /xl/workbook.xml 中,看起来像:

<workbook>
 ...
 <fileSharing readOnlyRecommended="true" userName="user" reservationPassword="DC45"/>
 ...

可以使用 org.openxmlformats.schemas.spreadsheetml.x2006.main.CTFileSharing which you can get/set in CTWorkbook which you can get from XSSFWorkbook.getCTWorkbook 设置 fileSharing 元素。

reservationPassword 的密码哈希值是通过特殊算法计算的。遗憾的是,大多数 Office Open XML 规范中并未正确描述此算法。我在 Office Open XML Part 4 - Transitional Migration Features.pdf, page 229 and 230.

中找到了正确的描述

完成此步骤后,您将拥有一个建议只读的工作簿和一个以写入权限打开的密码。

如果完成,您现在可以设置加密,如 Apache POI - Encryption support 所示。

示例:

import java.io.*;
import org.apache.poi.openxml4j.opc.*;
import org.apache.poi.poifs.filesystem.*;
import org.apache.poi.poifs.crypt.*;

import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;

import java.nio.ByteBuffer;

import org.openxmlformats.schemas.spreadsheetml.x2006.main.*;

public class OOXMLEncryptionTest {

 //password hashed using the low-order word algorithm defined in §14.7.1 of ECMA-376
 static short getPasswordHash(String szPassword) {
  int wPasswordHash;
  byte[] pch = szPassword.getBytes();
  int cchPassword = pch.length;
  wPasswordHash = 0;
  if (cchPassword > 0) {
   for (int i = cchPassword; i > 0; i--) {
    wPasswordHash = ((wPasswordHash >> 14) & 0x01) | ((wPasswordHash << 1) & 0x7FFF);
    wPasswordHash ^= pch[i-1];
   }
   wPasswordHash = ((wPasswordHash >> 14) & 0x01) | ((wPasswordHash << 1) & 0x7FFF);
   wPasswordHash ^= cchPassword;
   wPasswordHash ^= (0x8000 | ('N' << 8) | 'K');
  }
System.out.println(wPasswordHash); 
  return (short)(wPasswordHash);
 }

 public static void main(String[] args) throws Exception {

  // Open an Excel workbook and set ReadOnlyRecommended
  XSSFWorkbook workbook = (XSSFWorkbook)WorkbookFactory.create(new FileInputStream("ExcelTest.xlsx"));
  CTWorkbook ctWorkbook = workbook.getCTWorkbook();
  CTFileSharing ctfilesharing = ctWorkbook.getFileSharing();
  if (ctfilesharing == null) ctfilesharing = ctWorkbook.addNewFileSharing();
  ctfilesharing.setReadOnlyRecommended(true);
  ctfilesharing.setUserName("user");

  short passwordhash = getPasswordHash("baafoo");
System.out.println(passwordhash); 

  byte[] bpasswordhash = ByteBuffer.allocate(2).putShort(passwordhash).array();
  ctfilesharing.setReservationPassword(bpasswordhash);

  workbook.write(new FileOutputStream("ExcelTestRORecommended.xlsx"));
  workbook.close();

  // Now do the encryption
  POIFSFileSystem fs = new POIFSFileSystem();
  EncryptionInfo info = new EncryptionInfo(EncryptionMode.agile);
  // EncryptionInfo info = new EncryptionInfo(EncryptionMode.agile, CipherAlgorithm.aes192, HashAlgorithm.sha384, -1, -1, null);

  Encryptor enc = info.getEncryptor();
  enc.confirmPassword("foobaa");

  // Read in an existing OOXML file
  OPCPackage opc = OPCPackage.open(new File("ExcelTestRORecommended.xlsx"), PackageAccess.READ_WRITE);
  OutputStream os = enc.getDataStream(fs);
  opc.save(os);
  opc.close();

  // Write out the encrypted version
  FileOutputStream fos = new FileOutputStream("ExcelTestEncrypted.xlsx");
  fs.writeFilesystem(fos);
  fos.close();

 }
}

设置只读建议似乎对所有 Microsoft Office 文件类型都是一样的,因为在所有情况下您都将在文件保存时设置只读建议,但在幕后并非如此。 Microsoft 将其存储到文件中的方式非常不同。

Excel 中它是 ReadOnlyRecommended 在工作簿的 FileSharing 元素中并且仅使用非常不安全的 2 字节密码哈希。

Word中它是设置部分的WriteProtection元素。它使用现代加密方法使用盐渍密码哈希。

PowerPoint 中,演示文稿中的 ModifyVerifier 元素也使用了现代加密方法的加盐密码哈希。

以下示例展示了所有三种方法:

import java.io.*;

import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.openxmlformats.schemas.spreadsheetml.x2006.main.*;
import java.nio.ByteBuffer;

import org.apache.poi.xwpf.usermodel.*;
import org.apache.poi.POIXMLDocumentPart;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;

import org.apache.poi.xslf.usermodel.XMLSlideShow;
import org.openxmlformats.schemas.presentationml.x2006.main.*;

import org.apache.poi.poifs.crypt.CryptoFunctions;
import org.apache.poi.poifs.crypt.HashAlgorithm;
import java.security.SecureRandom;
import java.math.BigInteger;
import java.lang.reflect.Field;

public class RORecommendedTest {

 //password hashed using the low-order word algorithm defined in §14.7.1 of ECMA-376
 static short getPasswordHash(String szPassword) {
  int wPasswordHash;
  byte[] pch = szPassword.getBytes();
  int cchPassword = pch.length;
  wPasswordHash = 0;
  if (cchPassword > 0) {
   for (int i = cchPassword; i > 0; i--) {
    wPasswordHash = ((wPasswordHash >> 14) & 0x01) | ((wPasswordHash << 1) & 0x7FFF);
    wPasswordHash ^= pch[i-1];
   }
   wPasswordHash = ((wPasswordHash >> 14) & 0x01) | ((wPasswordHash << 1) & 0x7FFF);
   wPasswordHash ^= cchPassword;
   wPasswordHash ^= (0x8000 | ('N' << 8) | 'K');
  }
  return (short)(wPasswordHash);
 }

 public static void main(String[] args) throws Exception {

  // Open an Excel workbook and set ReadOnlyRecommended
  XSSFWorkbook workbook = (XSSFWorkbook)WorkbookFactory.create(new FileInputStream("ExcelTest.xlsx"));
  CTWorkbook ctWorkbook = workbook.getCTWorkbook();
  CTFileSharing ctfilesharing = ctWorkbook.getFileSharing();
  if (ctfilesharing == null) ctfilesharing = ctWorkbook.addNewFileSharing();
  ctfilesharing.setReadOnlyRecommended(true);
  ctfilesharing.setUserName("user");

  short passwordhash = getPasswordHash("baafoo");

  byte[] bpasswordhash = ByteBuffer.allocate(2).putShort(passwordhash).array();
  ctfilesharing.setReservationPassword(bpasswordhash);

  workbook.write(new FileOutputStream("ExcelTestRORecommended.xlsx"));
  workbook.close();


  // Open a Word document and set read only recommended aka WriteProtection
  XWPFDocument document = new XWPFDocument(new FileInputStream("WordTest.docx"));

  POIXMLDocumentPart part = null;
  for (int i = 0; i < document.getRelations().size(); i++) {
   part = document.getRelations().get(i);
   if (part instanceof XWPFSettings) break;
  }
  if (part instanceof XWPFSettings) {
   XWPFSettings settings = (XWPFSettings)part;

   Field _ctSettings = XWPFSettings.class.getDeclaredField("ctSettings"); 
   _ctSettings.setAccessible(true); 
   CTSettings ctSettings = (CTSettings)_ctSettings.get(settings);

   CTWriteProtection ctwriteprotection = ctSettings.getWriteProtection();
   if (ctwriteprotection == null) ctwriteprotection = ctSettings.addNewWriteProtection();
   ctwriteprotection.setRecommended(STOnOff.ON);

   ctwriteprotection.setCryptProviderType(org.openxmlformats.schemas.wordprocessingml.x2006.main.STCryptProv.RSA_FULL);
   ctwriteprotection.setCryptAlgorithmClass(org.openxmlformats.schemas.wordprocessingml.x2006.main.STAlgClass.HASH);
   ctwriteprotection.setCryptAlgorithmType(org.openxmlformats.schemas.wordprocessingml.x2006.main.STAlgType.TYPE_ANY);
   ctwriteprotection.setCryptAlgorithmSid(BigInteger.valueOf(4)); //SHA-1
   ctwriteprotection.setCryptSpinCount(BigInteger.valueOf(100000));

   SecureRandom random = new SecureRandom();
   byte[] salt = random.generateSeed(16);
   byte[] hash = CryptoFunctions.hashPassword("baafoo", HashAlgorithm.sha1, salt, 100000, false);

   ctwriteprotection.setHash(hash);
   ctwriteprotection.setSalt(salt);
  }

  document.write(new FileOutputStream("WordTestRORecommended.docx"));
  document.close();

  // Open a PowerPoint show and set read only recommended aka ModifyVerifier
  XMLSlideShow slideShow = new XMLSlideShow(new FileInputStream("PowerPntTest.pptx"));
  CTPresentation ctpresentation = slideShow.getCTPresentation();
  CTModifyVerifier ctmodifyverifier = ctpresentation.getModifyVerifier();
  if (ctmodifyverifier == null) ctmodifyverifier = ctpresentation.addNewModifyVerifier();

  ctmodifyverifier.setCryptProviderType(org.openxmlformats.schemas.presentationml.x2006.main.STCryptProv.RSA_FULL);
  ctmodifyverifier.setCryptAlgorithmClass(org.openxmlformats.schemas.presentationml.x2006.main.STAlgClass.HASH);
  ctmodifyverifier.setCryptAlgorithmType(org.openxmlformats.schemas.presentationml.x2006.main.STAlgType.TYPE_ANY);
  ctmodifyverifier.setCryptAlgorithmSid(4); //SHA-1
  ctmodifyverifier.setSpinCount(100000);

  SecureRandom random = new SecureRandom();
  byte[] salt = random.generateSeed(16);
  byte[] hash = CryptoFunctions.hashPassword("baafoo", HashAlgorithm.sha1, salt, 100000, false);

  ctmodifyverifier.setHashData(java.util.Base64.getEncoder().encodeToString(hash));
  ctmodifyverifier.setSaltData(java.util.Base64.getEncoder().encodeToString(salt));

  slideShow.write(new FileOutputStream("PowerPntTestRORecommended.pptx"));
  slideShow.close();

 }
}