Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ describe('Commands', () => {
describe('cardInfo', () => {
it('should display ATR and applications', async () => {
const { ctx, outputs } = createMockContext();
ctx.format = 'text';

const pseResponse = {
buffer: Buffer.from([
Expand Down Expand Up @@ -542,6 +543,58 @@ describe('Commands', () => {
assert.ok(outputs.some((o) => o.includes('3b9000')));
assert.ok(outputs.some((o) => o.includes('Test Reader')));
});

it('should output JSON when format is json', async () => {
const { ctx, outputs } = createMockContext();
ctx.format = 'json';

const pseResponse = {
buffer: Buffer.from([
0x6f, 0x1a, 0x84, 0x0e, 0x31, 0x50, 0x41, 0x59, 0x2e, 0x53, 0x59, 0x53, 0x2e, 0x44, 0x44, 0x46,
0x30, 0x31, 0xa5, 0x08, 0x88, 0x01, 0x01, 0x5f, 0x2d, 0x02, 0x65, 0x6e,
]),
sw1: 0x90,
sw2: 0x00,
isOk: () => true,
};

const recordResponse = {
buffer: Buffer.from([
0x70, 0x1a, 0x61, 0x18, 0x4f, 0x07, 0xa0, 0x00, 0x00, 0x00, 0x04, 0x10, 0x10, 0x50, 0x0a, 0x4d,
0x61, 0x73, 0x74, 0x65, 0x72, 0x43, 0x61, 0x72, 0x64, 0x87, 0x01, 0x01,
]),
sw1: 0x90,
sw2: 0x00,
isOk: () => true,
};

const emptyResponse = {
buffer: Buffer.alloc(0),
sw1: 0x6a,
sw2: 0x83,
isOk: () => false,
};

let readRecordCalls = 0;
const mockEmv = {
getAtr: () => '3b9000',
getReaderName: () => 'Test Reader',
selectPse: mock.fn(() => Promise.resolve(pseResponse)),
readRecord: mock.fn(() => {
readRecordCalls++;
if (readRecordCalls === 1) {
return Promise.resolve(recordResponse);
}
return Promise.resolve(emptyResponse);
}),
};

const result = await cardInfo(ctx, { emv: mockEmv });
assert.strictEqual(result, 0);
// Should be valid JSON
const json = JSON.parse(outputs[0] ?? '') as { atr: string };
assert.strictEqual(json.atr, '3b9000');
});
});

describe('dumpCard', () => {
Expand Down
96 changes: 68 additions & 28 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,24 +488,18 @@ export async function cardInfo(
return 1;
}

// Display ATR and reader
ctx.output('Card Information:');
ctx.output(` Reader: ${emv.getReaderName()}`);
ctx.output(` ATR: ${emv.getAtr()}`);
ctx.output('');
interface AppInfo {
aid: string;
label: string | undefined;
}
const apps: AppInfo[] = [];

// Try to list applications
const pseResponse = await emv.selectPse();
if (pseResponse.isOk()) {
const sfiData = findTag(pseResponse.buffer, 0x88);
const sfi = sfiData?.[0] ?? 1;

interface AppInfo {
aid: string;
label: string | undefined;
}
const apps: AppInfo[] = [];

for (let record = 1; record <= 10; record++) {
const response = await emv.readRecord(sfi, record);
if (!response.isOk()) break;
Expand All @@ -519,18 +513,34 @@ export async function cardInfo(
});
}
}
}

// Output based on format
if (ctx.format === 'json') {
const result = {
reader: emv.getReaderName(),
atr: emv.getAtr(),
applications: apps,
};
ctx.output(JSON.stringify(result, null, 2));
} else {
// Text format (default)
ctx.output('Card Information:');
ctx.output(` Reader: ${emv.getReaderName()}`);
ctx.output(` ATR: ${emv.getAtr()}`);
ctx.output('');

if (apps.length > 0) {
ctx.output('Applications:');
for (const app of apps) {
const labelStr = app.label ? ` (${app.label})` : '';
ctx.output(` ${app.aid}${labelStr}`);
}
} else {
} else if (pseResponse.isOk()) {
ctx.output('No applications found');
} else {
ctx.output('PSE not available');
}
} else {
ctx.output('PSE not available');
}

return 0;
Expand All @@ -549,32 +559,62 @@ export async function dumpCard(
return 1;
}

ctx.output('EMV Card Dump');
ctx.output('=============');
ctx.output('');
ctx.output(`ATR: ${emv.getAtr()}`);
ctx.output('');
interface RecordData {
sfi: number;
record: number;
data: string;
}
const records: RecordData[] = [];
let pseHex = '';
let sfi = 1;

// Select PSE first
const pseResponse = await emv.selectPse();
if (pseResponse.isOk()) {
ctx.output('PSE Response:');
ctx.output(` ${pseResponse.buffer.toString('hex')}`);
ctx.output('');

pseHex = pseResponse.buffer.toString('hex');
const sfiData = findTag(pseResponse.buffer, 0x88);
const sfi = sfiData?.[0] ?? 1;

ctx.output(`Reading SFI ${String(sfi)}:`);
sfi = sfiData?.[0] ?? 1;

for (let record = 1; record <= 10; record++) {
const response = await emv.readRecord(sfi, record);
if (!response.isOk()) break;

ctx.output(` Record ${String(record)}: ${response.buffer.toString('hex')}`);
records.push({
sfi,
record,
data: response.buffer.toString('hex'),
});
}
}

// Output based on format
if (ctx.format === 'json') {
const result = {
atr: emv.getAtr(),
pse: pseHex,
records,
};
ctx.output(JSON.stringify(result, null, 2));
} else {
ctx.output('PSE selection failed');
// Text format (default)
ctx.output('EMV Card Dump');
ctx.output('=============');
ctx.output('');
ctx.output(`ATR: ${emv.getAtr()}`);
ctx.output('');

if (pseResponse.isOk()) {
ctx.output('PSE Response:');
ctx.output(` ${pseHex}`);
ctx.output('');

ctx.output(`Reading SFI ${String(sfi)}:`);
for (const rec of records) {
ctx.output(` Record ${String(rec.record)}: ${rec.data}`);
}
} else {
ctx.output('PSE selection failed');
}
}

return 0;
Expand Down