Decrypting and encrypting is very complicated, but why does it need to be this way? Would it be possible to simply have an encrypt function that takes in an encryption key (that should be minimum at the size of a UUID, 128 bit), and then a decrypt function where you send in the same key to decrypt?
Example:
val encryptedString = "My string".encrypt(encryptionKey)
println(encryptedString.decrypt(encryptionKey)
The same crypto algo will be used everywhere, but is that a problem as long as the encryption key is secret and strong? An exception can be thrown if the encryption key is too short.
Encryption is complicated for a reason. There are many different encryption algorithms, many of them have multiple different configuration parameters, we need to setup padding, initialization vectors and stuff. Of course, we could pick some defaults, but there is really no one size fits all here. If you need to use exactly the same encryption in many places across your project, you can create such extensions by yourself.
Why is it not a one size fits all? Isn’t the whole point that it gets serialized to a string in a way that is not possible to decrypt without the key? How can other parameters be necessary? I’m not a crypto expert, and don’t know anything about vectors, initialization or padding, so maybe it’s better that some experts decide some reasonable parameters and then we can use those default functions?
For once, today a 512 bit key is considered an “age-of-universe” secure but tomorrow it’ll be hacked in two hours on a quantum computer. This has already happened multiple times (remember md5 hash?).
Yeah, but wouldn’t it then just be a matter of increasing the size of the encryption key that is passed to the encrypt/decrypt functions? Isn’t it just the size of the key that decides how easy it is to crack?
Well, cryptography is really one of the most complicated topics in the whole IT, and one of the hardest to get right. It is not possible to explain it all in a few words on a discussion forum You assumed a very narrow subset of all use cases for encryption, but in reality there are much more than that. But ok, let’s provide some key points:
As @jacek.s.gajek said, what is considered secure today, may be not secure in a 5 years from now. We constantly change algorithms. This is not only about increasing key sizes, we change whole algorithms or their parameters to keep up with the security best practices. And such builtin encrypt()/decrypt() extension couldn’t easily update them, because that would be a backward incompatible change.
If you encrypt/decrypt to use the data with anything else than your own application, you need to use exactly the same algorithm and config as the other party.
You may need to encrypt and decrypt using the same key (symmetric encryption) or using a pair of different keys (asymmetric encryption).
You may need the resulting ciphertext to be of the same length as the plaintext (stream ciphers) or you can accept growing it by a few bytes (block ciphers) (not exactly true).
You may need to balance between the performance and the security.
As a good practice, we should encrypt the data using a random nonce/IV (initialization vector) and then we need to store this IV together with the encrypted data. So this is not as simple as provide a key and the plaintext, store the ciphertext and that’s it. Technically, encrypt()/decrypt() could invent their own format for storing the ciphertext with the IV and generate the IV automatically, but again, this way stdlib introduces more and more custom behavior to the encryption algorithm which may be not exactly what the user needs.
It is not like it can’t be done. But usually, languages like Java or Kotlin don’t provide features like compress(), hash(), encrypt(), but more like gzip(), sha1()… aes_128_cbc_pkcs7(). The main difference is that for compressing or hashing usually there aren’t too many parameters that user may need to customize and that would affect the resulting data. It is much different with encryption. And creating an utility class that performs encryption/decryption with exactly what we need is pretty simple and then we can reuse the code.
(And that’s before we get onto key management, key exchange, entropy generation, and a host of other stuff that I understand even less of than you do!)
The problem with cryptography is that it’s very easy to come up with a scheme or program that seems to work securely, but has flaws that make it easier (or, in some cases, trivial) to defeat. In fact, cryptography may be the area of software development with the largest gap between the apparently-correct and the actually-correct. (Sometimes it takes experts years to discover flaws in publicly-available algorithms — but once discovered, flaws can be exploited by the bad guys very quickly.)
That’s why the usual advice is: Don’t Try To Do It Yourself!
There’s some really good replies from people on why this specific topic of encryption is hard to standardize.
I’d like to add that a lot of the requests to “please add ___ into the stdlib>” often do have decent default solutions or libraries but still shouldn’t be added to the stdlib.
For example, serialization, coroutines, and HTTP are all things you could try to standardize and include in your stdlib. You could bloat your stdlib with these tools and there may be some special cases to do so.
The stdlib is held to a higher standard of compatibility, slower development pace and degradation cycles. The stdlib versions with the language versions (so Kotlin 1.7.2 also means we get a stdlib 1.7.2). Kotlin values a slim stdlib and follows the convention of having “x” libraries (kotlinx) that are nearly standardized.
This is meant for the health of the stdlib to keep it flexible, portable, and decoupled from these tools. Libraries like kotlinx.serialization, kotlinx.coroutines, and ktor benefit in that they can version and grow at a faster pace. Users can be selective and upgrade independently.