mirror of
https://github.com/LukeHagar/yotocol.git
synced 2025-12-06 04:22:12 +00:00
255 lines
7.6 KiB
JavaScript
255 lines
7.6 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { NFC } from 'nfc-pcsc';
|
|
import * as p from '@clack/prompts';
|
|
import pc from 'picocolors';
|
|
import ndef from '@taptrack/ndef';
|
|
import filenamify from 'filenamify';
|
|
import { existsSync } from 'fs';
|
|
|
|
const nfc = new NFC();
|
|
let currentReader = null;
|
|
|
|
// URI prefix codes as defined in NFC Forum URI Record Type Definition
|
|
const URI_PREFIXES = {
|
|
0x00: '',
|
|
0x01: 'http://www.',
|
|
0x02: 'https://www.',
|
|
0x03: 'http://',
|
|
0x04: 'https://',
|
|
0x05: 'tel:',
|
|
0x06: 'mailto:',
|
|
0x07: 'ftp://anonymous:anonymous@',
|
|
0x08: 'ftp://ftp.',
|
|
0x09: 'ftps://',
|
|
0x0A: 'sftp://',
|
|
0x0B: 'smb://',
|
|
0x0C: 'nfs://',
|
|
0x0D: 'ftp://',
|
|
0x0E: 'dav://',
|
|
0x0F: 'news:',
|
|
0x10: 'telnet://',
|
|
0x11: 'imap:',
|
|
0x12: 'rtsp://',
|
|
0x13: 'urn:',
|
|
0x14: 'pop:',
|
|
0x15: 'sip:',
|
|
0x16: 'sips:',
|
|
0x17: 'tftp:',
|
|
0x18: 'btspp://',
|
|
0x19: 'btl2cap://',
|
|
0x1A: 'btgoep://',
|
|
0x1B: 'tcpobex://',
|
|
0x1C: 'irdaobex://',
|
|
0x1D: 'file://',
|
|
0x1E: 'urn:epc:id:',
|
|
0x1F: 'urn:epc:tag:',
|
|
0x20: 'urn:epc:pat:',
|
|
0x21: 'urn:epc:raw:',
|
|
0x22: 'urn:epc:',
|
|
0x23: 'urn:nfc:'
|
|
};
|
|
|
|
// Helper function to parse NDEF URI record
|
|
function parseUriRecord(data) {
|
|
if (data.length < 2) return null;
|
|
|
|
// The first byte is the prefix code
|
|
const prefixCode = data[0];
|
|
console.log('URI prefix code:', prefixCode.toString(16).padStart(2, '0'));
|
|
|
|
// The rest is the URI data
|
|
const uriData = data.slice(1);
|
|
console.log('URI data hex:', uriData.toString('hex'));
|
|
|
|
const prefix = URI_PREFIXES[prefixCode] || '';
|
|
const uri = uriData.toString('utf8');
|
|
|
|
return {
|
|
prefixCode,
|
|
prefix,
|
|
uri,
|
|
fullUri: prefix + uri,
|
|
raw: {
|
|
prefixCode,
|
|
uriData: uriData.toString('hex')
|
|
}
|
|
};
|
|
}
|
|
|
|
// Helper function to find NDEF message in raw data
|
|
function findNdefMessage(data) {
|
|
// Look for NDEF TLV (Type-Length-Value) structure
|
|
// TLV format: [Type (1 byte)][Length (1 byte)][Value (Length bytes)]
|
|
const ndefTlv = 0x03; // NDEF Message TLV
|
|
const terminatorTlv = 0xFE; // Terminator TLV
|
|
|
|
let offset = 0;
|
|
while (offset < data.length) {
|
|
const type = data[offset];
|
|
if (type === ndefTlv) {
|
|
const length = data[offset + 1];
|
|
const value = data.slice(offset + 2, offset + 2 + length);
|
|
return value;
|
|
} else if (type === terminatorTlv) {
|
|
break;
|
|
}
|
|
offset++;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Helper function to decode payload
|
|
function decodePayload(payload) {
|
|
// Split the comma-separated string into numbers
|
|
const numbers = payload.split(',').map(num => parseInt(num.trim(), 10));
|
|
|
|
// The first byte is the URI prefix code
|
|
const prefixCode = numbers[0];
|
|
const prefix = URI_PREFIXES[prefixCode] || '';
|
|
|
|
// Convert remaining numbers to characters
|
|
const chars = numbers.slice(1).map(num => String.fromCharCode(num));
|
|
|
|
// Join prefix and characters
|
|
return prefix + chars.join('');
|
|
}
|
|
|
|
async function main() {
|
|
p.intro(pc.blue('🔍 Yoto Card Reader'));
|
|
|
|
const s = p.spinner();
|
|
s.start('Initializing NFC reader...');
|
|
|
|
nfc.on('reader', async (reader) => {
|
|
s.stop('NFC reader ready');
|
|
currentReader = reader;
|
|
|
|
reader.on('card', async (card) => {
|
|
try {
|
|
// Read the card's UID
|
|
const uid = card.uid;
|
|
p.note(`
|
|
Card Type: ${card.type}
|
|
Card UID: ${uid}
|
|
`, 'Card Detected');
|
|
|
|
// Read the entire card memory
|
|
try {
|
|
const data = await reader.read(4, 48);
|
|
|
|
// Try to find NDEF message in the raw data
|
|
const ndefData = findNdefMessage(data);
|
|
if (ndefData) {
|
|
try {
|
|
// Parse the NDEF message using the library
|
|
const message = ndef.Message.fromBytes(ndefData);
|
|
const records = message.getRecords();
|
|
|
|
const parsedRecords = records.map(record => {
|
|
const recordInfo = {
|
|
type: record.getType().toString(),
|
|
tnf: record.getTnf(),
|
|
id: record.getId()?.toString(),
|
|
payload: record.getPayload().toString('hex'),
|
|
decodedPayload: decodePayload(record.getPayload().toString())
|
|
};
|
|
|
|
// Use library utilities to parse specific record types
|
|
if (record.getType().toString() === 'U') {
|
|
const uri = ndef.Utils.resolveUriRecordToString(record);
|
|
recordInfo.uri = uri;
|
|
} else if (record.getType().toString() === 'T') {
|
|
const textInfo = ndef.Utils.resolveTextRecord(record);
|
|
recordInfo.text = textInfo;
|
|
}
|
|
|
|
return recordInfo;
|
|
});
|
|
|
|
if (parsedRecords.length > 0) {
|
|
const url = parsedRecords[0].decodedPayload;
|
|
p.note(url, 'Card Contents');
|
|
|
|
// Read existing cards.json or create new if it doesn't exist
|
|
const fs = await import('fs/promises');
|
|
const cardsFile = 'cards.json';
|
|
const urlsFile = 'urls.json';
|
|
let cards = [];
|
|
let urls = [];
|
|
|
|
try {
|
|
const cardsData = await fs.readFile(cardsFile, 'utf8');
|
|
cards = JSON.parse(cardsData);
|
|
} catch (err) {
|
|
// File doesn't exist or is empty, start with empty array
|
|
}
|
|
|
|
try {
|
|
const urlsData = await fs.readFile(urlsFile, 'utf8');
|
|
urls = JSON.parse(urlsData);
|
|
} catch (err) {
|
|
// File doesn't exist or is empty, start with empty array
|
|
}
|
|
|
|
// Check if URL already exists
|
|
const existingCard = cards.find(card => card.url === url);
|
|
if (existingCard) {
|
|
p.note('URL already exists in cards.json', 'Info');
|
|
} else {
|
|
// Add new card entry
|
|
cards.push({
|
|
uid,
|
|
timestamp: new Date().toISOString(),
|
|
url,
|
|
data: parsedRecords
|
|
});
|
|
|
|
// Save updated cards.json
|
|
await fs.writeFile(cardsFile, JSON.stringify(cards, null, 2));
|
|
p.note('URL added to cards.json', 'Success');
|
|
}
|
|
|
|
// Check if URL exists in urls.json
|
|
if (!urls.includes(url)) {
|
|
urls.push(url);
|
|
// Save updated urls.json
|
|
await fs.writeFile(urlsFile, JSON.stringify(urls, null, 2));
|
|
p.note('URL added to urls.json', 'Success');
|
|
} else {
|
|
p.note('URL already exists in urls.json', 'Info');
|
|
}
|
|
} else {
|
|
p.note('No NDEF data found on card', 'Info');
|
|
}
|
|
} catch (ndefErr) {
|
|
p.log.error(`NDEF parse error: ${ndefErr.message}`);
|
|
}
|
|
} else {
|
|
p.note('No NDEF TLV structure found in raw data', 'Info');
|
|
}
|
|
} catch (readErr) {
|
|
p.log.error(`Error reading card: ${readErr.message}`);
|
|
}
|
|
} catch (err) {
|
|
p.log.error(`Error processing card: ${err.message}`);
|
|
}
|
|
});
|
|
|
|
reader.on('error', (err) => {
|
|
p.log.error(`Reader error: ${err.message}`);
|
|
});
|
|
|
|
reader.on('end', () => {
|
|
p.log.error('Reader disconnected');
|
|
});
|
|
});
|
|
|
|
nfc.on('error', (err) => {
|
|
s.stop('Error initializing NFC');
|
|
p.log.error(`NFC error: ${err.message}`);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
main(); |