1
$\begingroup$

I want to know if my usage of the web crypto API is sensible.

I want to use it for symmetric and public-key encryption and decryption, and signatures, but as a non-expert I am scared by the warning on the MDN page:

Warning: The Web Crypto API provides a number of low-level cryptographic primitives. It's very easy to misuse them, and the pitfalls involved can be very subtle.

Even assuming you use the basic cryptographic functions correctly, secure key management and overall security system design are extremely hard to get right, and are generally the domain of specialist security experts.

Errors in security system design and implementation can make the security of the system completely ineffective.

If you're not sure you know what you are doing, you probably shouldn't be using this API.

But having read the docs and these examples, it seems to me that the following usage is sensible.

Symmetric

Generate keys

function symmetricMakeKeys() {
  return window.crypto.subtle.generateKey(
    {name: "AES-GCM", length: 128},
    false,
    ["encrypt", "decrypt"])
}

Make IV

function makeIv() {
  return window.crypto.getRandomValues(new Uint8Array(12))
}

Encrypt

function symmetricEncrypt(key, data, iv) {
  return window.crypto.subtle.encrypt({name: "AES-GCM", iv: iv}, key, data)
}

Decrypt

function symmetricDecrypt(key, data, iv) {
  return window.crypto.subtle.decrypt({name: "AES-GCM", iv: iv}, key, data)
}

Public-key

Generate keys

function publicKeyGenerateKeys() {
  return window.crypto.subtle.generateKey(
    {name: "RSA-OAEP",
     modulusLength: 2048,
     publicExponent: new Uint8Array([1, 0, 1]),
     hash: "SHA-256"},
    false,
    ["encrypt", "decrypt"])
}

Encrypt

function publicKeyEncrypt(theirPublicKey, data) {
  return window.crypto.subtle.encrypt({name: "RSA-OAEP"}, theirPublicKey, data)    
}

Decrypt

function publicKeyDecrypt(myPrivateKey, data) {
  return window.crypto.subtle.decrypt({name: "RSA-OAEP"}, myPrivateKey, data)
}

Signatures

Generate keys

function signatureGenerateKeys() {
  return window.crypto.subtle.generateKey(
    {name: "ECDSA", namedCurve: "P-256"},
    false,
    ["sign", "verify"])
}

Sign

function signatureSign(myPrivateKey, data) {
  return window.crypto.subtle.sign(
    {name: "ECDSA", hash: {name: "SHA-256"}},
    myPrivateKey,
    data)
}

Verify

function signatureVerify(theirPublicKey, signature, data) {
  return window.crypto.subtle.verify(
    {name: "ECDSA", hash: {name: "SHA-256"}},
    theirPublicKey,
    signature,
    data)
}

Edit:

After the first answer and comment, I think it is helpful if I give a bit of context about what I need this for.

I'm building a messaging website, where the users' public keys are stored with their usernames on the server. The secret keys are stored only in the browser, along with the plain text of all their messages. Loss of the browser cache causes the loss of the keys and messages.

So the point of the encryption is to protect the messages while they are at rest on the server, waiting to be picked up by the recipient.

I know that a web app cannot provide proper separation between the server and the client, and that the user is trusting the server to serve up safe Javascript and to protect the key database from malicious modification. But the encryption does provide protection if an attacker got read-only access to the server.

Edit 2:

The signatures could be used instead of session tokens.

I don't want there to be any passwords or manual account creation. The account dies when the browser cache dies. So my options are:

  1. When someone visits the site, the server generates a session key, which is dumped in a long-lived cookie on the browser, and the server stores its hash. Subsequent requests, like message uploads and downloads, will use this token.

  2. When someone visits the site for the first time, their signing keys are generated and stored in the browser, and the public key uploaded to the server and stored with their username. Subsequent requests must contain the signature of a random use-once key downloaded from the server.

There seems to be a slight advantage to (2), because although the session tokens in (1) are stored hashed, the plain-text session tokens do exist on the server between coming out of TLS and being hashed, whereas with (1), there is never any secret information existing on the server. It is only a smallish advantage though, and there is the downside that each request will require an extra round trip to get the use-once key to sign.

$\endgroup$
  • $\begingroup$ This is not a programming SE, so the question is off-topic here. That said, without knowing exactly how it's all being used the default answer has to be that you're doing something wrong. Anything in "crypto.subtle" falls under that banner. Some things like using ECDSA or RSA are suboptimal at best. And there's no reason I know of to mix RSA-OAEP and AES-GCM, pick one (AES-GCM). $\endgroup$ – SAI Peregrinus Jun 29 at 19:30
  • 1
    $\begingroup$ @SAIPeregrinus I think this is the right place to ask, because I'm really asking about correct use of crypto primitives to do some common tasks, not about programming as such. I'd love to have a more high-level API, but unfortunately it's not provided in the API. I'm not mixing AES-GCM and RSA-OAEP: I'm using AES-GCM for symmetric crypto, and RSA-OAEP for public-key. Would you be able to explain in a simple way why ECDSA or RSA are suboptimal? RSA-OAEP is the only public-key algorithm provided in the web crypto API. $\endgroup$ – 5ndG Jun 29 at 19:40
  • $\begingroup$ Congrats, that's kind of the use case that I described. How are you planning to use the signature though? $\endgroup$ – Maarten Bodewes Jun 29 at 21:39
  • $\begingroup$ @MaartenBodewes see new edit. $\endgroup$ – 5ndG Jun 29 at 22:01
  • $\begingroup$ OK, that seems reasonable as well, although I'd never send out private keys. You create them locally and send back the public key instead. $\endgroup$ – Maarten Bodewes Jun 30 at 7:32
2
$\begingroup$

The thing that makes subtle crypto almost entirely useless is the lack of key management. Although you seem to use the primitives in the correct way, the key management is not specified at all in your question.

If, for instance, you cannot trust the TLS connection, then what chance is there that the public key used for encryption is trusted? About none. However, if it the TLS connection is trusted, why would you need the public key for? Not end-to-end transport security obviously.

So is public key encryption useless? Well, no. If you have a backend system on the server that is behind a TLS offloader or reverse proxy then you can still have some benefit having sensitive data pass through that in an encrypted form. That's especially useful if you need to store the encrypted data afterwards and you are afraid that somebody steals the database (or indeed, database server).

Symmetric key encryption is useful to perform hybrid encryption in the above scenario. Or you could use it for direct user specific encryption.

Signature generation is however rather useless unless you can trust the public key that comes with the private key. And, unless you have client authentication on the TLS connection (which, generally, you have not) it requires different methods of binding the connection to the account. Anybody can create a key pair, including adversaries. If you don't know who signs the data within the session then the signature doesn't have much meaning. In other words, there is no trusted binding of the user's identity to the signature unless arranged otherwise.

Above are just some of the "subtleties" that exist. As long as there is no connection to trusted certificates subtle crypto is a very dangerous beast. And I would surmise that it would be an entirely different, equally dangerous beast if it did support access to the certificate database used for TLS connections. There is a reason why that the cert database is missing in action.

What you did is show a good knowledge of best practices for the primitives. But that's unfortunately not where the danger lies. It is the key management and the reliance on known secure algorithms in entirely the wrong setting that makes it so dangerous.

| improve this answer | |
$\endgroup$
  • $\begingroup$ Thank you for this. I've edited the question to give a bit more context about what I'm trying to do. $\endgroup$ – 5ndG Jun 29 at 21:08
1
$\begingroup$

WebCrypto provides access to ECC. You don't need RSA at all. Personally I prefer to avoid RSA whenever possible. It's slow for everything but signature verification, and the keys are large. Since you're using ECDSA for signatures anyway, and not trying for legacy system compatibility, there's no reason for RSA.

You can use ECDH to let users communicate, with no need for RSA-OAEP. Let's say user A wants to send a message to user B.
User A requests user B's public static ECDH key from the server.
User A performs ECDH using user B's public static ECDH key and their own private static ECDH key.
User A uses the resulting encryption key for AES-GCM to encrypt the message.
User A may additionally sign the message, especially if you end up with some sort of ratcheting forward-secrecy system in place.

The above protocol probably has all sorts of flaws. You might want to consider if a Noise Protocol would work.

| improve this answer | |
$\endgroup$
  • $\begingroup$ Thank you, I think that's clever. As I've been thinking about this a lot lately, I've pretty much concluded that 1) I'm going to need to make an installed app rather than a web app, and 2) I just don't have the knowledge/experience/confidence to put together my own protocol from primitives. I need a high-level API like libsodium. $\endgroup$ – 5ndG Jul 6 at 19:53
  • $\begingroup$ It's basically what NaCl/libsodium's crypto_box() functions do (static ECDH, then AEAD). And it's definitely the right choice to go for a high-level API. If you still want a web app, use github.com/jedisct1/libsodium.js instead of just the browser crypto primitives. $\endgroup$ – SAI Peregrinus Jul 7 at 1:10
  • $\begingroup$ The advantage of the Web Crypto API over something like libsodium.js is that the secret keys can be set to be unavailable from JS, which helps a bit against XSS. And libsodium.js is quite big: 250KB gzipped. $\endgroup$ – 5ndG Jul 7 at 7:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.