/*
* EncFS Java Library
* Copyright (C) 2011 Mark R. Pariente
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*/
package org.mrpdaemon.sec.encfs;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Class containing static methods implementing crypto functionality for the
* rest of the library
*/
public final class EncFSCrypto {
private static final Logger LOG = LoggerFactory.getLogger(EncFSCrypto.class);
public final static String STREAM_CIPHER = "AES/CFB/NoPadding";
public final static String BLOCK_CIPHER = "AES/CBC/NoPadding";
private EncFSCrypto() {
}
// Create a new Mac object for the given key.
static Mac newMac(Key key) throws InvalidKeyException,
EncFSUnsupportedException {
Mac hmac;
try {
hmac = Mac.getInstance("HmacSHA1");
} catch (NoSuchAlgorithmException e) {
throw new EncFSUnsupportedException(e);
}
SecretKeySpec hmacKey = new SecretKeySpec(key.getEncoded(), "HmacSHA1");
hmac.init(hmacKey);
return hmac;
}
// Creates a new AES key with the given key bytes.
static Key newKey(byte[] keyBytes) {
return new SecretKeySpec(keyBytes, "AES");
}
// Return a a cipher with the given specification
static Cipher getCipher(String cipherSpec) throws EncFSUnsupportedException {
try {
return Cipher.getInstance(cipherSpec);
} catch (NoSuchAlgorithmException e) {
throw new EncFSUnsupportedException(e);
} catch (NoSuchPaddingException e) {
throw new EncFSUnsupportedException(e);
}
}
// Returns an IvParameterSpec for the given iv/seed
private static IvParameterSpec newIvSpec(Mac mac, byte[] iv, byte[] ivSeed) {
byte[] concat = new byte[EncFSVolume.IV_LENGTH_IN_BYTES+8];
System.arraycopy(iv, 0, concat, 0, EncFSVolume.IV_LENGTH_IN_BYTES);
if (ivSeed.length==4) {
// Concat 4 bytes of IV seed and 4 bytes of 0
for (int i = EncFSVolume.IV_LENGTH_IN_BYTES; i<EncFSVolume.IV_LENGTH_IN_BYTES+4; i++) {
concat[i] = ivSeed[EncFSVolume.IV_LENGTH_IN_BYTES+3-i];
}
for (int i = EncFSVolume.IV_LENGTH_IN_BYTES+4; i<EncFSVolume.IV_LENGTH_IN_BYTES+8; i++) {
concat[i] = 0;
}
} else {
// Use 8 bytes from IV seed
for (int i = EncFSVolume.IV_LENGTH_IN_BYTES; i<EncFSVolume.IV_LENGTH_IN_BYTES+8; i++) {
concat[i] = ivSeed[EncFSVolume.IV_LENGTH_IN_BYTES+7-i];
}
}
// Take first 16 bytes of the SHA-1 output (20 bytes)
byte[] ivResult = Arrays.copyOfRange(mac.doFinal(concat), 0,
EncFSVolume.IV_LENGTH_IN_BYTES);
return new IvParameterSpec(ivResult);
}
// Initialize the given cipher in the requested mode
static void cipherInit(Key key, Mac mac, int opMode, Cipher cipher,
byte[] iv, byte[] ivSeed) throws InvalidAlgorithmParameterException {
try {
cipher.init(opMode, key, newIvSpec(mac, iv, ivSeed));
} catch (InvalidKeyException e) {
LOG.error("cipherInit", e);
}
}
// Initialize the given cipher for a volume with the given parameters
static void cipherInit(EncFSVolume volume, int opMode, Cipher cipher,
byte[] ivSeed) throws InvalidAlgorithmParameterException {
cipherInit(volume.getKey(), volume.getMAC(), opMode, cipher,
volume.getIV(), ivSeed);
}
// Encrypt the key data
static byte[] encryptKeyData(byte[] volKeyData, byte[] passIvData,
Key passKey, Mac mac, byte[] mac32)
throws EncFSUnsupportedException, EncFSInvalidConfigException,
EncFSCorruptDataException {
byte[] cipherVolKeyData;
try {
cipherVolKeyData = StreamCrypto.streamEncrypt(
StreamCrypto.newStreamCipher(), mac, passKey, passIvData,
mac32, volKeyData);
} catch (InvalidAlgorithmParameterException e) {
throw new EncFSInvalidConfigException(e);
} catch (IllegalBlockSizeException e) {
throw new EncFSCorruptDataException(e);
} catch (BadPaddingException e) {
throw new EncFSCorruptDataException(e);
}
return cipherVolKeyData;
}
// Block encoding helper to do padding
static byte[] getBytesForBlockAlgorithm(String curPath) {
byte[] encodeBytes;// Only pad for block mode
int padLen = 16-(curPath.length()%16);
if (padLen==0) {
padLen = 16;
}
encodeBytes = new byte[curPath.length()+padLen];
for (int i = 0; i<curPath.length(); i++) {
encodeBytes[i] = curPath.getBytes()[i];
}
// Pad to the nearest 16 bytes, add a full block if needed
for (int i = 0; i<padLen; i++) {
encodeBytes[curPath.length()+i] = (byte) padLen;
}
return encodeBytes;
}
/**
* Decode the given fileName under the given volume and volume path
*
* @param volume
* Volume hosting the file
* @param fileName
* Encrypted file name
* @param volumePath
* Cleartext path of the file in the volume
*
* @return Decrypted file name
*
* @throws EncFSCorruptDataException
* Corrupt data in input name
* @throws EncFSChecksumException
* File checksum mismatch
*/
public static String decodeName(EncFSVolume volume, String fileName,
String volumePath) throws EncFSCorruptDataException,
EncFSChecksumException {
EncFSFilenameEncryptionAlgorithm algorithm = volume.getConfig()
.getFilenameAlgorithm();
switch (algorithm) {
case NULL:
return new NullFilenameDecryptionStrategy(volume, volumePath)
.decrypt(fileName);
case BLOCK:
return new BlockFilenameDecryptionStrategy(volume, volumePath)
.decrypt(fileName);
case STREAM:
return new StreamFilenameDecryptionStrategy(volume, volumePath)
.decrypt(fileName);
default:
throw new IllegalStateException("not implemented:"+algorithm);
}
}
/**
* Encode the given fileName under the given volume and volume path
*
* @param volume
* Volume hosting the file
* @param fileName
* Cleartext file name
* @param volumePath
* Cleartext path of the file in the volume
*
* @return Encrypted file name
*
* @throws EncFSCorruptDataException
* Corrupt data in config file
*/
public static String encodeName(EncFSVolume volume, String fileName,
String volumePath) throws EncFSCorruptDataException {
EncFSFilenameEncryptionAlgorithm algorithm = volume.getConfig()
.getFilenameAlgorithm();
switch (algorithm) {
case NULL:
return new NullFilenameEncryptionStrategy(volume, volumePath)
.encrypt(fileName);
case BLOCK:
return new BlockFilenameEncryptionStrategy(volume, volumePath)
.encrypt(fileName);
case STREAM:
return new StreamFilenameEncryptionStrategy(volume, volumePath)
.encrypt(fileName);
default:
throw new IllegalStateException("not implemented:"+algorithm);
}
}
/**
* Encode a given path under the given volume and volume path
*
* @param volume
* Volume hosting the path
* @param pathName
* Cleartext name of the path to encode (relative to volumePath)
* @param volumePath
* Cleartext volume path containing the path to encode
* @return Encrypted path
*/
public static String encodePath(EncFSVolume volume, String pathName,
String volumePath) throws EncFSCorruptDataException {
String[] pathParts = pathName.split(EncFSVolume.PATH_SEPARATOR);
String tmpVolumePath = volumePath;
String result = "";
if (pathName.startsWith(EncFSVolume.PATH_SEPARATOR)) {
result += EncFSVolume.PATH_SEPARATOR;
}
for (String pathPart : pathParts) {
// Check that we have a valid pathPart (to handle cases of // in the
// path)
if (pathPart.length()>0) {
String toEncFileName = EncFSCrypto.encodeName(volume, pathPart,
tmpVolumePath);
if (result.length()>0
&&!result.endsWith(EncFSVolume.PATH_SEPARATOR)) {
result += EncFSVolume.PATH_SEPARATOR;
}
result += toEncFileName;
if (!tmpVolumePath.endsWith(EncFSVolume.PATH_SEPARATOR)) {
tmpVolumePath += EncFSVolume.PATH_SEPARATOR;
}
tmpVolumePath += pathPart;
}
}
return result;
}
// Compute 64-bit MAC over the given input bytes
static byte[] mac64(Mac mac, byte[] input, int inputOffset) {
return mac64(mac, input, inputOffset, input.length-inputOffset);
}
// Compute 64-bit MAC over the given input bytes
static byte[] mac64(Mac mac, byte[] input, int inputOffset, int inputLen) {
mac.reset();
mac.update(input, inputOffset, inputLen);
byte[] macResult = mac.doFinal();
byte[] mac64 = new byte[8];
for (int i = 0; i<19; i++) // Note the 19 not 20
{
mac64[i%8] ^= macResult[i];
}
return mac64;
}
// Compute 64-bit MAC
private static byte[] mac64(Mac mac, byte[] input) {
byte[] macResult = mac.doFinal(input);
byte[] mac64 = new byte[8];
for (int i = 0; i<19; i++) // Note the 19 not 20
{
mac64[i%8] ^= macResult[i];
}
return mac64;
}
// Compute 32-bit MAC
private static byte[] mac32(Mac mac, byte[] input) {
byte[] mac64 = mac64(mac, input);
byte[] mac32 = new byte[4];
mac32[0] = (byte) (mac64[4]^mac64[0]);
mac32[1] = (byte) (mac64[5]^mac64[1]);
mac32[2] = (byte) (mac64[6]^mac64[2]);
mac32[3] = (byte) (mac64[7]^mac64[3]);
return mac32;
}
// Compute 16-bit MAC
static byte[] mac16(Mac mac, byte[] input) {
byte[] mac32 = mac32(mac, input);
byte[] mac16 = new byte[2];
mac16[0] = (byte) (mac32[2]^mac32[0]);
mac16[1] = (byte) (mac32[3]^mac32[1]);
return mac16;
}
// Compute 64-bit MAC and update chainedIv
static byte[] mac64(Mac mac, byte[] input, byte[] chainedIv) {
byte[] concat = new byte[input.length+chainedIv.length];
System.arraycopy(input, 0, concat, 0, input.length);
for (int i = input.length; i<input.length+chainedIv.length; i++) {
concat[i] = chainedIv[7-(i-input.length)];
}
byte[] macResult = mac.doFinal(concat);
byte[] mac64 = new byte[8];
for (int i = 0; i<19; i++) // Note the 19 not 20
{
mac64[i%8] ^= macResult[i];
}
if (chainedIv.length>0) {
// Propagate the result as the new chained IV
System.arraycopy(mac64, 0, chainedIv, 0, 8);
}
return mac64;
}
// Compute 32-bit MAC and update chainedIv
static byte[] mac32(Mac mac, byte[] input, byte[] chainedIv) {
byte[] mac64 = mac64(mac, input, chainedIv);
byte[] mac32 = new byte[4];
mac32[0] = (byte) (mac64[4]^mac64[0]);
mac32[1] = (byte) (mac64[5]^mac64[1]);
mac32[2] = (byte) (mac64[6]^mac64[2]);
mac32[3] = (byte) (mac64[7]^mac64[3]);
return mac32;
}
// Compute 16-bit MAC and update chainedIv
static byte[] mac16(Mac mac, byte[] input, byte[] chainedIv) {
byte[] mac32 = mac32(mac, input, chainedIv);
byte[] mac16 = new byte[2];
mac16[0] = (byte) (mac32[2]^mac32[0]);
mac16[1] = (byte) (mac32[3]^mac32[1]);
return mac16;
}
// Reverse the "shuffle bytes" transformation
static void unshuffleBytes(byte[] input) {
for (int i = (input.length-1); i>0; i--) {
// Note size - 1
input[i] ^= input[i-1];
}
}
// Apply the "shuffle bytes" transformation
static void shuffleBytes(byte[] buf) {
int size = buf.length;
for (int i = 0; i<size-1; ++i) {
buf[i+1] ^= buf[i];
}
}
// Flip the given byte input stream
static byte[] flipBytes(byte[] input) {
byte[] result = new byte[input.length];
int offset = 0;
int bytesLeft = input.length;
while (bytesLeft>0) {
int toFlip = Math.min(64, bytesLeft);
for (int i = 0; i<toFlip; i++) {
result[offset+i] = input[offset+toFlip-i-1];
}
bytesLeft -= toFlip;
offset += toFlip;
}
return result;
}
// Increment the given IV seed by one
static byte[] incrementIvSeedByOne(byte[] ivSeed)
throws EncFSUnsupportedException {
if (ivSeed.length==4) {
return EncFSUtil.convertIntToByteArrayBigEndian(EncFSUtil
.convertBigEndianByteArrayToInt(ivSeed)+1);
} else if (ivSeed.length==8) {
return EncFSUtil.convertLongToByteArrayBigEndian(EncFSUtil
.convertByteArrayToLong(ivSeed)+1);
} else {
throw new EncFSUnsupportedException("Unsupported IV length");
}
}
// Compute file IV
static byte[] computeFileIV(byte[] chainIv, byte[] macBytes) {
byte[] fileIv = new byte[8];
for (int i = 0; i<8; i++) {
fileIv[i] = (byte) (macBytes[i]^chainIv[i]);
}
return fileIv;
}
// Return first two bytes of a given 8 byte sequence
static byte[] getMacBytes(byte[] bytes) {
// TODO: make sure its multiple of 16
byte[] macBytes = new byte[8];
macBytes[6] = bytes[0];
macBytes[7] = bytes[1];
return macBytes;
}
// Compute chained IV
static byte[] computeChainedIV(EncFSVolume volume, String volumePath,
EncFSConfig config) {
// Chained IV computation
byte[] chainIv = new byte[8];
if (config.isChainedNameIV()) {
chainIv = StreamCrypto.computeChainIv(volume, volumePath);
}
return chainIv;
}
}