defmodule Drm do
@moduledoc """
A Digital Rights Management System for Elixir Applications/Modules.
Drm loads valid license files into genservers and then dispatches incoming requests to them for validation.
In this model we give each different otp application a fingerprint/hash, and associate it with a license key.
Here we are creating a new license for an otp app which we reference as "umbrella-app-hash-id", (this creates the license file, and loads it into the licensing server), then we check to see if the license is still valid
## Examples
iex> license = %{hash: "license-key12", meta: %{email: "demo@example.com", name: "licensee name"}, policy: %{name: "policy name", type: "free", expiration: nil, validation_type: "strict", checkin: false, checkin_interval: nil, max_fingerprints: nil, fingerprint: "umbrella-app-hash-id"}}
iex> License.create(license)
iex> License.fingerprint_valid?(license.policy.fingerprint)
true
"""
alias Drm, as: License
alias Drm.Vault
alias Drm.Schema.License, as: Schema
@default_path Path.expand("../../priv/license", __DIR__)
@doc false
@spec create() :: String.t()
def create() do
{:error, "license cannot be empty"}
end
@doc """
Create a new license
## Parameters
- `hash`: the license key string
- `meta`: a map of meta data to enclude in the license
- `policy`: a map of the main policy for the license
### Parameters
- `name` : the name of the policy
- `type`: the type of policy "free | commercial"
- `expiration`: the license experation date this is a Datetime.t -> int ie. DateTime.utc_now() |> to_unix
- `validation_type`: the validation type "strict | floating | concurrent"
- `checkin`: when to checkin "true | false"
- `checkin_interval`: when to checkin "nil | daily | weekly | monthly"
- `max_fingerprints`: the number of max fingerprints for this license
- `fingerprint`: the fingerprint for this license
### Validation Types
- `strict`: a license that implements the policy will be considered invalid if its machine limit is surpassed
- `floating`: a license that implements the policy will be valid across multiple machines
- `concurrent`: a licensing model, where you allow a set number of machines to be activated at one time, and exceeding that limit may invalidate all current sessions.
### Types
- `free`: a free license
- `commercial`: a free license
## Examples
iex> license = %{hash: "license-key12", meta: %{email: "demo@example.com", name: "licensee name"}, policy: %{name: "policy name", type: "free", expiration: nil, validation_type: "strict", checkin: false, checkin_interval: nil, max_fingerprints: nil, fingerprint: "main-app-name-umbrella-app-hash-id"}}
iex> License.create(license)
true
"""
@spec create(Map.t()) :: String.t()
def create(%{hash: hash, meta: meta, policy: policy}) do
allow_burner_emails = Application.get_env(:drm, :allow_burner_emails, false)
new_license =
case Map.has_key?(meta, "email") do
false ->
%{hash: hash, meta: meta, policy: policy}
true ->
case allow_burner_emails do
false ->
burner = Burnex.is_burner?(meta.email)
case burner do
true -> {:error, "burner emails are not allowed"}
false -> %{hash: hash, meta: meta, policy: policy}
end
true ->
%{hash: hash, meta: meta, policy: policy}
end
end
case new_license do
{:error, error} ->
{:error, error}
nil ->
{:error, "unable to create license encoding error"}
_ ->
path = Application.get_env(:drm, :path, @default_path)
File.mkdir(path)
filename_in_question = generate_filename(new_license)
path_in_question = path <> "/" <> filename_in_question
new_license = case File.exists?(path_in_question) do
true -> Map.put(new_license, :filename, path_in_question)
false -> filename = generate_filename(new_license)
path = path <> "/" <> filename
{_,encoded_license} = encode(new_license)
status = File.write(path, encoded_license)
case status do
:ok -> Map.put(new_license, :filename, filename)
_ -> Map.put(new_license, :filename, "")
end
### make user in db
new_license
end
Drm.License.Supervisor.start_child(new_license)
new_license
end
{_,encoded_license} = encode(new_license)
encoded_license
end
@doc """
Encode a license
## Parameters
- `hash`: the license key string
- `meta`: a map of meta data to enclude in the license
- `policy`: a map of the main policy for the license
### Parameters
- `name` : the name of the policy
- `type`: the type of policy "free | commercial"
- `expiration`: the license experation date this is a Datetime.t -> int ie. DateTime.utc_now() |> to_unix
- `validation_type`: the validation type "strict | floating | concurrent"
- `checkin`: when to checkin "true | false"
- `checkin_interval`: when to checkin "nil | daily | weekly | monthly"
- `max_fingerprints`: the number of max fingerprints for this license
- `fingerprint`: the fingerprint for this license
### Validation Types
- `strict`: a license that implements the policy will be considered invalid if its machine limit is surpassed
- `floating`: a license that implements the policy will be valid across multiple machines
- `concurrent`: a licensing model, where you allow a set number of machines to be activated at one time, and exceeding that limit may invalidate all current sessions.
### Types
- `free`: a free license
- `commercial`: a free license
## Examples
license = %{hash: "license-key", meta: %{email: "demo@example.com", name: "licensee name"}, policy: %{name: "policy name", type: "free", expiration: 55, validation_type: "strict", checkin: false, checkin_interval: nil, max_fingerprints: nil, fingerprint: "main-app-name-umbrella-app-hash-id"}}
License.encode(license)
"""
@spec encode(Map.t()) :: String.t()
def encode(license) do
{status, license} = Jason.encode license
{status, key} = case status do
:ok -> {:ok, Vault.dump(license)}
:error -> {:error, "encryption error"}
end
end
@doc """
Decode a license
## Examples
license_string = "1ASHD7P87VKlA1iC8Q3tdPFCthdeHxSOWS6BQfUv8gsC8yzNg6OeccIErfuKGvRWzzsRyZ7n/0RwE7ZuQCBL4eHPL5zhGCW5JunAKlsorpKdbMWACiv64q/JO3TOCBJSasd0grljX8z2OzKDeEyk7f0xfIleeL0jXfe+rF9/JC4o7vRHTwJS5va6r19fcWWB5u4AxQUw5tsJmcWBVX5TDwTH8WSJr8HK9xto8V6M1DNzNUKf3dLHBr32dVUjM+uNW2W2uy5Cl3LKIPxv+rmwZmTBZ/1kX8VrqE1BXCM7HttiwzmBEmbQJrvcnY5CAiO562HJTAM6C7RFsHGOtrwWINRzCkMxOffAeuHYy6G9S+ngasJBR/0a39HcA2Ic4mz5"
License.decode(license_string)
"""
@spec decode(String.t()) :: Map.t()
def decode(license) do
base64? = is_base64?(license)
case base64? do
false ->
{:error, "Encoding Error"}
true ->
{_,encoded_license} = Base.decode64(license)
{status, decrypted} =
case Vault.load(encoded_license) do
{status, decrypted} -> {status, decrypted}
v -> {:ok, v}
end
case status == :ok do
true ->
decoded = Jason.decode!(decrypted)
struct = Schema.from_json(decoded)
{:ok, struct}
false ->
{:error, "Encoding Error"}
end
end
end
@doc """
Delete a license by filename
## Examples
iex> License.delete("3454453444")
{:error, :enoent}
"""
@spec delete(String.t()) :: any()
def delete(file) do
path = Application.get_env(:drm, :path, @default_path)
filename = path <> "/" <> file <> ".key"
File.rm(filename)
end
@doc """
Validate that a license struct is valid
## Examples
license = %{hash: "license-key", meta: %{email: "demo@example.com", name: "licensee name"}, policy: %{name: "policy name", type: "free", expiration: 55, validation_type: "strict", checkin: false, checkin_interval: nil, max_fingerprints: nil, fingerprint: "main-app-name-umbrella-app-hash-id"}}
License.is_valid?(license)
"""
def is_valid?(license) do
expiration = license.policy.expiration
current_date = DateTime.to_unix(DateTime.utc_now())
valid_exp =
case expiration do
nil ->
true
_ ->
current_date < expiration
end
end
@doc """
Validate that a license struct is valid and matches the fingerprint
## Examples
license = %{hash: "license-key", meta: %{email: "demo@example.com", name: "licensee name"}, policy: %{name: "policy name", type: "free", expiration: 55, validation_type: "strict", checkin: false, checkin_interval: nil, max_fingerprints: nil, fingerprint: "main-app-name-umbrella-app-hash-id"}}
fingerprint = "main-app-name-umbrella-app-hash-id"
License.is_valid?(license, fingerprint)
"""
def is_valid?(license, fingerprint_in_question) do
expiration = license.policy.expiration
fingerprint = license.policy.fingerprint
current_date = DateTime.to_unix(DateTime.utc_now())
valid_exp =
case expiration do
nil ->
true
_ ->
current_date < expiration
end
case fingerprint do
nil ->
true
_ ->
valid_exp
end
end
@doc """
Validate an encrypted license string
## Examples
iex> license_string = "3454453444"
iex> License.valid?(license_string)
false
"""
@spec valid?(String.t()) :: any()
def valid?(license_string) do
base64? = is_base64?(license_string)
case base64? do
false ->
false
true ->
{status, decrypted} = Vault.load(license_string)
case status do
:ok ->
json = Jason.decode!(decrypted)
struct = Schema.from_json(json)
expiration = struct.policy.experation
current_date = DateTime.to_unix(DateTime.utc_now())
valid_exp =
case expiration do
nil ->
true
_ ->
current_date > expiration
end
:error ->
false
end
end
end
@doc """
Validate that an encrypted license is valid and matches the fingerprint
## Examples
iex> license_string = "3454453444"
iex> fingerprint = "umbrella-app-id"
iex> License.valid?(license_string, fingerprint)
false
"""
@spec valid?(String.t(), String.t()) :: any()
def valid?(license_string, fingerprint_in_question) do
base64? = is_base64?(license_string)
case base64? do
false ->
false
true ->
{status, decrypted} = Vault.load(license_string)
case status do
:ok ->
json = Jason.decode!(decrypted)
struct = Schema.from_json(json)
expiration = struct.policy.experation
fingerprint = struct.policy.fingerprint
current_date = DateTime.to_unix(DateTime.utc_now())
valid_exp =
case expiration do
nil ->
true
_ ->
current_date > expiration
end
case fingerprint do
nil ->
true
:error ->
false
_ ->
valid_exp
end
end
end
end
@doc """
check if the appid "fingerprint" exists
Examples
iex> fingerprint = "umbrella-app-id"
iex> License.fingerprint_valid?(fingerprint)
false
"""
def fingerprint_valid?(f) do
licenses = Drm.License.Supervisor.get_licenses_by_fingerprint(f)
Enum.count(licenses) > 0
end
@doc """
Export the license file
## Examples
iex> fingerprint = "umbrella-app-id"
iex> License.export(fingerprint)
{:error, "fingerprint not found"}
"""
@spec export(String.t()) :: any()
def export(id, type \\ "list") do
exported = Drm.License.Supervisor.get_licenses_by_fingerprint(id)
case exported do
[export] ->
case type do
"json" ->
Jason.encode!(export)
_ ->
[export]
end
_ ->
{:error, "fingerprint not found"}
end
end
@doc """
Remove all licenses
## Examples
iex> License.clear()
:ok
"""
@spec clear() :: String.t()
def clear() do
path = Application.get_env(:drm, :path, @default_path)
File.rm_rf(path)
File.mkdir(path)
end
@doc """
Generate a license key based on a hash
## Examples
hash = "4424552325453453"
License.generate_key(hash, 2)
"""
@spec generate_key(String.t(), Integer.t(), String.t()) :: any()
def generate_key(hash, number \\ 1, delimeter \\ "-") do
total = String.length(hash)
result = total / number
hash
|> String.codepoints()
|> Enum.chunk_every(round(result))
|> Enum.map(&Enum.join/1)
|> Enum.join(delimeter)
end
@doc """
Export license keys
## Examples
License.export_keys()
"""
@spec export_keys() :: Map.t()
def export_keys() do
%{key: Application.get_env(:drm, :key), salt: Application.get_env(:drm, :salt)}
end
@spec hash_id(Integer.t()) :: String.t()
defp hash_id(number \\ 20) do
Base.encode64(:crypto.strong_rand_bytes(number))
end
@spec is_base64?(String.t()) :: any()
def is_base64?(data) do
status = Base.decode64(data)
case status do
:error -> false
_ -> true
end
end
defp generate_filename(license)do
data = Base.encode64(license.hash)
data <> ".key"
end
end