defmodule Passbook do
@moduledoc """
Documentation for `Passbook`.
"""
@doc """
Generates a signed .pkpass file.
Options:
- `target_path`: Where to generate the .pkpass file. Defaults to tmp folder.
- `pass_name`: The name of the .pkpass file. Defaults to a random 8 char string.
- `delete_raw_pass` - If the raw pass files should be deleted, leaving only the .pkpass file. Defaults to `true`.
## Examples
iex> Passbook.generate(%Passbook.Pass{
background_color: "rgb(23, 187, 82)",
foreground_color: "rgb(100, 10, 110)",
barcode: %Passbook.LowerLevel.Barcode{
format: :qr,
alt_text: "1234",
message: "qr-code-content"
},
description: "This is a pass description",
organization_name: "My Organization",
pass_type_identifier: "123",
serial_number: "serial-number-123",
team_identifier: "team-identifier",
generic: %Passbook.PassStructure{
transit_type: :train,
primary_fields: [
%Passbook.LowerLevel.Field{
key: "my-key",
value: "my-value"
}
]
}}, ["icon.png": "path/to/file.png", "icon@2x.png": "path/to/file.png"], "path/to/wwdr.pem", "path/to/certificate.pem", "path/to/key.pem", "password", target_path: System.tmp_dir!(), pass_name: "mypass")
{:ok, "path/to/generated/mypass.pkpass"}
"""
def generate(
pass,
files,
wwdr_path,
certificate_path,
key_path,
password,
opts \\ []
)
def generate(
%Passbook.Pass{} = pass,
files,
wwdr_path,
certificate_path,
key_path,
password,
opts
) do
# Options setup
default = [
target_path: get_tmp_dir(),
pass_name: :crypto.strong_rand_bytes(16) |> Base.encode16(),
delete_raw_pass: true
]
opts = Keyword.merge(default, opts)
# Make sure target path is created and available
random = :crypto.strong_rand_bytes(16) |> Base.encode16()
target_path = opts[:target_path] <> random <> "/"
File.mkdir_p(Path.dirname(target_path))
# Generate pass.json
pass_json = Passbook.Pass.generate_json(pass)
File.write(target_path <> "pass.json", pass_json)
# Generate manifest.json
manifest_json = create_manifest(["pass.json": target_path <> "pass.json"] ++ files)
File.write(target_path <> "manifest.json", manifest_json)
# Generate signature
create_signature(
target_path <> "manifest.json",
target_path <> "signature",
certificate_path,
key_path,
wwdr_path,
password
)
# Copy all the files to the target folder
Enum.each(files, fn {filename, path} ->
File.copy(path, target_path <> Atom.to_string(filename))
end)
# Zip the files on a .pkpass, and optionally delete them
files =
File.ls!(target_path)
|> Enum.map(&String.to_charlist/1)
pkpass =
:zip.create(to_string(target_path <> "#{opts[:pass_name]}.pkpass"), files,
cwd: String.to_charlist(target_path)
)
if opts[:delete_raw_pass], do: Enum.map(files, &File.rm(target_path <> to_string(&1)))
pkpass
end
def generate(_, _, _, _, _, _, _), do: {:error, :invalid_data}
defp get_tmp_dir() do
tmp_dir = System.tmp_dir!()
cond do
String.slice(tmp_dir, -1..-1) == "/" -> tmp_dir
true -> tmp_dir <> "/"
end
end
defp create_manifest(files) do
for(
{filename, path} <- files,
into: %{},
do: {filename, hash(File.read!(path))}
)
|> Jason.encode!()
end
defp hash(file_content), do: :crypto.hash(:sha, file_content) |> Base.encode16(case: :lower)
defp create_signature(
manifest_path,
signature_path,
certificate_path,
key_path,
wwdr_certificate_path,
password
),
do:
:os.cmd(
'openssl smime -sign -signer #{certificate_path} -inkey #{key_path} -certfile #{wwdr_certificate_path} -in #{manifest_path} -out #{signature_path} -outform der -binary -passin pass:"#{password}"'
)
end