├── .nvmrc ├── .npmignore ├── .gitignore ├── .editorconfig ├── .eslintrc.yml ├── tsconfig.json ├── examples └── nfc-uid-logger.js ├── src ├── index.ts ├── context.ts ├── common.ts ├── reader.ts └── card.ts ├── package.json ├── LICENSE └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/erbium 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /examples 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /lib 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,json,yml}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | es2020: true 3 | node: true 4 | extends: 5 | - airbnb-base 6 | parserOptions: 7 | ecmaVersion: 11 8 | sourceType: module 9 | rules: 10 | no-bitwise: off 11 | object-curly-newline: ["error", {"ObjectPattern": { "multiline": true }}] 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "allowSyntheticDefaultImports": true, 5 | "target": "es6", 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "outDir": "lib", 9 | }, 10 | "include": [ 11 | "src/**/*" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /examples/nfc-uid-logger.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-constant-condition */ 2 | /* eslint-disable no-await-in-loop */ 3 | /* eslint-disable no-console */ 4 | const mifare = require('../lib'); 5 | 6 | (async () => { 7 | const ctx = mifare(); 8 | 9 | while (true) { 10 | console.log('before got card'); 11 | const card = await ctx.waitForCard(); 12 | console.log('got card'); 13 | try { 14 | const uid = await card.getUID(); 15 | console.log('getUID', uid); 16 | } catch (err) { 17 | console.log('Error on getUID', err); 18 | } 19 | await card.disconnect(); 20 | console.log('after disconnected'); 21 | } 22 | })(); 23 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | const Card = require('./card'); 2 | const Context = require('./context'); 3 | 4 | const { 5 | KEY_TYPE_A, 6 | KEY_TYPE_B, 7 | DEFAULT_KEY, 8 | DEFAULT_KEYS, 9 | DEFAULT_C1, 10 | DEFAULT_C2, 11 | DEFAULT_C3, 12 | DEFAULT_END_ACS, 13 | } = require('./common'); 14 | 15 | let globContext = null; 16 | 17 | const getContext = () => { 18 | if (globContext === null) { 19 | globContext = new Context(); 20 | } 21 | return globContext; 22 | }; 23 | 24 | module.exports = Object.assign(getContext, { 25 | Card, 26 | KEY_TYPE_A, 27 | KEY_TYPE_B, 28 | DEFAULT_KEY, 29 | DEFAULT_KEYS, 30 | DEFAULT_C1, 31 | DEFAULT_C2, 32 | DEFAULT_C3, 33 | DEFAULT_END_ACS, 34 | }); 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mifare-pcsc", 3 | "version": "1.0.0", 4 | "description": "Simple pcsclite wrapper for mifare 1K card", 5 | "main": "lib/index.js", 6 | "engines": { 7 | "node": ">=10.0.0" 8 | }, 9 | "dependencies": { 10 | "pcsclite": "^1.0.0" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/dennajort/node-mifare-pcsc.git" 15 | }, 16 | "keywords": [ 17 | "mifare", 18 | "pcsc", 19 | "pcsclite", 20 | "nfc", 21 | "rfid" 22 | ], 23 | "author": "dennajort", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/dennajort/node-mifare-pcsc/issues" 27 | }, 28 | "homepage": "https://github.com/dennajort/node-mifare-pcsc", 29 | "devDependencies": { 30 | "@types/node": "^14.6.0", 31 | "eslint": "^7.7.0", 32 | "eslint-config-airbnb-base": "^14.2.0", 33 | "eslint-plugin-import": "^2.22.0", 34 | "typescript": "^4.0.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import { once, EventEmitter } from 'events'; 2 | import pcsclite from 'pcsclite'; 3 | import { log } from './common'; 4 | import Reader from './reader'; 5 | import Card from './card'; 6 | 7 | class Context extends EventEmitter { 8 | pcsc: PCSCLite; 9 | 10 | constructor() { 11 | super(); 12 | this.pcsc = pcsclite(); 13 | 14 | this.pcsc.on('error', (err: Error) => { 15 | log(`PCSC Error: ${err}`); 16 | this.emit('error', err); 17 | }); 18 | 19 | const onCard = (card: Card) => { 20 | this.emit('card', card); 21 | }; 22 | 23 | this.pcsc.on('reader', (rawReader: CardReader) => { 24 | const reader = new Reader(rawReader); 25 | this.emit('reader', reader); 26 | reader.on('card', onCard); 27 | reader.once('end', () => { 28 | reader.removeListener('card', onCard); 29 | }); 30 | }); 31 | } 32 | 33 | async waitForCard(): Promise { 34 | const [card] = await once(this, 'card'); 35 | return card; 36 | } 37 | } 38 | 39 | export default Context; 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jean-Baptiste Gosselin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/common.ts: -------------------------------------------------------------------------------- 1 | const byteFromTwoHex = (high: number, low: number) => ((high & 0xF) << 4) + (low & 0xF); 2 | 3 | const KEY_TYPE_A = 0x60; 4 | const KEY_TYPE_B = 0x61; 5 | const DEFAULT_KEYS = [ 6 | Buffer.from([0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), 7 | Buffer.from([0xa0, 0xb0, 0xc0, 0xd0, 0xe0, 0xf0]), 8 | Buffer.from([0xa1, 0xb1, 0xc1, 0xd1, 0xe1, 0xf1]), 9 | Buffer.from([0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5]), 10 | Buffer.from([0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5]), 11 | Buffer.from([0x4d, 0x3a, 0x99, 0xc3, 0x51, 0xdd]), 12 | Buffer.from([0x1a, 0x98, 0x2c, 0x7e, 0x45, 0x9a]), 13 | Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), 14 | Buffer.from([0xd3, 0xf7, 0xd3, 0xf7, 0xd3, 0xf7]), 15 | Buffer.from([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]), 16 | ]; 17 | const DEFAULT_KEY = DEFAULT_KEYS[0]; 18 | const DEFAULT_C1 = 0x0; 19 | const DEFAULT_C2 = 0x0; 20 | const DEFAULT_C3 = 0x8; 21 | const DEFAULT_END_ACS = 0x69; 22 | 23 | const log = (() => { 24 | if (process.env.MIFARE_DEBUG) { 25 | // eslint-disable-next-line no-console 26 | return (...args: any[]) => console.log((new Date()).toISOString(), ...args); 27 | } 28 | return () => {}; 29 | })(); 30 | 31 | export { 32 | log, 33 | byteFromTwoHex, 34 | KEY_TYPE_A, 35 | KEY_TYPE_B, 36 | DEFAULT_KEY, 37 | DEFAULT_KEYS, 38 | DEFAULT_C1, 39 | DEFAULT_C2, 40 | DEFAULT_C3, 41 | DEFAULT_END_ACS, 42 | }; 43 | -------------------------------------------------------------------------------- /src/reader.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | import { log } from './common'; 4 | import Card from './card'; 5 | 6 | class Reader extends EventEmitter { 7 | reader: CardReader; 8 | 9 | constructor(reader: CardReader) { 10 | super(); 11 | this.reader = reader; 12 | 13 | const onStatus = async (status) => { 14 | const changes = this.reader.state ^ status.state; 15 | if (!changes) return; 16 | 17 | const isState = (state) => (changes & state) && (status.state & state); 18 | 19 | if (isState(this.reader.SCARD_STATE_EMPTY)) { 20 | this.log('card removed'); 21 | try { 22 | await this.disconnect(this.reader.SCARD_LEAVE_CARD); 23 | this.log('card disconnected'); 24 | } catch (err) { 25 | this.log(`error on disconnect ${err}`); 26 | } 27 | return; 28 | } 29 | 30 | if (isState(this.reader.SCARD_STATE_PRESENT)) { 31 | this.log('card inserted'); 32 | try { 33 | const protocol = await this.connect({ share_mode: this.reader.SCARD_SHARE_SHARED }); 34 | this.log('card connected'); 35 | setImmediate(() => this.emit('card', new Card(this, protocol))); 36 | } catch (err) { 37 | this.log(`error on connect ${err}`); 38 | } 39 | } 40 | }; 41 | 42 | this.reader.on('status', onStatus); 43 | this.reader.once('end', () => { 44 | this.reader.removeListener('status', onStatus); 45 | this.emit('end'); 46 | }); 47 | } 48 | 49 | log(...args: any[]) { 50 | log(`Reader(${this.reader.name})`, ...args); 51 | } 52 | 53 | transmit(input: Buffer, resLen: number, protocol: number): Promise { 54 | return new Promise((resolve, reject) => { 55 | this.reader.transmit(input, resLen, protocol, (err, data) => { 56 | setImmediate(() => { 57 | this.log('this.reader.transmit'); 58 | if (err) return reject(err); 59 | return resolve(data); 60 | }); 61 | }); 62 | }); 63 | } 64 | 65 | disconnect(disposition: number) { 66 | return new Promise((resolve, reject) => { 67 | this.reader.disconnect(disposition, (err) => { 68 | setImmediate(() => { 69 | if (err) return reject(err); 70 | return resolve(); 71 | }); 72 | }); 73 | }); 74 | } 75 | 76 | connect(options): Promise { 77 | return new Promise((resolve, reject) => { 78 | this.reader.connect(options, (err, protocol) => { 79 | setImmediate(() => { 80 | if (err) return reject(err); 81 | return resolve(protocol); 82 | }); 83 | }); 84 | }); 85 | } 86 | } 87 | 88 | export default Reader; 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-mifare-pcsc 2 | 3 | Simple pcsclite wrapper for mifare 1K card. 4 | 5 | Works with promises and provides an easy to use wrapper to communicate with Mifate 1K tags. 6 | 7 | ## Installation 8 | 9 | Under the hood it is using [node-pcsclite](https://github.com/santigimeno/node-pcsclite), you can follow installation instructions from there. 10 | 11 | ## Example 12 | 13 | ```javascript 14 | const mifare = require('mifare-pcsc'); 15 | 16 | (async () => { 17 | const ctx = mifare(); 18 | 19 | while (true) { 20 | console.log('Waiting for a card...'); 21 | const card = await ctx.waitForCard(); 22 | try { 23 | const uid = await card.getUID(); 24 | console.log('UID', uid); 25 | } catch (err) { 26 | console.log('Error on getUID', err); 27 | } 28 | await card.disconnect(); 29 | } 30 | })(); 31 | ``` 32 | 33 | ## API 34 | 35 | ### Class: `Context` 36 | This context manage the underlying pcsclite library and provide a card polling mechanism. 37 | 38 | #### Event: `'error'` 39 | - *err* `Error`. The error. 40 | 41 | #### Event: `'card'` 42 | - *card* `Card`. The connected card. 43 | 44 | #### `context.waitForCard()` 45 | Returns a Promise that resolve into a `Card` when it connects. 46 | 47 | ### Class: `Card` 48 | This class represents a connected card and methods to interact with it. 49 | 50 | #### `card.getUID()` 51 | Returns a Promise that resolve into a `Buffer` that contains the UID. 52 | 53 | #### `card.loadAuthKey(nb, key)` 54 | - *nb* `Number`. Key number. 55 | - *key* `Buffer`. 6 bytes Key. 56 | 57 | Load authentication key into the reader of the card. Returns a Promise. 58 | 59 | #### `card.authenticate(block, type, key)` 60 | - *block* `Number`. Block to authenticate. 61 | - *type* `Number`. Key type, provided by `mifare.KEY_TYPE_A` and `mifare.KEY_TYPE_B`. 62 | - *key* `Number`. Key number set when using `card.loadAuthKey`. 63 | 64 | Authenticate on block with a key. Returns a Promise. 65 | 66 | #### `card.readBlock(block, length)` 67 | - *block* `Number`. Block to read. 68 | - *length* `Number`. Length in bytes to read. 69 | 70 | Read a block. Returns a Promise that resolve to a `Buffer` containing the data. 71 | 72 | #### `card.updateBlock(block, data)` 73 | - *block* `Number`. Block to update. 74 | - *data* `Buffer`. Value that should be updated in the block. 75 | 76 | Update data in a block. Returns a Promise. 77 | 78 | #### `card.restoreBlock(src, dest)` 79 | - *src* `Number`. Block to read data from. 80 | - *dest* `Number`. Block to restore from *src*. 81 | 82 | Restore destination block with source block. Returns a Promise. 83 | 84 | #### `card.disconnect()` 85 | 86 | Early disconnect of card when finished to use. Optional. Returns a Promise. 87 | 88 | #### `card.transmit(input, resLen = 0)` 89 | - *input* `Buffer`. Command with data to send to the card. 90 | - *resLen* `Number`. Size of expected response from the card. 91 | 92 | Low-level method for sending arbitrary command to the card. 93 | Returns a Promise that resolve into a `Buffer`. 94 | 95 | #### `Card.unpackTrailer(block)` 96 | - *block* `Buffer`. Raw trailer block to unpack. 97 | 98 | Returns a parser trailer block like so 99 | -------------------------------------------------------------------------------- /src/card.ts: -------------------------------------------------------------------------------- 1 | import { 2 | byteFromTwoHex, 3 | KEY_TYPE_A, 4 | KEY_TYPE_B, 5 | } from './common'; 6 | import Reader from './reader'; 7 | 8 | export default class Card { 9 | reader: Reader; 10 | protocol: number; 11 | 12 | constructor(reader: Reader, protocol: number) { 13 | this.reader = reader; 14 | this.protocol = protocol; 15 | } 16 | 17 | static packACS({ c1, c2, c3, user }) { 18 | if (c1 < 0 || c1 > 0xF) throw new Error('C1 is out of range'); 19 | if (c2 < 0 || c2 > 0xF) throw new Error('C2 is out of range'); 20 | if (c3 < 0 || c3 > 0xF) throw new Error('C3 is out of range'); 21 | return Buffer.from([ 22 | byteFromTwoHex(~c2, ~c1), 23 | byteFromTwoHex(c1, ~c3), 24 | byteFromTwoHex(c3, c2), 25 | user, 26 | ]); 27 | } 28 | 29 | static unpackACS(buff: Buffer) { 30 | if (buff.length !== 4) throw new Error('Buffer length is wrong'); 31 | return { 32 | c1: (buff[1] & 0xF0) >> 4, 33 | c2: buff[2] & 0xF, 34 | c3: (buff[2] & 0xF0) >> 4, 35 | user: buff[3], 36 | }; 37 | } 38 | 39 | static packTrailer({ keya, keyb, acs }) { 40 | if (keya.length !== 6) throw new Error('KEY A length is wrong'); 41 | if (keyb.length !== 6) throw new Error('KEY B length is wrong'); 42 | return Buffer.concat([ 43 | Buffer.from(keya), 44 | Card.packACS(acs), 45 | Buffer.from(keyb), 46 | ]); 47 | } 48 | 49 | static unpackTrailer(block: Buffer) { 50 | if (block.length !== 16) throw new Error('Buffer length is wrong'); 51 | return { 52 | keya: block.slice(0, 6), 53 | acs: Card.unpackACS(block.slice(6, 10)), 54 | keyb: block.slice(10, 16), 55 | }; 56 | } 57 | 58 | async transmit(input: Buffer, resLen: number = 0) { 59 | const data = await this.reader.transmit(input, resLen + 2, this.protocol); 60 | if (data.length < 2) throw new Error(`Undefined data: 0x${data.toString('hex')}`); 61 | switch (data.readUInt16BE(data.length - 2)) { 62 | case 0x9000: 63 | return data.slice(0, data.length - 2); 64 | case 0x6300: 65 | throw new Error('Failed'); 66 | default: 67 | throw new Error(`Undefined data: 0x${data.toString('hex')}`); 68 | } 69 | } 70 | 71 | getUID() { 72 | return this.transmit(Buffer.from([0xFF, 0xCA, 0, 0, 0]), 0x8); 73 | } 74 | 75 | loadAuthKey(nb: number, key: Buffer) { 76 | if (nb < 0 || nb > 0x20) throw new Error('Key Number is out of range'); 77 | if (key.length !== 6) throw new Error('Key length should be 6'); 78 | return this.transmit(Buffer.concat([ 79 | Buffer.from([0xFF, 0x82, (nb === 0x20) ? 0x20 : 0, nb, 6]), 80 | Buffer.from(key), 81 | ])); 82 | } 83 | 84 | authenticate(block: number, type: number, key: number) { 85 | if (type !== KEY_TYPE_A && type !== KEY_TYPE_B) throw new Error('Wrong key type'); 86 | if (block < 0 || block > 0x3F) throw new Error('Block out of range'); 87 | if (key < 0 || key > 0x20) throw new Error('Key Number out of range'); 88 | return this.transmit(Buffer.from([0xFF, 0x86, 0, 0, 5, 1, 0, block, type, key])); 89 | } 90 | 91 | readBlock(block: number, length: number) { 92 | if (block < 0 || block > 0x3F) throw new Error('Block out of range'); 93 | if (length !== 0x10 && length !== 0x20 && length !== 0x30) throw new Error('Bad length'); 94 | return this.transmit(Buffer.from([0xFF, 0xB0, 0, block, length]), length); 95 | } 96 | 97 | updateBlock(block: number, data: Buffer) { 98 | if (block < 0 || block > 0x3F) throw new Error('Block out of range'); 99 | if (data.length !== 0x10 && data.length !== 0x20 && data.length !== 0x30) { 100 | throw new Error('Bad length'); 101 | } 102 | return this.transmit(Buffer.concat([ 103 | Buffer.from([0xFF, 0xD6, 0, block, data.length]), 104 | Buffer.from(data), 105 | ])); 106 | } 107 | 108 | restoreBlock(src: number, dest: number) { 109 | if (src < 0 || src > 0x3F) throw new Error('Source block out of range'); 110 | if (dest < 0 || dest > 0x3F) throw new Error('Destination block out of range'); 111 | if (((src / 4) | 0) !== ((dest / 4) | 0)) throw new Error('Blocks are not in the same sector'); 112 | return this.transmit(Buffer.from([0xFF, 0xD7, 0, src, 2, 3, dest])); 113 | } 114 | 115 | disconnect() { 116 | return this.reader.disconnect(this.reader.reader.SCARD_LEAVE_CARD); 117 | } 118 | } 119 | --------------------------------------------------------------------------------