- /* ====================================================================
- Licensed to the Apache Software Foundation (ASF) under one or more
- contributor license agreements. See the NOTICE file distributed with
- this work for additional information regarding copyright ownership.
- The ASF licenses this file to You 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 org.apache.poi.poifs.crypt.standard;
-
- import static org.apache.poi.poifs.crypt.DataSpaceMapUtils.createEncryptionEntry;
- import static org.apache.poi.poifs.crypt.standard.StandardDecryptor.generateSecretKey;
-
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.FilterOutputStream;
- import java.io.IOException;
- import java.io.OutputStream;
- import java.security.GeneralSecurityException;
- import java.security.MessageDigest;
- import java.security.SecureRandom;
- import java.util.Arrays;
- import java.util.Random;
-
- import javax.crypto.Cipher;
- import javax.crypto.CipherOutputStream;
- import javax.crypto.SecretKey;
-
- import org.apache.logging.log4j.LogManager;
- import org.apache.logging.log4j.Logger;
- import org.apache.poi.EncryptedDocumentException;
- import org.apache.poi.poifs.crypt.CryptoFunctions;
- import org.apache.poi.poifs.crypt.DataSpaceMapUtils;
- import org.apache.poi.poifs.crypt.EncryptionInfo;
- import org.apache.poi.poifs.crypt.EncryptionVerifier;
- import org.apache.poi.poifs.crypt.Encryptor;
- import org.apache.poi.poifs.filesystem.DirectoryNode;
- import org.apache.poi.poifs.filesystem.POIFSWriterEvent;
- import org.apache.poi.poifs.filesystem.POIFSWriterListener;
- import org.apache.poi.util.IOUtils;
- import org.apache.poi.util.LittleEndianByteArrayOutputStream;
- import org.apache.poi.util.LittleEndianConsts;
- import org.apache.poi.util.LittleEndianOutputStream;
- import org.apache.poi.util.TempFile;
-
- public class StandardEncryptor extends Encryptor {
- private static final Logger LOG = LogManager.getLogger(StandardEncryptor.class);
-
- protected StandardEncryptor() {}
-
- protected StandardEncryptor(StandardEncryptor other) {
- super(other);
- }
-
- @Override
- public void confirmPassword(String password) {
- // see [MS-OFFCRYPTO] - 2.3.3 EncryptionVerifier
- Random r = new SecureRandom();
- byte[] salt = new byte[16], verifier = new byte[16];
- r.nextBytes(salt);
- r.nextBytes(verifier);
-
- confirmPassword(password, null, null, salt, verifier, null);
- }
-
-
- /**
- * Fills the fields of verifier and header with the calculated hashes based
- * on the password and a random salt
- *
- * see [MS-OFFCRYPTO] - 2.3.4.7 ECMA-376 Document Encryption Key Generation
- */
- @Override
- public void confirmPassword(String password, byte[] keySpec, byte[] keySalt, byte[] verifier, byte[] verifierSalt, byte[] integritySalt) {
- StandardEncryptionVerifier ver = (StandardEncryptionVerifier)getEncryptionInfo().getVerifier();
-
- ver.setSalt(verifierSalt);
- SecretKey secretKey = generateSecretKey(password, ver, getKeySizeInBytes());
- setSecretKey(secretKey);
- Cipher cipher = getCipher(secretKey, null);
-
- try {
- byte[] encryptedVerifier = cipher.doFinal(verifier);
- MessageDigest hashAlgo = CryptoFunctions.getMessageDigest(ver.getHashAlgorithm());
- byte[] calcVerifierHash = hashAlgo.digest(verifier);
-
- // 2.3.3 EncryptionVerifier ...
- // An array of bytes that contains the encrypted form of the
- // hash of the randomly generated Verifier value. The length of the array MUST be the size of
- // the encryption block size multiplied by the number of blocks needed to encrypt the hash of the
- // Verifier. If the encryption algorithm is RC4, the length MUST be 20 bytes. If the encryption
- // algorithm is AES, the length MUST be 32 bytes. After decrypting the EncryptedVerifierHash
- // field, only the first VerifierHashSize bytes MUST be used.
- int encVerHashSize = ver.getCipherAlgorithm().encryptedVerifierHashLength;
- byte[] encryptedVerifierHash = cipher.doFinal(Arrays.copyOf(calcVerifierHash, encVerHashSize));
-
- ver.setEncryptedVerifier(encryptedVerifier);
- ver.setEncryptedVerifierHash(encryptedVerifierHash);
- } catch (GeneralSecurityException e) {
- throw new EncryptedDocumentException("Password confirmation failed", e);
- }
-
- }
-
- private Cipher getCipher(SecretKey key, String padding) {
- EncryptionVerifier ver = getEncryptionInfo().getVerifier();
- return CryptoFunctions.getCipher(key, ver.getCipherAlgorithm(), ver.getChainingMode(), null, Cipher.ENCRYPT_MODE, padding);
- }
-
- @Override
- public OutputStream getDataStream(final DirectoryNode dir)
- throws IOException, GeneralSecurityException {
- createEncryptionInfoEntry(dir);
- DataSpaceMapUtils.addDefaultDataSpace(dir);
- return new StandardCipherOutputStream(dir);
- }
-
- protected class StandardCipherOutputStream extends FilterOutputStream implements POIFSWriterListener {
- protected long countBytes;
- protected final File fileOut;
- protected final DirectoryNode dir;
-
- @SuppressWarnings({"resource", "squid:S2095"})
- private StandardCipherOutputStream(DirectoryNode dir, File fileOut) throws IOException {
- // although not documented, we need the same padding as with agile encryption
- // and instead of calculating the missing bytes for the block size ourselves
- // we leave it up to the CipherOutputStream, which generates/saves them on close()
- // ... we can't use "NoPadding" here
- //
- // see also [MS-OFFCRYPT] - 2.3.4.15
- // The final data block MUST be padded to the next integral multiple of the
- // KeyData.blockSize value. Any padding bytes can be used. Note that the StreamSize
- // field of the EncryptedPackage field specifies the number of bytes of
- // unencrypted data as specified in section 2.3.4.4.
- super(
- new CipherOutputStream(new FileOutputStream(fileOut), getCipher(getSecretKey(), "PKCS5Padding"))
- );
- this.fileOut = fileOut;
- this.dir = dir;
- }
-
- protected StandardCipherOutputStream(DirectoryNode dir) throws IOException {
- this(dir, TempFile.createTempFile("encrypted_package", "crypt"));
- }
-
- @Override
- public void write(byte[] b, int off, int len) throws IOException {
- out.write(b, off, len);
- countBytes += len;
- }
-
- @Override
- public void write(int b) throws IOException {
- out.write(b);
- countBytes++;
- }
-
- @Override
- public void close() throws IOException {
- // the CipherOutputStream adds the padding bytes on close()
- super.close();
- writeToPOIFS();
- }
-
- void writeToPOIFS() throws IOException {
- int oleStreamSize = (int)(fileOut.length()+LittleEndianConsts.LONG_SIZE);
- dir.createDocument(DEFAULT_POIFS_ENTRY, oleStreamSize, this);
- // TODO: any properties???
- }
-
- @Override
- public void processPOIFSWriterEvent(POIFSWriterEvent event) {
- try {
- LittleEndianOutputStream leos = new LittleEndianOutputStream(event.getStream());
-
- // StreamSize (8 bytes): An unsigned integer that specifies the number of bytes used by data
- // encrypted within the EncryptedData field, not including the size of the StreamSize field.
- // Note that the actual size of the \EncryptedPackage stream (1) can be larger than this
- // value, depending on the block size of the chosen encryption algorithm
- leos.writeLong(countBytes);
-
- try (FileInputStream fis = new FileInputStream(fileOut)) {
- IOUtils.copy(fis, leos);
- }
- if (!fileOut.delete()) {
- LOG.atError().log("Can't delete temporary encryption file: {}", fileOut);
- }
-
- leos.close();
- } catch (IOException e) {
- throw new EncryptedDocumentException(e);
- }
- }
- }
-
- protected int getKeySizeInBytes() {
- return getEncryptionInfo().getHeader().getKeySize()/8;
- }
-
- protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException {
- final EncryptionInfo info = getEncryptionInfo();
- final StandardEncryptionHeader header = (StandardEncryptionHeader)info.getHeader();
- final StandardEncryptionVerifier verifier = (StandardEncryptionVerifier)info.getVerifier();
-
- EncryptionRecord er = new EncryptionRecord(){
- @Override
- public void write(LittleEndianByteArrayOutputStream bos) {
- bos.writeShort(info.getVersionMajor());
- bos.writeShort(info.getVersionMinor());
- bos.writeInt(info.getEncryptionFlags());
- header.write(bos);
- verifier.write(bos);
- }
- };
-
- createEncryptionEntry(dir, "EncryptionInfo", er);
-
- // TODO: any properties???
- }
-
- @Override
- public StandardEncryptor copy() {
- return new StandardEncryptor(this);
- }
- }
|