Web Crypto (Part 1)

Have you ever tried to encrypt and decrypt data on the browser? Maybe you used one of the multiple pure-js solutions out there, or maybe one of the Web Assembly ports of dedicated libraries. In this post series we’re going to explore the possibilities that Web Crypto offers us.

Preliminaries

Before starting with Web Crypto, let’s take a look at the different typed arrays and utilities we will need.

Typed Arrays

An ArrayBuffer is a generic byte array, is just an array of bytes. The issue is that you can’t manipulate those bytes, basically because an ArrayBuffer doesn’t know how you want to access those bytes, do you want to get 32 bit integers? or the individual bytes? You need to specify how you want to access those bytes. There are basically two ways to do this, the one we’re going to use the most is with typed arrays, there are multiple classes we can use, one for each kind of “view” we want over this byte array. In our case, we’re going to use Uint8Array, because it let us view the individual bytes as an 8 bit unsigned integer (0-255), which is something that we’re used to when we think about byte arrays.

Another option is to use a DataView, it’s a much more flexible class than the typed arrays because it allows us to see those bytes in different ways using only one class, and it also allows us to control the endiannes of the bytes when we write and read them. But we’re not going to use this in our examples.

So, how do I create an UInt8Array?

// Copy the bytes from the passed iterable
let array1 = Uint8Array.from( [ 1, 2, 3, 4 ] );
// A view on a buffer
let array2 = new Uint8Array( anArrayBuffer );
// 5 elements, initialized to zero
let array3 = new Uint8Array( 5 );

And there’re multiple options, I recommend you read the documentation for the class.

After we have our typed array, we can just manipulate it like any other array:

let array = new Uint8Array( 32 ); // A 32 byte array
let index = 23;
let newByte = 0x12;
array[ index ] = newByte;
array[ index ] ^= 0x56;
console.log( array[ index ] );

Text Encoding

We’re going to work a lot with text, so we need a way to convert text to and from an array buffer or typed array. This is the work of the TextEncoder and TextDecoder classes.

They’re very easy to use, and you can use different encodings. We’re going to use the default (UTF-8)

let message = 'Hello, World!';
let encoded = new TextEncoder().encode( message );
let decoded = new TextDecoder().decode( encoded );

console.log( 'Original message:', message );
console.log( 'Encoded message:', encoded );
console.log( 'Decoded message:', decoded );

Random Values

Now, to Web Crypto! We’ll start with 2 very easy to use functions, those that generate random values.

A common need we’ll have is to get cryptographically-secure random bytes. This is where the getRandomValues from the crypto object comes in hand. It receives an integer typed array and it fills it with random bytes.

let randomData = new Uint8Array( 32 );
crypto.getRandomValues( randomData );
console.log( 'Random data:', randomData );

But be careful, the following are application notes taken directly from the MDN documentation:

To guarantee enough performance, implementations are not using a truly random number generator, but they are using a pseudo-random number generator seeded with a value with enough entropy. The pseudo-random number generator algorithm (PRNG) may vary across user agents, but is suitable for cryptographic purposes.

getRandomValues() is the only member of the Crypto interface which can be used from an insecure context.

Don’t use getRandomValues() to generate encryption keys. Instead, use the generateKey() method. There are a few reasons for this; for example, getRandomValues() is not guaranteed to be running in a secure context.

There is no minimum degree of entropy mandated by the Web Cryptography specification. User agents are instead urged to provide the best entropy they can when generating random numbers, using a well-defined, efficient pseudorandom number generator built into the user agent itself, but seeded with values taken from an external source of pseudorandom numbers, such as a platform-specific random number function, the Unix /dev/urandom device, or other source of random or pseudorandom data.

MDN Documentation

The other function, we won’t use it, but for reference here it is:

let UUIDv4 = crypto.randomUUID();
console.log( 'Generated UUIDv4', UUIDv4 );

It generates a UUID v4 and returns its textual representation.

Digests

A digest (cryptographic hash) is a one-way function with some interesting properties, but to sum it up, it’s a way to calculate a finite-length byte array from an arbitrary-length input, with the property that the function is impossible to reverse (that is, you can get the original value back), and it’s very difficult to crack (to try to get the original value) or produce collisions (to get another value that produces the same output).

We can use the digest function of the SubtleCrypto interface to produce these digests. We only have the “SHA” family of digets: SHA-1 (deprecated, because it’s insecure), SHA-256, SHA-384 and SHA-512. The number indicated the amount of bits of the output (except for SHA-1, which produces 160 bits). It’s very easy to use, let’s see an example:

let message = 'This is a message';
let encodedMessage = new TextEncoder().encode( message );
let digest = await crypto.subtle.digest( 'SHA-256', encodedMessage );
console.log( "The digest is:", digest );

This is one common method to store passwords (although, these hashes are NOT recommended for password storage), using a random salt to avoid attacks like rainbow table

Let’s make a simple example with this, but first, let’s throw some utility functions to the mix:

Uint8Array.prototype.toUint8Array = function toUint8Array() {
  return this;
};

String.prototype.toUint8Array = function toUint8Array() {
  return new TextEncoder().encode( this );
};

Object.prototype.toUint8Array = function toUint8Array() {
  return new Uint8Array( this );
};

Uint8Array.prototype.concat = function concat( ...arrayConvertibles ) {
  return this.constructor.concat( this, ...arrayConvertibles );
};

Uint8Array.concat = function concat( ...arrayConvertibles ) {
  let arrays = arrayConvertibles.map( it => it.toUint8Array() );
  let length = arrays.reduce( ( total, array ) => total + array.length, 0 );
  let newArray = new this( length );
  
  let destinationIndex = 0;
  arrays.forEach( array => {
    for ( let originIndex = 0; originIndex < array.length; originIndex++ )
      newArray[ destinationIndex++ ] = array[ originIndex ];
  } );

  return newArray;
};

Uint8Array.random = function random( length ) {
  let array = new this( length );
  crypto.getRandomValues( array );
  return array;
};

Uint8Array.prototype.equals = function equals( anArrayConvertible ) {
  let array = anArrayConvertible.toUint8Array();
  if ( this.length !== array.length )
    return false;
  for ( let i = 0; i < this.length; i++ ) {
    if ( this[ i ] !== array[ i ] )
      return false;
  }
  return true;
};

ArrayBuffer.prototype.equals = function equals( anArrayConvertible ) {
  return this.toUint8Array().equals( anArrayConvertible );
};

Now we are ready to calculate some hashes:

async function hashPassword( password ) {
  let randomSalt = Uint8Array.random( 16 );
  let encodedPassword = password.toUint8Array();
  let dataToDigest = randomSalt.concat( encodedPassword );
  let digest = await crypto.subtle.digest( 'SHA-256', dataToDigest );
  return randomSalt.concat( digest );
}

async function verifyPassword( password, hash ) {
  let randomSalt = hash.slice( 0, 16 );
  let expectedDigest = hash.slice( 16 );
  let encodedPassword = password.toUint8Array();
  let dataToDigest = randomSalt.concat( encodedPassword );
  let digest = await crypto.subtle.digest( 'SHA-256', dataToDigest );

  return expectedDigest.equals( digest );
}

let hash = await hashPassword( 'my super password' );
if ( await verifyPassword( 'my super password', hash ) )
  console.log( 'Password matches' );
else
  console.log( 'Wrong password' );

So we can store the hash (with its random salt) instead of the password, and we can use that to verify, at a later time, if the introduced password is correct or not. VERY IMPORTANT: This is just a simple example of using the digest function, is not what you should be using, there are special hash functions for passwords, you should be using that. This is just a simple example. Also note that there’re multiple ways to combine the salt with the password, and some ways might be weaker than others, so don’t do this in a production system.

HMAC

A hash-based message authentication code is a function that uses a cryptographic hash/digest function to “sign” a piece of data. It works by hashing the data and a secret together, so, if you have the secret, you can verify that the message hasn’t been tampered with by calculating and comparing the hash.

We could implement our own HMAC following the specification on the RFC 2104 , but we better not! Let’s not roll our own crypto. So, let’s make some functions to check messages:

async function hmac( message, key ) {
  let encodedMessage = message.toUint8Array();
  let importedKey = await crypto.subtle.importKey( 'raw', key.toUint8Array(), { name: 'HMAC', hash: 'SHA-256' }, [ false ], [ 'sign' ] );
  return crypto.subtle.sign( 'HMAC', importedKey, message.toUint8Array() );
}

async function verifyHmac( message, key, signature ) {
  let actualSignature = await hmac( message, key );
  return signature.equals( actualSignature );
}

let message = 'Hello, this is my message';
let key = 'this is my password';
let signature = await hmac( message, key );
if ( await verifyHmac( message, key, signature ) )
  console.log( 'Signature is OK' );
else
  console.log( 'Signature is INVALID' );

Si, here we used the sign function that is a little bit more complicated than the digest function because it can work with different signing algorithms. The main complication is that it requires a “key” in a way that the Web Crypto API can use it. For this we used the importKey function, we’re basically telling this function that we want to import the RAW data of the key, and that we’re going to use this key for an HMAC using the SHA-256 Hash function, and we’re using it to “sign” data, not to “verify” data.

We’re perfoming the verification manually, but we could also use the verify function like this:

async function verifyHmac( message, key, signature ) {
  let encodedMessage = message.toUint8Array();
  let importedKey = await crypto.subtle.importKey( 'raw', key.toUint8Array(), { name: 'HMAC', hash: 'SHA-256' }, [ false ], [ 'verify' ] );
  return crypto.subtle.verify( 'HMAC', importedKey, signature, message.toUint8Array() );
}

It should work exactly the same (note the change from 'sign' to 'verify' in the uses of the key), because signature verification is done like that, the signature is calculated and compared with the expected signature.

Now we can verify that a message hasn’t been altered if the emissor and the receptor of the message share the same secret (and an attacker doesn’t know that secret).

We’ll talk more about keys, signatures and encryption in later posts.

800 450 Ezequiel Aguerre

What you’ll get

  • A prototype of the product to be built reflecting the product’s vision.
  • The roadmap for development.

What we’ll be
working on

  • Business Analysis and Planning
  • Market and audience analysis
  • Users analysis: interviews & surveys
  • Value proposition analysis & definition
  • Prototyping
  • User testings
  • Roadmap definition

What you’ll get:

Obviously, what you came for!
An amazing, cool, trendy, app web
or any kind of digital product.

What we’ll be working on:

  • Product Research & Strategy
  • Marketing & Growth strategy
  • Frontend and backend development
  • UX/UI Design, Research & testing
  • QA & QC
  • Naming & Branding

What you’ll get:

An appealing and unique website with smooth usability that your audience will love to engage with.

What we’ll be

working on:

  • Brand analysis and goals definition
  • Wireframing
  • Look & Feel application
  • Web Development

Swish 365

Swish’s founders came to us to build an app so that parents could book their children’s basketball sessions in a simple and fast way. Together we developed a mobile app and web app that also allows them to have visibility on the progress and results of their children.

Services offered
Product Discovery
Product Development

Technology
Mobile app for IOS and
Android App + Web App

iOS App
Android App

i

Privacy Preferences

When you visit our website, it may store information through your browser from specific services, usually in the form of cookies. Here you can change your Privacy preferences. It is worth noting that blocking some types of cookies may impact your experience on our website and the services we are able to offer.

For performance and security reasons we use Cloudflare
required
Our website uses cookies, mainly from 3rd party services. Define your Privacy Preferences and/or agree to our use of cookies.