Skip to content

Commit

Permalink
mfkey32 skip records by known keys
Browse files Browse the repository at this point in the history
  • Loading branch information
taichunmin committed Nov 19, 2024
1 parent 6f7e92a commit 85f19eb
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 28 deletions.
4 changes: 2 additions & 2 deletions pages/demos.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion pug/src/hf14a-scanner.pug
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ block script
async btnCopy (text, container = null) {
if (!container) container = document.body
const dom = document.createElement('textarea')
dom.value = text
dom.value = text = `${text}`
container.appendChild(dom)
dom.select()
dom.setSelectionRange(0, 1e6) // For mobile devices
Expand Down
31 changes: 22 additions & 9 deletions pug/src/mfkey32.pug
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ block script
await ultra.cmdHf14aSetAntiCollData(tag)
await ultra.cmdMf1SetDetectionEnable(true) // enable detection
const block0 = ChameleonUltra.mf1GenMagicBlock0(tag)
ultra.emitter.emit('debug', 'web', `block0 = ${block0.toString('hex')}`)
ultra.emitter.emit('debug', 'web', `block0 = ${toHex(block0)}`)
await ultra.cmdMf1EmuWriteBlock(0, block0) // set block0
await ultra.cmdSlotSaveSettings()
await this.swalFire({ icon: 'success', title: 'Emulate successfully!' })
Expand Down Expand Up @@ -234,6 +234,7 @@ block script
currentProgressStep: 1,
text: `Retrive logs: ${logs.length} / ${logCnt}`,
}))
await this.sleep(0) // 等待 UI 更新
const tmp = await ultra.cmdMf1GetDetectionLogs(logs.length)
logs.push(...tmp)
}
Expand All @@ -245,9 +246,8 @@ block script
ultra.emitter.emit('debug', 'web', `logs = ${JSON.stringify(logs)}`)

// calc how many mfkey32v2 need to be run
const bufToHex = buf => buf?.toString('hex').toUpperCase()
const blockToSector = block => block < 128 ? block >>> 2 : 24 + (block >>> 4)
logs = _.values(_.groupBy(logs, log => `${bufToHex(log.uid)}-${blockToSector(log.block)}-${'AB'[+log.isKeyB]}`))
logs = _.values(_.groupBy(logs, log => `${toHex(log.uid)}-${blockToSector(log.block)}-${'AB'[+log.isKeyB]}`))
console.log(logs)
const recoverMax = _.sumBy(logs, arr => arr.length * (arr.length - 1) / 2)
this.$set(this.ss, 'detects', [])
Expand All @@ -258,20 +258,33 @@ block script
for (let j = i + 1; j < grp.length; j++) {
this.showLoading(_.merge(swalCfg, {
currentProgressStep: 2,
text: `Recover key: ${recoverCnt} / ${recoverMax}`,
text: `Recover key: ${recoverCnt++} / ${recoverMax}`,
}))
await this.sleep(0) // 等待 UI 更新
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: toHex(key1),
}], _.isEqual))
r0.skip = r1.skip = true
for (let 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 更新
}
}
}
Expand Down Expand Up @@ -302,7 +315,7 @@ block script
async btnCopy (text, container = null) {
if (!container) container = document.body
const dom = document.createElement('textarea')
dom.value = text
dom.value = text = `${text}`
container.appendChild(dom)
dom.select()
dom.setSelectionRange(0, 1e6) // For mobile devices
Expand Down Expand Up @@ -340,7 +353,7 @@ block script
nt1: r1.nt,
nr1: r1.nr,
ar1: r1.ar,
}).toString('hex')
})
},
btnSetTestFromDetect (detect) {
_.merge(this.ss, _.pick(detect, ['block', 'keyType', 'key']))
Expand Down
2 changes: 1 addition & 1 deletion pug/src/mifare-keychain.pug
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ block script
async btnCopy (text, container = null) {
if (!container) container = document.body
const dom = document.createElement('textarea')
dom.value = text
dom.value = text = `${text}`
container.appendChild(dom)
dom.select()
dom.setSelectionRange(0, 1e6) // For mobile devices
Expand Down
2 changes: 1 addition & 1 deletion pug/src/mifare1k.pug
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/ChameleonUltra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
14 changes: 3 additions & 11 deletions src/Crypto1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
47 changes: 45 additions & 2 deletions src/Crypto1.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/**
* @example
* ```js
* import Crypto1 from 'chameleon-ultra.js/Crypto1'
* ```
*/
import { Buffer } from '@taichunmin/buffer'
import _ from 'lodash'
Expand Down Expand Up @@ -53,6 +55,7 @@ const fastfwd = [

/**
* JavaScript implementation of the Crypto1 cipher.
* @see [crypto1.c | RfidResearchGroup/proxmark3](https://github.com/RfidResearchGroup/proxmark3/blob/master/common/crapto1/crypto1.c)
*/
export default class Crypto1 {
/**
Expand Down Expand Up @@ -166,7 +169,7 @@ export default class Crypto1 {
* @returns The lfsr output bit.
*/
lfsrBit (input: number, isEncrypted: number): number {
const { evenParity32, filter, toBool, toUint32 } = Crypto1
const { evenParity32, filter, toBool } = Crypto1
const output = filter(this.odd)

const feedin = (output & toBool(isEncrypted)) ^
Expand All @@ -175,7 +178,7 @@ export default class Crypto1 {
(LF_POLY_EVEN & this.even)

;[this.odd, this.even] = [
toUint32(this.even << 1 | evenParity32(feedin)),
(this.even << 1) & 0xFFFFFF | evenParity32(feedin),
this.odd,
]

Expand Down Expand Up @@ -766,6 +769,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: 'CF0A3C7E',
* key: 'A9AC67832330',
* nr: 'FEDAC6D2',
* 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<string, any> = { 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 -
Expand Down

0 comments on commit 85f19eb

Please sign in to comment.