This commit is contained in:
nicolas.dorier 2023-10-04 17:13:37 +09:00
parent 99467d8e7a
commit daeb09a907
No known key found for this signature in database
GPG Key ID: 6618763EF09186FE
21 changed files with 1948 additions and 1860 deletions

168
.editorconfig Normal file
View File

@ -0,0 +1,168 @@
# editorconfig.org
# top-most EditorConfig file
root = true
# Default settings:
# A newline ending every file
# Use 4 spaces as indentation
[*]
insert_final_newline = true
indent_style = space
indent_size = 4
charset = utf-8
space_before_self_closing = true
dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
end_of_line = crlf
dotnet_style_namespace_match_folder = true:suggestion
[*.json]
indent_size = 2
[swagger*.json]
indent_size = 4
# C# files
[*.cs]
# New line preferences
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_within_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
csharp_indent_labels = flush_left
# avoid this. unless absolutely necessary
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
# only use var when it's obvious what the variable type is
csharp_style_var_for_built_in_types = false:none
csharp_style_var_when_type_is_apparent = false:none
csharp_style_var_elsewhere = false:suggestion
# use language keywords instead of BCL types
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
# name all constant fields using PascalCase
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.required_modifiers = const
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# internal and private fields should be _camelCase
dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
dotnet_naming_style.camel_case_underscore_style.required_prefix = _
dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
# Code style defaults
dotnet_sort_system_directives_first = true
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = false
# Expression-level preferences
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
# Expression-bodied members
csharp_style_expression_bodied_methods = false:none
csharp_style_expression_bodied_constructors = false:none
csharp_style_expression_bodied_operators = false:none
csharp_style_expression_bodied_properties = true:none
csharp_style_expression_bodied_indexers = true:none
csharp_style_expression_bodied_accessors = true:none
# Pattern matching
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
# Null checking preferences
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = do_not_ignore
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
csharp_style_prefer_null_check_over_type_check = true:warning
csharp_prefer_simple_using_statement = true:warning
csharp_using_directive_placement = outside_namespace:silent
csharp_prefer_braces = true:silent
csharp_style_namespace_declarations = file_scoped:suggestion
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_top_level_statements = true:silent
csharp_style_prefer_primary_constructors = true:suggestion
csharp_style_expression_bodied_lambdas = true:silent
# C++ Files
[*.{cpp,h,in}]
curly_bracket_next_line = true
indent_brace_style = Allman
# Xml project files
[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]
indent_size = 2
# Xml build files
[*.builds]
indent_size = 2
# Xml files
[*.{xml,stylecop,resx,ruleset}]
indent_size = 2
# Xml config files
[*.{props,targets,config,nuspec}]
indent_size = 2
# Shell scripts
[*.sh]
end_of_line = lf
[*.{cmd, bat}]
end_of_line = crlf

View File

@ -3,18 +3,22 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.NTag424", "src\BTCPayServer.NTag424\BTCPayServer.NTag424.csproj", "{1AF51732-C36B-48DE-BEE5-4C5AA6EB0DAB}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer.NTag424", "src\BTCPayServer.NTag424\BTCPayServer.NTag424.csproj", "{1AF51732-C36B-48DE-BEE5-4C5AA6EB0DAB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BoltCardTools.Tests", "tests\BoltCardTools.Tests.csproj", "{4ABD12CC-3B7C-4C8C-8576-19F32C402584}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BoltCardTools.Tests", "tests\BoltCardTools.Tests.csproj", "{4ABD12CC-3B7C-4C8C-8576-19F32C402584}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{AE043F8E-1026-4502-8E7A-6F9C6C75EAF6}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitattributes = .gitattributes
.gitignore = .gitignore
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1AF51732-C36B-48DE-BEE5-4C5AA6EB0DAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1AF51732-C36B-48DE-BEE5-4C5AA6EB0DAB}.Debug|Any CPU.Build.0 = Debug|Any CPU
@ -25,4 +29,7 @@ Global
{4ABD12CC-3B7C-4C8C-8576-19F32C402584}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4ABD12CC-3B7C-4C8C-8576-19F32C402584}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@ -1,191 +1,189 @@
using System;
using System.Diagnostics.Metrics;
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using static BoltCardTools.Helpers;
namespace BoltCardTools
namespace BoltCardTools;
public class AESKey
{
public class AESKey
{
public const int BLOCK_SIZE = 16;
byte[] _bytes;
public byte[] ToBytes() => _bytes.ToArray();
public static AESKey Parse(string hex)
public const int BLOCK_SIZE = 16;
byte[] _bytes;
public byte[] ToBytes() => _bytes.ToArray();
public static AESKey Parse(string hex)
{
return new AESKey(hex.HexToBytes());
}
AESKey(byte[] bytes)
{
AssertKeySize(bytes);
_bytes = bytes;
}
public AESKey(ReadOnlySpan<byte> bytes)
{
AssertKeySize(bytes);
_bytes = bytes.ToArray();
}
private static void AssertKeySize(ReadOnlySpan<byte> bytes)
{
if (bytes.Length != BLOCK_SIZE)
throw new ArgumentException($"AES key must be {BLOCK_SIZE} bytes long");
}
public AESKey Derive(byte[] input)
{
return new AESKey(CMac(input));
}
public byte[] Decrypt(ReadOnlySpan<byte> cypherText, byte[]? iv = null)
{
iv ??= new byte[BLOCK_SIZE];
using MemoryStream ms = new MemoryStream(cypherText.ToArray());
using var aes = Aes.Create();
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.None;
using var cs = new CryptoStream(ms, aes.CreateDecryptor(_bytes, iv), CryptoStreamMode.Read);
var output = new byte[cypherText.Length];
cs.Read(output);
return output;
}
public byte[] Encrypt(byte[] clearText, byte[]? iv = null, CipherMode mode = CipherMode.CBC)
{
return AesEncrypt(_bytes, iv ?? new byte[BLOCK_SIZE], clearText, mode);
}
public byte[] GetSunMac(PICCData piccData, byte[]? payload = null)
{
return GetSunMac(piccData.Uid, piccData.Counter, payload);
}
public byte[] GetSunMac(byte[]? uid, int? counter, byte[]? payload = null)
{
payload ??= Array.Empty<byte>();
var derived = SesSDMFileReadMACKey(uid, counter);
var cmac = derived.CMac(payload);
return Truncate(cmac);
}
public PICCData DecryptSun(byte[] data)
{
return PICCData.Create(Decrypt(data));
}
AESKey SesSDMFileReadMACKey(byte[]? uid, int? counter)
{
int i = 0;
var sv2 = new byte[16];
sv2[i++] = 0x3c;
sv2[i++] = 0xc3;
sv2[i++] = 0x00;
sv2[i++] = 0x01;
sv2[i++] = 0x00;
sv2[i++] = 0x80;
if (uid is not null)
{
return new AESKey(hex.HexToBytes());
sv2[i++] = uid[0];
sv2[i++] = uid[1];
sv2[i++] = uid[2];
sv2[i++] = uid[3];
sv2[i++] = uid[4];
sv2[i++] = uid[5];
sv2[i++] = uid[6];
}
AESKey(byte[] bytes)
{
AssertKeySize(bytes);
_bytes = bytes;
}
public AESKey(ReadOnlySpan<byte> bytes)
{
AssertKeySize(bytes);
_bytes = bytes.ToArray();
}
if (counter is int)
{
sv2[i++] = (byte)counter;
sv2[i++] = (byte)(counter >> 8);
sv2[i++] = (byte)(counter >> 16);
}
return Derive(sv2);
}
//AESKey SesSDMFileReadENCKey(AESKey key, byte[] uid, int counter)
//{
// byte[] sv1 =
// {
// 0xc3, 0x3c, 0x00, 0x01, 0x00, 0x80,
// uid[0], uid[1], uid[2], uid[3], uid[4], uid[5], uid[6],
// ((byte)counter), (byte)(counter >> 8), (byte)(counter >> 16)
// };
// return key.Derive(sv1);
//}
private static byte[] AesEncrypt(byte[] key, byte[] iv, byte[] data, CipherMode mode = CipherMode.CBC)
{
using MemoryStream ms = new MemoryStream();
using var aes = Aes.Create();
aes.Mode = mode;
aes.Padding = PaddingMode.None;
private static void AssertKeySize(ReadOnlySpan<byte> bytes)
{
if (bytes.Length != BLOCK_SIZE)
throw new ArgumentException($"AES key must be {BLOCK_SIZE} bytes long");
}
using var cs = new CryptoStream(ms, aes.CreateEncryptor(key, iv), CryptoStreamMode.Write);
cs.Write(data, 0, data.Length);
cs.FlushFinalBlock();
public AESKey Derive(byte[] input)
{
return new AESKey(CMac(input));
}
public byte[] Decrypt(ReadOnlySpan<byte> cypherText, byte[]? iv = null)
{
iv ??= new byte[BLOCK_SIZE];
using MemoryStream ms = new MemoryStream(cypherText.ToArray());
using var aes = Aes.Create();
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.None;
using var cs = new CryptoStream(ms, aes.CreateDecryptor(_bytes, iv), CryptoStreamMode.Read);
var output = new byte[cypherText.Length];
cs.Read(output);
return output;
}
public byte[] Encrypt(byte[] clearText, byte[]? iv = null, CipherMode mode = CipherMode.CBC)
{
return AesEncrypt(_bytes, iv ?? new byte[BLOCK_SIZE], clearText, mode);
}
public byte[] GetSunMac(PICCData piccData, byte[]? payload = null)
{
return GetSunMac(piccData.Uid, piccData.Counter, payload);
}
public byte[] GetSunMac(byte[]? uid, int? counter, byte[]? payload = null)
{
payload ??= Array.Empty<byte>();
var derived = SesSDMFileReadMACKey(uid, counter);
var cmac = derived.CMac(payload);
return Truncate(cmac);
}
public PICCData DecryptSun(byte[] data)
{
return PICCData.Create(Decrypt(data));
}
AESKey SesSDMFileReadMACKey(byte[]? uid, int? counter)
{
int i = 0;
var sv2 = new byte[16];
sv2[i++] = 0x3c;
sv2[i++] = 0xc3;
sv2[i++] = 0x00;
sv2[i++] = 0x01;
sv2[i++] = 0x00;
sv2[i++] = 0x80;
if (uid is byte[])
{
sv2[i++] = uid[0];
sv2[i++] = uid[1];
sv2[i++] = uid[2];
sv2[i++] = uid[3];
sv2[i++] = uid[4];
sv2[i++] = uid[5];
sv2[i++] = uid[6];
}
if (counter is int)
{
sv2[i++] = (byte)counter;
sv2[i++] = (byte)(counter >> 8);
sv2[i++] = (byte)(counter >> 16);
}
return Derive(sv2);
}
//AESKey SesSDMFileReadENCKey(AESKey key, byte[] uid, int counter)
//{
// byte[] sv1 =
// {
// 0xc3, 0x3c, 0x00, 0x01, 0x00, 0x80,
// uid[0], uid[1], uid[2], uid[3], uid[4], uid[5], uid[6],
// ((byte)counter), (byte)(counter >> 8), (byte)(counter >> 16)
// };
// return key.Derive(sv1);
//}
private static byte[] AesEncrypt(byte[] key, byte[] iv, byte[] data, CipherMode mode = CipherMode.CBC)
{
using MemoryStream ms = new MemoryStream();
using var aes = Aes.Create();
aes.Mode = mode;
aes.Padding = PaddingMode.None;
return ms.ToArray();
}
using var cs = new CryptoStream(ms, aes.CreateEncryptor(key, iv), CryptoStreamMode.Write);
cs.Write(data, 0, data.Length);
cs.FlushFinalBlock();
public byte[] CMac(byte[] data)
{
var key = _bytes;
// SubKey generation
// step 1, AES-128 with key K is applied to an all-zero input block.
byte[] L = AesEncrypt(key, new byte[16], new byte[16]);
return ms.ToArray();
}
// step 2, K1 is derived through the following operation:
byte[]
FirstSubkey =
RotateLeft(L); //If the most significant bit of L is equal to 0, K1 is the left-shift of L by 1 bit.
if ((L[0] & 0x80) == 0x80)
FirstSubkey[15] ^=
0x87; // Otherwise, K1 is the exclusive-OR of const_Rb and the left-shift of L by 1 bit.
public byte[] CMac(byte[] data)
{
var key = _bytes;
// SubKey generation
// step 1, AES-128 with key K is applied to an all-zero input block.
byte[] L = AesEncrypt(key, new byte[16], new byte[16]);
// step 3, K2 is derived through the following operation:
byte[]
SecondSubkey =
RotateLeft(FirstSubkey); // If the most significant bit of K1 is equal to 0, K2 is the left-shift of K1 by 1 bit.
if ((FirstSubkey[0] & 0x80) == 0x80)
SecondSubkey[15] ^=
0x87; // Otherwise, K2 is the exclusive-OR of const_Rb and the left-shift of K1 by 1 bit.
// step 2, K1 is derived through the following operation:
byte[]
FirstSubkey =
RotateLeft(L); //If the most significant bit of L is equal to 0, K1 is the left-shift of L by 1 bit.
if ((L[0] & 0x80) == 0x80)
FirstSubkey[15] ^=
0x87; // Otherwise, K1 is the exclusive-OR of const_Rb and the left-shift of L by 1 bit.
// MAC computing
if (((data.Length != 0) && (data.Length % 16 == 0)) == true)
{
// If the size of the input message block is equal to a positive multiple of the block size (namely, 128 bits),
// the last block shall be exclusive-OR'ed with K1 before processing
for (int j = 0; j < FirstSubkey.Length; j++)
data[data.Length - 16 + j] ^= FirstSubkey[j];
}
else
{
// Otherwise, the last block shall be padded with 10^i
byte[] padding = new byte[16 - data.Length % 16];
padding[0] = 0x80;
// step 3, K2 is derived through the following operation:
byte[]
SecondSubkey =
RotateLeft(FirstSubkey); // If the most significant bit of K1 is equal to 0, K2 is the left-shift of K1 by 1 bit.
if ((FirstSubkey[0] & 0x80) == 0x80)
SecondSubkey[15] ^=
0x87; // Otherwise, K2 is the exclusive-OR of const_Rb and the left-shift of K1 by 1 bit.
data = data.Concat(padding.AsEnumerable()).ToArray();
// MAC computing
if (((data.Length != 0) && (data.Length % 16 == 0)) == true)
{
// If the size of the input message block is equal to a positive multiple of the block size (namely, 128 bits),
// the last block shall be exclusive-OR'ed with K1 before processing
for (int j = 0; j < FirstSubkey.Length; j++)
data[data.Length - 16 + j] ^= FirstSubkey[j];
}
else
{
// Otherwise, the last block shall be padded with 10^i
byte[] padding = new byte[16 - data.Length % 16];
padding[0] = 0x80;
// and exclusive-OR'ed with K2
for (int j = 0; j < SecondSubkey.Length; j++)
data[data.Length - 16 + j] ^= SecondSubkey[j];
}
data = data.Concat(padding.AsEnumerable()).ToArray();
// The result of the previous process will be the input of the last encryption.
byte[] encResult = AesEncrypt(key, new byte[16], data);
// and exclusive-OR'ed with K2
for (int j = 0; j < SecondSubkey.Length; j++)
data[data.Length - 16 + j] ^= SecondSubkey[j];
}
byte[] HashValue = new byte[16];
Array.Copy(encResult, encResult.Length - HashValue.Length, HashValue, 0, HashValue.Length);
// The result of the previous process will be the input of the last encryption.
byte[] encResult = AesEncrypt(key, new byte[16], data);
return HashValue;
}
byte[] HashValue = new byte[16];
Array.Copy(encResult, encResult.Length - HashValue.Length, HashValue, 0, HashValue.Length);
static byte[] RotateLeft(byte[] b)
{
byte[] r = new byte[b.Length];
byte carry = 0;
return HashValue;
}
for (int i = b.Length - 1; i >= 0; i--)
{
ushort u = (ushort)(b[i] << 1);
r[i] = (byte)((u & 0xff) + carry);
carry = (byte)((u & 0xff00) >> 8);
}
static byte[] RotateLeft(byte[] b)
{
byte[] r = new byte[b.Length];
byte carry = 0;
for (int i = b.Length - 1; i >= 0; i--)
{
ushort u = (ushort)(b[i] << 1);
r[i] = (byte)((u & 0xff) + carry);
carry = (byte)((u & 0xff00) >> 8);
}
return r;
}
}
return r;
}
}

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2022 Frans van Dorsselaer
// SPDX-FileCopyrightText: 2022 Frans van Dorsselaer
//
// SPDX-License-Identifier: MIT
// https://github.com/dorssel/dotnet-aes-extra/
@ -6,272 +6,271 @@
using System;
using System.Security.Cryptography;
namespace BoltCardTools
namespace BoltCardTools;
/// <summary>
/// Computes a Cipher-based Message Authentication Code (CMAC) by using the symmetric key AES block cipher.
/// </summary>
public sealed class AesCmac
: KeyedHashAlgorithm
{
/// <summary>
/// Computes a Cipher-based Message Authentication Code (CMAC) by using the symmetric key AES block cipher.
/// </summary>
public sealed class AesCmac
: KeyedHashAlgorithm
{
const int BLOCKSIZE = 16; // bytes
const int BLOCKSIZE = 16; // bytes
/// <inheritdoc cref="KeyedHashAlgorithm.Create()" />
/// <remarks>This static override defaults to <see cref="AesCmac" />.</remarks>
public static new KeyedHashAlgorithm Create() => new AesCmac();
/// <inheritdoc cref="KeyedHashAlgorithm.Create()" />
/// <remarks>This static override defaults to <see cref="AesCmac" />.</remarks>
public static new KeyedHashAlgorithm Create() => new AesCmac();
/// <inheritdoc cref="KeyedHashAlgorithm.Create(string)" />
public static new KeyedHashAlgorithm? Create(string algorithmName)
{
if (algorithmName == null)
{
throw new ArgumentNullException(nameof(algorithmName));
}
return algorithmName == nameof(AesCmac) ? Create() : null;
}
/// <inheritdoc cref="KeyedHashAlgorithm.Create(string)" />
public static new KeyedHashAlgorithm? Create(string algorithmName)
{
if (algorithmName == null)
{
throw new ArgumentNullException(nameof(algorithmName));
}
return algorithmName == nameof(AesCmac) ? Create() : null;
}
/// <summary>
/// Initializes a new instance of the <see cref="AesCmac" /> class with a randomly generated key.
/// </summary>
public AesCmac()
{
AesEcb = Aes.Create();
AesEcb.Mode = CipherMode.ECB; // DevSkim: ignore DS187371
AesEcb.Padding = PaddingMode.None;
CryptoTransform = AesEcb.CreateEncryptor();
HashSizeValue = BLOCKSIZE * 8;
}
/// <summary>
/// Initializes a new instance of the <see cref="AesCmac" /> class with a randomly generated key.
/// </summary>
public AesCmac()
{
AesEcb = Aes.Create();
AesEcb.Mode = CipherMode.ECB; // DevSkim: ignore DS187371
AesEcb.Padding = PaddingMode.None;
CryptoTransform = AesEcb.CreateEncryptor();
HashSizeValue = BLOCKSIZE * 8;
}
/// <summary>
/// Initializes a new instance of the <see cref="AesCmac" /> class with the specified key data.
/// </summary>
/// <param name="key">The secret key for AES-CMAC algorithm.</param>
public AesCmac(byte[] key)
: this()
{
Key = key;
}
/// <summary>
/// Initializes a new instance of the <see cref="AesCmac" /> class with the specified key data.
/// </summary>
/// <param name="key">The secret key for AES-CMAC algorithm.</param>
public AesCmac(byte[] key)
: this()
{
Key = key;
}
void ZeroizeState()
{
CryptographicOperations.ZeroMemory(C);
CryptographicOperations.ZeroMemory(Partial);
}
void ZeroizeState()
{
CryptographicOperations.ZeroMemory(C);
CryptographicOperations.ZeroMemory(Partial);
}
#region IDisposable
bool IsDisposed;
#region IDisposable
bool IsDisposed;
/// <inheritdoc cref="KeyedHashAlgorithm.Dispose(bool)" />
protected override void Dispose(bool disposing)
{
if (!IsDisposed)
{
if (disposing)
{
CryptoTransform.Dispose();
AesEcb.Dispose();
ZeroizeState();
}
IsDisposed = true;
}
base.Dispose(disposing);
}
#endregion
/// <inheritdoc cref="KeyedHashAlgorithm.Dispose(bool)" />
protected override void Dispose(bool disposing)
{
if (!IsDisposed)
{
if (disposing)
{
CryptoTransform.Dispose();
AesEcb.Dispose();
ZeroizeState();
}
IsDisposed = true;
}
base.Dispose(disposing);
}
#endregion
/// <inheritdoc cref="KeyedHashAlgorithm.Key" />
public override byte[] Key
{
get => AesEcb.Key;
set
{
CryptoTransform.Dispose();
AesEcb.Key = value;
CryptoTransform = AesEcb.CreateEncryptor();
}
}
/// <inheritdoc cref="KeyedHashAlgorithm.Key" />
public override byte[] Key
{
get => AesEcb.Key;
set
{
CryptoTransform.Dispose();
AesEcb.Key = value;
CryptoTransform = AesEcb.CreateEncryptor();
}
}
readonly Aes AesEcb;
ICryptoTransform CryptoTransform;
readonly Aes AesEcb;
ICryptoTransform CryptoTransform;
// See: NIST SP 800-38B, Section 6.2, Step 5
readonly byte[] C = new byte[BLOCKSIZE];
// See: NIST SP 800-38B, Section 6.2, Step 5
readonly byte[] C = new byte[BLOCKSIZE];
// See: NIST SP 800-38B, Section 4.2.2
//
// In-place: X = CIPH_K(X)
void CIPH_K_InPlace(byte[] X_Base, int X_Offset = 0)
{
CryptoTransform.TransformBlock(X_Base, X_Offset, BLOCKSIZE, X_Base, X_Offset);
}
// See: NIST SP 800-38B, Section 4.2.2
//
// In-place: X = CIPH_K(X)
void CIPH_K_InPlace(byte[] X_Base, int X_Offset = 0)
{
CryptoTransform.TransformBlock(X_Base, X_Offset, BLOCKSIZE, X_Base, X_Offset);
}
// See: NIST SP 800-38B, Section 6.1
//
// Returns: first ? K1 : K2
byte[] SUBK(bool first)
{
var X = new byte[BLOCKSIZE];
// Step 1: X has the role of L
CIPH_K_InPlace(X);
// Step 2: X has the role of K1
dbl_InPlace(X);
if (first)
{
// Step 4: return K1
return X;
}
// Step 3: X has the role of K1
dbl_InPlace(X);
// Step 4: return K2
return X;
}
// See: NIST SP 800-38B, Section 6.1
//
// Returns: first ? K1 : K2
byte[] SUBK(bool first)
{
var X = new byte[BLOCKSIZE];
// Step 1: X has the role of L
CIPH_K_InPlace(X);
// Step 2: X has the role of K1
dbl_InPlace(X);
if (first)
{
// Step 4: return K1
return X;
}
// Step 3: X has the role of K1
dbl_InPlace(X);
// Step 4: return K2
return X;
}
/// <inheritdoc cref="HashAlgorithm.Initialize" />
public override void Initialize()
{
// See: NIST SP 800-38B, Section 6.2, Step 5
ZeroizeState();
/// <inheritdoc cref="HashAlgorithm.Initialize" />
public override void Initialize()
{
// See: NIST SP 800-38B, Section 6.2, Step 5
ZeroizeState();
PartialLength = 0;
}
PartialLength = 0;
}
readonly byte[] Partial = new byte[BLOCKSIZE];
int PartialLength;
readonly byte[] Partial = new byte[BLOCKSIZE];
int PartialLength;
// See: NIST SP 800-38B, Section 6.2, Step 6
void AddBlock(byte[] blockBase, int blockOffset = 0)
{
xor_InPlace(C, 0, blockBase, blockOffset, BLOCKSIZE);
CIPH_K_InPlace(C);
}
// See: NIST SP 800-38B, Section 6.2, Step 6
void AddBlock(byte[] blockBase, int blockOffset = 0)
{
xor_InPlace(C, 0, blockBase, blockOffset, BLOCKSIZE);
CIPH_K_InPlace(C);
}
/// <inheritdoc cref="HashAlgorithm.HashCore(byte[], int, int)" />
protected override void HashCore(byte[] array, int ibStart, int cbSize)
{
if (cbSize == 0)
{
return;
}
/// <inheritdoc cref="HashAlgorithm.HashCore(byte[], int, int)" />
protected override void HashCore(byte[] array, int ibStart, int cbSize)
{
if (cbSize == 0)
{
return;
}
// If we have a non-empty && non-full Partial block already -> append to that first.
if ((0 < PartialLength) && (PartialLength < BLOCKSIZE))
{
var count = Math.Min(cbSize, BLOCKSIZE - PartialLength);
Array.Copy(array, ibStart, Partial, PartialLength, count);
PartialLength += count;
if (count == cbSize)
{
// No more data supplied, we're done. Even if we filled up Partial completely,
// because we don't know if it will be the final block.
return;
}
ibStart += count;
cbSize -= count;
}
// If we have a non-empty && non-full Partial block already -> append to that first.
if ((0 < PartialLength) && (PartialLength < BLOCKSIZE))
{
var count = Math.Min(cbSize, BLOCKSIZE - PartialLength);
Array.Copy(array, ibStart, Partial, PartialLength, count);
PartialLength += count;
if (count == cbSize)
{
// No more data supplied, we're done. Even if we filled up Partial completely,
// because we don't know if it will be the final block.
return;
}
ibStart += count;
cbSize -= count;
}
// We get here only if Partial is either empty or full (i.e. we are block-aligned) && there is more to "hash".
if (PartialLength == BLOCKSIZE)
{
// Since there is more to hash, this is not the final block.
// See: NIST SP 800-38B, Section 6.2, Steps 3 and 6
AddBlock(Partial);
PartialLength = 0;
}
// We get here only if Partial is either empty or full (i.e. we are block-aligned) && there is more to "hash".
if (PartialLength == BLOCKSIZE)
{
// Since there is more to hash, this is not the final block.
// See: NIST SP 800-38B, Section 6.2, Steps 3 and 6
AddBlock(Partial);
PartialLength = 0;
}
// We get here only if Partial is empty && there is more to "hash".
// Add complete, non-final blocks. Never add the last block given in this call since we don't know if that will be the final block.
for (int i = 0, nonFinalBlockCount = (cbSize - 1) / BLOCKSIZE; i < nonFinalBlockCount; i++)
{
// See: NIST SP 800-38B, Section 6.2, Steps 3 and 6
AddBlock(array, ibStart);
ibStart += BLOCKSIZE;
cbSize -= BLOCKSIZE;
}
// We get here only if Partial is empty && there is more to "hash".
// Add complete, non-final blocks. Never add the last block given in this call since we don't know if that will be the final block.
for (int i = 0, nonFinalBlockCount = (cbSize - 1) / BLOCKSIZE; i < nonFinalBlockCount; i++)
{
// See: NIST SP 800-38B, Section 6.2, Steps 3 and 6
AddBlock(array, ibStart);
ibStart += BLOCKSIZE;
cbSize -= BLOCKSIZE;
}
// Save what we have left (we always have some, by construction).
Array.Copy(array, ibStart, Partial, 0, cbSize);
PartialLength = cbSize;
}
// Save what we have left (we always have some, by construction).
Array.Copy(array, ibStart, Partial, 0, cbSize);
PartialLength = cbSize;
}
/// <inheritdoc cref="HashAlgorithm.HashFinal" />
protected override byte[] HashFinal()
{
// Partial now has the role of Mn*
if (PartialLength == BLOCKSIZE)
{
// See: NIST SP 800-38B, Section 6.2, Step 1: K1
var K1 = SUBK(true);
xor_InPlace(Partial, 0, K1, 0, BLOCKSIZE);
// Partial now has the role of Mn
}
else
{
// Add padding
Partial[PartialLength] = 0x80;
for (var i = PartialLength + 1; i < BLOCKSIZE; ++i)
{
Partial[i] = 0x00;
}
// See: NIST SP 800-38B, Section 6.2, Step 1: K2
var K2 = SUBK(false);
xor_InPlace(Partial, 0, K2, 0, BLOCKSIZE);
// Partial now has the role of Mn
}
// See: NIST SP 800-38B, Section 6.2, Steps 4 and 6
AddBlock(Partial);
PartialLength = 0;
/// <inheritdoc cref="HashAlgorithm.HashFinal" />
protected override byte[] HashFinal()
{
// Partial now has the role of Mn*
if (PartialLength == BLOCKSIZE)
{
// See: NIST SP 800-38B, Section 6.2, Step 1: K1
var K1 = SUBK(true);
xor_InPlace(Partial, 0, K1, 0, BLOCKSIZE);
// Partial now has the role of Mn
}
else
{
// Add padding
Partial[PartialLength] = 0x80;
for (var i = PartialLength + 1; i < BLOCKSIZE; ++i)
{
Partial[i] = 0x00;
}
// See: NIST SP 800-38B, Section 6.2, Step 1: K2
var K2 = SUBK(false);
xor_InPlace(Partial, 0, K2, 0, BLOCKSIZE);
// Partial now has the role of Mn
}
// See: NIST SP 800-38B, Section 6.2, Steps 4 and 6
AddBlock(Partial);
PartialLength = 0;
// NOTE: KeyedHashAlgorithm exposes the returned array reference as the
// Hash property, so we must *not* return C itself as it may be reused.
var cmac = new byte[BLOCKSIZE];
C.CopyTo(cmac, 0);
// NOTE: KeyedHashAlgorithm exposes the returned array reference as the
// Hash property, so we must *not* return C itself as it may be reused.
var cmac = new byte[BLOCKSIZE];
C.CopyTo(cmac, 0);
ZeroizeState();
ZeroizeState();
return cmac;
}
return cmac;
}
static void xor_InPlace(byte[] X_Base, int X_Offset, byte[] Y_Base, int Y_Offset, int count)
{
for (var i = 0; i < count; ++i)
{
X_Base[X_Offset + i] ^= Y_Base[Y_Offset + i];
}
}
// See: NIST SP 800-38B, Section 6.1
// See: RFC 5297, Section 2.1
//
// In place: S = dbl(S)
static void dbl_InPlace(byte[] S)
{
// See: NIST SP 800-38B, Section 5.3
// See: RFC 5297, Section 2.3
const int Rb = 0b10000111;
static void xor_InPlace(byte[] X_Base, int X_Offset, byte[] Y_Base, int Y_Offset, int count)
{
for (var i = 0; i < count; ++i)
{
X_Base[X_Offset + i] ^= Y_Base[Y_Offset + i];
}
}
// See: NIST SP 800-38B, Section 6.1
// See: RFC 5297, Section 2.1
//
// In place: S = dbl(S)
static void dbl_InPlace(byte[] S)
{
// See: NIST SP 800-38B, Section 5.3
// See: RFC 5297, Section 2.3
const int Rb = 0b10000111;
// See: NIST SP 800-38B, Section 6.1, Step 2/3
if (LeftShiftOne_InPlace(S))
{
S[BLOCKSIZE - 1] ^= Rb;
}
}
// See: NIST SP 800-38B, Section 6.1, Step 2/3
if (LeftShiftOne_InPlace(S))
{
S[BLOCKSIZE - 1] ^= Rb;
}
}
// See: NIST SP 800-38B, Section 4.2
//
// In place: X = (X << 1)
// Returns final carry.
static bool LeftShiftOne_InPlace(byte[] X)
{
var carry = false;
for (var i = X.Length - 1; i >= 0; --i)
{
var nextCarry = (X[i] & 0x80) != 0;
_ = unchecked(X[i] <<= 1);
if (carry)
{
X[i] |= 1;
}
carry = nextCarry;
}
return carry;
}
}
// See: NIST SP 800-38B, Section 4.2
//
// In place: X = (X << 1)
// Returns final carry.
static bool LeftShiftOne_InPlace(byte[] X)
{
var carry = false;
for (var i = X.Length - 1; i >= 0; --i)
{
var nextCarry = (X[i] & 0x80) != 0;
_ = unchecked(X[i] <<= 1);
if (carry)
{
X[i] |= 1;
}
carry = nextCarry;
}
return carry;
}
}

View File

@ -1,32 +1,27 @@
using NdefLibrary.Ndef;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NdefLibrary.Ndef;
namespace BoltCardTools
namespace BoltCardTools;
public class BoltCard
{
public class BoltCard
{
public BoltCard(Ntag424 ntag)
public BoltCard(Ntag424 ntag)
{
Ntag = ntag;
}
public Ntag424 Ntag { get; }
private static byte[] CreateNDefMessage(string lnUrl)
{
var queryString = "p=00000000000000000000000000000000&c=0000000000000000";
var templateUrl = lnUrl.Contains("?", StringComparison.OrdinalIgnoreCase) ?
$"{lnUrl}?{queryString}" :
$"{lnUrl}&{queryString}";
var message = new NdefLibrary.Ndef.NdefMessage
{
Ntag = ntag;
}
public Ntag424 Ntag { get; }
private static byte[] CreateNDefMessage(string lnUrl)
{
var queryString = "p=00000000000000000000000000000000&c=0000000000000000";
var templateUrl = lnUrl.Contains("?", StringComparison.OrdinalIgnoreCase) ?
$"{lnUrl}?{queryString}" :
$"{lnUrl}&{queryString}";
var message = new NdefLibrary.Ndef.NdefMessage
{
new NdefUriRecord() { Uri = templateUrl }
};
return message.ToByteArray();
}
}
new NdefUriRecord() { Uri = templateUrl }
};
return message.ToByteArray();
}
}

View File

@ -1,74 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace BoltCardTools
namespace BoltCardTools;
internal static class Extensions
{
internal static class Extensions
{
public static bool IsSame(this byte[] arr1, byte[] arr2)
{
if (arr1.Length != arr2.Length)
return false;
for (int i = 0; i < arr1.Length; i++)
if (arr1[i] != arr2[i])
return false;
return true;
}
public static string ToHex(this byte[] data)
{
return Convert.ToHexString(data, 0, data.Length).ToLowerInvariant();
}
public static byte[] HexToBytes(this string hex)
{
if (hex == null)
throw new ArgumentNullException(nameof(hex));
if (hex.Length % 2 == 1)
throw new FormatException("Invalid Hex String");
if (hex.Length < (hex.Length >> 1))
throw new ArgumentException("output should be bigger", nameof(hex));
var output = new byte[hex.Length / 2];
try
{
for (int i = 0, j = 0; i < hex.Length; i += 2, j++)
{
var a = IsDigitCore(hex[i]);
var b = IsDigitCore(hex[i + 1]);
if (a == 0xff || b == 0xff)
throw new FormatException("Invalid Hex String");
output[j] = (byte)(((uint)a << 4) | (uint)b);
}
}
catch (IndexOutOfRangeException) { throw new FormatException("Invalid Hex String"); }
return output;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static byte IsDigitCore(char c)
{
return CharToHexLookup[c];
}
static byte[] CharToHexLookup => new byte[]
{
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 63
0xFF, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 79
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 95
0xFF, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 111
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 127
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 143
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 159
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 175
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 191
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 255
public static bool IsSame(this byte[] arr1, byte[] arr2)
{
if (arr1.Length != arr2.Length)
return false;
for (int i = 0; i < arr1.Length; i++)
if (arr1[i] != arr2[i])
return false;
return true;
}
public static string ToHex(this byte[] data)
{
return Convert.ToHexString(data, 0, data.Length).ToLowerInvariant();
}
public static byte[] HexToBytes(this string hex)
{
if (hex == null)
throw new ArgumentNullException(nameof(hex));
if (hex.Length % 2 == 1)
throw new FormatException("Invalid Hex String");
if (hex.Length < (hex.Length >> 1))
throw new ArgumentException("output should be bigger", nameof(hex));
var output = new byte[hex.Length / 2];
try
{
for (int i = 0, j = 0; i < hex.Length; i += 2, j++)
{
var a = IsDigitCore(hex[i]);
var b = IsDigitCore(hex[i + 1]);
if (a == 0xff || b == 0xff)
throw new FormatException("Invalid Hex String");
output[j] = (byte)(((uint)a << 4) | (uint)b);
}
}
catch (IndexOutOfRangeException) { throw new FormatException("Invalid Hex String"); }
return output;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static byte IsDigitCore(char c)
{
return CharToHexLookup[c];
}
static byte[] CharToHexLookup => new byte[]
{
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 63
0xFF, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 79
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 95
0xFF, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 111
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 127
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 143
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 159
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 175
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 191
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 255
};
}
}

View File

@ -1,265 +1,264 @@
using System;
using System;
using System.Collections.Generic;
namespace BoltCardTools
namespace BoltCardTools;
public enum AccessCondition
{
public enum AccessCondition
{
Key0 = 0, Key1 = 1, Key2 = 2, Key3 = 3, Key4 = 4,
Free = 0x0E, Never = 0x0F
}
public enum AccessRight
{
Read,
Write,
Change
}
public record SDMAccessRights
{
public SDMAccessRights()
{
MetaRead = AccessCondition.Never;
FileRead = AccessCondition.Never;
CtrRet = AccessCondition.Never;
}
public SDMAccessRights(ReadOnlySpan<byte> conditions)
{
MetaRead = (AccessCondition)((byte)(conditions[1] & 0b1111_0000) >> 4);
FileRead = (AccessCondition)(conditions[1] & 0b0000_1111);
CtrRet = (AccessCondition)(conditions[0] & 0b0000_1111);
}
public AccessCondition MetaRead { get; set; }
public AccessCondition FileRead { get; set; }
public AccessCondition CtrRet { get; set; }
public byte[] ToBytes()
{
var fileSettings = new byte[2];
fileSettings[1] = (byte)(((byte)MetaRead << 4) | ((byte)FileRead & 0b0000_1111));
fileSettings[0] = (byte)(((byte)CtrRet & 0b0000_1111) | (byte)0xf0);
return fileSettings;
}
}
public record AccessRights
{
public AccessRights(ReadOnlySpan<byte> conditions)
{
Read = (AccessCondition)((byte)(conditions[1] & 0b1111_0000) >> 4);
Write = (AccessCondition)(conditions[1] & 0b0000_1111);
ReadWrite = (AccessCondition)((byte)(conditions[0] & 0b1111_0000) >> 4);
Change = (AccessCondition)(conditions[0] & 0b0000_1111);
}
public AccessRights() : this(DataFile.NDEF)
{
}
public AccessRights(DataFile file)
{
if (file == DataFile.CC)
{
Read = AccessCondition.Free;
Write = AccessCondition.Key0;
ReadWrite = AccessCondition.Key0;
Change = AccessCondition.Key0;
}
else if (file == DataFile.NDEF)
{
Read = AccessCondition.Free;
Write = AccessCondition.Free;
ReadWrite = AccessCondition.Free;
Change = AccessCondition.Key0;
}
else
{
Read = AccessCondition.Key2;
Write = AccessCondition.Key3;
ReadWrite = AccessCondition.Key3;
Change = AccessCondition.Key0;
}
}
public bool IsAllowed(int keyNo, AccessRight right)
{
var keyno = (AccessCondition)keyNo;
if (right == AccessRight.Change)
return Change == keyno || Change == AccessCondition.Free;
if (ReadWrite == keyno || ReadWrite == AccessCondition.Free)
return true;
if (right == AccessRight.Write)
return Write == keyno || Write == AccessCondition.Free;
if (right == AccessRight.Read)
return Read == keyno || Read == AccessCondition.Free;
return false;
}
public AccessCondition Write { get; set; }
public AccessCondition ReadWrite { get; set; }
public AccessCondition Read { get; set; }
public AccessCondition Change { get; set; }
public byte[] ToBytes()
{
var fileSettings = new byte[2];
fileSettings[1] = (byte)(((byte)Read << 4) | ((byte)Write & 0b0000_1111));
fileSettings[0] = (byte)(((byte)ReadWrite << 4) | ((byte)Change & 0b0000_1111));
return fileSettings;
}
}
public record FileSettings
{
public FileSettings(DataFile file)
{
SDMMirroring = false;
CommMode = CommMode.Plain;
AccessRights = new AccessRights(file);
}
public FileSettings(byte[] fileSettings, bool update)
{
int i = 0;
if (!update)
{
i++;
}
SDMMirroring = (fileSettings[i] & 0b0100_0000) != 0;
CommMode = (fileSettings[i] & 0b0000_0011) switch
{
0b01 => CommMode.MAC,
0b11 => CommMode.Full,
_ => CommMode.Plain
};
i++;
AccessRights = new AccessRights(fileSettings[i..(i + 2)]);
i += 2;
if (!update)
{
i += 3; // Size
}
if (!SDMMirroring)
return;
var sdmOptions = fileSettings[i];
SDMUID = (0b1000_0000 & sdmOptions) != 0;
SDMReadCtr = (0b0100_0000 & sdmOptions) != 0;
SDMReadCtrLimit = (0b0010_0000 & sdmOptions) != 0;
SDMENCFileData = (0b0001_0000 & sdmOptions) != 0;
i++;
SDMAccessRights = new SDMAccessRights(fileSettings[i..(i + 2)]);
i += 2;
if (SDMUID && SDMAccessRights.MetaRead == AccessCondition.Free)
{
UIDOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]);
i += 3;
}
if (SDMReadCtr && SDMAccessRights.MetaRead == AccessCondition.Free)
{
SDMReadCtrOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]);
i += 3;
}
if (SDMAccessRights.MetaRead != AccessCondition.Free && SDMAccessRights.MetaRead != AccessCondition.Never)
{
PICCDataOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]);
i += 3;
}
if (SDMAccessRights.FileRead != AccessCondition.Never)
{
SDMMACInputOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]);
i += 3;
}
if (SDMAccessRights.FileRead != AccessCondition.Never && SDMENCFileData)
{
SDMENCOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]);
i += 3;
SDMENCLength = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]);
i += 3;
}
if (SDMAccessRights.FileRead != AccessCondition.Never)
{
SDMMACOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]);
i += 3;
}
if (SDMReadCtrLimit)
{
SDMReadCtrLimitValue = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]);
i += 3;
}
}
public bool SDMUID { get; set; }
public bool SDMReadCtr { get; set; }
public bool SDMReadCtrLimit { get; set; }
public int SDMReadCtrLimitValue { get; set; }
public bool SDMENCFileData { get; set; }
public SDMAccessRights SDMAccessRights { get; set; } = new SDMAccessRights();
public byte[] ToBytes()
{
List<byte> output = new List<byte>();
byte fileOptions = CommMode switch
{
CommMode.MAC => 0b01,
CommMode.Full => 0b11,
_ => 0b00
};
if (SDMMirroring)
fileOptions |= 0b0100_0000;
output.Add(fileOptions);
output.AddRange(AccessRights.ToBytes());
if (!SDMMirroring)
return output.ToArray();
var sdmOptions = 0x01;
if (SDMUID)
sdmOptions |= 0b1000_0000;
if (SDMReadCtr)
sdmOptions |= 0b0100_0000;
if (SDMReadCtrLimit)
sdmOptions |= 0b0010_0000;
if (SDMENCFileData)
sdmOptions |= 0b0001_0000;
output.Add((byte)sdmOptions);
output.AddRange(SDMAccessRights.ToBytes());
if (SDMUID && SDMAccessRights.MetaRead == AccessCondition.Free)
{
output.AddRange(Helpers.UIntTo3BytesLE(UIDOffset));
}
if (SDMReadCtr && SDMAccessRights.MetaRead == AccessCondition.Free)
{
output.AddRange(Helpers.UIntTo3BytesLE(SDMReadCtrOffset));
}
if (SDMAccessRights.MetaRead != AccessCondition.Free && SDMAccessRights.MetaRead != AccessCondition.Never)
{
output.AddRange(Helpers.UIntTo3BytesLE(PICCDataOffset));
}
if (SDMAccessRights.FileRead != AccessCondition.Never)
{
output.AddRange(Helpers.UIntTo3BytesLE(SDMMACInputOffset));
}
if (SDMAccessRights.FileRead != AccessCondition.Never && SDMENCFileData)
{
output.AddRange(Helpers.UIntTo3BytesLE(SDMENCOffset));
output.AddRange(Helpers.UIntTo3BytesLE(SDMENCLength));
}
if (SDMAccessRights.FileRead != AccessCondition.Never)
{
output.AddRange(Helpers.UIntTo3BytesLE(SDMMACOffset));
}
if (SDMReadCtrLimit)
{
output.AddRange(Helpers.UIntTo3BytesLE(SDMReadCtrLimitValue));
}
return output.ToArray();
}
public int SDMMACOffset { get; set; }
public int SDMMACInputOffset { get; set; }
public int SDMENCLength { get; set; }
public int SDMENCOffset { get; set; }
public int PICCDataOffset { get; set; }
public int SDMReadCtrOffset { get; set; }
public int UIDOffset { get; set; }
public AccessRights AccessRights { get; set; }
public bool IsAllowed(int keyNo, AccessRight right) => AccessRights.IsAllowed(keyNo, right);
public bool SDMMirroring { get; set; }
public CommMode CommMode { get; set; }
}
Key0 = 0, Key1 = 1, Key2 = 2, Key3 = 3, Key4 = 4,
Free = 0x0E, Never = 0x0F
}
public enum AccessRight
{
Read,
Write,
Change
}
public record SDMAccessRights
{
public SDMAccessRights()
{
MetaRead = AccessCondition.Never;
FileRead = AccessCondition.Never;
CtrRet = AccessCondition.Never;
}
public SDMAccessRights(ReadOnlySpan<byte> conditions)
{
MetaRead = (AccessCondition)((byte)(conditions[1] & 0b1111_0000) >> 4);
FileRead = (AccessCondition)(conditions[1] & 0b0000_1111);
CtrRet = (AccessCondition)(conditions[0] & 0b0000_1111);
}
public AccessCondition MetaRead { get; set; }
public AccessCondition FileRead { get; set; }
public AccessCondition CtrRet { get; set; }
public byte[] ToBytes()
{
var fileSettings = new byte[2];
fileSettings[1] = (byte)(((byte)MetaRead << 4) | ((byte)FileRead & 0b0000_1111));
fileSettings[0] = (byte)(((byte)CtrRet & 0b0000_1111) | (byte)0xf0);
return fileSettings;
}
}
public record AccessRights
{
public AccessRights(ReadOnlySpan<byte> conditions)
{
Read = (AccessCondition)((byte)(conditions[1] & 0b1111_0000) >> 4);
Write = (AccessCondition)(conditions[1] & 0b0000_1111);
ReadWrite = (AccessCondition)((byte)(conditions[0] & 0b1111_0000) >> 4);
Change = (AccessCondition)(conditions[0] & 0b0000_1111);
}
public AccessRights() : this(DataFile.NDEF)
{
}
public AccessRights(DataFile file)
{
if (file == DataFile.CC)
{
Read = AccessCondition.Free;
Write = AccessCondition.Key0;
ReadWrite = AccessCondition.Key0;
Change = AccessCondition.Key0;
}
else if (file == DataFile.NDEF)
{
Read = AccessCondition.Free;
Write = AccessCondition.Free;
ReadWrite = AccessCondition.Free;
Change = AccessCondition.Key0;
}
else
{
Read = AccessCondition.Key2;
Write = AccessCondition.Key3;
ReadWrite = AccessCondition.Key3;
Change = AccessCondition.Key0;
}
}
public bool IsAllowed(int keyNo, AccessRight right)
{
var keyno = (AccessCondition)keyNo;
if (right == AccessRight.Change)
return Change == keyno || Change == AccessCondition.Free;
if (ReadWrite == keyno || ReadWrite == AccessCondition.Free)
return true;
if (right == AccessRight.Write)
return Write == keyno || Write == AccessCondition.Free;
if (right == AccessRight.Read)
return Read == keyno || Read == AccessCondition.Free;
return false;
}
public AccessCondition Write { get; set; }
public AccessCondition ReadWrite { get; set; }
public AccessCondition Read { get; set; }
public AccessCondition Change { get; set; }
public byte[] ToBytes()
{
var fileSettings = new byte[2];
fileSettings[1] = (byte)(((byte)Read << 4) | ((byte)Write & 0b0000_1111));
fileSettings[0] = (byte)(((byte)ReadWrite << 4) | ((byte)Change & 0b0000_1111));
return fileSettings;
}
}
public record FileSettings
{
public FileSettings(DataFile file)
{
SDMMirroring = false;
CommMode = CommMode.Plain;
AccessRights = new AccessRights(file);
}
public FileSettings(byte[] fileSettings, bool update)
{
int i = 0;
if (!update)
{
i++;
}
SDMMirroring = (fileSettings[i] & 0b0100_0000) != 0;
CommMode = (fileSettings[i] & 0b0000_0011) switch
{
0b01 => CommMode.MAC,
0b11 => CommMode.Full,
_ => CommMode.Plain
};
i++;
AccessRights = new AccessRights(fileSettings[i..(i + 2)]);
i += 2;
if (!update)
{
i += 3; // Size
}
if (!SDMMirroring)
return;
var sdmOptions = fileSettings[i];
SDMUID = (0b1000_0000 & sdmOptions) != 0;
SDMReadCtr = (0b0100_0000 & sdmOptions) != 0;
SDMReadCtrLimit = (0b0010_0000 & sdmOptions) != 0;
SDMENCFileData = (0b0001_0000 & sdmOptions) != 0;
i++;
SDMAccessRights = new SDMAccessRights(fileSettings[i..(i + 2)]);
i += 2;
if (SDMUID && SDMAccessRights.MetaRead == AccessCondition.Free)
{
UIDOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]);
i += 3;
}
if (SDMReadCtr && SDMAccessRights.MetaRead == AccessCondition.Free)
{
SDMReadCtrOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]);
i += 3;
}
if (SDMAccessRights.MetaRead != AccessCondition.Free && SDMAccessRights.MetaRead != AccessCondition.Never)
{
PICCDataOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]);
i += 3;
}
if (SDMAccessRights.FileRead != AccessCondition.Never)
{
SDMMACInputOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]);
i += 3;
}
if (SDMAccessRights.FileRead != AccessCondition.Never && SDMENCFileData)
{
SDMENCOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]);
i += 3;
SDMENCLength = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]);
i += 3;
}
if (SDMAccessRights.FileRead != AccessCondition.Never)
{
SDMMACOffset = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]);
i += 3;
}
if (SDMReadCtrLimit)
{
SDMReadCtrLimitValue = Helpers.BytesToUIntLE(fileSettings[i..(i + 3)]);
i += 3;
}
}
public bool SDMUID { get; set; }
public bool SDMReadCtr { get; set; }
public bool SDMReadCtrLimit { get; set; }
public int SDMReadCtrLimitValue { get; set; }
public bool SDMENCFileData { get; set; }
public SDMAccessRights SDMAccessRights { get; set; } = new SDMAccessRights();
public byte[] ToBytes()
{
List<byte> output = new List<byte>();
byte fileOptions = CommMode switch
{
CommMode.MAC => 0b01,
CommMode.Full => 0b11,
_ => 0b00
};
if (SDMMirroring)
fileOptions |= 0b0100_0000;
output.Add(fileOptions);
output.AddRange(AccessRights.ToBytes());
if (!SDMMirroring)
return output.ToArray();
var sdmOptions = 0x01;
if (SDMUID)
sdmOptions |= 0b1000_0000;
if (SDMReadCtr)
sdmOptions |= 0b0100_0000;
if (SDMReadCtrLimit)
sdmOptions |= 0b0010_0000;
if (SDMENCFileData)
sdmOptions |= 0b0001_0000;
output.Add((byte)sdmOptions);
output.AddRange(SDMAccessRights.ToBytes());
if (SDMUID && SDMAccessRights.MetaRead == AccessCondition.Free)
{
output.AddRange(Helpers.UIntTo3BytesLE(UIDOffset));
}
if (SDMReadCtr && SDMAccessRights.MetaRead == AccessCondition.Free)
{
output.AddRange(Helpers.UIntTo3BytesLE(SDMReadCtrOffset));
}
if (SDMAccessRights.MetaRead != AccessCondition.Free && SDMAccessRights.MetaRead != AccessCondition.Never)
{
output.AddRange(Helpers.UIntTo3BytesLE(PICCDataOffset));
}
if (SDMAccessRights.FileRead != AccessCondition.Never)
{
output.AddRange(Helpers.UIntTo3BytesLE(SDMMACInputOffset));
}
if (SDMAccessRights.FileRead != AccessCondition.Never && SDMENCFileData)
{
output.AddRange(Helpers.UIntTo3BytesLE(SDMENCOffset));
output.AddRange(Helpers.UIntTo3BytesLE(SDMENCLength));
}
if (SDMAccessRights.FileRead != AccessCondition.Never)
{
output.AddRange(Helpers.UIntTo3BytesLE(SDMMACOffset));
}
if (SDMReadCtrLimit)
{
output.AddRange(Helpers.UIntTo3BytesLE(SDMReadCtrLimitValue));
}
return output.ToArray();
}
public int SDMMACOffset { get; set; }
public int SDMMACInputOffset { get; set; }
public int SDMENCLength { get; set; }
public int SDMENCOffset { get; set; }
public int PICCDataOffset { get; set; }
public int SDMReadCtrOffset { get; set; }
public int UIDOffset { get; set; }
public AccessRights AccessRights { get; set; }
public bool IsAllowed(int keyNo, AccessRight right) => AccessRights.IsAllowed(keyNo, right);
public bool SDMMirroring { get; set; }
public CommMode CommMode { get; set; }
}

View File

@ -1,123 +1,119 @@
using System;
using System.Collections.Generic;
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BoltCardTools
namespace BoltCardTools;
internal class Helpers
{
internal class Helpers
{
internal static byte[] Truncate(byte[] cmac)
{
var halfMac = new byte[cmac.Length / 2];
for (var i = 1; i < cmac.Length; i += 2)
{
halfMac[i >> 1] = cmac[i];
}
return halfMac;
}
public static byte[] RotateLeft(byte[] bytesArr)
{
byte first = bytesArr[0];
byte[] rotatedArray = new byte[bytesArr.Length];
Array.Copy(bytesArr, 1, rotatedArray, 0, bytesArr.Length - 1);
rotatedArray[bytesArr.Length - 1] = first;
return rotatedArray;
}
internal static byte[] XOR(byte[] a1, byte[] a2)
{
byte[] res = new byte[a1.Length];
for (int i = 0; i < a1.Length; i++)
res[i] = (byte)(a1[i] ^ a2[i]);
return res;
}
internal static byte[] Truncate(byte[] cmac)
{
var halfMac = new byte[cmac.Length / 2];
for (var i = 1; i < cmac.Length; i += 2)
{
halfMac[i >> 1] = cmac[i];
}
return halfMac;
}
public static byte[] RotateLeft(byte[] bytesArr)
{
byte first = bytesArr[0];
byte[] rotatedArray = new byte[bytesArr.Length];
Array.Copy(bytesArr, 1, rotatedArray, 0, bytesArr.Length - 1);
rotatedArray[bytesArr.Length - 1] = first;
return rotatedArray;
}
internal static byte[] XOR(byte[] a1, byte[] a2)
{
byte[] res = new byte[a1.Length];
for (int i = 0; i < a1.Length; i++)
res[i] = (byte)(a1[i] ^ a2[i]);
return res;
}
internal static uint BytesToUIntLE(ReadOnlySpan<byte> bytes)
{
return bytes[0] + ((uint)bytes[1] << 8) + ((uint)bytes[2] << 16) + ((uint)bytes[3] << 24);
}
internal static byte[] UIntToBytesLE(uint value)
{
return new byte[] { (byte)(value & 0xff), (byte)((value >> 8) & 0xff), (byte)((value >> 16) & 0xff), (byte)((value >> 24) & 0xff) };
}
public static int BytesToUIntLE(byte[] value)
{
return value[0] + ((int)value[1] << 8) + ((int)value[2] << 16);
}
public static byte[] UIntTo3BytesLE(int value)
{
return new byte[] { (byte)(value & 0xff), (byte)((value >> 8) & 0xff), (byte)((value >> 16) & 0xff) };
}
public static byte[] UIntTo3BytesBE(int value)
{
return new byte[] { (byte)((value >> 16) & 0xff), (byte)((value >> 8) & 0xff), (byte)(value & 0xff) };
}
internal static byte[] UShortToBytesLE(int value)
{
if (value > ushort.MaxValue)
return new byte[] { 0xFF, 0xFF };
return new byte[] { (byte)(value & 0xff), (byte)((value >> 8) & 0xff) };
}
internal static uint BytesToUIntLE(ReadOnlySpan<byte> bytes)
{
return bytes[0] + ((uint)bytes[1] << 8) + ((uint)bytes[2] << 16) + ((uint)bytes[3] << 24);
}
internal static byte[] UIntToBytesLE(uint value)
{
return new byte[] { (byte)(value & 0xff), (byte)((value >> 8) & 0xff), (byte)((value >> 16) & 0xff), (byte)((value >> 24) & 0xff) };
}
public static int BytesToUIntLE(byte[] value)
{
return value[0] + ((int)value[1] << 8) + ((int)value[2] << 16);
}
public static byte[] UIntTo3BytesLE(int value)
{
return new byte[] { (byte)(value & 0xff), (byte)((value >> 8) & 0xff), (byte)((value >> 16) & 0xff) };
}
public static byte[] UIntTo3BytesBE(int value)
{
return new byte[] { (byte)((value >> 16) & 0xff), (byte)((value >> 8) & 0xff), (byte)(value & 0xff) };
}
internal static byte[] UShortToBytesLE(int value)
{
if (value > ushort.MaxValue)
return new byte[] { 0xFF, 0xFF };
return new byte[] { (byte)(value & 0xff), (byte)((value >> 8) & 0xff) };
}
internal static byte[] Concat(params byte[]?[] arrays)
{
var res = new byte[arrays.Sum(a => a?.Length ?? 0)];
int offset = 0;
foreach (var a in arrays)
{
if (a is null)
continue;
Array.Copy(a, 0, res, offset, a.Length);
offset += a.Length;
}
return res;
}
internal static byte[] Concat(params byte[]?[] arrays)
{
var res = new byte[arrays.Sum(a => a?.Length ?? 0)];
int offset = 0;
foreach (var a in arrays)
{
if (a is null)
continue;
Array.Copy(a, 0, res, offset, a.Length);
offset += a.Length;
}
return res;
}
//https://github.com/alexgorbatchev/crc/blob/master/src/calculators/crcjam.ts
readonly static uint[] TABLE = new uint[]{
0x00000000u, 0x77073096u, 0xee0e612cu, 0x990951bau, 0x076dc419u, 0x706af48fu, 0xe963a535u, 0x9e6495a3u,
0x0edb8832u, 0x79dcb8a4u, 0xe0d5e91eu, 0x97d2d988u, 0x09b64c2bu, 0x7eb17cbdu, 0xe7b82d07u, 0x90bf1d91u,
0x1db71064u, 0x6ab020f2u, 0xf3b97148u, 0x84be41deu, 0x1adad47du, 0x6ddde4ebu, 0xf4d4b551u, 0x83d385c7u,
0x136c9856u, 0x646ba8c0u, 0xfd62f97au, 0x8a65c9ecu, 0x14015c4fu, 0x63066cd9u, 0xfa0f3d63u, 0x8d080df5u,
0x3b6e20c8u, 0x4c69105eu, 0xd56041e4u, 0xa2677172u, 0x3c03e4d1u, 0x4b04d447u, 0xd20d85fdu, 0xa50ab56bu,
0x35b5a8fau, 0x42b2986cu, 0xdbbbc9d6u, 0xacbcf940u, 0x32d86ce3u, 0x45df5c75u, 0xdcd60dcfu, 0xabd13d59u,
0x26d930acu, 0x51de003au, 0xc8d75180u, 0xbfd06116u, 0x21b4f4b5u, 0x56b3c423u, 0xcfba9599u, 0xb8bda50fu,
0x2802b89eu, 0x5f058808u, 0xc60cd9b2u, 0xb10be924u, 0x2f6f7c87u, 0x58684c11u, 0xc1611dabu, 0xb6662d3du,
0x76dc4190u, 0x01db7106u, 0x98d220bcu, 0xefd5102au, 0x71b18589u, 0x06b6b51fu, 0x9fbfe4a5u, 0xe8b8d433u,
0x7807c9a2u, 0x0f00f934u, 0x9609a88eu, 0xe10e9818u, 0x7f6a0dbbu, 0x086d3d2du, 0x91646c97u, 0xe6635c01u,
0x6b6b51f4u, 0x1c6c6162u, 0x856530d8u, 0xf262004eu, 0x6c0695edu, 0x1b01a57bu, 0x8208f4c1u, 0xf50fc457u,
0x65b0d9c6u, 0x12b7e950u, 0x8bbeb8eau, 0xfcb9887cu, 0x62dd1ddfu, 0x15da2d49u, 0x8cd37cf3u, 0xfbd44c65u,
0x4db26158u, 0x3ab551ceu, 0xa3bc0074u, 0xd4bb30e2u, 0x4adfa541u, 0x3dd895d7u, 0xa4d1c46du, 0xd3d6f4fbu,
0x4369e96au, 0x346ed9fcu, 0xad678846u, 0xda60b8d0u, 0x44042d73u, 0x33031de5u, 0xaa0a4c5fu, 0xdd0d7cc9u,
0x5005713cu, 0x270241aau, 0xbe0b1010u, 0xc90c2086u, 0x5768b525u, 0x206f85b3u, 0xb966d409u, 0xce61e49fu,
0x5edef90eu, 0x29d9c998u, 0xb0d09822u, 0xc7d7a8b4u, 0x59b33d17u, 0x2eb40d81u, 0xb7bd5c3bu, 0xc0ba6cadu,
0xedb88320u, 0x9abfb3b6u, 0x03b6e20cu, 0x74b1d29au, 0xead54739u, 0x9dd277afu, 0x04db2615u, 0x73dc1683u,
0xe3630b12u, 0x94643b84u, 0x0d6d6a3eu, 0x7a6a5aa8u, 0xe40ecf0bu, 0x9309ff9du, 0x0a00ae27u, 0x7d079eb1u,
0xf00f9344u, 0x8708a3d2u, 0x1e01f268u, 0x6906c2feu, 0xf762575du, 0x806567cbu, 0x196c3671u, 0x6e6b06e7u,
0xfed41b76u, 0x89d32be0u, 0x10da7a5au, 0x67dd4accu, 0xf9b9df6fu, 0x8ebeeff9u, 0x17b7be43u, 0x60b08ed5u,
0xd6d6a3e8u, 0xa1d1937eu, 0x38d8c2c4u, 0x4fdff252u, 0xd1bb67f1u, 0xa6bc5767u, 0x3fb506ddu, 0x48b2364bu,
0xd80d2bdau, 0xaf0a1b4cu, 0x36034af6u, 0x41047a60u, 0xdf60efc3u, 0xa867df55u, 0x316e8eefu, 0x4669be79u,
0xcb61b38cu, 0xbc66831au, 0x256fd2a0u, 0x5268e236u, 0xcc0c7795u, 0xbb0b4703u, 0x220216b9u, 0x5505262fu,
0xc5ba3bbeu, 0xb2bd0b28u, 0x2bb45a92u, 0x5cb36a04u, 0xc2d7ffa7u, 0xb5d0cf31u, 0x2cd99e8bu, 0x5bdeae1du,
0x9b64c2b0u, 0xec63f226u, 0x756aa39cu, 0x026d930au, 0x9c0906a9u, 0xeb0e363fu, 0x72076785u, 0x05005713u,
0x95bf4a82u, 0xe2b87a14u, 0x7bb12baeu, 0x0cb61b38u, 0x92d28e9bu, 0xe5d5be0du, 0x7cdcefb7u, 0x0bdbdf21u,
0x86d3d2d4u, 0xf1d4e242u, 0x68ddb3f8u, 0x1fda836eu, 0x81be16cdu, 0xf6b9265bu, 0x6fb077e1u, 0x18b74777u,
0x88085ae6u, 0xff0f6a70u, 0x66063bcau, 0x11010b5cu, 0x8f659effu, 0xf862ae69u, 0x616bffd3u, 0x166ccf45u,
0xa00ae278u, 0xd70dd2eeu, 0x4e048354u, 0x3903b3c2u, 0xa7672661u, 0xd06016f7u, 0x4969474du, 0x3e6e77dbu,
0xaed16a4au, 0xd9d65adcu, 0x40df0b66u, 0x37d83bf0u, 0xa9bcae53u, 0xdebb9ec5u, 0x47b2cf7fu, 0x30b5ffe9u,
0xbdbdf21cu, 0xcabac28au, 0x53b39330u, 0x24b4a3a6u, 0xbad03605u, 0xcdd70693u, 0x54de5729u, 0x23d967bfu,
0xb3667a2eu, 0xc4614ab8u, 0x5d681b02u, 0x2a6f2b94u, 0xb40bbe37u, 0xc30c8ea1u, 0x5a05df1bu, 0x2d02ef8du,
};
public static uint CRCJam(byte[] data)
{
uint crc = 0xffffffff;
for (int index = 0; index < data.Length; index++)
{
crc = TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8);
}
return crc;
}
}
//https://github.com/alexgorbatchev/crc/blob/master/src/calculators/crcjam.ts
readonly static uint[] TABLE = new uint[]{
0x00000000u, 0x77073096u, 0xee0e612cu, 0x990951bau, 0x076dc419u, 0x706af48fu, 0xe963a535u, 0x9e6495a3u,
0x0edb8832u, 0x79dcb8a4u, 0xe0d5e91eu, 0x97d2d988u, 0x09b64c2bu, 0x7eb17cbdu, 0xe7b82d07u, 0x90bf1d91u,
0x1db71064u, 0x6ab020f2u, 0xf3b97148u, 0x84be41deu, 0x1adad47du, 0x6ddde4ebu, 0xf4d4b551u, 0x83d385c7u,
0x136c9856u, 0x646ba8c0u, 0xfd62f97au, 0x8a65c9ecu, 0x14015c4fu, 0x63066cd9u, 0xfa0f3d63u, 0x8d080df5u,
0x3b6e20c8u, 0x4c69105eu, 0xd56041e4u, 0xa2677172u, 0x3c03e4d1u, 0x4b04d447u, 0xd20d85fdu, 0xa50ab56bu,
0x35b5a8fau, 0x42b2986cu, 0xdbbbc9d6u, 0xacbcf940u, 0x32d86ce3u, 0x45df5c75u, 0xdcd60dcfu, 0xabd13d59u,
0x26d930acu, 0x51de003au, 0xc8d75180u, 0xbfd06116u, 0x21b4f4b5u, 0x56b3c423u, 0xcfba9599u, 0xb8bda50fu,
0x2802b89eu, 0x5f058808u, 0xc60cd9b2u, 0xb10be924u, 0x2f6f7c87u, 0x58684c11u, 0xc1611dabu, 0xb6662d3du,
0x76dc4190u, 0x01db7106u, 0x98d220bcu, 0xefd5102au, 0x71b18589u, 0x06b6b51fu, 0x9fbfe4a5u, 0xe8b8d433u,
0x7807c9a2u, 0x0f00f934u, 0x9609a88eu, 0xe10e9818u, 0x7f6a0dbbu, 0x086d3d2du, 0x91646c97u, 0xe6635c01u,
0x6b6b51f4u, 0x1c6c6162u, 0x856530d8u, 0xf262004eu, 0x6c0695edu, 0x1b01a57bu, 0x8208f4c1u, 0xf50fc457u,
0x65b0d9c6u, 0x12b7e950u, 0x8bbeb8eau, 0xfcb9887cu, 0x62dd1ddfu, 0x15da2d49u, 0x8cd37cf3u, 0xfbd44c65u,
0x4db26158u, 0x3ab551ceu, 0xa3bc0074u, 0xd4bb30e2u, 0x4adfa541u, 0x3dd895d7u, 0xa4d1c46du, 0xd3d6f4fbu,
0x4369e96au, 0x346ed9fcu, 0xad678846u, 0xda60b8d0u, 0x44042d73u, 0x33031de5u, 0xaa0a4c5fu, 0xdd0d7cc9u,
0x5005713cu, 0x270241aau, 0xbe0b1010u, 0xc90c2086u, 0x5768b525u, 0x206f85b3u, 0xb966d409u, 0xce61e49fu,
0x5edef90eu, 0x29d9c998u, 0xb0d09822u, 0xc7d7a8b4u, 0x59b33d17u, 0x2eb40d81u, 0xb7bd5c3bu, 0xc0ba6cadu,
0xedb88320u, 0x9abfb3b6u, 0x03b6e20cu, 0x74b1d29au, 0xead54739u, 0x9dd277afu, 0x04db2615u, 0x73dc1683u,
0xe3630b12u, 0x94643b84u, 0x0d6d6a3eu, 0x7a6a5aa8u, 0xe40ecf0bu, 0x9309ff9du, 0x0a00ae27u, 0x7d079eb1u,
0xf00f9344u, 0x8708a3d2u, 0x1e01f268u, 0x6906c2feu, 0xf762575du, 0x806567cbu, 0x196c3671u, 0x6e6b06e7u,
0xfed41b76u, 0x89d32be0u, 0x10da7a5au, 0x67dd4accu, 0xf9b9df6fu, 0x8ebeeff9u, 0x17b7be43u, 0x60b08ed5u,
0xd6d6a3e8u, 0xa1d1937eu, 0x38d8c2c4u, 0x4fdff252u, 0xd1bb67f1u, 0xa6bc5767u, 0x3fb506ddu, 0x48b2364bu,
0xd80d2bdau, 0xaf0a1b4cu, 0x36034af6u, 0x41047a60u, 0xdf60efc3u, 0xa867df55u, 0x316e8eefu, 0x4669be79u,
0xcb61b38cu, 0xbc66831au, 0x256fd2a0u, 0x5268e236u, 0xcc0c7795u, 0xbb0b4703u, 0x220216b9u, 0x5505262fu,
0xc5ba3bbeu, 0xb2bd0b28u, 0x2bb45a92u, 0x5cb36a04u, 0xc2d7ffa7u, 0xb5d0cf31u, 0x2cd99e8bu, 0x5bdeae1du,
0x9b64c2b0u, 0xec63f226u, 0x756aa39cu, 0x026d930au, 0x9c0906a9u, 0xeb0e363fu, 0x72076785u, 0x05005713u,
0x95bf4a82u, 0xe2b87a14u, 0x7bb12baeu, 0x0cb61b38u, 0x92d28e9bu, 0xe5d5be0du, 0x7cdcefb7u, 0x0bdbdf21u,
0x86d3d2d4u, 0xf1d4e242u, 0x68ddb3f8u, 0x1fda836eu, 0x81be16cdu, 0xf6b9265bu, 0x6fb077e1u, 0x18b74777u,
0x88085ae6u, 0xff0f6a70u, 0x66063bcau, 0x11010b5cu, 0x8f659effu, 0xf862ae69u, 0x616bffd3u, 0x166ccf45u,
0xa00ae278u, 0xd70dd2eeu, 0x4e048354u, 0x3903b3c2u, 0xa7672661u, 0xd06016f7u, 0x4969474du, 0x3e6e77dbu,
0xaed16a4au, 0xd9d65adcu, 0x40df0b66u, 0x37d83bf0u, 0xa9bcae53u, 0xdebb9ec5u, 0x47b2cf7fu, 0x30b5ffe9u,
0xbdbdf21cu, 0xcabac28au, 0x53b39330u, 0x24b4a3a6u, 0xbad03605u, 0xcdd70693u, 0x54de5729u, 0x23d967bfu,
0xb3667a2eu, 0xc4614ab8u, 0x5d681b02u, 0x2a6f2b94u, 0xb40bbe37u, 0xc30c8ea1u, 0x5a05df1bu, 0x2d02ef8du,
};
public static uint CRCJam(byte[] data)
{
uint crc = 0xffffffff;
for (int index = 0; index < data.Length; index++)
{
crc = TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8);
}
return crc;
}
}

View File

@ -1,14 +1,8 @@
using PCSC.Iso7816;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BoltCardTools
namespace BoltCardTools;
public interface IAPDUTransport
{
public interface IAPDUTransport
{
Task<NtagResponse> SendAPDU(NTagCommand apdu);
}
Task<NtagResponse> SendAPDU(NTagCommand apdu);
}

View File

@ -1,420 +1,409 @@
using Microsoft.VisualBasic;
using NdefLibrary.Ndef;
using PCSC.Iso7816;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using NdefLibrary.Ndef;
using static BoltCardTools.Helpers;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace BoltCardTools
namespace BoltCardTools;
public enum ISOLevel
{
public enum ISOLevel
{
PICC, // MF
Application // DF
}
public enum DataFile
{
CC = 0xE103,
NDEF = 0xE104,
Proprietary = 0xE105
}
public class Ntag424
{
public record Session(int KeyNo, AESKey ENCKey, AESKey MACKey, byte[] TransactionId)
{
public int Counter { get; set; }
public byte[] EncryptCommand(byte[] data)
{
if (data.Length == 0)
return data;
data = PaddingForEnc(data);
var iv = ENCKey.Encrypt(
Concat(
"A55A".HexToBytes(),
TransactionId,
UShortToBytesLE(Counter),
"0000000000000000".HexToBytes()
), null, CipherMode.ECB
);
return ENCKey.Encrypt(data, iv);
}
internal static byte[] PaddingForEnc(byte[] data)
{
var len = data.Length;
var paddingLen = 16 - (len % 16);
var padded = new byte[len + paddingLen];
Array.Copy(data, padded, len);
padded[len] = 0x80;
return padded;
}
public byte[] DecryptResponse(byte rc, byte[] data, CommMode commMode)
{
if (commMode is CommMode.Plain || data.Length == 0)
return data;
var mac = data[^8..];
data = data[..^8];
var expectedMac = this.GetMac(rc, data);
if (!expectedMac.IsSame(mac))
throw new UnexpectedResponseException("Invalid MAC");
if (data.Length == 0 || commMode == CommMode.MAC)
return data;
var iv = ENCKey.Encrypt(
Concat(
"5AA5".HexToBytes(),
TransactionId,
UShortToBytesLE(Counter),
"0000000000000000".HexToBytes()
), null, CipherMode.ECB
);
var decrypted = ENCKey.Decrypt(data, iv);
var paddingStart = Array.LastIndexOf(decrypted, (byte)0x80);
return decrypted[0..paddingStart];
}
public byte[] GetMac(byte ins, byte[]? data)
{
var macValue = Concat(
new byte[] { ins },
UShortToBytesLE(Counter),
TransactionId,
data ?? Array.Empty<byte>());
var mac = MACKey.CMac(macValue);
return Truncate(mac);
}
}
readonly IAPDUTransport Transport;
public Ntag424(IAPDUTransport transport)
{
Transport = transport;
}
public Session? CurrentSession { get; private set; }
public async Task IsoSelectFile(ISOLevel level)
{
await SendAPDU(NtagCommands.ISOSelectFile with
{
P1 = 0x04,
P2 = 0x00,
Data = (level switch
{
ISOLevel.PICC => "d2760000850100",
ISOLevel.Application => "d2760000850101",
_ => throw new ArgumentException(nameof(level))
}).HexToBytes()
});
}
public async Task IsoSelectFile(DataFile file)
{
await SendAPDU(NtagCommands.ISOSelectFile with
{
P1 = 0x00,
P2 = 0x00,
Data = new byte[] { (byte)((int)file >> 8), (byte)file },
Le = 0
});
}
public Task<Session> AuthenticateEV2NonFirst(int keyNo, AESKey key)
{
return AuthenticateEV2(keyNo, key, false);
}
public Task<Session> AuthenticateEV2First(int keyNo, AESKey key)
{
return AuthenticateEV2(keyNo, key, true);
}
public async Task<Session> AuthenticateEV2(int keyNo, AESKey key, bool first)
{
int sessionCounter = CurrentSession?.Counter ?? 0;
if (first)
{
await IsoSelectFile(ISOLevel.Application);
sessionCounter = 0;
}
else
{
if (CurrentSession is null)
throw new InvalidOperationException("Authentication required for AuthenticateEV2NonFirst");
sessionCounter = CurrentSession.Counter;
}
NtagResponse resp;
if (first)
{
resp = await SendAPDU(NtagCommands.AuthenticateEV2FirstPart1 with
{
Data = $"{(byte)keyNo:x2}03000000".HexToBytes()
});
}
else
{
resp = await SendAPDU(NtagCommands.AuthenticateEV2NonFirstPart1 with
{
Data = new byte[] { (byte)keyNo }
});
}
var rndB = key.Decrypt(resp.Data);
var rndBp = RotateLeft(rndB);
var rndA = RandomNumberGenerator.GetBytes(16);
var encRnd = key.Encrypt(Concat(rndA, rndBp));
var secondPart = first ? NtagCommands.AuthenticateEV2FirstPart2 : NtagCommands.AuthenticateEV2NonFirstPart2;
resp = await SendAPDU(secondPart with
{
Data = encRnd
});
var data = key.Decrypt(resp.Data);
var rndAp = RotateLeft(rndA);
byte[] tid;
byte[] actualRndAp;
if (first)
{
tid = data[0..4];
actualRndAp = data[4..20];
}
else
{
tid = CurrentSession!.TransactionId;
actualRndAp = data[0..16];
}
if (!rndAp.IsSame(actualRndAp))
throw new UnexpectedResponseException("Invalid RndAp response");
var rndMix = Concat(
rndA[0..2],
XOR(rndA[2..8], rndB[0..6]),
rndB[6..16],
rndA[8..16]);
var encKey = key.Derive(Concat(
"A55A00010080".HexToBytes(),
rndMix));
var macKey = key.Derive(Concat(
"5AA500010080".HexToBytes(),
rndMix));
var session = new Session(keyNo, encKey, macKey, tid)
{
Counter = sessionCounter
};
CurrentSession = session;
return session;
}
private async Task<NtagResponse> SendAPDU(NTagCommand command)
{
CommMode commandMode;
if (command.CommMode is CommMode m)
{
commandMode = m;
}
else
{
commandMode = CurrentSession is null ? CommMode.Plain : CommMode.Full;
command = command with
{
CommMode = commandMode
};
}
if (commandMode is not CommMode.Plain)
{
if (CurrentSession is null)
throw new InvalidOperationException("Authentication required");
command = command.Encode(CurrentSession);
}
if (CurrentSession is not null)
CurrentSession.Counter++;
var resp = await Transport.SendAPDU(command);
command.ThrowIfUnexpected(resp);
if (commandMode is not CommMode.Plain && CurrentSession is not null)
{
resp = resp.Decode(CurrentSession, commandMode);
}
return resp;
}
public async Task<byte[]> GetCardUID()
{
return (await SendAPDU(NtagCommands.GetCardUID)).Data;
}
public async Task<FileSettings> GetFileSettings(DataFile file = DataFile.NDEF)
{
return new FileSettings((await SendAPDU(NtagCommands.GetFileSettings with
{
Data = GetFileNo(file)
})).Data, false);
}
public async Task ChangeFileSettings(DataFile file = DataFile.NDEF, FileSettings? fileSettings = null)
{
fileSettings ??= new FileSettings(file);
await SendAPDU(NtagCommands.ChangeFileSettings with
{
Data = Concat(
GetFileNo(file),
fileSettings.ToBytes()
)
});
}
public async Task<NdefMessage> ReadNDef()
{
await IsoSelectFile(ISOLevel.Application);
await IsoSelectFile(DataFile.NDEF);
var size = (await SendAPDU(NtagCommands.ISOReadBinary with
{
P1 = 0,
P2 = 0,
Le = 2
})).Data[1];
var data = (await SendAPDU(NtagCommands.ISOReadBinary with
{
P1 = 0,
P2 = 2,
Le = size
})).Data;
return NdefMessage.FromByteArray(data);
}
public async Task<byte[]> ReadFile(DataFile file, int offset, int length)
{
var commMode = await GetCommMode(file, AccessRight.Read);
return (await SendAPDU(NtagCommands.ReadData with
{
CommMode = commMode,
CommandHeaderSize = 7,
Data = Concat(
GetFileNo(file),
UIntTo3BytesLE(offset),
UIntTo3BytesLE(length)
)
})).Data;
}
private async Task<CommMode> GetCommMode(DataFile file, AccessRight requiredRight)
{
if (CurrentSession is null)
return CommMode.Plain;
var settings = await GetFileSettings(file);
if (!settings.IsAllowed(CurrentSession.KeyNo, requiredRight))
throw new SecurityException($"The key {CurrentSession.KeyNo} doesn't have the necessary permissions");
return settings.CommMode;
}
private static byte[] GetFileNo(DataFile file)
{
return new byte[] { file switch
{
DataFile.CC => 0x01,
DataFile.NDEF => 0x02,
DataFile.Proprietary => 0x03,
_ => throw new ArgumentException(nameof(file))
} };
}
public async Task WriteNDef(NdefMessage message)
{
var ndefMessageBytes = message.ToByteArray();
var content = new byte[220]; // Normally we have 256 bytes, but APDU has a size limit we need some margin
content[0] = (byte)(ndefMessageBytes.Length >> 8);
content[1] = (byte)ndefMessageBytes.Length;
Array.Copy(ndefMessageBytes, 0, content, 2, Math.Min(content.Length - 2, ndefMessageBytes.Length));
await SendAPDU(NtagCommands.WriteData with
{
CommMode = await GetCommMode(DataFile.NDEF, AccessRight.Write),
Data = Concat(
GetFileNo(DataFile.NDEF),
new byte[] { 0x00, 0x00, 0x00 },
UIntTo3BytesLE(content.Length),
content
)
});
}
public async Task ChangeKey(int keyNo, AESKey newKey, AESKey? oldKey = null, int version = 0)
{
if (CurrentSession is null || CurrentSession.KeyNo != 0)
throw new InvalidOperationException("Authentication required with KeyNo 0");
byte[] data;
if (keyNo == 0)
{
data = Concat(
newKey.ToBytes(),
new byte[] { (byte)version }
);
}
else
{
oldKey ??= new AESKey(new byte[16]);
data = Concat(
XOR(newKey.ToBytes(), oldKey.ToBytes()),
new byte[] { (byte)version },
UIntToBytesLE(CRCJam(newKey.ToBytes())));
}
await SendAPDU(NtagCommands.ChangeKey with
{
Data = Concat(
new byte[] { (byte)keyNo },
data
)
});
}
public async Task SetupBoltcard(string lnurlw)
{
if (!lnurlw.Contains('?', StringComparison.OrdinalIgnoreCase))
lnurlw += "?";
else
lnurlw += "&";
lnurlw += "p=00000000000000000000000000000000&c=0000000000000000";
var ndef = new NdefMessage
{
new NdefUriRecord() { Uri = lnurlw }
};
await WriteNDef(ndef);
var ndefBytes = ndef.ToByteArray();
var pIndex = Array.LastIndexOf(ndefBytes, (byte)'p') + 4;
var cIndex = Array.LastIndexOf(ndefBytes, (byte)'c') + 4;
var settings = new FileSettings(DataFile.NDEF)
{
AccessRights = new ()
{
ReadWrite = AccessCondition.Key0,
Change = AccessCondition.Key0,
Write = AccessCondition.Key0,
Read = AccessCondition.Free
},
SDMMirroring = true,
SDMUID = true,
SDMReadCtr = true,
SDMAccessRights = new ()
{
MetaRead = AccessCondition.Key1,
FileRead = AccessCondition.Key2,
CtrRet = AccessCondition.Never
},
SDMMACInputOffset = cIndex,
SDMMACOffset = cIndex,
PICCDataOffset = pIndex
};
await ChangeFileSettings(fileSettings: settings);
}
}
PICC, // MF
Application // DF
}
public enum DataFile
{
CC = 0xE103,
NDEF = 0xE104,
Proprietary = 0xE105
}
public class Ntag424
{
public record Session(int KeyNo, AESKey ENCKey, AESKey MACKey, byte[] TransactionId)
{
public int Counter { get; set; }
public byte[] EncryptCommand(byte[] data)
{
if (data.Length == 0)
return data;
data = PaddingForEnc(data);
var iv = ENCKey.Encrypt(
Concat(
"A55A".HexToBytes(),
TransactionId,
UShortToBytesLE(Counter),
"0000000000000000".HexToBytes()
), null, CipherMode.ECB
);
return ENCKey.Encrypt(data, iv);
}
internal static byte[] PaddingForEnc(byte[] data)
{
var len = data.Length;
var paddingLen = 16 - (len % 16);
var padded = new byte[len + paddingLen];
Array.Copy(data, padded, len);
padded[len] = 0x80;
return padded;
}
public byte[] DecryptResponse(byte rc, byte[] data, CommMode commMode)
{
if (commMode is CommMode.Plain || data.Length == 0)
return data;
var mac = data[^8..];
data = data[..^8];
var expectedMac = this.GetMac(rc, data);
if (!expectedMac.IsSame(mac))
throw new UnexpectedResponseException("Invalid MAC");
if (data.Length == 0 || commMode == CommMode.MAC)
return data;
var iv = ENCKey.Encrypt(
Concat(
"5AA5".HexToBytes(),
TransactionId,
UShortToBytesLE(Counter),
"0000000000000000".HexToBytes()
), null, CipherMode.ECB
);
var decrypted = ENCKey.Decrypt(data, iv);
var paddingStart = Array.LastIndexOf(decrypted, (byte)0x80);
return decrypted[0..paddingStart];
}
public byte[] GetMac(byte ins, byte[]? data)
{
var macValue = Concat(
new byte[] { ins },
UShortToBytesLE(Counter),
TransactionId,
data ?? Array.Empty<byte>());
var mac = MACKey.CMac(macValue);
return Truncate(mac);
}
}
readonly IAPDUTransport Transport;
public Ntag424(IAPDUTransport transport)
{
Transport = transport;
}
public Session? CurrentSession { get; private set; }
public async Task IsoSelectFile(ISOLevel level)
{
await SendAPDU(NtagCommands.ISOSelectFile with
{
P1 = 0x04,
P2 = 0x00,
Data = (level switch
{
ISOLevel.PICC => "d2760000850100",
ISOLevel.Application => "d2760000850101",
_ => throw new ArgumentException(nameof(level))
}).HexToBytes()
});
}
public async Task IsoSelectFile(DataFile file)
{
await SendAPDU(NtagCommands.ISOSelectFile with
{
P1 = 0x00,
P2 = 0x00,
Data = new byte[] { (byte)((int)file >> 8), (byte)file },
Le = 0
});
}
public Task<Session> AuthenticateEV2NonFirst(int keyNo, AESKey key)
{
return AuthenticateEV2(keyNo, key, false);
}
public Task<Session> AuthenticateEV2First(int keyNo, AESKey key)
{
return AuthenticateEV2(keyNo, key, true);
}
public async Task<Session> AuthenticateEV2(int keyNo, AESKey key, bool first)
{
int sessionCounter = CurrentSession?.Counter ?? 0;
if (first)
{
await IsoSelectFile(ISOLevel.Application);
sessionCounter = 0;
}
else
{
if (CurrentSession is null)
throw new InvalidOperationException("Authentication required for AuthenticateEV2NonFirst");
sessionCounter = CurrentSession.Counter;
}
NtagResponse resp;
if (first)
{
resp = await SendAPDU(NtagCommands.AuthenticateEV2FirstPart1 with
{
Data = $"{(byte)keyNo:x2}03000000".HexToBytes()
});
}
else
{
resp = await SendAPDU(NtagCommands.AuthenticateEV2NonFirstPart1 with
{
Data = new byte[] { (byte)keyNo }
});
}
var rndB = key.Decrypt(resp.Data);
var rndBp = RotateLeft(rndB);
var rndA = RandomNumberGenerator.GetBytes(16);
var encRnd = key.Encrypt(Concat(rndA, rndBp));
var secondPart = first ? NtagCommands.AuthenticateEV2FirstPart2 : NtagCommands.AuthenticateEV2NonFirstPart2;
resp = await SendAPDU(secondPart with
{
Data = encRnd
});
var data = key.Decrypt(resp.Data);
var rndAp = RotateLeft(rndA);
byte[] tid;
byte[] actualRndAp;
if (first)
{
tid = data[0..4];
actualRndAp = data[4..20];
}
else
{
tid = CurrentSession!.TransactionId;
actualRndAp = data[0..16];
}
if (!rndAp.IsSame(actualRndAp))
throw new UnexpectedResponseException("Invalid RndAp response");
var rndMix = Concat(
rndA[0..2],
XOR(rndA[2..8], rndB[0..6]),
rndB[6..16],
rndA[8..16]);
var encKey = key.Derive(Concat(
"A55A00010080".HexToBytes(),
rndMix));
var macKey = key.Derive(Concat(
"5AA500010080".HexToBytes(),
rndMix));
var session = new Session(keyNo, encKey, macKey, tid)
{
Counter = sessionCounter
};
CurrentSession = session;
return session;
}
private async Task<NtagResponse> SendAPDU(NTagCommand command)
{
CommMode commandMode;
if (command.CommMode is CommMode m)
{
commandMode = m;
}
else
{
commandMode = CurrentSession is null ? CommMode.Plain : CommMode.Full;
command = command with
{
CommMode = commandMode
};
}
if (commandMode is not CommMode.Plain)
{
if (CurrentSession is null)
throw new InvalidOperationException("Authentication required");
command = command.Encode(CurrentSession);
}
if (CurrentSession is not null)
CurrentSession.Counter++;
var resp = await Transport.SendAPDU(command);
command.ThrowIfUnexpected(resp);
if (commandMode is not CommMode.Plain && CurrentSession is not null)
{
resp = resp.Decode(CurrentSession, commandMode);
}
return resp;
}
public async Task<byte[]> GetCardUID()
{
return (await SendAPDU(NtagCommands.GetCardUID)).Data;
}
public async Task<FileSettings> GetFileSettings(DataFile file = DataFile.NDEF)
{
return new FileSettings((await SendAPDU(NtagCommands.GetFileSettings with
{
Data = GetFileNo(file)
})).Data, false);
}
public async Task ChangeFileSettings(DataFile file = DataFile.NDEF, FileSettings? fileSettings = null)
{
fileSettings ??= new FileSettings(file);
await SendAPDU(NtagCommands.ChangeFileSettings with
{
Data = Concat(
GetFileNo(file),
fileSettings.ToBytes()
)
});
}
public async Task<NdefMessage> ReadNDef()
{
await IsoSelectFile(ISOLevel.Application);
await IsoSelectFile(DataFile.NDEF);
var size = (await SendAPDU(NtagCommands.ISOReadBinary with
{
P1 = 0,
P2 = 0,
Le = 2
})).Data[1];
var data = (await SendAPDU(NtagCommands.ISOReadBinary with
{
P1 = 0,
P2 = 2,
Le = size
})).Data;
return NdefMessage.FromByteArray(data);
}
public async Task<byte[]> ReadFile(DataFile file, int offset, int length)
{
var commMode = await GetCommMode(file, AccessRight.Read);
return (await SendAPDU(NtagCommands.ReadData with
{
CommMode = commMode,
CommandHeaderSize = 7,
Data = Concat(
GetFileNo(file),
UIntTo3BytesLE(offset),
UIntTo3BytesLE(length)
)
})).Data;
}
private async Task<CommMode> GetCommMode(DataFile file, AccessRight requiredRight)
{
if (CurrentSession is null)
return CommMode.Plain;
var settings = await GetFileSettings(file);
if (!settings.IsAllowed(CurrentSession.KeyNo, requiredRight))
throw new SecurityException($"The key {CurrentSession.KeyNo} doesn't have the necessary permissions");
return settings.CommMode;
}
private static byte[] GetFileNo(DataFile file)
{
return new byte[] { file switch
{
DataFile.CC => 0x01,
DataFile.NDEF => 0x02,
DataFile.Proprietary => 0x03,
_ => throw new ArgumentException(nameof(file))
} };
}
public async Task WriteNDef(NdefMessage message)
{
var ndefMessageBytes = message.ToByteArray();
var content = new byte[220]; // Normally we have 256 bytes, but APDU has a size limit we need some margin
content[0] = (byte)(ndefMessageBytes.Length >> 8);
content[1] = (byte)ndefMessageBytes.Length;
Array.Copy(ndefMessageBytes, 0, content, 2, Math.Min(content.Length - 2, ndefMessageBytes.Length));
await SendAPDU(NtagCommands.WriteData with
{
CommMode = await GetCommMode(DataFile.NDEF, AccessRight.Write),
Data = Concat(
GetFileNo(DataFile.NDEF),
new byte[] { 0x00, 0x00, 0x00 },
UIntTo3BytesLE(content.Length),
content
)
});
}
public async Task ChangeKey(int keyNo, AESKey newKey, AESKey? oldKey = null, int version = 0)
{
if (CurrentSession is null || CurrentSession.KeyNo != 0)
throw new InvalidOperationException("Authentication required with KeyNo 0");
byte[] data;
if (keyNo == 0)
{
data = Concat(
newKey.ToBytes(),
new byte[] { (byte)version }
);
}
else
{
oldKey ??= new AESKey(new byte[16]);
data = Concat(
XOR(newKey.ToBytes(), oldKey.ToBytes()),
new byte[] { (byte)version },
UIntToBytesLE(CRCJam(newKey.ToBytes())));
}
await SendAPDU(NtagCommands.ChangeKey with
{
Data = Concat(
new byte[] { (byte)keyNo },
data
)
});
}
public async Task SetupBoltcard(string lnurlw)
{
if (!lnurlw.Contains('?', StringComparison.OrdinalIgnoreCase))
lnurlw += "?";
else
lnurlw += "&";
lnurlw += "p=00000000000000000000000000000000&c=0000000000000000";
var ndef = new NdefMessage
{
new NdefUriRecord() { Uri = lnurlw }
};
await WriteNDef(ndef);
var ndefBytes = ndef.ToByteArray();
var pIndex = Array.LastIndexOf(ndefBytes, (byte)'p') + 4;
var cIndex = Array.LastIndexOf(ndefBytes, (byte)'c') + 4;
var settings = new FileSettings(DataFile.NDEF)
{
AccessRights = new()
{
ReadWrite = AccessCondition.Key0,
Change = AccessCondition.Key0,
Write = AccessCondition.Key0,
Read = AccessCondition.Free
},
SDMMirroring = true,
SDMUID = true,
SDMReadCtr = true,
SDMAccessRights = new()
{
MetaRead = AccessCondition.Key1,
FileRead = AccessCondition.Key2,
CtrRet = AccessCondition.Never
},
SDMMACInputOffset = cIndex,
SDMMACOffset = cIndex,
PICCDataOffset = pIndex
};
await ChangeFileSettings(fileSettings: settings);
}
}

View File

@ -1,191 +1,183 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Security.AccessControl;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Transactions;
using static BoltCardTools.Helpers;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace BoltCardTools
namespace BoltCardTools;
public enum CommMode
{
public enum CommMode
{
Plain,
Full,
MAC
}
public record NTagError(ushort sw1sw2, string Code, string Description)
{
public override string ToString()
{
return $"{Code} ({sw1sw2:x4}): {Description}";
}
}
public record NTagCommand(string Name, byte CLA, byte INS, byte? P1, byte? P2, byte? Lc, byte[]? Data, byte? Le, ushort ExpectedStatus, CommMode? CommMode, int CommandHeaderSize = 0)
{
public List<NTagError> ErrorCodes = new List<NTagError>();
static List<NTagError> DefaultErrorCodes = new List<NTagError>()
{
new NTagError(0x9100, "OPERATION_OK", "Successful operation."),
new NTagError(0x911C, "ILLEGAL_COMMAND_CODE", "Command code not supported."),
new NTagError(0x911E, "INTEGRITY_ERROR", "CRC or MAC does not match data. Padding bytes not valid."),
new NTagError(0x9140, "NO_SUCH_KEY", "Invalid key number specified."),
new NTagError(0x917E, "LENGTH_ERROR", "Length of command string invalid."),
new NTagError(0x919D, "PERMISSION_DENIED", "Current configuration / status does not allow the requested command."),
new NTagError(0x919E, "PARAMETER_ERROR", "Value of the parameter(s) invalid."),
new NTagError(0x91AD, "AUTHENTICATION_DELAY", "Currently not allowed to authenticate. Keep trying until full delay is spent."),
new NTagError(0x91AE, "AUTHENTICATION_ERROR", "Current authentication status does not allow the requested command."),
new NTagError(0x91AF, "ADDITIONAL_FRAME", "Additional data frame is expected to be sent."),
new NTagError(0x91BE, "BOUNDARY_ERROR", "Attempt to read/write data from/to beyond the files/records limits. Attempt to exceed the limits of a value file."),
new NTagError(0x91CA, "COMMAND_ABORTED", "Previous Command was not fully completed. Not all Frames were requested or provided by the PCD."),
new NTagError(0x91F0, "FILE_NOT_FOUND", "Specified file number does not exist."),
new NTagError(0x6700, "WRONG_LENGTH", "Wrong length; no further indication."),
new NTagError(0x6982, "SECURITY_STATUS_NOT_SATISFIED", "Security status not satisfied."),
new NTagError(0x6985, "CONDITIONS_OF_USE_NOT_SATISFIED", "Conditions of use not satisfied."),
new NTagError(0x6A80, "INCORRECT_PARAMETERS_IN_DATA_FIELD", "Incorrect parameters in the command data field."),
new NTagError(0x6A82, "FILE_OR_APPLICATION_NOT_FOUND", "File or application not found."),
new NTagError(0x6A86, "INCORRECT_PARAMETERS_P1_P2", "Incorrect parameters P1-P2."),
new NTagError(0x6A87, "LC_INCONSISTENT_WITH_PARAMETERS_P1_P2", "Lc inconsistent with parameters P1-P2."),
new NTagError(0x6C00, "WRONG_LE_FIELD", "Wrong Le field."),
new NTagError(0x6D00, "INSTRUCTION_CODE_NOT_SUPPORTED_OR_INVALID", "Instruction code not supported or invalid."),
new NTagError(0x6E00, "CLASS_NOT_SUPPORTED", "Class not supported."),
new NTagError(0x9000, "NORMAL_PROCESSING", "Normal processing (no further qualification).")
};
internal void ThrowIfUnexpected(NtagResponse resp)
{
if (resp.sw1sw2 != ExpectedStatus)
{
var errorCode = ErrorCodes.FirstOrDefault(c => c.sw1sw2 == resp.sw1sw2);
errorCode ??= DefaultErrorCodes.FirstOrDefault(c => c.sw1sw2 == resp.sw1sw2);
if (errorCode is null)
throw new UnexpectedStatusException(Name, ExpectedStatus, resp.sw1sw2);
else
throw new UnexpectedStatusException(Name, ExpectedStatus, errorCode);
}
}
public byte[] ToBytes()
{
var list = new List<byte>
{
CLA,
INS
};
if (!P1.HasValue)
throw new InvalidOperationException("P1 not provided");
if (!P2.HasValue)
throw new InvalidOperationException("P2 not provided");
list.Add(P1.Value);
list.Add(P2.Value);
if (Data != null)
{
if (Lc.HasValue)
{
var realLc = Lc.Value;
if (CommMode is BoltCardTools.CommMode.Full)
{
var encDataSize = realLc - CommandHeaderSize;
realLc = (byte)CommandHeaderSize;
realLc += (byte)(16 - (encDataSize % 16)); // Padding
realLc += 8; // Add mac
}
if (CommMode is BoltCardTools.CommMode.MAC)
{
realLc += 8; // Add mac
}
if (realLc != Data.Length)
throw new InvalidOperationException("Invalid Data length");
}
list.Add((byte)(Data.Length));
list.AddRange(Data);
}
if (Le.HasValue)
{
list.Add(Le.Value);
}
return list.ToArray();
}
public override string ToString()
{
return ToBytes().ToHex();
}
internal NTagCommand Encode(Ntag424.Session currentSession)
{
if (CommMode is null)
throw new InvalidOperationException("CommMode isn't set");
if (CommMode is BoltCardTools.CommMode.Plain)
return this;
var data = Data;
if (CommMode is BoltCardTools.CommMode.Full && data is not null)
{
var nonEncrypted = data[0..CommandHeaderSize];
var encrypted = data[CommandHeaderSize..];
data = Concat(nonEncrypted, currentSession.EncryptCommand(encrypted));
}
var mac = currentSession.GetMac(INS, data);
data = Concat(data, mac);
return this with
{
Data = data
};
}
}
internal class NtagCommands
{
internal readonly static NTagCommand AuthenticateEV2FirstPart1 = new(Name: "AuthenticateEV2FirstPart1", CLA: 0x90, INS: 0x71, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: CommMode.Plain);
internal readonly static NTagCommand AuthenticateEV2FirstPart2 = new(Name: "AuthenticateEV2FirstPart2", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: 32, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Plain);
internal readonly static NTagCommand AuthenticateEV2NonFirstPart1 = new(Name: "AuthenticateEV2NonFirstPart1", CLA: 0x90, INS: 0x77, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: CommMode.Plain);
internal readonly static NTagCommand AuthenticateEV2NonFirstPart2 = new(Name: "AuthenticateEV2NonFirstPart2", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: 32, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Plain);
internal readonly static NTagCommand AuthenticateLRPFirstPart1 = new(Name: "AuthenticateLRPFirstPart1", CLA: 0x90, INS: 0x71, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: null);
internal readonly static NTagCommand AuthenticateLRPFirstPart2 = new(Name: "AuthenticateLRPFirstPart2", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: 32, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: null);
internal readonly static NTagCommand AuthenticateLRPNonFirstPart1 = new(Name: "AuthenticateLRPNonFirstPart1", CLA: 0x90, INS: 0x77, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: null);
internal readonly static NTagCommand AuthenticateLRPNonFirstPart2 = new(Name: "AuthenticateLRPNonFirstPart2", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: 32, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: null);
internal readonly static NTagCommand ChangeFileSettings = new(Name: "ChangeFileSettings", CLA: 0x90, INS: 0x5F, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full, CommandHeaderSize: 1);
internal readonly static NTagCommand ChangeKey = new(Name: "ChangeKey", CLA: 0x90, INS: 0xC4, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full, CommandHeaderSize: 1)
{
ErrorCodes =
{
new NTagError(0x91CA, "COMMAND_ABORTED", "Chained command or multiple pass command ongoing."),
new NTagError(0x911E, "INTEGRITY_ERROR", "Integrity error in cryptogram or invalid secure messaging MAC (Secure Messaging)."),
new NTagError(0x917E, "LENGTH_ERROR", "Command size not allowed."),
new NTagError(0x919E, "PARAMETER_ERROR", "Parameter value not allowed."),
new NTagError(0x9140, "NO_SUCH_KEY", "Targeted key does not exist."),
new NTagError(0x919D, "PERMISSION_DENIED", "At PICC level, targeting any OriginalityKey which cannot be changed."),
new NTagError(0x91AE, "AUTHENTICATION_ERROR", "At application level, missing active authentication with AppMasterKey while targeting any AppKey."),
new NTagError(0x91EE, "MEMORY_ERROR", "Failure when reading or writing to non-volatile memory.")
}
};
internal readonly static NTagCommand GetCardUID = new(Name: "GetCardUID", CLA: 0x90, INS: 0x51, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full);
internal readonly static NTagCommand GetFileCounters = new(Name: "GetFileCounters", CLA: 0x90, INS: 0xF6, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full);
internal readonly static NTagCommand GetFileSettings = new(Name: "GetFileSettings", CLA: 0x90, INS: 0xF5, P1: 0, P2: 0, Lc: 1, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.MAC)
{
ErrorCodes =
{
new NTagError(0x91CA, "COMMAND_ABORTED", "Chained command or multiple pass command ongoing."),
new NTagError(0x911E, "INTEGRITY_ERROR", "Invalid secure messaging MAC (only)."),
new NTagError(0x917E, "LENGTH_ERROR", "Command size not allowed."),
new NTagError(0x919E, "PARAMETER_ERROR", "Parameter value not allowed."),
new NTagError(0x919D, "PERMISSION_DENIED", "PICC level (MF) is selected."),
new NTagError(0x91F0, "FILE_NOT_FOUND", "File with targeted FileNo does not exist for the targeted application."),
new NTagError(0x91EE, "MEMORY_ERROR", "Failure when reading or writing to non-volatile memory.")
}
};
internal readonly static NTagCommand GetKeyVersion = new(Name: "GetKeyVersion", CLA: 0x90, INS: 0x64, P1: 0, P2: 0, Lc: 1, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.MAC);
internal readonly static NTagCommand GetVersionPart1 = new(Name: "GetVersionPart1", CLA: 0x90, INS: 0x60, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: CommMode.MAC);
internal readonly static NTagCommand GetVersionPart2 = new(Name: "GetVersionPart2", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: CommMode.MAC);
internal readonly static NTagCommand GetVersionPart3 = new(Name: "GetVersionPart3", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.MAC);
internal readonly static NTagCommand ISOReadBinary = new(Name: "ISOReadBinary", CLA: 0x00, INS: 0xB0, P1: null, P2: null, Lc: null, Data: null, Le: null, ExpectedStatus: 0x9000, CommMode: CommMode.Plain);
internal readonly static NTagCommand ReadData = new(Name: "ReadData", CLA: 0x90, INS: 0xAD, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: null, CommandHeaderSize: 7);
internal readonly static NTagCommand Read_Sig = new(Name: "Read_Sig", CLA: 0x90, INS: 0x3C, P1: 0, P2: 0, Lc: 1, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full);
internal readonly static NTagCommand ISOSelectFile = new(Name: "ISOSelectFile", CLA: 0x00, INS: 0xA4, P1: null, P2: null, Lc: null, Data: null, Le: null, ExpectedStatus: 0x9000, CommMode: CommMode.Plain);
internal readonly static NTagCommand SetConfiguration = new(Name: "SetConfiguration", CLA: 0x90, INS: 0x5C, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full, CommandHeaderSize: 1);
internal readonly static NTagCommand ISOUpdateBinary = new(Name: "ISOUpdateBinary", CLA: 0x00, INS: 0xD6, P1: null, P2: null, Lc: null, Data: null, Le: null, ExpectedStatus: 0x9000, CommMode: CommMode.Plain);
internal readonly static NTagCommand WriteData = new(Name: "WriteData", CLA: 0x90, INS: 0x8D, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: null, CommandHeaderSize: 7);
}
Plain,
Full,
MAC
}
public record NTagError(ushort sw1sw2, string Code, string Description)
{
public override string ToString()
{
return $"{Code} ({sw1sw2:x4}): {Description}";
}
}
public record NTagCommand(string Name, byte CLA, byte INS, byte? P1, byte? P2, byte? Lc, byte[]? Data, byte? Le, ushort ExpectedStatus, CommMode? CommMode, int CommandHeaderSize = 0)
{
public List<NTagError> ErrorCodes = new List<NTagError>();
static List<NTagError> DefaultErrorCodes = new List<NTagError>()
{
new NTagError(0x9100, "OPERATION_OK", "Successful operation."),
new NTagError(0x911C, "ILLEGAL_COMMAND_CODE", "Command code not supported."),
new NTagError(0x911E, "INTEGRITY_ERROR", "CRC or MAC does not match data. Padding bytes not valid."),
new NTagError(0x9140, "NO_SUCH_KEY", "Invalid key number specified."),
new NTagError(0x917E, "LENGTH_ERROR", "Length of command string invalid."),
new NTagError(0x919D, "PERMISSION_DENIED", "Current configuration / status does not allow the requested command."),
new NTagError(0x919E, "PARAMETER_ERROR", "Value of the parameter(s) invalid."),
new NTagError(0x91AD, "AUTHENTICATION_DELAY", "Currently not allowed to authenticate. Keep trying until full delay is spent."),
new NTagError(0x91AE, "AUTHENTICATION_ERROR", "Current authentication status does not allow the requested command."),
new NTagError(0x91AF, "ADDITIONAL_FRAME", "Additional data frame is expected to be sent."),
new NTagError(0x91BE, "BOUNDARY_ERROR", "Attempt to read/write data from/to beyond the files/records limits. Attempt to exceed the limits of a value file."),
new NTagError(0x91CA, "COMMAND_ABORTED", "Previous Command was not fully completed. Not all Frames were requested or provided by the PCD."),
new NTagError(0x91F0, "FILE_NOT_FOUND", "Specified file number does not exist."),
new NTagError(0x6700, "WRONG_LENGTH", "Wrong length; no further indication."),
new NTagError(0x6982, "SECURITY_STATUS_NOT_SATISFIED", "Security status not satisfied."),
new NTagError(0x6985, "CONDITIONS_OF_USE_NOT_SATISFIED", "Conditions of use not satisfied."),
new NTagError(0x6A80, "INCORRECT_PARAMETERS_IN_DATA_FIELD", "Incorrect parameters in the command data field."),
new NTagError(0x6A82, "FILE_OR_APPLICATION_NOT_FOUND", "File or application not found."),
new NTagError(0x6A86, "INCORRECT_PARAMETERS_P1_P2", "Incorrect parameters P1-P2."),
new NTagError(0x6A87, "LC_INCONSISTENT_WITH_PARAMETERS_P1_P2", "Lc inconsistent with parameters P1-P2."),
new NTagError(0x6C00, "WRONG_LE_FIELD", "Wrong Le field."),
new NTagError(0x6D00, "INSTRUCTION_CODE_NOT_SUPPORTED_OR_INVALID", "Instruction code not supported or invalid."),
new NTagError(0x6E00, "CLASS_NOT_SUPPORTED", "Class not supported."),
new NTagError(0x9000, "NORMAL_PROCESSING", "Normal processing (no further qualification).")
};
internal void ThrowIfUnexpected(NtagResponse resp)
{
if (resp.sw1sw2 != ExpectedStatus)
{
var errorCode = ErrorCodes.FirstOrDefault(c => c.sw1sw2 == resp.sw1sw2);
errorCode ??= DefaultErrorCodes.FirstOrDefault(c => c.sw1sw2 == resp.sw1sw2);
if (errorCode is null)
throw new UnexpectedStatusException(Name, ExpectedStatus, resp.sw1sw2);
else
throw new UnexpectedStatusException(Name, ExpectedStatus, errorCode);
}
}
public byte[] ToBytes()
{
var list = new List<byte>
{
CLA,
INS
};
if (!P1.HasValue)
throw new InvalidOperationException("P1 not provided");
if (!P2.HasValue)
throw new InvalidOperationException("P2 not provided");
list.Add(P1.Value);
list.Add(P2.Value);
if (Data != null)
{
if (Lc.HasValue)
{
var realLc = Lc.Value;
if (CommMode is BoltCardTools.CommMode.Full)
{
var encDataSize = realLc - CommandHeaderSize;
realLc = (byte)CommandHeaderSize;
realLc += (byte)(16 - (encDataSize % 16)); // Padding
realLc += 8; // Add mac
}
if (CommMode is BoltCardTools.CommMode.MAC)
{
realLc += 8; // Add mac
}
if (realLc != Data.Length)
throw new InvalidOperationException("Invalid Data length");
}
list.Add((byte)(Data.Length));
list.AddRange(Data);
}
if (Le.HasValue)
{
list.Add(Le.Value);
}
return list.ToArray();
}
public override string ToString()
{
return ToBytes().ToHex();
}
internal NTagCommand Encode(Ntag424.Session currentSession)
{
if (CommMode is null)
throw new InvalidOperationException("CommMode isn't set");
if (CommMode is BoltCardTools.CommMode.Plain)
return this;
var data = Data;
if (CommMode is BoltCardTools.CommMode.Full && data is not null)
{
var nonEncrypted = data[0..CommandHeaderSize];
var encrypted = data[CommandHeaderSize..];
data = Concat(nonEncrypted, currentSession.EncryptCommand(encrypted));
}
var mac = currentSession.GetMac(INS, data);
data = Concat(data, mac);
return this with
{
Data = data
};
}
}
internal class NtagCommands
{
internal readonly static NTagCommand AuthenticateEV2FirstPart1 = new(Name: "AuthenticateEV2FirstPart1", CLA: 0x90, INS: 0x71, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: CommMode.Plain);
internal readonly static NTagCommand AuthenticateEV2FirstPart2 = new(Name: "AuthenticateEV2FirstPart2", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: 32, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Plain);
internal readonly static NTagCommand AuthenticateEV2NonFirstPart1 = new(Name: "AuthenticateEV2NonFirstPart1", CLA: 0x90, INS: 0x77, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: CommMode.Plain);
internal readonly static NTagCommand AuthenticateEV2NonFirstPart2 = new(Name: "AuthenticateEV2NonFirstPart2", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: 32, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Plain);
internal readonly static NTagCommand AuthenticateLRPFirstPart1 = new(Name: "AuthenticateLRPFirstPart1", CLA: 0x90, INS: 0x71, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: null);
internal readonly static NTagCommand AuthenticateLRPFirstPart2 = new(Name: "AuthenticateLRPFirstPart2", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: 32, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: null);
internal readonly static NTagCommand AuthenticateLRPNonFirstPart1 = new(Name: "AuthenticateLRPNonFirstPart1", CLA: 0x90, INS: 0x77, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: null);
internal readonly static NTagCommand AuthenticateLRPNonFirstPart2 = new(Name: "AuthenticateLRPNonFirstPart2", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: 32, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: null);
internal readonly static NTagCommand ChangeFileSettings = new(Name: "ChangeFileSettings", CLA: 0x90, INS: 0x5F, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full, CommandHeaderSize: 1);
internal readonly static NTagCommand ChangeKey = new(Name: "ChangeKey", CLA: 0x90, INS: 0xC4, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full, CommandHeaderSize: 1)
{
ErrorCodes =
{
new NTagError(0x91CA, "COMMAND_ABORTED", "Chained command or multiple pass command ongoing."),
new NTagError(0x911E, "INTEGRITY_ERROR", "Integrity error in cryptogram or invalid secure messaging MAC (Secure Messaging)."),
new NTagError(0x917E, "LENGTH_ERROR", "Command size not allowed."),
new NTagError(0x919E, "PARAMETER_ERROR", "Parameter value not allowed."),
new NTagError(0x9140, "NO_SUCH_KEY", "Targeted key does not exist."),
new NTagError(0x919D, "PERMISSION_DENIED", "At PICC level, targeting any OriginalityKey which cannot be changed."),
new NTagError(0x91AE, "AUTHENTICATION_ERROR", "At application level, missing active authentication with AppMasterKey while targeting any AppKey."),
new NTagError(0x91EE, "MEMORY_ERROR", "Failure when reading or writing to non-volatile memory.")
}
};
internal readonly static NTagCommand GetCardUID = new(Name: "GetCardUID", CLA: 0x90, INS: 0x51, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full);
internal readonly static NTagCommand GetFileCounters = new(Name: "GetFileCounters", CLA: 0x90, INS: 0xF6, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full);
internal readonly static NTagCommand GetFileSettings = new(Name: "GetFileSettings", CLA: 0x90, INS: 0xF5, P1: 0, P2: 0, Lc: 1, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.MAC)
{
ErrorCodes =
{
new NTagError(0x91CA, "COMMAND_ABORTED", "Chained command or multiple pass command ongoing."),
new NTagError(0x911E, "INTEGRITY_ERROR", "Invalid secure messaging MAC (only)."),
new NTagError(0x917E, "LENGTH_ERROR", "Command size not allowed."),
new NTagError(0x919E, "PARAMETER_ERROR", "Parameter value not allowed."),
new NTagError(0x919D, "PERMISSION_DENIED", "PICC level (MF) is selected."),
new NTagError(0x91F0, "FILE_NOT_FOUND", "File with targeted FileNo does not exist for the targeted application."),
new NTagError(0x91EE, "MEMORY_ERROR", "Failure when reading or writing to non-volatile memory.")
}
};
internal readonly static NTagCommand GetKeyVersion = new(Name: "GetKeyVersion", CLA: 0x90, INS: 0x64, P1: 0, P2: 0, Lc: 1, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.MAC);
internal readonly static NTagCommand GetVersionPart1 = new(Name: "GetVersionPart1", CLA: 0x90, INS: 0x60, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: CommMode.MAC);
internal readonly static NTagCommand GetVersionPart2 = new(Name: "GetVersionPart2", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x91AF, CommMode: CommMode.MAC);
internal readonly static NTagCommand GetVersionPart3 = new(Name: "GetVersionPart3", CLA: 0x90, INS: 0xAF, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.MAC);
internal readonly static NTagCommand ISOReadBinary = new(Name: "ISOReadBinary", CLA: 0x00, INS: 0xB0, P1: null, P2: null, Lc: null, Data: null, Le: null, ExpectedStatus: 0x9000, CommMode: CommMode.Plain);
internal readonly static NTagCommand ReadData = new(Name: "ReadData", CLA: 0x90, INS: 0xAD, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: null, CommandHeaderSize: 7);
internal readonly static NTagCommand Read_Sig = new(Name: "Read_Sig", CLA: 0x90, INS: 0x3C, P1: 0, P2: 0, Lc: 1, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full);
internal readonly static NTagCommand ISOSelectFile = new(Name: "ISOSelectFile", CLA: 0x00, INS: 0xA4, P1: null, P2: null, Lc: null, Data: null, Le: null, ExpectedStatus: 0x9000, CommMode: CommMode.Plain);
internal readonly static NTagCommand SetConfiguration = new(Name: "SetConfiguration", CLA: 0x90, INS: 0x5C, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: CommMode.Full, CommandHeaderSize: 1);
internal readonly static NTagCommand ISOUpdateBinary = new(Name: "ISOUpdateBinary", CLA: 0x00, INS: 0xD6, P1: null, P2: null, Lc: null, Data: null, Le: null, ExpectedStatus: 0x9000, CommMode: CommMode.Plain);
internal readonly static NTagCommand WriteData = new(Name: "WriteData", CLA: 0x90, INS: 0x8D, P1: 0, P2: 0, Lc: null, Data: null, Le: 0, ExpectedStatus: 0x9100, CommMode: null, CommandHeaderSize: 7);
}

View File

@ -1,19 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BoltCardTools;
namespace BoltCardTools
public record NtagResponse(byte[] Data, ushort sw1sw2)
{
public record NtagResponse(byte[] Data, ushort sw1sw2)
{
internal NtagResponse Decode(Ntag424.Session currentSession, CommMode commMode)
{
return this with
{
Data = currentSession.DecryptResponse((byte)sw1sw2, Data, commMode)
};
}
}
internal NtagResponse Decode(Ntag424.Session currentSession, CommMode commMode)
{
return this with
{
Data = currentSession.DecryptResponse((byte)sw1sw2, Data, commMode)
};
}
}

View File

@ -1,53 +1,46 @@
using PCSC;
using PCSC.Extensions;
using PCSC.Iso7816;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using PCSC;
using PCSC.Extensions;
namespace BoltCardTools
namespace BoltCardTools;
public class PCSCAPDUTransport : IAPDUTransport
{
public class PCSCAPDUTransport : IAPDUTransport
{
public readonly ISCardReader CardReader;
public PCSCAPDUTransport(ISCardReader cardReader)
{
ArgumentNullException.ThrowIfNull(cardReader);
CardReader = cardReader;
}
public readonly ISCardReader CardReader;
public PCSCAPDUTransport(ISCardReader cardReader)
{
ArgumentNullException.ThrowIfNull(cardReader);
CardReader = cardReader;
}
public Task<NtagResponse> SendAPDU(NTagCommand apdu)
{
//return Task.Factory.StartNew(() =>
//{
// var bytes = apdu.ToBytes();
// Console.WriteLine(bytes.ToHex());
// Console.WriteLine("---");
// var resp = new byte[256];
// int received = resp.Length;
// var sc = CardReader.Transmit(bytes, resp, ref received);
// if (sc != SCardError.Success)
// sc.Throw();
// var sw1sw2 = (ushort)(resp[received - 2] << 8 | resp[received - 1]);
// var data = resp[..(received - 2)];
// return new NtagResponse(data, sw1sw2);
//}, TaskCreationOptions.LongRunning);
public Task<NtagResponse> SendAPDU(NTagCommand apdu)
{
//return Task.Factory.StartNew(() =>
//{
// var bytes = apdu.ToBytes();
// Console.WriteLine(bytes.ToHex());
// Console.WriteLine("---");
// var resp = new byte[256];
// int received = resp.Length;
// var sc = CardReader.Transmit(bytes, resp, ref received);
// if (sc != SCardError.Success)
// sc.Throw();
// var sw1sw2 = (ushort)(resp[received - 2] << 8 | resp[received - 1]);
// var data = resp[..(received - 2)];
// return new NtagResponse(data, sw1sw2);
//}, TaskCreationOptions.LongRunning);
var bytes = apdu.ToBytes();
Console.WriteLine("Command APDU : " + bytes.ToHex());
var resp = new byte[512];
int received = resp.Length;
var sc = CardReader.Transmit(bytes, resp, ref received);
if (sc != SCardError.Success)
sc.Throw();
Console.WriteLine("Response APDU : " + resp[..received].ToHex());
var sw1sw2 = (ushort)(resp[received - 2] << 8 | resp[received - 1]);
var data = resp[..(received - 2)];
return Task.FromResult(new NtagResponse(data, sw1sw2));
}
}
var bytes = apdu.ToBytes();
Console.WriteLine("Command APDU : " + bytes.ToHex());
var resp = new byte[512];
int received = resp.Length;
var sc = CardReader.Transmit(bytes, resp, ref received);
if (sc != SCardError.Success)
sc.Throw();
Console.WriteLine("Response APDU : " + resp[..received].ToHex());
var sw1sw2 = (ushort)(resp[received - 2] << 8 | resp[received - 1]);
var data = resp[..(received - 2)];
return Task.FromResult(new NtagResponse(data, sw1sw2));
}
}

View File

@ -1,28 +1,28 @@

using System;
namespace BoltCardTools;
public record PICCData(byte[]? Uid, int? Counter)
{
public static PICCData Create(ReadOnlySpan<byte> data)
{
bool hasUid = (data[0] & 0b1000_0000) != 0;
bool hasCounter = (data[0] & 0b0100_0000) != 0;
if (hasUid && ((data[0] & 0b0000_0111) != 0b0000_0111))
throw new InvalidOperationException("Invalid PICCData");
int i = 1;
byte[]? uid = null;
int? counter = null;
if (hasUid)
{
uid = data[i..(i + 7)].ToArray();
i += 7;
}
if (hasCounter)
{
counter = data[i] | data[i + 1] << 8 | data[i + 2] << 16;
}
return new PICCData(uid, counter);
}
public static PICCData Create(ReadOnlySpan<byte> data)
{
bool hasUid = (data[0] & 0b1000_0000) != 0;
bool hasCounter = (data[0] & 0b0100_0000) != 0;
if (hasUid && ((data[0] & 0b0000_0111) != 0b0000_0111))
throw new InvalidOperationException("Invalid PICCData");
int i = 1;
byte[]? uid = null;
int? counter = null;
if (hasUid)
{
uid = data[i..(i + 7)].ToArray();
i += 7;
}
if (hasCounter)
{
counter = data[i] | data[i + 1] << 8 | data[i + 2] << 16;
}
return new PICCData(uid, counter);
}
}

View File

@ -1,54 +1,49 @@
using BoltCardTools;
using System.Linq;
using System.Threading.Tasks;
using BoltCardTools;
using NdefLibrary.Ndef;
using PCSC;
using PCSC.Extensions;
using PCSC.Iso7816;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
// https://github.com/boltcard/boltcard-wallet/blob/master/class/Ntag424.js
// https://github.com/boltcard/boltcard-wallet/blob/master/screen/boltcard/create.js#L201
// https://github.com/danm-de/pcsc-sharp
public class Program
{
public static async Task Main(string[] args)
{
var message = NdefMessage.FromByteArray("D1012C5503746573742E636F6D3F703D303030303030303030303030303026633D303030303030303030303030303030".HexToBytes());
public static async Task Main(string[] args)
{
var message = NdefMessage.FromByteArray("D1012C5503746573742E636F6D3F703D303030303030303030303030303026633D303030303030303030303030303030".HexToBytes());
// http://test.com?p=00000000000000&c=000000000000000, description=, uriType=sms, mirrorFlags=null, mUri=http://test.com?p=00000000000000&c=000000000000000
// http://test.com?p=00000000000000&c=000000000000000, description=, uriType=sms, mirrorFlags=null, mUri=http://test.com?p=00000000000000&c=000000000000000
var contextFactory = ContextFactory.Instance;
using (var ctx = contextFactory.Establish(SCardScope.System))
{
var readerName = ctx.GetReaders().FirstOrDefault();
if (readerName != null)
{
using var reader = new SCardReader(ctx);
reader.Connect(readerName, SCardShareMode.Exclusive, SCardProtocol.Any).ThrowIfNotSuccess();
var transport = new PCSCAPDUTransport(reader);
var ntag = new Ntag424(transport);
var key = new AESKey(new byte[16]);
await ntag.AuthenticateEV2First(0, key);
await ntag.AuthenticateEV2NonFirst(0, key);
var contextFactory = ContextFactory.Instance;
using var ctx = contextFactory.Establish(SCardScope.System);
var readerName = ctx.GetReaders().FirstOrDefault();
if (readerName != null)
{
using var reader = new SCardReader(ctx);
reader.Connect(readerName, SCardShareMode.Exclusive, SCardProtocol.Any).ThrowIfNotSuccess();
var transport = new PCSCAPDUTransport(reader);
var ntag = new Ntag424(transport);
var key = new AESKey(new byte[16]);
await ntag.AuthenticateEV2First(0, key);
await ntag.AuthenticateEV2NonFirst(0, key);
//Console.WriteLine("UID: " + (await ntag.GetCardUID()).ToHex());
//await ntag.SetupBoltcard("http://test.com");
//await ntag.ChangeFileSettings();
//Console.WriteLine("UID: " + (await ntag.GetCardUID()).ToHex());
//await ntag.SetupBoltcard("http://test.com");
//await ntag.ChangeFileSettings();
//await ntag.ReadFile(DataFile.NDEF, 0, 10);
//await ntag.WriteNDef(message);
//await ntag.ReadFile(DataFile.NDEF, 0, 10);
//await ntag.WriteNDef(message);
//await ntag.IsoSelectFile(DataFile.CC);
//var d = await ntag.ReadFile(DataFile.NDEF, 0, 0);
//Console.WriteLine(d.ToHex());
//await ntag.GetCardUID
//var access = await ntag.GetFileSettings();
}
}
}
//await ntag.ReadFile(DataFile.NDEF, 0, 10);
//await ntag.WriteNDef(message);
//await ntag.ReadFile(DataFile.NDEF, 0, 10);
//await ntag.WriteNDef(message);
//await ntag.IsoSelectFile(DataFile.CC);
//var d = await ntag.ReadFile(DataFile.NDEF, 0, 0);
//Console.WriteLine(d.ToHex());
//await ntag.GetCardUID
//var access = await ntag.GetFileSettings();
}
}
}
}

View File

@ -1,8 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
[assembly:InternalsVisibleTo("BoltCardTools.Tests")]
[assembly: InternalsVisibleTo("BoltCardTools.Tests")]

View File

@ -1,29 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System;
namespace BoltCardTools
namespace BoltCardTools;
public class UnexpectedResponseException : Exception
{
public class UnexpectedResponseException : Exception
{
public UnexpectedResponseException(string? message) : base(message)
{
public UnexpectedResponseException(string? message) : base(message)
{
}
}
public class UnexpectedStatusException : UnexpectedResponseException
{
public UnexpectedStatusException(string commandName, int expectedStatus, NTagError error)
: base($"Error for {commandName}: Expected {expectedStatus:X4}, Actual: {error}")
{
Details = error;
}
public NTagError? Details { get; }
public UnexpectedStatusException(string commandName, int expectedStatus, int actualStatus)
: base($"Unexpected status for {commandName}: Expected: {expectedStatus:X4}, Actual: {actualStatus:X4}")
{
}
}
}
}
public class UnexpectedStatusException : UnexpectedResponseException
{
public UnexpectedStatusException(string commandName, int expectedStatus, NTagError error)
: base($"Error for {commandName}: Expected {expectedStatus:X4}, Actual: {error}")
{
Details = error;
}
public NTagError? Details { get; }
public UnexpectedStatusException(string commandName, int expectedStatus, int actualStatus)
: base($"Unexpected status for {commandName}: Expected: {expectedStatus:X4}, Actual: {actualStatus:X4}")
{
}
}

View File

@ -23,7 +23,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\src\BoltCardTools.csproj" />
<ProjectReference Include="..\src\BTCPayServer.NTag424\BTCPayServer.NTag424.csproj" />
</ItemGroup>

View File

@ -1,39 +1,32 @@
using PCSC;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.PortableExecutable;
using System.Text;
using System.Threading.Tasks;
using PCSC;
namespace BoltCardTools.Tests
namespace BoltCardTools.Tests;
public record CardReaderContext(ISCardReader CardReader, IContextFactory ContextFactory, ISCardContext Context) : IDisposable
{
public record CardReaderContext(ISCardReader CardReader, IContextFactory ContextFactory, ISCardContext Context) : IDisposable
{
public static CardReaderContext Create()
{
var contextFactory = PCSC.ContextFactory.Instance;
var context = contextFactory.Establish(SCardScope.System);
var readerNames = context.GetReaders();
var readerName = readerNames.FirstOrDefault();
if (readerName is null)
{
throw new InvalidOperationException("No readers found");
}
var reader = new SCardReader(context);
reader.Connect(readerName, SCardShareMode.Shared, SCardProtocol.Any);
return new CardReaderContext(reader, contextFactory, context);
}
public void Dispose()
{
CardReader.Dispose();
Context.Dispose();
}
public static CardReaderContext Create()
{
var contextFactory = PCSC.ContextFactory.Instance;
var context = contextFactory.Establish(SCardScope.System);
var readerNames = context.GetReaders();
var readerName = readerNames.FirstOrDefault();
if (readerName is null)
{
throw new InvalidOperationException("No readers found");
}
var reader = new SCardReader(context);
reader.Connect(readerName, SCardShareMode.Shared, SCardProtocol.Any);
return new CardReaderContext(reader, contextFactory, context);
}
public void Dispose()
{
CardReader.Dispose();
Context.Dispose();
}
public Ntag424 CreateNTag424()
{
var transport = new PCSCAPDUTransport(this.CardReader);
return new Ntag424(transport);
}
}
public Ntag424 CreateNTag424()
{
var transport = new PCSCAPDUTransport(this.CardReader);
return new Ntag424(transport);
}
}

View File

@ -1 +1 @@
global using Xunit;
global using Xunit;

View File

@ -1,196 +1,190 @@
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using NdefLibrary.Ndef;
using PCSC;
using PCSC.Extensions;
using PCSC.Iso7816;
using System.Reflection.Emit;
using System.Text;
using System.Text.RegularExpressions;
using NdefLibrary.Ndef;
namespace BoltCardTools.Tests;
public class UnitTest1
{
[Fact]
public void CanCreateAPDUFromNtagCommand()
{
var actual = (NtagCommands.ISOSelectFile with
{
P1 = 0x04,
P2 = 0x00,
Data = "d2760000850101".HexToBytes()
}).ToBytes().ToHex();
var expected = "00A4040007D2760000850101".ToLowerInvariant();
Assert.Equal(expected, actual);
[Fact]
public void CanCreateAPDUFromNtagCommand()
{
var actual = (NtagCommands.ISOSelectFile with
{
P1 = 0x04,
P2 = 0x00,
Data = "d2760000850101".HexToBytes()
}).ToBytes().ToHex();
var expected = "00A4040007D2760000850101".ToLowerInvariant();
Assert.Equal(expected, actual);
actual = (NtagCommands.ISOSelectFile with
{
P1 = 0x04,
P2 = 0x00,
Data = "d2760000850101".HexToBytes(),
Le = 0
}).ToBytes().ToHex();
expected = "00A4040007D276000085010100".ToLowerInvariant();
Assert.Equal(expected, actual);
}
actual = (NtagCommands.ISOSelectFile with
{
P1 = 0x04,
P2 = 0x00,
Data = "d2760000850101".HexToBytes(),
Le = 0
}).ToBytes().ToHex();
expected = "00A4040007D276000085010100".ToLowerInvariant();
Assert.Equal(expected, actual);
}
//from https://github.com/boltcard/boltcard/blob/7745c9f20d5ad0129cb4b3fc534441038e79f5e6/docs/TEST_VECTORS.md
[Theory]
[InlineData("E19CCB1FED8892CE", "04996c6a926980", 3)]
[InlineData("66B4826EA4C155B4", "04996c6a926980", 5)]
[InlineData("CC61660C020B4D96", "04996c6a926980", 7)]
public void CanCalculateSunMac(string expected, string uid, int ctr)
{
var key = new AESKey(Convert.FromHexString("b45775776cb224c75bcde7ca3704e933"));
var actual = key.GetSunMac(uid.HexToBytes(), ctr);
Assert.Equal(expected.ToLowerInvariant(), actual.ToHex());
}
//from https://github.com/boltcard/boltcard/blob/7745c9f20d5ad0129cb4b3fc534441038e79f5e6/docs/TEST_VECTORS.md
[Theory]
[InlineData("E19CCB1FED8892CE", "04996c6a926980", 3)]
[InlineData("66B4826EA4C155B4", "04996c6a926980", 5)]
[InlineData("CC61660C020B4D96", "04996c6a926980", 7)]
public void CanCalculateSunMac(string expected, string uid, int ctr)
{
var key = new AESKey(Convert.FromHexString("b45775776cb224c75bcde7ca3704e933"));
var actual = key.GetSunMac(uid.HexToBytes(), ctr);
Assert.Equal(expected.ToLowerInvariant(), actual.ToHex());
}
//from https://github.com/boltcard/boltcard/blob/7745c9f20d5ad0129cb4b3fc534441038e79f5e6/docs/TEST_VECTORS.md
[Theory]
[InlineData("4E2E289D945A66BB13377A728884E867", "04996c6a926980", 3)]
[InlineData("00F48C4F8E386DED06BCDC78FA92E2FE", "04996c6a926980", 5)]
[InlineData("0DBF3C59B59B0638D60B5842A997D4D1", "04996c6a926980", 7)]
public void CanDecryptSunPICCData(string encrypted, string uid, int ctr)
{
var key = new AESKey(Convert.FromHexString("0c3b25d92b38ae443229dd59ad34b85d"));
var picc = key.DecryptSun(encrypted.HexToBytes());
Assert.Equal(ctr, picc.Counter);
Assert.Equal(uid.ToLowerInvariant(), picc.Uid?.ToHex());
}
//from https://github.com/boltcard/boltcard/blob/7745c9f20d5ad0129cb4b3fc534441038e79f5e6/docs/TEST_VECTORS.md
[Theory]
[InlineData("4E2E289D945A66BB13377A728884E867", "04996c6a926980", 3)]
[InlineData("00F48C4F8E386DED06BCDC78FA92E2FE", "04996c6a926980", 5)]
[InlineData("0DBF3C59B59B0638D60B5842A997D4D1", "04996c6a926980", 7)]
public void CanDecryptSunPICCData(string encrypted, string uid, int ctr)
{
var key = new AESKey(Convert.FromHexString("0c3b25d92b38ae443229dd59ad34b85d"));
var picc = key.DecryptSun(encrypted.HexToBytes());
Assert.Equal(ctr, picc.Counter);
Assert.Equal(uid.ToLowerInvariant(), picc.Uid?.ToHex());
}
[Theory]
[InlineData("01020304050607080910111213141516", "0102030405060708091011121314151680000000000000000000000000000000")]
[InlineData("010203040506070809101112131415", "01020304050607080910111213141580")]
[InlineData("01", "01800000000000000000000000000000")]
public void CanDoPadding(string data, string padded)
{
var actual = Ntag424.Session.PaddingForEnc(data.HexToBytes()).ToHex();
Assert.Equal(padded, actual);
}
[Theory]
[InlineData("01020304050607080910111213141516", "0102030405060708091011121314151680000000000000000000000000000000")]
[InlineData("010203040506070809101112131415", "01020304050607080910111213141580")]
[InlineData("01", "01800000000000000000000000000000")]
public void CanDoPadding(string data, string padded)
{
var actual = Ntag424.Session.PaddingForEnc(data.HexToBytes()).ToHex();
Assert.Equal(padded, actual);
}
[Fact]
public void CanCreateCommModeMAC()
{
var session = new Ntag424.Session(0, new AESKey(new byte[16]), new AESKey("8248134A386E86EB7FAF54A52E536CB6".HexToBytes()), "7A21085E".HexToBytes());
var command = NtagCommands.GetFileSettings with
{
CommMode = CommMode.MAC,
Data = new byte[] { 0x02 }
};
command = command.Encode(session);
var apdu = command.ToBytes().ToHex();
Assert.Equal("90F5000009026597A457C8CD442C00".ToLower(), apdu);
var resp = new NtagResponse("0040EEEE000100D1FE001F00004400004400002000006A00002A474282E7A47986".HexToBytes(), 0x9100);
command.ThrowIfUnexpected(resp);
session.Counter++;
var respData = resp.Decode(session, CommMode.MAC).Data.ToHex();
Assert.Equal("0040EEEE000100D1FE001F00004400004400002000006A0000".ToLower(), respData);
}
[Fact]
public void CanCreateCommModeFull()
{
var session = new Ntag424.Session(0, new AESKey("7305E2CCA5B0377617CDBFEB96C9B358".HexToBytes()), new AESKey("8B485037C8C2FB400D79BF0AB956F28F".HexToBytes()), "856C1841".HexToBytes());
var command = NtagCommands.WriteData with
{
CommMode = CommMode.Full,
Data = "02000000800000005ED1015B5500687474703A2F2F7777772E6D69666172652E6E65742F70726F64756374732F6E746167733F265549443D3034323136353441434634433830264374723D30303030303126436D61633D323145323336303832363645334345410000000000000000000000000000000000000000000000000000000000000000".HexToBytes()
};
command = command.Encode(session);
var apdu = command.ToBytes().ToHex();
// Why there are 4 more bytes in the doc??
// Original: 908D00009F02000000800000B4716C58E71A09F6D869AB7810C2E94BD02F13DF2159433D581F50185B11535F3E7A068582B04B5E4BDE374A788DF7AD8C4C5473F7B30D9496BD8F3F8ED51D506D3194FDEA51A877C2EB28A0A8FD2B34E196800A7D2F0AD1CBED98E311E2F7667DA10DF3CF4CE6A5658B89695EDAD9F500000000D9AD1E4C41748D34BC6B15A2B45B050F34765F3E9D2CF701E0C7F781477F7B91B97CBB2A236F876C00
Assert.Equal("908D00009F02000000800000B4716C58E71A09F6D869AB7810C2E94BD02F13DF2159433D581F50185B11535F3E7A068582B04B5E4BDE374A788DF7AD8C4C5473F7B30D9496BD8F3F8ED51D506D3194FDEA51A877C2EB28A0A8FD2B34E196800A7D2F0AD1CBED98E311E2F7667DA10DF3CF4CE6A5658B89695EDAD9F5D9AD1E4C41748D34BC6B15A2B45B050F34765F3E9D2CF701E0C7F781477F7B91B97CBB2A236F876C00".ToLower(), apdu);
[Fact]
public void CanCreateCommModeMAC()
{
var session = new Ntag424.Session(0, new AESKey(new byte[16]), new AESKey("8248134A386E86EB7FAF54A52E536CB6".HexToBytes()), "7A21085E".HexToBytes());
var command = NtagCommands.GetFileSettings with
{
CommMode = CommMode.MAC,
Data = new byte[] { 0x02 }
};
command = command.Encode(session);
var apdu = command.ToBytes().ToHex();
Assert.Equal("90F5000009026597A457C8CD442C00".ToLower(), apdu);
var resp = new NtagResponse("0040EEEE000100D1FE001F00004400004400002000006A00002A474282E7A47986".HexToBytes(), 0x9100);
command.ThrowIfUnexpected(resp);
session.Counter++;
var respData = resp.Decode(session, CommMode.MAC).Data.ToHex();
Assert.Equal("0040EEEE000100D1FE001F00004400004400002000006A0000".ToLower(), respData);
}
[Fact]
public void CanCreateCommModeFull()
{
var session = new Ntag424.Session(0, new AESKey("7305E2CCA5B0377617CDBFEB96C9B358".HexToBytes()), new AESKey("8B485037C8C2FB400D79BF0AB956F28F".HexToBytes()), "856C1841".HexToBytes());
var command = NtagCommands.WriteData with
{
CommMode = CommMode.Full,
Data = "02000000800000005ED1015B5500687474703A2F2F7777772E6D69666172652E6E65742F70726F64756374732F6E746167733F265549443D3034323136353441434634433830264374723D30303030303126436D61633D323145323336303832363645334345410000000000000000000000000000000000000000000000000000000000000000".HexToBytes()
};
command = command.Encode(session);
var apdu = command.ToBytes().ToHex();
// Why there are 4 more bytes in the doc??
// Original: 908D00009F02000000800000B4716C58E71A09F6D869AB7810C2E94BD02F13DF2159433D581F50185B11535F3E7A068582B04B5E4BDE374A788DF7AD8C4C5473F7B30D9496BD8F3F8ED51D506D3194FDEA51A877C2EB28A0A8FD2B34E196800A7D2F0AD1CBED98E311E2F7667DA10DF3CF4CE6A5658B89695EDAD9F500000000D9AD1E4C41748D34BC6B15A2B45B050F34765F3E9D2CF701E0C7F781477F7B91B97CBB2A236F876C00
Assert.Equal("908D00009F02000000800000B4716C58E71A09F6D869AB7810C2E94BD02F13DF2159433D581F50185B11535F3E7A068582B04B5E4BDE374A788DF7AD8C4C5473F7B30D9496BD8F3F8ED51D506D3194FDEA51A877C2EB28A0A8FD2B34E196800A7D2F0AD1CBED98E311E2F7667DA10DF3CF4CE6A5658B89695EDAD9F5D9AD1E4C41748D34BC6B15A2B45B050F34765F3E9D2CF701E0C7F781477F7B91B97CBB2A236F876C00".ToLower(), apdu);
var resp = new NtagResponse("DDDB9EC959B3EFEB".HexToBytes(), 0x9100);
command.ThrowIfUnexpected(resp);
var resp = new NtagResponse("DDDB9EC959B3EFEB".HexToBytes(), 0x9100);
command.ThrowIfUnexpected(resp);
session.Counter++;
var respData = resp.Decode(session, CommMode.MAC).Data.ToHex();
Assert.Empty(respData);
}
session.Counter++;
var respData = resp.Decode(session, CommMode.MAC).Data.ToHex();
Assert.Empty(respData);
}
[Fact]
public void CanCreateFileSettings()
{
var actual = new FileSettings(DataFile.NDEF)
{
AccessRights = new()
{
ReadWrite = AccessCondition.Key0,
Change = AccessCondition.Key0,
Write = AccessCondition.Key0,
Read = AccessCondition.Free
},
SDMMirroring = true,
SDMUID = true,
SDMReadCtr = true,
SDMAccessRights = new()
{
MetaRead = AccessCondition.Key1,
FileRead = AccessCondition.Key2,
CtrRet = AccessCondition.Never
},
PICCDataOffset = 3,
SDMMACOffset = 2,
SDMMACInputOffset = 1
}.ToBytes().ToHex();
[Fact]
public void CanCreateFileSettings()
{
var actual = new FileSettings(DataFile.NDEF)
{
AccessRights = new()
{
ReadWrite = AccessCondition.Key0,
Change = AccessCondition.Key0,
Write = AccessCondition.Key0,
Read = AccessCondition.Free
},
SDMMirroring = true,
SDMUID = true,
SDMReadCtr = true,
SDMAccessRights = new()
{
MetaRead = AccessCondition.Key1,
FileRead = AccessCondition.Key2,
CtrRet = AccessCondition.Never
},
PICCDataOffset = 3,
SDMMACOffset = 2,
SDMMACInputOffset = 1
}.ToBytes().ToHex();
Assert.Equal("4000E0C1FF12030000010000020000".ToLower(), actual);
}
Assert.Equal("4000E0C1FF12030000010000020000".ToLower(), actual);
}
[Fact]
public async Task CanAuthenticate()
{
using var ctx = CardReaderContext.Create();
var ntag = ctx.CreateNTag424();
var key = new AESKey(new byte[16]);
await ntag.AuthenticateEV2First(0, key);
var uid1 = await ntag.GetCardUID();
await ntag.AuthenticateEV2NonFirst(0, key);
var uid2 = await ntag.GetCardUID();
Assert.Equal(uid1.ToHex(), uid2.ToHex());
}
[Fact]
public async Task CanAuthenticate()
{
using var ctx = CardReaderContext.Create();
var ntag = ctx.CreateNTag424();
var key = new AESKey(new byte[16]);
await ntag.AuthenticateEV2First(0, key);
var uid1 = await ntag.GetCardUID();
await ntag.AuthenticateEV2NonFirst(0, key);
var uid2 = await ntag.GetCardUID();
Assert.Equal(uid1.ToHex(), uid2.ToHex());
}
[Fact]
public async Task CanChangeKey()
{
using var ctx = CardReaderContext.Create();
var ntag = ctx.CreateNTag424();
var key1 = new AESKey(new byte[16]);
var key2b = new byte[16];
key2b[^1] = 1;
var key2 = new AESKey(key2b);
await ntag.AuthenticateEV2First(0, key1);
await ntag.ChangeKey(0, key1);
[Fact]
public async Task CanChangeKey()
{
using var ctx = CardReaderContext.Create();
var ntag = ctx.CreateNTag424();
var key1 = new AESKey(new byte[16]);
var key2b = new byte[16];
key2b[^1] = 1;
var key2 = new AESKey(key2b);
await ntag.AuthenticateEV2First(0, key1);
await ntag.ChangeKey(0, key1);
await ntag.AuthenticateEV2First(0, key1);
await ntag.ChangeKey(1, key1);
await ntag.ChangeKey(1, key2, key1);
await ntag.ChangeKey(1, key1, key2);
}
await ntag.AuthenticateEV2First(0, key1);
await ntag.ChangeKey(1, key1);
await ntag.ChangeKey(1, key2, key1);
await ntag.ChangeKey(1, key1, key2);
}
[Fact]
public async Task CanDoBoltcard()
{
using var ctx = CardReaderContext.Create();
var ntag = ctx.CreateNTag424();
var key = new AESKey(new byte[16]);
await ntag.AuthenticateEV2First(0, key);
await ntag.SetupBoltcard("http://test.com");
var message = await ntag.ReadNDef();
var uri = new NdefUriRecord(message[0]).Uri;
var p = Regex.Match(uri, "p=([^&]*)&").Groups[1].Value.ToLowerInvariant();
var c = Regex.Match(uri, "c=(.*)").Groups[1].Value.ToLowerInvariant();
var piccData = key.DecryptSun(p.HexToBytes());
Assert.Equal(c, key.GetSunMac(piccData).ToHex());
}
[Fact]
public async Task CanDoBoltcard()
{
using var ctx = CardReaderContext.Create();
var ntag = ctx.CreateNTag424();
var key = new AESKey(new byte[16]);
await ntag.AuthenticateEV2First(0, key);
await ntag.SetupBoltcard("http://test.com");
var message = await ntag.ReadNDef();
var uri = new NdefUriRecord(message[0]).Uri;
var p = Regex.Match(uri, "p=([^&]*)&").Groups[1].Value.ToLowerInvariant();
var c = Regex.Match(uri, "c=(.*)").Groups[1].Value.ToLowerInvariant();
var piccData = key.DecryptSun(p.HexToBytes());
Assert.Equal(c, key.GetSunMac(piccData).ToHex());
}
[Fact]
public void CanCalculateCRC()
{
var bytes = new byte[] { 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100 };
var result = Helpers.CRCJam(bytes);
Assert.Equal(unchecked((uint)(-0xd4a1186)), result);
}
}
[Fact]
public void CanCalculateCRC()
{
var bytes = new byte[] { 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100 };
var result = Helpers.CRCJam(bytes);
Assert.Equal(unchecked((uint)(-0xd4a1186)), result);
}
}