Terence Eden’s Blog 2025-03-01T22:43:25Z https://shkspr.mobi/blog/feed/atom/ WordPress https://shkspr.mobi/blog/wp-content/uploads/2023/07/cropped-avatar-32x32.jpeg @edent <![CDATA[Towards a test-suite for TOTP codes]]> https://shkspr.mobi/blog/?p=58593 2025-03-01T22:43:25Z 2025-03-02T12:34:39Z <![CDATA[Because I'm a massive nerd, I actually try to read specification documents. As I've ranted ad nauseam about the current TOTP spec being irresponsibly obsolete. The three major implementations of the spec - Google, Apple, and Yubico - all subtly disagree on how it should be implemented. Every other MFA app has their own idiosyncratic variants. The official RFC is infuriatingly vague. That's no good for a security specification. Multiple implementations are great, multiple interpretations are…]]> <![CDATA[

Because I'm a massive nerd, I actually try to read specification documents. As I've ranted ad nauseam about the current TOTP0 spec being irresponsibly obsolete.

The three major implementations of the spec - Google, Apple, and Yubico - all subtly disagree on how it should be implemented. Every other MFA app has their own idiosyncratic variants. The official RFC is infuriatingly vague. That's no good for a security specification. Multiple implementations are great, multiple interpretations are not.

So I've built a nascent test suite - you can use it to see if your favourite app can correctly implement the TOTP standard.

Screenshot showing a QR code and numeric codes.

Please do contribute tests and / or feedback.

Here's what the standard actually says - see if you can find apps which don't implement it correctly.

Background

Time-based One Time Passwords are based on HOTP - HMAC-Based One-Time Password.

HOTP uses counters; a new password is regularly generated. TOTP uses time as the counter. At the time of writing this post, there have been about 1,740,800,000 seconds since the UNIX Epoc. So a TOTP with an period of 30 seconds is on counter (1,740,800,000 ➗ 30) = 58,026,666. Every 30 seconds, that counter increments by one.

Number of digits

How many digits should your 2FA token have? Google says 6 or 8. YubiCo graciously allows 7. Why those limits? Who knows!?

The HOTP specification gives an example of 6 digits. The example generates a code of 0x50ef7f19 which, in decimal, is 1357872921. It then takes the last 6 digits to produce the code 872921.

The TOTP RFC say:

Basically, the output of the HMAC-SHA-1 calculation is truncated to obtain user-friendly values 1.2. Background

But doesn't say how far to truncate.

There's nothing I can see in the spec that prevents an implementer using all 10. The HOTP spec, however, does place a minimum requirement - but no maximum:

Implementations MUST extract a 6-digit code at a minimum and possibly 7 and 8-digit code. Depending on security requirements, Digit = 7 or more SHOULD be considered in order to extract a longer HOTP value. RFC 4226 - 5.3. Generating an HOTP Value

(As a minor point, the first digit is restricted to 0-2, so being 10 digits long isn't significantly stronger than 9 digits.)

Is a 4 digit code acceptable? The security might be weaker, but the usability is greater. Most apps will allow a one digit code to be returned. If no digits are specified, what should the default be?

Algorithm

The given algorithm in the HOTP spec is SHA-1.

In order to create the HOTP value, we will use the HMAC-SHA-1 algorithm RFC 4226 - 5.2. Description

As we now know, SHA-1 has some fundamental weaknesses. The spec comments (perhaps somewhat naïvely) about SHA-1:

The new attacks on SHA-1 have no impact on the security of HMAC-SHA-1. RFC 4226 - B.2. HMAC-SHA-1 Status

I daresay that's accurate. But the TOTP authors disagree and allow a for some different algorithms to be used. The specification for HMAC says:

HMAC can be used with any iterative cryptographic hash function, e.g., MD5, SHA-1 [Emphasis added] RFC 2104 - HMAC: Keyed-Hashing for Message Authentication

So most TOTP implementation allow SHA-1, SHA-256, and SHA-512.

TOTP implementations MAY use HMAC-SHA-256 or HMAC-SHA-512 functions […] instead of the HMAC-SHA-1 function that has been specified for the HOTP computation RFC 6238 - TOTP: Time-Based One-Time Password Algorithm

But the HOTP spec goes on to say:

Current candidates for such hash functions include SHA-1, MD5, RIPEMD-128/160. These different realizations of HMAC will be denoted by HMAC-SHA1, HMAC-MD5, HMAC-RIPEMD RFC 2104 - Introduction

So, should your TOTP app be able to handle an MD5 HMAC, or even SHA3-384? Will it? If no algorithm is specified, what should the default be?

Period

As discussed, this is what increments the counter for HOTP. The Google Spec says:

The period parameter defines a period that a TOTP code will be valid for, in seconds. The default value is 30.

The TOTP RFC says:

We RECOMMEND a default time-step size of 30 seconds 5.2. Validation and Time-Step Size

It doesn't make sense to have a negative number of second. But what about one second? What about a thousand? Lots of apps artificially restrict TOTP codes to 15, 30, or 60 seconds. But there's no specification to define a maximum or minimum value.

A user with mobility difficulties or on a high-latency connection probably wants a 5 minute validity period. Conversely, machine-to-machine communication can probably be done with a single-second (or lower) time period.

Secret

Google says the secret is

an arbitrary key value encoded in Base32 according to RFC 3548. The padding specified in RFC 3548 section 2.2 is not required and should be omitted.

Whereas Apple says it is:

An arbitrary key value encoded in Base32. Secrets should be at least 160 bits.

Can a shared secret be a single character? What about a thousand? Will padding characters cause a secret to be rejected or can they be safely stripped?

Label

The label allows you to have multiple codes for the same service. For example Big Bank:Personal Account and Big Bank:Family Savings. The Google spec is slightly confusing:

The issuer prefix and account name should be separated by a literal or url-encoded colon, and optional spaces may precede the account name. Neither issuer nor account name may themselves contain a colon.

What happens if they are not URl encoded? What about Matrix accounts which use a colon in their account name? Why are spaces allowed to precede the account name? Is there any practical limit to the length of these strings?

If no label is specified, what should the default be?

Issuer

Google says this parameter is:

Strongly Recommended The issuer parameter is a string value indicating the provider or service this account is associated with, URL-encoded according to RFC 3986. If the issuer parameter is absent, issuer information may be taken from the issuer prefix of the label. If both issuer parameter and issuer label prefix are present, they should be equal.

Apple merely says:

The domain of the site or app. The password manager uses this field to suggest credentials when setting up a new code generator.

Yubico equivocates with

The issuer parameter is recommended, but it can be absent. Also, the issuer parameter and issuer string in label should be equal.

If it isn't a domain, will Apple reject it? What happens if the issuer and the label don't match?

Next Steps

  • If you're a user, please contribute tests or give feedback.
  • If you're a developer, please check your app conforms to the specification.
  • If you're from Google, Apple, Yubico, or another security company - wanna help me write up a proper RFC so this doesn't cause issues in the future?

  1. Time-based One Time Passwords. Not the TV show you remember from your youth, grandad. ↩︎

]]>
4
@edent <![CDATA[Using the Web Crypto API to Generate TOTP Codes in JavaScript Without 3rd Party Libraries]]> https://shkspr.mobi/blog/?p=58536 2025-02-28T23:09:05Z 2025-03-01T12:34:57Z <![CDATA[The Web Crypto API is, thankfully, nothing to do with scammy cryptocurrencies. Instead, it provides access to powerful cryptographic features which were previously only available in 3rd party tools. So, is it possible to build a TOTP code generator without using any external JS libraries? Yes! And it is (relatively) simple. Here's the code that I've written. It is slightly verbose and contains a lot of logging so you can see what it is doing. I've annotated it with links to the various…]]> <![CDATA[

The Web Crypto API is, thankfully, nothing to do with scammy cryptocurrencies. Instead, it provides access to powerful cryptographic features which were previously only available in 3rd party tools.

So, is it possible to build a TOTP0 code generator without using any external JS libraries? Yes! And it is (relatively) simple.

Here's the code that I've written. It is slightly verbose and contains a lot of logging so you can see what it is doing. I've annotated it with links to the various specifications so you can see where some of the decisions come from. I've compared the output to several popular TOTP code generators and it appears to match. You probably shouldn't use this in production, and you should audit it thoroughly.

I'm sure there are bugs to be fixed and performance enhancements to be made. Feel free to leave a comment here or on the repo if you spot anything.

I consider this code to be trivial but, if it makes you happy, you may consider it licensed under MIT.

async function generateTOTP( 
    base32Secret = "QWERTY", 
    interval = 30, 
    length = 6, 
    algorithm = "SHA-1" ) {

    //  Are the interval and length valid?
    if ( interval <  1 ) throw new Error( "Interval is too short" );
    if ( length   <  1 ) throw new Error( "Length is too low"     );
    if ( length   > 10 ) throw new Error( "Length is too high"    );

    //  Is the algorithm valid?
    //  https://datatracker.ietf.org/doc/html/rfc6238#section-1.2
    algorithm = algorithm.toUpperCase();
    if ( algorithm.match( "SHA-1|SHA-256|SHA-384|SHA-512" ) == null ) throw new Error( "Algorithm not known" );

    //  Decode the secret
    //  The Base32 Alphabet is specified at https://datatracker.ietf.org/doc/html/rfc4648#section-6
    const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
    let bits = "";

    //  Some secrets are padded with the `=` character. Remove padding.
    //  https://datatracker.ietf.org/doc/html/rfc3548#section-2.2
    base32Secret = base32Secret.replace( /=+$/, "" )

    //  Loop through the trimmed secret
    for ( let char of base32Secret ) {
        //  Ensure the secret's characters are upper case
        const value = alphabet.indexOf( char.toUpperCase() );

        //  If the character doesn't appear in the alphabet.
        if (value === -1) throw new Error( "Invalid Base32 character" );

        //  Binary representation of where the character is in the alphabet
        bits += value.toString( 2 ).padStart( 5, "0" );
    }

    //  Turn the bits into bytes
    let bytes = [];
    //  Loop through the bits, eight at a time
    for ( let i = 0; i < bits.length; i += 8 ) {
        if ( bits.length - i >= 8 ) {
                bytes.push( parseInt( bits.substring( i, i + 8 ), 2 ) );
        }
    }

    //  Turn those bytes into an array
    const decodedSecret = new Uint8Array( bytes );
    console.log( "decodedSecret is " + decodedSecret )

    //  Number of seconds since Unix Epoch
    const timeStamp = Date.now() / 1000; 
    console.log( "timeStamp is " + timeStamp )

    //  Number of intervals since Unix Epoch
    //  https://datatracker.ietf.org/doc/html/rfc6238#section-4.2
    const timeCounter = Math.floor( timeStamp / interval );
    console.log( "timeCounter is " + timeCounter )

    //  Number of intervals in hexadecimal
    const timeHex = timeCounter.toString( 16 );
    console.log( "timeHex is " + timeHex )

    //  Left-Pad with 0
    paddedHex = timeHex.toString(2).padStart( 16, "0" );
    console.log( "paddedHex is " + paddedHex )

    //  Set up a buffer to hold the data
    const timeBuffer = new ArrayBuffer( 8 );
    const timeView   = new DataView( timeBuffer );

    //  Take the hex string, split it into 2-character chunks 
    const timeBytes = paddedHex.match( /.{1,2}/g ).map(
        //  Convert to bytes
        byte => parseInt( byte, 16 )
    );

    //  Write each byte into timeBuffer.
    for ( let i = 0; i < 8; i++ ) {
         timeView.setUint8(i, timeBytes[i]);
    }
    console.log( "timeView is ",  new Uint8Array( timeView   ) );
    console.log( "timeBuffer is", new Uint8Array( timeBuffer ) );

    //  Use Web Crypto API to generate the HMAC key
    //  https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
    const key = await crypto.subtle.importKey(
        "raw",
        decodedSecret,
        { 
            name: "HMAC", 
            hash: algorithm 
        },
        false,
        ["sign"]
    );

    //  Sign the timeBuffer with the generated HMAC key
    //  https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/sign
    const signature = await crypto.subtle.sign( "HMAC", key, timeBuffer );

    //  Get HMAC as bytes
    const hmac = new Uint8Array( signature );
    console.log( "hmac is ", hmac );

    //  https://datatracker.ietf.org/doc/html/rfc4226#section-5.4
    //  Use the last byte to generate the offset
    const offset = hmac[ hmac.length - 1 ] & 0x0f;
    console.log( "offset is " + offset )

    //  Bit Twiddling operations
    const binaryCode = 
        ( ( hmac[ offset     ] & 0x7f ) << 24 ) |
        ( ( hmac[ offset + 1 ] & 0xff ) << 16 ) |
        ( ( hmac[ offset + 2 ] & 0xff ) <<  8 ) |
        ( ( hmac[ offset + 3 ] & 0xff ) );

    //  Turn the binary code into a decimal string
    stringOTP = binaryCode.toString();
    console.log( "stringOTP is " + stringOTP );

    //  Count backwards from the last character for the length of the code
    otp = stringOTP.slice( -length) 
    console.log( "otp is " + otp );
    //  Pad with 0 to full length
    otp = otp.padStart( length, "0" );
    console.log( "padded otp is " + otp );

    //  All done!
    return otp;
}


// Generate a TOTP code
( async () => {
    console.log( await generateTOTP( "4FCDTLHR446DPFCKUA46UFIAYTQIDSZ2", 30, 6, "SHA-1" ) );
} )();

It works with the three specified algorithms, generating between 1 and 10 digits, and works with any positive integer interval. Not all combinations are sensible; a one digit code valid for two minutes would be silly. But it is up to you to use this responsibly.

I hope I've shown that you don't need to rely on 3rd party libraries to do your cryptography; you can do it all in the browser instead.

You can grab the code from Codeberg.


  1. *sigh* Please don't be the boring dolt who makes a joke about Top of The Pops. Yes, I know they share the same initialism. And, yes, it's funny how nonce means something different in cryptography compared to British English. ↩︎

]]>
2
@edent <![CDATA[ManyTag Colour eInk Badge SDK - Minimum Viable Example for Android]]> https://shkspr.mobi/blog/?p=58487 2025-02-26T23:06:13Z 2025-02-28T12:34:30Z <![CDATA[Last year, I reviewed a Four-Colour eInk Name Badge - the ManyTag HSN371. The hardware itself is perfectly fine, but the Android app isn't great. It is complicated, crash-prone, and not available in the app-store. After some back-and-forth with the manufacturer, they agreed to send me their Android SDK and documentation. Sadly, the PDF they sent me was riddled with errors and the software library is also a bit dodgy. So, with the help of Edward Toroshchyn and a hefty amount of automated…]]> <![CDATA[

Last year, I reviewed a Four-Colour eInk Name Badge - the ManyTag HSN371. The hardware itself is perfectly fine, but the Android app isn't great. It is complicated, crash-prone, and not available in the app-store.

After some back-and-forth with the manufacturer, they agreed to send me their Android SDK and documentation. Sadly, the PDF they sent me was riddled with errors and the software library is also a bit dodgy. So, with the help of Edward Toroshchyn and a hefty amount of automated boiler-plate, I managed to get it working.

The full code is open source, but here's a quick walk-through of the important bits.

First, the AAR library needs to be imported into the project. Place it in app/libs and then include it in the Gradle build file:

dependencies {
    implementation(files("libs/badge_nfc_api-release.aar"))
}

The key to getting it working is in the Android permissions. It needs Bluetooth, NFC, and location. So the manifest has to contain:

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.NFC"/>
<uses-permission android:name="android.permission.NFC_TRANSACTION_EVENT"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

The following imports are needed from the Android Archive library:

import cn.manytag.badge_nfc_api.manager.BadgeWriteManager
import cn.manytag.badge_nfc_api.manager.OnNFCReaderCallback
import cn.manytag.badge_nfc_api.manager.OnBluetoothCallBack
import cn.manytag.badge_nfc_api.manager.OnSendImageCallback

The library needs to be initialised with:

val state = BadgeWriteManager.getInstance().init(this)

When the phone reads the NFC tag, it gets a bunch of information:

BadgeWriteManager.getInstance().setOnNFCReaderCallback(object : OnNFCReaderCallback {
    override fun onReaderMessage(i: Int, tag: Tag) {
        if (i == 0) {
            BadgeWriteManager.getInstance().readNFC(tag)
        }
    }

    //  Get the data from the badge
    override fun onReaderType(tag: Tag, isodep: IsoDep, i: Int, type: String, size: String, color: String) {
        if (i == 0) {
            nfcData = """
                NFC Tag Detected!!!
                Tag: $tag
                IsoDep: $isodep
                Type: $type
                Size: $size
                Color: $color
            """.trimIndent()
            colorFromNFC = color
            tagObject = tag
            isoDepObject = isodep
            badgeType = type
            badgeSize = size
        }
    }
})

The color is most important right now. It says whether the badge is black and white, or black and white and red, or black and white and red and yellow.

After picking an image from the filesystem, it needs to be dithered into the correct colour format:

processedBitmap = originalBitmap?.let { bitmap ->
    colorFromNFC?.let { color ->
        BadgeWriteManager.getInstance().processImage(bitmap, color)
    }
}

Finally, the processed image needs to be converted to bytes and then sent to the badge via Bluetooth:

if (processedBitmap != null && badgeType != null && badgeSize != null && colorFromNFC != null) {
    val imgData = BadgeWriteManager.getInstance().getImageData(processedBitmap!!, colorFromNFC!!)

    BadgeWriteManager.getInstance().sentImageResource(
        tagObject!!, isoDepObject!!, imgData, badgeType!!, badgeSize!!, colorFromNFC!!
    )
}

I realise this is a bit "draw the rest of the owl" but that should be enough to get you started on building an app which can communicate with these badges.

The app I've built isn't the prettiest in the world but at least it works. It scans a badge, gets its info, picks an image, dithers it, then sends it to the badge.

Screenshot of an app.

You can play with the code on CodeBerg.

]]>
0
@edent <![CDATA[Theatre Review: The Last Laugh ★★★★★]]> https://shkspr.mobi/blog/?p=58534 2025-02-27T12:51:14Z 2025-02-27T12:34:58Z <![CDATA[This is three excellent plays in one. First, a ghost story. Second, a tribute act. Thirdly, a meditation on the nature of comedy. In many ways, it is the complement to Inside Number 9 playing next door. Cooper, Morecambe, and Monkhouse were dead to begin with. Perhaps you grew up watching them live at the Palladium, or on grainy VHS tapes, or in microbursts on TikTok. But they got their last live laugh several decades prior to today. Nevertheless, their comedy lineage remains. Every…]]> <![CDATA[

Actors impersonating Tommy Cooper, Eric Morecambe and Bob Monkhouse. This is three excellent plays in one. First, a ghost story. Second, a tribute act. Thirdly, a meditation on the nature of comedy.

In many ways, it is the complement to Inside Number 9 playing next door.

Cooper, Morecambe, and Monkhouse were dead to begin with. Perhaps you grew up watching them live at the Palladium, or on grainy VHS tapes, or in microbursts on TikTok. But they got their last live laugh several decades prior to today.

Nevertheless, their comedy lineage remains. Every comedian milking a laugh or mining for a joke owes a huge debt to these men. So they have been reanimated for our pleasure. Just as behind every laughing jester is a crying clown, behind every grin is a bleached skull. What happens in the waiting room between life and death? Do we get to laugh with our pals or are we tormented by their ghosts?

We're granted a peak backstage at an event which never happened. What if these three comedians wound up in the same dingy dressing room before a show? It isn't exactly behind-the-scenes at the Yalta Conference, but we're probably not here for a dramatic retelling; we want to see our old favourites brought back to life. And that's exactly what we get.

90 minutes of pure tribute-act would probably be unbearable. People flock to musical tributes because The Beatles are unlikely to play your neighbourhood pub - but tribute comedians are usually relegated to a few minutes from an impressionist. The actors - Bob Golding as Morecambe, Simon Cartwright as Monkhouse and Damian Williams as Cooper - are uncanny. They perfectly bring their characters to life. They walk a dangerous tightrope between parody and mimic. Perhaps there's a touch of over-reliance on clichéd cadence - but they're able to recreate the jokes in a pitch-perfect way. So who am I to complain?

Finally, the characters ask what is comedy? Should writers be credited or is it the performer who deserves the laugh? Do double-act inevitably lead to resentment? Big questions for our heroes to chew on, but it is more for the audience to mull-over on the journey home. If a joke gets a laugh, it is funny. That's it. When you watch an impressionist tell someone else's joke - and one you've heard a hundred times before - is it still funny? Are you laughing at the recreation or at the memory?

The show wisely avoids an interval - the momentum of the jokes keep us going so you don't quite notice how depressing and ponderous it is becoming. Blokes can't talk about their emotions, so every moment of vulnerability is undercut by a witticism.

If you wore out your (or your parents) tapes, you'll recognise most of the jokes on offer. That's no bad thing; we're here to reminisce. Paul Henry's production has elevated the art of the tribute act to something quite spectacular. It is pure joy to pretend even for just a moment that our old friends are still here with us and still making us laugh.

The Last Laugh in in London for the next 4 weeks and then goes on tour. Well worth seeing.

]]>
0
@edent <![CDATA[Change the way dates are presented in WordPress's admin view]]> https://shkspr.mobi/blog/?p=58427 2025-02-22T13:41:05Z 2025-02-26T12:34:21Z <![CDATA[WordPress does not respect an admin's preferred date format. Here's how the admin list of posts looks to me: I don't want it to look like that. I want it in RFC3339 format. I know what you're thinking, just change the default date display - but that only seems to work in some areas of WordPress. It doesn't change the column-date format. Here's what mine is set to: So that doesn't work. Instead, you need to use the slightly obscure post_date_column_time filter Add this to your theme's …]]> <![CDATA[

WordPress does not respect an admin's preferred date format.

Here's how the admin list of posts looks to me:

Column with the date format separated by slashes.

I don't want it to look like that. I want it in RFC3339 format.

I know what you're thinking, just change the default date display - but that only seems to work in some areas of WordPress. It doesn't change the column-date format. Here's what mine is set to:

Settings screen showing date format set to dashes.

So that doesn't work.

Instead, you need to use the slightly obscure post_date_column_time filter

Add this to your theme's functions.php:

//  Admin view - change date format
function rfc3339_post_date_time( $time, $post ) {
    //  Modify the default time format
    $rfc3339_time = date( "Y-m-d H:i", strtotime( $post->post_date ) );
    return $rfc3339_time;
}
add_filter( "post_date_column_time", "rfc3339_post_date_time", 10, 2 );

And, hey presto, your date column will look like this: Column with the date format separated by dashes.

Obviously, you can change that code to whichever date format you prefer.

]]>
1
@edent <![CDATA[Book Review: Web Accessibility Cookbook - Creating Inclusive Experiences by Manuel Matuzovic ★★★★★]]> https://shkspr.mobi/blog/?p=58325 2025-02-26T21:33:42Z 2025-02-25T12:34:57Z <![CDATA[My friend Manuel has sent me his latest book to review - and it is a corker. The best thing about this book is that it doesn't waste any time trying to convince you that Accessibility Is Good™. You're a professional web developer; you know that. Instead, it gets straight down to brass-tacks and gives you immediate and useful examples of what to do. You could read the book linearly - but it is much more suited to dipping into. Want to know exactly how to do something? There's almost certainly a…]]> <![CDATA[

Book cover featuring a happy dog.My friend Manuel has sent me his latest book to review - and it is a corker. The best thing about this book is that it doesn't waste any time trying to convince you that Accessibility Is Good™. You're a professional web developer; you know that. Instead, it gets straight down to brass-tacks and gives you immediate and useful examples of what to do.

You could read the book linearly - but it is much more suited to dipping into. Want to know exactly how to do something? There's almost certainly a recipe in here for you. Within the first few minutes of reading, I'd already discovered some stuff I didn't know - for example, the <q> element changes its quote style based upon its parent's language.

It is, of course, a fully accessible ePub - with plenty of useful alt text and semantically-rich metadata. Even better, it is gorgeously formatted - with excellent use of colour and typesetting throughout.

Sample of the book showing highlighted code and semantic headings.

I particularly like that every section ends with a "discussion". The accessibility world is one of compromises - he invites you to think about the choices you're making and their trade-offs.

Manuel is very generous with his links to sources. You'll find dozens of blog posts, articles, and tutorials written by amazing people. It is slightly annoying that all the links go through the https://oreil.ly/ link shortener. I guess it means they can be updated if the original link dies, but it would be nice to see the destination before clicking.

Like lots of O'Reilly books, it is priced firmly in the "professional development" bracket. So get your boss to buy you a copy!

]]>
2
@edent <![CDATA[The least secure TOTP code possible]]> https://shkspr.mobi/blog/?p=58360 2025-02-24T18:56:39Z 2025-02-24T12:34:05Z <![CDATA[If you use Multi-Factor Authentication, you'll be well used to scanning in QR codes which allow you to share a secret code with a website. These are known as Time-based One Time Passwords (TOTP). As I've moaned about before, TOTP has never been properly standardised. It's a mish-mash of half-finished proposals with no active development, no test suite, and no-one looking after it. Which is exactly what you want from a security specification, right?! So let's try to find some edge-cases and…]]> <![CDATA[

If you use Multi-Factor Authentication, you'll be well used to scanning in QR codes which allow you to share a secret code with a website. These are known as Time-based One Time Passwords (TOTP0).

As I've moaned about before, TOTP has never been properly standardised. It's a mish-mash of half-finished proposals with no active development, no test suite, and no-one looking after it. Which is exactly what you want from a security specification, right?!

So let's try to find some edge-cases and see where things break down.

One Punch Man

This is possibly the least secure TOTP code I could create. Scan it and see whether your app will accept it.

QR code.

What makes it so crap? There are three things which protect you when using TOTP.

  1. The shared secret. In this case, it is abcdefghijklmno - OK, that's not the easiest thing to guess, but it isn't exactly complex.
  2. The amount time the code is valid for before changing. Most TOTP codes last 30 seconds, this lasts 120.
  3. The length of the code. Most codes are 6 digits long. In theory, the spec allows 8 digits. This is 1. Yup. A single digit.
BitWarden showing a single digit for 119 seconds.

If you were thick enough to use this1, an attacker would have a 1/10 chance of simply guessing your MFA code. If they saw you type it in, they'd have a couple of minutes in which to reuse it.

Can modern TOTP apps add this code? I crowdsourced the answers.

Surprisingly, a few apps accept it! Aegis, 1password, and BitWarden will happily store it and show you a 1 digit code for 120 seconds.

A few reject it. Authy, Google Authenticator, and OpenOTP claim the code is broken and won't add it.

But, weirdly, a few interpret it incorrectly! The native iOS app, Microsoft Authenticator, and KeepassXC store the code, but treat it as a 6 digit, 30 second code.

Do The Right Thing

What is the right thing to do in this case? The code is outside the (very loosely defined) specification. Postel's Law tells us that we should try our best to interpret malformed data - which is what Aegis and BitWarden do.

But, in a security context, that could be dangerous. Perhaps rejecting a dodgy code makes more sense?

What is absolutely daft2 is ignoring the bits of the code you don't like and substituting your own data! Luckily, in a normal TOTP enrolment, the user has to enter a code to prove they've saved it correctly. Entering in a 6 digit code where only 1 is expected is likely to fail.

We're Only Human

A one-digit code is ridiculous. But what about the other extreme? Would a 128-digit code be acceptable? For a human, no; it would be impossible to type in correctly. For a machine with a shared secret, it possibly makes sense.

On a high-latency connection or with users who may have mobility difficulties, a multi-minute timeframe could be sensible. For something of extremely high security, sub-30 seconds may be necessary.

But, again, the specification hasn't evolved to meet user needs. It is stagnant and decaying.

What's Next?

There's an draft proposal to tighten up to TOTP spec which has expired.

It would be nice if the major security players came together to work out a formal and complete specification for this vital piece of security architecture. But I bet it won't ever happen.

So there you have it. We're told to rely on TOTP for our MFA - yet the major apps all disagree on how the standard should be implemented. This is a recipe for an eventual security disaster.

How do we fix it?


  1. Yes! Just like Top of The Pops! The famous British TV show! Wow! I bet you're the first person in history to make that joke! Have a biscuit. ↩︎

  2. Please don't! ↩︎

  3. I wanted to use the words "utterly fucking stupid" but I felt it was unprofessional. ↩︎

]]>
4
@edent <![CDATA[Why are QR Codes with capital letters smaller than QR codes with lower-case letters?]]> https://shkspr.mobi/blog/?p=58337 2025-02-19T22:26:11Z 2025-02-23T12:34:37Z <![CDATA[Take a look at these two QR codes. Scan them if you like, I promise there's nothing dodgy in them.     Left is upper-case HTTPS://EDENT.TEL/ and right is lower-case https://edent.tel/ You can clearly see that the one on the left is a "smaller" QR as it has fewer bits of data in it. Both go to the same URl, the only difference is the casing. What's going on? Your first thought might be that there's a different level of error-correction. QR codes can have increasing levels of redundancy i…]]> <![CDATA[

Take a look at these two QR codes. Scan them if you like, I promise there's nothing dodgy in them.


QR CODE   QR Code.


Left is upper-case HTTPS://EDENT.TEL/ and right is lower-case https://edent.tel/

You can clearly see that the one on the left is a "smaller" QR as it has fewer bits of data in it. Both go to the same URl, the only difference is the casing.

What's going on?

Your first thought might be that there's a different level of error-correction. QR codes can have increasing levels of redundancy in order to make sure they can be scanned when damaged. But, in this case, they both have Low error correction.

The smaller code is "Type 1" - it is 21px * 21px. The larger is "Type 2" with 25px * 25px.

The official specification describes the versions in more details. The smaller code should be able to hold 25 alphanumeric character. But https://edent.tel/ is only 18 characters long. So why is it bumped into a larger code?

Using a decoder like ZXING it is possible to see the raw bytes of each code.

UPPER

20 93 1a a6 54 63 dd 28   
35 1b 50 e9 3b dc 00 ec
11 ec 11 

lower:

41 26 87 47 47 07 33 a2   
f2 f6 56 46 56 e7 42 e7
46 56 c2 f0 ec 11 ec 11   
ec 11 ec 11 ec 11 ec 11
ec 11 

You might have noticed that they both end with the same sequence: ec 11 Those are "padding bytes" because the data needs to completely fill the QR code. But - hang on! - not only does the UPPER one safely contain the text, it also has some spare padding?

The answer lies in the first couple of bytes.

Once the raw bytes have been read, a QR scanner needs to know exactly what sort of code it is dealing with. The first four bits tell it the mode. Let's convert the hex to binary and then split after the first four bits:

Type HEX BIN Split
UPPER 20 93 00100000 10010011 0010 000010010011
lower 41 26 01000001 00100110 0100 000100100110

The UPPER code is 0010 which indicates it is Alphanumeric - the standard says the next 9 bits show the length of data.

The lower code is 0100 which indicates it is Byte mode - the standard says the next 8 bits show the length of data.

Type HEX BIN Split
UPPER 20 93 00100000 10010011 0010 0000 10010
lower 41 26 01000001 00100110 0100 000 10010

Look at that! They both have a length of 10010 which, converted to binary, is 18 - the exact length of the text.

Alphanumeric users 11 bits for every two characters, Byte mode uses (you guessed it!) 8 bits per single character.

But why is the lower-case code pushed into Byte mode? Isn't it using letters and number?

Well, yes. But in order to store data efficiently, Alphanumeric mode only has a limited subset of characters available. Upper-case letters, and a handful of punctuation symbols: space $ % * + - . / :

Luckily, that's enough for a protocol, domain, and path. Sadly, no GET parameters.

So, there you have it. If you want the smallest possible physical size for a QR code which contains a URl, make sure the text is all in capital letters.

]]>
17
@edent <![CDATA[Book Review: In Search of Lost Time - Marcel Proust ⯪☆☆☆☆]]> https://shkspr.mobi/blog/?p=58291 2025-02-20T23:12:59Z 2025-02-22T12:34:58Z <![CDATA[A friend mentioned that they were going to a Proust book club where they'd be discussing Swann's Way, the first volume of the masterpiece. "Well," I thought, "That sounds like a fun challenge!" It was not. I picked up the Standard eBooks version translated by C. K. Scott Moncrieff and started my journey. It starts with a young man having a wet dream and then, in excruciating detail, describing the process of waking up. The writing starts as dreamy but quickly becomes obtuse. The story (such…]]> <![CDATA[

A book cover.A friend mentioned that they were going to a Proust book club where they'd be discussing Swann's Way, the first volume of the masterpiece. "Well," I thought, "That sounds like a fun challenge!"

It was not.

I picked up the Standard eBooks version translated by C. K. Scott Moncrieff and started my journey.

It starts with a young man having a wet dream and then, in excruciating detail, describing the process of waking up. The writing starts as dreamy but quickly becomes obtuse. The story (such as it is) has a recursive quality which never quite resolves into anything coherent.

Once, at a conference, I casually asked an attendee how he'd travelled to the venue. I was subsequently trapped in a twenty-minute monologue where I was told every last detail of which train he'd taken, where the seat cover fabric was manufactured, who designed the ticketing software, and how the person next to him chewed too loudly. He was just about to tell me about the flavour of crisp the passenger had, when I decided to feign a nosebleed and ran away.

Proust's narrator feels like what would once have been called an "Idiot Savant". He has an eidetic memory and isn't afraid to bludgeon the reader with it.

There are faint hints in the text that the narrator’s family consider him to be a mooncalf.

“That is not the way to make him strong and active,” she would say sadly, “especially this little man, who needs all the strength and character that he can get.”

Obviously, you can't go around diagnosing fictional characters based on TikTok stereotypes. And yet…

I would arrange them in the order of their talent in lists which I used to murmur to myself all day long.

The narrator is either totally unaware of social norms or wilfully blind to them. Having caught his Uncle "entertaining an actress" he is sworn to secrecy. Whereupon:

I found it simpler to let [my parents] have a full account, omitting no detail, of the visit I had paid that afternoon. In doing this I had no thought of causing my uncle any unpleasantness.

He's an unsympathetic character with no self-awareness and a propensity to tell endless tales with no point, no moral, and of no consequence.

I will (begrudgingly) admit that I did laugh a couple of times. Notably:

I became at once a man, and did what all we grown men do when face to face with suffering and injustice; I preferred not to see them.

And

So we at least thought; as for my uncle, his fatal readiness to pay pretty widows (who had perhaps never been married) and countesses (whose high-sounding titles were probably no more than noms de guerre)

Bit it is thin gruel.

I got a quarter of the way though before realising that I wasn't reading. I was running my eyes over the words and hoping to find something - anything - of interest in there.

I ended, more-or-less, at this fine passage:

I had recognised it as a book which had been well spoken of, in my hearing, by the schoolmaster or the schoolfriend who, at that particular time, seemed to me to be entrusted with the secret of Truth and Beauty, things half-felt by me, half-incomprehensible, the full understanding of which was the vague but permanent object of my thoughts.

I'm sure the other 75% is equally erudite. But, for me, it was like being trapped at a party with someone who only wants to tell you the route they drove on the motorway. And how that reminds them of the journey their sister once took driving to Carmarthen. And why the song on the car radio brought back memories of a petrol station in Slough.

The bit about the madeleines isn't nearly as iconic as people suggest.

]]>
4
@edent <![CDATA[Theatre Review: Trash ★★★⯪☆]]> https://shkspr.mobi/blog/?p=58399 2025-02-20T23:12:46Z 2025-02-21T12:34:11Z <![CDATA[I went into this as a cynic and came out a grinning maniac. Look, it is basically "Stomp" but for kids. It's a join-in pantomime where four babbling fools play with junk in a recycling centre to make music. Oh, sure, you could analyse it as being a blend of Commedia dell'arte and modern dance, but it is closer to Minions. All cartoon violence, generic-Euro-mumble speech, and tunes that they'll recognise when they're older (but the parents will love). The kids in the audience were constantly…]]> <![CDATA[

I went into this as a cynic and came out a grinning maniac. Look, it is basically "Stomp" but for kids. It's a join-in pantomime where four babbling fools play with junk in a recycling centre to make music.

Oh, sure, you could analyse it as being a blend of Commedia dell'arte and modern dance, but it is closer to Minions. All cartoon violence, generic-Euro-mumble speech, and tunes that they'll recognise when they're older (but the parents will love). The kids in the audience were constantly shrieking with laughter and the adults were chuckling with delight too.

The (mandatory) audience participation is delightful and, besides, you're already clapping along so you might as well start singing. It reminded me of those shows you get in theme-parks; all dry-ice and family-friendly sketches. There's nothing challenging or subversive - it's just people bashing things on their heads in order to make music.

For a 90 minute show, the £18 tickets are fairly priced. It is silly - and I truly mean that as a compliment. Any kids you take with you will immediately want to become performers and will be dancing all the way home.

]]>
1
@edent <![CDATA[Book Review: The Rituals of Dinner - The Origins, Evolution, Eccentricities and Meaning of Table Manners by Margaret Visser ★★★★⯪]]> https://shkspr.mobi/blog/?p=58285 2025-02-17T23:39:44Z 2025-02-20T12:34:35Z <![CDATA[The purpose of table manners is to stop us killing each other. That's the rather provocative assertion in Margaret Visser's excellent deconstruction of why we have such elaborate and infuriating rituals around eating. It starts, naturally enough, with a chapter on human sacrifice. It is grim, violent, and soaked in blood. A delightful amuse-bouche this isn't! But it makes the case that this is (part) of the origin of our modern table manners. We no longer need to appease the gods and secure a …]]> <![CDATA[

Book cover.The purpose of table manners is to stop us killing each other. That's the rather provocative assertion in Margaret Visser's excellent deconstruction of why we have such elaborate and infuriating rituals around eating.

It starts, naturally enough, with a chapter on human sacrifice. It is grim, violent, and soaked in blood. A delightful amuse-bouche this isn't! But it makes the case that this is (part) of the origin of our modern table manners.

We no longer need to appease the gods and secure a plentiful harvest, but those rituals echo down because they are hewn into our culture.

Ritual is action frequently repeated, in a form largely laid down in advance; it aims to get those actions right. Everyone present knows what should happen, and notices when it does not. Dinner too is habitual

Food is performance. It is something I think I instinctively knew, but it is fascinating having it spelled out in such a blunt fashion.

a meal can be thought of as a ritual and a work of art, with limits laid down, desires aroused and fulfilled, enticements, variety, patterning, and plot.

A meal has a plot?!? Well, of course! The starter, main, and dessert are our beginning, middle, and end. A meal that starts with chocolate mixed with cauliflower and finishes with a roast drenched in porridge is as nonsensical as a murder mystery with no murder and a Hobbit scarecrow as the protagonist.

For a book about dinner, it is quick to point out that manners and ritual invade every aspect of our life. The prose is stunningly prescient:

This is a time of transition, when old manners are dying and new ones are still being forged. A good many of our uncertainties, discomforts, and disagreements stem from this state of flux. Sometimes we hold the terrifying conviction that the social fabric is breaking up altogether, and that human life is becoming brutish and ugly because of a general backsliding from previous social agreements that everyone should habitually behave with consideration for others.

As a teenager, you probably whinged about all the "unjust" rules foisted on you by your parents and schools. Everything seemed so petty and counter-intuitive. Why can't you keep your elbows on the table and talk with your mouth full? Visser is a kind and patient teacher.

Other people inevitably make demands on us and inhibit us, partly in order to make room for themselves; we learn that it is in our best interests to play the game, because we also require the freedom which other people’s restraint allows to us.

Unlike other cultural books, this doesn't just focus on WEIRD nations; we're happily trundled off to explore the rituals of a dozen cultures. This could quite easily have fallen into the trap of "aren't other people strange!" but Visser is as clinical as an alien anthropologist. She perfectly observes our cultural foibles as well - making it clear that every culture is strange.

The formality of hamburgers lies in their relentlessly predictable shape, and in the superimposed and separate layers of food which make sophisticated references to parts of the sequential model for a formal meal.

The book is a delight. It would benefit from a few illustrations to help make some of the points clearer, but it is a fun and educational read.

]]>
0
@edent <![CDATA[Automatic Kobo and Kindle eBook Arbitrage]]> https://shkspr.mobi/blog/?p=58241 2025-02-19T11:09:54Z 2025-02-19T12:34:43Z <![CDATA[This post will show you how to programmatically get the cheapest possible price on eBooks from Kobo. Background Amazon have decided to stop letting customers download their purchased eBooks onto their computers. That means I can't strip the DRM and read on my non-Amazon eReader. So I guess I'm not spending money with Amazon any more. I'm moving to Kobo for three main reasons: They provide standard ePubs for download. ePub DRM is trivial to remove. Kobo will undercut Amazon's prices! …]]> <![CDATA[

This post will show you how to programmatically get the cheapest possible price on eBooks from Kobo.

Background

Amazon have decided to stop letting customers download their purchased eBooks onto their computers. That means I can't strip the DRM and read on my non-Amazon eReader.

So I guess I'm not spending money with Amazon any more. I'm moving to Kobo for three main reasons:

  1. They provide standard ePubs for download.
  2. ePub DRM is trivial to remove.
  3. Kobo will undercut Amazon's prices!

Here's the thing. I want to buy my eBooks. It is trivial to pirate almost any modern book. But, call me crazy, I like rewarding writers with a few pennies. That said, I'm not made of money, so I want to get the best (legal) deal possible.

Kobo do a price-match with other eBook retailers. It says:

We'll award a credit to your Kobo account equal to the price difference, plus 10% of the competitor’s price.

I found a book I wanted which was £4.99 on Kobo. The Amazon Kindle price was £4.31.

4.99 - ( (4.99 - 4.31) + (4.31 * 0.1) ) = 3.88

I purchased the book, sent a request for a price match, and got this email a few hours later:

We’re pleased to confirm that the price match you requested has been successfully processed. The credit has been applied to your Kobo account. Credit amount: £ 1.11 GBP

OK! So what steps can we automate, and which will have to remain manual?

Amazon Pricing API

Amazon have a Product Advertising API. You will need to register for the Amazon Affiliate Program and make some qualifying sales before you get API access.

In order to search for an ISBN and get the price back, you need to send:

{
 "Keywords": "isbn:9781473613546",
 "Resources": ["Offers.Listings.Price"],
}

Using the updated Python API for PAAPI:

from paapi5_python_sdk import DefaultApi, SearchItemsRequest, SearchItemsResource, PartnerType

def search_items():
    access_key = "ABC"
    secret_key = "123"
    partner_tag = "shkspr-21"
    host = "webservices.amazon.co.uk"
    region = "eu-west-1"

    api = DefaultApi(access_key=access_key, secret_key=secret_key, host=host, region=region)

    request = SearchItemsRequest(
        partner_tag=partner_tag,
        partner_type=PartnerType.ASSOCIATES,
        keywords="isbn:9781473613546",
        search_index="All",
        item_count=1,
        resources=["Offers.Listings.Price"]
    )

    response = api.search_items(request)

    print(response)

search_items()

(Add your own access key, secret key, and tag. You may need to change the host and region depending on where you are in the world.)

That returns something like:

{
    "search_result": {
        "items": [
            {
                "asin": "B09JLQHHXN",
                "detail_page_url": "https://www.amazon.co.uk/dp/B09JLQHHXN?tag=shkspr-21&linkCode=osi&th=1&psc=1",
                "offers": {
                    "listings": [
                        {
                            "price": {
                                "amount": 2.99,
                                "currency": "GBP",
                                "display_amount": "£2.99"
                            }
                        }
                    ]
                }
            }
        ]
    }
}

(I've truncated the above so it only shows the relevant information.)

Kobo ISBN & Price

Let's get the ISBN and Price of a book on Kobo. There's no easy API to do this. But, thankfully, Kobo embeds some Schema.org metadata.

Look at the source code for https://www.kobo.com/gb/en/ebook/venomous-lumpsucker-1

<div id="ratings-widget-details-wrapper" class="kobo-gizmo"
     data-kobo-gizmo="RatingAndReviewWidget"
     data-kobo-gizmo-config ='{&quot;googleBook&quot;:&quot;{\r\n  \&quot;@context\&quot;: \&quot;http://schema.org\&quot;,\r\n  \&quot;@type\&quot;: \&quot;Book\&quot;,\r\n  \&quot;name\&quot;: \&quot;Venomous Lumpsucker\&quot;,\r\n  \&quot;genre\&quot;: [\r\n    \&quot;Fiction \\u0026 Literature\&quot;,\r\n    \&quot;Humorous\&quot;,\r\n    \&quot;Literary\&quot;\r\n  ],\r\n  \&quot;inLanguage\&quot;: \&quot;en\&quot;,\r\n  \&quot;author\&quot;: {\r\n    \&quot;@type\&quot;: \&quot;Person\&quot;,\r\n    \&quot;name\&quot;: \&quot;Ned Beauman\&quot;\r\n  },\r\n  \&quot;workExample\&quot;: {\r\n    \&quot;@type\&quot;: \&quot;Book\&quot;,\r\n    \&quot;author\&quot;: {\r\n      \&quot;@type\&quot;: \&quot;Person\&quot;,\r\n      \&quot;name\&quot;: \&quot;Ned Beauman\&quot;\r\n    },\r\n    \&quot;isbn\&quot;: \&quot;9781473613546\&quot; …'>
</div>

Getting the data from the data-kobo-gizmo-config is a little tricky.

  • Using Python Requests won't work because Kobo seem to run a JS CAPTCHA to detect scraping.
  • There is a Calibre-Web Kobo plugin but it requires you to have a physical Kobo eReader in order to get an API key.
  • The Rakuten API is only for the Japanese store.

So we have to use the Selenium WebDriver to scrape the data:

from selenium import webdriver
from bs4 import BeautifulSoup
import json

#   Open the web page
browser = webdriver.Firefox()
browser.get("https://www.kobo.com/gb/en/ebook/venomous-lumpsucker-1")

#   Get the source
html_source = browser.page_source

#   Soupify
soup = BeautifulSoup(html_source, 'html.parser')

#   Get the encoded JSON Schema
schema = soup.find_all(id="ratings-widget-details-wrapper")[0].get("data-kobo-gizmo-config")

#   Convert to object from JSON
parsed_data = json.loads(schema)

#   Decode the nested JSON strings
parsed_data["googleBook"] = json.loads(parsed_data["googleBook"])

#    Get ISBN and Price
price = parsed_data["googleBook"]["workExample"]["potentialAction"]["expectsAcceptanceOf"]["price"]
isbn  = parsed_data["googleBook"]["workExample"]["isbn"]
print(isbn)
print(price)

Kobo Wishlist

OK, nearly there! Given a Kobo book URl we can get the price and ISBN, then use that ISBN to get the Kindle price. But how do we get the Kobo book URl in the first place?

I'm adding all the books I want to my Kobo Wishlist.

Inside the Wishlist is a scrap of JavaScript which contains this JSON:

{
    "value": {
        "Items": [
            {
                "Title": "Venomous Lumpsucker",
                "Price": "£2.99",
                "ProductUrl": "/gb/en/ebook/venomous-lumpsucker-1",
            }
        ],
        "TotalItemCount": 11,
        "ItemCountByProductType": {
            "book": 11
        },
        "PageIndex": 1,
        "TotalNumPages": 1,
       }
}

(Simplified to make it easier to understand.)

Although there's a price, there's no ISBN, So you'll need to use the "ProductUrl" to get the ISBN and Price as above.

Sadly, unlike Amazon, there's no way to publicly share a wishlist. Getting the JSON requires logging in, so it's back to Selenium again!

This should be enough:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from bs4 import BeautifulSoup
import time

browser = webdriver.Firefox()
browser.get("https://www.kobo.com/gb/en/account/wishlist")

#       Log in
username_box = browser.find_element(By.NAME, "LogInModel.UserName")
username_box.clear()
username_box.send_keys('[email protected]')

password_box = browser.find_element(By.NAME, "LogInModel.Password")
password_box.clear()
password_box.send_keys('p455w0rd')

password_box.send_keys(Keys.RETURN)

time.sleep(5) # Wait for load and rendering

But the Kobo presents a CAPTCHA which prevents login.

There is an unofficial API which, sadly, doesn't seem to work at the moment.

Next Steps

For now, I'm saving specific Kobo book URls into a file and then running a scrape once per day. Hopefully, the unofficial Kobo API will be working again soon.

]]>
7
@edent <![CDATA[Theatre Review: Inside No. 9 Stage/Fright ★★★★★]]> https://shkspr.mobi/blog/?p=58179 2025-02-17T22:33:07Z 2025-02-18T12:34:19Z <![CDATA[This is spoiler-free review. In one episode of Inside Number Nine, two old comedians are bickering. In a moment of understated savagery one says to the other "That's a cheap laugh, Len." Len replies with a mischievous twinkle in his eye, "Oh, come on. A laugh's a laugh however you earn it." That sets up the central tension for the West-End-Spectacular version of the show - Stage/Fright. What makes for a "cheap" scream of terror? It's easy to have a jump scare, or an on-stage explosion. The…]]> <![CDATA[

Poster for Stage Fright.This is spoiler-free review.

In one episode of Inside Number Nine, two old comedians are bickering. In a moment of understated savagery one says to the other "That's a cheap laugh, Len."

Len replies with a mischievous twinkle in his eye, "Oh, come on. A laugh's a laugh however you earn it."

That sets up the central tension for the West-End-Spectacular version of the show - Stage/Fright.

What makes for a "cheap" scream of terror? It's easy to have a jump scare, or an on-stage explosion. The audience will shriek but then quickly descend into giggle as they realise how embarrassingly naïve they've been.

How do you ratchet up the tension on-stage? What call-backs work as well for tickling your funny bone and chilling you to the bone?

Stage/Fright balances all of that perfectly. For every "cheap" laugh there's a belly-laugh which has been well and truly earned. For every made-you-jump there's a scene of creeping dread. For every little jab at a theatrical folly, there's a deep cut of West End satire.

Pemberton and Shearsmith are delightfully generous to their outstanding cast. A script which could so easily have been dominated by their double-act is, instead, handed over graciously which gives the cast a real chance to shine.

It is an excellent show. Funny, scary, tense, wry, and affectionate. If you can manage to scrounge a ticket, I urge you to do so.

Mild Spoilers

I was initially underwhelmed by the show. After the cold-open is a beat-for-beat recreation of the episode "Bernie Clifton's Dressing Room". It was good, but felt like I was watching a "best of" stage show. I was worried that the whole show would be rehashes of old episodes. Thankfully no! In context, it was remarkable how well integrated the story was.

My wife and I saw it on different nights, so we were able to compare notes about what we saw. Some of the bits I thought were genuinely part of the show turned out to be actual flubs - whereas something I thought was a flub was deliberate.

Pre- and Post-Show

As I continue to whinge about, theatres need to up their game in terms of getting people in the mood. The inside of Wyndham's Theatre is devoid of props, photos, or anything related to the show. The standard programme is a measly affair with most of the pages dedicated to the history of the theatre rather than the show. There's an expensive deluxe programme with more content. There's nothing to do in the interval other than queue for inadequate toilets and overpriced drinks.

Where's the build up? Why aren't there social objects to take a selfie with? What can be done to make people feel like this is a better experience than watching TV? How do you encourage people to see the theatre as an exciting and vital venue that they want to come back to?

A well-hyped show like this doesn't have any trouble putting bums on seats. But they could at least make some effort to make the audience feel special.

]]>
2
@edent <![CDATA[Singing the TfL Blues]]> https://shkspr.mobi/blog/?p=58204 2025-02-14T12:12:59Z 2025-02-17T12:34:38Z <![CDATA[I am a regular user of Transport for London's services. On my phone I have the TfL Go app for finding my way around the city, and a web shortcut to a specific bus stop so I can find my way home. Why are they different shades of blue⁉️⁉️⁉️ TfL, like most large organisations, have brand guidelines. It enables them to set a consistent look and feel across their services which, hopefully, makes it easier for users to identify them. A glowing roundel in the night tells you you're near a tube stat…]]> <![CDATA[

I am a regular user of Transport for London's services. On my phone I have the TfL Go app for finding my way around the city, and a web shortcut to a specific bus stop so I can find my way home.

Two TfL icons with subtly different blue colours.

Why are they different shades of blue⁉⁉⁉

TfL, like most large organisations, have brand guidelines. It enables them to set a consistent look and feel across their services which, hopefully, makes it easier for users to identify them. A glowing roundel in the night tells you you're near a tube station, the colours of the lines reassures you that you jumped on the right train, even the font lets you know you're in the right place.

They publish the TfL Colour Standard - a short document which explains what all their colours are.

TfL Colour Guidelines.

If you examine the TfL Go app you'll see that its icon is R0 G25 B168 (#0019A8).

But on the web, their standard Favicon uses R17 G64 B145 (#114091).

Yet their Apple Favicon uses R17 G59 B146 (#113B92).

Over the years, the colour standard has been refined. Issue 1 from 2003 had the corporate blue as R0 G45 B115 (#002D73) with a "web safe" version of #003399.

By Issue 2 in 2007, the corporate colour was set at R0 G25 B168, with a web safe version of #000099. The same is present in 2007's Issue 3.

Given the RGB value has been in set in stone for over 15 years, where does this discrepancy come from?

I don't think it is an accessibility issue. TfL have great documentation on how they meet WCAG and I can't see the correct corporate colour causing any issues.

As far as I can tell, the #113B92 colour first appeared on the web around 2012.

The #114091 Facicon first appeared around 2014.

Along the way, they also had this nifty iPhone icon with, you guessed it, another shade of blue #0044a3.

Glossy iOS icon with a train on it.

Perhaps it is a conversion issue? What's the CMYK?

The colour standard says corporate blue is C100 M97 Y3 K3 But the TfL elements standard says it is C100 M88 Y0 K5.

Both agree that it should be Pantone 072.

Looking at the Pantone website that blue is #1007a0.

Which, If I convert to CMYK is C90 M96 Y0 K37.

Any way you slice it, that's several completely different shades of blue!

For You, Blue

Colours are hard. Humans have varying perceptions of shades and hues. We have several different ways of representing these colours. Applying colour to a screen is different to applying it in paint or fabric.

True consistency across different media is almost impossible.

But, on digital media, having a single colour is a relatively simple technical issue. Pick a single RGB (or HSL colour and stick to it.

There's no reason that I can fathom that these two icons should be different. If you disagree, please let me know what basic error I have made.

]]>
2
@edent <![CDATA[Some esoteric versioning schemes (monotonic moronity)]]> https://shkspr.mobi/blog/?p=58043 2025-02-09T23:01:55Z 2025-02-12T12:34:33Z <![CDATA[Since time immemorial, software has had version numbers. A developer releases V1 of their product. Some time later, they add new features or fix bugs, and release the next version. What should that next version be called? Modern software broadly bifurcates into two competing standards; SemVer and CalVer. SemVer Semantic Versioning is usually in the form 1.2.3, the last digit is usually for minor bug fixes, the second digit for new functionality, and the primary digit for big and/or breaking …]]> <![CDATA[

Since time immemorial, software has had version numbers. A developer releases V1 of their product. Some time later, they add new features or fix bugs, and release the next version.

What should that next version be called? Modern software broadly bifurcates into two competing standards; SemVer and CalVer.

SemVer

Semantic Versioning is usually in the form 1.2.3, the last digit is usually for minor bug fixes, the second digit for new functionality, and the primary digit for big and/or breaking changes.

The semantics are pretty loose. There's no real consensus on when a new "primary" number should be issued. There are two main weaknesses:

  1. The numbers might not be decimals. Is V1.29 newer or older than V1.3?
  2. There's no semantic information about when the software was released.

Which leads us to…

CalVer

Calendar Versioning is, ironically, more semantic than SemVer. The version number is the date when the software was released. For example, Ubuntu releases are in the form of YY.MM - the latest stable release at the time of writing is 24.04 - so we can tell that it was released in April 2024.

There are three main problems with this approach.

  1. ISO8601 or GTFO! Surely these should use YYYY-MM to make it obvious this is a date?
  2. Minor bug fixes are often given a release number like 24.04.1 - is that still obvious it is date-based? Was it really released on the 1st of April?
  3. No information about big and/or breaking changes. Software released several years apart may be functionally identical whereas software released days apart may be incompatible.

Alternatives

So, what other ways can we number software versions?

EffVer

Effort Versioning is, I think, a sensible way to standardise SemVer. It attempts to show how much effort it takes to move between versions.

PrideVer

How much Pride do you have in your software release?

This is SemVer for people with an ego and the coding chops to match.

RuffVer

Ruff is a sort of bastard child between SemVer and CalVer, but adds this delightful complication:

Stable releases use even numbers in minor version component: 2024.30.0, 2024.32.0, 2024.34.0, … Preview releases use odd numbers in minor version component: 2024.31.0, 2024.33.0, 2024.35.0, …

It's the versioning equivalent of setting up a fully scalable cloud database and hand-chiselling HTML out of stone for the cookery blog you update twice per year.

0Ver

Zero-based Versioning tells us that it is forbidden to ask and a sin to know when a piece of software will be completed.

Essentially, it is SemVer for cowards who are afraid to commit. The opposite of PriveVer.

PiVer

The venerable TeX uses Pi Versioning. The current version is 3.141592653, the next version will be 3.1415926535.

As the software gets refined, it gradually reaches a state of perfection. This is a charming versioning scheme which shouldn't be used by anyone other than Knuth lest hubris overtake you!

NameVer

Sometimes marketing takes the reins and insists that consumers need a Named Version to help prevent confusion.

Ubuntu uses things like Bionic Beaver, Distinct Dropbear, and Mantic Minotaur. By convention, names increase alphabetically, so you should know that Jaundiced Jackdaw is before Killer Kangaroo - until you've released 26 version and have to wrap around the alphabet again.

NameVer is helpful for distinct products which aren't related, but probably more confusing than necessary.

WinVer

Microsoft Windows uses this very logical scheme - 1, 2, 3, 3.11, 98, 2000, Me, XP, Vista, 7, 8, 10, 11.

It starts with more-or-less SemVer, then jumps to CalVer, then 4 digit CalVer, then to NameVer, then back to SemVer - skipping 9 because of alleged technical reasons.

Do not attempt to use this versioning unless you want to anger both gods and mortals.

KelVer

Absolute Zero is defined as 0K. And so, Kelvin Versioning counts down to stability.

Almost the opposite of PiVer - the closer this gets to zero, the closer the code is to being complete.

This versioning scheme is affront to most sane people. But here's to the crazy ones.

Non-Monotonic

You will notice that all of the above are monotonic. That is, they all proceed in one direction and never reverse. Any subsequent version was definitely released later than a previous version. So, in a sense, they all contain some level of semantics.

But they don't have to.

HashVer

Taking the Cryptographic Hash of the code, or a commit, allows one to create Hash Versioning. For example 43317b7 is a HashVer for something which would otherwise have the dull and unworthy name of v0.118.1

But, of course, a hash does have a modicum of semantic information - even if it is only loosely related to the content of the code. What if there were something with no semantics and no monotonic behaviour!?!?

RandVer

Embrace the weird with Random Versioning! It its heart, RandVer says pick any number that hasn't been used before.

Perhaps V7 is followed by V2.5, which is overtaken by V0xDEADBEEF

Absolutely guaranteed to have zero semantic content.

What have we learned today?

The square-root of bugger-all.

]]>
11
@edent <![CDATA[A small contribution to curl]]> https://shkspr.mobi/blog/?p=58048 2025-02-11T12:26:37Z 2025-02-11T12:34:13Z <![CDATA[The venerable curl is one of the most fundamental pieces of code in the modern world. A seemingly simply utility - it enables other programs to interact with URls - it runs on millions of cars, is inside nearly every TV, used by billions of people, and is even in use on Mars. And, as of last week, features a small contribution by me! Look, I'm not an experienced bit-twiddler. I can't micro-optimise algorithms or spot intricate C-based memory leaks. What I can do is get annoyed at poor…]]> <![CDATA[

The venerable curl is one of the most fundamental pieces of code in the modern world. A seemingly simply utility - it enables other programs to interact with URls - it runs on millions of cars, is inside nearly every TV, used by billions of people, and is even in use on Mars.

And, as of last week, features a small contribution by me!

Look, I'm not an experienced bit-twiddler. I can't micro-optimise algorithms or spot intricate C-based memory leaks. What I can do is get annoyed at poor documentation!

You see, documentation and code comments are vitally important. Poor spelling might trip up non-native speakers, bad examples confuse learners, and ambiguous wording is a barrier to understanding.

As was written by the sages:

a computer language is not just a way of getting a computer to perform operations but rather that it is a novel formal medium for expressing ideas about methodology. Thus, programs must be written for people to read, and only incidentally for machines to execute. Abelson, Sussman, and Sussman Structure and Interpretation of Computer Programs

So, what did I fix? A few years ago, I noticed Google's documentation used example domains it didn't control. The same thing was happening in the curl source code.

One example domain used was "HTTPS://your.favourite.ssl.site" - when that code was written 23 years ago, the .site TLD didn't exist. Now it does.

Is there a serious risk that someone will register ssl.site and use it to take over the machine of anyone who unthinkingly follows that example? Probably not. But it also isn't terribly clear that it is an example. So I changed it to secure.site.example which uses the reserved .example TLD.

That should make it clear to everyone that it is a placeholder example and will prevent anyone from misusing that domain.

Similarly, there were a few comments which used domain.com as an example. However, that's a real website - so I updated those to example.com.

I was delighted to see the changes accepted.

daniel stenberg saying "Welcome Terence Eden as #curl commit author 1342"

And I was only slightly disappointed to have narrowly missed out on being contributor 1337, but being number 1342 ain't so bad 😄

You can see the full list of changes on GitHub.

Much like my patch to the Linux Kernel this might be considered a trivial matter - but I honestly believe that clear and accurate documentation can be as important as the code itself.

Huge thanks to Daniel for creating curl, for making such a welcoming environment for new contributors, and for handing out such brilliant stickers at FOSDEM!

A laptop covered with stickers - prominent is curl. ]]>
4
@edent <![CDATA[Review: Phantom Peak - JONACON London 2025 ★★★★★]]> https://shkspr.mobi/blog/?p=58057 2025-02-10T12:54:13Z 2025-02-10T12:34:12Z <![CDATA[I was lucky enough to score playtest tickets for the new season of Phantom Peak - the open world, interactive and immersive puzzle experience in London. I'd never been before and generally have a mixed reaction to these sorts of immersive shows. I loved Doctor Who - Time Fracture but found 1984 to be underwhelming. Phantom Peak takes you inside an Old West mining town in a weird steam-punk alternate reality. The corrupt mayor is on the prowl, demons stalk the land, love is in the air, but can …]]> <![CDATA[

I was lucky enough to score playtest tickets for the new season of Phantom Peak - the open world, interactive and immersive puzzle experience in London. I'd never been before and generally have a mixed reaction to these sorts of immersive shows. I loved Doctor Who - Time Fracture but found 1984 to be underwhelming.

Phantom Peak takes you inside an Old West mining town in a weird steam-punk alternate reality. The corrupt mayor is on the prowl, demons stalk the land, love is in the air, but can you uncover the secrets of town before it is too late!??!!

It was exceptional!

With a large cast of incredibly talented improvisers, a delightfully daft plot, and lushly decorated sets, I was tempted to just sit back and drink in the atmosphere. But there's no time to lounge around; there are puzzles to solve!

Firstly, I have to acknowledge that the playtest was nearly complete. Several of the videos were replaced with text and few of the puzzle elements had some rough edges - but that's what this sort of preview is for. We were encouraged to give feedback to the writers (nestled in a corner) and to let them know what things were confusing.

There are 10 different "trails" to complete, we just-about managed to do 4 of them. Even if you were an expert player, I think you'd struggle to do more than five, so there is some re-playability if you want to come again. The individual elements of the trails all have the same basic template:

  • Speak to a person, get a clue.
  • Go to a different location, use that clue, get another task.
  • Watch a video or listen to some audio, get another clue.
  • Scurry across to someone else and give them information.
  • Interact with some of the gloriously tactile machines to get the next location.

And repeat. In truth, they aren't puzzles as much as tasks. You won't be deciphering anagrams, opening combination locks, or piecing together different things. But it is a lot of fun and, across our tasks, I think we managed to visit most rooms in the space. Every completed trail earns you tokens which you can use to help your colour-coordinated team win.

You'll need a fully charged phone as there's a useful interactive website to track your progress and enter the clues you've found. If you get stuck, there is a "help" button - or you can ask the cast who all are game and willing to help you out with tips, tricks, and witty asides (but do not ask about the pet rocks). It's all very low-stakes; you aren't going to be trapped or ostracised if you can't complete something. There's no rush, play at your own pace.

That said, Liz and I enjoyed running about between the various rooms determined to experience as much as possible. As we did, we caught sight of what the other players were doing. Everyone was happy to chat about what they were experiencing and eagerly exchanged tips & tricks. About half the testers had played a previous season, and were really enthusiastic about the experience. I'm sure there were plenty of inside jokes, but I didn't feel like I missed anything by being a newbie. There were plenty of kids and teenagers - who all seemed to be having a whale of a time. None of the storylines are too raucous or salacious for younger minds.

There were a few niggles which probably weren't the fault of the playtest. Some of the rooms can get a little crowded so you may have to watch someone else solve a different quest before you get on to your next step. The crowding can also make it difficult to hear some of the audio-only clues - although videos are subtitled. There are various heaters around the indoor parts of the experience, but it is pretty cold - so bring a coat. I suspect the outside parts work better in summer.

The "team" element didn't really work for me. We were split into groups with different wrist-band colours. Nominally, that put us in different worker groups - but that didn't have any effect on the story or trails. The tokens we won were tallied up by team - but the piles looked pretty equal to me. It might have been interesting if there was a distinct ending at the closing ceremony depending on which team was victorious. Or perhaps if there were some team-specific storyline elements. As it was, it felt a little tacked-on.

Would I go again? Adult ticket prices are between £40 & £48 each - which is excellent value for money considering it is a four-hour experience. Certainly better value for money than TaskMaster Live. There are plenty of toilets, a not-too-extortionate bar, and snacks to buy. There's a large outside section with a separate bar (which was closed for the test).

You can spend the four hours mooching about, drinking cocktails, playing at the arcade, and chatting to townsfolk. Or you can whizz about exploring graveyards, conducting matchmaking, and punching codes into terminals.

It is a lot of fun and cleverly constructed. There are plot-strands which we only caught glimpses of, but didn't get a chance to explore. There are a delightful amount of pop-culture references (some of which I had explained to me by an over-excited teenager) and a healthy amount of satire.

So, yeah, I can see us going back - although perhaps when it is a little bit warmer!

Photos and (Minor) Spoilers

One quest had me wearing these nifty glasses. I was rather sad when I had to give them up. Me and an actor both wearing red glasses.

There are seemingly hundreds of little details about the world scattered throughout the venue. List of prohibited things.

There was a minor bit of confusion during one trail where we were asked information about "Terrence" which, coincidentally, is my name. Luckily, we cleared it up by establishing that I am not a platypus. Newspaper story about Terrence the Platypus.

Some quests are slightly spooky - but most are a bit silly. Grave and candle.

The overall texture of the world is lush. Steampunk wooden computer with glitchy screen.

]]>
1
@edent <![CDATA[Presenting ActivityBot at FOSDEM]]> https://shkspr.mobi/blog/?p=58000 2025-02-10T09:34:01Z 2025-02-09T12:34:17Z <![CDATA[Because I'm an optimist, I submitted a few talks to FOSDEM in the hope one might be accepted. Because I'm lucky, I got two speaking slots. Because I'm an idiot, I decided to do both talks. On the same day. An hour apart. On opposite ends of the venue. Fool! My first talk was at the Social Web Birds-of-a-Feather session. I told people about my ActivityBot social networking server and how I built it into a single file. In the spirit of minimalism, I only had 8 minutes to present. Time for a…]]> <![CDATA[

Because I'm an optimist, I submitted a few talks to FOSDEM in the hope one might be accepted. Because I'm lucky, I got two speaking slots. Because I'm an idiot, I decided to do both talks. On the same day. An hour apart. On opposite ends of the venue.

Fool!

My first talk was at the Social Web Birds-of-a-Feather session. I told people about my ActivityBot social networking server and how I built it into a single file. In the spirit of minimalism, I only had 8 minutes to present. Time for a speed-run!

Sadly / Luckily there's no audio or video of the session (if you have some, let me know) so you'll have to make do with some slides. Speaker notes are included - read them rapidly for the full effect!

I built an ActivityPub bot server in 64KB of PHP. Sorry!

Feedback and Photos

]]>
4
@edent <![CDATA[endless.downward.spiral - is this the beginning of the end of What3Words?]]> https://shkspr.mobi/blog/?p=58027 2025-02-08T12:25:52Z 2025-02-08T12:34:25Z <![CDATA[Long-time readers know that I am not a fan of What Three Words. I think it is a closed, proprietary, and user-unfriendly attempt to enclose the commons. I consider that it has some dangerous failure modes. A year ago, The Financial Times wrote about What3Words' business woes. But it looks like things are about to get a lot worse. As reported by a user on Reddit, Mercedes cars no longer support What3words. I was in touch with What3words customer support and they confirmed me that Mercedes…]]> <![CDATA[

Long-time readers know that I am not a fan of What Three Words. I think it is a closed, proprietary, and user-unfriendly attempt to enclose the commons. I consider that it has some dangerous failure modes.

A year ago, The Financial Times wrote about What3Words' business woes. But it looks like things are about to get a lot worse.

As reported by a user on Reddit, Mercedes cars no longer support What3words.

I was in touch with What3words customer support and they confirmed me that Mercedes didn’t renewed their What3word license so blocking the service embedded in all their products.

Now, we shouldn't necessarily trust what a random customer service agent says. Nor should we trust a single post on a forum. But if you visit the W3W cars page you'll see a list of the vehicle manufacturers they work with. List of car manufacturers.

Mercedes-Benz is still there - but clicking on the link takes you to a dead page. The links to other manufacturers work.

There's also a popular YouTuber reporting the same problem:

The pull quote from Mercedes themselves is:

The What3Words features was discontinued in December due to low usage by our customers.

OK, so one car company deciding not to use the app isn't the end of the world, right?

Well, as my friend Bloor points out:

This would be the same Mercedes Benz who invested in w3w. […] I’d say that if one of your investors doesn’t want to buy your product, then your product fucking sucks. And/or If your licence fees are so high that even an INVESTOR won’t pay them, your pricing fucking sucks.

He also shows that, apparently, a Director of W3W from Mercedes resigned late last year.

So, is it game over for W3W? Their report from July 2024 identified these risks:

Commercial risk The success of the business is dependent on the development, conversion and retention of a pipeline of commercial contracts to take the business cash flow positive and profitable. Behavioural change risk The Group has created a new addressing format, with the aim of becoming a universal standard for location referencing. A key aspect of this is acquiring and retaining a high volume of newly engaged consumers, creating wide-scale network effects and consumer behaviour change to ultimately deliver commercial contracts.

Even going by the publicly available plans the cost of a W3W lookup is about ⅓rd of a penny. I imagine that Mercedes pay considerably less than that. And yet, an investor who had 4,030,000 Series C1 Preferred Shares, have decided that their customers aren't interested enough in W3W to justify the cost of integrating it into their vehicles.

That's the commercial risk and the behavioural change risk both at once. It appears to me that they can't retain their current corporate customers and don't seem to be able to attract or keep individual consumers.

W3W only succeeds with sufficient network effects. After 12 years of operation, it is yet to reach anything approaching critical mass. Its attempt to insinuate itself within the emergency services (who use it for free) doesn't seem to have transformed into mass adoption. Its premium customers appear to be dropping it. Search and Rescue teams warn against using it.

What's left? The inherent technical flaws in the What3Words algorithm can't be fixed and the intractable commercial flaws in its business model aren't helping. The W3W financial report announced losses of £16 million, against a turnover of £1 million.

How much longer can they go on?

]]>
16
@edent <![CDATA[Review: Voviggol Finger Ring Presentation Clicker ★★★★⯪]]> https://shkspr.mobi/blog/?p=55525 2025-02-05T08:56:47Z 2025-02-07T12:34:10Z <![CDATA[I was packing for FOSDEM when I suddenly realised that I'd lost my clicker. Disaster! Here's a shortlist of what I need in a presentation remote: Ring style to fit on my finger USB-C Works on Linux Frickin' lazor beams! The only one I could find which matched all that was this Voviggol unit. Ring Here's how it looks hooked to my hand: The ring is stretchy and will fit around the thickest thumb. It grips the finger tight and didn't fly off even when I was gesticulating wildly. USB-C …]]> <![CDATA[

I was packing for FOSDEM when I suddenly realised that I'd lost my clicker. Disaster!

Here's a shortlist of what I need in a presentation remote:

  1. Ring style to fit on my finger
  2. USB-C
  3. Works on Linux
  4. Frickin' lazor beams!

The only one I could find which matched all that was this Voviggol unit.

A clicker with a dual USB A/C puck.

Ring

Here's how it looks hooked to my hand:

Finger sized clicker.

The ring is stretchy and will fit around the thickest thumb. It grips the finger tight and didn't fly off even when I was gesticulating wildly.

USB-C for everything

It uses USB-C for charging and comes with a removeable dongle which has both A and C connectors. The dongle attaches into the clicker with a magnet.

If you connect the clicker to a computer to charge, it doesn't show up as a device.

Linux Compatibility

On Linux, the dongle shows up as:

1ea7:1066 SHARKOON Technologies GmbH Wireless Present

It worked with both USB-A and -C, and showed the same information.

Android

It also shows up as a keyboard on Android. Mostly useful for if you're reading a book and want to flick through the pages without using the touchscreen.

Connecting it to Android will temporarily show a mouse cursor on screen. Running evtest on Linux shows the device presenting as:

/dev/input/event18: Wireless Present
/dev/input/event19: Wireless Present Mouse
/dev/input/event20: Wireless Present Consumer Control
/dev/input/event21: Wireless Present System Control

So a fairly generic dongle, but with a restricted set of inputs.

LAZORS!

Press big button. Red beam of light. Attracts cats. Nice.

Buttons

The left and right buttons can be reprogrammed. By default, a short press gives you page up/down and a long press gives you volume up/down.

The top button is screen-blank (it literally sends the letter "b").

The bottom button is odd. The first time you press it, the device sends esc. The second time you press it, Left Shift + F5

Personally, I'd've preferred if the remote just had larger left and right buttons, but I guess the others are kind of useful.

All buttons worked on Linux and Android.

Change the buttons

Press and hold < and > for 3 second and the clicker will swap between 3 modes:

  1. Up / Down
  2. Left / Right
  3. Page Up / Page Down

There's no way to change the other buttons.

Interface

There's a tiny and somewhat flimsy switch on the side to turn it on or off.

A little LED goes red when it is charging, and flashes blue when you press a button. Fairly unobtrusive.

Distance

I didn't formally test how far away it worked, but I pranced around the stage in a noisy radio environment and the 2.4GHz radio worked just fine.

Is it encrypted? Dunno. Probably best not to leave it plugged in all the time.

Verdict

For £20? Basically fine. I prefer ring-style presentation remotes because they let my flap my hands without losing the controller.

The buttons are a smidge small, and you'll probably forget how to reprogram it. But it does the job well. It came with a comically short A-C charging cable which was immediate e-waste.

Let's hope I don't lose this one!

]]>
1