Reputation: 1698
EDIT 1
In the decryptFile method, the decrypt part will not output anything..
let decrypted = CryptoJS.AES.decrypt(e.target.result, CryptoJS.enc.Utf8.parse(key), {
iv: CryptoJS.enc.Utf8.parse(iv),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
EDIT 2 The link that has been given in the comments section partially solves the problem. It does encrypt and decrypt cross platform, but it is rather slow due to PBKDF2 with SHA256 hashing. I cannot find a way to only use the AES part and not the PKBDF2 part.
ORIGINAL TEXT
I am using the same key and IV for both the Java and Javascript version. I cannot decrypt a file in Javascript that has been encrypted in Java, nor can I decrypt a file in Java that has been encrypted in Javascript. I need those two to be compatible with eachother, but I cannot figure out how I am trying to decrypt a file in Javascript that was previously encrypted in Java. I have succesfully implemented decrypting and encrypting text between the two, but when I would like to for example decrypt a file that is encrypted in Java, it just won't work.
Encrypt / decrypt a file in Java:
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class Test {
private SecretKey secretKey;
private IvParameterSpec ivParameterSpec;
private String key = "ThisIsMyGreatKey";
private byte[] ivKey = "ABCDEFGHabcdefgh".getBytes();
public static void main(String[] args) {
try {
new Test().run();
} catch (Exception e) {
e.printStackTrace();
}
}
private void run() {
ivParameterSpec = new IvParameterSpec(ivKey);
secretKey = new SecretKeySpec(key.getBytes(), "AES");
encryptOrDecryptFile(Cipher.ENCRYPT_MODE,
new File("src/cactus.jpg"), new File("src/cactus-encrypted.jpg"));
}
private void encryptOrDecryptFile(int mode, File inputFile, File outputFile) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(mode, secretKey, ivParameterSpec);
// Read input
byte[] input = new byte[(int) inputFile.length()];
FileInputStream inputStream = new FileInputStream(inputFile);
inputStream.read(input);
// Encrypt and write to output
byte[] output = cipher.doFinal(input);
FileOutputStream outputStream = new FileOutputStream(outputFile);
outputStream.write(output);
inputStream.close();
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Encrypt / decrypt in Javascript
<input type="file" id="file-input" onchange="handleFile(this)">
<button onclick="useEncryptionForFile()" id="encrypt-file">Encrypt File</button>
<button onclick="useDecryptionForFile()" id="decrypt-file">Decrypt File</button>
<textarea id="output"></textarea>
<img id="example">
<script>
let key = "ThisIsMyGreatKey";
let iv = "ABCDEFGHabcdefgh";
let useEncryption, useDecryption;
let input = document.getElementById("file-input");
let output = document.getElementById("output");
let example = document.getElementById("example");
function handleFile(element) {
if (element.files && element.files[0]) {
let file = element.files[0];
if (useDecryption) {
decryptFile(file);
} else {
encryptFile(file);
}
}
}
function encryptFile(file) {
let reader = new FileReader();
reader.onload = function (e) {
let encrypted = CryptoJS.AES.encrypt(e.target.result, CryptoJS.enc.Utf8.parse(key), {
iv: CryptoJS.enc.Utf8.parse(iv),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
output.textContent = encrypted;
let a = document.createElement("a");
a.setAttribute('href', 'data:application/octet-stream,' + encrypted);
a.setAttribute('download', file.name + '.encrypted');
a.click();
};
reader.readAsDataURL(file);
}
function decryptFile(file) {
let reader = new FileReader();
reader.onload = function (e) {
let decrypted = CryptoJS.AES.decrypt(e.target.result, CryptoJS.enc.Utf8.parse(key), {
iv: CryptoJS.enc.Utf8.parse(iv),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
// Decrypted is emtpy
output.textContent = decrypted;
// Desperate try to get something working
example.src = "data:image/png;base64," + btoa(decrypted);
let a = document.createElement("a");
a.setAttribute('href', decrypted);
a.setAttribute('download', file.name.replace('encrypted', 'decrypted'));
a.click();
};
reader.readAsText(file);
}
function useEncryptionForFile() {
document.getElementById("encrypt-file").style.backgroundColor = "#757575";
document.getElementById("decrypt-file").style.backgroundColor = "#FFFFFF";
useEncryption = true;
useDecryption = false;
}
function useDecryptionForFile() {
document.getElementById("encrypt-file").style.backgroundColor = "#FFFFFF";
document.getElementById("decrypt-file").style.backgroundColor = "#757575";
useDecryption = true;
useEncryption = false;
}
</script>
I also made a Fiddle in case you'd like that more :), and the Java source can be downloaded here.
In the Java source I used a cactus.jpg as file, but any file could be used :). The cactus can be found here.
How can I decrypt the file that has been encrypted in Java? I have tried converting the blob contents to String, retrieving the data as ArrayBuffer and convert it to String, receive it as text and pass it to the decryption method, but nothing seems to work.
The library I am using in Javascript is CryptoJS, and in Java the standard Crypto libraries.
I found other similar (1,2) questions. However, I think they differ too much, as the answer of those questions does not concern this issue, but rather a small mistake.
If I've forgotten any data, please tell me.
Upvotes: 15
Views: 3335
Reputation: 50328
The link that has been given in the comments section partially solves the problem. It does encrypt and decrypt cross platform, but it is rather slow due to PBKDF2 with SHA256 hashing. I cannot find a way to only use the AES part and not the PKBDF2 part.
The purpose of PBKDF2 is to turn a user-chosen password (which is typically a variable-length text string, and rarely has more than a few dozen bits of effective entropy*) into an AES key (which must be a binary string of exactly 128, 192 or 256 bits, depending on the AES variant used, and should effectively have full entropy** if you want the cipher to be as strong as it's meant to be).
To do that, it needs to be slow; the only way to make guessing a password with, say, 30 bits of entropy as hard as guessing an AES key with 128 bits of entropy would be to make the process of converting a password into a key take as much time as 2128 - 30 = 298 AES encryptions. Of course, that's not really possible, so in practice people tend to apply only a few thousand (≈ 210) to a few billion (≈ 230) PBKDF2 iterations and hope that it's enough. Which it might be, if your iteration count is closer to the upper end of the range, if the user is smart enough and motivated enough to pick a reasonably good password (say, a random Diceware passphrase instead of abc123
or pa$$w0rd
) to begin with, and if your adversary is not the NSA.
Anyway, the point of all that is that, if you want to use password-based encryption, then you pretty much have to use PBKDF2 (or something similar). However, that does not necessarily mean that you have to run the slow key derivation function every time you encrypt or decrypt something. In fact, if you know that you'll need to encrypt or decrypt multiple files with the same password, it's much more efficient to derive the AES key from the password once, and then store the AES key in memory for as long as you need it. (Doing that securely is not trivial, either, but many crypto libraries will handle it at least reasonably well if you use their built-in key objects to store the keys, and in any case it's unlikely to be the weakest link in your application's security.)
The other option, of course, is not to use passwords at all, but simply to generate a (pseudo)random AES key (using a cryptographically secure random bit string generator) and store on all devices that need access to it. Again, of course, the hard part here is securely storing the key.
In particular, if you're doing crypto on the client side with in-browser JavaScript, then simply embedding the key in the JS code (or anywhere else on the page) will expose it to anyone with access to the browser dev console (and may also result in the key being left lying e.g. in the browser's disk cache). And of course you should be using HTTPS, since otherwise anyone e.g. eavesdropping on the client's public WiFi connection could also grab a copy of the key.
Ps. You might notice that I haven't actually included any code showing how to do plain AES encryption with PBKDF2 above. I haven't done that for two reasons:
If you don't understand the crypto primitives you're working with well enough to, say, separate the key derivation from the encryption, then you really shouldn't be writing crypto code (as anything other than a toy exercise) anyway.
That may sound harsh, but it's the reality — unlike many other subfields of programming, where you can just take a piece of code that you don't fully understand and tweak it until it works, with crypto (and with security-related code in general) you need to know what you're doing and do it right the first time.
With other types of code, something that seems to work most of the time is often good enough, and you can fix any remaining bugs as they come up. With crypto, buggy code can easily be completely insecure while looking like it's working perfectly, and you won't find out that it's broken until someone breaks it and steals all the data you worked so hard to keep secret. Or possibly only long after that has already happened.
In any case, the collection of code samples you linked to (<disclaimer>and which I have not reviewed in any detail</disclaimer>) already includes a set of methods that take binary AES keys and do not use PBKDF2. Specifically, those are the encrypt()
and decrypt()
methods (as opposed to encryptString()
and decryptString()
, which do use PBKDF2).
Note that, when calling encrypt()
and/or decrypt()
, you will need to provide the AES key in the format expected by the implementation. In general, that may depend on the underlying crypto library used by the code. The Java implementation, for example, appears to be expecting a 128 / 8 = 16 element byte[]
array. (It would be kind of nice if it could also directly take a SecretKeySpec
object, but it doesn't.) The JS WebCrypto implementation seems to want a 16-byte Uint8Array
instead. That's apparently fine for the node.js implementation as well, although it can also accept a Buffer
.
*) That is to say, a decent password cracking program, based on in-depth knowledge of common human password-picking methods and habits, will rarely take more than a few billion (≈ 230) or trillion (≈ 240) attempts to guess most human-chosen passwords. In fact, depending on how lazy or inexperienced your users are, even just testing the few thousand most common passwords may be quite effective.
**) That is, guessing the key should not be easier than guessing a perfectly randomly chosen bitstring of the same length.
Upvotes: 2
Reputation: 2308
Firstly, try sending simple encrypted text from java to javascript or vice versa and test if the code is working.
If the code is working for Simple text, i.e. , you are able to send a encrypted String from Java and successfully decrypt it in JavaScript or vice versa, then what you can do is Base64 encode the encrypted bytes/file and then transfer the text and then decode and decrypt it on other end.
If the code is not working for simple text, then you can try encrypting a simple text in javascript and java independently and check if the result are same. If not, there is some mismatch in encryption/decryption logic between java and javascript.
EDIT:
As you mentioned that the code is working for String, I have show below an example to convert file into Base64 String using apache common codec library in java.
private static String encodeFileToBase64Binary(String fileName) throws IOException {
File file = new File(fileName);
byte[] encoded = Base64.encodeBase64(FileUtils.readFileToByteArray(file));
return new String(encoded, StandardCharsets.US_ASCII);
}
Now you encrypt this String and send it to javascript. In javascript first decrypt the String and then convert it into a file object.
Eg.
function base64toFile(encodedstring,filename,mimeType){
return new File([encodedstring.arrayBuffer()],filename, {type:mimeType});
}
//Usage example:
base64toFile('aGVsbG8gd29ybGQ=', 'hello.txt', 'text/plain');
Upvotes: 3
Reputation: 9805
The problem is that you interpret the decryption result as a UTF8 string. That isn't how it works. Files are just arbitrary bytes, they don't necessarily make up a UTF8 string. The result of your decryption is correct if you just don't try to interpret it as UTF8.
Upvotes: 4