defmodule NPM.Integrity do
@moduledoc """
Verify package integrity using Subresource Integrity (SRI) hashes.
npm packages include integrity strings in the format:
`algorithm-base64hash` (e.g. `sha512-abc123...`).
"""
@doc """
Verify data against an SRI integrity string.
Returns `:ok` if the hash matches, `{:error, :integrity_mismatch}` otherwise.
Returns `:ok` for empty integrity strings (no verification).
"""
@spec verify(binary(), String.t()) :: :ok | {:error, :integrity_mismatch}
def verify(_data, ""), do: :ok
def verify(_data, nil), do: :ok
def verify(data, integrity) do
if compute_and_match?(data, integrity), do: :ok, else: {:error, :integrity_mismatch}
end
@doc """
Compute the SHA-512 SRI integrity string for binary data.
"""
@spec compute_sha512(binary()) :: String.t()
def compute_sha512(data) do
hash = :crypto.hash(:sha512, data) |> Base.encode64()
"sha512-#{hash}"
end
@doc """
Compute the SHA-256 SRI integrity string for binary data.
"""
@spec compute_sha256(binary()) :: String.t()
def compute_sha256(data) do
hash = :crypto.hash(:sha256, data) |> Base.encode64()
"sha256-#{hash}"
end
@doc """
Parse an SRI string into `{algorithm, hash}` tuple.
"""
@spec parse(String.t()) :: {:ok, {String.t(), String.t()}} | :error
def parse(integrity) when is_binary(integrity) do
case String.split(integrity, "-", parts: 2) do
[algo, hash] when algo in ["sha256", "sha384", "sha512"] -> {:ok, {algo, hash}}
_ -> :error
end
end
def parse(_), do: :error
@doc """
Get the algorithm used in an SRI string.
"""
@spec algorithm(String.t()) :: String.t() | nil
def algorithm(integrity) do
case parse(integrity) do
{:ok, {algo, _}} -> algo
:error -> nil
end
end
defp compute_and_match?(data, integrity) do
case parse(integrity) do
{:ok, {algo, expected_hash}} ->
actual_hash = hash_data(algo, data)
actual_hash == expected_hash
:error ->
false
end
end
defp hash_data("sha256", data), do: :crypto.hash(:sha256, data) |> Base.encode64()
defp hash_data("sha384", data), do: :crypto.hash(:sha384, data) |> Base.encode64()
defp hash_data("sha512", data), do: :crypto.hash(:sha512, data) |> Base.encode64()
defp hash_data(_, _data), do: ""
end