Private/OpenTag3DImage.ps1
|
# Builds a full NTAG21x memory image from an OpenTag3D record or payload. # Shared by Export-OpenTag3DPayload and the GUI's edit screen. function Get-OpenTag3DTagSpec { [CmdletBinding()] param([Parameter(Mandatory)] [ValidateSet('NTAG213','NTAG215','NTAG216')] [string]$TagType) # UserSize = writable user memory, TotalSize = full dump incl. header + config pages switch ($TagType) { 'NTAG213' { @{ UserSize = 144; TotalSize = 180; CC = 0x12 } } 'NTAG215' { @{ UserSize = 504; TotalSize = 540; CC = 0x3E } } 'NTAG216' { @{ UserSize = 888; TotalSize = 924; CC = 0x6D } } } } function New-OpenTag3DNdefRecord { <# .SYNOPSIS Wraps an OpenTag3D payload in an application/opentag3d MIME record. #> [CmdletBinding()] param([Parameter(Mandatory)] [byte[]]$Payload) $type = [Text.Encoding]::ASCII.GetBytes('application/opentag3d') if ($Payload.Length -lt 256) { # MB | ME | SR | TNF 0x02 (MIME media) $header = [byte[]]@(0xD2, [byte]$type.Length, [byte]$Payload.Length) } else { $header = [byte[]]@(0xC2, [byte]$type.Length, [byte](($Payload.Length -shr 24) -band 0xFF), [byte](($Payload.Length -shr 16) -band 0xFF), [byte](($Payload.Length -shr 8) -band 0xFF), [byte]($Payload.Length -band 0xFF)) } return ,[byte[]]($header + $type + $Payload) } function Get-OpenTag3DPayloadStart { <# .SYNOPSIS Offset of the application/opentag3d payload within user memory. .DESCRIPTION Get-OpenTag3DNdefPayload needs the whole message; this needs only the headers, so a few pages read off a tag are enough to find the version bytes without pulling the entire payload back. Like the full reader it takes the first application/opentag3d record rather than assuming the first record is the right one. Returns $null when the bytes given stop before that record's header ends, or when the message holds no such record - the caller decides whether that is worth reporting. #> [CmdletBinding()] param([Parameter(Mandatory)] [byte[]]$UserMemory) $msg = Get-OpenTag3DNdefMessage -UserMemory $UserMemory if (-not $msg) { return $null } # Walk to the end of what was read rather than the message's declared length: the point # of this function is to work on a partial read. foreach ($rec in Get-OpenTag3DNdefRecord -Buffer $UserMemory -Start $msg.Start) { if ($rec.Tnf -eq 0x02 -and $rec.Type -eq 'application/opentag3d' -and -not $rec.Chunked) { return $rec.PayloadStart } } return $null } function Get-OpenTag3DRecordPayload { <# .SYNOPSIS The payload inside an application/opentag3d NDEF record. .DESCRIPTION The inverse of New-OpenTag3DNdefRecord, for when a record has to be unwrapped, changed and wrapped again - converting a lookup result between spec versions, for instance. Returns $null if this is not a record of that type. #> [CmdletBinding()] param([Parameter(Mandatory)] [byte[]]$Record) if ($Record.Length -lt 3) { return $null } $flags = $Record[0] $short = ($flags -band 0x10) -ne 0 $typeLen = $Record[1] $i = 2 if ($short) { $payLen = $Record[$i]; $i += 1 } else { if ($Record.Length -lt 6) { return $null } $payLen = ([int]$Record[$i] -shl 24) -bor ([int]$Record[$i+1] -shl 16) -bor ([int]$Record[$i+2] -shl 8) -bor $Record[$i+3] $i += 4 } if (($flags -band 0x08) -ne 0) { $i += 1 } # ID length present if ($i + $typeLen + $payLen -gt $Record.Length) { return $null } $type = [Text.Encoding]::ASCII.GetString($Record[$i..($i + $typeLen - 1)]) if ($type -ne 'application/opentag3d') { return $null } $i += $typeLen if ($payLen -le 0) { return $null } return ,[byte[]]$Record[$i..($i + $payLen - 1)] } function New-OpenTag3DImage { <# .SYNOPSIS Builds a complete tag image around an NDEF record or a raw payload. .PARAMETER Data For -Format Ndef, the full NDEF record. For -Format Raw, the bare payload. #> [CmdletBinding()] param( [Parameter(Mandatory)] [byte[]]$Data, [Parameter(Mandatory)] [ValidateSet('NTAG213','NTAG215','NTAG216')] [string]$TagType, [Parameter(Mandatory)] [ValidateSet('Ndef','Raw')] [string]$Format ) $spec = Get-OpenTag3DTagSpec -TagType $TagType if ($Format -eq 'Ndef') { # NDEF TLV: 0x03, length (1 byte, or 0xFF + 2 bytes if >254), message, 0xFE terminator $len = if ($Data.Length -lt 255) { ,[byte]$Data.Length } else { [byte[]]@(0xFF, [byte](([int]$Data.Length -shr 8) -band 0xFF), [byte]($Data.Length -band 0xFF)) } $content = [byte[]]@(0x03) + $len + $Data + [byte[]]@(0xFE) } else { $content = $Data } if ($content.Length -gt $spec.UserSize) { # OpenTag3D 2.x dropped NTAG213: 216 bytes of payload plus NDEF framing cannot fit # 144 bytes of user memory, whatever the data. Say so rather than a bare size. $payloadVersion = if ($Format -eq 'Ndef' -and $Data.Length -gt 24) { Get-OpenTag3DPayloadVersion -Payload $Data[24..($Data.Length - 1)] } else { Get-OpenTag3DPayloadVersion -Payload $Data } if ($TagType -eq 'NTAG213' -and $payloadVersion -and (Get-OpenTag3DSpec -SpecVersion $payloadVersion).Major -ge 2) { throw "OpenTag3D $payloadVersion cannot be written to an NTAG213: $($Data.Length) bytes of record plus NDEF framing comes to $($content.Length), against 144 bytes of user memory. Use an NTAG215 or NTAG216, or build the tag as 1.003." } throw "Content is $($content.Length) bytes - exceeds $TagType user memory ($($spec.UserSize))." } $image = [byte[]]::new($spec.TotalSize) # --- pages 0-2: placeholder UID with valid BCCs, internal, static lock bytes --- # Real UIDs are factory-programmed and read-only; these pages exist so the file is a # structurally valid dump, not because they are ever written to a tag. $u = [byte[]]@(0x04,0x00,0x00,0x00,0x00,0x00,0x00) # 0x04 = NXP manufacturer [Array]::Copy($u, 0, $image, 0, 3) $image[3] = 0x88 -bxor $u[0] -bxor $u[1] -bxor $u[2] # BCC0 [Array]::Copy($u, 3, $image, 4, 4) $image[8] = $u[3] -bxor $u[4] -bxor $u[5] -bxor $u[6] # BCC1 $image[9] = 0x48 # internal $image[10] = 0x00 # static lock 0 $image[11] = 0x00 # static lock 1 # --- page 3: capability container --- $image[12] = 0xE1 $image[13] = 0x10 $image[14] = [byte]$spec.CC $image[15] = 0x00 # --- pages 4+: user memory --- $userStart = 16 # page 4 $userEnd = $spec.TotalSize - 20 # first byte of the dynamic lock page [Array]::Copy($content, 0, $image, $userStart, $content.Length) # --- explicitly zero the remainder of user memory --- $padStart = $userStart + $content.Length $padLength = $userEnd - $padStart if ($padLength -gt 0) { [Array]::Clear($image, $padStart, $padLength) Write-Verbose "Padded $padLength bytes with 0x00 (offset $padStart to $($userEnd - 1))" } # --- final 5 pages: dynamic lock + CFG0 + CFG1 + PWD + PACK (factory defaults) --- $trailer = [byte[]]@( 0x00,0x00,0x00,0xBD, # dynamic lock bytes 0x04,0x00,0x00,0xFF, # CFG0: MIRROR, RFUI, MIRROR_PAGE, AUTH0 0x00,0x05,0x00,0x00, # CFG1: ACCESS, RFUI 0xFF,0xFF,0xFF,0xFF, # PWD 0x00,0x00,0x00,0x00 # PACK + RFUI ) [Array]::Copy($trailer, 0, $image, $spec.TotalSize - 20, 20) return ,[byte[]]$image } |