Skip to content

Commit

Permalink
Added output stream implementation, based on work by Carl Lindberg.
Browse files Browse the repository at this point in the history
  • Loading branch information
dmjones committed Aug 21, 2014
1 parent 54898d2 commit f7739a1
Show file tree
Hide file tree
Showing 4 changed files with 486 additions and 8 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/target
/.classpath
/.profile
/.project
/.settings/
36 changes: 29 additions & 7 deletions src/main/java/org/cryptonode/jncryptor/AES256JNCryptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,36 @@ public class AES256JNCryptor implements JNCryptor {
*/
static final String HMAC_ALGORITHM = "HmacSHA256";

private static final String AES_NAME = "AES";
private static final String KEY_DERIVATION_ALGORITHM = "PBKDF2WithHmacSHA1";
private static final int PBKDF_DEFAULT_ITERATIONS = 10000;
private static final int VERSION = 3;
private static final int AES_256_KEY_SIZE = 256 / 8;
/**
* AES algorithm name.
*/
static final String AES_NAME = "AES";

/**
* The key derivation algorith name
*/
static final String KEY_DERIVATION_ALGORITHM = "PBKDF2WithHmacSHA1";

/**
* The default number of PBKDF2 iterations.
*/
static final int PBKDF_DEFAULT_ITERATIONS = 10000;

/**
* The data format version number.
*/
static final int VERSION = 3;

/**
* The size of an AES key (useful constant).
*/
static final int AES_256_KEY_SIZE = 256 / 8;

private static final int AES_BLOCK_SIZE = 16;

// Salt length exposed as package private to aid unit testing
/**
* Size of the salt (in bytes)
*/
static final int SALT_LENGTH = 8;

// SecureRandom is threadsafe
Expand Down Expand Up @@ -367,7 +389,7 @@ public byte[] encryptData(byte[] plaintext, char[] password)
* the number of bytes to return
* @return random bytes
*/
private static byte[] getSecureRandomData(int length) {
static byte[] getSecureRandomData(int length) {
byte[] result = new byte[length];
SECURE_RANDOM.nextBytes(result);
return result;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
/* Copyright 2014 Duncan Jones
*
* Licensed 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.cryptonode.jncryptor;

import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.GeneralSecurityException;

import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;

/**
* Writes RNCryptor-format (version 3) data in a stream fashion. The stream must
* be closed to properly write the data.
*/
public class AES256JNCryptorOutputStream extends OutputStream {

private CipherOutputStream cipherStream;
private MacOutputStream macOutputStream;
private boolean writtenHeader;
private final boolean passwordBased;
private byte[] encryptionSalt;
private byte[] iv;
private byte[] hmacSalt;

/**
* Creates an output stream for key-encrypted data.
*
* @param out
* the {@code OutputStream} to write the JNCryptor data to
* @param encryptionKey
* the key to encrypt with
* @param hmacKey
* the key to calculate the HMAC with
*/
public AES256JNCryptorOutputStream(OutputStream out, SecretKey encryptionKey,
SecretKey hmacKey) throws CryptorException {

Validate.notNull(out, "Output stream cannot be null.");
Validate.notNull(encryptionKey, "Encryption key cannot be null.");
Validate.notNull(hmacKey, "HMAC key cannot be null.");

byte[] iv = AES256JNCryptor
.getSecureRandomData(AES256Ciphertext.AES_BLOCK_SIZE);

passwordBased = false;
createStreams(encryptionKey, hmacKey, iv, out);
}

/**
* Creates an output stream for password-encrypted data, using a specific
* number of PBKDF iterations.
*
* @param out
* the {@code OutputStream} to write the JNCryptor data to
* @param password
* the password
* @param iterations
* the number of PBKDF iterations to perform
*/
public AES256JNCryptorOutputStream(OutputStream out, char[] password,
int iterations) throws CryptorException {

Validate.notNull(out, "Output stream cannot be null.");
Validate.notNull(password, "Password cannot be null.");
Validate.isTrue(password.length > 0, "Password cannot be empty.");

AES256JNCryptor cryptor = new AES256JNCryptor(iterations);

encryptionSalt = AES256JNCryptor
.getSecureRandomData(AES256JNCryptor.SALT_LENGTH);
SecretKey encryptionKey = cryptor.keyForPassword(password, encryptionSalt);

hmacSalt = AES256JNCryptor.getSecureRandomData(AES256JNCryptor.SALT_LENGTH);
SecretKey hmacKey = cryptor.keyForPassword(password, hmacSalt);

iv = AES256JNCryptor.getSecureRandomData(AES256Ciphertext.AES_BLOCK_SIZE);

passwordBased = true;
createStreams(encryptionKey, hmacKey, iv, out);
}

/**
* Creates an output stream for password-encrypted data.
*
* @param out
* the {@code OutputStream} to write the JNCryptor data to
* @param password
* the password
*/
public AES256JNCryptorOutputStream(OutputStream out, char[] password)
throws CryptorException {
this(out, password, AES256JNCryptor.PBKDF_DEFAULT_ITERATIONS);
}

/**
* Creates the cipher and MAC streams required,
*
* @param encryptionKey
* the encryption key
* @param hmacKey
* the HMAC key
* @param iv
* the IV
* @param out
* the output stream we are wrapping
* @throws CryptorException
*/
private void createStreams(SecretKey encryptionKey, SecretKey hmacKey,
byte[] iv, OutputStream out) throws CryptorException {

this.iv = iv;

try {
Cipher cipher = Cipher.getInstance(AES256JNCryptor.AES_CIPHER_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, new IvParameterSpec(iv));

try {
Mac mac = Mac.getInstance(AES256JNCryptor.HMAC_ALGORITHM);
mac.init(hmacKey);

macOutputStream = new MacOutputStream(out, mac);
cipherStream = new CipherOutputStream(macOutputStream, cipher);

} catch (GeneralSecurityException e) {
throw new CryptorException("Failed to initialize HMac", e);
}

} catch (GeneralSecurityException e) {
throw new CryptorException("Failed to initialize AES cipher", e);
}
}

/**
* Writes the header data to the output stream.
*
* @throws IOException
*/
private void writeHeader() throws IOException {
/* Write out the header */
if (passwordBased) {
macOutputStream.write(AES256JNCryptor.VERSION);
macOutputStream.write(AES256Ciphertext.FLAG_PASSWORD);
macOutputStream.write(encryptionSalt);
macOutputStream.write(hmacSalt);
macOutputStream.write(iv);
} else {
macOutputStream.write(AES256JNCryptor.VERSION);
macOutputStream.write(0);
macOutputStream.write(iv);
}
}

@Override
public void write(int b) throws IOException {
if (!writtenHeader) {
writeHeader();
writtenHeader = true;
}
cipherStream.write(b);
}

@Override
public void write(byte[] b, int off, int len) throws IOException {
if (!writtenHeader) {
writeHeader();
writtenHeader = true;
}
cipherStream.write(b, off, len);
}

@Override
public void close() throws IOException {
cipherStream.close();
}

/**
* An output stream to update a Mac object with all bytes passed through, then
* write the Mac data to the stream upon close to complete the RNCryptor file
* format.
*/
private static class MacOutputStream extends FilterOutputStream {
private final Mac mac;

MacOutputStream(OutputStream out, Mac mac) {
super(out);
this.mac = mac;
}

@Override
public void write(int b) throws IOException {
mac.update((byte) b);
out.write(b);
}

@Override
public void write(byte[] b, int off, int len) throws IOException {
mac.update(b, off, len);
out.write(b, off, len);
}

@Override
public void close() throws IOException {
byte[] macData = mac.doFinal();
out.write(macData);
out.flush();
out.close();
}
}
}
Loading

0 comments on commit f7739a1

Please sign in to comment.