I was doing something like this, and discovered just how careful you need to be. I wanted to do password encryption using widely available primitives so that I could do re-keying within MySQL easily. I was already doing AES256 CTR and following the prescribed usage to the letter.
fileKey = random256Bits()
ivPerWrite = random256Bits()
//version 1 of the file
EncryptAES256CTR(fileKey, ivPerWrite, ioReader, ioWriter)
//version 2 of the file
ivPerWrite2 = random256Bits()
EncryptAES256CTR(fileKey, ivPerWrite2, ioReader, ioWriter)
The block mode chosen matters in a deep way. You need to be careful about thinking of this as mostly equivalent ciphers (that you can negotiate with your attacker in some cases!). I picked CTR because I need to seek through the cipher and pull ranges of plaintext out of the ciphertext. You can have the same key for multiple versions of the same file, but you cannot reuse the IV when you write multiple versions of it. I was careful to ensure that every time I encrypted, I did so with a new IV. The short answer to why this is ... is that the last step of CTR is xor. CTR is a one-time pad with a pseudo-random keystream. Trivial pen and paper attacks are doable if the key stream repeats. It will repeat if IV+blockNumber is used under the same key for any block of ciphertext. So I got encrypting the file right. But...
Then I needed to encrypt the keys using a password somehow. You would think this would be hard to get wrong. So if you have a randomly generated key, and you xor it with pseudo-random bits (a sha256 hash of a password), then what could go wrong?. It was encrypting a permission between a user and an object. So I salted the hash with the user id to "diversify" the keys. I hadn't included in the hash the id of the object as well because I didn't have an id yet. But I only realized a bit later that this is isomorphic to the first block of a CTR. So "salting" isn't the issue, because the attack is not going to involve brute-force tables per user. The issue is (key,iv) repeating, just like with a one-time-pad. If you encrypt the same key with the same derived key, then the derived key can cancel, allowing you to perform algebraic attacks. Trent handles alice and bob's encrypted data. Trent generates a and b by hashing its secret with the username. Trent created all random keys x and y. But one day, bob gets his hands on the encrypted data:
alice granted x (a+x)
alice granted y (b+y)
bob granted x (b+x)
bob forges a grant for y: (a+x+a+y+b+x) = (b+y)
It is not leaking any keys or cipher texts yet, but it is allowing forgeries (that an attacker would need to get to Trent's database somehow). Because neither alice nor bob know any of the keys x or y (they belong to Trent), they can't cancel out x or y to get b or a. So the sha hash that included the password that went into a and b (along with the user id), should just be replaced with a completely random IV per encrypted key. So, make it never repeat so that you can't cancel anything:
(a + x)
(a' + y)
(b + x)
((a + x) + (a' + y)
Bob can't get cancellation, because every a and b are now unique, because they are generated like: sha256(pass+x.iv)
And note that even fully secure AES256 CTR doesn't prevent forgeries (same attack!) because the last step of any CTR is xor.
(grumpyCat.jpg + happyCat.jpg)
(a + happyCat)
alice forges (a + grumpyCat.jpg) from the ciphertext.
When you use a hashed password to encrypt a file, xor with the hashed password doesn't pass the secret through the hash. It effectively does an HMAC on an iv. To also authenticate that the encryption was done by the system, a sha256(password+encryptedKey) should be done as well.
Ok, then use a mac that can be created and verified by Trent so that Trent can not be fooled by new entries put into the database:
encKey = sha256(trentKey+permission.iv) ^ fileKey
authEncKey = sha256(trentKey+encKey)
This effectively produces a signature that Trent made and Trent can verify. It is separate from Trent's encrypted copy of the key that he can decrypt when he needs to.
So, if you look at how CTR works, it appears that a hash can be substituted where the cipher goes, because it's only used in one direction. But beware! It doesn't authenticate anything unless the data being encrypted and the secret pass through it. If you "salt" the hash (or equivalently put an IV into a cipher) and xor it with small random data like a key, an IV that never repeats should go into it.
def CTR(key, iv, totalBlocks, inBlocks, outBlocks, oneWayFunc):
for i in range(0, totalBlocks):
outBlocks[i] = inBlocks[i] ^ oneWayFunc(key,iv+i)
That oneWayFunc is usually a cipher. But CTR mode does not take advantage of it being reversible. It might as well be a hash. Notably, the data itself does not pass through this function. That means that being encrypted under this key in no ways implies that it was actually performed by Trent. That will need to be a separate step.