A Matrix Card is typically used in online games for authentication. I build a simple version of a Matrix Card in .NET Core today, let's check it out.

How Matrix Card Works

This is a typical Matrix Card for the game "World of Warcraft". It has a serial number to bind to the owner's game account, and then use the numbers in the table for validation.

Suppose the hacker already knows your account number and password, but because you're tied to a Matrix Card. Therefore, when logging in to the game, the game will randomly select a certain number (generally 3) in the grid, asked to enter the corresponding number, such as A1=928, C8=985, B10=640. And because the hacker didn't get your Matrix Card, he didn't know the number in the matrix and couldn't log in to your account. 

Even if the hacker crawled your input several times, because per each login, the cells are randomly selected combinations that are all different, so for a 7X12 secret card, hackers need to catch tens of thousands of times, in order to fully grasp your Matrix Card information. However, the account owner can change the Matix Card at any time.

.NET Core Implementation

I have written two articles on this same topic 8 years ago. But nowadays, with modern .NET Core and C#, we can do it better. I've built a demo with Matrix Card generation, Validation, Serialization, and Loading Data.

For a complete source code: https://go.edi.wang/fw/5d12778d

Cell

Cell is used to describe cells in the Matrix Card. For a Cell, it has three properties: row, column, and value. I use RowIndex, ColIndex, Value. For ease of display, I added the ColumnName property to display the column label as an English letter (slightly different from the official Matrix Card design here).

public class Cell
{
    public int RowIndex { get; }
    public int ColIndex { get; }
    public ColumnCode ColumnName => (ColumnCode)ColIndex;
    public int Value { get; set; }

    public Cell(int rowIndex, int colIndex, int val = 0)
    {
        RowIndex = rowIndex;
        ColIndex = colIndex;
        Value = val;
    }
}
public enum ColumnCode
{
    A = 0,
    B = 1,
    C = 2,
    D = 3,
    E = 4
}

ColumnCode can expand to your own needs, and I've only written five values so far.

Card

Card is used to describe a Matrix Card. So in addition to a bunch of Cell, you will have a card number (Id), as well as information such as the number of rows, columns, and so on.

public class Card
{
    public Guid Id { get; set; }
    public int Rows { get; set; }
    public int Cols { get; set; }
    public List<Cell> Cells { get; set; }

    public Card(int rows = 5, int cols = 5)
    {
        Id = Guid.NewGuid();

        Rows = rows;
        Cols = cols;
        Cells = new List<Cell>();
    }
}

However, considering that the serialized data shouldn't have too much redundant information, adding the CellData property is used to simplify Cells data representation. Put the data in Cells into a comma-separated string. For this purpose, it is wrapped in a Json string with Card properties and will not look too long.

[JsonIgnore]
public List<Cell> Cells { get; set; }

public string CellData
{
    get
    {
        var vals = Cells.Select(c => c.Value);
        return string.Join(',', vals);
    }
}
Generating Matrix Card data

First, based on the number of rows and columns, a two-bit array is generated, populated with random values of 0-100. The range of values can be changed according to your own needs.

private static int[,] GenerateRandomMatrix(int rows, int cols)
{
    var r = new Random();
    var arr = new int[rows, cols];

    for (var row = 0; row < rows; row++)
    {
        for (var col = 0; col < cols; col++)
        {
            arr[row, col] = r.Next(0, 100);
        }
    }

    return arr;
}

The result value is then assigned to the Cells property by row and column

private void FillCellData(int[,] array)
{
    for (var row = 0; row < Rows; row++)
    {
        for (var col = 0; col < Cols; col++)
        {
            var c = new Cell(row, col, array[row, col]);
            Cells.Add(c);
        }
    }
}

Printing the Matrix Card information to console:

private static void PrintCard(Card card)
{
    Console.WriteLine("  |\tA\tB\tC\tD\tE\t");
    Console.WriteLine("----------------------------------------------");

    var i = 0;
    for (var k = 0; k < card.Rows; k++)
    {
        Console.Write(k + " |\t");
        for (var l = 0; l < card.Cols; l++)
        {
            Console.Write(card.Cells[i].Value + "\t");
            i++;
        }
        Console.WriteLine();
    }
}
Load Cells data

In addition to generating data, we also support loading existing data into Cells.

Because the previously simplified Cells data is a string split by a comma, we need to break it up into an array and convert the type back to int, and then populate the Cells property with the previously written FillCellData() method.

public Card LoadCellData(string strMatrix)
{
    var tempArrStr = strMatrix.Split(',');
    if (tempArrStr.Length != Rows * Cols)
    {
        throw new ArgumentException(
            "The number of elements in the matrix does not match the current card cell numbers.", nameof(strMatrix));
    }

    var arr = new int[Rows, Cols];
    var index = 0;

    for (var row = 0; row < Rows; row++)
    {
        for (var col = 0; col < Cols; col++)
        {
            arr[row, col] = int.Parse(tempArrStr[index]);
            index++;
        }
    }

    FillCellData(arr);
    return this;
}
Random selection and validation

I also use Random type to randomly select a given number of cells within a given range of ranks, but not from Cells property, because we do not need to return the value of the actual cell. In the server/client scenario, validation should always be done on the server, not on the client, so do not return values.

public IEnumerable<Cell> PickRandomCells(int howMany)
{
    var r = new Random();
    for (var i = 0; i < howMany; i++)
    {
        var randomCol = r.Next(0, Cols);
        var randomRow = r.Next(0, Rows);
        var c = new Cell(randomRow, randomCol);
        yield return c;
    }
}

Because the cell information returned contains rows and columns, we can compare the information that already exists in Cells when the user enters the value.

For each cell that needs to be verified:

  1. Find cells with the same ranks in Cells.
  2. Comparing whether the values of these two are equal, you no longer need to verify the next cell if you encounter inequality and return false directly.

With the super easy LINQ in C#, we can do it without manually write many for loops:

public bool Validate(IEnumerable<Cell> cellsToValidate)
{
    return (
        from cell in cellsToValidate
        let thisCell = Cells.Find(p => p.ColIndex == cell.ColIndex && p.RowIndex == cell.RowIndex)
        select thisCell.Value == cell.Value)
        .All(matches => matches);
}

Full source code and demo application: https://go.edi.wang/fw/5d12778d