Building a Secure Crypto Hashing using Vue Composables

Emmanuel UfereEmmanuel Ufere
6 min read

I was researching how best I could implement a web-based encryption method without using the popular atob() function in the browser, as that can easily be decrypted by anyone without requiring any form of decryption password. That was when I came across crypto-based hashing.
I found out that cryptographic API has a lot things it handles, starting from secure password handling, data integrity, and authentication. The Web Crypto API provides powerful browser-native tools, but working with it directly is like squid games 😄(the Netflix series). At first, i was seeing a lot of things tthat I believed to be jargons because they were not making sense to me, but with time and a lot of coffee i understood it to an extent, so I will try to make it as simple as possible. Because i use vue, i have created this in a composable for simplicity sake so let’s see below what it looks like.

If you don’t know much about vue composables, you can check it up at: what is a vue compoables

Now let’s start with what a TextEncoder does.

The TextEncoder is a Web API that converts strings into binary data (specifically, UTF-8 encoded bytes).

An Example is:

const encoder = new TextEncoder()
const text = "Hello World"
const bytes = encoder.encode(text)
console.log(text)  // "Hello World" (string)
console.log(bytes) // Uint8Array [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]

The encode() method transforms each character into its UTF-8 byte representation. For example:

  • 'H' becomes 72

  • 'e' becomes 101

  • 'l' becomes 108

  • And so on...

Why Crypto Functions Need It

Cryptographic operations work with binary data, not text strings. When you want to hash something like "password123", the crypto functions need the actual bytes that represent those characters not the string itself. It’s just like we were taught in our early days of computer studies: humans understand text and readable content, while computers understand binary (basically numbers and bytes).

// Hashing a password
async function hashSHA256(input: string): Promise<string> {
    const data = encoder.encode(input)  // Convert string(human readable format) to bytes
    const hash = await crypto.subtle.digest('SHA-256', data)  // Hash the bytes
    return bufferToHex(hash)
}

// PBKDF2 key derivation
const passwordKey = await crypto.subtle.importKey(
    'raw', 
    encoder.encode(password),  // Password string → bytes
    { name: 'PBKDF2' }, 
    false, 
    ['deriveBits']
)

// HMAC computation
const keyMaterial = encoder.encode(keyStr)  // Key string → bytes
const message = encoder.encode(message)     // Message string → bytes

UTF-8 Encoding

TextEncoder specifically uses UTF-8 encoding, which:

  • Represents ASCII characters (like English letters) as single bytes

  • Uses multiple bytes for non-ASCII characters (like émojis, accented letters)

  • Is the standard encoding for web content.

const encoder = new TextEncoder()
console.log(encoder.encode("A"))       // [65]
console.log(encoder.encode("é"))       // [195, 169] (2 bytes)
console.log(encoder.encode("🚀"))      // [240, 159, 154, 128] (4 bytes)

Why Not Just Use String Methods?

You might wonder why not just convert strings to bytes manually. The reason is that UTF-8 encoding is complex:

// ❌ This doesn't work correctly for all characters
String("café").split('').map(c => c.charCodeAt(0))  // [99, 97, 102, 233]

// ✅ TextEncoder handles UTF-8 properly  
new TextEncoder().encode("café")  // [99, 97, 102, 195, 169]

The é character needs to be encoded as two bytes (195, 169) in UTF-8, which charCodeAt() can't handle correctly.

Security Importance

Using TextEncoder ensures that:

  1. Consistent encoding: The same string always produces the same bytes

  2. Proper UTF-8 handling: International characters are encoded correctly

  3. Predictable hashing: Hash results are consistent across different systems and browsers

Without proper encoding, you could get different hash results for the same logical string, breaking password verification and other security features.

Now i will try and explain what the constantTimeEqual functions does to wrap up this because it’s already getting too long. But i will drop the rest of the explanation on article two of Building a Secure Crypto Hashing using Vue Composables 2.

The constantTimeEqual function is a critical security feature that prevents timing attacks.

function constantTimeEqual(a: string, b: string): boolean {
    if (a.length !== b.length) return false
    let result = 0
    for (let i = 0; i < a.length; i++) {
        result |= a.charCodeAt(i) ^ b.charCodeAt(i)
    }
    return result === 0
}

This compares two strings character by character, but always takes the same amount of time regardless of where (or if) differences are found.

The Problem with Normal String Comparison

A naive string comparison might look like this:

function unsafeEqual(a: string, b: string): boolean {
    if (a.length !== b.length) return false
    for (let i = 0; i < a.length; i++) {
        if (a[i] !== b[i]) {
            return false  // Returns immediately when difference found
        }
    }
    return true
}

Note: This is vulnerable to timing attacks.

The issue is that this function returns false as soon as it finds the first different character, making it faster when differences occur early.

Now what do we mean by timing attack?

Timing Attack Example

Imagine an attacker trying to guess a hash:

const correctHash = "a1b2c3d4e5f6..."
const attempts = [
    "x1b2c3d4e5f6...",  // Wrong at position 0 → very fast
    "a0b2c3d4e5f6...",  // Wrong at position 1 → slightly slower  
    "a1x2c3d4e5f6...",  // Wrong at position 2 → even slower
    "a1b2x3d4e5f6..."   // Wrong at position 3 → slowest so far (lol)
]

By measuring response times, an attacker can deduce:

  • First attempt was fastest → first character is probably NOT 'x'

  • Fourth attempt was slowest → first 3 characters are probably correct

    This leaks information about the correct value

How constantTimeEqual Prevents This

The function uses bitwise operations to avoid early returns, basically prologing the duration regardless of what string was texted first:

let result = 0
for (let i = 0; i < a.length; i++) {
    result |= a.charCodeAt(i) ^ b.charCodeAt(i)
}
return result === 0

Step by Step:

  1. XOR Operation (^): Comparing each character
'a'.charCodeAt(0) ^ 'a'.charCodeAt(0)  // 97 ^ 97 = 0 (same)
'a'.charCodeAt(0) ^ 'b'.charCodeAt(0)  // 97 ^ 98 = 3 (different)
  1. OR Assignment (|=): Accumulating any differences
result = 0
result |= 0    // result = 0 (no difference found yet)
result |= 3    // result = 3 (difference found, stays non-zero)
result |= 0    // result = 3 (still non-zero)
  1. Always processes every character: The loop never breaks early

  2. Final check: result === 0 only if ALL characters matched

Real World impact? You may ask

In the above photo snippet, you can see i used it in the verifyPassword function, and why:

async function verifyPassword(
    inputPassword: string, 
    storedSaltBase64: string, 
    storedDerivedHex: string
): Promise<boolean> {
    const derived = await deriveKeyPBKDF2(inputPassword, storedSaltBase64)
    return constantTimeEqual(derived, storedDerivedHex)  // Safe comparison
}

Without constant-time comparison, an attacker could:

  1. Submit password attempts

  2. Measure response times

  3. Gradually deduce the correct password hash

  4. Eventually crack the password

Why Does This Matters?

Timing attacks are:

  • Practical: Can be executed over networks

  • Subtle: Often overlooked by developers

  • Devastating: Can reveal passwords, tokens, or other secrets

  • Real: Used in actual security breaches

The constantTimeEqual function is a simple but crucial defense that ensures your cryptographic comparisons don't leak information through timing variations.

I will stop this here for now cause i need to get another coffee and probably create a different bug to fix 😀

Bye for now.

2
Subscribe to my newsletter

Read articles from Emmanuel Ufere directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Emmanuel Ufere
Emmanuel Ufere

Emmanuel Ufere (9ice_guy) is an experienced Software Developer with over 4 years of professional expertise in delivering robust, scalable solutions across diverse technical challenges. He specializes in problem-solving complex development requirements while maintaining high code quality and project efficiency. Emmanuel is passionate about fostering collaborative relationships within the tech community, mentoring fellow developers, and contributing to open-source projects. His commitment to knowledge sharing and positive impact drives him to actively support team growth and industry advancement. Known for his reliable delivery and adaptable approach, Emmanuel thrives in challenging environments where innovative solutions are required. He consistently seeks opportunities to contribute his expertise while expanding his technical skillset through continuous learning and community engagement.