diff --git a/README.md b/README.md index ff4880c..1fe0a1c 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,21 @@ See the [SSB Binary Field Encodings Specification](https://github.com/ssbc/ssb-b ## Encoding ```elixir -SsbBfe.encode(value) +SsbBfe.encode("@HEqy940T6uB+T+d9Jaa58aNfRzLx9eRWqkZljBmnkmk=.ed25519") +<<0, 0, 28, 74, 178, 247, 141, 19, 234, 224, 126, 79, 231, 125, 37, 166, 185, + 241, 163, 95, 71, 50, 241, 245, 228, 86, 170, 70, 101, 140, 25, 167, 146, +105>> ``` ## Decoding ```elixir -SsbBfe.decode(value) +SsbBfe.decode( + <<0, 0, 28, 74, 178, 247, 141, 19, 234, 224, 126, 79, 231, 125, 37, 166, 185, + 241, 163, 95, 71, 50, 241, 245, 228, 86, 170, 70, 101, 140, 25, 167, 146, + 105>> +) +"@HEqy940T6uB+T+d9Jaa58aNfRzLx9eRWqkZljBmnkmk=.ed25519" ``` ## Supported Types diff --git a/lib/ssb_bfe.ex b/lib/ssb_bfe.ex index b248df6..207c6d0 100644 --- a/lib/ssb_bfe.ex +++ b/lib/ssb_bfe.ex @@ -1,20 +1,22 @@ defmodule SsbBfe do @moduledoc """ - Documentation for `SsbBfe`. - """ + Binary Field Encodings (BFE) for Secure Scuttlebutt (SSB). - @doc """ - Hello world. + Encode and decode TFD values. ## Examples - iex> SsbBfe.hello() - :world + iex> SsbBfe.encode("@HEqy940T6uB+T+d9Jaa58aNfRzLx9eRWqkZljBmnkmk=.ed25519") + <<0, 0, 28, 74, 178, 247, 141, 19, 234, 224, 126, 79, 231, 125, 37, 166, 185, + 241, 163, 95, 71, 50, 241, 245, 228, 86, 170, 70, 101, 140, 25, 167, 146, + 105>> + + ...> SsbBfe.decode(<<0, 0, 28, 74, 178, 247, 141, 19, 234, 224, 126, 79, 231, 125, 37, 166, 185, + 241, 163, 95, 71, 50, 241, 245, 228, 86, 170, 70, 101, 140, 25, 167, 146, + 105>>) + "@HEqy940T6uB+T+d9Jaa58aNfRzLx9eRWqkZljBmnkmk=.ed25519" """ - def hello do - :world - end # ENCODE @@ -33,8 +35,8 @@ defmodule SsbBfe do end def encode(value) when is_tuple(value) do - Enum.map(Tuple.to_list(value), fn x -> encode(x) end) |> - List.to_tuple() + Enum.map(Tuple.to_list(value), fn x -> encode(x) end) + |> List.to_tuple() end def encode(value) when is_bitstring(value) do @@ -89,8 +91,8 @@ defmodule SsbBfe do 6 == first_byte -> SsbBfe.Decoder.decode_generic(value) - nil -> - true + true -> + nil end end @@ -111,7 +113,7 @@ defmodule SsbBfe do end def decode(value) when is_tuple(value) do - Enum.map(Tuple.to_list(value), fn x -> decode(x) end) |> - List.to_tuple() + Enum.map(Tuple.to_list(value), fn x -> decode(x) end) + |> List.to_tuple() end end diff --git a/lib/ssb_bfe/decoder.ex b/lib/ssb_bfe/decoder.ex index 74fe5aa..3db550e 100644 --- a/lib/ssb_bfe/decoder.ex +++ b/lib/ssb_bfe/decoder.ex @@ -1,42 +1,56 @@ defmodule SsbBfe.Decoder do + # Split the TF tag from the data bytes and base64 encode them. defp extract_base64_data(bin, tf_tag) do [_, base64_data] = :binary.split(bin, tf_tag) Base.encode64(base64_data) end - def decode_blob(blob) do - encoded_base64_data = extract_base64_data(blob, <<2, 0>>) + @doc """ + Take a blob ID as an encoded binary and return the dedoded string representing the TFD. + """ + def decode_blob(blob_id) do + encoded_base64_data = extract_base64_data(blob_id, <<2, 0>>) "&" <> encoded_base64_data <> ".sha256" end + @doc """ + Take an encrypted box as an encoded binary and return the decoded string representind the TFD. + + `decode_box/1` calls the appropriate `decode_box/2` clause based on the value of the TF tag extracted from the encoded box. + """ def decode_box(box) do tf_tag = binary_part(box, 0, 2) decode_box(box, tf_tag) end - # Matches box. def decode_box(box, <<5, 0>>) do encoded_base64_data = extract_base64_data(box, <<5, 0>>) encoded_base64_data <> ".box" end - # Matches box2. def decode_box(box, <<5, 1>>) do encoded_base64_data = extract_base64_data(box, <<5, 1>>) encoded_base64_data <> ".box2" end - def decode_feed(feed) do - tf_tag = binary_part(feed, 0, 2) - decode_feed(feed, tf_tag) + @doc """ + Take a feed ID as an encoded binary and return the decoded string representing the TFD. + + `decode_feed/1` calls the appropriate `decode_feed/2` clause based on the value of the TF tag extracted from the encoded feed. + """ + def decode_feed(feed_id) do + tf_tag = binary_part(feed_id, 0, 2) + decode_feed(feed_id, tf_tag) end - # Matches classic feed. - def decode_feed(feed, <<0, 0>>) do - encoded_base64_data = extract_base64_data(feed, <<0, 0>>) + def decode_feed(feed_id, <<0, 0>>) do + encoded_base64_data = extract_base64_data(feed_id, <<0, 0>>) "@" <> encoded_base64_data <> ".ed25519" end + @doc """ + Take an encoded generic value as an encoded binary and return `true`, `false`, `nil`, plain bytes or a decoded string. + """ def decode_generic(<<6, 1, 1>>), do: true def decode_generic(<<6, 1, 0>>), do: false @@ -48,28 +62,36 @@ defmodule SsbBfe.Decoder do decode_generic(generic, tf_tag) end - # Matches generic string. def decode_generic(str, <<6, 0>>) do [_, str_data] = :binary.split(str, <<6, 0>>) str_data end - # Matches generic bytes. - def decode_generic(bytes, <<6, 3>>), do: bytes - - def decode_msg(msg) do - tf_tag = binary_part(msg, 0, 2) - decode_msg(msg, tf_tag) + def decode_generic(bytes, <<6, 3>>) do + [_, bytes] = :binary.split(bytes, <<6, 3>>) + bytes end - # Matches classic message. - def decode_msg(msg, <<1, 0>>) do - encoded_base64_data = extract_base64_data(msg, <<1, 0>>) + @doc """ + Take a message ID as an encoded binary and return the decoded string representing the TFD. + + `decode_msg/1` calls the appropriate `decode_msg/2` clause based on the value of the TF tag extracted from the encoded message. + """ + def decode_msg(msg_id) do + tf_tag = binary_part(msg_id, 0, 2) + decode_msg(msg_id, tf_tag) + end + + def decode_msg(msg_id, <<1, 0>>) do + encoded_base64_data = extract_base64_data(msg_id, <<1, 0>>) "%" <> encoded_base64_data <> ".sha256" end - def decode_sig(sig) do - encoded_base64_data = extract_base64_data(sig, <<4, 0>>) + @doc """ + Take a signature ID as an encoded binary, extract and encode the base64 data and return the dedoded string representing the TFD. + """ + def decode_sig(sig_id) do + encoded_base64_data = extract_base64_data(sig_id, <<4, 0>>) encoded_base64_data <> ".sig.ed25519" end end diff --git a/lib/ssb_bfe/encoder.ex b/lib/ssb_bfe/encoder.ex index 77db783..ae96ecd 100644 --- a/lib/ssb_bfe/encoder.ex +++ b/lib/ssb_bfe/encoder.ex @@ -11,42 +11,65 @@ defmodule SsbBfe.Encoder do Base.decode64(base64_data) end + @doc """ + Take a blob ID as a string, match on the type-format tag, extract and decode the base64 data and return the encoded bytes representing the TFD. + """ def encode_blob(blob_id) do blob_tf_tag = SsbBfe.Types.get_blob_type(blob_id) {:ok, decoded_base64_data} = extract_base64_data(blob_id) blob_tf_tag <> decoded_base64_data end + @doc """ + Take a boolean value and return the encoded bytes representing the TFD. + """ def encode_bool(true), do: <<6, 1, 1>> def encode_bool(false), do: <<6, 1, 0>> + @doc """ + Take an encrypted box as a string, match on the type-format tag, extract and decode the base64 data and return the encoded bytes representing the TFD. + """ def encode_box(box_str) do box_tf_tag = SsbBfe.Types.get_box_type(box_str) {:ok, decoded_base64_data} = extract_base64_data(box_str, ".") box_tf_tag <> decoded_base64_data end + @doc """ + Take a feed ID as a string, match on the type-format tag, extract and decode the base64 data and return the encoded bytes representing the TFD. + """ def encode_feed(feed_id) do feed_tf_tag = SsbBfe.Types.get_feed_type(feed_id) {:ok, decoded_base64_data} = extract_base64_data(feed_id) feed_tf_tag <> decoded_base64_data end + @doc """ + Take a message ID as a string, match on the type-format tag, extract and decode the base64 data and return the encoded bytes representing the TFD. + """ def encode_msg(msg_id) do msg_tf_tag = SsbBfe.Types.get_msg_type(msg_id) {:ok, decoded_base64_data} = extract_base64_data(msg_id) msg_tf_tag <> decoded_base64_data end + @doc """ + Take a `nil` value and return the encoded bytes representing the TFD. + """ def encode_nil(), do: <<6, 2>> - def encode_sig(sig) do - {:ok, decoded_base64_data} = extract_base64_data(sig, ".sig.ed25519") - <<4, 0>> <> decoded_base64_data + @doc """ + Take a signature ID as a string, match on the type-format tag, extract and decode the base64 data and return the encoded bytes representing the TFD. + """ + def encode_sig(sig_id) do + sig_tf_tag = SsbBfe.Types.get_sig_type(sig_id) + {:ok, decoded_base64_data} = extract_base64_data(sig_id, ".sig.ed25519") + sig_tf_tag <> decoded_base64_data end + @doc """ + Take a string value and return the encoded bytes representing the TFD. + """ def encode_str(str), do: <<6, 0>> <> str - - # def encode_uri(uri) -end +end diff --git a/lib/ssb_bfe/types.ex b/lib/ssb_bfe/types.ex index f3eb12c..793ea29 100644 --- a/lib/ssb_bfe/types.ex +++ b/lib/ssb_bfe/types.ex @@ -1,17 +1,21 @@ defmodule SsbBfe.Types do - @doc ~S""" + @doc """ Take a blob ID as a string and return the encoded bytes representing the blob - type-format. Return `nil` if the ID does not end with `.sha256`. + type-format. Throw an error if the ID does not end with `.sha256`. """ def get_blob_type(blob_id) do - if String.ends_with?(blob_id, ".sha256") do - <<2, 0>> + cond do + String.ends_with?(blob_id, ".sha256") -> + <<2, 0>> + + true -> + throw({:unknown_format, blob_id}) end end - @doc ~S""" + @doc """ Take a box as a string and return the encoded bytes representing the box - type-format. Return `nil` if the ID does not end with `.box` or `.box2`. + type-format. Throw an error if the ID does not end with `.box` or `.box2`. """ def get_box_type(boxed_str) do cond do @@ -22,23 +26,27 @@ defmodule SsbBfe.Types do <<5, 1>> true -> - nil + throw({:unknown_format, boxed_str}) end end - @doc ~S""" + @doc """ Take a feed ID (key) as a string and return the encoded bytes representing - the feed type-format. Return `nil` if the ID does not end with `.ed25519`. + the feed type-format. Throw an error if the ID does not end with `.ed25519`. """ def get_feed_type(feed_id) do - if String.ends_with?(feed_id, ".ed25519") do - <<0, 0>> + cond do + String.ends_with?(feed_id, ".ed25519") -> + <<0, 0>> + + true -> + throw({:unknown_format, feed_id}) end end - @doc ~S""" + @doc """ Take a message ID as a string and return the encoded bytes representing - the message type-format. Return `nil` if the ID does not end with `.sha256` + the message type-format. Throw an error if the ID does not end with `.sha256` or `.cloaked`. """ def get_msg_type(msg_id) do @@ -50,7 +58,21 @@ defmodule SsbBfe.Types do <<1, 2>> true -> - nil + throw({:unknown_format, msg_id}) + end + end + + @doc """ + Take a signature ID as a string and return the encoded bytes representing + the signature type-format. Throw an error if the ID does not end with `.sig.ed25519`. + """ + def get_sig_type(sig_id) do + cond do + String.ends_with?(sig_id, ".sig.ed25519") -> + <<4, 0>> + + true -> + throw({:unknown_format, sig_id}) end end end diff --git a/test/ssb_bfe_test.exs b/test/ssb_bfe_test.exs index 656ff65..f6dc05a 100644 --- a/test/ssb_bfe_test.exs +++ b/test/ssb_bfe_test.exs @@ -5,22 +5,39 @@ defmodule SsbBfeTest do setup do [ blob: "&S7+CwHM6dZ9si5Vn4ftpk/l/ldbRMqzzJos+spZbWf4=.sha256", - blob_encoded: <<2, 0, 75, 191, 130, 192, 115, 58, 117, 159, 108, 139, 149, 103, 225, 251, 105, 147, 249, 127, 149, 214, 209, 50, 172, 243, 38, 139, 62, 178, 150, 91, 89, 254>>, + blob_encoded: + <<2, 0, 75, 191, 130, 192, 115, 58, 117, 159, 108, 139, 149, 103, 225, 251, 105, 147, 249, + 127, 149, 214, 209, 50, 172, 243, 38, 139, 62, 178, 150, 91, 89, 254>>, bool_true_encoded: <<6, 1, 1>>, bool_false_encoded: <<6, 1, 0>>, box1: "bG92ZSBjb2xsYXBzZXMgc3BhY2V0aW1l.box", - box1_encoded: <<5, 0, 108, 111, 118, 101, 32, 99, 111, 108, 108, 97, 112, 115, 101, 115, 32, 115, 112, 97, 99, 101, 116, 105, 109, 101>>, + box1_encoded: + <<5, 0, 108, 111, 118, 101, 32, 99, 111, 108, 108, 97, 112, 115, 101, 115, 32, 115, 112, + 97, 99, 101, 116, 105, 109, 101>>, box2: "bG92ZSBjb2xsYXBzZXMgc3BhY2V0aW1l.box2", - box2_encoded: <<5, 1, 108, 111, 118, 101, 32, 99, 111, 108, 108, 97, 112, 115, 101, 115, 32, 115, 112, 97, 99, 101, 116, 105, 109, 101>>, + box2_encoded: + <<5, 1, 108, 111, 118, 101, 32, 99, 111, 108, 108, 97, 112, 115, 101, 115, 32, 115, 112, + 97, 99, 101, 116, 105, 109, 101>>, feed_classic: "@d/zDvFswFbQaYJc03i47C9CgDev+/A8QQSfG5l/SEfw=.ed25519", - feed_classic_encoded: <<0, 0, 119, 252, 195, 188, 91, 48, 21, 180, 26, 96, 151, 52, 222, 46, 59, 11, 208, 160, 13, 235, 254, 252, 15, 16, 65, 39, 198, 230, 95, 210, 17, 252>>, + feed_classic_encoded: + <<0, 0, 119, 252, 195, 188, 91, 48, 21, 180, 26, 96, 151, 52, 222, 46, 59, 11, 208, 160, + 13, 235, 254, 252, 15, 16, 65, 39, 198, 230, 95, 210, 17, 252>>, msg_classic: "%R8heq/tQoxEIPkWf0Kxn1nCm/CsxG2CDpUYnAvdbXY8=.sha256", - msg_classic_encoded: <<1, 0, 71, 200, 94, 171, 251, 80, 163, 17, 8, 62, 69, 159, 208, 172, 103, 214, 112, 166, 252, 43, 49, 27, 96, 131, 165, 70, 39, 2, 247, 91, 93, 143>>, + msg_classic_encoded: + <<1, 0, 71, 200, 94, 171, 251, 80, 163, 17, 8, 62, 69, 159, 208, 172, 103, 214, 112, 166, + 252, 43, 49, 27, 96, 131, 165, 70, 39, 2, 247, 91, 93, 143>>, nil_encoded: <<6, 2>>, - sig: "nkY4Wsn9feosxvX7bpLK7OxjdSrw6gSL8sun1n2TMLXKySYK9L5itVQnV2nQUctFsrUOa2istD2vDk1B0uAMBQ==.sig.ed25519", - sig_encoded: <<4, 0, 158, 70, 56, 90, 201, 253, 125, 234, 44, 198, 245, 251, 110, 146, 202, 236, 236, 99, 117, 42, 240, 234, 4, 139, 242, 203, 167, 214, 125, 147, 48, 181, 202, 201, 38, 10, 244, 190, 98, 181, 84, 39, 87, 105, 208, 81, 203, 69, 178, 181, 14, 107, 104, 172, 180, 61, 175, 14, 77, 65, 210, 224, 12, 5>>, + sig: + "nkY4Wsn9feosxvX7bpLK7OxjdSrw6gSL8sun1n2TMLXKySYK9L5itVQnV2nQUctFsrUOa2istD2vDk1B0uAMBQ==.sig.ed25519", + sig_encoded: + <<4, 0, 158, 70, 56, 90, 201, 253, 125, 234, 44, 198, 245, 251, 110, 146, 202, 236, 236, + 99, 117, 42, 240, 234, 4, 139, 242, 203, 167, 214, 125, 147, 48, 181, 202, 201, 38, 10, + 244, 190, 98, 181, 84, 39, 87, 105, 208, 81, 203, 69, 178, 181, 14, 107, 104, 172, 180, + 61, 175, 14, 77, 65, 210, 224, 12, 5>>, str: "golden ripples in the meshwork", - str_encoded: <<6, 0, 103, 111, 108, 100, 101, 110, 32, 114, 105, 112, 112, 108, 101, 115, 32, 105, 110, 32, 116, 104, 101, 32, 109, 101, 115, 104, 119, 111, 114, 107>> + str_encoded: + <<6, 0, 103, 111, 108, 100, 101, 110, 32, 114, 105, 112, 112, 108, 101, 115, 32, 105, 110, + 32, 116, 104, 101, 32, 109, 101, 115, 104, 119, 111, 114, 107>> ] end @@ -28,7 +45,7 @@ defmodule SsbBfeTest do test "classic feed is encoded correctly", context do encoded_feed = SsbBfe.encode(context.feed_classic) - + assert encoded_feed == context.feed_classic_encoded end @@ -91,7 +108,10 @@ defmodule SsbBfeTest do test "map is encoded correctly", context do encoded_map = SsbBfe.encode(%{"bool" => false, "feed" => context.feed_classic}) - assert encoded_map == %{"bool" => context.bool_false_encoded, "feed" => context.feed_classic_encoded} + assert encoded_map == %{ + "bool" => context.bool_false_encoded, + "feed" => context.feed_classic_encoded + } end test "tuple is encoded correctly", context do @@ -131,7 +151,7 @@ defmodule SsbBfeTest do assert decoded_box == context.box1 end - + test "box2 is decoded correctly", context do decoded_box2 = SsbBfe.decode(context.box2_encoded) @@ -159,17 +179,22 @@ defmodule SsbBfeTest do end test "list is decoded correctly", context do - decoded_list = SsbBfe.decode([context.bool_true_encoded, context.nil_encoded, context.str_encoded]) + decoded_list = + SsbBfe.decode([context.bool_true_encoded, context.nil_encoded, context.str_encoded]) assert decoded_list == [true, nil, context.str] end test "map is decoded correctly", context do - decoded_map = SsbBfe.decode(%{"bool" => context.bool_false_encoded, "feed" => context.feed_classic_encoded}) + decoded_map = + SsbBfe.decode(%{ + "bool" => context.bool_false_encoded, + "feed" => context.feed_classic_encoded + }) assert decoded_map == %{"bool" => false, "feed" => context.feed_classic} end - + test "tuple is decoded correctly", context do decoded_tuple = SsbBfe.decode({7, context.msg_classic_encoded})