defmodule Radius.Packet do require Logger alias Radius.Dict.Attribute alias Radius.Dict.Vendor alias Radius.Dict.Value alias Radius.Dict.EntryNotFoundError defstruct code: nil, id: nil, length: nil, auth: nil, attrs: [], raw: nil, secret: nil @doc """ Decode radius packet """ def decode(data, secret) do pkt = %{raw: data, secret: secret, attrs: nil} |> decode_header |> decode_payload struct(__MODULE__, pkt) end defp decode_header(%{raw: raw} = ctx) do <> = raw if byte_size(rest) < length - 20 do {:error, :packet_too_short} else if byte_size(ctx.raw) != length do raise "Packet length not match." end ctx |> Map.merge(%{ code: decode_code(code), id: id, length: length, auth: auth, rest: rest }) end end defp decode_code(1), do: "Access-Request" defp decode_code(2), do: "Access-Accept" defp decode_code(3), do: "Access-Reject" defp decode_code(11), do: "Access-Challenge" defp decode_code(4), do: "Accounting-Request" defp decode_code(5), do: "Accounting-Response" defp decode_code(12), do: "Status-Server" defp decode_code(13), do: "Status-Client" defp decode_code(x), do: x defp decode_payload(ctx) do decode_tlv(ctx.rest, [], {1, 1}) |> resolve_tlv(ctx) end defp decode_tlv(<<>>, acc, _), do: acc |> Enum.reverse() # not to decode USR style VSAs defp decode_tlv(bin, _, {_, 0}), do: bin defp decode_tlv(bin, acc, {tl, ll} = fmt) when byte_size(bin) > tl + ll do tl = tl * 8 ll = ll * 8 <> = bin length = length - 2 <> = rest decode_tlv(rest, [{type, value} | acc], fmt) end defp resolve_tlv(attrs, ctx) when is_list(attrs) do attrs = attrs |> Enum.map(&resolve_tlv(&1, ctx, nil)) Map.put(ctx, :attrs, attrs) end # VSA Entry defp resolve_tlv({26, value}, ctx, nil) do type = "Vendor-Specific" <> = value try do v = Vendor.by_id(vid) value = case decode_tlv(rest, [], v.format) do bin when is_binary(bin) -> bin tlv when is_list(tlv) -> Enum.map(tlv, fn x -> resolve_tlv(x, ctx, v.id) end) end {{type, v.name}, value} rescue _e in EntryNotFoundError -> {type, value} end end defp resolve_tlv({type, value} = tlv, ctx, vendor) do try do attr = Attribute.by_id(vendor, type) type = attr.name has_tag = Keyword.has_key?(attr.opts, :has_tag) {tag, value} = case value do <<0, rest::binary>> when has_tag == true -> {nil, rest} <> when tag in 1..0x1F and has_tag == true -> {tag, rest} _ -> {nil, value} end value = value |> decode_value(attr.type) |> resolve_value(vendor, attr.id) |> decrypt_value(Keyword.get(attr.opts, :encrypt), ctx.auth, ctx.secret) if tag do {type, {tag, value}} else {type, value} end rescue _e in EntryNotFoundError -> tlv end end defp decode_value(<>, :byte), do: val defp decode_value(<>, :short), do: val defp decode_value(<>, :integer), do: val defp decode_value(<>, :signed), do: val defp decode_value(<>, :date), do: val defp decode_value(<>, :ifid), do: val defp decode_value(<>, :ipaddr), do: {a, b, c, d} defp decode_value(<>, :ipv6addr) do for(<>, do: x) |> :erlang.list_to_tuple() end defp decode_value(bin, _t) do bin end defp resolve_value(val, vid, aid) do try do v = Value.by_value(vid, aid, val) v.name rescue _e in EntryNotFoundError -> val end end defp decrypt_value(bin, nil, _, _), do: bin defp decrypt_value(bin, 1, auth, secret) do Radius.Util.decrypt_rfc2865(bin, secret, auth) end defp decrypt_value(bin, 2, auth, secret) do Radius.Util.decrypt_rfc2868(bin, secret, auth) end defp decrypt_value(bin, a, _, _) do Logger.error("Unknown encrypt type: #{inspect(a)}") bin end @doc """ Return an iolist of encoded packet for request packets, leave packet.auth == nil, then I will generate one from random bytes. for reply packets, set packet.auth = request.auth, I will calc the reply hash with it. packet.attrs :: [attr] attr :: {type,value} type :: String.t | integer | {"Vendor-Specific", vendor} value :: integer | String.t | ipaddr vendor :: String.t | integer ipaddr :: {a,b,c,d} | {a,b,c,d,e,f,g,h} """ def encode(packet, options \\ []) do sign? = options |> Keyword.get(:sign, false) raw? = options |> Keyword.get(:raw, false) {auth, reply?} = if packet.auth == nil do {:crypto.strong_rand_bytes(16), false} else {packet.auth, true} end packet = %{packet | auth: auth} packet = if sign? do attrs = packet.attrs ++ [ {"Message-Authenticator", <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>} ] %{packet | attrs: attrs} else packet end attrs = encode_attrs(packet) code = encode_code(packet.code) length = 20 + :erlang.iolist_size(attrs) header = <> attrs = if sign? do signature = :crypto.mac(:hmac, :md5, packet.secret, [header, attrs]) [last | attrs] = attrs |> Enum.reverse() crop_len = byte_size(last) - 16 last = <> [last | attrs] |> Enum.reverse() else attrs end header = if reply? and raw? == false do resp_auth = :crypto.hash_init(:md5) |> :crypto.hash_update(header) |> :crypto.hash_update(attrs) |> :crypto.hash_update(packet.secret) |> :crypto.hash_final() <> else header end [header, attrs] end defp encode_attrs(%{attrs: a} = ctx) do Enum.map(a, fn x -> x |> resolve_attr(ctx) |> encode_attr end) end # back-door for VSAs, encode_vsa could retuen an iolist defp encode_attr({26, value}), do: [26, :erlang.iolist_size(value) + 2, value] defp encode_attr({tag, value}) when is_binary(value) do len = byte_size(value) + 2 if len > 0xFF do raise "value oversized: #{inspect({tag, value})}" end <> end defp encode_attr({tag, value}) when is_integer(value) do if value > 0xFFFFFFFF do Logger.warn("value truncated: #{inspect({tag, value})}") end <> end defp encode_attr({type, value, attr}) do {t, l} = attr.vendor.format value = if Keyword.has_key?(attr.opts, :has_tag) do {tag, value} = case value do {tag, value} when tag in 0..0x1F -> {tag, value} {tag, _value} -> raise "Tag over-range, should be [0-0x1f], got: #{tag}" value -> {0, value} end value = encode_value(value, attr.type) <> else encode_value(value, attr.type) end length = byte_size(value) + t + l ll = l * 8 tl = t * 8 <> end defp encrypt_value({tag, bin}, attr, ctx), do: {tag, encrypt_value(bin, attr, ctx)} defp encrypt_value(bin, attr, ctx), do: encrypt_value(bin, Keyword.get(attr.opts, :encrypt), ctx.auth, ctx.secret) defp encrypt_value(bin, nil, _, _), do: bin defp encrypt_value(bin, 1, auth, secret) do Radius.Util.encrypt_rfc2865(bin, secret, auth) end defp encrypt_value(bin, 2, auth, secret) do Radius.Util.encrypt_rfc2868(bin, secret, auth) end defp encrypt_value(bin, a, _, _) do Logger.error("Unknown encrypt type: #{inspect(a)}") bin end defp encode_value(val, :byte) when is_integer(val), do: <> defp encode_value(val, :short) when is_integer(val), do: <> defp encode_value(val, :integer) when is_integer(val), do: <> defp encode_value(val, :signed) when is_integer(val), do: <> defp encode_value(val, :date) when is_integer(val), do: <> defp encode_value(val, :ifid) when is_integer(val), do: <> defp encode_value({a, b, c, d}, :ipaddr), do: <> defp encode_value(x, :ipaddr) when is_integer(x), do: <> defp encode_value(x, :ipv6addr) when is_tuple(x) and tuple_size(x) == 8 do for x <- :erlang.tuple_to_list(x), into: "", do: <> end defp encode_value(bin, _), do: bin defp resolve_attr({{type, vid}, value}, ctx) when type == "Vendor-Specific" or type == 26 do {26, encode_vsa(vid, value, ctx)} end defp resolve_attr(tlv, ctx) do resolve_attr(tlv, ctx, %Vendor{}) end defp resolve_attr({type, value}, ctx, vendor) do case lookup_attr(vendor, type) do nil -> {type, value} a -> {a.id, lookup_value(a, value) |> encrypt_value(a, ctx), a} end end defp lookup_attr(vendor, type) when is_integer(type) do try do Attribute.by_id(vendor.id, type) rescue _e in EntryNotFoundError -> nil end end # Raise an error if attr not defined defp lookup_attr(_vendor, type) when is_binary(type) do Attribute.by_name(type) end defp lookup_value(attr, {tag, val}) do {tag, lookup_value(attr, val)} end defp lookup_value(%{type: :integer} = attr, val) when is_binary(val) do try do v = Value.by_name(attr.vendor.name, attr.name, val) v.value rescue _e in EntryNotFoundError -> # raise "Value can not be resolved: #{attr.name}: #{val}" val end end defp lookup_value(_, val), do: val defp encode_vsa(vid, value, ctx) when is_binary(value) and is_binary(vid), do: encode_vsa(Vendor.by_name(vid).id, value, ctx) defp encode_vsa(vid, value, _) when is_binary(value) and is_integer(vid), do: <> defp encode_vsa(vid, vsa, ctx) when is_tuple(vsa), do: encode_vsa(vid, [vsa], ctx) defp encode_vsa(vid, vsa, ctx) when is_binary(vid), do: encode_vsa(Vendor.by_name(vid), vsa, ctx) defp encode_vsa(vid, vsa, ctx) when is_integer(vid), do: encode_vsa(Vendor.by_id(vid), vsa, ctx) defp encode_vsa(vendor, vsa, ctx) do val = Enum.map(vsa, fn x -> x |> resolve_attr(ctx, vendor) |> encode_attr end) [<> | val] end defp encode_code(x) when is_integer(x), do: x defp encode_code("Access-Request"), do: 1 defp encode_code("Access-Accept"), do: 2 defp encode_code("Access-Reject"), do: 3 defp encode_code("Access-Challenge"), do: 11 defp encode_code("Accounting-Request"), do: 4 defp encode_code("Accounting-Response"), do: 5 defp encode_code("Status-Server"), do: 12 defp encode_code("Status-Client"), do: 13 @doc """ Return the value of a given attribute, if found, or default otherwise. """ def get_attr(packet, attr_name, default \\ nil) do result = packet.attrs |> Enum.find(default, fn {^attr_name, _} -> true _ -> false end) case result do {_, value} -> value _ -> nil end end @doc """ Verify if the packet signature is valid. """ def verify(packet) do # TODO: this code is going to fail when validating replies sig1 = packet |> Radius.Packet.get_attr("Message-Authenticator") if sig1 != nil do attrs = packet.attrs |> Enum.filter(fn {k, _} -> k != "Message-Authenticator" end) raw = %{packet | attrs: attrs} |> Radius.Packet.encode(raw: true, sign: true) |> IO.iodata_to_binary() crop_len = byte_size(raw) - 16 <<_::bytes-size(crop_len), sig2::binary>> = raw sig1 == sig2 else false end end end # defmodule Packet