diff --git a/README b/README index 6ababf4..38848ce 100644 --- a/README +++ b/README @@ -7,8 +7,12 @@ the IO expander and the PN532 are written in Lua. Files: hslockmk2/ - this directory - /code.lua - main Lua source code - /lua-libs/ - necessary Lua libraries + /main.lua - main Lua script + /i2c.lua - I2C functions + /nfc.lua - NFC/PN532 functions + /auth.lua - auth API functions + + /lua-libs/ - other Lua libraries /mips-bin/ - C libraries compiled for MIPS/OpenWRT luai2c.tar.gz - C i2c library luasha2.tar.gz - C sha2 library diff --git a/auth.lua b/auth.lua new file mode 100644 index 0000000..7e6fd3d --- /dev/null +++ b/auth.lua @@ -0,0 +1,53 @@ +require('socket') +local sha2 = require('libsha2') +local https = require('ssl.https') + +q3k.Auth = {} + +-- the PIN is a table like { 1, 2, 3, 4 } +-- the NFC ID is a table like { 0xde, 0xad, 0xbe, 0xef } +local CalculateHash = function(PIN, NFCID) + local PINString = string.format("%i%i%i%i", PIN[1], PIN[2], PIN[3], PIN[4]) + local PINNumber = tonumber(PINString) + local IDString = string.format("%02x%02x%02x%02x", NFCID[4], NFCID[3], NFCID[2], NFCID[1]) + local Source = string.format("%08x:%s", PINNumber, IDString) + return sha2.sha256hex(Source) +end + +local GetUserFromHash = function(Hash) + local Body, Code, Headers, Status = https.request('https://auth.hackerspace.pl/mifare', 'hash=' .. Hash) + if Code ~= 200 then + return nil + end + if #Body > 100 then + -- probably an error code + return nil + end + return Body +end + + +---------------- +-- Public API -- +---------------- + +q3k.Auth.CardStatus = { + -- No such card in the system - disallow + NO_MATCH=1, + -- Card okay - allow + OKAY=2, + -- Card okay, but member should pay soon - allow, but notify + PAYMENT_DUE=3, + -- Card okay, but member is way behing in payments - disallow + PAYMENT_REQUIRED=4 +} + +q3k.Auth.GetCardStatus = function(PIN, NFCID) + local Hash = CalculateHash(PIN, NFCID) + local User = GetUserFromHash(Hash) + + if User == nil then + return { Status = q3k.Auth.CardStatus.NO_MATCH } + end + return { Status = q3k.Auth.CardStatus.OKAY, User = User } +end diff --git a/code.lua b/code.lua deleted file mode 100644 index 818e3c1..0000000 --- a/code.lua +++ /dev/null @@ -1,234 +0,0 @@ --- The MIT License (MIT) --- --- Copyright (c) 2013 Sergiusz 'q3k' Bazański --- --- Permission is hereby granted, free of charge, to any person obtaining a copy --- of this software and associated documentation files (the "Software"), to deal --- in the Software without restriction, including without limitation the rights --- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell --- copies of the Software, and to permit persons to whom the Software is --- furnished to do so, subject to the following conditions: --- --- The above copyright notice and this permission notice shall be included in --- all copies or substantial portions of the Software. --- --- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR --- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, --- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE --- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER --- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, --- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN --- THE SOFTWARE. - --- This is the HSWAW lock mk2. source code. WIP. refucktor badly needed. - -local i2c = require("libluai2c") -require('bit') -require('socket') -require('posix') -local sha2 = require('libsha2') -local https = require('ssl.https') - -q3k = {} -q3k.Config = {} - -q3k.Config.I2CBus = 0 - -q3k.Config.I2CGPIO = 0x40 -q3k.Config.I2CNFC = 0x24 - -q3k.I2CWrite = function(Address, ...) - local Data = {...} - local Bytes = string.format("%02x ", Address) - for _, Byte in pairs(Data) do - Bytes = Bytes .. string.format("%02x ", Byte) - end - local DataString = string.char(unpack(Data)) - return i2c.write(q3k.Config.I2CBus, Address, DataString) -end - -q3k.I2CRead = function(Address, BytesOut, ...) - local Data = {...} - local DataString = string.char(unpack(Data)) - local Status, Data = i2c.read(q3k.Config.I2CBus, Address, BytesOut, DataString) - local Return = {} - - if Status == 0 then - Data:gsub(".", function(c) - Return[#Return+1] = string.byte(c) - end) - return Return - end -end - --- sigh. luaposix on openwrt doesn't have nanosleep() -q3k.Sleep = function(Seconds) - socket.select(nil, nil, Seconds) -end - --- GPIO Functions -q3k.SetupGPIO = function() - -- Set up pins 28, 29, 30, 31 as output - q3k.I2CWrite(q3k.Config.I2CGPIO, 0x0F, 0x55) - q3k.DoorClose() - -- Set up pins 12, 13, 14, 15 as input without pullups - q3k.I2CWrite(q3k.Config.I2CGPIO, 0x0B, 0xAA) - - -- Turn on GPIO Multiplexer - q3k.I2CWrite(q3k.Config.I2CGPIO, 0x04, 0x01) -end - -q3k.DoorOpen = function() - -- Turn on pin 31 (door) - q3k.I2CWrite(q3k.Config.I2CGPIO, 0x3F, 0x01) -end - -q3k.DoorClose = function() - -- Turn off pin 31 (door) - q3k.I2CWrite(q3k.Config.I2CGPIO, 0x3F, 0x00) -end - --- NFC (PN532) functions -local PN532_PREAMBLE = 0x00 -local PN532_STARTCODE1 = 0x00 -local PN532_STARTCODE2 = 0xFF -local PN532_POSTAMBLE = 0x00 - -local PN532_HOSTTOPN532 = 0xD4 -local PN532_PN532TOHOST = 0xD5 - -q3k.NFCIRQRead = function() - local Data = q3k.I2CRead(q3k.Config.I2CGPIO, 1, 0x2C) - if Data then - return Data[1] - else - return 1 - end -end - -q3k.NFCWaitForIRQ = function(Timeout) - local Timeout = Timeout or 5 - local WaitStart = os.time() - while os.time() < WaitStart + Timeout do - local IRQStatus = q3k.NFCIRQRead() - if IRQStatus == 0 then - break - end - q3k.Sleep(0.2) - end - local IRQStatus = q3k.NFCIRQRead() - if IRQStatus ~= 0 then - return false - end - return true -end - -q3k.NFCWriteCommand = function(...) - local Command = {...} - local Checksum = PN532_PREAMBLE + PN532_STARTCODE1 + PN532_STARTCODE2 - local WireCommand = { PN532_PREAMBLE, PN532_STARTCODE1, PN532_STARTCODE2 } - - WireCommand[#WireCommand+1] = (#Command + 1) - WireCommand[#WireCommand+1] = bit.band(bit.bnot(#Command +1) + 1, 0xFF) - - Checksum = Checksum + PN532_HOSTTOPN532 - WireCommand[#WireCommand+1] = PN532_HOSTTOPN532 - - for _, Byte in pairs(Command) do - Checksum = Checksum + Byte - WireCommand[#WireCommand+1] = Byte - end - - WireCommand[#WireCommand+1] = bit.band(bit.bnot(Checksum), 0xFF) - WireCommand[#WireCommand+1] = PN532_POSTAMBLE - - q3k.I2CWrite(q3k.Config.I2CNFC, unpack(WireCommand)) -end - -q3k.NFCSendAndAck = function(...) - q3k.NFCWriteCommand(...) - local IRQArrived = q3k.NFCWaitForIRQ() - if not IRQArrived then - return false - end - local ACK = q3k.I2CRead(q3k.Config.I2CNFC, 8) - if ACK[1] == 1 and ACK[2] == 0 and ACK[3] == 0 and ACK[4] == 255 - and ACK[5] == 0 and ACK[6] == 255 and ACK[7] == 0 then - return true - else - return false - end -end - -q3k.NFCReadFrame = function(Count) - local Bytes = q3k.I2CRead(q3k.Config.I2CNFC, Count+2) - table.remove(Bytes, 1) - table.remove(Bytes, #Bytes) - return Bytes -end - --- Mock until we have a keypad -q3k.ReadPIN = function() - print "[mock] Entering PIN..." - q3k.Sleep(2) - print "[mock] PIN entered." - return { 0, 0, 0, 0 } -end - --- API stuff - --- the PIN is a table like { 1, 2, 3, 4 } --- the NFC ID is a table like { 0xde, 0xad, 0xbe, 0xef } -q3k.CalculateHash = function(PIN, NFCID) - local PINString = string.format("%i%i%i%i", PIN[1], PIN[2], PIN[3], PIN[4]) - local PINNumber = tonumber(PINString) - local IDString = string.format("%02x%02x%02x%02x", NFCID[4], NFCID[3], NFCID[2], NFCID[1]) - local Source = string.format("%08x:%s", PINNumber, IDString) - return sha2.sha256hex(Source) -end - -q3k.GetUserFromHash = function(Hash) - local Body, Code, Headers, Status = https.request('https://auth.hackerspace.pl/mifare', 'hash=' .. Hash) - if Code ~= 200 then - return nil - end - if #Body > 100 then - -- probably an error code - return nil - end - return Body -end - --- debug bytes -local db = function(Data) - local s = "" - for _, Byte in pairs(Data) do - s = s .. string.format("%02x ", Byte) - end - print(s) -end - -local main = function() - q3k.SetupGPIO() - q3k.NFCSendAndAck(0x14, 0x01, 0x14, 0x01) - - while true do - q3k.NFCSendAndAck(0x4A, 1, 0) - if q3k.NFCWaitForIRQ(120) then - print "Card arrived in field" - local Bytes = q3k.NFCReadFrame(20) - if Bytes ~= nil and #Bytes == 20 then - local NFCID = { Bytes[14], Bytes[15], Bytes[16], Bytes[17] } - local Hash = q3k.CalculateHash(q3k.ReadPIN(), NFCID) - local User = q3k.GetUserFromHash(Hash) - if User == nil then - print "FAILED! no such user!" - else - print(string.format("User: %s", User)) - end - end - end - end -end - -main() diff --git a/i2c.lua b/i2c.lua new file mode 100644 index 0000000..dc2fb82 --- /dev/null +++ b/i2c.lua @@ -0,0 +1,34 @@ +local i2c = require('libluai2c') + +---------------- +-- Public API -- +---------------- + +q3k.I2C = {} + +-- Write to „Address” on the I2C bus +q3k.I2C.Write = function(Address, ...) + local Data = {...} + local Bytes = string.format("%02x ", Address) + for _, Byte in pairs(Data) do + Bytes = Bytes .. string.format("%02x ", Byte) + end + local DataString = string.char(unpack(Data)) + return i2c.write(q3k.Config.I2CBus, Address, DataString) +end + +-- Read „BytesOut” bytes from „Address” on the I2C bus +q3k.I2C.Read = function(Address, BytesOut, ...) + local Data = {...} + local DataString = string.char(unpack(Data)) + local Status, Data = i2c.read(q3k.Config.I2CBus, Address, BytesOut, DataString) + local Return = {} + + if Status == 0 then + Data:gsub(".", function(c) + Return[#Return+1] = string.byte(c) + end) + return Return + end +end + diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..5d72d6d --- /dev/null +++ b/main.lua @@ -0,0 +1,91 @@ +-- The MIT License (MIT) +-- +-- Copyright (c) 2013 Sergiusz 'q3k' Bazański +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy +-- of this software and associated documentation files (the "Software"), to deal +-- in the Software without restriction, including without limitation the rights +-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +-- copies of the Software, and to permit persons to whom the Software is +-- furnished to do so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in +-- all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +-- THE SOFTWARE. + +-- This is the HSWAW lock mk2. source code. + +-- System libraries +require('posix') + +-- Configuration +q3k = {} +q3k.Config = {} +q3k.Config.I2CBus = 0 +q3k.Config.I2CGPIO = 0x40 +q3k.Config.I2CNFC = 0x24 + +-- Code libraries +require('i2c') +require('nfc') +require('auth') + +-- GPIO Functions +q3k.SetupGPIO = function() + -- Set up pins 28, 29, 30, 31 as output + q3k.I2C.Write(q3k.Config.I2CGPIO, 0x0F, 0x55) + q3k.DoorClose() + -- Set up pins 12, 13, 14, 15 as input without pullups + q3k.I2C.Write(q3k.Config.I2CGPIO, 0x0B, 0xAA) + + -- Turn on GPIO Multiplexer + q3k.I2C.Write(q3k.Config.I2CGPIO, 0x04, 0x01) +end + +q3k.DoorOpen = function() + -- Turn on pin 31 (door) + q3k.I2C.Write(q3k.Config.I2CGPIO, 0x3F, 0x01) +end + +q3k.DoorClose = function() + -- Turn off pin 31 (door) + q3k.I2C.Write(q3k.Config.I2CGPIO, 0x3F, 0x00) +end + +-- Mock until we have a keypad +q3k.ReadPIN = function() + print "[mock] Entering PIN..." + posix.sleep(2) + print "[mock] PIN entered." + return { 0, 0, 0, 0 } +end + +local main = function() + q3k.SetupGPIO() + q3k.NFC.Setup() + + while true do + q3k.NFC.WaitForCard(120, function(NFCID) + local PIN = q3k.ReadPIN() + local Result = q3k.Auth.GetCardStatus(PIN, NFCID) + local Status = Result.Status + local CS = q3k.Auth.CardStatus + if Status == CS.NO_MATCH then + print "No such user!" + elseif Status == CS.OKAY then + print(string.format("Hello %s!", Result.User)) + else + print "Unknown status." + end + end) + end +end + +main() diff --git a/nfc.lua b/nfc.lua new file mode 100644 index 0000000..8c8336a --- /dev/null +++ b/nfc.lua @@ -0,0 +1,133 @@ +-- NFC-related functions. +-- This implementation is for the PN532 chip. + +require('socket') +require('bit') + +q3k.NFC = {} + +local PN532_PREAMBLE = 0x00 +local PN532_STARTCODE1 = 0x00 +local PN532_STARTCODE2 = 0xFF +local PN532_POSTAMBLE = 0x00 + +local PN532_HOSTTOPN532 = 0xD4 +local PN532_PN532TOHOST = 0xD5 + +------------------------ +-- Internal functions -- +------------------------ + +-- sigh. luaposix on openwrt doesn't have nanosleep() +local Sleep = function(Seconds) + socket.select(nil, nil, Seconds) +end + +-- Get the IRQ pin status +local IRQRead = function() + local Data = q3k.I2C.Read(q3k.Config.I2CGPIO, 1, 0x2C) + if Data then + return Data[1] + else + return 1 + end +end + +-- Wait „Timeout” seconds for IRQ (or 5, if not given) +local WaitForIRQ = function(Timeout) + local Timeout = Timeout or 5 + local WaitStart = os.time() + while os.time() < WaitStart + Timeout do + local IRQStatus = IRQRead() + if IRQStatus == 0 then + break + end + Sleep(0.2) + end + local IRQStatus = IRQRead() + if IRQStatus ~= 0 then + return false + end + return true +end + +-- Write a PN532 command +local WriteCommand = function(...) + local Command = {...} + local Checksum = PN532_PREAMBLE + PN532_STARTCODE1 + PN532_STARTCODE2 + local WireCommand = { PN532_PREAMBLE, PN532_STARTCODE1, PN532_STARTCODE2 } + + WireCommand[#WireCommand+1] = (#Command + 1) + WireCommand[#WireCommand+1] = bit.band(bit.bnot(#Command +1) + 1, 0xFF) + + Checksum = Checksum + PN532_HOSTTOPN532 + WireCommand[#WireCommand+1] = PN532_HOSTTOPN532 + + for _, Byte in pairs(Command) do + Checksum = Checksum + Byte + WireCommand[#WireCommand+1] = Byte + end + + WireCommand[#WireCommand+1] = bit.band(bit.bnot(Checksum), 0xFF) + WireCommand[#WireCommand+1] = PN532_POSTAMBLE + + q3k.I2C.Write(q3k.Config.I2CNFC, unpack(WireCommand)) +end + +-- Send a PN532 and see if we get an ACK +local SendAndAck = function(...) + WriteCommand(...) + local IRQArrived = WaitForIRQ() + if not IRQArrived then + return false + end + local ACK = q3k.I2C.Read(q3k.Config.I2CNFC, 8) + if ACK[1] == 1 and ACK[2] == 0 and ACK[3] == 0 and ACK[4] == 255 + and ACK[5] == 0 and ACK[6] == 255 and ACK[7] == 0 then + return true + else + return false + end +end + +-- Read a PN532 frame +local ReadFrame = function(Count) + local Bytes = q3k.I2C.Read(q3k.Config.I2CNFC, Count+2) + table.remove(Bytes, 1) + table.remove(Bytes, #Bytes) + return Bytes +end + +---------------- +-- Public API -- +---------------- + +-- Call this once the I2C bus and GPIO multiplexer are ready +q3k.NFC.Setup = function() + -- enable SAM with stuff. + if SendAndAck(0x14, 0x01, 0x14, 0x01) == false then + print "SAM configuration failed!" + return false + end + return true +end + +-- Waits for a NFC card to appear in the field +-- „Seconds” is how long the reader will block +-- „Callback” is a callback which is called when a card appears, and takes +-- a single argument which is the NFC ID like this: { 0x00, 0x01, 0x02, 0x03 } +q3k.NFC.WaitForCard = function(Seconds, Callback) + if SendAndAck(0x4A, 0x01, 0x0) == false then + print "Sending card read command failed!" + return false + end + if WaitForIRQ(Seconds) then + local Bytes = ReadFrame(20) + if Bytes ~= nil and #Bytes == 20 then + local NFCID = { Bytes[14], Bytes[15], Bytes[16], Bytes[17] } + Callback(NFCID) + end + return true + end + return true +end