defmodule BSV.Message do
@moduledoc """
The Message module provides functions for encrypting, decrypting, signing and
verifying arbitrary messages using Bitcoin keys.
Message encryption uses the Electrum-compatible BIE1 ECIES algorithm. Message
signing uses the Bitcoin Signed Message algorithm. Both alorithms are broadly
supported by popular BSV libraries in other languages.
## Encryption
A sender encrypts the message using the recipient's PubKey. The recipient
decrypts the message with their PrivKey.
iex> msg = "Secret test message"
iex> encrypted = Message.encrypt(msg, @bob_keypair.pubkey)
iex> Message.decrypt(encrypted, @bob_keypair.privkey)
{:ok, "Secret test message"}
## Signing
A sender signs a message with their PrivKey. The recipient verifies the
message using the sender's PubKey.
iex> msg = "Secret test message"
iex> sig = Message.sign(msg, @alice_keypair.privkey)
iex> Message.verify(sig, msg, @alice_keypair.pubkey)
true
"""
alias BSV.{Address, Hash, KeyPair, PrivKey, PubKey, VarInt}
import BSV.Util, only: [decode: 2, decode!: 2, encode: 2]
@doc """
Decrypts the given message with the private key.
## Options
The accepted options are:
* `:encoding` - Optionally decode the binary with either the `:base64` or `:hex` encoding scheme.
"""
@spec decrypt(binary(), PrivKey.t(), keyword()) ::
{:ok, binary()} |
{:error, term()}
def decrypt(data, %PrivKey{} = privkey, opts \\ []) do
encoding = Keyword.get(opts, :encoding)
encrypted = decode!(data, encoding)
len = byte_size(encrypted) - 69
<<
"BIE1", # magic bytes
ephemeral_pubkey::binary-33, # ephermeral pubkey
ciphertext::binary-size(len), # ciphertext
mac::binary-32 # mac hash
>> = encrypted
<<d::big-256>> = privkey.d
# Derive ECDH key and sha512 hash
ecdh_point = ephemeral_pubkey
|> PubKey.from_binary!()
|> Map.get(:point)
|> Curvy.Point.mul(d)
key_hash = %PubKey{point: ecdh_point}
|> PubKey.to_binary()
|> Hash.sha512()
# iv and enc_key used in AES, mac_key used in HMAC
<<iv::binary-16, enc_key::binary-16, mac_key::binary-32>> = key_hash
with ^mac <- Hash.sha256_hmac("BIE1" <> ephemeral_pubkey <> ciphertext, mac_key),
msg when is_binary(msg) <- :crypto.crypto_one_time(:aes_128_cbc, enc_key, iv, ciphertext, false)
do
{:ok, pkcs7_unpad(msg)}
end
end
@doc """
Encrypts the given message with the public key.
## Options
The accepted options are:
* `:encoding` - Optionally encode the binary with either the `:base64` or `:hex` encoding scheme.
"""
@spec encrypt(binary(), PubKey.t(), keyword()) :: binary()
def encrypt(message, %PubKey{} = pubkey, opts \\ []) do
encoding = Keyword.get(opts, :encoding)
# Generate ephemeral keypair
ephemeral_key = KeyPair.new()
<<d::big-256>> = ephemeral_key.privkey.d
# Derive ECDH key and sha512 hash
ecdh_point = pubkey.point
|> Curvy.Point.mul(d)
key_hash = %PubKey{point: ecdh_point}
|> PubKey.to_binary()
|> Hash.sha512()
# iv and enc_key used in AES, mac_key used in HMAC
<<iv::binary-16, enc_key::binary-16, mac_key::binary-32>> = key_hash
cyphertext = :crypto.crypto_one_time(:aes_128_cbc, enc_key, iv, pkcs7_pad(message), true)
encrypted = "BIE1" <> PubKey.to_binary(ephemeral_key.pubkey) <> cyphertext
mac = Hash.sha256_hmac(encrypted, mac_key)
encode(encrypted <> mac, encoding)
end
@doc """
Signs the given message with the PrivKey.
By default signatures are returned `base64` encoded. Use the `encoding: :raw`
option to return a raw binary signature.
## Options
The accepted options are:
* `:encoding` - Encode the binary with either the `:base64`, `:hex` or `:raw` encoding scheme.
"""
@spec sign(binary(), PrivKey.t(), keyword()) :: binary()
def sign(message, %PrivKey{} = privkey, opts \\ []) do
opts = opts
|> Keyword.put_new(:encoding, :base64)
|> Keyword.merge([
compact: true,
compressed: privkey.compressed,
hash: false
])
message
|> bsm_digest()
|> Curvy.sign(privkey.d, opts)
end
@doc """
Verifies the given signature against the given message using the PubKey.
By default signatures are assumed to be `base64` encoded. Use the `:encoding`
option to specify a different signature encoding.
## Options
The accepted options are:
* `:encoding` - Decode the signature with either the `:base64`, `:hex` or `:raw` encoding scheme.
"""
@spec verify(binary(), binary(), PubKey.t() | Address.t(), keyword()) ::
boolean() |
{:error, term()}
def verify(signature, message, pubkey_or_address, opts \\ []) do
encoding = Keyword.get(opts, :encoding, :base64)
with {:ok, sig} <- decode(signature, encoding) do
case do_verify(sig, bsm_digest(message), pubkey_or_address) do
res when is_boolean(res) -> res
:error -> false
error -> error
end
end
end
# Handles signature verification with address or pubkey
def do_verify(sig, message, %Address{} = address) do
with %Curvy.Key{} = key <- Curvy.recover_key(sig, message, hash: false),
^address <- Address.from_pubkey(%PubKey{point: key.point})
do
Curvy.verify(sig, message, key, hash: false)
end
end
def do_verify(sig, message, %PubKey{} = pubkey) do
Curvy.verify(sig, message, PubKey.to_binary(pubkey), hash: false)
end
# Prefixes the message with magic bytes and hashes
defp bsm_digest(msg) do
prefix = "Bitcoin Signed Message:\n"
b1 = VarInt.encode(byte_size(prefix))
b2 = VarInt.encode(byte_size(msg))
Hash.sha256_sha256(<<b1::binary, prefix::binary, b2::binary, msg::binary>>)
end
# Pads the message using PKCS7
defp pkcs7_pad(msg) do
case rem(byte_size(msg), 16) do
0 -> msg
pad ->
pad = 16 - pad
msg <> :binary.copy(<<pad>>, pad)
end
end
# Unpads the message using PKCS7
defp pkcs7_unpad(msg) do
case :binary.last(msg) do
pad when 0 < pad and pad < 16 ->
:binary.part(msg, 0, byte_size(msg) - pad)
_ ->
msg
end
end
end