--- -*-lua-*- --- --- Wireshark protocol dissector for TrIPE --- --- (c) 2017 Straylight/Edgeware --- -------- Licensing notice --------------------------------------------------- --- --- This file is part of Trivial IP Encryption (TrIPE). --- --- TrIPE is free software; you can redistribute it and/or modify --- it under the terms of the GNU General Public License as published by --- the Free Software Foundation; either version 2 of the License, or --- (at your option) any later version. --- --- TrIPE is distributed in the hope that it will be useful, --- but WITHOUT ANY WARRANTY; without even the implied warranty of --- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --- GNU General Public License for more details. --- --- You should have received a copy of the GNU General Public License --- along with TrIPE; if not, write to the Free Software Foundation, --- Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. local tripe = Proto("tripe", "TrIPE VPN") ----------------------------------------------------------------------------- --- Configuration handling. local CONFIG = { -- Information about the configuration variables. This table, when it's -- set up, maps the internal names, which are used to refer to -- configuration variables in the rest of this code, to a little structure: -- -- * `var' names the variable, and is the usual key for lookups; -- -- * `name' is the label used in the dialogue box; -- -- * `type' is the type of variable, currently either `enum' or `int'; -- -- * `descr' is a longer (but generally fairly useless) description for -- use in a tooltip; -- -- * `allowed' is a sequence of allowed values for an `enum' variable; -- and -- -- * `min' and `max' are the limits on permitted values for an `int' -- variable (and may be omitted). -- -- More slots are added at runtime: -- -- * `map' is a table mapping string values to their integer indices, as -- stored in Wireshark's preferences database. -- -- Initially, though, the table is given as a sequence, so that the -- preferences can be populated in a consistent (and approximately logical) -- order. { var = "bulk", name = "Bulk transform", type = "enum", allowed = { "v0", "iiv", "naclbox" }, descr = "Bulk cryptographic transform", default = "v0" }, { var = "hashsz", name = "Hash length", type = "int", min = 0, descr = "Hash length (bytes)", default = 20 }, { var = "tagsz", name = "Tag length", type = "int", min = 0, descr = "Authentication tag length (bytes)", default = 10 }, { var = "ivsz", name = "IV length", type = "int", min = 0, descr = "Initialization vector length (bytes)", default = 8 }, { var = "kx", name = "Key-exchange group", type = "enum", allowed = { "dh", "ec", "x25519", "x448" }, descr = "Key-exchange group type", default = "dh" }, { var = "scsz", name = "Scalar length", type = "int", min = 1, descr = "Scalar field-element length (bytes)", default = 32 }, } local C = { } -- The working values of the configuration variables. local function set_config(k, v) -- Set configuration variable K to the value V. -- -- K is a string naming the variable to set. V is the new value, which may -- be a string or a number. -- -- For `int' variables, V is converted to a number if necessary, and then -- checked against the permitted bounds. -- -- For `enum' variables, things are more complicated. If V is a string, -- it's checked against the permitted values. If V is a number, it's -- converted back into the corresponding string. local info = CONFIG[k] if info == nil then error("unknown config key `" .. k .. "'") end if info.type == "enum" then if type(v) == "number" then local t = info.allowed[v] if t == nil then error(string.format("bad index %d for `%s'", n, k)) end v = t else if info.map[v] == nil then error(string.format("bad value `%s' for `%s'", v, k)) end end elseif info.type == "int" then local n = tonumber(v) if n == nil then error("bad number `" .. v .. "'") end if n ~= math.floor(n) then error("value `" .. v .. "' is not an integer") end if (info.min ~= nil and n < info.min) or (info.max ~= nil and n > info.max) then error(string.format("value %d out of range for `%s'", n, k)) end v = n end C[k] = v end -- Set up the configuration information. Configure preferences objects on -- the dissector. For `enum' variables, build the `map' slots. for i, v in ipairs(CONFIG) do local k = v.var CONFIG[k] = v if v.type == "enum" then local tab = { } v.map = { } for i, t in pairs(v.allowed) do v.map[t] = i tab[i] = { i, t, i } end tripe.prefs[k] = Pref.enum(v.name, v.map[v.default], v.descr, tab) elseif v.type == "int" then tripe.prefs[k] = Pref.uint(v.name, v.default, v.descr) end end local function prefs_changed() -- Notice that the preferences have been changed and update `C'. for k, _ in pairs(CONFIG) do if type(k) == "string" then set_config(k, tripe.prefs[k]) end end end tripe.prefs_changed = prefs_changed -- Populate the configuration table from the stored preferences or their -- default values. prefs_changed() -- Now work through arguments passed in on the command line. Annoyingly, -- while one can set preferences on the Wireshark command line, these are -- done /before/ Lua scripts are loaded, so the silly thing thinks the -- preference slots don't exist. So we have to do it a different way. for _, arg in ipairs({...}) do local k, v = arg:match("(.+)=(.+)") if k == nil or v == nil then error("bad option syntax `" .. arg .. "'") end se_config(k, v) end ----------------------------------------------------------------------------- --- Protocol dissection primitives. local PF = { } -- The table of protocol fields, filled in later. -- The `dissect_*' functions follow a common protocol. They parse a thing -- from a packet buffer BUF, of size SZ, starting from POS, and store -- interesting things in a given TREE; when they're done, they return the -- updated index where the next interesting thing might be. As a result, -- it's usually a simple matter to parse a packet by invoking the appropriate -- primitive dissectors in the right order. local function dissect_wtf(buf, tree, pos, sz) -- If POS is not at the end of the buffer, note that there's unexpected -- stuff in the packet. if pos < sz then tree:add(PF["tripe.wtf"], buf(pos, sz - pos)) end return sz end -- Dissect a ciphertext of some particular kind. local dissect_ct = { } function dissect_ct.naclbox(buf, tree, pos, sz) tree:add(PF["tripe.ciphertext.tag"], buf(pos, 16)); pos = pos + 16 tree:add(PF["tripe.ciphertext.seq"], buf(pos, 4)); pos = pos + 4 tree:add(PF["tripe.ciphertext.body"], buf(pos, sz - pos)) end function dissect_ct.iiv(buf, tree, pos, sz) tree:add(PF["tripe.ciphertext.tag"], buf(pos, C.tagsz)); pos = pos + C.tagsz tree:add(PF["tripe.ciphertext.seq"], buf(pos, 4)); pos = pos + 4 tree:add(PF["tripe.ciphertext.body"], buf(pos, sz - pos)) end function dissect_ct.v0(buf, tree, pos, sz) tree:add(PF["tripe.ciphertext.tag"], buf(pos, C.tagsz)); pos = pos + C.tagsz tree:add(PF["tripe.ciphertext.seq"], buf(pos, 4)); pos = pos + 4 tree:add(PF["tripe.ciphertext.iv"], buf(pos, C.ivsz)); pos = pos + C.ivsz tree:add(PF["tripe.ciphertext.body"], buf(pos, sz - pos)) end local function dissect_ciphertext(buf, tree, label, pos, sz) -- Dissect a ciphertext, making the whole thing be a little subtree with -- the given LABEL. local t = tree:add(PF[label], buf(pos, sz - pos)) dissect_ct[C.bulk](buf, t, pos, sz) return pos end local function dissect_packet(buf, tree, pos, sz) return dissect_ciphertext(buf, tree, "tripe.packet.payload", pos, sz) end -- Dissect a group element of some particular kind. local dissect_ge = { } function dissect_ge.dh(buf, tree, pos, sz) tree:add(PF["tripe.dh.len"], buf(pos, 2)) xsz = buf(pos, 2):uint(); pos = pos + 2 tree:add(PF["tripe.dh.x"], buf(pos, xsz)); pos = pos + xsz return pos end function dissect_ge.ec(buf, tree, pos, sz) tree:add(PF["tripe.ec.xlen"], buf(pos, 2)) xsz = buf(pos, 2):uint(); pos = pos + 2 tree:add(PF["tripe.ec.x"], buf(pos, xsz)); pos = pos + xsz tree:add(PF["tripe.ec.ylen"], buf(pos, 2)) ysz = buf(pos, 2):uint(); pos = pos + 2 tree:add(PF["tripe.ec.y"], buf(pos, ysz)); pos = pos + ysz return pos end function dissect_ge.x25519(buf, tree, pos, sz) tree:add(PF["tripe.x25519.x"], buf(pos, 32)) return pos + 32 end function dissect_ge.x448(buf, tree, pos, sz) tree:add(PF["tripe.x448.x"], buf(pos, 56)) return pos + 56 end local function dissect_my_challenge(buf, tree, pos, sz) -- We don't know how long the group element is going to be. We can set the -- length later, but (at least in older versions) it doesn't work so well -- to increase the length, so make it large to start out, and shrink it -- later. local t = tree:add(PF["tripe.keyexch.mychal"], buf(pos, sz - pos)) local q = dissect_ge[C.kx](buf, t, pos, sz) t:set_len(q - pos) return q end local function dissect_my_cookie(buf, tree, pos, sz) tree:add(PF["tripe.keyexch.mycookie"], buf(pos, C.hashsz)) return pos + C.hashsz end local function dissect_your_cookie(buf, tree, pos, sz) tree:add(PF["tripe.keyexch.yourcookie"], buf(pos, C.hashsz)) return pos + C.hashsz end local kx_scsz = { x25519 = 32, x448 = 56 } -- Hardwired scalar sizes. local function dissect_check(buf, tree, pos, sz) local scsz = kx_scsz[C.kx] or C.scsz tree:add(PF["tripe.keyexch.check"], buf(pos, scsz)) return pos + scsz end local function dissect_reply(buf, tree, pos, sz) return dissect_ciphertext(buf, tree, "tripe.keyexch.reply", pos, sz) end local function dissect_switch(buf, tree, pos, sz) return dissect_ciphertext(buf, tree, "tripe.keyexch.switch", pos, sz) end local function dissect_switchok(buf, tree, pos, sz) return dissect_ciphertext(buf, tree, "tripe.keyexch.switchok", pos, sz) end local function dissect_misc_payload(buf, tree, pos, sz) tree:add(PF["tripe.misc.payload"], buf(pos, sz - pos)) return sz end local function dissect_misc_ciphertext(buf, tree, pos, sz) return dissect_ciphertext(buf, tree, "tripe.misc.ciphertext", pos, sz) end ----------------------------------------------------------------------------- --- The protocol information table. local PKTINFO = { -- This is the main table which describes the protocol. The top level maps -- category codes to structures: -- -- * `label' is the category code's symbolic name; -- -- * `subtype' is the field name for the subtype code; -- -- * `info' is a prefix for the information column display; and -- -- * `sub' is a table describing the individual subtypes. -- -- The subtype table similarly maps subtype codes to structures: -- -- * `label' is the subtype code's symbolic name; -- -- * `info' is the suffix for the information column display; and -- -- * `dissect' is a sequence of primitive dissectors to run in order to -- parse the rest of the packet. [0] = { label = "MSG_PACKET", subtype = "tripe.packet.type", info = "Packet data", sub = { [0] = { label = "PACKET_IP", info = "encapsulated IP datagram", dissect = { dissect_packet} } } }, [1] = { label = "MSG_KEYEXCH", subtype = "tripe.keyexch.type", info = "Key exchange", sub = { [0] = { label = "KX_PRECHAL", info = "pre-challenge", dissect = { dissect_my_challenge, dissect_wtf } }, [1] = { label = "KX_CHAL", info = "challenge", dissect = { dissect_my_challenge, dissect_your_cookie, dissect_check, dissect_wtf } }, [2] = { label = "KX_REPLY", info = "reply", dissect = { dissect_my_challenge, dissect_your_cookie, dissect_check, dissect_reply } }, [3] = { label = "KX_SWITCH", info = "switch", dissect = { dissect_my_cookie, dissect_your_cookie, dissect_switch } }, [4] = { label = "KX_SWITCHOK", info = "switch-ok", dissect = { dissect_switchok } }, } }, [2] = { label = "MSG_MISC", subtype = "tripe.misc.type", info = "Miscellaneous", sub = { [0] = { label = "MISC_NOP", info = "no-operation (keepalive)", dissect = { dissect_misc_payload } }, [1] = { label = "MISC_PING", info = "transport-level ping", dissect = { dissect_misc_payload } }, [2] = { label = "MISC_PONG", info = "transport-level ping reply", dissect = { dissect_misc_payload } }, [3] = { label = "MISC_EPING", info = "crypto-level ping", dissect = { dissect_misc_ciphertext } }, [4] = { label = "MISC_EPONG", info = "crypto-level ping reply", dissect = { dissect_misc_ciphertext } }, [5] = { label = "MISC_GREET", info = "greeting", dissect = { dissect_misc_payload } }, } } } do -- Work through the master table and build `cattab' and `subtab' tables, -- mapping category and subtype codes to their symbolic names for -- presentation. The `subtab' is a two-level table, needing two layers of -- indexing. local cattab = { } local subtab = { } for i, v in pairs(PKTINFO) do cattab[i] = v.label if v.sub ~= nil then subtab[i] = { } for j, w in pairs(v.sub) do subtab[i][j] = w.label end end end local ftab = { -- The protocol fields. This table maps the field names to structures -- used to build the fields, which are then stored in `PF' (declared way -- above): -- -- * `name' is the field name to show in the dissector tree view; -- -- * `type' is the field type; -- -- * `base' is a tweak describing how the field should be formatted; -- -- * `mask' is used to single out a piece of a larger bitfield; and -- -- * `tab' names a mapping table used to convert numerical values to -- symbolic names. ["tripe.type"] = { name = "Message type", type = ftypes.UINT8, base = base.HEX }, ["tripe.cat"] = { name = "Message category", type = ftypes.UINT8, base = base.DEC, mask = 0xf0, tab = cattab }, ["tripe.packet.type"] = { name = "Packet subcode", type = ftypes.UINT8, base = base.DEC, mask = 0x0f, tab = subtab[0] }, ["tripe.packet.payload"] = { name = "Encrypted packet", type = ftypes.NONE }, ["tripe.keyexch.type"] = { name = "Key-exchange subcode", type = ftypes.UINT8, base = base.DEC, mask = 0x0f, tab = subtab[1] }, ["tripe.keyexch.mychal"] = { name = "Sender's challenge R = r P", type = ftypes.NONE }, ["tripe.keyexch.mycookie"] = { name = "Hash of recipient's challenge = H(R, ...)", type = ftypes.BYTES, base = base.SPACE }, ["tripe.keyexch.yourcookie"] = { name = "Hash of sender's challenge = H(R', ...)", type = ftypes.BYTES, base = base.SPACE }, ["tripe.keyexch.reply"] = { name = "Encrypted reply = k R'", type = ftypes.NONE }, ["tripe.keyexch.switch"] = { name = "Encrypted reply and switch request = k R', H(...)", type = ftypes.NONE }, ["tripe.keyexch.switchok"] = { name = "Encrypted switch confirmation = H(...)", type = ftypes.NONE }, ["tripe.misc.type"] = { name = "Miscellenaous subcode", type = ftypes.UINT8, base = base.DEC, mask = 0x0f, tab = subtab[2] }, ["tripe.misc.payload"] = { name = "Miscellaneous payload", type = ftypes.BYTES, base = base.SPACE }, ["tripe.misc.ciphertext"] = { name = "Miscellaneous encrypted payload", type = ftypes.NONE }, ["tripe.wtf"] = { name = "Unexpected trailing data", type = ftypes.BYTES, base = base.SPACE }, ["tripe.keyexch.check"] = { name = "Sender's challenge check value = r XOR H(r K', ...)", type = ftypes.BYTES, base = base.SPACE }, ["tripe.ciphertext.seq"] = { name = "Sequence number", type = ftypes.UINT32, base = base.DEC }, ["tripe.ciphertext.iv"] = { name = "Initialization vector", type = ftypes.BYTES, base = base.SPACE }, ["tripe.ciphertext.tag"] = { name = "Authentication tag", type = ftypes.BYTES, base = base.SPACE }, ["tripe.ciphertext.body"] = { name = "Encrypted data", type = ftypes.BYTES, base = base.SPACE }, ["tripe.dh.len"] = { name = "DH group element length", type = ftypes.UINT16, base = base.DEC }, ["tripe.dh.x"] = { name = "DH group element value", type = ftypes.BYTES, base = base.SPACE }, ["tripe.ec.xlen"] = { name = "Elliptic curve x-coordinate length", type = ftypes.UINT16, base = base.DEC }, ["tripe.ec.x"] = { name = "Elliptic curve x-coordinate value", type = ftypes.BYTES, base = base.SPACE }, ["tripe.ec.ylen"] = { name = "Elliptic curve y-coordinate length", type = ftypes.UINT16, base = base.DEC }, ["tripe.ec.y"] = { name = "Elliptic curve y-coordinate value", type = ftypes.BYTES, base = base.SPACE }, ["tripe.x25519.x"] = { name = "X25519 x-coordinate", type = ftypes.BYTES, base = base.SPACE }, ["tripe.x448.x"] = { name = "X448 x-coordinate", type = ftypes.BYTES, base = base.SPACE }, } -- Convert this table into the protocol fields, and populate `PF'. local ff = { } local i = 1 -- Figure out whether we can use `none' fields (see below). -- probe for this easily local use_none_p = rawget(ProtoField, 'none') ~= nil for abbr, args in pairs(ftab) do -- An annoying hack. Older versions of Wireshark don't allow setting -- fields with type `none', which is a shame because they're ideal as -- internal tree nodes. ty = args.type b = args.base if ty == ftypes.NONE and not use_none_p then ty = ftypes.BYTES b = base.SPACE end -- Go make the field. local f = ProtoField.new(args.name, abbr, ty, args.tab, b, args.mask, args.descr) PF[abbr] = f ff[i] = f; i = i + 1 end tripe.fields = PF end ----------------------------------------------------------------------------- --- The main dissector. function tripe.dissector(buf, pinfo, tree) -- Fill in the obvious stuff. pinfo.cols.protocol = "TrIPE" local sz = buf:reported_length_remaining() local sub = tree:add(tripe, buf(0, sz), "TrIPE packet") local p = 1 -- Decode the packet type octet. local tycode = buf(0, 1):uint() local ty = sub:add(PF["tripe.type"], buf(0, 1)) ty:add(PF["tripe.cat"], buf(0, 1)) local cat = bit.rshift(bit.band(tycode, 0xf0), 4) local subty = bit.band(tycode, 0x0f) local info = PKTINFO[cat] -- Dispatch using the master protocol table. if info == nil then pinfo.cols.info = string.format("Unknown category code %u, " .. "unknown type code %u", cat, subty) else ty:add(PF[info.subtype], buf(0, 1)) local subinfo = info.sub[subty] if subinfo == nil then pinfo.cols.info = string.format("%s, unknown type code %u", info.info, subty) else pinfo.cols.info = string.format("%s, %s", info.info, subinfo.info) p = 1 for _, d in ipairs(subinfo.dissect) do p = d(buf, sub, p, sz) end end end -- Return the final position we reached. return p end -- We're done. Register the dissector. DissectorTable.get("udp.port"):add(4070, tripe) -------- That's all, folks --------------------------------------------------