/* ****************************************************************************** * Copyright (c) 2006-2016 XMind Ltd. and others. * * This file is a part of XMind 3. XMind releases 3 and * above are dual-licensed under the Eclipse Public License (EPL), * which is available at http://www.eclipse.org/legal/epl-v10.html * and the GNU Lesser General Public License (LGPL), * which is available at http://www.gnu.org/licenses/lgpl.html * See http://www.xmind.net/license.html for details. * * Contributors: * XMind Ltd. - initial API and implementation *******************************************************************************/ /** * */ package org.xmind.core.internal.security; import static org.xmind.core.internal.dom.DOMConstants.ATTR_ALGORITHM_NAME; import static org.xmind.core.internal.dom.DOMConstants.ATTR_ITERATION_COUNT; import static org.xmind.core.internal.dom.DOMConstants.ATTR_KEY_DERIVATION_NAME; import static org.xmind.core.internal.dom.DOMConstants.ATTR_KEY_IV; import static org.xmind.core.internal.dom.DOMConstants.ATTR_KEY_SIZE; import static org.xmind.core.internal.dom.DOMConstants.ATTR_SALT; import static org.xmind.core.internal.dom.DOMConstants.TAG_ALGORITHM; import static org.xmind.core.internal.dom.DOMConstants.TAG_KEY_DERIVATION; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.Key; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.security.spec.KeySpec; import java.util.Random; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.CipherOutputStream; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; import org.xmind.core.Core; import org.xmind.core.CoreException; import org.xmind.core.IEncryptionData; import org.xmind.core.IEntryStreamNormalizer; import org.xmind.core.IFileEntry; import org.xmind.core.io.ChecksumTrackingOutputStream; import org.xmind.core.io.ChecksumVerifiedInputStream; /** * This class provides file entry encryption/decryption based on a password. * Instances of this class that have the same password are considered equal to * each other. * * @author Frank Shaka * @since 3.6.50 */ public class PasswordProtectedNormalizer implements IEntryStreamNormalizer { private static final String ALGORITHM_NAME = "AES/CBC/PKCS5Padding"; //$NON-NLS-1$ private static final String OLD37_KEY_DERIVATION_ALGORITHM_NAME = "PKCS12"; //$NON-NLS-1$ private static final String KEY_DERIVATION_ALGORITHM_NAME = "PBKDF2WithHmacSHA512"; //$NON-NLS-1$ private static final String KEY_DERIVATION_ITERATION_COUNT = "1024"; //$NON-NLS-1$ private static final String CHECKSUM_TYPE = "MD5"; //$NON-NLS-1$ private static final String KEY_DERIVATION_SIZE = "128"; //$NON-NLS-1$ /** * The randomizer */ private static Random random = null; /** * The password */ private final String password; /** * */ public PasswordProtectedNormalizer(String password) { if (password == null) throw new IllegalArgumentException("password is null"); //$NON-NLS-1$ this.password = password; } /* * (non-Javadoc) * @see org.xmind.core.IEntryStreamNormalizer#normalizeOutputStream(java.io. * OutputStream, org.xmind.core.IFileEntry) */ public OutputStream normalizeOutputStream(OutputStream stream, IFileEntry fileEntry) throws IOException, CoreException { fileEntry.deleteEncryptionData(); IEncryptionData encData = fileEntry.createEncryptionData(); encData.setAttribute(ALGORITHM_NAME, TAG_ALGORITHM, ATTR_ALGORITHM_NAME); encData.setAttribute(KEY_DERIVATION_ALGORITHM_NAME, TAG_KEY_DERIVATION, ATTR_KEY_DERIVATION_NAME); encData.setAttribute(generateSalt(), TAG_KEY_DERIVATION, ATTR_SALT); encData.setAttribute(KEY_DERIVATION_ITERATION_COUNT, TAG_KEY_DERIVATION, ATTR_ITERATION_COUNT); encData.setAttribute(KEY_DERIVATION_SIZE, TAG_KEY_DERIVATION, ATTR_KEY_SIZE); encData.setAttribute(generateIV(), TAG_KEY_DERIVATION, ATTR_KEY_IV); encData.setChecksumType(CHECKSUM_TYPE); boolean oldEncrptWay = beforeEncrpt37(encData); Cipher cipher = createCipher(true, oldEncrptWay, encData, password); OutputStream out = new CipherOutputStream(stream, cipher); if (encData.getChecksumType() != null) { out = new ChecksumTrackingOutputStream(encData, new ChecksumOutputStream(out)); } return out; } private boolean beforeEncrpt37(IEncryptionData encData) { String keyAlgoName = encData.getAttribute(TAG_KEY_DERIVATION, ATTR_KEY_DERIVATION_NAME); return OLD37_KEY_DERIVATION_ALGORITHM_NAME.equals(keyAlgoName); } /* * (non-Javadoc) * @see org.xmind.core.IEntryStreamNormalizer#normalizeInputStream(java.io. * InputStream, org.xmind.core.IFileEntry) */ public InputStream normalizeInputStream(InputStream stream, IFileEntry fileEntry) throws IOException, CoreException { IEncryptionData encData = fileEntry.getEncryptionData(); if (encData == null) return stream; boolean oldEncrptWay = beforeEncrpt37(encData); Cipher oldCipher = createCipher(false, oldEncrptWay, encData, password); InputStream in = new CipherInputStream(stream, oldCipher); if (encData.getChecksumType() != null) { in = new ChecksumVerifiedInputStream(new ChecksumInputStream(in), encData.getChecksum()); } return in; } private Cipher createCipher(boolean encrypt, boolean oldWay, IEncryptionData encData, String password) throws CoreException { checkEncryptionData(encData); Key aesKey = createKey(oldWay, encData, password); byte[] iv = getIV(encData); IvParameterSpec ivParameter = new IvParameterSpec(iv); Cipher cipher = null; try { cipher = Cipher.getInstance(ALGORITHM_NAME); } catch (NoSuchAlgorithmException e) { throw new CoreException(Core.ERROR_FAIL_INIT_CRYPTOGRAM, e); } catch (NoSuchPaddingException e) { throw new CoreException(Core.ERROR_FAIL_INIT_CRYPTOGRAM, e); } try { cipher.init(encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, aesKey, ivParameter); } catch (InvalidKeyException e) { throw new CoreException(Core.ERROR_WRONG_PASSWORD, e); } catch (InvalidAlgorithmParameterException e) { throw new CoreException(Core.ERROR_FAIL_INIT_CRYPTOGRAM, e); } return cipher; } private Key createKey(boolean old, IEncryptionData encData, String password) throws CoreException { byte[] key = old ? getOldKeyByte(encData, password) : getKeyByte(encData, password); return new SecretKeySpec(key, "AES"); //$NON-NLS-1$ } private byte[] getKeyByte(IEncryptionData encData, String password) throws CoreException { SecretKeyFactory keyFactory = null; try { keyFactory = SecretKeyFactory .getInstance(KEY_DERIVATION_ALGORITHM_NAME); } catch (NoSuchAlgorithmException e) { throw new CoreException(Core.ERROR_FAIL_INIT_CRYPTOGRAM, e); } KeySpec keySpec = new PBEKeySpec(password.toCharArray(), getSalt(encData), getIterationCount(encData), getKeySize(encData)); try { return keyFactory.generateSecret(keySpec).getEncoded(); } catch (InvalidKeySpecException e) { throw new CoreException(Core.ERROR_WRONG_PASSWORD, e); } } private byte[] getOldKeyByte(IEncryptionData encData, String password) throws CoreException { PKCS12KeyGenerator keyGen = null; try { keyGen = new PKCS12KeyGenerator(MessageDigest.getInstance("MD5")); //$NON-NLS-1$ } catch (NoSuchAlgorithmException e) { throw new CoreException(Core.ERROR_FAIL_INIT_CRYPTOGRAM, e); } byte[] pwBytes = password == null ? new byte[0] : PKCS12KeyGenerator .PKCS12PasswordToBytes(password.toCharArray()); keyGen.init(pwBytes, getSalt(encData), getIterationCount(encData)); byte[] key = keyGen.generateDerivedKey(getKeySize(encData)); return key; } private void checkEncryptionData(IEncryptionData encData) throws CoreException { String algoName = encData.getAttribute(TAG_ALGORITHM, ATTR_ALGORITHM_NAME); if (algoName == null || !ALGORITHM_NAME.equals(algoName)) throw new CoreException(Core.ERROR_FAIL_INIT_CRYPTOGRAM); String keyAlgoName = encData.getAttribute(TAG_KEY_DERIVATION, ATTR_KEY_DERIVATION_NAME); if (keyAlgoName == null || !(KEY_DERIVATION_ALGORITHM_NAME .equals(keyAlgoName) || OLD37_KEY_DERIVATION_ALGORITHM_NAME.equals(keyAlgoName))) throw new CoreException(Core.ERROR_FAIL_INIT_CRYPTOGRAM); } private int getIterationCount(IEncryptionData encData) { return encData.getIntAttribute(1024, TAG_KEY_DERIVATION, ATTR_ITERATION_COUNT); } private byte[] getSalt(IEncryptionData encData) throws CoreException { String saltString = encData.getAttribute(TAG_KEY_DERIVATION, ATTR_SALT); if (saltString == null) throw new CoreException(Core.ERROR_FAIL_INIT_CRYPTOGRAM); return Base64.base64ToByteArray(saltString); } private byte[] getIV(IEncryptionData encData) throws CoreException { String ivString = encData.getAttribute(TAG_KEY_DERIVATION, ATTR_KEY_IV); if (ivString == null) { return new byte[16]; } return Base64.base64ToByteArray(ivString); } private int getKeySize(IEncryptionData encData) throws CoreException { String keySizeString = encData.getAttribute(TAG_KEY_DERIVATION, ATTR_KEY_SIZE); if (keySizeString == null) { return Integer.parseInt(KEY_DERIVATION_SIZE); } return Integer.parseInt(keySizeString); } /* * (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (obj == this) return true; if (obj == null || !(obj instanceof PasswordProtectedNormalizer)) return false; PasswordProtectedNormalizer that = (PasswordProtectedNormalizer) obj; return this.password.equals(that.password); } /* * (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return 37 ^ password.hashCode(); } private static Random getRandom() { if (random == null) random = new Random(); return random; } private static String generateSalt() { return Base64.byteArrayToBase64(generateSaltBytes()); } private static String generateIV() { return Base64.byteArrayToBase64(generateIVBytes()); } private static byte[] generateSaltBytes() { byte[] bytes = new byte[8]; getRandom().nextBytes(bytes); return bytes; } private static byte[] generateIVBytes() { byte[] bytes = new byte[16]; getRandom().nextBytes(bytes); return bytes; } }