惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Full Disclosure
WordPress大学
WordPress大学
小众软件
小众软件
Cloudbric
Cloudbric
AWS News Blog
AWS News Blog
腾讯CDC
量子位
人人都是产品经理
人人都是产品经理
大猫的无限游戏
大猫的无限游戏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
Vulnerabilities – Threatpost
Scott Helme
Scott Helme
Hugging Face - Blog
Hugging Face - Blog
博客园_首页
C
CXSECURITY Database RSS Feed - CXSecurity.com
The Hacker News
The Hacker News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
IT之家
IT之家
Jina AI
Jina AI
Attack and Defense Labs
Attack and Defense Labs
S
SegmentFault 最新的问题
Simon Willison's Weblog
Simon Willison's Weblog
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
T
Tailwind CSS Blog
Last Week in AI
Last Week in AI
博客园 - 【当耐特】
Google Online Security Blog
Google Online Security Blog
美团技术团队
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
Visual Studio Blog
罗磊的独立博客
L
LINUX DO - 最新话题
博客园 - Franky
博客园 - 叶小钗
Apple Machine Learning Research
Apple Machine Learning Research
The Last Watchdog
The Last Watchdog
J
Java Code Geeks
AI
AI
C
Cisco Blogs
酷 壳 – CoolShell
酷 壳 – CoolShell
C
Cyber Attacks, Cyber Crime and Cyber Security
Cisco Talos Blog
Cisco Talos Blog
博客园 - 三生石上(FineUI控件)
雷峰网
雷峰网
Help Net Security
Help Net Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
云风的 BLOG
云风的 BLOG
I
Intezer
S
Securelist

Little Things

Madeira — May 2025 | Little Things OpenClaw Agent Deploys OpenClaw to a Phone via SSH | Little Things From Failed Simulation to Daily Copilot - My OpenClaw Setup | Little Things Building Agentic Workflows for my HomeLab | Little Things Starting a photography series | Little Things Exploring the browser rendering process | Little Things Managing Interactive Demos in MDX | Little Things Interactive post on OKLCH color space | Little Things A Guide to animations that feels right | Little Things Interactive Story - A Journey of Choices | Little Things A Time Before Algorithms Took Over the 90's Web | Little Things Animations - Liquid background hover effect | Little Things Why does this site exist? | Little Things
No Authentication Like Button | Little Things
2024-09-22 · via Little Things

If you have a personal blogging website, you might want to add a like button to your posts. This feature can help you understand which posts resonate with your audience and give you a sense of what content is most popular. However, would you expect your users to sign in to like a post? In my site, you might have seen a tree which grows when you click on it. This is a like button that doesn’t require any authentication. And you can like a post upto 5 times. Try opening it in incognito and you wills see that it still remembers the likes. This is your unique fingerprint. This is what I am going to show you how to do in this post. Open this page in incognito and see you will get the same fingerprint. Also try other browsers and you will get the same fingerprint if the browser is using the same engine.

Why would not you want to authenticate a like button?

There are plenty of personal websites that I visit and I don’t want to sign in to like a post. I just want to show my appreciation for the content. I don’t want to create an account or sign in with my social media account. I just want to click a button and move on. And its ok if its a little less secure. It doesn’t have to be perfect. It just has to work and avoid spam. This is the same experience that I want to provide to users visiting my site. I want them to like a post without any friction. And its ok if the same user who liked the post before came after few weeks and liked the post again. Personal websites are not big enough to worry about this.

What options do we have?

The first thought that comes to make this work is with cookie. But its extremely flawed. You can easily clear your cookies and like a post again. Not just that, I dont want to use cookies in the first place.

The next option could be validating the IP address. But this is also flawed. You can easily change your IP address and like a post again. Not just that if you are browsing a site from within a company’s network, all the users will have the same IP address and they can like a post only once. So it wont act as a unique identifier. Not just that, whatever internet I latch onto, I will have a different IP address. So I can like a post again which could be spam.

We can use the user agent string and the screen resolution to create a unique identifier. However, you can imagine you may get duplicates. For example, if you are using a public computer, you will have the same user agent string and screen resolution.

The next option could be trying to figure out a device id. But browsers wont make this possible as it would be a privacy concern and websites which are heavily into tracking will exploit this. However, we can take advantage of inconsistencies that are created by the browser during processing. One such inconsistency is the OfflineAudioContext API. This API is used to create audio processing graphs. We can use this API to create a unique identifier for the user. This is what I have used in my site.

Audio Fingerprinting

One of the most interesting aspects of browser fingerprinting is audio fingerprinting. This method utilizes the Web Audio API to generate a unique audio signature based on mathematical waveforms. The beauty of audio fingerprinting lies in its uniqueness and stability. The audio signals generated through this technique can provide a reliable fingerprint that varies between browsers with different engine while remaining consistent across sessions.

The Web Audio API: Building Blocks

Before we dig deeper into audio fingerprinting, let’s briefly explore the Web Audio API—the backbone of this technique. This powerful system allows developers to manipulate audio through a series of interconnected nodes, creating complex audio graphs within an AudioContext.

  1. AudioContext: This is the core of the audio system, managing audio node creation and processing execution.

  2. AudioBuffer: This component stores audio snippets in memory, represented in LPCM (Linear Pulse Code Modulation) format.

  3. Oscillator: An oscillator serves as a source of sound, generating periodic waveforms like sine, square, or triangular waves. The default frequency is 440 Hz, corresponding to the musical note A4.

  4. DynamicsCompressorNode: This node reduces the volume of the loudest parts of the signal, preventing distortion. It has several properties that we can manipulate for variability across different browsers.

    • Threshold: The volume level above which compression occurs.
    • Knee: The smooth transition range above the threshold.
    • Ratio: The input-output change ratio in decibels.
    • Reduction: The current amount of gain reduction.
    • Attack and Release: Time parameters for gain adjustment.

Each browser’s implementation of the Web Audio API introduces subtle differences in how audio is processed, influenced by factors like CPU architecture and operating system variations. Also, the dynamics compressor adds another layer of complexity by modifying audio signals based on adjustable parameters like threshold and ratio, which can vary between browsers.

This combination results in distinct audio waveforms for each user’s environment, making the generated audio fingerprints unique and stable identifiers that can persist across different sessions and even in incognito modes.

Creating an Audio Fingerprint

Now that we have the basic concepts, let’s dig into the audio fingerprinting process. We start by declaring some properties to store our audio context, oscillator, and compressor nodes, as well as the fingerprint itself. I wrote a class but you can choose to use a function as well.

class AudioFingerprint {
    #audioContext: OfflineAudioContext | null = null;
    #currentTime: number | null = null;
    #oscillatorNode: OscillatorNode | null = null;
    #compressorNode: DynamicsCompressorNode | null = null;
    #fingerprint: string | null = null;
    #onCompleteCallback: ((fingerprint: string) => void) | null = null;
}

# makes a property or a method private.

Next, we define our AudioContext. Safari supports a prefixed version, so we account for that.

#createAudioContext(): void {
    //@ts-ignore
    const OfflineContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
    this.#audioContext = new OfflineContext(1, 5000, 44100);
}

Next, we create an oscillator instance that generates a triangular waveform at 1,000 Hz:

#createOscillatorNode(): void {
  if (this.#audioContext) {
    this.#oscillatorNode = this.#audioContext.createOscillator();
    this.#oscillatorNode.type = 'triangle';
    this.#oscillatorNode.frequency.setValueAtTime(10000, this.#currentTime || 0);
  }
}

We then introduce a compressor to add variability to our signal:

#createCompressorNode(): void {
  this.#compressorNode = this.#audioContext.createDynamicsCompressor();

  this.#setCompressorValue(this.#compressorNode.threshold, -50);
  this.#setCompressorValue(this.#compressorNode.knee, 40);
  this.#setCompressorValue(this.#compressorNode.ratio, 12);
  this.#setCompressorValue(this.#compressorNode.attack, 0);
  this.#setCompressorValue(this.#compressorNode.release, 0.25);
}

Next, we connect the nodes and generate the audio fingerprint:

createFingerPrint(callback: (fingerprint: string) => void, debug: boolean = false): void {
  this.#onCompleteCallback = callback;
  try {
    this.#initializeAudioContext();

    if (this.#oscillatorNode && this.#compressorNode && this.#audioContext) {
      this.#oscillatorNode.connect(this.#compressorNode);
      this.#compressorNode.connect(this.#audioContext.destination);

      this.#oscillatorNode.start(0);
      this.#audioContext.startRendering();

      this.#audioContext.oncomplete = this.#handleAudioComplete.bind(this);
    }
  } catch (error) {
    if (debug) {
        console.error('Audio Fingerprinting Error:', error);
    }
  }
}

The samples array holds our audio data, and we derive a fingerprint from this by summing the absolute values of the sample array:

// reference - https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript#answer-52171480
export const calculateHash = function (str, seed = 0) {
  // `seed` is the only optional argument, so it can be the second parameter
};

console.log(calculateHash(samples));

Where to Store the Hash?

So you still need a database. But this is easy. You can register in upstash and create a redis database. You will be provided with a connection string which you can use to connect to the redis db. The data structure is simple where the key is the post slug and value can be a map of the hash and the count of likes. Something like this:

{
  "post-xx-slug": {
    "userLikes": {
      "hash-1": 4,
      "hash-2": 2,
      "hash-3": 3
    },
    "count": 9
  }
}

Complete code

import { calculateHash } from './crypt';

class AudioFingerprint {
  // private
  #audioContext: OfflineAudioContext | null = null;
  #currentTime: number | null = null;
  #oscillatorNode: OscillatorNode | null = null;
  #compressorNode: DynamicsCompressorNode | null = null;
  #fingerprint: string | null = null;
  #onCompleteCallback: ((fingerprint: string) => void) | null = null;

  createFingerPrint(
    callback: (fingerprint: string) => void,
    debug: boolean = false
  ): void {
    this.#onCompleteCallback = callback;

    try {
      this.#initializeAudioContext();

      if (this.#oscillatorNode && this.#compressorNode && this.#audioContext) {
        this.#oscillatorNode.connect(this.#compressorNode);
        this.#compressorNode.connect(this.#audioContext.destination);

        this.#oscillatorNode.start(0);
        this.#audioContext.startRendering();

        this.#audioContext.oncomplete = this.#handleAudioComplete.bind(this);
      }
    } catch (error) {
      if (debug) {
        console.error('Audio Fingerprinting Error:', error);
      }
    }
  }

  #initializeAudioContext(): void {
    this.#createAudioContext();
    if (this.#audioContext) {
      this.#currentTime = this.#audioContext.currentTime;
      this.#createOscillatorNode();
      this.#createCompressorNode();
    }
  }

  #createAudioContext(): void {
    //@ts-ignore
    const OfflineContext =
      window.OfflineAudioContext || window.webkitOfflineAudioContext;
    this.#audioContext = new OfflineContext(1, 5000, 44100);
  }

  #createOscillatorNode(): void {
    if (this.#audioContext) {
      this.#oscillatorNode = this.#audioContext.createOscillator();
      this.#oscillatorNode.type = 'triangle';
      this.#oscillatorNode.frequency.setValueAtTime(
        10000,
        this.#currentTime || 0
      );
    }
  }

  #createCompressorNode(): void {
    if (this.#audioContext) {
      this.#compressorNode = this.#audioContext.createDynamicsCompressor();

      this.#setCompressorValue(this.#compressorNode.threshold, -50);
      this.#setCompressorValue(this.#compressorNode.knee, 40);
      this.#setCompressorValue(this.#compressorNode.ratio, 12);
      this.#setCompressorValue(this.#compressorNode.attack, 0);
      this.#setCompressorValue(this.#compressorNode.release, 0.25);
    }
  }

  #setCompressorValue(param: AudioParam, value: number): void {
    param.setValueAtTime(value, this.#audioContext!.currentTime);
  }

  #handleAudioComplete(event: OfflineAudioCompletionEvent): void {
    this.#generateFingerprint(event);
    if (this.#compressorNode) {
      this.#compressorNode.disconnect();
    }
  }

  #generateFingerprint(event: OfflineAudioCompletionEvent): void {
    let output = '';
    const channelData = event.renderedBuffer.getChannelData(0);

    for (let i = 4500; i < 5000; i++) {
      output += Math.abs(channelData[i]);
    }

    this.#fingerprint = output.toString();

    if (typeof this.#onCompleteCallback === 'function' && this.#fingerprint) {
      this.#onCompleteCallback(this.#fingerprint);
    }
  }
}
// Expose this function
export const getFingerPrint = async () => {
  return new Promise((resolve: (fingerprint: string) => void) => {
    const audioFingerprint = new AudioFingerprint();
    audioFingerprint.createFingerPrint(async (fingerprint: string) => {
      fingerprint = window.btoa(fingerprint as string);
      resolve(calculateHash(fingerprint, 0) as unknown as string);
    }, true);
  });
};

Redis Query

You will need 2 functions for this.

One is to fetch all the likes of a particular post and the likes of the current user.

const data = await redis.get(slug);
const userLikes = data.totalLikes[userId];

And the other one is to post a like. You should do some checks before saving. If you are allowing users to like more than once, then you should handle that carefully here.

await redis.set(
  slug,
  JSON.stringify({
    count: likeData.count,
    totalLikes: { ...data?.totalLikes, [userId]: likeData.me },
  })
);

For both the above functions, you need to send the fingerprint from the client. Thats how you get the userId.

Conclusion

Audio fingerprinting is a way to track users without all the annoying pop-ups and concents. For side projects and personal websites, having that overhead to authenticate users is not worth it. It does not work unless you are a well known author in the content you produce. I have tested this in Chrome, Safari, Arc and they produce the same fingerprint. And considering the market share of these browsers, I am happy with the results. What do you think?