Private/QrCode.ps1
|
# The QR code a Technician points a phone camera at. # # The device flow asks the Technician to open one page on their own phone, and that page is # the same every time. So the symbol for it is computed once, at development time, and # shipped here as data - rather than encoding a URL at runtime, which would mean carrying a # Reed-Solomon implementation inside a diagnostics tool. That is exactly the sort of code # that is subtly wrong for a year and then discovered by a Technician standing at a # Customer Site with a phone that will not scan. # # Generated with QRCoder 1.3.3 at error correction level M, which puts this at version 3: # 29x29 modules. It encodes $script:DeviceLoginUri and nothing else. Regenerating it means # replacing the whole table; the tests assert the structure every QR symbol must have - # finder patterns, both timing patterns, the alignment pattern - so a truncated or mangled # paste fails the suite instead of shipping as a square that scans to nothing. # # To regenerate, if the address ever changes. The module is a development tool and is not # a dependency of Gutcheck: # # Install-Module QRCodeGenerator -Scope CurrentUser # Import-Module QRCodeGenerator # loads QRCoder alongside it # $d = [QRCoder.QRCodeGenerator]::new().CreateQrCode( # 'https://login.microsoft.com/device', [QRCoder.QRCodeGenerator+ECCLevel]::M) # $m = $d.ModuleMatrix # includes a 4-module quiet zone, which we re-add ourselves # for ($y = 4; $y -lt $m.Count - 4; $y++) { # -join (4..($m[$y].Count - 5) | ForEach-Object { if ($m[$y][$_]) { '#' } else { '.' } }) # } $script:DeviceLoginUri = 'https://login.microsoft.com/device' $script:DeviceLoginQr = @( '#######...######...##.#######' '#.....#..#..#####.....#.....#' '#.###.#.##..##.#..###.#.###.#' '#.###.#.#.....##.##...#.###.#' '#.###.#.##.#...######.#.###.#' '#.....#.#.###....#..#.#.....#' '#######.#.#.#.#.#.#.#.#######' '........###...#.#.###........' '#.#####..###.....#.#..#####..' '...#.#.##.######..#######...#' '#....###...#.######.#.###....' '##..#..#.#...#.##..#.##..#.#.' '####..#.......##.#..##...##..' '.##.#...#.###...#########...#' '#.###.#.##..#..###..#.##.##..' '##.###.#..###.#....######..#.' '.#.#####.#.##..#.#.#.....##..' '#.#.......#..##.#########.#.#' '#.#.#.#....#.####.....##..#..' '#...##...##..#.#...#...#...#.' '#.#.###.#.....#..##.#####.###' '........#.##....#####...#####' '#######....##..#.#.##.#.###..' '#.....#.#.###.#.#..##...#..##' '#.###.#.#..#...###..#####.##.' '#.###.#.#..##.#.#.###....####' '#.###.#.#...#..##....#######.' '#.....#..##.#..##.#.###.##.#.' '#######.####.###.#.#...#..#..' ) # A symbol printed hard against a dark console is a symbol no camera resolves. Four modules # is what the specification asks for and what scanners are built to expect. $script:QrQuietZone = 4 # Every line a Run prints under the symbol starts three spaces in, and a square that starts # at column zero reads as though it belongs to something else. Written with no colour, so # it is terminal background rather than more quiet zone - otherwise the white block starts # where it started before and nothing looks aligned. $script:QrIndent = ' ' # U+2580 UPPER HALF BLOCK. A console cell is about twice as tall as it is wide, so a module # drawn as one cell comes out stretched, and a module drawn as two spaces comes out square # at four times the area. This character splits the cell instead: its foreground colour is # the module above, its background colour the module below. Two rows to a line, one column # to a module, and the modules land square at a quarter of the size. $script:QrHalfBlock = [char]0x2580 function Test-QrHalfBlockRenders { <# .SYNOPSIS Whether this console can print the half block at all. .DESCRIPTION Asked rather than assumed. It survives the OEM code pages Windows consoles default to - CP850 and CP437 both carry it - but a console set to something that cannot represent it would draw a grid of substitution marks, which is a worse QR code than no QR code. The round trip is the honest test: encode the character the way this console will, decode it back, and see whether it is still itself. #> [CmdletBinding()] [OutputType([bool])] param() try { $encoding = [Console]::OutputEncoding if (-not $encoding) { return $false } [bool]($encoding.GetString($encoding.GetBytes($script:QrHalfBlock)) -eq [string]$script:QrHalfBlock) } catch { $false } } function Get-QrDisplayWidth { <# .SYNOPSIS How many columns drawing the symbol would take. Pure. #> [CmdletBinding()] [OutputType([int])] param( [Parameter(Mandatory)][int]$Modules, [switch]$Wide ) $across = $Modules + (2 * $script:QrQuietZone) if ($Wide) { return ($across * 2) + $script:QrIndent.Length } $across + $script:QrIndent.Length } function Test-QrFits { <# .SYNOPSIS Whether this console is wide enough to draw the symbol undistorted. .DESCRIPTION A symbol that wraps is not a symbol, it is two halves of one - and it would scroll the code and the URL out of sight on the way past. A narrow window is a reason to print the address and stop, not a reason to draw something unreadable. #> [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)][int]$Modules, [switch]$Wide ) $needed = Get-QrDisplayWidth -Modules $Modules -Wide:$Wide try { $available = $Host.UI.RawUI.WindowSize.Width } catch { return $false } # A host that reports nothing sensible - a redirected stream, a remote session - is one # where drawing a picture is not the helpful choice. if (-not $available -or $available -le 0) { return $false } $available -ge $needed } function Get-QrPaddedRow { <# .SYNOPSIS The symbol's rows with the quiet zone around them. Pure. .DESCRIPTION The quiet zone is drawn rather than left to the console, because a console's background is whatever the Technician chose and a camera needs light. An odd number of rows gains one more light row, so that the half blocks pair up evenly. #> [CmdletBinding()] [OutputType([string[]])] param([Parameter(Mandatory)][string[]]$Row, [switch]$EvenRows) $blank = '.' * ($Row[0].Length + (2 * $script:QrQuietZone)) $side = '.' * $script:QrQuietZone $padded = New-Object System.Collections.Generic.List[string] for ($i = 0; $i -lt $script:QrQuietZone; $i++) { $padded.Add($blank) } foreach ($line in $Row) { $padded.Add($side + $line + $side) } for ($i = 0; $i -lt $script:QrQuietZone; $i++) { $padded.Add($blank) } if ($EvenRows -and ($padded.Count % 2)) { $padded.Add($blank) } $padded.ToArray() } function Get-QrColour { <# .SYNOPSIS A module's colour. Set explicitly, both of them: whatever a Technician has made their console look like, a QR code is black on white. #> [CmdletBinding()] [OutputType([string])] param([Parameter(Mandatory)][char]$Module) if ($Module -eq '#') { 'Black' } else { 'White' } } function Write-QrLinePair { <# .SYNOPSIS Two rows of modules on one console line, written in runs rather than per module - which is the difference between drawing instantly and drawing visibly. #> [CmdletBinding()] param( [Parameter(Mandatory)][AllowEmptyString()][string]$Upper, [Parameter(Mandatory)][AllowEmptyString()][string]$Lower ) Write-Host $script:QrIndent -NoNewline $start = 0 for ($i = 1; $i -le $Upper.Length; $i++) { $changed = $i -eq $Upper.Length -or $Upper[$i] -ne $Upper[$start] -or $Lower[$i] -ne $Lower[$start] if ($changed) { Write-Host ([string]$script:QrHalfBlock * ($i - $start)) -NoNewline ` -ForegroundColor (Get-QrColour -Module $Upper[$start]) ` -BackgroundColor (Get-QrColour -Module $Lower[$start]) $start = $i } } Write-Host '' } function Write-QrWideRow { <# .SYNOPSIS One row of modules as two-space blocks, for a console that cannot print the half block. Twice as wide and twice as tall, and made of spaces, which every console can draw whatever its code page. #> [CmdletBinding()] param([Parameter(Mandatory)][AllowEmptyString()][string]$Row) Write-Host $script:QrIndent -NoNewline $start = 0 for ($i = 1; $i -le $Row.Length; $i++) { if ($i -eq $Row.Length -or $Row[$i] -ne $Row[$start]) { Write-Host (' ' * ($i - $start)) -NoNewline -BackgroundColor (Get-QrColour -Module $Row[$start]) $start = $i } } Write-Host '' } function Show-DeviceLoginQr { <# .SYNOPSIS Draws the QR code for the device login page, and says whether it drew it. .DESCRIPTION The shipped symbol encodes one fixed address. If Entra ever answers with a different verification_uri, drawing it would point a Technician's phone at the wrong page - a failure that looks like the phone's fault and is not. So the address is compared first, and anything unexpected falls back to printing what Entra actually said. #> [CmdletBinding()] [OutputType([bool])] param([Parameter(Mandatory)][AllowNull()][AllowEmptyString()][string]$VerificationUri) if ($VerificationUri -ne $script:DeviceLoginUri) { return $false } $compact = Test-QrHalfBlockRenders if (-not (Test-QrFits -Modules $script:DeviceLoginQr.Count -Wide:(-not $compact))) { return $false } Write-Host '' if ($compact) { $rows = Get-QrPaddedRow -Row $script:DeviceLoginQr -EvenRows for ($i = 0; $i -lt $rows.Count; $i += 2) { Write-QrLinePair -Upper $rows[$i] -Lower $rows[$i + 1] } } else { foreach ($row in (Get-QrPaddedRow -Row $script:DeviceLoginQr)) { Write-QrWideRow -Row $row } } Write-Host '' $true } |