Skip to content

Commit 5912b3e

Browse files
committed
feat(coldcard): add nfc import and signing support
1 parent 14682bc commit 5912b3e

28 files changed

Lines changed: 1226 additions & 294 deletions

android/app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
android:name="android.permission.READ_EXTERNAL_STORAGE"
66
android:maxSdkVersion="32"
77
/>
8-
<!-- <uses-permission android:name="android.permission.NFC"/> -->
8+
<uses-permission android:name="android.permission.NFC" />
9+
<uses-feature
10+
android:name="android.hardware.nfc"
11+
android:required="false"
12+
/>
913
<uses-feature
1014
android:name="android.hardware.bluetooth"
1115
android:required="false"

ios/Runner.xcodeproj/project.pbxproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,7 @@
488488
buildSettings = {
489489
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
490490
CLANG_ENABLE_MODULES = YES;
491+
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
491492
CURRENT_PROJECT_VERSION = 178;
492493
DEVELOPMENT_TEAM = BX99T32YGS;
493494
ENABLE_BITCODE = NO;
@@ -679,6 +680,7 @@
679680
buildSettings = {
680681
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
681682
CLANG_ENABLE_MODULES = YES;
683+
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
682684
CURRENT_PROJECT_VERSION = 178;
683685
DEVELOPMENT_TEAM = BX99T32YGS;
684686
ENABLE_BITCODE = NO;
@@ -708,6 +710,7 @@
708710
buildSettings = {
709711
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
710712
CLANG_ENABLE_MODULES = YES;
713+
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
711714
CURRENT_PROJECT_VERSION = 178;
712715
DEVELOPMENT_TEAM = BX99T32YGS;
713716
ENABLE_BITCODE = NO;

ios/Runner/DebugProfile.entitlements

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5+
<key>com.apple.developer.nfc.readersession.formats</key>
6+
<array>
7+
<string>TAG</string>
8+
</array>
59
<key>keychain-access-groups</key>
610
<array/>
711
</dict>

ios/Runner/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
<string>$(FLUTTER_BUILD_NAME)</string>
2525
<key>CFBundleSignature</key>
2626
<string>????</string>
27+
<key>NFCReaderUsageDescription</key>
28+
<string>Connect with your hardware wallet over NFC</string>
2729
<key>CFBundleURLTypes</key>
2830
<array>
2931
<dict>

ios/Runner/Release.entitlements

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5+
<key>com.apple.developer.nfc.readersession.formats</key>
6+
<array>
7+
<string>TAG</string>
8+
</array>
59
<key>keychain-access-groups</key>
610
<array/>
711
</dict>

lib/core/entities/signer_device_entity.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ enum QrType { none, bbqr, urqr }
55
enum SignerDeviceEntity {
66
bitbox02,
77
coldcardQ,
8+
coldcardMk4,
89
jade,
910
keystone,
1011
krux,

lib/core/storage/tables/wallet_metadata_table.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ enum Signer {
2727
enum SignerDevice {
2828
bitbox02,
2929
coldcardQ,
30+
coldcardMk4,
3031
jade,
3132
keystone,
3233
krux,
@@ -41,6 +42,7 @@ enum SignerDevice {
4142
static SignerDevice fromEntity(SignerDeviceEntity entity) => switch (entity) {
4243
SignerDeviceEntity.bitbox02 => SignerDevice.bitbox02,
4344
SignerDeviceEntity.coldcardQ => SignerDevice.coldcardQ,
45+
SignerDeviceEntity.coldcardMk4 => SignerDevice.coldcardMk4,
4446
SignerDeviceEntity.jade => SignerDevice.jade,
4547
SignerDeviceEntity.keystone => SignerDevice.keystone,
4648
SignerDeviceEntity.krux => SignerDevice.krux,
@@ -56,6 +58,7 @@ enum SignerDevice {
5658
SignerDeviceEntity toEntity() => switch (this) {
5759
SignerDevice.bitbox02 => SignerDeviceEntity.bitbox02,
5860
SignerDevice.coldcardQ => SignerDeviceEntity.coldcardQ,
61+
SignerDevice.coldcardMk4 => SignerDeviceEntity.coldcardMk4,
5962
SignerDevice.jade => SignerDeviceEntity.jade,
6063
SignerDevice.keystone => SignerDeviceEntity.keystone,
6164
SignerDevice.krux => SignerDeviceEntity.krux,
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import 'dart:convert' show base64, utf8;
2+
3+
import 'package:convert/convert.dart';
4+
import 'package:crypto/crypto.dart';
5+
import 'package:ndef/ndef.dart' as ndef;
6+
7+
const _bitcoinNfcTypePrefix = 'urn:nfc:ext:';
8+
const _bitcoinPsbtType = 'bitcoin.org:psbt';
9+
const _bitcoinTxnType = 'bitcoin.org:txn';
10+
const _bitcoinSha256Type = 'bitcoin.org:sha256';
11+
const _jsonMimeType = 'application/json';
12+
13+
String? payloadFromNdefRecords(List<ndef.NDEFRecord> records) {
14+
if (records.isEmpty) return null;
15+
16+
final bitcoinPayload = _payloadFromBitcoinExternalRecords(records);
17+
if (bitcoinPayload != null || _hasBitcoinPayloadRecord(records)) {
18+
return bitcoinPayload;
19+
}
20+
21+
return _payloadFromRecord(records.last);
22+
}
23+
24+
String? _payloadFromBitcoinExternalRecords(List<ndef.NDEFRecord> records) {
25+
final txnPayload = _externalPayloadForType(records, _bitcoinTxnType);
26+
if (txnPayload != null) {
27+
if (!_matchesSha256Checksum(records, txnPayload)) return null;
28+
return hex.encode(txnPayload);
29+
}
30+
31+
final psbtPayload = _externalPayloadForType(records, _bitcoinPsbtType);
32+
if (psbtPayload != null) {
33+
if (!_matchesSha256Checksum(records, psbtPayload)) return null;
34+
return base64.encode(psbtPayload);
35+
}
36+
37+
return null;
38+
}
39+
40+
bool _hasBitcoinPayloadRecord(List<ndef.NDEFRecord> records) {
41+
return _hasExternalRecordForType(records, _bitcoinTxnType) ||
42+
_hasExternalRecordForType(records, _bitcoinPsbtType);
43+
}
44+
45+
String? _payloadFromRecord(ndef.NDEFRecord record) {
46+
if (record is ndef.TextRecord) {
47+
final text = record.text;
48+
return text == null || text.isEmpty ? null : text;
49+
}
50+
51+
if (record is ndef.ExternalRecord) {
52+
final payload = record.payload;
53+
if (payload == null || payload.isEmpty) return null;
54+
return hex.encode(payload);
55+
}
56+
57+
if (record is ndef.MimeRecord && record.decodedType == _jsonMimeType) {
58+
final payload = record.payload;
59+
if (payload == null || payload.isEmpty) return null;
60+
61+
try {
62+
return utf8.decode(payload);
63+
} on FormatException {
64+
return null;
65+
}
66+
}
67+
68+
return null;
69+
}
70+
71+
List<int>? _externalPayloadForType(List<ndef.NDEFRecord> records, String type) {
72+
for (final record in records.whereType<ndef.ExternalRecord>()) {
73+
if (_normalizedExternalType(record) != type) continue;
74+
75+
final payload = record.payload;
76+
if (payload == null || payload.isEmpty) return null;
77+
return payload;
78+
}
79+
80+
return null;
81+
}
82+
83+
bool _hasExternalRecordForType(List<ndef.NDEFRecord> records, String type) {
84+
return records.whereType<ndef.ExternalRecord>().any(
85+
(record) => _normalizedExternalType(record) == type,
86+
);
87+
}
88+
89+
String? _normalizedExternalType(ndef.ExternalRecord record) {
90+
final decodedType = record.decodedType;
91+
if (decodedType == null || decodedType.isEmpty) return null;
92+
93+
if (decodedType.startsWith(_bitcoinNfcTypePrefix)) {
94+
return decodedType.substring(_bitcoinNfcTypePrefix.length);
95+
}
96+
97+
return decodedType;
98+
}
99+
100+
bool _matchesSha256Checksum(List<ndef.NDEFRecord> records, List<int> payload) {
101+
final expectedHash = _externalPayloadForType(records, _bitcoinSha256Type);
102+
if (expectedHash == null) return true;
103+
104+
return _bytesEqual(expectedHash, sha256.convert(payload).bytes);
105+
}
106+
107+
bool _bytesEqual(List<int> left, List<int> right) {
108+
if (left.length != right.length) return false;
109+
110+
for (var i = 0; i < left.length; i++) {
111+
if (left[i] != right[i]) return false;
112+
}
113+
114+
return true;
115+
}

0 commit comments

Comments
 (0)