From a00b9f4a7afb60cbc9b0462bac7eca76a46f826b Mon Sep 17 00:00:00 2001 From: taichunmin Date: Mon, 18 Nov 2024 11:42:19 +0800 Subject: [PATCH] mfkey32 skip records by known keys --- pages/demos.md | 4 ++-- pug/src/mfkey32.pug | 18 ++++++++++++++++-- pug/src/mifare1k.pug | 2 +- src/ChameleonUltra.ts | 2 +- src/Crypto1.test.ts | 14 +++----------- src/Crypto1.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 63 insertions(+), 17 deletions(-) diff --git a/pages/demos.md b/pages/demos.md index 10870f0..7ac7eee 100644 --- a/pages/demos.md +++ b/pages/demos.md @@ -122,9 +122,9 @@ A tool for Xiaomi Watch to clone encrypted Mifare Classic tag. ## [mifare-value.html](https://taichunmin.idv.tw/chameleon-ultra.js/mifare-value.html) -在台灣有些 MIFARE Classic 卡片使用 value block 來儲存卡片的餘額,但是有些中國魔術卡不支援 value block 指令,所以無法使用這些魔術卡來複製 MIFARE Classic 卡片。這個工具可以讓你測試卡片是否支援 value block 指令。 +在台灣有些系統會使用 MIFARE Classic 卡片的 value block 來儲存餘額,value block 的 increment/decrement/restore 指令的資料是由 2 個部分所組成,在第 2 部分傳送完成後卡片不會回傳 ACK,所以讀卡機如果在 Timeout 之前沒有收到 NACK,就可以視為執行成功並繼續執行下一個指令。但有些魔術卡需要更多時間來完成指令,否則就會執行失敗,這個工具可以讓你測試卡片是否能夠成功執行 value block 的指令。 -A ChameleonUltra tool for MIFARE Classic value block commands. Some MIFARE Classic cards in Taiwan are using value block to store the balance of the card. But some chinese magic cards didn't support value block commands. So you can't use these magic cards to clone original MIFARE Classic cards. This tool can help you to test whether the card support value block commands or not. +In Taiwan, some systems use the value block of MIFARE Classic cards to store balances. The data for the value block's increment/decrement/restore commands consists of two parts. After the second part is transmitted, the card will not return ACK. Therefore, if the reader does not receive a NACK before timeout, it can be considered successful and proceed to the next command. However, some magic cards needs more time to complete the value block's command, otherwise the command will fail. This tool allows you to test whether the card can successfully execute the value block commands. ![](https://i.imgur.com/jJ3pNvn.png) diff --git a/pug/src/mfkey32.pug b/pug/src/mfkey32.pug index 69ba87d..fc32a60 100644 --- a/pug/src/mfkey32.pug +++ b/pug/src/mfkey32.pug @@ -260,17 +260,31 @@ block script currentProgressStep: 2, text: `Recover key: ${recoverCnt} / ${recoverMax}`, })) + recoverCnt++ const r1 = grp[j] + if (r0.skip || r1.skip) continue // skip by known key try { + const key1 = this.mfkey32v2(r0.uid, r0, r1) this.$set(this.ss, 'detects', _.unionWith(this.ss.detects, [{ block: r0.block, keyType: +r0.isKeyB, - key: this.mfkey32v2(r0.uid, r0, r1), + key: key1, }], _.isEqual)) + r0.skip = r1.skip = true + for (const k = 0; k < grp.length; k++) { + if (grp[k].skip) continue + const isReaderHasKey = Crypto1.mfkey32IsReaderHasKey({ + uid: grp[k].uid, + nt: grp[k].nt, + nr: grp[k].nr, + ar: grp[k].ar, + key: key1, + }) + if (isReaderHasKey) grp[k].skip = true + } } catch (err) { ultra.emitter.emit('error', err) } - recoverCnt++ await this.sleep(0) // 等待 UI 更新 } } diff --git a/pug/src/mifare1k.pug b/pug/src/mifare1k.pug index c0188a6..05bd26f 100644 --- a/pug/src/mifare1k.pug +++ b/pug/src/mifare1k.pug @@ -293,7 +293,7 @@ block script const sectorData = Buffer.from(dump[i], 'hex') await ultra.cmdMf1EmuWriteBlock(i << 2, sectorData) } - await this.cmdSlotSaveSettings() + await ultra.cmdSlotSaveSettings() await this.swalFire({ icon: 'success', title: 'Emulate success' }) } catch (err) { ultra.emitter.emit('error', err) diff --git a/src/ChameleonUltra.ts b/src/ChameleonUltra.ts index 1c79405..a6d8883 100644 --- a/src/ChameleonUltra.ts +++ b/src/ChameleonUltra.ts @@ -3522,7 +3522,7 @@ export class ChameleonUltra { await onChunkKeys?.({ keys: chunkKeys, mask }) const tmp = await this.cmdMf1CheckKeysOfSectors({ keys: chunkKeys, mask }) if (_.isNil(tmp)) break // all founded - for (let i = 0; i < 10; i++) mask[i] |= tmp.found[i] + mask.or(tmp.found) for (let i = 0; i < maxSectors * 2; i++) { if (_.isNil(tmp.sectorKeys[i])) continue foundKeys[i] = tmp.sectorKeys[i] diff --git a/src/Crypto1.test.ts b/src/Crypto1.test.ts index b670dc5..9b49a5b 100644 --- a/src/Crypto1.test.ts +++ b/src/Crypto1.test.ts @@ -275,15 +275,7 @@ test.each([ { uid: '65535D33', key: '974C262B9278', nt: 'BE2B7B5D', nrEnc: 'B1E1B891', arEnc: '2CF7A248' }, { uid: '65535D33', key: 'A9AC67832330', nt: '2C198BE4', nrEnc: 'FEDAC6D2', arEnc: 'CF0A3C7E' }, { uid: '65535D33', key: 'A9AC67832330', nt: 'F73E638F', nrEnc: '4F4F867A', arEnc: '18CCB40B' }, -] as const)('tag send nt and use key to verify nrEnc and arEnc', async ({ uid, key, nt, nrEnc, arEnc }) => { - const tag: any = {} - _.merge(tag, _.mapValues({ uid, nt, nrEnc, arEnc }, hex => Buffer.from(hex, 'hex').readUInt32BE(0))) - tag.state = new Crypto1() - tag.state.setLfsr(Buffer.from(key, 'hex').readUIntBE(0, 6)) - tag.ks0 = tag.state.lfsrWord(tag.uid ^ tag.nt, 0) - tag.ks1 = tag.state.lfsrWord(tag.nrEnc, 1) - tag.ks2 = tag.state.lfsrWord(0, 0) - tag.ar = (tag.ks2 ^ tag.arEnc) >>> 0 - const expected = Crypto1.prngSuccessor(tag.nt, 64) - expect(tag.ar).toEqual(expected) +] as const)('.mfkey32IsReaderHasKey()', async ({ uid, key, nt, nrEnc, arEnc }) => { + const actual = Crypto1.mfkey32IsReaderHasKey({ uid, nt, nr: nrEnc, ar: arEnc, key: Buffer.from(key, 'hex') }) + expect(actual).toEqual(true) }) diff --git a/src/Crypto1.ts b/src/Crypto1.ts index 88e8afd..4dd277e 100644 --- a/src/Crypto1.ts +++ b/src/Crypto1.ts @@ -766,6 +766,46 @@ export default class Crypto1 { throw new Error('failed to recover key') } + /** + * A method for Tag to validate Reader has the correct key. + * @param opts.ar - The encrypted prng successor of `opts.nt`. + * @param opts.key - The 6-bytes key to be test. + * @param opts.nr - The encrypted nonce from reader. + * @param opts.nt - The nonce from tag. + * @param opts.uid - The 4-bytes uid of tag. + * @example + * ```js + * const { Buffer } = await import('https://cdn.jsdelivr.net/npm/chameleon-ultra.js@0/+esm') + * const { default: Crypto1 } = await import('https://cdn.jsdelivr.net/npm/chameleon-ultra.js@0/dist/Crypto1.mjs/+esm') + * + * console.log(Crypto1.mfkey32IsReaderHasKey({ + * ar: '5C7C6F89', + * key: 'A9AC67832330', + * nr: 'CF0A3C7E', + * nt: '2C198BE4', + * uid: '65535D33', + * }).toString('hex')) // true + * ``` + */ + static mfkey32IsReaderHasKey (opts: { + ar: UInt32Like + key: Buffer + nr: UInt32Like + nt: UInt32Like + uid: UInt32Like + }): boolean { + if (!Buffer.isBuffer(opts.key) || opts.key.length !== 6) throw new TypeError('invalid opts.key') + const { castToUint32, prngSuccessor, toUint32 } = Crypto1 + const tag: Record = { state: new Crypto1() } + ;[tag.uid, tag.nt, tag.nrEnc, tag.arEnc] = _.map(['uid', 'nt', 'nr', 'ar'] as const, k => castToUint32(opts[k])) + tag.state.setLfsr(opts.key.readUIntBE(0, 6)) + tag.ks0 = tag.state.lfsrWord(tag.uid ^ tag.nt, 0) + tag.ks1 = tag.state.lfsrWord(tag.nrEnc, 1) + tag.ks2 = tag.state.lfsrWord(0, 0) + tag.ar = toUint32(tag.ks2 ^ tag.arEnc) + return tag.ar === prngSuccessor(tag.nt, 64) + } + /** * Recover the key with the successfully authentication between the reader and the tag. * @param opts -