defmodule AbtDid do @moduledoc """ Generates the DID from secret key or publick key. """ alias AbtDid.TypeBytes alias AbtDid.Type, as: DidType alias Mcrypto.Hasher.Keccak alias Mcrypto.Hasher.Sha3 alias Mcrypto.Hasher.Sha2 @ed25519 %Mcrypto.Signer.Ed25519{} @secp256k1 %Mcrypto.Signer.Secp256k1{} @keccak %Keccak{} @keccak_384 %Keccak{size: 384} @keccak_512 %Keccak{size: 512} @sha3 %Sha3{} @sha3_384 %Sha3{size: 384} @sha3_512 %Sha3{size: 512} @sha2 %Sha2{round: 1} @prefix "did:abt:" @doc """ Generates the DID from secret key. Options: `:form`: Determines the form of DID returned. `:long` - The returned DID will be prefixed by "did:abt:". `:short` - The retuned DID has only DID string. `:encode`: Detemines whether or not encode the DID. `true` - The returned DID will be encoded as Base58. `false` - The returned DID will be in binary format and `:form` will be set as `:short`. ## Examples iex> sk = "3E0F9A313300226D51E33D5D98A126E86396956122E97E32D31CEE2277380B83FF47B3022FA503EAA1E9FA4B20FA8B16694EA56096F3A2E9109714062B3486D9" |> Base.decode16!() iex> AbtDid.sk_to_did(%AbtDid.Type{}, sk) "did:abt:z1ioGHFYiEemfLa3hQjk4JTwWTQPu1g2YxP" iex> sk = "3E0F9A313300226D51E33D5D98A126E86396956122E97E32D31CEE2277380B83FF47B3022FA503EAA1E9FA4B20FA8B16694EA56096F3A2E9109714062B3486D9" |> Base.decode16!() iex> AbtDid.sk_to_did(AbtDid.Type.node, sk) "did:abt:z89nF4GRYvgw5mqk8NqVVC7NeZLWKbcbQY7V" iex> sk = "3E0F9A313300226D51E33D5D98A126E86396956122E97E32D31CEE2277380B83FF47B3022FA503EAA1E9FA4B20FA8B16694EA56096F3A2E9109714062B3486D9" |> Base.decode16!() iex> AbtDid.sk_to_did(AbtDid.Type.validator, sk) "did:abt:zyt2vg6n8424c9xdXLGj1g27finM77ZN5KQL" iex> sk = "3E0F9A313300226D51E33D5D98A126E86396956122E97E32D31CEE2277380B83FF47B3022FA503EAA1E9FA4B20FA8B16694EA56096F3A2E9109714062B3486D9" |> Base.decode16!() iex> AbtDid.sk_to_did(%AbtDid.Type{role_type: :node}, sk) ** (RuntimeError) The hash_type must be :sha2 and key_type must be :ed25519 if the role_type is :node or :validator. iex> sk = "26954E19E8781905E2CF91A18AE4F36A954C142176EE1BC27C2635520C49BC55" |> Base.decode16!() iex> AbtDid.sk_to_did(%AbtDid.Type{role_type: :validator, key_type: :secp256k1}, sk) ** (RuntimeError) The hash_type must be :sha2 and key_type must be :ed25519 if the role_type is :node or :validator. iex> sk = "26954E19E8781905E2CF91A18AE4F36A954C142176EE1BC27C2635520C49BC55" |> Base.decode16!() iex> AbtDid.sk_to_did(%AbtDid.Type{key_type: :secp256k1}, sk) "did:abt:z1Ee1H8g248HqroacmEnZzMYgbhjz1Z2WSvv" iex> sk = "26954E19E8781905E2CF91A18AE4F36A954C142176EE1BC27C2635520C49BC55" |> Base.decode16!() iex> AbtDid.sk_to_did(%AbtDid.Type{key_type: :secp256k1}, sk, form: :short) "z1Ee1H8g248HqroacmEnZzMYgbhjz1Z2WSvv" iex> sk = "26954E19E8781905E2CF91A18AE4F36A954C142176EE1BC27C2635520C49BC55" |> Base.decode16!() iex> AbtDid.sk_to_did(%AbtDid.Type{key_type: :secp256k1}, sk, encode: false) <<0, 33, 228, 184, 246, 38, 116, 137, 126, 215, 93, 240, 247, 53, 110, 130, 198, 249, 166, 74, 92, 19, 243, 204, 12, 211>> """ @spec sk_to_did(DidType.t(), binary(), Keyword.t()) :: String.t() def sk_to_did(did_type, sk, opts \\ []) do pk = sk_to_pk(did_type.key_type, sk) pk_to_did(did_type, pk, opts) end @doc """ Generates the DID from publick key. Options: `:form`: Determines the form of DID returned. `:long` - The returned DID will be prefixed by "did:abt:". `:short` - The retuned DID has only DID string. `:encode`: Detemines whether or not encode the DID. `true` - The returned DID will be encoded as Base58. `false` - The returned DID will be in binary format and `:form` will be set as `:short`. """ @spec pk_to_did(DidType.t(), binary(), Keyword.t()) :: String.t() def pk_to_did(did_type, pk, opts \\ []) do <> = hash(did_type.hash_type, pk) pk_hash_to_did(did_type, pk_hash, opts) end @doc """ Generate the DID from a public key hash. Options: `:form`: Determines the form of DID returned. `:long` - The returned DID will be prefixed by "did:abt:". `:short` - The retuned DID has only DID string. `:encode`: Detemines whether or not encode the DID. `true` - The returned DID will be encoded as Base58. `false` - The returned DID will be in binary format and `:form` will be set as `:short`. ## Examples iex> sk = "3E0F9A313300226D51E33D5D98A126E86396956122E97E32D31CEE2277380B83FF47B3022FA503EAA1E9FA4B20FA8B16694EA56096F3A2E9109714062B3486D9" |> Base.decode16!() iex> did = AbtDid.sk_to_did(AbtDid.Type.validator, sk) iex> pk_hash = AbtDid.get_pubkey_hash(did) iex> AbtDid.pkhash_to_did(:validator, pk_hash) "did:abt:zyt2vg6n8424c9xdXLGj1g27finM77ZN5KQL" iex> pk = "FF47B3022FA503EAA1E9FA4B20FA8B16694EA56096F3A2E9109714062B3486D9" |> Base.decode16!() iex> AbtDid.pk_to_did(AbtDid.Type.node, pk) "did:abt:z89nF4GRYvgw5mqk8NqVVC7NeZLWKbcbQY7V" iex> pk_hash = "D1B287B1ACB71A980568C99A3AB32A8ED1D9C1BB" |> Base.decode16!() iex> AbtDid.pkhash_to_did(:node, pk_hash) "did:abt:z89nF4GRYvgw5mqk8NqVVC7NeZLWKbcbQY7V" """ def pkhash_to_did(_, pk_hash, opts \\ []) @spec pkhash_to_did(:node, binary() | String.t(), Keyword.t()) :: String.t() def pkhash_to_did(:node, pk_hash, opts) do %AbtDid.Type{role_type: :node, hash_type: :sha2, key_type: :ed25519} |> pk_hash_to_did(pk_hash, opts) end @spec pkhash_to_did(:validator, binary() | String.t(), Keyword.t()) :: String.t() def pkhash_to_did(:validator, pk_hash, opts) do %AbtDid.Type{role_type: :validator, hash_type: :sha2, key_type: :ed25519} |> pk_hash_to_did(pk_hash, opts) end @spec pkhash_to_did(:tether, binary() | String.t(), Keyword.t()) :: String.t() def pkhash_to_did(:tether, pk_hash, opts) do %AbtDid.Type{role_type: :tether, hash_type: :sha2, key_type: :ed25519} |> pk_hash_to_did(pk_hash, opts) end @doc """ Verifies if a public key and a DID match with each other. """ @spec match_pk?(String.t(), binary()) :: boolean() def match_pk?(@prefix <> did, pk), do: match_pk?(did, pk) def match_pk?(did, pk) do <> = Multibase.decode!(did) did_type = TypeBytes.bytes_to_struct(type_bytes) pk_to_did(did_type, pk) == @prefix <> did rescue _ -> false end @doc """ Verifies if a DID is valid by checking the checksum. ## Examples iex> AbtDid.is_valid?("did:abt:z1muQ3xqHQK2uiACHyChikobsiY5kLqtShA") true iex> AbtDid.is_valid?("z1muQ3xqHQK2uiACHyChikobsiY5kLqtShA") true iex> AbtDid.is_valid?("z2muQ3xqHQK2uiACHyChikobsiY5kLqtShA") false iex> AbtDid.is_valid?("z1muQ3xqHQK2uiACHyChikobsiY5kLqtSha") false """ def is_valid?(@prefix <> did), do: is_valid?(did) def is_valid?(did) do <> = Multibase.decode!(did) did_type = TypeBytes.bytes_to_struct(type_bytes) <> = hash(did_type.hash_type, type_bytes <> pk_hash) expected == actual rescue _ -> false end @doc """ Gets the DID type information from the DID. ## Examples iex> AbtDid.get_did_type("did:abt:z1muQ3xqHQK2uiACHyChikobsiY5kLqtShA") %AbtDid.Type{hash_type: :sha3, key_type: :ed25519, role_type: :account} iex> AbtDid.get_did_type("z1muQ3xqHQK2uiACHyChikobsiY5kLqtShA") %AbtDid.Type{hash_type: :sha3, key_type: :ed25519, role_type: :account} """ @spec get_did_type(String.t()) :: DidType.t() def get_did_type(@prefix <> did), do: get_did_type(did) def get_did_type(did) do <> = Multibase.decode!(did) TypeBytes.bytes_to_struct(type_bytes) end @doc """ Gets the public key hash of this DID. ## Examples iex> pk = <<136, 159, 157, 15, 85, 1, 98, 93, 76, 139, 60, 21, 243, 144, 249, 180, 60, 69, 140, 215, 195, 6, 33, 122, 117, 140, 241, 209, 47, 83, 173, 77>> iex> did = AbtDid.pk_to_did(AbtDid.Type.validator, pk) iex> AbtDid.get_pubkey_hash(did) "BB6FD53B8B12E79CE94768B0349836AB9ED81D85" """ @spec get_pubkey_hash(<<_::8, _::_*8>>, keyword()) :: binary() def get_pubkey_hash(did, opts \\ [encode: true]) def get_pubkey_hash(@prefix <> did, opts), do: get_pubkey_hash(did, opts) def get_pubkey_hash(did, opts) do <<_::binary-size(2), pubkey_hash::binary-size(20), _::binary-size(4)>> = Multibase.decode!(did) case Keyword.get(opts, :encode, true) do true -> Base.encode16(pubkey_hash) _ -> pubkey_hash end end ############ private functiosn ############ # Options: # `:form`: Determines the form of DID returned. `:long` - The returned DID will be prefixed by "did:abt:". `:short` - The retuned DID has only DID string. # `:encode`: Detemines whether or not encode the DID. `true` - The returned DID will be encoded as Base58. `false` - The returned DID will be in binary format and `:form` will be set as `:short`. @spec pk_hash_to_did(DidType.t(), binary() | String.t(), Keyword.t()) :: String.t() defp pk_hash_to_did(did_type, pk_hash, opts) do pk_hash_bin = if byte_size(pk_hash) == 40 and String.valid?(pk_hash) do Base.decode16!(pk_hash, case: :mixed) else pk_hash end do_pk_hash_to_did(did_type, pk_hash_bin, opts) end # Options: # `:form`: Determines the form of DID returned. `:long` - The returned DID will be prefixed by "did:abt:". `:short` - The retuned DID has only DID string. # `:encode`: Detemines whether or not encode the DID. `true` - The returned DID will be encoded as Base58. `false` - The returned DID will be in binary format and `:form` will be set as `:short`. @spec do_pk_hash_to_did(DidType.t(), binary(), Keyword.t()) :: String.t() defp do_pk_hash_to_did(%DidType{} = did_type, pk_hash, opts) do AbtDid.Type.check_did_type!(did_type) type_bytes = TypeBytes.struct_to_bytes(did_type) <> = hash(did_type.hash_type, type_bytes <> pk_hash) encode = Keyword.get(opts, :encode, true) form = Keyword.get(opts, :form, :long) case encode do false -> type_bytes <> pk_hash <> check_sum true -> case form do :long -> @prefix <> Multibase.encode!(type_bytes <> pk_hash <> check_sum, :base58_btc) :short -> Multibase.encode!(type_bytes <> pk_hash <> check_sum, :base58_btc) end end end defp sk_to_pk(:ed25519, sk), do: Mcrypto.sk_to_pk(@ed25519, sk) defp sk_to_pk(:secp256k1, sk), do: Mcrypto.sk_to_pk(@secp256k1, sk) defp hash(:keccak, data), do: Mcrypto.hash(@keccak, data) defp hash(:sha3, data), do: Mcrypto.hash(@sha3, data) defp hash(:keccak_384, data), do: Mcrypto.hash(@keccak_384, data) defp hash(:sha3_384, data), do: Mcrypto.hash(@sha3_384, data) defp hash(:keccak_512, data), do: Mcrypto.hash(@keccak_512, data) defp hash(:sha3_512, data), do: Mcrypto.hash(@sha3_512, data) defp hash(:sha2, data), do: Mcrypto.hash(@sha2, data) end