# Copyright 2019 Hubert Chathi # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule Polyjuice.Util.JSON do @moduledoc """ JSON-related functions. """ # maximum integer size = 2^53 @max_int 0x20000000000000 @doc ~S""" Encode a value as canonical JSON. See https://matrix.org/docs/spec/appendices#canonical-json for more information about canonical JSON. Examples: iex> Polyjuice.Util.JSON.canonical_json(%{"a" => true, "b" => false, "c" => nil, "d" => 42, "e" => [], "f" => %{}}) {:ok, "{\"a\":true,\"b\":false,\"c\":null,\"d\":42,\"e\":[],\"f\":{}}"} iex> Polyjuice.Util.JSON.canonical_json(%{"本"=>2,"日"=>1}) {:ok, "{\"日\":1,\"本\":2}"} iex> Polyjuice.Util.JSON.canonical_json("a日\t\"\\\x01\x0f\x10\x1f") {:ok, "\"a日\\t\\\"\\\\\\u0001\\u000F\\u0010\\u001F\""} iex> Polyjuice.Util.JSON.canonical_json([1,2,3,4]) {:ok, "[1,2,3,4]"} Unknown or invalid values will cause an error iex> Polyjuice.Util.JSON.canonical_json([{}]) :error iex> Polyjuice.Util.JSON.canonical_json([:foo]) :error iex> Polyjuice.Util.JSON.canonical_json([0x40000000000000]) :error iex> Polyjuice.Util.JSON.canonical_json([1.1]) :error """ @spec canonical_json(val :: true | false | nil | integer() | String.t() | list() | map()) :: {:ok, String.t()} | :error def canonical_json(val) when (is_integer(val) and -@max_int < val and val < @max_int) or is_binary(val) or is_map(val) or is_list(val) do try do {:ok, encode_canonical_json(val) |> IO.iodata_to_binary()} rescue _ -> :error end end # Some of the implementation of encode_canonical_json is heavily # inspired/copied from # https://github.com/devinus/poison/blob/master/lib/poison/encoder.ex The # implementations of encode_canonical_json, json_escape, and chunk_size may # be used under the conditions of this file's license, or the license of the # Poison library (currently CC0). defp encode_canonical_json(true), do: "true" defp encode_canonical_json(false), do: "false" defp encode_canonical_json(nil), do: "null" defp encode_canonical_json(val) when is_integer(val) and -@max_int < val and val < @max_int do Integer.to_string(val) end defp encode_canonical_json(""), do: "\"\"" defp encode_canonical_json(str) when is_binary(str), do: [?", json_escape(str), ?"] defp encode_canonical_json([]), do: "[]" defp encode_canonical_json(list) when is_list(list) do fun = &[?,, encode_canonical_json(&1)] [[?, | head] | tail] = Enum.map(list, fun) [?[, [head | tail], ?]] end defp encode_canonical_json(map) when is_map(map) and map_size(map) < 1, do: "{}" defp encode_canonical_json(map) when is_map(map) do fun = fn {key, val} -> [?,, encode_canonical_json(key), ":", encode_canonical_json(val)] end [[?, | head] | tail] = map |> Map.to_list() |> Enum.sort() |> Enum.map(fun) [?{, [head | tail], ?}] end defp json_escape(""), do: "" for {char, seq} <- Enum.zip('"\\\n\t\r\f\b', '"\\ntrfb') do defp json_escape(<> <> rest) do [unquote("\\" <> <>) | json_escape(rest)] end end defp json_escape(<> <> rest) when char <= 0x0F do ["\\u000", Integer.to_string(char, 16) | json_escape(rest)] end defp json_escape(<> <> rest) when char <= 0x1F do ["\\u00", Integer.to_string(char, 16) | json_escape(rest)] end defp json_escape(string) do size = chunk_size(string, 0) <> = string [chunk | json_escape(rest)] end defp chunk_size("", acc), do: acc defp chunk_size(<> <> _, acc) when char <= 0x1F or char in '"\\\n\t\r\f\b' do acc end defp chunk_size(<<_char>> <> rest, acc) do chunk_size(rest, acc + 1) end @doc ~S""" Sign a JSON object. See https://matrix.org/docs/spec/appendices#signing-json for more information about signing JSON. Examples: iex> Polyjuice.Util.JSON.sign( ...> %{}, ...> "domain", ...> Polyjuice.Util.Ed25519.SigningKey.from_base64("YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA1", "1") ...> ) { :ok, %{ "signatures" => %{ "domain" => %{ "ed25519:1" => "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ" } } } } iex> Polyjuice.Util.JSON.sign( ...> %{"one" => 1, "two" => "Two"}, ...> "domain", ...> Polyjuice.Util.Ed25519.SigningKey.from_base64("YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA1", "1") ...> ) { :ok, %{ "one" => 1, "signatures" => %{ "domain" => %{ "ed25519:1" => "KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw" } }, "two" => "Two" } } """ @spec sign(val :: map(), user :: String.t(), key :: Polyjuice.Util.SigningKey.t()) :: {:ok, map()} | :error def sign(%{} = val, user, key) when is_binary(user) do with {:ok, json} <- val |> Map.drop(["unsigned", "signatures"]) |> canonical_json() do signature = Polyjuice.Util.SigningKey.sign(key, json) signatures = Map.get(val, "signatures", %{}) user_sigs = Map.get(signatures, user, %{}) {:ok, Map.put( val, "signatures", Map.put( signatures, user, Map.put(user_sigs, Polyjuice.Util.SigningKey.id(key), signature) ) )} else _ -> :error end end @doc ~S""" Verify a signed JSON object. See https://matrix.org/docs/spec/appendices#signing-json for more information about signing JSON. Examples: iex> Polyjuice.Util.JSON.signed?( ...> %{ ...> "signatures" => %{ ...> "domain" => %{ ...> "ed25519:1" => "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ" ...> } ...> } ...> }, ...> "domain", ...> Polyjuice.Util.Ed25519.VerifyKey.from_base64("XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI", "1") ...> ) true iex> Polyjuice.Util.JSON.signed?( ...> %{ ...> "one" => 1, ...> "signatures" => %{ ...> "domain" => %{ ...> "ed25519:1" => "KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw" ...> } ...> }, ...> "two" => "Two" ...> }, ...> "domain", ...> Polyjuice.Util.Ed25519.VerifyKey.from_base64("XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI", "1") ...> ) true Missing signature: iex> Polyjuice.Util.JSON.signed?( ...> %{ ...> "signatures" => %{ ...> "domain" => %{ ...> "ed25519:2" => "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ" ...> } ...> } ...> }, ...> "domain", ...> Polyjuice.Util.Ed25519.VerifyKey.from_base64("XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI", "1") ...> ) false Incorrect signature: iex> Polyjuice.Util.JSON.signed?( ...> %{ ...> "signatures" => %{ ...> "domain" => %{ ...> "ed25519:1" => "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000" ...> } ...> } ...> }, ...> "domain", ...> Polyjuice.Util.Ed25519.VerifyKey.from_base64("XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI", "1") ...> ) false """ @spec signed?(val :: map(), user :: String.t(), key :: Polyjuice.Util.VerifyKey.t()) :: boolean() def signed?(%{} = val, user, key) when is_binary(user) do with {:ok, json} <- val |> Map.drop(["unsigned", "signatures"]) |> canonical_json() do signature = val |> Map.get("signatures", %{}) |> Map.get(user, %{}) |> Map.get(Polyjuice.Util.VerifyKey.id(key)) if signature == nil do false else Polyjuice.Util.VerifyKey.verify(key, json, signature) end else _ -> false end end @doc deprecated: "use `Polyjuice.Util.JSON.signed?/3` instead" @deprecated "Use Polyjuice.Util.JSON.signed?/3 instead" def verify(%{} = val, user, key) when is_binary(user), do: signed?(val, user, key) end