diff --git a/pug/src/device-settings.pug b/pug/src/device-settings.pug index 345733c..9b26713 100644 --- a/pug/src/device-settings.pug +++ b/pug/src/device-settings.pug @@ -106,11 +106,11 @@ block content block script script(crossorigin="anonymous", src="https://cdn.jsdelivr.net/npm/joi@17/dist/joi-browser.min.js") script. - const { AnimationMode, ButtonAction, ButtonType, ChameleonDebug, ChameleonUltra, DeviceMode, WebbleAdapter, WebserialAdapter } = ChameleonUltraJS // eslint-disable-line - const ultraUsb = new ChameleonUltra(true) + const { AnimationMode, ButtonAction, ButtonType, ChameleonDebug, ChameleonUltra, DeviceMode, WebbleAdapter, WebserialAdapter } = window.ChameleonUltraJS + const ultraUsb = new ChameleonUltra() ultraUsb.use(new ChameleonDebug()) ultraUsb.use(new WebserialAdapter()) - const ultraBle = new ChameleonUltra(true) + const ultraBle = new ChameleonUltra() ultraBle.use(new ChameleonDebug()) ultraBle.use(new WebbleAdapter()) diff --git a/pug/src/mfkey32.pug b/pug/src/mfkey32.pug index 11e74d8..ca10fd2 100644 --- a/pug/src/mfkey32.pug +++ b/pug/src/mfkey32.pug @@ -108,11 +108,11 @@ block content block script script. - const { Buffer, ChameleonDebug, ChameleonUltra, DeviceMode, FreqType, Mf1KeyType, TagType, WebbleAdapter, WebserialAdapter } = ChameleonUltraJS // eslint-disable-line - const ultraUsb = new ChameleonUltra(true) + const { Buffer, ChameleonDebug, ChameleonUltra, DeviceMode, FreqType, Mf1KeyType, TagType, WebbleAdapter, WebserialAdapter } = window.ChameleonUltraJS + const ultraUsb = new ChameleonUltra() ultraUsb.use(new ChameleonDebug()) ultraUsb.use(new WebserialAdapter()) - const ultraBle = new ChameleonUltra(true) + const ultraBle = new ChameleonUltra() ultraBle.use(new ChameleonDebug()) ultraBle.use(new WebbleAdapter()) @@ -194,7 +194,6 @@ block script await ultra.cmdSlotChangeTagType(slot, tagType ? TagType.MIFARE_4096 : TagType.MIFARE_1024) await ultra.cmdSlotResetTagType(slot, tagType ? TagType.MIFARE_4096 : TagType.MIFARE_1024) await ultra.cmdSlotSetEnable(slot, FreqType.HF, true) - await ultra.cmdSlotSetActive(slot) // set anti-coll const tag = { atqa: Buffer.from(atqa, 'hex').reverse(), @@ -211,6 +210,7 @@ block script tag.atqa.copy(block0, tag.uid.length + 2) // atqa console.log(`block0 = ${block0.toString('hex')}`) await ultra.cmdMf1EmuWriteBlock(0, block0) // set block0 + await ultra.cmdSlotSetActive(slot) await ultra.cmdSlotSaveSettings() await Swal.fire({ icon: 'success', title: 'Emulate successfully!' }) } catch (err) { diff --git a/pug/src/mifare-value.pug b/pug/src/mifare-value.pug index ba8eff2..6ba42b1 100644 --- a/pug/src/mifare-value.pug +++ b/pug/src/mifare-value.pug @@ -93,11 +93,11 @@ block content block script script(crossorigin="anonymous", src="https://cdn.jsdelivr.net/npm/joi@17/dist/joi-browser.min.js") script. - const { Buffer, ChameleonDebug, ChameleonUltra, DeviceMode, Mf1KeyType, Mf1VblockOperator, WebbleAdapter, WebserialAdapter } = ChameleonUltraJS // eslint-disable-line - const ultraUsb = new ChameleonUltra(true) + const { Buffer, ChameleonDebug, ChameleonUltra, Mf1KeyType, Mf1VblockOperator, WebbleAdapter, WebserialAdapter } = window.ChameleonUltraJS + const ultraUsb = new ChameleonUltra() ultraUsb.use(new ChameleonDebug()) ultraUsb.use(new WebserialAdapter()) - const ultraBle = new ChameleonUltra(true) + const ultraBle = new ChameleonUltra() ultraBle.use(new ChameleonDebug()) ultraBle.use(new WebbleAdapter()) diff --git a/pug/src/mifare-xiaomi.pug b/pug/src/mifare-xiaomi.pug index defc082..4a3c0ff 100644 --- a/pug/src/mifare-xiaomi.pug +++ b/pug/src/mifare-xiaomi.pug @@ -36,7 +36,7 @@ block content .card.shadow-sm.mb-2 h6.card-header.bg-light ① Mifare Dump .card-body.px-3.pt-3.pb-2.letter-spacing-n1px - p.text-sm Click "Import" button to import a mifare dump exported from #[a(target="_blank", href=`${baseurl}mifare1k.html`) mifare1k.html] or other tools like MCT, Proxmark3. + p.text-sm Click "Import" button to import a mifare dump exported from #[a(target="_blank", href=`${baseurl}mifare1k.html`) mifare1k.html] or other tools like MCT, Proxmark3. Keys in dump will be used to sync with Xiaomi Watch. Dump will be deleted after page closed. input.d-none(type="file", ref="dumpImport", @change="dumpImport?.cb?.($event.target.files[0])") .input-group.input-group-sm.mb-2.was-validated .input-group-prepend: span.input-group-text.justify-content-center UID @@ -55,7 +55,7 @@ block content .col.px-1: button.btn.btn-block.btn-outline-primary(@click="btnExportDump") #[i.fa.fa-fw.fa-floppy-o] Export button.btn.btn-sm.btn-block.btn-outline-danger.mb-2(@click="btnResetDump") #[i.fa.mr-1.fa-repeat] Reset to empty dump .card.shadow-sm.mb-2 - h6.card-header.bg-light ② Emulate Non-Encrypted Tag + h6.card-header.bg-light ② ChameleonUltra Emulate .card-body.px-3.pt-3.pb-2.letter-spacing-n1px p.text-sm Choose a slot and click the "Emulate" button to emulate as non-encrypted tag. Then use Xiaomi Watch to clone the ChameleonUltra. .input-group.input-group-sm.mb-2 @@ -64,16 +64,19 @@ block content option(v-for="i of _.range(8)" :value="i") Slot {{ i + 1 }} button.btn.btn-block.btn-outline-success.mb-2(@click="btnEmuWrite") #[i.fa.mr-1.fa-sign-in] Emulate .card.shadow-sm.mb-2 - h6.card-header.bg-light ③ Sync Dump to Xiaomi Watch + h6.card-header.bg-light ③ Sync with Xiaomi Watch .card-body.px-3.pt-3.pb-2.letter-spacing-n1px - p.text-sm.mb-2 After clone, click "Write" button to write original dump to watch. + p.text-sm.mb-2 After clone, click "Write" button to write original dump to Xiaomi Watch. + .custom-control.custom-checkbox.mb-2 + input.custom-control-input#ss-checkUidBeforeWrite(type="checkbox", v-model="ss.checkUidBeforeWrite") + label.custom-control-label(for="ss-checkUidBeforeWrite") Verify UID before write button.btn.btn-block.btn-outline-primary.mb-2(@click="btnGen2Write") #[i.fa.mr-1.fa-download] Write .card-body.px-3.pt-3.pb-2.letter-spacing-n1px.border-top - p.text-sm.mb-2 Click "Verify" button to compare between dump and watch (skip block 0). - button.btn.btn-block.btn-outline-info.mb-2(@click="btnGen2Verify") #[i.fa.mr-1.fa-exchange] Verify + p.text-sm.mb-2 Click "Verify" button to compare between dump and Xiaomi Watch (skip block 0). + button.btn.btn-block.btn-outline-success.mb-2(@click="btnGen2Verify") #[i.fa.mr-1.fa-exchange] Verify .card-body.px-3.pt-3.pb-2.letter-spacing-n1px.border-top - p.text-sm.mb-2 Click "Read" button to read dump from watch (skip block 0). After read, click "Export" button above to save the dump. - button.btn.btn-block.btn-outline-warning.mb-2(@click="btnGen2Read") #[i.fa.mr-1.fa-upload] Read + p.text-sm.mb-2 Click "Read" button to read from Xiaomi Watch (skip block 0). After read, you can click "Export" or "Write" button. + button.btn.btn-block.btn-outline-info.mb-2(@click="btnGen2Read") #[i.fa.mr-1.fa-upload] Read .modal.fade(tabindex="-1", ref="dumpExport") .modal-dialog.modal-dialog-centered.modal-xl .modal-content @@ -108,21 +111,21 @@ block content block script script. - const { Buffer, ChameleonDebug, ChameleonUltra, DeviceMode, FreqType, Mf1KeyType, TagType, WebbleAdapter, WebserialAdapter } = ChameleonUltraJS // eslint-disable-line - const ultraUsb = new ChameleonUltra(true) + const { Buffer, ChameleonDebug, ChameleonUltra, DeviceMode, FreqType, TagType, WebbleAdapter, WebserialAdapter } = window.ChameleonUltraJS + const ultraUsb = new ChameleonUltra() ultraUsb.use(new ChameleonDebug()) ultraUsb.use(new WebserialAdapter()) - const ultraBle = new ChameleonUltra(true) + const ultraBle = new ChameleonUltra() ultraBle.use(new ChameleonDebug()) ultraBle.use(new WebbleAdapter()) const toHex = buf => _.toUpper(buf.toString('hex')) - const WELL_KNOWN_KEYS = Buffer.from(['ffffffffffff'].join(''), 'hex').chunk(6) + const WELL_KNOWN_KEYS = Buffer.from(['FFFFFFFFFFFF'].join(''), 'hex').chunk(6) function getEmptyDump () { const buf = new Buffer(1024) - const blkFactory = Buffer.from('deadbeef220804000177a2cc35afa51d', 'hex') - const blkAcl = Buffer.from('ffffffffffffff078069ffffffffffff', 'hex') + const blkFactory = Buffer.from('DEADBEEF220804000177A2CC35AFA51D', 'hex') + const blkAcl = Buffer.from('FFFFFFFFFFFFFF078069FFFFFFFFFFFF', 'hex') buf.set(blkFactory, 0) // block 0 for (let i = 0; i < 16; i++) buf.set(blkAcl, i * 64 + 48) // block 4n+3 return buf @@ -136,6 +139,7 @@ block script }, ss: { atqa: '0004', + checkUidBeforeWrite: true, dumpB64: getEmptyDump().toString('base64url'), sak: '08', slot: 0, @@ -257,9 +261,9 @@ block script if (/^[+]Sector: \d+$/.test(row)) { blockNo = _.parseInt(row.slice(9)) * 4 } else if (/^[0-9a-fA-F-]{32}$/.test(row)) { // hex - if (blockNo >= 64) continue + if (blockNo >= 64) throw new Error(`Invalid block number: ${blockNo}`) const blockbuf = Buffer.from(row.replaceAll('-', '0'), 'hex') - if (blockbuf.length !== 16) continue + if (blockbuf.length !== 16) throw new Error(`Invalid block size: ${blockbuf.length} bytes`) blockbuf.copy(buf, blockNo * 16) blockNo++ } @@ -268,7 +272,9 @@ block script }, async btnExportDump () { const { dump } = this - const { uid, atqa, sak } = this.ss + const uid = toHex(Buffer.from(this.ss.uid, 'hex')) + const atqa = toHex(Buffer.from(this.ss.atqa, 'hex').reverse()) + const sak = toHex(Buffer.from(this.ss.sak, 'hex')) // json const json = { @@ -324,21 +330,21 @@ block script } }, async btnEmuWrite () { - const { atqa, sak, uid, ultra, dump } = this + const { ultra, dump } = this + const { slot } = this.ss try { - const { slot } = this.ss - console.log({ atqa, sak, slot, uid }) + this.showLoading({ text: 'Emulating tag...' }) const slotName = await ultra.cmdSlotGetFreqName(slot, FreqType.HF) ?? '(no name)' - const msg1 = `The hf data of slot ${slot + 1} "${slotName}" will be REPLACE! Continue?` + const msg1 = `Slot ${slot + 1} "${slotName}" will be REPLACE! Continue?` if (!await this.confirm(msg1, 'Yes', 'Cancel')) return this.showLoading({ text: 'Emulating tag...' }) await ultra.cmdChangeDeviceMode(DeviceMode.TAG) // reset slot await ultra.cmdSlotChangeTagType(slot, TagType.MIFARE_1024) await ultra.cmdSlotResetTagType(slot, TagType.MIFARE_1024) + await ultra.cmdSlotSetEnable(slot, FreqType.HF, true) await ultra.cmdMf1SetAntiCollMode(true) await ultra.cmdMf1EmuWriteBlock(0, dump.subarray(0, 16)) // set block0 - await ultra.cmdSlotSetEnable(slot, FreqType.HF, true) await ultra.cmdSlotSetActive(slot) await ultra.cmdSlotSaveSettings() await Swal.fire({ icon: 'success', title: 'Emulate successfully!' }) @@ -352,6 +358,7 @@ block script try { const msg1 = 'Mifare Data in Xiaomi Watch will be REPLACE! Continue?' if (!await this.confirm(msg1, 'Yes', 'Cancel')) return + if (this.ss.checkUidBeforeWrite) await this.mfVerifyUid() const genSwalCfg = i => ({ html: `
Writing Mifare / Gen2:${i} / 16
`, }) @@ -368,6 +375,7 @@ block script const { success } = await ultra.mf1WriteSectorByKeys(i, keys, sectorData) for (let j = 0; j < 4; j++) if (!success[j]) failed.push(i * 4 + j) } catch (err) { + if (!ultra.isConnected()) throw err ultra.emitter.emit('error', err) for (let j = 0; j < 4; j++) failed.push(i * 4 + j) } @@ -382,6 +390,12 @@ block script await Swal.fire({ icon: 'error', title: 'Write failed', text: err.message }) } }, + async mfVerifyUid () { + const { ultra } = this + this.showLoading({ text: 'Verify UID...' }) + const scaned = _.first(await ultra.cmdHf14aScan()) + if (!scaned.uid.equals(Buffer.from(this.ss.uid, 'hex'))) throw new Error(`UID mismatch, read = ${toHex(scaned.uid)}`) + }, async mfGen2Read () { const { dump, ultra } = this const genSwalCfg = i => ({ @@ -402,8 +416,9 @@ block script newDump.set(sectorData, i * 64) for (let j = 0; j < 4; j++) if (!success[j]) failed.push(i * 4 + j) } catch (err) { - for (let j = 0; j < 4; j++) failed.push(i * 4 + j) + if (!ultra.isConnected()) throw err ultra.emitter.emit('error', err) + for (let j = 0; j < 4; j++) failed.push(i * 4 + j) } this.showLoading(genSwalCfg(i + 1)) } @@ -415,12 +430,13 @@ block script async btnGen2Verify () { const { dump, ultra } = this try { + await this.mfVerifyUid() const other = await this.mfGen2Read() const diffs = [] for (let i = 1; i < 64; i++) { - const sector1 = dump.subarray(i * 16).subarray(0, 16) - const sector2 = other.subarray(i * 16).subarray(0, 16) - if (!sector1.equals(sector2)) diffs.push(i) + const blk1 = dump.subarray(i * 16).subarray(0, 16) + const blk2 = other.subarray(i * 16).subarray(0, 16) + if (!blk1.equals(blk2)) diffs.push(i) } if (diffs.length !== 0) throw new Error(`${diffs.length} blocks are different: ${diffs.join(',')}`) await Swal.fire({ icon: 'success', title: 'All blocks is equal.' }) diff --git a/pug/src/mifare1k.pug b/pug/src/mifare1k.pug index 434c5c1..99cfa32 100644 --- a/pug/src/mifare1k.pug +++ b/pug/src/mifare1k.pug @@ -96,11 +96,11 @@ block content .card.shadow-sm.mb-2 h6.card-header.bg-light #[i.fa.fa-id-card.mr-1] Mifare Dump .card-body.px-3.pt-3.pb-2.letter-spacing-n1px - input.d-none(type="file", ref="cardImport", @change="cardImport?.cb?.($event.target.files[0])") + input.d-none(type="file", ref="dumpImport", @change="dumpImport?.cb?.($event.target.files[0])") .row.mx-n1.mb-2 - .col.px-1: button.btn.btn-block.btn-outline-success(@click="btnCardImport") #[i.fa.fa-fw.fa-file-code-o] Import - .col.px-1: button.btn.btn-block.btn-outline-primary(@click="btnCardExport") #[i.fa.fa-fw.fa-floppy-o] Export - button.btn.btn-sm.btn-block.btn-outline-danger.mb-2(@click="btnCardReset") #[i.fa.mr-1.fa-repeat] Reset to empty dump + .col.px-1: button.btn.btn-block.btn-outline-success(@click="btnImportDump") #[i.fa.fa-fw.fa-file-code-o] Import + .col.px-1: button.btn.btn-block.btn-outline-primary(@click="btnExportDump") #[i.fa.fa-fw.fa-floppy-o] Export + button.btn.btn-sm.btn-block.btn-outline-danger.mb-2(@click="btnResetDump") #[i.fa.mr-1.fa-repeat] Reset to empty dump .card-body.px-3.pt-3.pb-2.letter-spacing-n1px.border-top h6.card-title Anit Collision .input-group.input-group-sm.mb-2.was-validated @@ -125,8 +125,8 @@ block content .input-group-prepend: label.input-group-text.justify-content-center.flex-column(style="width: 2rem", :for="`i-toggle-${i}`") input.my-2(type="checkbox", v-model="ss.toggle[i]", :id="`i-toggle-${i}`") span {{ `0${i}`.slice(-2) }} - textarea.form-control(rows="4", v-model="ss.body[i]", :class="isValidBlock(ss.body[i]) ? 'is-valid' : 'is-invalid'") - .modal.fade(tabindex="-1", ref="cardExport") + textarea.form-control(rows="4", v-model="ss.dump[i]", :class="isValidBlock(ss.dump[i]) ? 'is-valid' : 'is-invalid'") + .modal.fade(tabindex="-1", ref="dumpExport") .modal-dialog.modal-dialog-centered.modal-xl .modal-content .modal-header @@ -134,57 +134,55 @@ block content button.close(type="button", data-dismiss="modal") #[span ×] .modal-body a.btn.btn-block.btn-outline-primary.mb-2.text-left( - :download="cardExport.json.download", - :href="cardExport.json.href", + :download="dumpExport.json.download", + :href="dumpExport.json.href", target="_blank") - .my-1 {{ cardExport.json.download }} + .my-1 {{ dumpExport.json.download }} h6.text-muted.mb-1 Click to download as JSON format. This format can be used in Proxmark3 and Chameleon Mini GUI. a.btn.btn-block.btn-outline-primary.text-left( - :download="cardExport.bin.download", - :href="cardExport.bin.href", + :download="dumpExport.bin.download", + :href="dumpExport.bin.href", target="_blank") - .my-1 {{ cardExport.bin.download }} + .my-1 {{ dumpExport.bin.download }} h6.text-muted.mb-1 Click to download as BIN format. This format can be used in Proxmark3, libnfc, mfoc... a.btn.btn-block.btn-outline-primary.text-left( - :download="cardExport.eml.download", - :href="cardExport.eml.href", + :download="dumpExport.eml.download", + :href="dumpExport.eml.href", target="_blank") - .my-1 {{ cardExport.eml.download }} + .my-1 {{ dumpExport.eml.download }} h6.text-muted.mb-1 Click to download as EML format. This format can be used in Proxmark3 emulator. a.btn.btn-block.btn-outline-primary.text-left( - :download="cardExport.mct.download", - :href="cardExport.mct.href", + :download="dumpExport.mct.download", + :href="dumpExport.mct.href", target="_blank") - .my-1 {{ cardExport.mct.download }} + .my-1 {{ dumpExport.mct.download }} h6.text-muted.mb-1 Click to download as MCT format. This format can be used in Mifare Classic Tool. block script script(crossorigin="anonymous", src="https://cdn.jsdelivr.net/npm/joi@17/dist/joi-browser.min.js") script. - const { AnimationMode, Buffer, ButtonAction, ChameleonDebug, ChameleonUltra, DeviceMode, FreqType, TagType, WebbleAdapter, WebserialAdapter } = ChameleonUltraJS // eslint-disable-line - const ultraUsb = new ChameleonUltra(true) + const { Buffer, ChameleonDebug, ChameleonUltra, DeviceMode, FreqType, TagType, WebbleAdapter, WebserialAdapter } = window.ChameleonUltraJS + const ultraUsb = new ChameleonUltra() ultraUsb.use(new ChameleonDebug()) ultraUsb.use(new WebserialAdapter()) - const ultraBle = new ChameleonUltra(true) + const ultraBle = new ChameleonUltra() ultraBle.use(new ChameleonDebug()) ultraBle.use(new WebbleAdapter()) - const WELL_KNOWN_KEYS = ['ffffffffffff', 'a0a1a2a3a4a5', 'd3f7d3f7d3f7'] + const toHex = buf => _.toUpper(buf.toString('hex')) + const WELL_KNOWN_KEYS = ['FFFFFFFFFFFF', 'A0A1A2A3A4A5', 'D3F7D3F7D3F7'] - function getEmptyCardBody () { - const blkDefault = { - factory: 'deadbeef220804000177a2cc35afa51d', - empty: '00000000000000000000000000000000', - acl: 'ffffffffffffff078069ffffffffffff', - } - return _.times(16, secNo => { - const blocks = _.times(4, blkNo => { - if (secNo === 0 && blkNo === 0) return blkDefault.factory - else if (blkNo === 3) return blkDefault.acl - else return blkDefault.empty - }) - return blocks.join('\n') - }) + function getEmptyDump () { + const buf = new Buffer(1024) + const blkFactory = Buffer.from('DEADBEEF220804000177A2CC35AFA51D', 'hex') + const blkAcl = Buffer.from('FFFFFFFFFFFFFF078069FFFFFFFFFFFF', 'hex') + buf.set(blkFactory, 0) // block 0 + for (let i = 0; i < 16; i++) buf.set(blkAcl, i * 64 + 48) // block 4n+3 + return buf + } + + function toSectorsHex (dump) { + return _.map(dump.chunk(64), sectorDump => _.map(sectorDump.chunk(16), toHex).join('\n')) } window.vm = new Vue({ @@ -197,7 +195,7 @@ block script antiColl: 0, atqa: '0004', ats: '', - body: getEmptyCardBody(), + dump: toSectorsHex(getEmptyDump()), detection: 0, gen1a: 0, gen2: 0, @@ -206,11 +204,11 @@ block script sak: '08', slot: 0, toggle: _.times(16, () => true), - uid: 'deadbeef', + uid: 'DEADBEEF', write: 0, }, - cardImport: { cb: null }, - cardExport: { + dumpImport: { cb: null }, + dumpExport: { json: { download: '', href: '' }, bin: { download: '', href: '' }, eml: { download: '', href: '' }, @@ -243,18 +241,19 @@ block script }, async btnEmuRead () { const { ultra } = this + const { slot } = this.ss try { this.showLoading({ text: 'Loading emulator' }) - await ultra.cmdSlotSetActive(this.ss.slot) + await ultra.cmdSlotSetActive(slot) this.$set(this, 'ss', { ...this.ss, - name: await ultra.cmdSlotGetFreqName(this.ss.slot, FreqType.HF), + name: await ultra.cmdSlotGetFreqName(slot, FreqType.HF), ..._.mapValues(await ultra.cmdMf1GetEmuSettings(), _.toInteger), // antiColl, detection, gen1a, gen2, write }) this.mfCardSetAntiColl(await ultra.cmdHf14aGetAntiCollData()) for (let i = 0; i < 16; i++) { if (!this.ss.toggle[i]) continue - this.mfCardSetSector(i, await ultra.cmdMf1EmuReadBlock(i << 2, 4)) + this.mfSetSectorDump(i, await ultra.cmdMf1EmuReadBlock(i << 2, 4)) } await Swal.fire({ icon: 'success', title: 'Load success' }) } catch (err) { @@ -264,29 +263,35 @@ block script }, async btnEmuWrite () { const { ultra } = this + const { antiColl, atqa, ats, detection, dump, gen1a, gen2, name, sak, slot, toggle, uid, write } = this.ss try { - const msg1 = `Slot ${this.ss.slot + 1} will be REPLACE! Continue?` + this.showLoading({ text: 'Emulating tag...' }) + const slotName = await ultra.cmdSlotGetFreqName(slot, FreqType.HF) ?? '(no name)' + const msg1 = `Slot ${slot + 1} "${slotName}" will be REPLACE! Continue?` if (!await this.confirm(msg1, 'Yes', 'Cancel')) return - this.showLoading({ text: 'Emulating' }) - await ultra.cmdSlotSetActive(this.ss.slot) - const freqName = this.ss.name - if (_.isString(freqName) && freqName.length > 0) await ultra.cmdSlotSetFreqName(this.ss.slot, FreqType.HF, freqName) - await ultra.cmdMf1SetAntiCollMode(this.ss.antiColl) - await ultra.cmdMf1SetDetectionEnable(this.ss.detection) - await ultra.cmdMf1SetGen1aMode(this.ss.gen1a) - await ultra.cmdMf1SetGen2Mode(this.ss.gen2) - await ultra.cmdMf1SetWriteMode(this.ss.write) + this.showLoading({ text: 'Emulating tag...' }) + await ultra.cmdChangeDeviceMode(DeviceMode.TAG) + await ultra.cmdSlotChangeTagType(slot, TagType.MIFARE_1024) + await ultra.cmdSlotResetTagType(slot, TagType.MIFARE_1024) + await ultra.cmdSlotSetEnable(slot, FreqType.HF, true) + if (_.isString(name) && name.length > 0) await ultra.cmdSlotSetFreqName(slot, FreqType.HF, name) + await ultra.cmdMf1SetAntiCollMode(antiColl) + await ultra.cmdMf1SetDetectionEnable(detection) + await ultra.cmdMf1SetGen1aMode(gen1a) + await ultra.cmdMf1SetGen2Mode(gen2) + await ultra.cmdMf1SetWriteMode(write) await ultra.cmdHf14aSetAntiCollData({ - atqa: Buffer.from(this.ss.atqa, 'hex').reverse(), - ats: Buffer.from(this.ss.ats, 'hex'), - sak: Buffer.from(this.ss.sak, 'hex'), - uid: Buffer.from(this.ss.uid, 'hex'), + atqa: Buffer.from(atqa, 'hex').reverse(), + ats: Buffer.from(ats, 'hex'), + sak: Buffer.from(sak, 'hex'), + uid: Buffer.from(uid, 'hex'), }) for (let i = 0; i < 16; i++) { - if (!this.ss.toggle[i]) continue - const sectorData = Buffer.from(this.ss.body[i], 'hex') + if (!toggle[i]) continue + const sectorData = Buffer.from(dump[i], 'hex') await ultra.cmdMf1EmuWriteBlock(i << 2, sectorData) } + await ultra.cmdSlotSetActive(slot) await Swal.fire({ icon: 'success', title: 'Emulate success' }) } catch (err) { ultra.emitter.emit('error', err) @@ -295,15 +300,19 @@ block script }, async btnEmuReset () { const { ultra } = this + const { slot } = this.ss try { - const msg1 = `Slot ${this.ss.slot + 1} will be RESET! Continue?` + this.showLoading({ text: 'Resetting...' }) + const slotName = await ultra.cmdSlotGetFreqName(slot, FreqType.HF) ?? '(no name)' + const msg1 = `Slot ${slot + 1} "${slotName}" will be RESET! Continue?` if (!await this.confirm(msg1, 'Yes', 'Cancel')) return - this.showLoading({ text: 'Resetting' }) + this.showLoading({ text: 'Resetting...' }) + await ultra.cmdChangeDeviceMode(DeviceMode.TAG) // reset slot - await ultra.cmdSlotChangeTagType(this.ss.slot, TagType.MIFARE_1024) - await ultra.cmdSlotResetTagType(this.ss.slot, TagType.MIFARE_1024) - await ultra.cmdSlotSetEnable(this.ss.slot, FreqType.HF, true) - await ultra.cmdSlotSetActive(this.ss.slot) + await ultra.cmdSlotChangeTagType(slot, TagType.MIFARE_1024) + await ultra.cmdSlotResetTagType(slot, TagType.MIFARE_1024) + await ultra.cmdSlotSetEnable(slot, FreqType.HF, true) + await ultra.cmdSlotSetActive(slot) await Swal.fire({ icon: 'success', title: 'Reset success' }) } catch (err) { ultra.emitter.emit('error', err) @@ -320,7 +329,7 @@ block script this.mfCardSetAntiColl(_.first(await ultra.cmdHf14aScan())) for (let i = 0; i < 16; i++) { if (!this.ss.toggle[i]) continue - this.mfCardSetSector(i, await ultra.mf1Gen1aReadBlocks(i << 2, 4)) + this.mfSetSectorDump(i, await ultra.mf1Gen1aReadBlocks(i << 2, 4)) this.showLoading(genSwalCfg(i + 1)) } await ultra.cmdChangeDeviceMode(DeviceMode.TAG) @@ -341,7 +350,7 @@ block script this.showLoading(genSwalCfg(0)) for (let i = 0; i < 16; i++) { if (!this.ss.toggle[i]) continue - const sectorData = Buffer.from(this.ss.body[i], 'hex') + const sectorData = Buffer.from(this.ss.dump[i], 'hex') await ultra.mf1Gen1aWriteBlocks(i << 2, sectorData) this.showLoading(genSwalCfg(i + 1)) } @@ -366,7 +375,7 @@ block script try { if (!this.ss.toggle[i]) continue const { data: sectorData, success } = await ultra.mf1ReadSectorByKeys(i, keys) - this.mfCardSetSector(i, sectorData) + this.mfSetSectorDump(i, sectorData) for (let j = 0; j < 4; j++) if (!success[j]) failed.push(i * 4 + j) // reorder keys: found key will be put to first of keys for (const key1 of _.map([48, 58], i => sectorData.subarray(i, i + 6))) { @@ -374,6 +383,7 @@ block script if (idx >= 0) keys.unshift(...keys.splice(idx, 1)) } } catch (err) { + if (!ultra.isConnected()) throw err for (let j = 0; j < 4; j++) failed.push(i * 4 + j) ultra.emitter.emit('error', err) } @@ -401,10 +411,11 @@ block script for (let i = 0; i < 16; i++) { try { if (!this.ss.toggle[i]) continue - const sectorData = Buffer.from(this.ss.body[i], 'hex') + const sectorData = Buffer.from(this.ss.dump[i], 'hex') const { success } = await ultra.mf1WriteSectorByKeys(i, keys, sectorData) for (let j = 0; j < 4; j++) if (!success[j]) failed.push(i * 4 + j) } catch (err) { + if (!ultra.isConnected()) throw err for (let j = 0; j < 4; j++) failed.push(i * 4 + j) ultra.emitter.emit('error', err) } @@ -427,9 +438,9 @@ block script let keys = [] for (let i = 0; i < 16; i++) { if (!this.ss.toggle[i]) continue - const sectorData = Buffer.from(this.ss.body[i], 'hex') + const sectorData = Buffer.from(this.ss.dump[i], 'hex') if (sectorData.length !== 64) continue - keys.push(..._.map([48, 58], offset => sectorData.subarray(offset, offset + 6).toString('hex'))) + keys.push(..._.map([48, 58], offset => toHex(sectorData.subarray(offset, offset + 6)))) } keys = _.uniq(keys) if (keys.length === 0) throw new Error('No keys found') @@ -445,12 +456,12 @@ block script if (!await this.confirm(msg1, 'Yes', 'Cancel')) return this.$set(this.ss, 'keys', WELL_KNOWN_KEYS.join('\n')) }, - async btnCardImport () { + async btnImportDump () { const { ultra } = this try { const file = await new Promise(resolve => { - this.$set(this.cardImport, 'cb', tmpFile => { if (!_.isNil(tmpFile)) resolve(tmpFile) }) - const $ref = this.$refs.cardImport + this.$set(this.dumpImport, 'cb', tmpFile => { if (!_.isNil(tmpFile)) resolve(tmpFile) }) + const $ref = this.$refs.dumpImport $ref.value = '' $ref.click() }) @@ -462,17 +473,17 @@ block script const buf = new Buffer(await file.arrayBuffer()) switch (ext) { case 'bin': - await this.btnCardImportBin(file, buf) + await this.btnImportDumpBin(file, buf) break case 'json': case 'json5': - await this.btnCardImportJson(file, buf) + await this.btnImportDumpJson(file, buf) break case 'eml': - await this.btnCardImportEml(file, buf) + await this.btnImportDumpEml(file, buf) break case 'mct': - await this.btnCardImportMct(file, buf) + await this.btnImportDumpMct(file, buf) break default: throw new Error(`Unsupported file extension: ${ext}`) @@ -483,42 +494,35 @@ block script await Swal.fire({ icon: 'error', title: 'Import failed', text: err.message }) } }, - async btnCardImportBin (file, buf) { + async btnImportDumpBin (file, buf) { if (file.size !== 1024) throw new Error(`Invalid file size: ${file.size} bytes`) - for (let i = 0; i < 16; i++) { - const sectorData = buf.subarray(i * 64, (i + 1) * 64) - this.$set(this.ss.body, i, _.map(sectorData.chunk(16), chunk => chunk.toString('hex')).join('\n')) - } + _.each(buf.chunk(64), (sectorDump, i) => this.mfSetSectorDump(i, sectorDump)) }, - async btnCardImportJson (file, buf) { + async btnImportDumpJson (file, buf) { const json = JSON5.parse(buf.toString('utf8')) if (json.FileType !== 'mfcard') throw new Error(`Invalid file type: ${json.FileType}`) - if (!_.isNil(json?.Card?.UID)) this.$set(this.ss, 'uid', _.toLower(json.Card.UID)) - if (!_.isNil(json?.Card?.ATQA)) this.$set(this.ss, 'atqa', Buffer.from(json.Card.ATQA, 'hex').reverse().toString('hex')) - if (!_.isNil(json?.Card?.SAK)) this.$set(this.ss, 'sak', json.Card.SAK) + if (!_.isNil(json?.Card?.UID)) this.$set(this.ss, 'uid', toHex(Buffer.from(json.Card.UID, 'hex'))) + if (!_.isNil(json?.Card?.ATQA)) this.$set(this.ss, 'atqa', toHex(Buffer.from(json.Card.ATQA, 'hex').reverse())) + if (!_.isNil(json?.Card?.SAK)) this.$set(this.ss, 'sak', toHex(Buffer.from(json.Card.SAK, 'hex'))) if (!_.isNil(json?.blocks)) { for (let i = 0; i < 16; i++) { - const sectorData = new Buffer(64) + const sectorDump = new Buffer(64) for (let j = 0; j < 4; j++) { - const blockhex = json?.blocks?.[i * 4 + j] ?? '' - if (blockhex.length !== 32) continue - const blockbuf = Buffer.from(blockhex.replaceAll('-', '0'), 'hex') - if (blockbuf.length !== 16) continue - blockbuf.copy(sectorData, j * 16) + const blkHex = (json?.blocks?.[i * 4 + j] ?? '').replaceAll('-', '0') + const blkBuf = Buffer.from(blkHex, 'hex') + if (blkBuf.length !== 16) continue + blkBuf.copy(sectorDump, j * 16) } - this.mfCardSetSector(i, sectorData) + this.mfSetSectorDump(i, sectorDump) } } }, - async btnCardImportEml (file, buf) { + async btnImportDumpEml (file, buf) { buf = Buffer.from(buf.toString('utf8').replaceAll('-', '0'), 'hex') if (buf.length !== 1024) throw new Error(`Invalid eml size: ${buf.length} bytes`) - for (let i = 0; i < 16; i++) { - const sectorData = buf.subarray(i * 64, (i + 1) * 64) - this.$set(this.ss.body, i, _.map(sectorData.chunk(16), chunk => chunk.toString('hex')).join('\n')) - } + _.each(buf.chunk(64), (sectorDump, i) => this.mfSetSectorDump(i, sectorDump)) }, - async btnCardImportMct (file, buf) { + async btnImportDumpMct (file, buf) { const rows = buf.toString('utf8').split(/\r?\n/) buf = new Buffer(1024) let blockNo = 0 @@ -533,65 +537,62 @@ block script blockNo++ } } - for (let i = 0; i < 16; i++) this.mfCardSetSector(i, buf.subarray(i * 64)) + _.each(buf.chunk(64), (sectorDump, i) => this.mfSetSectorDump(i, sectorDump)) }, - async btnCardExport () { - const card = new Buffer(1024) + async btnExportDump () { + const dump = new Buffer(1024) for (let i = 0; i < 16; i++) { - const sectorData = Buffer.from(this.ss.body[i], 'hex') + const sectorData = Buffer.from(this.ss.dump[i], 'hex') if (sectorData.length !== 64) continue // skip invalid sector - sectorData.copy(card, i * 64) + sectorData.copy(dump, i * 64) } - const uid = Buffer.from(this.ss.uid, 'hex') - const atqa = Buffer.from(this.ss.atqa, 'hex').reverse() - const sak = Buffer.from(this.ss.sak, 'hex') - - // helper - const toHex = buf => _.toUpper(buf.toString('hex')) + const uid = toHex(Buffer.from(this.ss.uid, 'hex')) + const atqa = toHex(Buffer.from(this.ss.atqa, 'hex').reverse()) + const sak = toHex(Buffer.from(this.ss.sak, 'hex')) // json const json = { Created: 'chameleon-ultra.js', FileType: 'mfcard', Card: { - UID: toHex(uid), - ATQA: toHex(atqa), - SAK: toHex(sak), + UID: uid, + ATQA: atqa, + SAK: sak, }, - blocks: _.fromPairs(_.times(64, i => [i, toHex(card.subarray(i * 16, i * 16 + 16))])), + blocks: _.fromPairs(_.entries(_.map(dump.chunk(16), toHex))), } - this.$set(this.cardExport, 'json', { + this.$set(this.dumpExport, 'json', { href: URL.createObjectURL(new Blob([JSON.stringify(json, null, 2)], { type: 'application/octet-stream' })), - download: `hf-mf-${toHex(uid)}.json`, + download: `hf-mf-${uid}.json`, }) // bin - this.$set(this.cardExport, 'bin', { - href: URL.createObjectURL(new Blob([card], { type: 'application/octet-stream' })), - download: `hf-mf-${toHex(uid)}.bin`, + this.$set(this.dumpExport, 'bin', { + href: URL.createObjectURL(new Blob([dump], { type: 'application/octet-stream' })), + download: `hf-mf-${uid}.bin`, }) // eml - const eml = _.map(card.chunk(16), b => toHex(b)).join('\n') - this.$set(this.cardExport, 'eml', { + const eml = _.map(dump.chunk(16), toHex).join('\n') + this.$set(this.dumpExport, 'eml', { href: URL.createObjectURL(new Blob([eml], { type: 'application/octet-stream' })), - download: `hf-mf-${toHex(uid)}.eml`, + download: `hf-mf-${uid}.eml`, }) // mct - const mct = _.map(card.chunk(64), (sector, sectorNo) => { - return `+Sector: ${sectorNo}\n${_.map(sector.chunk(16), b => toHex(b)).join('\n')}` + const mct = _.map(dump.chunk(64), (sector, sectorNo) => { + return `+Sector: ${sectorNo}\n${_.map(sector.chunk(16), toHex).join('\n')}` }).join('\n') - this.$set(this.cardExport, 'mct', { + this.$set(this.dumpExport, 'mct', { href: URL.createObjectURL(new Blob([mct], { type: 'application/octet-stream' })), - download: `hf-mf-${toHex(uid)}.mct`, + download: `hf-mf-${uid}.mct`, }) await new Promise(resolve => this.$nextTick(resolve)) // wait for DOM update - const $ref = window.jQuery(this.$refs.cardExport) + const $ref = window.jQuery(this.$refs.dumpExport) $ref.modal('show') }, - async btnCardReset () { + async btnResetDump () { const { ultra } = this try { const msg1 = 'Mifare dump will be RESET! Continue?' @@ -601,7 +602,7 @@ block script atqa: '0004', sak: '08', ats: '', - body: getEmptyCardBody(), + dump: toSectorsHex(getEmptyDump()), toggle: _.times(16, () => true), } this.$set(this, 'ss', { ...this.ss, ...tmp }) @@ -635,14 +636,14 @@ block script this.$set(this.ss, k, buf.toString('hex')) } }, - mfCardSetSector (sectorNo, sectorData) { - const sectorEml = _.map(sectorData.chunk(16), block => block.toString('hex')).join('\n') - this.$set(this.ss.body, sectorNo, sectorEml) + mfSetSectorDump (sectorNo, sectorData) { + const sectorEml = _.map(sectorData.chunk(16), toHex).join('\n') + this.$set(this.ss.dump, sectorNo, sectorEml) }, mfCardGetKeys () { const keys = _.chain(Buffer.from(this.ss.keys, 'hex').chunk(6)) .filter(key => Buffer.isBuffer(key) && key.length === 6) - .uniqBy(key => key.toString('hex')) + .uniqWith(Buffer.equals) .value() if (keys.length === 0) throw new Error('No keys found') return keys diff --git a/pug/src/test.pug b/pug/src/test.pug index 7f7f603..20d36a4 100644 --- a/pug/src/test.pug +++ b/pug/src/test.pug @@ -24,11 +24,11 @@ block script script(crossorigin="anonymous", src="https://cdn.jsdelivr.net/npm/vconsole@3/dist/vconsole.min.js") script. window.vConsole = new window.VConsole() - const { Buffer, ChameleonDebug, ChameleonUltra, Debug, WebbleAdapter, WebserialAdapter } = ChameleonUltraJS // eslint-disable-line - const ultraUsb = new ChameleonUltra(true) + const { Buffer, ChameleonDebug, ChameleonUltra, WebbleAdapter, WebserialAdapter } = window.ChameleonUltraJS // eslint-disable-line no-unused-vars, @typescript-eslint/no-unused-vars + const ultraUsb = new ChameleonUltra() ultraUsb.use(new ChameleonDebug()) ultraUsb.use(new WebserialAdapter()) - const ultraBle = new ChameleonUltra(true) + const ultraBle = new ChameleonUltra() ultraBle.use(new ChameleonDebug()) ultraBle.use(new WebbleAdapter()) diff --git a/src/ChameleonUltra.ts b/src/ChameleonUltra.ts index 1f94f50..95cda1b 100644 --- a/src/ChameleonUltra.ts +++ b/src/ChameleonUltra.ts @@ -316,11 +316,14 @@ export class ChameleonUltra { const respGenerator = new EventAsyncGenerator() this.emitter.on('resp', respGenerator.onData) this.emitter.once('disconnected', respGenerator.onClose) - let timeout: any | undefined + let timeout: NodeJS.Timeout | undefined respGenerator.removeCallback = () => { this.emitter.removeListener('resp', respGenerator.onData) this.emitter.removeListener('disconnected', respGenerator.onClose) - if (!_.isNil(timeout)) clearTimeout(timeout) + if (!_.isNil(timeout)) { + clearTimeout(timeout) + timeout = undefined // prevent memory leak: https://lucumr.pocoo.org/2024/6/5/node-timeout/ + } } return async () => { timeout = setTimeout(() => {