/* ==================================================================== | |
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.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.POILogFactory; | |
import org.apache.poi.util.POILogger; | |
import org.apache.poi.util.TempFile; | |
public class StandardEncryptor extends Encryptor implements Cloneable { | |
private static final POILogger logger = POILogFactory.getLogger(StandardEncryptor.class); | |
protected StandardEncryptor() { | |
} | |
@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") | |
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); | |
FileInputStream fis = new FileInputStream(fileOut); | |
try { | |
IOUtils.copy(fis, leos); | |
} finally { | |
fis.close(); | |
} | |
if (!fileOut.delete()) { | |
logger.log(POILogger.ERROR, "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 clone() throws CloneNotSupportedException { | |
return (StandardEncryptor)super.clone(); | |
} | |
} |