defmodule BSV.PrivKey do
@moduledoc """
A PrivKey is a data structure representing a Bitcoin private key.
Internally, a private key is a secret 256-bit integer within the range of the
ECDSA `secp256k1` parmaeters. Each private key corresponds to a public key
which is a coordinate on the `secp256k1` curve.
"""
import BSV.Util, only: [decode: 2, encode: 2]
defstruct d: nil, compressed: true
@typedoc "Private key struct"
@type t() :: %__MODULE__{
d: privkey_bin(),
compressed: boolean()
}
@typedoc "Private key 256-bit binary"
@type privkey_bin() :: <<_::256>>
@typedoc """
Wallet Import Format private key
WIF encoded keys is a common way to represent private Keys in Bitcoin. WIF
encoded keys are shorter and include a built-in error checking and a type byte.
"""
@type privkey_wif() :: String.t()
@version_bytes %{
main: <<0x80>>,
test: <<0xEF>>
}
@doc """
Generates and returns a new `t:BSV.PrivKey.t/0`.
## Options
The accepted options are:
* `:compressed` - Denotes whether the correspding `t:BSV.PubKey.t/0` is compressed on not. Defaults `true`.
"""
@spec new(keyword()) :: t()
def new(opts \\ []) do
{_pubkey, privkey} = :crypto.generate_key(:ecdh, :secp256k1)
from_binary!(privkey, opts)
end
@doc """
Parses the given binary into a `t:BSV.PrivKey.t/0`.
Returns the result in an `:ok` / `:error` tuple pair.
## Options
The accepted options are:
* `:compressed` - Denotes whether the correspding `t:BSV.PubKey.t/0` is compressed on not. Defaults `true`.
* `:encoding` - Optionally decode the binary with either the `:base64` or `:hex` encoding scheme.
## Examples
iex> PrivKey.from_binary("3cff04633088622e4599dc2ebf843f82cef3463b910d34a752a13622abae379b", encoding: :hex)
{:ok, %PrivKey{
d: <<60, 255, 4, 99, 48, 136, 98, 46, 69, 153, 220, 46, 191, 132, 63, 130, 206, 243, 70, 59, 145, 13, 52, 167, 82, 161, 54, 34, 171, 174, 55, 155>>
}}
"""
@spec from_binary(binary(), keyword()) :: {:ok, t()} | {:error, term()}
def from_binary(privkey, opts \\ []) when is_binary(privkey) do
encoding = Keyword.get(opts, :encoding)
compressed = Keyword.get(opts, :compressed, true)
case decode(privkey, encoding) do
{:ok, <<d::binary-32>>} ->
{:ok, struct(__MODULE__, d: d, compressed: compressed)}
{:ok, d} ->
{:error, {:invalid_privkey, byte_size(d)}}
{:error, error} ->
{:error, error}
end
end
@doc """
Parses the given binary into a `t:BSV.PrivKey.t/0`.
As `from_binary/2` but returns the result or raises an exception.
"""
@spec from_binary!(binary(), keyword()) :: t()
def from_binary!(privkey, opts \\ []) when is_binary(privkey) do
case from_binary(privkey, opts) do
{:ok, privkey} ->
privkey
{:error, error} ->
raise BSV.DecodeError, error
end
end
@doc """
Decodes the given `t:BSV.PrivKey.privkey_wif/0` into a `t:BSV.PrivKey.t/0`.
Returns the result in an `:ok` / `:error` tuple pair.
## Examples
iex> PrivKey.from_wif("KyGHAK8MNohVPdeGPYXveiAbTfLARVrQuJVtd3qMqN41UEnTWDkF")
{:ok, %PrivKey{
d: <<60, 255, 4, 99, 48, 136, 98, 46, 69, 153, 220, 46, 191, 132, 63, 130, 206, 243, 70, 59, 145, 13, 52, 167, 82, 161, 54, 34, 171, 174, 55, 155>>
}}
"""
@spec from_wif(privkey_wif()) :: {:ok, t()} | {:error, term()}
def from_wif(wif) when is_binary(wif) do
version_byte = @version_bytes[BSV.network()]
case B58.decode58_check(wif) do
{:ok, {<<d::binary-32, 1>>, ^version_byte}} ->
{:ok, struct(__MODULE__, d: d, compressed: true)}
{:ok, {<<d::binary-32>>, ^version_byte}} ->
{:ok, struct(__MODULE__, d: d, compressed: false)}
{:ok, {<<d::binary>>, version_byte}} when byte_size(d) in [32,33] ->
{:error, {:invalid_base58_check, version_byte, BSV.network()}}
_error ->
{:error, :invalid_wif}
end
end
@doc """
Decodes the given `t:BSV.PrivKey.privkey_wif/0` into a `t:BSV.PrivKey.t/0`.
As `from_wif/1` but returns the result or raises an exception.
"""
@spec from_wif!(privkey_wif()) :: t()
def from_wif!(wif) when is_binary(wif) do
case from_wif(wif) do
{:ok, privkey} ->
privkey
{:error, error} ->
raise BSV.DecodeError, error
end
end
@doc """
Serialises the given `t:BSV.PrivKey.t/0` into a binary.
## Options
The accepted options are:
* `:encoding` - Optionally encode the binary with either the `:base64` or `:hex` encoding scheme.
## Examples
iex> PrivKey.to_binary(@privkey, encoding: :hex)
"3cff04633088622e4599dc2ebf843f82cef3463b910d34a752a13622abae379b"
"""
@spec to_binary(t()) :: binary()
def to_binary(%__MODULE__{d: d}, opts \\ []) do
encoding = Keyword.get(opts, :encoding)
encode(d, encoding)
end
@doc """
Encodes the given `t:BSV.PrivKey.t/0` as a `t:BSV.PrivKey.privkey_wif/0`.
## Examples
iex> PrivKey.to_wif(@privkey)
"KyGHAK8MNohVPdeGPYXveiAbTfLARVrQuJVtd3qMqN41UEnTWDkF"
"""
@spec to_wif(t()) :: privkey_wif()
def to_wif(%__MODULE__{d: d, compressed: compressed}) do
version_byte = @version_bytes[BSV.network()]
privkey_with_suffix = case compressed do
true -> <<d::binary, 0x01>>
false -> d
end
B58.encode58_check!(privkey_with_suffix, version_byte)
end
end