org.apache.poi.poifs.crypt.standard.StandardEncryptor Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of apache-poi Show documentation
Show all versions of apache-poi Show documentation
The Apache Commons Codec package contains simple encoder and decoders for
various formats such as Base64 and Hexadecimal. In addition to these
widely used encoders and decoders, the codec package also maintains a
collection of phonetic encoding utilities.
/* ====================================================================
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);
}
}