Current section
Files
Jump to
Current section
Files
lib/polyjuice/util/json.ex
# Copyright 2019 Hubert Chathi <hubert@uhoreg.ca>
#
# 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(<<unquote(char)>> <> rest) do
[unquote("\\" <> <<seq>>) | json_escape(rest)]
end
end
defp json_escape(<<char>> <> rest) when char <= 0x0F do
["\\u000", Integer.to_string(char, 16) | json_escape(rest)]
end
defp json_escape(<<char>> <> 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)
<<chunk::binary-size(size), rest::binary>> = string
[chunk | json_escape(rest)]
end
defp chunk_size("", acc), do: acc
defp chunk_size(<<char>> <> _, 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