C#/LINQ to find Thompson/Speedtouch router's default SSID/Access keys

Whilst researching content for a wireless security presentation for university, I came across this page (now dead, unfortunately). It details the algorithm used to calculate the default password for some O2/BE/BT routers.

I thought it would provide an interesting exercise to convert the algorithm to C#, using LINQ. Here it is:

using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

class Program
{
    static void Main(string[] args)
    {
        if (args.Length < 4)
        {
            Console.WriteLine("Need 4 args: SSID, year, monthFrom, monthTo");
            Console.WriteLine("SSID = Hex part of SSID");
            Console.WriteLine("Year = 2 digit year e.g. 08");
            Console.WriteLine("monthFrom and monthTo = 0-indexed month numbers");
            Environment.Exit(1);
        }

        string ssid = args[0].ToUpper();
        string year = args[1];
        int monthFrom = int.Parse(args[2]);
        int monthTo = int.Parse(args[3]);

        string key = FindKey(ssid, year, monthFrom, monthTo) ?? "No key found!";

        Console.WriteLine("Key is: " + key);
    }

    static string FindKey(string ssid, string year, int mFrom, int mTo)
    {
        int SSIDlen = ssid.Length;
        var sha1 = SHA1.Create();

        var digitChars = Enumerable.Range((int)'0', 10);
        var letterChars = Enumerable.Range((int)'A', 26);

        // Create hex strings of 0-9 and A-Z.
        var chars = digitChars.Concat(letterChars).Select(i => i.ToString("X"));

        var charTuples = from c1 in chars
                         from c2 in chars
                         from c3 in chars
                         from month in Enumerable.Range(mFrom, mTo - mFrom)
                         select Tuple.Create(month, c1, c2, c3);

        return charTuples.Select(t =>
        {
            // Make a correctly-formed serial.
            var serial = "CP" + year + (t.Item1 < 10 ? "0" : "") +
                t.Item1 + t.Item2 + t.Item3 + t.Item4;

            var serialBytes = Encoding.UTF8.GetBytes(serial);
            var sha1Bytes = sha1.ComputeHash(serialBytes);

            var hash = String.Join("", sha1Bytes.Select(b => b.ToString("X")));

            // Last 3 bytes are the SSID (
            var testSSID = hash.Substring(hash.Length - SSIDlen);

            // If we've got a match, the first 5 bytes are the default access key.
            return testSSID == ssid ? hash.Substring(0, 10) : "";
        }).FirstOrDefault(s => s != "");
    }
}