How do I store encrypted files on a web server and decrypt them locally? I want to store files (images) on a public webserver and let users see them if they know a password.  The server shouldn't have the unecrypted files and the server can only serve files, not perform any server-size computation.

One thing that I know about crypto is that I know very little so I'd like a review of the following plan:

Description of the idea:

I plan to encrypt the images with AES-256, random IV, and save them as a concatenation of IV and encryption output.  Some images will have the same passwords, some won't.

To view these files, the user will type his password into an input box on a webpage and javascript will download the encrypted file and use the password that he typed in to decrypt the image and display it.

The download and decryption of images is slow and I expect it to fail if the password is wrong so I'd like to store a "password check" that will execute locally, in javascript.  My idea is to store, in a plain-text "index file", some sort of hash of the correct password.  (The index file is far faster to download than the image.)  The javascript will locally check if the hash of the password matches what's listed in the index file before downloading and displaying images.

I plan to create the index file and encrypted images in .Net and do all the javascript with crypto-js, so any algorithm that I use should be available with those.  I want to minimize download times and javascript run time if it doesn't compromise security.

Questions:


How do I expand the user's password into a valid AES key?  So far, I plan to use PBKDF2 because it's available in .Net and crypto-js.


Should I have a separate salt for each image?

If so, should I store that salt in the encrypted image with the IV/encrypted-file or in the index file?  What advantages/disadvantages are there to the options?  For instance, I could do PBKDF2 while waiting for the image to download.


How do I store the password check?  So far, I plan to use PBKDF2 for this, too, and put a random salt and PBKDF2 output in the index file.  The per-password salts would be different from the per-image salts, of course, because otherwise the AES key for each image would be in plain text in the index file!


Is there any reason that PBKDF2 would be a bad choice for both password-check and image-decrypt?

Any security mistakes that I didn't think of?  I know that downloaded javascript can't be trusted but I'll let my users decide if they want to take that risk or use https to get the javascript.


Edit, resolution: My resolution is Ilmari Karonen excellent answer.  Per valid password, I generate 128-bits with PBKDF2, each time with a new, random salt.  I store the salt per password in the index file.  Per photo, I generate a random IV and random key and encrypt.  In the photo index file, per photo, I store the photo's key, encrypted by the per-password PBKDF2.  I also store a second encryption of the photo's key, this time with the first byte of the photo's key incremented by 1 modulo 256.  That is used to check that the password, already verified as valid for some photos, is valid per-photo.  Doubling the PBKDF2 bits for authentication purposes is far slower than just attempting all the image-key decryptions and testing the results.

This is crypto, not stackoverflow, so I won't include the code here but all the encrypting is done with .Net and all the decrypting in javascript and it works.  I can provide it freely if someone wants it.
You were doing fine up to the point where you wrote "JavaScript".

Of course, JavaScript as a language is not fundamentally unusable for crypto (although, as a high-level scripting language, any crypto primitives implemented in JavaScript are likely to be rather slow and hard to secure against side channel attacks).  However, when you write "JavaScript", I assume you mean code sent by the server and executed on the client by a browser.

This security model is fundamentally flawed for two reasons:


first, if you're not using HTTPS, then anyone who can intercept the communication between the client and the server (by, say, ARP or DNS poisoning, or a rogue WiFi access point) can modify the code sent to the client and insert their own back doors; and
second, even if you do use HTTPS to secure the connection between the client and the server, the client is still blindly executing any code given to it by the server.


If your goal, as you write, is to prevent the server from learning or tampering with the data by encrypting it on the client side, then you haven't really accomplished anything of the kind: the encryption may be done on the client, but it's done using code sent by the server, and there's nothing preventing that code from storing the user's keys and passwords and hiding them among data sent to the server.

(Of course, a careful inspection of the code and the network traffic it generates might uncover such back doors, but most users will be neither able nor willing to carry out such an inspection.  And it's not enough for just one or two users to do it, since the other users have no reason to trust that the code they've been sent is the same as the code that's been sent to others.)

For more information on why this is a bad security architecture, see e.g. this post from Bruce Schneier's blog and the articles linked from it.

OK, so what should you do then?  Well, if you want to run some JavaScript (or other) code on the browser without the server having full control over it, you could make it a browser plug-in that the user can install.  This still won't be entirely secure, since the plug-in itself could be compromised, but at least it limits the attack surface a little.  (Of course, you should probably make sure the plugin isn't able to silently upgrade itself, since that would just reopen the hole completely.)

You can also try publishing cryptographic hashes (e.g. SHA-256; not MD5, that's broken) of your code to allow users to verify that it hasn't been tampered.  Of course, if I download both the code and the hashes from your site, you (or an attacker in between, if you didn't use SSL) might have tampered with both, but at least, if I was careful and paranoid enough, I could in principle look for copies of the hashes elsewhere (e.g. Google's cache) to be sure that I've got the same software that others have.

The bottom line, though, is simply that it's really hard to provide a crypto service which is secure against the potential compromise of the service provider itself.  You can either do your very best, and inform your users about the remaining holes &mdash; or you can give up, do the crypto on the server and just use SSL for transmitting the data (and maybe hint at users that they may want to encrypt their data locally first, just in case).  But please don't try to fool your users with insecure half measures like in-browser JavaScript crypto.



Ps. Rereading your question, I notice that you did address, or at least wave your hands at, this issue at the end.  Still, my point stands.  Compared with the big glaring hole of running potentially untrusted crypto code, I see no obvious flaws in any of the rest of it.

Anyway, given that PBKDF2 is (deliberately) slow, especially in JavaScript, you'll probably want to minimize the number of times a legitimate client needs to execute it.  So I'd suggest something like this:


Let $P$ be the user's password.  When the user enters it, the client hashes it into a 128-bit key $K_P = \text{PBKDF2}(P)$ (preferably salting it with some user-specific value).
When each file is encrypted, a random 128-bit key $K_F$ is generated.  The file is encrypted using AES in a suitable mode (not ECB) with the key $K_F$ and sent to the server.
The client encrypts $K_F$ with $K_P$ using AES in ECB mode (which is OK, since we're just encrypting a single 128-bit block), yielding $C_K = \text{AES}_{K_P}(K_F)$.  It also encrypts $K_F' = K_F + 1$, where $+$ denotes addition modulo $2^{128}$ (or even just modulo $2^{32}$ or $2^8$, with the remaining bits untouched), yielding $C_K' = \text{AES}_{K_P}(K_F') = \text{AES}_{K_P}(K_F + 1)$.
The client sends $C_K$ and $C_K'$ to the server, which stores them in the index.
When the client wants to decrypt a file, it first downloads the index, decrypts $C_K$ and $C_K'$ to get $K_F$ and $K_F'$, and checks that $K_F' = K_F + 1$; if not, it asks the user the confirm that the password is correct.
If the check passes, the client now knows $K_F$, and can download and decrypt the file.
If the user wants to change their password, the client just needs to recompute $C_K$ and $C_K'$ for each file and send the new values to the server; the file keys $K_F$ need not be changed, and thus the files themselves need not be re-encrypted (although it's nice if the client does provide that option too, in case the users suspects the file keys may have been compromised).