HSSF CryptoAPI decryption support

git-svn-id: https://svn.apache.org/repos/asf/poi/branches/hssf_cryptoapi@1755461 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Andreas Beeker 2016-08-08 00:10:44 +00:00
parent c4ac2e7758
commit 0bfefdfc04
30 changed files with 1051 additions and 1104 deletions

View File

@ -17,8 +17,19 @@
package org.apache.poi.hssf.record; package org.apache.poi.hssf.record;
import java.io.IOException;
import org.apache.poi.EncryptedDocumentException; import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.crypt.EncryptionInfo;
import org.apache.poi.poifs.crypt.EncryptionMode;
import org.apache.poi.poifs.crypt.binaryrc4.BinaryRC4EncryptionHeader;
import org.apache.poi.poifs.crypt.binaryrc4.BinaryRC4EncryptionVerifier;
import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionHeader;
import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionVerifier;
import org.apache.poi.poifs.crypt.xor.XOREncryptionHeader;
import org.apache.poi.poifs.crypt.xor.XOREncryptionVerifier;
import org.apache.poi.util.HexDump; import org.apache.poi.util.HexDump;
import org.apache.poi.util.LittleEndianByteArrayOutputStream;
import org.apache.poi.util.LittleEndianOutput; import org.apache.poi.util.LittleEndianOutput;
/** /**
@ -31,225 +42,79 @@ public final class FilePassRecord extends StandardRecord implements Cloneable {
private static final int ENCRYPTION_XOR = 0; private static final int ENCRYPTION_XOR = 0;
private static final int ENCRYPTION_OTHER = 1; private static final int ENCRYPTION_OTHER = 1;
private int _encryptionType; private int encryptionType;
private KeyData _keyData; private EncryptionInfo encryptionInfo;
private int dataLength;
private static interface KeyData extends Cloneable {
void read(RecordInputStream in);
void serialize(LittleEndianOutput out);
int getDataSize();
void appendToString(StringBuffer buffer);
KeyData clone(); // NOSONAR
}
public static final class Rc4KeyData implements KeyData, Cloneable {
private static final int ENCRYPTION_OTHER_RC4 = 1;
private static final int ENCRYPTION_OTHER_CAPI_2 = 2;
private static final int ENCRYPTION_OTHER_CAPI_3 = 3;
private static final int ENCRYPTION_OTHER_CAPI_4 = 4;
private byte[] _salt;
private byte[] _encryptedVerifier;
private byte[] _encryptedVerifierHash;
private int _encryptionInfo;
private int _minorVersionNo;
public void read(RecordInputStream in) {
_encryptionInfo = in.readUShort();
switch (_encryptionInfo) {
case ENCRYPTION_OTHER_RC4:
// handled below
break;
case ENCRYPTION_OTHER_CAPI_2:
case ENCRYPTION_OTHER_CAPI_3:
case ENCRYPTION_OTHER_CAPI_4:
throw new EncryptedDocumentException(
"HSSF does not currently support CryptoAPI encryption");
default:
throw new RecordFormatException("Unknown encryption info " + _encryptionInfo);
}
_minorVersionNo = in.readUShort();
if (_minorVersionNo!=1) {
throw new RecordFormatException("Unexpected VersionInfo number for RC4Header " + _minorVersionNo);
}
_salt = FilePassRecord.read(in, 16);
_encryptedVerifier = FilePassRecord.read(in, 16);
_encryptedVerifierHash = FilePassRecord.read(in, 16);
}
public void serialize(LittleEndianOutput out) {
out.writeShort(_encryptionInfo);
out.writeShort(_minorVersionNo);
out.write(_salt);
out.write(_encryptedVerifier);
out.write(_encryptedVerifierHash);
}
public int getDataSize() {
return 54;
}
public byte[] getSalt() {
return _salt.clone();
}
public void setSalt(byte[] salt) {
this._salt = salt.clone();
}
public byte[] getEncryptedVerifier() {
return _encryptedVerifier.clone();
}
public void setEncryptedVerifier(byte[] encryptedVerifier) {
this._encryptedVerifier = encryptedVerifier.clone();
}
public byte[] getEncryptedVerifierHash() {
return _encryptedVerifierHash.clone();
}
public void setEncryptedVerifierHash(byte[] encryptedVerifierHash) {
this._encryptedVerifierHash = encryptedVerifierHash.clone();
}
public void appendToString(StringBuffer buffer) {
buffer.append(" .rc4.info = ").append(HexDump.shortToHex(_encryptionInfo)).append("\n");
buffer.append(" .rc4.ver = ").append(HexDump.shortToHex(_minorVersionNo)).append("\n");
buffer.append(" .rc4.salt = ").append(HexDump.toHex(_salt)).append("\n");
buffer.append(" .rc4.verifier = ").append(HexDump.toHex(_encryptedVerifier)).append("\n");
buffer.append(" .rc4.verifierHash = ").append(HexDump.toHex(_encryptedVerifierHash)).append("\n");
}
@Override
public Rc4KeyData clone() {
Rc4KeyData other = new Rc4KeyData();
other._salt = this._salt.clone();
other._encryptedVerifier = this._encryptedVerifier.clone();
other._encryptedVerifierHash = this._encryptedVerifierHash.clone();
other._encryptionInfo = this._encryptionInfo;
other._minorVersionNo = this._minorVersionNo;
return other;
}
}
public static final class XorKeyData implements KeyData, Cloneable {
/**
* key (2 bytes): An unsigned integer that specifies the obfuscation key.
* See [MS-OFFCRYPTO], 2.3.6.2 section, the first step of initializing XOR
* array where it describes the generation of 16-bit XorKey value.
*/
private int _key;
/**
* verificationBytes (2 bytes): An unsigned integer that specifies
* the password verification identifier.
*/
private int _verifier;
public void read(RecordInputStream in) {
_key = in.readUShort();
_verifier = in.readUShort();
}
public void serialize(LittleEndianOutput out) {
out.writeShort(_key);
out.writeShort(_verifier);
}
public int getDataSize() {
// TODO: Check!
return 6;
}
public int getKey() {
return _key;
}
public int getVerifier() {
return _verifier;
}
public void setKey(int key) {
this._key = key;
}
public void setVerifier(int verifier) {
this._verifier = verifier;
}
public void appendToString(StringBuffer buffer) {
buffer.append(" .xor.key = ").append(HexDump.intToHex(_key)).append("\n");
buffer.append(" .xor.verifier = ").append(HexDump.intToHex(_verifier)).append("\n");
}
@Override
public XorKeyData clone() {
XorKeyData other = new XorKeyData();
other._key = this._key;
other._verifier = this._verifier;
return other;
}
}
private FilePassRecord(FilePassRecord other) { private FilePassRecord(FilePassRecord other) {
_encryptionType = other._encryptionType; dataLength = other.dataLength;
_keyData = other._keyData.clone(); encryptionType = other.encryptionType;
try {
encryptionInfo = other.encryptionInfo.clone();
} catch (CloneNotSupportedException e) {
throw new EncryptedDocumentException(e);
}
} }
public FilePassRecord(RecordInputStream in) { public FilePassRecord(RecordInputStream in) {
_encryptionType = in.readUShort(); dataLength = in.remaining();
encryptionType = in.readUShort();
switch (_encryptionType) { EncryptionMode preferredMode;
switch (encryptionType) {
case ENCRYPTION_XOR: case ENCRYPTION_XOR:
_keyData = new XorKeyData(); preferredMode = EncryptionMode.xor;
break; break;
case ENCRYPTION_OTHER: case ENCRYPTION_OTHER:
_keyData = new Rc4KeyData(); preferredMode = EncryptionMode.cryptoAPI;
break; break;
default: default:
throw new RecordFormatException("Unknown encryption type " + _encryptionType); throw new EncryptedDocumentException("invalid encryption type");
} }
_keyData.read(in); try {
encryptionInfo = new EncryptionInfo(in, preferredMode);
} catch (IOException e) {
throw new EncryptedDocumentException(e);
} }
private static byte[] read(RecordInputStream in, int size) {
byte[] result = new byte[size];
in.readFully(result);
return result;
} }
public void serialize(LittleEndianOutput out) { public void serialize(LittleEndianOutput out) {
out.writeShort(_encryptionType); out.writeShort(encryptionType);
assert(_keyData != null);
_keyData.serialize(out); byte data[] = new byte[1024];
LittleEndianByteArrayOutputStream bos = new LittleEndianByteArrayOutputStream(data, 0);
switch (encryptionInfo.getEncryptionMode()) {
case xor:
((XOREncryptionHeader)encryptionInfo.getHeader()).write(bos);
((XOREncryptionVerifier)encryptionInfo.getVerifier()).write(bos);
break;
case binaryRC4:
out.writeShort(encryptionInfo.getVersionMajor());
out.writeShort(encryptionInfo.getVersionMinor());
((BinaryRC4EncryptionHeader)encryptionInfo.getHeader()).write(bos);
((BinaryRC4EncryptionVerifier)encryptionInfo.getVerifier()).write(bos);
break;
case cryptoAPI:
out.writeShort(encryptionInfo.getVersionMajor());
out.writeShort(encryptionInfo.getVersionMinor());
((CryptoAPIEncryptionHeader)encryptionInfo.getHeader()).write(bos);
((CryptoAPIEncryptionVerifier)encryptionInfo.getVerifier()).write(bos);
break;
default:
throw new RuntimeException("not supported");
}
out.write(data, 0, bos.getWriteIndex());
} }
protected int getDataSize() { protected int getDataSize() {
assert(_keyData != null); return dataLength;
return _keyData.getDataSize();
} }
public Rc4KeyData getRc4KeyData() { public EncryptionInfo getEncryptionInfo() {
return (_keyData instanceof Rc4KeyData) return encryptionInfo;
? (Rc4KeyData) _keyData
: null;
}
public XorKeyData getXorKeyData() {
return (_keyData instanceof XorKeyData)
? (XorKeyData) _keyData
: null;
}
private Rc4KeyData checkRc4() {
Rc4KeyData rc4 = getRc4KeyData();
if (rc4 == null) {
throw new RecordFormatException("file pass record doesn't contain a rc4 key.");
}
return rc4;
} }
public short getSid() { public short getSid() {
@ -265,8 +130,13 @@ public final class FilePassRecord extends StandardRecord implements Cloneable {
StringBuffer buffer = new StringBuffer(); StringBuffer buffer = new StringBuffer();
buffer.append("[FILEPASS]\n"); buffer.append("[FILEPASS]\n");
buffer.append(" .type = ").append(HexDump.shortToHex(_encryptionType)).append("\n"); buffer.append(" .type = ").append(HexDump.shortToHex(encryptionType)).append("\n");
_keyData.appendToString(buffer); String prefix = " ."+encryptionInfo.getEncryptionMode();
buffer.append(prefix+".info = ").append(HexDump.shortToHex(encryptionInfo.getVersionMajor())).append("\n");
buffer.append(prefix+".ver = ").append(HexDump.shortToHex(encryptionInfo.getVersionMinor())).append("\n");
buffer.append(prefix+".salt = ").append(HexDump.toHex(encryptionInfo.getVerifier().getSalt())).append("\n");
buffer.append(prefix+".verifier = ").append(HexDump.toHex(encryptionInfo.getVerifier().getEncryptedVerifier())).append("\n");
buffer.append(prefix+".verifierHash = ").append(HexDump.toHex(encryptionInfo.getVerifier().getEncryptedVerifierHash())).append("\n");
buffer.append("[/FILEPASS]\n"); buffer.append("[/FILEPASS]\n");
return buffer.toString(); return buffer.toString();
} }

View File

@ -17,18 +17,16 @@
package org.apache.poi.hssf.record; package org.apache.poi.hssf.record;
import java.io.InputStream; import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.apache.poi.EncryptedDocumentException; import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.hssf.eventusermodel.HSSFEventFactory; import org.apache.poi.hssf.eventusermodel.HSSFEventFactory;
import org.apache.poi.hssf.eventusermodel.HSSFListener; import org.apache.poi.hssf.eventusermodel.HSSFListener;
import org.apache.poi.hssf.record.FilePassRecord.Rc4KeyData;
import org.apache.poi.hssf.record.FilePassRecord.XorKeyData;
import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
import org.apache.poi.hssf.record.crypto.Biff8RC4Key;
import org.apache.poi.hssf.record.crypto.Biff8XORKey;
import org.apache.poi.poifs.crypt.Decryptor; import org.apache.poi.poifs.crypt.Decryptor;
import org.apache.poi.poifs.crypt.EncryptionInfo;
/** /**
* A stream based way to get at complete records, with * A stream based way to get at complete records, with
@ -114,31 +112,18 @@ public final class RecordFactoryInputStream {
userPassword = Decryptor.DEFAULT_PASSWORD; userPassword = Decryptor.DEFAULT_PASSWORD;
} }
Biff8EncryptionKey key; EncryptionInfo info = fpr.getEncryptionInfo();
if (fpr.getRc4KeyData() != null) { try {
Rc4KeyData rc4 = fpr.getRc4KeyData(); if (!info.getDecryptor().verifyPassword(userPassword)) {
Biff8RC4Key rc4key = Biff8RC4Key.create(userPassword, rc4.getSalt());
key = rc4key;
if (!rc4key.validate(rc4.getEncryptedVerifier(), rc4.getEncryptedVerifierHash())) {
throw new EncryptedDocumentException( throw new EncryptedDocumentException(
(Decryptor.DEFAULT_PASSWORD.equals(userPassword) ? "Default" : "Supplied") (Decryptor.DEFAULT_PASSWORD.equals(userPassword) ? "Default" : "Supplied")
+ " password is invalid for salt/verifier/verifierHash"); + " password is invalid for salt/verifier/verifierHash");
} }
} else if (fpr.getXorKeyData() != null) { } catch (GeneralSecurityException e) {
XorKeyData xor = fpr.getXorKeyData(); throw new EncryptedDocumentException(e);
Biff8XORKey xorKey = Biff8XORKey.create(userPassword, xor.getKey());
key = xorKey;
if (!xorKey.validate(userPassword, xor.getVerifier())) {
throw new EncryptedDocumentException(
(Decryptor.DEFAULT_PASSWORD.equals(userPassword) ? "Default" : "Supplied")
+ " password is invalid for key/verifier");
}
} else {
throw new EncryptedDocumentException("Crypto API not yet supported.");
} }
return new RecordInputStream(original, key, _initialRecordsSize); return new RecordInputStream(original, info, _initialRecordsSize);
} }
public boolean hasEncryption() { public boolean hasEncryption() {

View File

@ -25,6 +25,7 @@ import java.util.Locale;
import org.apache.poi.hssf.dev.BiffViewer; import org.apache.poi.hssf.dev.BiffViewer;
import org.apache.poi.hssf.record.crypto.Biff8DecryptingStream; import org.apache.poi.hssf.record.crypto.Biff8DecryptingStream;
import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
import org.apache.poi.poifs.crypt.EncryptionInfo;
import org.apache.poi.util.Internal; import org.apache.poi.util.Internal;
import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianConsts;
import org.apache.poi.util.LittleEndianInput; import org.apache.poi.util.LittleEndianInput;
@ -33,8 +34,6 @@ import org.apache.poi.util.LittleEndianInputStream;
/** /**
* Title: Record Input Stream<P> * Title: Record Input Stream<P>
* Description: Wraps a stream and provides helper methods for the construction of records.<P> * Description: Wraps a stream and provides helper methods for the construction of records.<P>
*
* @author Jason Height (jheight @ apache dot org)
*/ */
public final class RecordInputStream implements LittleEndianInput { public final class RecordInputStream implements LittleEndianInput {
/** Maximum size of a single record (minus the 4 byte header) without a continue*/ /** Maximum size of a single record (minus the 4 byte header) without a continue*/
@ -122,7 +121,7 @@ public final class RecordInputStream implements LittleEndianInput {
this (in, null, 0); this (in, null, 0);
} }
public RecordInputStream(InputStream in, Biff8EncryptionKey key, int initialOffset) throws RecordFormatException { public RecordInputStream(InputStream in, EncryptionInfo key, int initialOffset) throws RecordFormatException {
if (key == null) { if (key == null) {
_dataInput = getLEI(in); _dataInput = getLEI(in);
_bhi = new SimpleHeaderInput(in); _bhi = new SimpleHeaderInput(in);

View File

@ -17,103 +17,202 @@
package org.apache.poi.hssf.record.crypto; package org.apache.poi.hssf.record.crypto;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.PushbackInputStream;
import org.apache.poi.EncryptedDocumentException; import org.apache.poi.hssf.record.BOFRecord;
import org.apache.poi.hssf.record.BiffHeaderInput; import org.apache.poi.hssf.record.BiffHeaderInput;
import org.apache.poi.hssf.record.FilePassRecord;
import org.apache.poi.hssf.record.InterfaceHdrRecord;
import org.apache.poi.hssf.record.RecordFormatException;
import org.apache.poi.poifs.crypt.ChunkedCipherInputStream;
import org.apache.poi.poifs.crypt.Decryptor;
import org.apache.poi.poifs.crypt.EncryptionInfo;
import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.LittleEndianConsts;
import org.apache.poi.util.LittleEndianInput; import org.apache.poi.util.LittleEndianInput;
import org.apache.poi.util.LittleEndianInputStream;
/**
*
* @author Josh Micich
*/
public final class Biff8DecryptingStream implements BiffHeaderInput, LittleEndianInput { public final class Biff8DecryptingStream implements BiffHeaderInput, LittleEndianInput {
private final LittleEndianInput _le; private static final int RC4_REKEYING_INTERVAL = 1024;
private final Biff8Cipher _cipher;
public Biff8DecryptingStream(InputStream in, int initialOffset, Biff8EncryptionKey key) { private final EncryptionInfo info;
if (key instanceof Biff8RC4Key) { private ChunkedCipherInputStream ccis;
_cipher = new Biff8RC4(initialOffset, (Biff8RC4Key)key); private final byte buffer[] = new byte[LittleEndianConsts.LONG_SIZE];
} else if (key instanceof Biff8XORKey) { private boolean shouldSkipEncryptionOnCurrentRecord = false;
_cipher = new Biff8XOR(initialOffset, (Biff8XORKey)key);
public Biff8DecryptingStream(InputStream in, int initialOffset, EncryptionInfo info) throws RecordFormatException {
try {
byte initialBuf[] = new byte[initialOffset];
InputStream stream;
if (initialOffset == 0) {
stream = in;
} else { } else {
throw new EncryptedDocumentException("Crypto API not supported yet."); stream = new PushbackInputStream(in, initialOffset);
((PushbackInputStream)stream).unread(initialBuf);
} }
if (in instanceof LittleEndianInput) { this.info = info;
// accessing directly is an optimisation Decryptor dec = this.info.getDecryptor();
_le = (LittleEndianInput) in; dec.setChunkSize(RC4_REKEYING_INTERVAL);
} else { ccis = (ChunkedCipherInputStream)dec.getDataStream(stream, Integer.MAX_VALUE, 0);
// less optimal, but should work OK just the same. Often occurs in junit tests.
_le = new LittleEndianInputStream(in); if (initialOffset > 0) {
ccis.readFully(initialBuf);
}
} catch (Exception e) {
throw new RecordFormatException(e);
} }
} }
@Override
public int available() { public int available() {
return _le.available(); return ccis.available();
} }
/** /**
* Reads an unsigned short value without decrypting * Reads an unsigned short value without decrypting
*/ */
@Override
public int readRecordSID() { public int readRecordSID() {
int sid = _le.readUShort(); readPlain(buffer, 0, LittleEndianConsts.SHORT_SIZE);
_cipher.skipTwoBytes(); int sid = LittleEndian.getUShort(buffer, 0);
_cipher.startRecord(sid); shouldSkipEncryptionOnCurrentRecord = isNeverEncryptedRecord(sid);
return sid; return sid;
} }
/** /**
* Reads an unsigned short value without decrypting * Reads an unsigned short value without decrypting
*/ */
@Override
public int readDataSize() { public int readDataSize() {
int dataSize = _le.readUShort(); readPlain(buffer, 0, LittleEndianConsts.SHORT_SIZE);
_cipher.skipTwoBytes(); int dataSize = LittleEndian.getUShort(buffer, 0);
_cipher.setNextRecordSize(dataSize); ccis.setNextRecordSize(dataSize);
return dataSize; return dataSize;
} }
@Override
public double readDouble() { public double readDouble() {
long valueLongBits = readLong(); long valueLongBits = readLong();
double result = Double.longBitsToDouble(valueLongBits); double result = Double.longBitsToDouble(valueLongBits);
if (Double.isNaN(result)) { if (Double.isNaN(result)) {
throw new RuntimeException("Did not expect to read NaN"); // (Because Excel typically doesn't write NaN // (Because Excel typically doesn't write NaN
throw new RuntimeException("Did not expect to read NaN");
} }
return result; return result;
} }
@Override
public void readFully(byte[] buf) { public void readFully(byte[] buf) {
readFully(buf, 0, buf.length); readFully(buf, 0, buf.length);
} }
@Override
public void readFully(byte[] buf, int off, int len) { public void readFully(byte[] buf, int off, int len) {
_le.readFully(buf, off, len); if (shouldSkipEncryptionOnCurrentRecord) {
_cipher.xor(buf, off, len); readPlain(buf, off, buf.length);
} else {
ccis.readFully(buf, off, len);
}
} }
@Override
public int readUByte() { public int readUByte() {
return readByte() & 0xFF; return readByte() & 0xFF;
} }
@Override
public byte readByte() { public byte readByte() {
return (byte) _cipher.xorByte(_le.readUByte()); if (shouldSkipEncryptionOnCurrentRecord) {
readPlain(buffer, 0, LittleEndianConsts.BYTE_SIZE);
return buffer[0];
} else {
return ccis.readByte();
}
} }
@Override
public int readUShort() { public int readUShort() {
return readShort() & 0xFFFF; return readShort() & 0xFFFF;
} }
@Override
public short readShort() { public short readShort() {
return (short) _cipher.xorShort(_le.readUShort()); if (shouldSkipEncryptionOnCurrentRecord) {
readPlain(buffer, 0, LittleEndianConsts.SHORT_SIZE);
return LittleEndian.getShort(buffer);
} else {
return ccis.readShort();
}
} }
@Override
public int readInt() { public int readInt() {
return _cipher.xorInt(_le.readInt()); if (shouldSkipEncryptionOnCurrentRecord) {
readPlain(buffer, 0, LittleEndianConsts.INT_SIZE);
return LittleEndian.getInt(buffer);
} else {
return ccis.readInt();
}
} }
@Override
public long readLong() { public long readLong() {
return _cipher.xorLong(_le.readLong()); if (shouldSkipEncryptionOnCurrentRecord) {
readPlain(buffer, 0, LittleEndianConsts.LONG_SIZE);
return LittleEndian.getLong(buffer);
} else {
return ccis.readLong();
} }
} }
/**
* @return the absolute position in the stream
*/
public long getPosition() {
return ccis.getPos();
}
/**
* TODO: Additionally, the lbPlyPos (position_of_BOF) field of the BoundSheet8 record MUST NOT be encrypted.
*
* @return <code>true</code> if record type specified by <tt>sid</tt> is never encrypted
*/
private static boolean isNeverEncryptedRecord(int sid) {
switch (sid) {
case BOFRecord.sid:
// sheet BOFs for sure
// TODO - find out about chart BOFs
case InterfaceHdrRecord.sid:
// don't know why this record doesn't seem to get encrypted
case FilePassRecord.sid:
// this only really counts when writing because FILEPASS is read early
// UsrExcl(0x0194)
// FileLock
// RRDInfo(0x0196)
// RRDHead(0x0138)
return true;
default:
return false;
}
}
private void readPlain(byte b[], int off, int len) {
try {
int readBytes = ccis.readPlain(b, off, len);
if (readBytes < len) {
throw new RecordFormatException("buffer underrun");
}
} catch (IOException e) {
throw new RecordFormatException(e);
}
}
}

View File

@ -16,34 +16,9 @@
==================================================================== */ ==================================================================== */
package org.apache.poi.hssf.record.crypto; package org.apache.poi.hssf.record.crypto;
import javax.crypto.SecretKey;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.poifs.crypt.Decryptor;
public abstract class Biff8EncryptionKey {
protected SecretKey _secretKey;
/**
* Create using the default password and a specified docId
* @param salt 16 bytes
*/
public static Biff8EncryptionKey create(byte[] salt) {
return Biff8RC4Key.create(Decryptor.DEFAULT_PASSWORD, salt);
}
public static Biff8EncryptionKey create(String password, byte[] salt) {
return Biff8RC4Key.create(password, salt);
}
/**
* @return <code>true</code> if the keyDigest is compatible with the specified saltData and saltHash
*/
public boolean validate(byte[] saltData, byte[] saltHash) {
throw new EncryptedDocumentException("validate is not supported (in super-class).");
}
public final class Biff8EncryptionKey {
/** /**
* Stores the BIFF8 encryption/decryption password for the current thread. This has been done * Stores the BIFF8 encryption/decryption password for the current thread. This has been done
* using a {@link ThreadLocal} in order to avoid further overloading the various public APIs * using a {@link ThreadLocal} in order to avoid further overloading the various public APIs

View File

@ -1,195 +0,0 @@
/* ====================================================================
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.hssf.record.crypto;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import javax.crypto.Cipher;
import javax.crypto.ShortBufferException;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.hssf.record.BOFRecord;
import org.apache.poi.hssf.record.FilePassRecord;
import org.apache.poi.hssf.record.InterfaceHdrRecord;
/**
* Used for both encrypting and decrypting BIFF8 streams. The internal
* {@link Cipher} instance is renewed (re-keyed) every 1024 bytes.
*/
final class Biff8RC4 implements Biff8Cipher {
private static final int RC4_REKEYING_INTERVAL = 1024;
private Cipher _rc4;
/**
* This field is used to keep track of when to change the {@link Cipher}
* instance. The change occurs every 1024 bytes. Every byte passed over is
* counted.
*/
private int _streamPos;
private int _nextRC4BlockStart;
private int _currentKeyIndex;
private boolean _shouldSkipEncryptionOnCurrentRecord;
private final Biff8RC4Key _key;
private ByteBuffer _buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
public Biff8RC4(int initialOffset, Biff8RC4Key key) {
if (initialOffset >= RC4_REKEYING_INTERVAL) {
throw new RuntimeException("initialOffset (" + initialOffset + ")>"
+ RC4_REKEYING_INTERVAL + " not supported yet");
}
_key = key;
_rc4 = _key.getCipher();
_streamPos = 0;
rekeyForNextBlock();
_streamPos = initialOffset;
_shouldSkipEncryptionOnCurrentRecord = false;
encryptBytes(new byte[initialOffset], 0, initialOffset);
}
private void rekeyForNextBlock() {
_currentKeyIndex = _streamPos / RC4_REKEYING_INTERVAL;
_key.initCipherForBlock(_rc4, _currentKeyIndex);
_nextRC4BlockStart = (_currentKeyIndex + 1) * RC4_REKEYING_INTERVAL;
}
private void encryptBytes(byte data[], int offset, final int bytesToRead) {
if (bytesToRead == 0) return;
if (_shouldSkipEncryptionOnCurrentRecord) {
// even when encryption is skipped, we need to update the cipher
byte dataCpy[] = new byte[bytesToRead];
System.arraycopy(data, offset, dataCpy, 0, bytesToRead);
data = dataCpy;
offset = 0;
}
try {
_rc4.update(data, offset, bytesToRead, data, offset);
} catch (ShortBufferException e) {
throw new EncryptedDocumentException("input buffer too small", e);
}
}
public void startRecord(int currentSid) {
_shouldSkipEncryptionOnCurrentRecord = isNeverEncryptedRecord(currentSid);
}
/**
* TODO: Additionally, the lbPlyPos (position_of_BOF) field of the BoundSheet8 record MUST NOT be encrypted.
*
* @return <code>true</code> if record type specified by <tt>sid</tt> is never encrypted
*/
private static boolean isNeverEncryptedRecord(int sid) {
switch (sid) {
case BOFRecord.sid:
// sheet BOFs for sure
// TODO - find out about chart BOFs
case InterfaceHdrRecord.sid:
// don't know why this record doesn't seem to get encrypted
case FilePassRecord.sid:
// this only really counts when writing because FILEPASS is read early
// UsrExcl(0x0194)
// FileLock
// RRDInfo(0x0196)
// RRDHead(0x0138)
return true;
}
return false;
}
/**
* Used when BIFF header fields (sid, size) are being read. The internal
* {@link Cipher} instance must step even when unencrypted bytes are read
*/
public void skipTwoBytes() {
xor(_buffer.array(), 0, 2);
}
public void xor(byte[] buf, int pOffset, int pLen) {
int nLeftInBlock;
nLeftInBlock = _nextRC4BlockStart - _streamPos;
if (pLen <= nLeftInBlock) {
// simple case - this read does not cross key blocks
encryptBytes(buf, pOffset, pLen);
_streamPos += pLen;
return;
}
int offset = pOffset;
int len = pLen;
// start by using the rest of the current block
if (len > nLeftInBlock) {
if (nLeftInBlock > 0) {
encryptBytes(buf, offset, nLeftInBlock);
_streamPos += nLeftInBlock;
offset += nLeftInBlock;
len -= nLeftInBlock;
}
rekeyForNextBlock();
}
// all full blocks following
while (len > RC4_REKEYING_INTERVAL) {
encryptBytes(buf, offset, RC4_REKEYING_INTERVAL);
_streamPos += RC4_REKEYING_INTERVAL;
offset += RC4_REKEYING_INTERVAL;
len -= RC4_REKEYING_INTERVAL;
rekeyForNextBlock();
}
// finish with incomplete block
encryptBytes(buf, offset, len);
_streamPos += len;
}
public int xorByte(int rawVal) {
_buffer.put(0, (byte)rawVal);
xor(_buffer.array(), 0, 1);
return _buffer.get(0);
}
public int xorShort(int rawVal) {
_buffer.putShort(0, (short)rawVal);
xor(_buffer.array(), 0, 2);
return _buffer.getShort(0);
}
public int xorInt(int rawVal) {
_buffer.putInt(0, rawVal);
xor(_buffer.array(), 0, 4);
return _buffer.getInt(0);
}
public long xorLong(long rawVal) {
_buffer.putLong(0, rawVal);
xor(_buffer.array(), 0, 8);
return _buffer.getLong(0);
}
public void setNextRecordSize(int recordSize) {
/* no-op */
}
}

View File

@ -1,155 +0,0 @@
/* ====================================================================
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.hssf.record.crypto;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.SecretKeySpec;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.crypt.CipherAlgorithm;
import org.apache.poi.poifs.crypt.CryptoFunctions;
import org.apache.poi.poifs.crypt.HashAlgorithm;
import org.apache.poi.util.HexDump;
import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.LittleEndianConsts;
import org.apache.poi.util.POILogFactory;
import org.apache.poi.util.POILogger;
public class Biff8RC4Key extends Biff8EncryptionKey {
// these two constants coincidentally have the same value
public static final int KEY_DIGEST_LENGTH = 5;
private static final int PASSWORD_HASH_NUMBER_OF_BYTES_USED = 5;
private static POILogger log = POILogFactory.getLogger(Biff8RC4Key.class);
Biff8RC4Key(byte[] keyDigest) {
if (keyDigest.length != KEY_DIGEST_LENGTH) {
throw new IllegalArgumentException("Expected 5 byte key digest, but got " + HexDump.toHex(keyDigest));
}
CipherAlgorithm ca = CipherAlgorithm.rc4;
_secretKey = new SecretKeySpec(keyDigest.clone(), ca.jceId);
}
/**
* Create using the default password and a specified docId
* @param salt 16 bytes
*/
public static Biff8RC4Key create(String password, byte[] salt) {
return new Biff8RC4Key(createKeyDigest(password, salt));
}
/**
* @return <code>true</code> if the keyDigest is compatible with the specified saltData and saltHash
*/
public boolean validate(byte[] verifier, byte[] verifierHash) {
check16Bytes(verifier, "verifier");
check16Bytes(verifierHash, "verifierHash");
// validation uses the RC4 for block zero
Cipher rc4 = getCipher();
initCipherForBlock(rc4, 0);
byte[] verifierPrime = verifier.clone();
byte[] verifierHashPrime = verifierHash.clone();
try {
rc4.update(verifierPrime, 0, verifierPrime.length, verifierPrime);
rc4.update(verifierHashPrime, 0, verifierHashPrime.length, verifierHashPrime);
} catch (ShortBufferException e) {
throw new EncryptedDocumentException("buffer too short", e);
}
MessageDigest md5 = CryptoFunctions.getMessageDigest(HashAlgorithm.md5);
md5.update(verifierPrime);
byte[] finalVerifierResult = md5.digest();
if (log.check(POILogger.DEBUG)) {
byte[] verifierHashThatWouldWork = xor(verifierHash, xor(verifierHashPrime, finalVerifierResult));
log.log(POILogger.DEBUG, "valid verifierHash value", HexDump.toHex(verifierHashThatWouldWork));
}
return Arrays.equals(verifierHashPrime, finalVerifierResult);
}
Cipher getCipher() {
CipherAlgorithm ca = CipherAlgorithm.rc4;
Cipher rc4 = CryptoFunctions.getCipher(_secretKey, ca, null, null, Cipher.ENCRYPT_MODE);
return rc4;
}
static byte[] createKeyDigest(String password, byte[] docIdData) {
check16Bytes(docIdData, "docId");
int nChars = Math.min(password.length(), 16);
byte[] passwordData = new byte[nChars*2];
for (int i=0; i<nChars; i++) {
char ch = password.charAt(i);
passwordData[i*2+0] = (byte) ((ch << 0) & 0xFF);
passwordData[i*2+1] = (byte) ((ch << 8) & 0xFF);
}
MessageDigest md5 = CryptoFunctions.getMessageDigest(HashAlgorithm.md5);
md5.update(passwordData);
byte[] passwordHash = md5.digest();
md5.reset();
for (int i=0; i<16; i++) {
md5.update(passwordHash, 0, PASSWORD_HASH_NUMBER_OF_BYTES_USED);
md5.update(docIdData, 0, docIdData.length);
}
byte[] result = CryptoFunctions.getBlock0(md5.digest(), KEY_DIGEST_LENGTH);
return result;
}
void initCipherForBlock(Cipher rc4, int keyBlockNo) {
byte buf[] = new byte[LittleEndianConsts.INT_SIZE];
LittleEndian.putInt(buf, 0, keyBlockNo);
MessageDigest md5 = CryptoFunctions.getMessageDigest(HashAlgorithm.md5);
md5.update(_secretKey.getEncoded());
md5.update(buf);
SecretKeySpec skeySpec = new SecretKeySpec(md5.digest(), _secretKey.getAlgorithm());
try {
rc4.init(Cipher.ENCRYPT_MODE, skeySpec);
} catch (GeneralSecurityException e) {
throw new EncryptedDocumentException("Can't rekey for next block", e);
}
}
private static byte[] xor(byte[] a, byte[] b) {
byte[] c = new byte[a.length];
for (int i = 0; i < c.length; i++) {
c[i] = (byte) (a[i] ^ b[i]);
}
return c;
}
private static void check16Bytes(byte[] data, String argName) {
if (data.length != 16) {
throw new IllegalArgumentException("Expected 16 byte " + argName + ", but got " + HexDump.toHex(data));
}
}
}

View File

@ -1,153 +0,0 @@
/* ====================================================================
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.hssf.record.crypto;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import javax.crypto.Cipher;
import org.apache.poi.hssf.record.BOFRecord;
import org.apache.poi.hssf.record.FilePassRecord;
import org.apache.poi.hssf.record.InterfaceHdrRecord;
public class Biff8XOR implements Biff8Cipher {
private final Biff8XORKey _key;
private ByteBuffer _buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
private boolean _shouldSkipEncryptionOnCurrentRecord;
private final int _initialOffset;
private int _dataLength = 0;
private int _xorArrayIndex = 0;
public Biff8XOR(int initialOffset, Biff8XORKey key) {
_key = key;
_initialOffset = initialOffset;
}
public void startRecord(int currentSid) {
_shouldSkipEncryptionOnCurrentRecord = isNeverEncryptedRecord(currentSid);
}
public void setNextRecordSize(int recordSize) {
/*
* From: http://social.msdn.microsoft.com/Forums/en-US/3dadbed3-0e68-4f11-8b43-3a2328d9ebd5
*
* The initial value for XorArrayIndex is as follows:
* XorArrayIndex = (FileOffset + Data.Length) % 16
*
* The FileOffset variable in this context is the stream offset into the Workbook stream at
* the time we are about to write each of the bytes of the record data.
* This (the value) is then incremented after each byte is written.
*/
_xorArrayIndex = (_initialOffset+_dataLength+recordSize) % 16;
}
/**
* TODO: Additionally, the lbPlyPos (position_of_BOF) field of the BoundSheet8 record MUST NOT be encrypted.
*
* @return <code>true</code> if record type specified by <tt>sid</tt> is never encrypted
*/
private static boolean isNeverEncryptedRecord(int sid) {
switch (sid) {
case BOFRecord.sid:
// sheet BOFs for sure
// TODO - find out about chart BOFs
case InterfaceHdrRecord.sid:
// don't know why this record doesn't seem to get encrypted
case FilePassRecord.sid:
// this only really counts when writing because FILEPASS is read early
// UsrExcl(0x0194)
// FileLock
// RRDInfo(0x0196)
// RRDHead(0x0138)
return true;
}
return false;
}
/**
* Used when BIFF header fields (sid, size) are being read. The internal
* {@link Cipher} instance must step even when unencrypted bytes are read
*/
public void skipTwoBytes() {
_dataLength += 2;
}
/**
* Decrypts a xor obfuscated byte array.
* The data is decrypted in-place
*
* @see <a href="http://msdn.microsoft.com/en-us/library/dd908506.aspx">2.3.7.3 Binary Document XOR Data Transformation Method 1</a>
*/
public void xor(byte[] buf, int pOffset, int pLen) {
if (_shouldSkipEncryptionOnCurrentRecord) {
_dataLength += pLen;
return;
}
// The following is taken from the Libre Office implementation
// It seems that the encrypt and decrypt method is mixed up
// in the MS-OFFCRYPTO docs
byte xorArray[] = _key._secretKey.getEncoded();
for (int i=0; i<pLen; i++) {
byte value = buf[pOffset+i];
value = rotateLeft(value, 3);
value ^= xorArray[_xorArrayIndex];
buf[pOffset+i] = value;
_xorArrayIndex = (_xorArrayIndex + 1) % 16;
_dataLength++;
}
}
private static byte rotateLeft(byte bits, int shift) {
return (byte)(((bits & 0xff) << shift) | ((bits & 0xff) >>> (8 - shift)));
}
public int xorByte(int rawVal) {
_buffer.put(0, (byte)rawVal);
xor(_buffer.array(), 0, 1);
return _buffer.get(0);
}
public int xorShort(int rawVal) {
_buffer.putShort(0, (short)rawVal);
xor(_buffer.array(), 0, 2);
return _buffer.getShort(0);
}
public int xorInt(int rawVal) {
_buffer.putInt(0, rawVal);
xor(_buffer.array(), 0, 4);
return _buffer.getInt(0);
}
public long xorLong(long rawVal) {
_buffer.putLong(0, rawVal);
xor(_buffer.array(), 0, 8);
return _buffer.getLong(0);
}
}

View File

@ -1,44 +0,0 @@
/* ====================================================================
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.hssf.record.crypto;
import javax.crypto.spec.SecretKeySpec;
import org.apache.poi.poifs.crypt.CryptoFunctions;
public class Biff8XORKey extends Biff8EncryptionKey {
final int _xorKey;
public Biff8XORKey(String password, int xorKey) {
_xorKey = xorKey;
byte xorArray[] = CryptoFunctions.createXorArray1(password);
_secretKey = new SecretKeySpec(xorArray, "XOR");
}
public static Biff8XORKey create(String password, int xorKey) {
return new Biff8XORKey(password, xorKey);
}
public boolean validate(String password, int verifier) {
int keyComp = CryptoFunctions.createXorKey1(password);
int verifierComp = CryptoFunctions.createXorVerifier1(password);
return (_xorKey == keyComp && verifierComp == verifier);
}
}

View File

@ -21,11 +21,13 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.ShortBufferException;
import org.apache.poi.EncryptedDocumentException; import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.util.Internal; import org.apache.poi.util.Internal;
import org.apache.poi.util.LittleEndianInput;
import org.apache.poi.util.LittleEndianInputStream; import org.apache.poi.util.LittleEndianInputStream;
@Internal @Internal
@ -34,29 +36,27 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream {
private final int _chunkBits; private final int _chunkBits;
private final long _size; private final long _size;
private final byte[] _chunk; private final byte[] _chunk, _plain;
private final Cipher _cipher; private final Cipher _cipher;
private int _lastIndex; private int _lastIndex;
private long _pos; private long _pos;
private boolean _chunkIsValid = false; private boolean _chunkIsValid = false;
public ChunkedCipherInputStream(LittleEndianInput stream, long size, int chunkSize) public ChunkedCipherInputStream(InputStream stream, long size, int chunkSize)
throws GeneralSecurityException { throws GeneralSecurityException {
this(stream, size, chunkSize, 0); this(stream, size, chunkSize, 0);
} }
public ChunkedCipherInputStream(LittleEndianInput stream, long size, int chunkSize, int initialPos) public ChunkedCipherInputStream(InputStream stream, long size, int chunkSize, int initialPos)
throws GeneralSecurityException { throws GeneralSecurityException {
super((InputStream)stream); super(stream);
_size = size; _size = size;
_pos = initialPos; _pos = initialPos;
this._chunkSize = chunkSize; this._chunkSize = chunkSize;
if (chunkSize == -1) { int cs = chunkSize == -1 ? 4096 : chunkSize;
_chunk = new byte[4096]; _chunk = new byte[cs];
} else { _plain = new byte[cs];
_chunk = new byte[chunkSize];
}
_chunkBits = Integer.bitCount(_chunk.length-1); _chunkBits = Integer.bitCount(_chunk.length-1);
_lastIndex = (int)(_pos >> _chunkBits); _lastIndex = (int)(_pos >> _chunkBits);
_cipher = initCipherForBlock(null, _lastIndex); _cipher = initCipherForBlock(null, _lastIndex);
@ -88,6 +88,10 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream {
@Override @Override
public int read(byte[] b, int off, int len) throws IOException { public int read(byte[] b, int off, int len) throws IOException {
return read(b, off, len, false);
}
private int read(byte[] b, int off, int len, boolean readPlain) throws IOException {
int total = 0; int total = 0;
if (available() <= 0) { if (available() <= 0) {
@ -110,7 +114,9 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream {
return total; return total;
} }
count = Math.min(avail, Math.min(count, len)); count = Math.min(avail, Math.min(count, len));
System.arraycopy(_chunk, (int)(_pos & chunkMask), b, off, count);
System.arraycopy(readPlain ? _plain : _chunk, (int)(_pos & chunkMask), b, off, count);
off += count; off += count;
len -= count; len -= count;
_pos += count; _pos += count;
@ -164,7 +170,7 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
private int getChunkMask() { protected int getChunkMask() {
return _chunk.length-1; return _chunk.length-1;
} }
@ -183,18 +189,81 @@ public abstract class ChunkedCipherInputStream extends LittleEndianInputStream {
final int todo = (int)Math.min(_size, _chunk.length); final int todo = (int)Math.min(_size, _chunk.length);
int readBytes = 0, totalBytes = 0; int readBytes = 0, totalBytes = 0;
do { do {
readBytes = super.read(_chunk, totalBytes, todo-totalBytes); readBytes = super.read(_plain, totalBytes, todo-totalBytes);
totalBytes += Math.max(0, readBytes); totalBytes += Math.max(0, readBytes);
} while (readBytes != -1 && totalBytes < todo); } while (readBytes != -1 && totalBytes < todo);
if (readBytes == -1 && _pos+totalBytes < _size) { if (readBytes == -1 && _pos+totalBytes < _size && _size < Integer.MAX_VALUE) {
throw new EOFException("buffer underrun"); throw new EOFException("buffer underrun");
} }
if (_chunkSize == -1) { System.arraycopy(_plain, 0, _chunk, 0, totalBytes);
_cipher.update(_chunk, 0, totalBytes, _chunk);
invokeCipher(totalBytes, _chunkSize > -1);
}
/**
* Helper function for overriding the cipher invocation, i.e. XOR doesn't use a cipher
* and uses it's own implementation
*
* @return
* @throws BadPaddingException
* @throws IllegalBlockSizeException
* @throws ShortBufferException
*/
protected int invokeCipher(int totalBytes, boolean doFinal) throws GeneralSecurityException {
if (doFinal) {
return _cipher.doFinal(_chunk, 0, totalBytes, _chunk);
} else { } else {
_cipher.doFinal(_chunk, 0, totalBytes, _chunk); return _cipher.update(_chunk, 0, totalBytes, _chunk);
} }
} }
/**
* Used when BIFF header fields (sid, size) are being read. The internal
* {@link Cipher} instance must step even when unencrypted bytes are read
*/
public int readPlain(byte b[], int off, int len) throws IOException {
if (len <= 0) {
return len;
}
int readBytes, total = 0;
do {
readBytes = read(b, off, len, true);
total += Math.max(0, readBytes);
} while (readBytes > -1 && total < len);
return total;
}
/**
* Some ciphers (actually just XOR) are based on the record size,
* which needs to be set before encryption
*
* @param recordSize the size of the next record
*/
public void setNextRecordSize(int recordSize) {
}
/**
* @return the chunk bytes
*/
protected byte[] getChunk() {
return _chunk;
}
/**
* @return the plain bytes
*/
protected byte[] getPlain() {
return _plain;
}
/**
* @return the absolute position in the stream
*/
public long getPos() {
return _pos;
}
} }

View File

@ -26,7 +26,10 @@ import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.ShortBufferException;
import org.apache.poi.EncryptedDocumentException; import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.filesystem.DirectoryNode; import org.apache.poi.poifs.filesystem.DirectoryNode;
@ -153,19 +156,17 @@ public abstract class ChunkedCipherOutputStream extends FilterOutputStream {
int ciLen; int ciLen;
try { try {
boolean doFinal = true;
if (_chunkSize == STREAMING) { if (_chunkSize == STREAMING) {
if (continued) { if (continued) {
ciLen = _cipher.update(_chunk, 0, posInChunk, _chunk); doFinal = false;
} else {
ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk);
} }
// reset stream (not only) in case we were interrupted by plain stream parts // reset stream (not only) in case we were interrupted by plain stream parts
_pos = 0; _pos = 0;
} else { } else {
_cipher = initCipherForBlock(_cipher, index, lastChunk); _cipher = initCipherForBlock(_cipher, index, lastChunk);
ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk);
} }
ciLen = invokeCipher(posInChunk, doFinal);
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
throw new IOException("can't re-/initialize cipher", e); throw new IOException("can't re-/initialize cipher", e);
} }
@ -173,6 +174,23 @@ public abstract class ChunkedCipherOutputStream extends FilterOutputStream {
out.write(_chunk, 0, ciLen); out.write(_chunk, 0, ciLen);
} }
/**
* Helper function for overriding the cipher invocation, i.e. XOR doesn't use a cipher
* and uses it's own implementation
*
* @return
* @throws BadPaddingException
* @throws IllegalBlockSizeException
* @throws ShortBufferException
*/
protected int invokeCipher(int posInChunk, boolean doFinal) throws GeneralSecurityException {
if (doFinal) {
return _cipher.doFinal(_chunk, 0, posInChunk, _chunk);
} else {
return _cipher.update(_chunk, 0, posInChunk, _chunk);
}
}
@Override @Override
public void close() throws IOException { public void close() throws IOException {
try { try {

View File

@ -29,7 +29,6 @@ import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;
import org.apache.poi.poifs.filesystem.OPOIFSFileSystem; import org.apache.poi.poifs.filesystem.OPOIFSFileSystem;
import org.apache.poi.poifs.filesystem.POIFSFileSystem; import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.util.LittleEndianInput;
public abstract class Decryptor implements Cloneable { public abstract class Decryptor implements Cloneable {
public static final String DEFAULT_PASSWORD="VelvetSweatshop"; public static final String DEFAULT_PASSWORD="VelvetSweatshop";
@ -66,7 +65,7 @@ public abstract class Decryptor implements Cloneable {
* @param initialPos initial/current byte position within the stream * @param initialPos initial/current byte position within the stream
* @return decrypted stream * @return decrypted stream
*/ */
public InputStream getDataStream(LittleEndianInput stream, int size, int initialPos) public InputStream getDataStream(InputStream stream, int size, int initialPos)
throws IOException, GeneralSecurityException { throws IOException, GeneralSecurityException {
throw new RuntimeException("this decryptor doesn't support reading from a stream"); throw new RuntimeException("this decryptor doesn't support reading from a stream");
} }

View File

@ -20,6 +20,7 @@ import static org.apache.poi.poifs.crypt.EncryptionMode.agile;
import static org.apache.poi.poifs.crypt.EncryptionMode.binaryRC4; import static org.apache.poi.poifs.crypt.EncryptionMode.binaryRC4;
import static org.apache.poi.poifs.crypt.EncryptionMode.cryptoAPI; import static org.apache.poi.poifs.crypt.EncryptionMode.cryptoAPI;
import static org.apache.poi.poifs.crypt.EncryptionMode.standard; import static org.apache.poi.poifs.crypt.EncryptionMode.standard;
import static org.apache.poi.poifs.crypt.EncryptionMode.xor;
import java.io.IOException; import java.io.IOException;
@ -35,6 +36,7 @@ import org.apache.poi.util.LittleEndianInput;
/** /**
*/ */
public class EncryptionInfo implements Cloneable { public class EncryptionInfo implements Cloneable {
private final EncryptionMode encryptionMode;
private final int versionMajor; private final int versionMajor;
private final int versionMinor; private final int versionMinor;
private final int encryptionFlags; private final int encryptionFlags;
@ -75,49 +77,55 @@ public class EncryptionInfo implements Cloneable {
public EncryptionInfo(POIFSFileSystem fs) throws IOException { public EncryptionInfo(POIFSFileSystem fs) throws IOException {
this(fs.getRoot()); this(fs.getRoot());
} }
/** /**
* Opens for decryption * Opens for decryption
*/ */
public EncryptionInfo(OPOIFSFileSystem fs) throws IOException { public EncryptionInfo(OPOIFSFileSystem fs) throws IOException {
this(fs.getRoot()); this(fs.getRoot());
} }
/** /**
* Opens for decryption * Opens for decryption
*/ */
public EncryptionInfo(NPOIFSFileSystem fs) throws IOException { public EncryptionInfo(NPOIFSFileSystem fs) throws IOException {
this(fs.getRoot()); this(fs.getRoot());
} }
/** /**
* Opens for decryption * Opens for decryption
*/ */
public EncryptionInfo(DirectoryNode dir) throws IOException { public EncryptionInfo(DirectoryNode dir) throws IOException {
this(dir.createDocumentInputStream("EncryptionInfo"), false); this(dir.createDocumentInputStream("EncryptionInfo"), null);
} }
public EncryptionInfo(LittleEndianInput dis, boolean isCryptoAPI) throws IOException { public EncryptionInfo(LittleEndianInput dis, EncryptionMode preferredEncryptionMode) throws IOException {
final EncryptionMode encryptionMode; if (preferredEncryptionMode == xor) {
versionMajor = xor.versionMajor;
versionMinor = xor.versionMinor;
} else {
versionMajor = dis.readUShort(); versionMajor = dis.readUShort();
versionMinor = dis.readUShort(); versionMinor = dis.readUShort();
}
if ( versionMajor == binaryRC4.versionMajor if ( versionMajor == xor.versionMajor
&& versionMinor == xor.versionMinor) {
encryptionMode = xor;
encryptionFlags = -1;
} else if ( versionMajor == binaryRC4.versionMajor
&& versionMinor == binaryRC4.versionMinor) { && versionMinor == binaryRC4.versionMinor) {
encryptionMode = binaryRC4; encryptionMode = binaryRC4;
encryptionFlags = -1; encryptionFlags = -1;
} else if (!isCryptoAPI } else if (
&& versionMajor == agile.versionMajor 2 <= versionMajor && versionMajor <= 4
&& versionMinor == 2) {
encryptionMode = (preferredEncryptionMode == cryptoAPI) ? cryptoAPI : standard;
encryptionFlags = dis.readInt();
} else if (
versionMajor == agile.versionMajor
&& versionMinor == agile.versionMinor){ && versionMinor == agile.versionMinor){
encryptionMode = agile; encryptionMode = agile;
encryptionFlags = dis.readInt(); encryptionFlags = dis.readInt();
} else if (!isCryptoAPI
&& 2 <= versionMajor && versionMajor <= 4
&& versionMinor == standard.versionMinor) {
encryptionMode = standard;
encryptionFlags = dis.readInt();
} else if (isCryptoAPI
&& 2 <= versionMajor && versionMajor <= 4
&& versionMinor == cryptoAPI.versionMinor) {
encryptionMode = cryptoAPI;
encryptionFlags = dis.readInt();
} else { } else {
encryptionFlags = dis.readInt(); encryptionFlags = dis.readInt();
throw new EncryptedDocumentException( throw new EncryptedDocumentException(
@ -170,6 +178,7 @@ public class EncryptionInfo implements Cloneable {
, int blockSize , int blockSize
, ChainingMode chainingMode , ChainingMode chainingMode
) { ) {
this.encryptionMode = encryptionMode;
versionMajor = encryptionMode.versionMajor; versionMajor = encryptionMode.versionMajor;
versionMinor = encryptionMode.versionMinor; versionMinor = encryptionMode.versionMinor;
encryptionFlags = encryptionMode.encryptionFlags; encryptionFlags = encryptionMode.encryptionFlags;
@ -236,6 +245,10 @@ public class EncryptionInfo implements Cloneable {
this.encryptor = encryptor; this.encryptor = encryptor;
} }
public EncryptionMode getEncryptionMode() {
return encryptionMode;
}
@Override @Override
public EncryptionInfo clone() throws CloneNotSupportedException { public EncryptionInfo clone() throws CloneNotSupportedException {
EncryptionInfo other = (EncryptionInfo)super.clone(); EncryptionInfo other = (EncryptionInfo)super.clone();

View File

@ -33,7 +33,9 @@ public enum EncryptionMode {
/* @see <a href="http://msdn.microsoft.com/en-us/library/dd906097(v=office.12).aspx">2.3.4.5 \EncryptionInfo Stream (Standard Encryption)</a> */ /* @see <a href="http://msdn.microsoft.com/en-us/library/dd906097(v=office.12).aspx">2.3.4.5 \EncryptionInfo Stream (Standard Encryption)</a> */
standard("org.apache.poi.poifs.crypt.standard.StandardEncryptionInfoBuilder", 4, 2, 0x24), standard("org.apache.poi.poifs.crypt.standard.StandardEncryptionInfoBuilder", 4, 2, 0x24),
/* @see <a href="http://msdn.microsoft.com/en-us/library/dd925810(v=office.12).aspx">2.3.4.10 \EncryptionInfo Stream (Agile Encryption)</a> */ /* @see <a href="http://msdn.microsoft.com/en-us/library/dd925810(v=office.12).aspx">2.3.4.10 \EncryptionInfo Stream (Agile Encryption)</a> */
agile("org.apache.poi.poifs.crypt.agile.AgileEncryptionInfoBuilder", 4, 4, 0x40) agile("org.apache.poi.poifs.crypt.agile.AgileEncryptionInfoBuilder", 4, 4, 0x40),
/* @see <a href="https://msdn.microsoft.com/en-us/library/dd907599(v=office.12).aspx">XOR Obfuscation</a> */
xor("org.apache.poi.poifs.crypt.xor.XOREncryptionInfoBuilder", 0, 0, 0)
; ;
public final String builder; public final String builder;

View File

@ -29,11 +29,9 @@ import javax.crypto.spec.SecretKeySpec;
import org.apache.poi.EncryptedDocumentException; import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.crypt.*; import org.apache.poi.poifs.crypt.*;
import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIDecryptor;
import org.apache.poi.poifs.filesystem.DirectoryNode; import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.poifs.filesystem.DocumentInputStream; import org.apache.poi.poifs.filesystem.DocumentInputStream;
import org.apache.poi.util.LittleEndian; import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.LittleEndianInput;
import org.apache.poi.util.StringUtil; import org.apache.poi.util.StringUtil;
public class BinaryRC4Decryptor extends Decryptor implements Cloneable { public class BinaryRC4Decryptor extends Decryptor implements Cloneable {
@ -53,7 +51,7 @@ public class BinaryRC4Decryptor extends Decryptor implements Cloneable {
super(stream, size, _chunkSize); super(stream, size, _chunkSize);
} }
public BinaryRC4CipherInputStream(LittleEndianInput stream) public BinaryRC4CipherInputStream(InputStream stream)
throws GeneralSecurityException { throws GeneralSecurityException {
super(stream, Integer.MAX_VALUE, _chunkSize); super(stream, Integer.MAX_VALUE, _chunkSize);
} }
@ -140,7 +138,8 @@ public class BinaryRC4Decryptor extends Decryptor implements Cloneable {
return new BinaryRC4CipherInputStream(dis, _length); return new BinaryRC4CipherInputStream(dis, _length);
} }
public InputStream getDataStream(LittleEndianInput stream) @Override
public InputStream getDataStream(InputStream stream, int size, int initialPos)
throws IOException, GeneralSecurityException { throws IOException, GeneralSecurityException {
return new BinaryRC4CipherInputStream(stream); return new BinaryRC4CipherInputStream(stream);
} }

View File

@ -45,7 +45,6 @@ import org.apache.poi.util.BitFieldFactory;
import org.apache.poi.util.BoundedInputStream; import org.apache.poi.util.BoundedInputStream;
import org.apache.poi.util.IOUtils; import org.apache.poi.util.IOUtils;
import org.apache.poi.util.LittleEndian; import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.LittleEndianInput;
import org.apache.poi.util.LittleEndianInputStream; import org.apache.poi.util.LittleEndianInputStream;
import org.apache.poi.util.StringUtil; import org.apache.poi.util.StringUtil;
@ -146,7 +145,7 @@ public class CryptoAPIDecryptor extends Decryptor implements Cloneable {
} }
@Override @Override
public ChunkedCipherInputStream getDataStream(LittleEndianInput stream, int size, int initialPos) public ChunkedCipherInputStream getDataStream(InputStream stream, int size, int initialPos)
throws IOException, GeneralSecurityException { throws IOException, GeneralSecurityException {
return new CryptoAPICipherInputStream(stream, size, initialPos); return new CryptoAPICipherInputStream(stream, size, initialPos);
} }
@ -233,7 +232,7 @@ public class CryptoAPIDecryptor extends Decryptor implements Cloneable {
return CryptoAPIDecryptor.this.initCipherForBlock(existing, block); return CryptoAPIDecryptor.this.initCipherForBlock(existing, block);
} }
public CryptoAPICipherInputStream(LittleEndianInput stream, long size, int initialPos) public CryptoAPICipherInputStream(InputStream stream, long size, int initialPos)
throws GeneralSecurityException { throws GeneralSecurityException {
super(stream, size, _chunkSize, initialPos); super(stream, size, _chunkSize, initialPos);
} }

View File

@ -0,0 +1,174 @@
/* ====================================================================
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.xor;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.apache.poi.poifs.crypt.ChunkedCipherInputStream;
import org.apache.poi.poifs.crypt.CryptoFunctions;
import org.apache.poi.poifs.crypt.Decryptor;
import org.apache.poi.poifs.crypt.EncryptionInfo;
import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.util.LittleEndian;
public class XORDecryptor extends Decryptor implements Cloneable {
private long _length = -1L;
private int _chunkSize = 512;
private class XORCipherInputStream extends ChunkedCipherInputStream {
private final int _initialOffset;
private int _recordStart = 0;
private int _recordEnd = 0;
@Override
protected Cipher initCipherForBlock(Cipher existing, int block)
throws GeneralSecurityException {
return XORDecryptor.this.initCipherForBlock(existing, block);
}
public XORCipherInputStream(InputStream stream, int initialPos)
throws GeneralSecurityException {
super(stream, Integer.MAX_VALUE, _chunkSize);
_initialOffset = initialPos;
}
@Override
protected int invokeCipher(int totalBytes, boolean doFinal) {
final int pos = (int)getPos();
final byte xorArray[] = getEncryptionInfo().getDecryptor().getSecretKey().getEncoded();
final byte chunk[] = getChunk();
final byte plain[] = getPlain();
final int posInChunk = pos & getChunkMask();
/*
* From: http://social.msdn.microsoft.com/Forums/en-US/3dadbed3-0e68-4f11-8b43-3a2328d9ebd5
*
* The initial value for XorArrayIndex is as follows:
* XorArrayIndex = (FileOffset + Data.Length) % 16
*
* The FileOffset variable in this context is the stream offset into the Workbook stream at
* the time we are about to write each of the bytes of the record data.
* This (the value) is then incremented after each byte is written.
*/
final int xorArrayIndex = _initialOffset+_recordEnd+(pos-_recordStart);
for (int i=0; pos+i < _recordEnd && i < totalBytes; i++) {
// The following is taken from the Libre Office implementation
// It seems that the encrypt and decrypt method is mixed up
// in the MS-OFFCRYPTO docs
byte value = plain[posInChunk+i];
value = rotateLeft(value, 3);
value ^= xorArray[(xorArrayIndex+i) & 0x0F];
chunk[posInChunk+i] = value;
}
// the other bytes will be encoded, when setNextRecordSize is called the next time
return totalBytes;
}
private byte rotateLeft(byte bits, int shift) {
return (byte)(((bits & 0xff) << shift) | ((bits & 0xff) >>> (8 - shift)));
}
/**
* Decrypts a xor obfuscated byte array.
* The data is decrypted in-place
*
* @see <a href="http://msdn.microsoft.com/en-us/library/dd908506.aspx">2.3.7.3 Binary Document XOR Data Transformation Method 1</a>
*/
@Override
public void setNextRecordSize(int recordSize) {
_recordStart = (int)getPos();
_recordEnd = _recordStart+recordSize;
int pos = (int)getPos();
byte chunk[] = getChunk();
int chunkMask = getChunkMask();
int nextBytes = Math.min(recordSize, chunk.length-(pos & chunkMask));
invokeCipher(nextBytes, true);
}
}
protected XORDecryptor() {
}
@Override
public boolean verifyPassword(String password) {
XOREncryptionVerifier ver = (XOREncryptionVerifier)getEncryptionInfo().getVerifier();
int keyVer = LittleEndian.getUShort(ver.getEncryptedKey());
int verifierVer = LittleEndian.getUShort(ver.getEncryptedVerifier());
int keyComp = CryptoFunctions.createXorKey1(password);
int verifierComp = CryptoFunctions.createXorVerifier1(password);
if (keyVer == keyComp && verifierVer == verifierComp) {
byte xorArray[] = CryptoFunctions.createXorArray1(password);
setSecretKey(new SecretKeySpec(xorArray, "XOR"));
return true;
} else {
return false;
}
}
@Override
public Cipher initCipherForBlock(Cipher cipher, int block)
throws GeneralSecurityException {
return null;
}
protected static Cipher initCipherForBlock(Cipher cipher, int block,
EncryptionInfo encryptionInfo, SecretKey skey, int encryptMode)
throws GeneralSecurityException {
return null;
}
@Override
public ChunkedCipherInputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException {
throw new RuntimeException("not supported");
}
@Override
public InputStream getDataStream(InputStream stream, int size, int initialPos)
throws IOException, GeneralSecurityException {
return new XORCipherInputStream(stream, initialPos);
}
@Override
public long getLength() {
if (_length == -1L) {
throw new IllegalStateException("Decryptor.getDataStream() was not called");
}
return _length;
}
@Override
public void setChunkSize(int chunkSize) {
_chunkSize = chunkSize;
}
@Override
public XORDecryptor clone() throws CloneNotSupportedException {
return (XORDecryptor)super.clone();
}
}

View File

@ -15,16 +15,23 @@
limitations under the License. limitations under the License.
==================================================================== */ ==================================================================== */
package org.apache.poi.hssf.record.crypto; package org.apache.poi.poifs.crypt.xor;
import org.apache.poi.poifs.crypt.EncryptionHeader;
import org.apache.poi.poifs.crypt.standard.EncryptionRecord;
import org.apache.poi.util.LittleEndianByteArrayOutputStream;
public interface Biff8Cipher { public class XOREncryptionHeader extends EncryptionHeader implements EncryptionRecord, Cloneable {
void startRecord(int currentSid);
void setNextRecordSize(int recordSize); protected XOREncryptionHeader() {
void skipTwoBytes(); }
void xor(byte[] buf, int pOffset, int pLen);
int xorByte(int rawVal); @Override
int xorShort(int rawVal); public void write(LittleEndianByteArrayOutputStream littleendianbytearrayoutputstream) {
int xorInt(int rawVal); }
long xorLong(long rawVal);
@Override
public XOREncryptionHeader clone() throws CloneNotSupportedException {
return (XOREncryptionHeader)super.clone();
}
} }

View File

@ -0,0 +1,62 @@
/* ====================================================================
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.xor;
import java.io.IOException;
import org.apache.poi.poifs.crypt.ChainingMode;
import org.apache.poi.poifs.crypt.CipherAlgorithm;
import org.apache.poi.poifs.crypt.Decryptor;
import org.apache.poi.poifs.crypt.EncryptionInfo;
import org.apache.poi.poifs.crypt.EncryptionInfoBuilder;
import org.apache.poi.poifs.crypt.Encryptor;
import org.apache.poi.poifs.crypt.HashAlgorithm;
import org.apache.poi.util.LittleEndianInput;
public class XOREncryptionInfoBuilder implements EncryptionInfoBuilder {
public XOREncryptionInfoBuilder() {
}
@Override
public void initialize(EncryptionInfo info, LittleEndianInput dis)
throws IOException {
info.setHeader(new XOREncryptionHeader());
info.setVerifier(new XOREncryptionVerifier(dis));
Decryptor dec = new XORDecryptor();
dec.setEncryptionInfo(info);
info.setDecryptor(dec);
Encryptor enc = new XOREncryptor();
enc.setEncryptionInfo(info);
info.setEncryptor(enc);
}
@Override
public void initialize(EncryptionInfo info,
CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm,
int keyBits, int blockSize, ChainingMode chainingMode) {
info.setHeader(new XOREncryptionHeader());
info.setVerifier(new XOREncryptionVerifier());
Decryptor dec = new XORDecryptor();
dec.setEncryptionInfo(info);
info.setDecryptor(dec);
Encryptor enc = new XOREncryptor();
enc.setEncryptionInfo(info);
info.setEncryptor(enc);
}
}

View File

@ -0,0 +1,61 @@
/* ====================================================================
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.xor;
import org.apache.poi.poifs.crypt.EncryptionVerifier;
import org.apache.poi.poifs.crypt.standard.EncryptionRecord;
import org.apache.poi.util.LittleEndianByteArrayOutputStream;
import org.apache.poi.util.LittleEndianInput;
public class XOREncryptionVerifier extends EncryptionVerifier implements EncryptionRecord, Cloneable {
protected XOREncryptionVerifier() {
setEncryptedKey(new byte[2]);
setEncryptedVerifier(new byte[2]);
}
protected XOREncryptionVerifier(LittleEndianInput is) {
/**
* key (2 bytes): An unsigned integer that specifies the obfuscation key.
* See [MS-OFFCRYPTO], 2.3.6.2 section, the first step of initializing XOR
* array where it describes the generation of 16-bit XorKey value.
*/
byte key[] = new byte[2];
is.readFully(key);
setEncryptedKey(key);
/**
* verificationBytes (2 bytes): An unsigned integer that specifies
* the password verification identifier.
*/
byte verifier[] = new byte[2];
is.readFully(verifier);
setEncryptedVerifier(verifier);
}
@Override
public void write(LittleEndianByteArrayOutputStream bos) {
bos.write(getEncryptedKey());
bos.write(getEncryptedVerifier());
}
@Override
public XOREncryptionVerifier clone() throws CloneNotSupportedException {
return (XOREncryptionVerifier)super.clone();
}
}

View File

@ -0,0 +1,99 @@
/* ====================================================================
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.xor;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Random;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.poifs.crypt.ChunkedCipherOutputStream;
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.Encryptor;
import org.apache.poi.poifs.crypt.HashAlgorithm;
import org.apache.poi.poifs.crypt.standard.EncryptionRecord;
import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.util.LittleEndianByteArrayOutputStream;
public class XOREncryptor extends Encryptor implements Cloneable {
protected XOREncryptor() {
}
@Override
public void confirmPassword(String password) {
}
@Override
public void confirmPassword(String password, byte keySpec[],
byte keySalt[], byte verifier[], byte verifierSalt[],
byte integritySalt[]) {
}
@Override
public OutputStream getDataStream(DirectoryNode dir)
throws IOException, GeneralSecurityException {
OutputStream countStream = new XORCipherOutputStream(dir);
return countStream;
}
protected int getKeySizeInBytes() {
return -1;
}
protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException {
}
@Override
public XOREncryptor clone() throws CloneNotSupportedException {
return (XOREncryptor)super.clone();
}
protected class XORCipherOutputStream extends ChunkedCipherOutputStream {
@Override
protected Cipher initCipherForBlock(Cipher cipher, int block, boolean lastChunk)
throws GeneralSecurityException {
return XORDecryptor.initCipherForBlock(cipher, block, getEncryptionInfo(), getSecretKey(), Cipher.ENCRYPT_MODE);
}
@Override
protected void calculateChecksum(File file, int i) {
}
@Override
protected void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile)
throws IOException, GeneralSecurityException {
XOREncryptor.this.createEncryptionInfoEntry(dir);
}
public XORCipherOutputStream(DirectoryNode dir)
throws IOException, GeneralSecurityException {
super(dir, 512);
}
}
}

View File

@ -53,7 +53,8 @@ public final class DocumentEncryptionAtom extends PositionDependentRecordAtom {
ByteArrayInputStream bis = new ByteArrayInputStream(source, start+8, len-8); ByteArrayInputStream bis = new ByteArrayInputStream(source, start+8, len-8);
LittleEndianInputStream leis = new LittleEndianInputStream(bis); LittleEndianInputStream leis = new LittleEndianInputStream(bis);
ei = new EncryptionInfo(leis, true); ei = new EncryptionInfo(leis, EncryptionMode.cryptoAPI);
leis.close();
} }
public DocumentEncryptionAtom() { public DocumentEncryptionAtom() {
@ -121,6 +122,7 @@ public final class DocumentEncryptionAtom extends PositionDependentRecordAtom {
LittleEndian.putInt(_header, 4, bos.getWriteIndex()); LittleEndian.putInt(_header, 4, bos.getWriteIndex());
out.write(_header); out.write(_header);
out.write(data, 0, bos.getWriteIndex()); out.write(data, 0, bos.getWriteIndex());
bos.close();
} }
@Override @Override

View File

@ -21,8 +21,8 @@ import org.apache.poi.hssf.record.aggregates.AllRecordAggregateTests;
import org.apache.poi.hssf.record.cf.TestCellRange; import org.apache.poi.hssf.record.cf.TestCellRange;
import org.apache.poi.hssf.record.chart.AllChartRecordTests; import org.apache.poi.hssf.record.chart.AllChartRecordTests;
import org.apache.poi.hssf.record.common.TestUnicodeString; import org.apache.poi.hssf.record.common.TestUnicodeString;
import org.apache.poi.hssf.record.crypto.AllHSSFEncryptionTests;
import org.apache.poi.hssf.record.pivot.AllPivotRecordTests; import org.apache.poi.hssf.record.pivot.AllPivotRecordTests;
import org.apache.poi.poifs.crypt.AllEncryptionTests;
import org.apache.poi.ss.formula.constant.TestConstantValueParser; import org.apache.poi.ss.formula.constant.TestConstantValueParser;
import org.apache.poi.ss.formula.ptg.AllFormulaTests; import org.apache.poi.ss.formula.ptg.AllFormulaTests;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -34,7 +34,7 @@ import org.junit.runners.Suite;
@RunWith(Suite.class) @RunWith(Suite.class)
@Suite.SuiteClasses({ @Suite.SuiteClasses({
AllChartRecordTests.class, AllChartRecordTests.class,
AllHSSFEncryptionTests.class, AllEncryptionTests.class,
AllFormulaTests.class, AllFormulaTests.class,
AllPivotRecordTests.class, AllPivotRecordTests.class,
AllRecordAggregateTests.class, AllRecordAggregateTests.class,

View File

@ -1,102 +0,0 @@
/* ====================================================================
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.hssf.record.crypto;
import java.util.Arrays;
import junit.framework.ComparisonFailure;
import junit.framework.TestCase;
import org.apache.poi.util.HexDump;
import org.apache.poi.util.HexRead;
/**
* Tests for {@link Biff8EncryptionKey}
*
* @author Josh Micich
*/
public final class TestBiff8EncryptionKey extends TestCase {
private static byte[] fromHex(String hexString) {
return HexRead.readFromString(hexString);
}
public void testCreateKeyDigest() {
byte[] docIdData = fromHex("17 F6 D1 6B 09 B1 5F 7B 4C 9D 03 B4 81 B5 B4 4A");
byte[] keyDigest = Biff8RC4Key.createKeyDigest("MoneyForNothing", docIdData);
byte[] expResult = fromHex("C2 D9 56 B2 6B");
if (!Arrays.equals(expResult, keyDigest)) {
throw new ComparisonFailure("keyDigest mismatch", HexDump.toHex(expResult), HexDump.toHex(keyDigest));
}
}
public void testValidateWithDefaultPassword() {
String docIdSuffixA = "F 35 52 38 0D 75 4A E6 85 C2 FD 78 CE 3D D1 B6"; // valid prefix is 'D'
String saltHashA = "30 38 BE 5E 93 C5 7E B4 5F 52 CD A1 C6 8F B6 2A";
String saltDataA = "D4 04 43 EC B7 A7 6F 6A D2 68 C7 DF CF A8 80 68";
String docIdB = "39 D7 80 41 DA E4 74 2C 8C 84 F9 4D 39 9A 19 2D";
String saltDataSuffixB = "3 EA 8D 52 11 11 37 D2 BD 55 4C 01 0A 47 6E EB"; // valid prefix is 'C'
String saltHashB = "96 19 F5 D0 F1 63 08 F1 3E 09 40 1E 87 F0 4E 16";
confirmValid(true, "D" + docIdSuffixA, saltDataA, saltHashA);
confirmValid(true, docIdB, "C" + saltDataSuffixB, saltHashB);
confirmValid(false, "E" + docIdSuffixA, saltDataA, saltHashA);
confirmValid(false, docIdB, "B" + saltDataSuffixB, saltHashB);
}
public void testValidateWithSuppliedPassword() {
String docId = "DF 35 52 38 0D 75 4A E6 85 C2 FD 78 CE 3D D1 B6";
String saltData = "D4 04 43 EC B7 A7 6F 6A D2 68 C7 DF CF A8 80 68";
String saltHashA = "8D C2 63 CC E1 1D E0 05 20 16 96 AF 48 59 94 64"; // for password '5ecret'
String saltHashB = "31 0B 0D A4 69 55 8E 27 A1 03 AD C9 AE F8 09 04"; // for password '5ecret'
confirmValid(true, docId, saltData, saltHashA, "5ecret");
confirmValid(false, docId, saltData, saltHashA, "Secret");
confirmValid(true, docId, saltData, saltHashB, "Secret");
confirmValid(false, docId, saltData, saltHashB, "secret");
}
private static void confirmValid(boolean expectedResult,
String docIdHex, String saltDataHex, String saltHashHex) {
confirmValid(expectedResult, docIdHex, saltDataHex, saltHashHex, null);
}
private static void confirmValid(boolean expectedResult,
String docIdHex, String saltDataHex, String saltHashHex, String password) {
byte[] docId = fromHex(docIdHex);
byte[] saltData = fromHex(saltDataHex);
byte[] saltHash = fromHex(saltHashHex);
Biff8EncryptionKey key;
if (password == null) {
key = Biff8EncryptionKey.create(docId);
} else {
key = Biff8EncryptionKey.create(password, docId);
}
boolean actResult = key.validate(saltData, saltHash);
if (expectedResult) {
assertTrue("validate failed", actResult);
} else {
assertFalse("validate succeeded unexpectedly", actResult);
}
}
}

View File

@ -0,0 +1,62 @@
/* ====================================================================
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.hssf.usermodel;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import org.apache.poi.hssf.HSSFITestDataProvider;
import org.apache.poi.hssf.extractor.ExcelExtractor;
import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
import org.junit.AfterClass;
import org.junit.Test;
public class TestCryptoAPI {
final HSSFITestDataProvider ssTests = HSSFITestDataProvider.instance;
@AfterClass
public static void resetPW() {
Biff8EncryptionKey.setCurrentUserPassword(null);
}
@Test
public void bug59857() throws IOException {
Biff8EncryptionKey.setCurrentUserPassword("abc");
HSSFWorkbook wb1 = ssTests.openSampleWorkbook("xor-encryption-abc.xls");
String textExpected = "Sheet1\n1\n2\n3\n";
String textActual = new ExcelExtractor(wb1).getText();
assertEquals(textExpected, textActual);
wb1.close();
Biff8EncryptionKey.setCurrentUserPassword("password");
HSSFWorkbook wb2 = ssTests.openSampleWorkbook("password.xls");
textExpected = "A ZIP bomb is a variant of mail-bombing. After most commercial mail servers began checking mail with anti-virus software and filtering certain malicious file types, trojan horse viruses tried to send themselves compressed into archives, such as ZIP, RAR or 7-Zip. Mail server software was then configured to unpack archives and check their contents as well. That gave black hats the idea to compose a \"bomb\" consisting of an enormous text file, containing, for example, only the letter z repeated millions of times. Such a file compresses into a relatively small archive, but its unpacking (especially by early versions of mail servers) would use a high amount of processing power, RAM and swap space, which could result in denial of service. Modern mail server computers usually have sufficient intelligence to recognize such attacks as well as sufficient processing power and memory space to process malicious attachments without interruption of service, though some are still susceptible to this technique if the ZIP bomb is mass-mailed.";
textActual = new ExcelExtractor(wb2).getText();
assertTrue(textActual.contains(textExpected));
wb2.close();
Biff8EncryptionKey.setCurrentUserPassword("freedom");
HSSFWorkbook wb3 = ssTests.openSampleWorkbook("35897-type4.xls");
textExpected = "Sheet1\nhello there!\n";
textActual = new ExcelExtractor(wb3).getText();
assertEquals(textExpected, textActual);
wb3.close();
}
}

View File

@ -15,20 +15,19 @@
limitations under the License. limitations under the License.
==================================================================== */ ==================================================================== */
package org.apache.poi.hssf.record.crypto; package org.apache.poi.poifs.crypt;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.Suite; import org.junit.runners.Suite;
/** /**
* Collects all tests for package <tt>org.apache.poi.hssf.record.crypto</tt>. * Collects all tests for package <tt>org.apache.poi.poifs.crypt</tt>.
*
* @author Josh Micich
*/ */
@RunWith(Suite.class) @RunWith(Suite.class)
@Suite.SuiteClasses({ @Suite.SuiteClasses({
TestBiff8DecryptingStream.class, TestBiff8DecryptingStream.class,
TestBiff8EncryptionKey.class TestCipherAlgorithm.class,
TestXorEncryption.class
}) })
public final class AllHSSFEncryptionTests { public final class AllEncryptionTests {
} }

View File

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
==================================================================== */ ==================================================================== */
package org.apache.poi.hssf.record.crypto; package org.apache.poi.poifs.crypt;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@ -23,17 +23,18 @@ import static org.junit.Assert.assertFalse;
import java.io.InputStream; import java.io.InputStream;
import java.util.Arrays; import java.util.Arrays;
import junit.framework.AssertionFailedError; import javax.crypto.spec.SecretKeySpec;
import junit.framework.ComparisonFailure;
import org.apache.poi.hssf.record.crypto.Biff8DecryptingStream;
import org.apache.poi.util.HexDump; import org.apache.poi.util.HexDump;
import org.apache.poi.util.HexRead; import org.apache.poi.util.HexRead;
import org.junit.Test; import org.junit.Test;
import junit.framework.AssertionFailedError;
import junit.framework.ComparisonFailure;
/** /**
* Tests for {@link Biff8DecryptingStream} * Tests for {@link Biff8DecryptingStream}
*
* @author Josh Micich
*/ */
public final class TestBiff8DecryptingStream { public final class TestBiff8DecryptingStream {
@ -49,12 +50,10 @@ public final class TestBiff8DecryptingStream {
public MockStream(int initialValue) { public MockStream(int initialValue) {
_initialValue = initialValue; _initialValue = initialValue;
} }
public int read() { public int read() {
return (_initialValue+_position++) & 0xFF; return (_initialValue+_position++) & 0xFF;
} }
public int getPosition() {
return _position;
}
} }
private static final class StreamTester { private static final class StreamTester {
@ -70,7 +69,11 @@ public final class TestBiff8DecryptingStream {
public StreamTester(MockStream ms, String keyDigestHex, int expectedFirstInt) { public StreamTester(MockStream ms, String keyDigestHex, int expectedFirstInt) {
_ms = ms; _ms = ms;
byte[] keyDigest = HexRead.readFromString(keyDigestHex); byte[] keyDigest = HexRead.readFromString(keyDigestHex);
_bds = new Biff8DecryptingStream(_ms, 0, new Biff8RC4Key(keyDigest)); EncryptionInfo ei = new EncryptionInfo(EncryptionMode.binaryRC4);
Decryptor dec = ei.getDecryptor();
dec.setSecretKey(new SecretKeySpec(keyDigest, "RC4"));
_bds = new Biff8DecryptingStream(_ms, 0, ei);
assertEquals(expectedFirstInt, _bds.readInt()); assertEquals(expectedFirstInt, _bds.readInt());
_errorsOccurred = false; _errorsOccurred = false;
} }
@ -84,11 +87,11 @@ public final class TestBiff8DecryptingStream {
* Also confirms that read position of the underlying stream is aligned. * Also confirms that read position of the underlying stream is aligned.
*/ */
public void rollForward(int fromPosition, int toPosition) { public void rollForward(int fromPosition, int toPosition) {
assertEquals(fromPosition, _ms.getPosition()); assertEquals(fromPosition, _bds.getPosition());
for (int i = fromPosition; i < toPosition; i++) { for (int i = fromPosition; i < toPosition; i++) {
_bds.readByte(); _bds.readByte();
} }
assertEquals(toPosition, _ms.getPosition()); assertEquals(toPosition, _bds.getPosition());
} }
public void confirmByte(int expVal) { public void confirmByte(int expVal) {

View File

@ -17,14 +17,14 @@
package org.apache.poi.poifs.crypt; package org.apache.poi.poifs.crypt;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import org.apache.poi.EncryptedDocumentException; import org.apache.poi.EncryptedDocumentException;
import org.junit.Test; import org.junit.Test;
public class TestCipherAlgorithm { public class TestCipherAlgorithm {
@Test @Test
public void test() { public void validInputs() {
assertEquals(128, CipherAlgorithm.aes128.defaultKeySize); assertEquals(128, CipherAlgorithm.aes128.defaultKeySize);
for(CipherAlgorithm alg : CipherAlgorithm.values()) { for(CipherAlgorithm alg : CipherAlgorithm.values()) {
@ -33,27 +33,20 @@ public class TestCipherAlgorithm {
assertEquals(CipherAlgorithm.aes128, CipherAlgorithm.fromEcmaId(0x660E)); assertEquals(CipherAlgorithm.aes128, CipherAlgorithm.fromEcmaId(0x660E));
assertEquals(CipherAlgorithm.aes192, CipherAlgorithm.fromXmlId("AES", 192)); assertEquals(CipherAlgorithm.aes192, CipherAlgorithm.fromXmlId("AES", 192));
}
try { @Test(expected=EncryptedDocumentException.class)
public void invalidEcmaId() {
CipherAlgorithm.fromEcmaId(0); CipherAlgorithm.fromEcmaId(0);
fail("Should throw exception");
} catch (EncryptedDocumentException e) {
// expected
} }
try { @Test(expected=EncryptedDocumentException.class)
public void invalidXmlId1() {
CipherAlgorithm.fromXmlId("AES", 1); CipherAlgorithm.fromXmlId("AES", 1);
fail("Should throw exception");
} catch (EncryptedDocumentException e) {
// expected
} }
try { @Test(expected=EncryptedDocumentException.class)
public void invalidXmlId2() {
CipherAlgorithm.fromXmlId("RC1", 0x40); CipherAlgorithm.fromXmlId("RC1", 0x40);
fail("Should throw exception");
} catch (EncryptedDocumentException e) {
// expected
} }
} }
}

View File

@ -15,13 +15,14 @@
limitations under the License. limitations under the License.
==================================================================== */ ==================================================================== */
package org.apache.poi.hssf.record.crypto; package org.apache.poi.poifs.crypt;
import static org.hamcrest.core.IsEqual.equalTo; import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import org.apache.poi.hssf.HSSFTestDataSamples; import org.apache.poi.hssf.HSSFTestDataSamples;
import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.poifs.crypt.CryptoFunctions; import org.apache.poi.poifs.crypt.CryptoFunctions;

View File

@ -0,0 +1,106 @@
/* ====================================================================
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.binaryrc4;
import static org.apache.poi.util.HexRead.readFromString;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.security.GeneralSecurityException;
import javax.crypto.SecretKey;
import org.apache.poi.poifs.crypt.Decryptor;
import org.apache.poi.poifs.crypt.EncryptionInfo;
import org.apache.poi.poifs.crypt.EncryptionMode;
import org.junit.Test;
public class TestBinaryRC4 {
@Test
public void createKeyDigest() throws GeneralSecurityException {
byte[] docIdData = readFromString("17 F6 D1 6B 09 B1 5F 7B 4C 9D 03 B4 81 B5 B4 4A");
byte[] expResult = readFromString("C2 D9 56 B2 6B");
EncryptionInfo ei = new EncryptionInfo(EncryptionMode.binaryRC4);
BinaryRC4EncryptionVerifier ver = (BinaryRC4EncryptionVerifier)ei.getVerifier();
ver.setSalt(docIdData);
SecretKey sk = BinaryRC4Decryptor.generateSecretKey("MoneyForNothing", ver);
assertArrayEquals("keyDigest mismatch", expResult, sk.getEncoded());
}
@Test
public void testValidateWithDefaultPassword() throws GeneralSecurityException {
String docIdSuffixA = "F 35 52 38 0D 75 4A E6 85 C2 FD 78 CE 3D D1 B6"; // valid prefix is 'D'
String saltHashA = "30 38 BE 5E 93 C5 7E B4 5F 52 CD A1 C6 8F B6 2A";
String saltDataA = "D4 04 43 EC B7 A7 6F 6A D2 68 C7 DF CF A8 80 68";
String docIdB = "39 D7 80 41 DA E4 74 2C 8C 84 F9 4D 39 9A 19 2D";
String saltDataSuffixB = "3 EA 8D 52 11 11 37 D2 BD 55 4C 01 0A 47 6E EB"; // valid prefix is 'C'
String saltHashB = "96 19 F5 D0 F1 63 08 F1 3E 09 40 1E 87 F0 4E 16";
confirmValid(true, "D" + docIdSuffixA, saltDataA, saltHashA);
confirmValid(true, docIdB, "C" + saltDataSuffixB, saltHashB);
confirmValid(false, "E" + docIdSuffixA, saltDataA, saltHashA);
confirmValid(false, docIdB, "B" + saltDataSuffixB, saltHashB);
}
@Test
public void testValidateWithSuppliedPassword() throws GeneralSecurityException {
String docId = "DF 35 52 38 0D 75 4A E6 85 C2 FD 78 CE 3D D1 B6";
String saltData = "D4 04 43 EC B7 A7 6F 6A D2 68 C7 DF CF A8 80 68";
String saltHashA = "8D C2 63 CC E1 1D E0 05 20 16 96 AF 48 59 94 64"; // for password '5ecret'
String saltHashB = "31 0B 0D A4 69 55 8E 27 A1 03 AD C9 AE F8 09 04"; // for password '5ecret'
confirmValid(true, docId, saltData, saltHashA, "5ecret");
confirmValid(false, docId, saltData, saltHashA, "Secret");
confirmValid(true, docId, saltData, saltHashB, "Secret");
confirmValid(false, docId, saltData, saltHashB, "secret");
}
private static void confirmValid(boolean expectedResult,
String docIdHex, String saltDataHex, String saltHashHex) throws GeneralSecurityException {
confirmValid(expectedResult, docIdHex, saltDataHex, saltHashHex, null);
}
private static void confirmValid(boolean expectedResult, String docIdHex,
String saltDataHex, String saltHashHex, String password) throws GeneralSecurityException {
byte[] docId = readFromString(docIdHex);
byte[] saltData = readFromString(saltDataHex);
byte[] saltHash = readFromString(saltHashHex);
EncryptionInfo ei = new EncryptionInfo(EncryptionMode.binaryRC4);
BinaryRC4EncryptionVerifier ver = (BinaryRC4EncryptionVerifier)ei.getVerifier();
ver.setSalt(docId);
ver.setEncryptedVerifier(saltData);
ver.setEncryptedVerifierHash(saltHash);
String pass = password == null ? Decryptor.DEFAULT_PASSWORD : password;
boolean actResult = ei.getDecryptor().verifyPassword(pass);
if (expectedResult) {
assertTrue("validate failed", actResult);
} else {
assertFalse("validate succeeded unexpectedly", actResult);
}
}
}