This can be easily done by putting any block encryption algorithm into CTR mode. CTR mode with a single key looks like:
ciphertext = plaintext XOR cipher(key, counter)
Where counter is initialized to your IV and incremented for each block. Decryption is exactly the same operation. As such, if you CTR-encrypt twice with two keys, you get:
ciphertext = plaintext XOR cipher(key0, counter) XOR cipher(key1, counter)
And since XOR is commutative, you can reverse it in either order.
This has the nice property that you don't need to have all keys in the same location. Consider: Alice, Bob, and Charlie are participating in a protocol in which Charlie will double encrypt data for both Alice and Bob (this protocol will assume all point-to-point communication is secured through usual SSL-like channels):
- Alice and Bob perform an authenticated Diffie-Hellman exchange to produce the IV. This IV is then sent to Charlie.
- Alice computes digest(key0, IV + ctr) for ctr = 0...number-of-ciphertext-blocks, and sends the result KS_A to Charlie
- Bob computes digest(key1, IV + ctr) for ctr = 0...number-of-ciphertext-blocks, and sends the result KS_B to Charlie
- Charlie computes KS_A XOR KS_B XOR plaintext, and sends the resulting ciphertext to both Alice and Bob.
- Alice and Bob each sign a tuple (IV, hash(ciphertext), description-of-encrypted-data). This is attached to the ciphertext.
Later, to decrypt:
- Charlie (performing the decryption) sends the signed (IV, hash(ciphertext)) tuples to each of Alice and Bob, as well as the ciphertext.
- Alice verifies his signed tuple, computes KS_A, and sends ciphertext XOR KS_A = D_A to Charlie
- Bob verifies his signed tuple, computes KS_B, and sends ciphertext XOR KS_B = D_B to Charlie
- Charlie computes KS = D_A XOR D_B = KS_A XOR KS_B
- Charlie computes plaintext = ciphertext XOR KS
The purpose of the signed tuple here and DH exchange is to ensure Alice and Bob can't be tricked into decryption the wrong stream by sending them a different IV. This may not be relevant in your usage scenario. Also, the role of Charlie may be played by Alice or Bob in a real implementation.
If you're worried about the potential security risks of CTR mode, one other option would be to use CTR-mode encryption on a session key, which in turn is used to encrypt in a more normal mode, such as CBC. That is:
sessionkey = RANDOM
IV_0 = RANDOM
IV_1 = RANDOM
enc_sessionkey = sessionkey XOR cipher(key0, IV_0) XOR cipher(key1, IV_0)
ciphertext = enc_sessionkey + IV_0 + IV_1 + cipherCBC(IV_1, sessionkey, plaintext)
Although some other posters have commented on secret sharing, this is overkill if you don't need the property that only a subset of keys are needed for decryption - ie, with secret sharing you might encrypt with three keys, but require only any two to decrypt. If you want to require all keys, secret sharing schemes aren't really necessary.