Lua Plugins Reference

Updated: March 7th, 2018

Language

Lua plugins are run using the standard Lua 5.3 interpreter.

Plugins may use the following standard Lua 5.3 symbols:

  • assert
  • bit32
  • collectgarbage
  • coroutine
  • error
  • getmetatable
  • ipairs
  • load
  • math
  • next
  • pairs
  • pcall
  • rawequal
  • rawget
  • rawlen
  • rawset
  • select
  • setmetatable
  • string
  • table
  • tonumber
  • tostring
  • type
  • utf8
  • xpcall
  • _VERSION
  • _G

The following will be supported soon:

  • print
  • require

Support is not planned for the following:

  • loadfile
  • arg
  • debug
  • dofile
  • package
  • os
  • io

The following SmartKey-defined symbols are also in scope, detailed below:

  • cbor
  • json
  • Error
  • Blob
  • EntityId
  • Sobject
  • App
  • User
  • Plugin
  • Group
  • AuditLog
  • principal
  • this_plugin
  • digest
  • null

Invoking plugins

A plugin must define a function run.

When the plugin is invoked, run will be called with the following parameters:

  • input: The JSON data with which the plugin was invoked, decoded into a Lua object as in json.decode, or nil if no input data was supplied.
  • url: The URL that was used to invoke the plugin, represented as a Lua table:
    • url.path: A Lua array of path components, for example, ['sys', 'v1', 'plugins', '08ce8c3e-50fc-4c95-a633-ca417e6f2828']. Users and apps may append arbitrary custom path components when invoking a plugin.
    • url.query: The query string (the part of the URL after ?), or nil if the URL does not include a query string.
  • method: The method used to invoke the plugin, either 'POST' or 'GET'.

The function run may accept any prefix of these arguments, for example:

  • function run() ... end
  • function run(input) ... end
  • function run(input, url, method) ... end

The function run may return the following:

  • nil: the plugin invocation returns successfully with no content.
  • an encodable Lua object: the object is encoded to JSON as in json.encode and returned.
  • nil and an Error: the plugin invocation errors as described in the Error section below.

The Blob class

A Blob represents arbitrary binary data.

SmartKey REST APIs use JSON and distinguish between

  • UTF-8 strings (e.g. key name), which use JSON strings, and
  • binary data (e.g. key material), which use base64-encoded JSON strings.

JSON strings must be valid UTF-8, so in general raw binary data is not a valid JSON string.

Lua plugin APIs use Lua strings for UTF-8 strings and Blobs for binary data. For interoperability with the JSON REST APIs, a base64-encoded Lua string may be used instead of a Blob where binary data is expected.

Constructors

Blob.from_bytes(string)

Constructs a Blob from a Lua string with raw binary data. Note that Lua strings do not need to be valid UTF-8. E.g.: local blob = Blob.from_bytes('\1\2\255').

Blob.from_base64(string)

Constructs a Blob from base64-encoded data in a Lua string. E.g.: local blob = Blob.from_base64('AQL/').

Blob.from_hex(string)

Constructs a Blob from hex-encoded data in a Lua string. E.g.: local blob = Blob.from_hex('0102FF').

Blob.random(num_bytes)

Constructs a random Blob with the given number of bytes. The bytes will be generated using SmartKey’s cryptographically secure random number generator. Blob.random { bytes = num_bytes } and Blob.random { bits = num_bits } are also supported. num_bytes must be a whole number; num_bits must be a multiple of 8.

Blob.pack(fmt, …)

This is equivalent to Blob.from_bytes(string.pack(fmt, ...)). See the documentation for string.pack. E.g.: assert(Blob.pack('>i8', 0xDEADBEEF):hex() == '00000000DEADBEEF')

Methods

blob:unpack(fmt)

This is equivalent to string.unpack(fmt, blob:bytes()). See the documentation for string.unpack. E.g.: assert(Blob.from_hex('00000000DEADBEEF'):unpack('>i8') == 0xDEADBEEF)

blob:bytes()

Returns a Lua string with the raw binary data. E.g.: assert(blob:bytes() == '\1\2\255')

blob:base64()

Returns a Lua string with the base64-encoded binary data. E.g.: assert(blob:base64() == 'AQL/')

blob:hex()

Returns a Lua string with the hex-encoded binary data. E.g.: assert(blob:hex() == '0102FF')

blob:slice(lo_byte, hi_byte)

Returns a blob consisting of the bytes between lo_byte and hi_byte, 1-indexed and inclusive. For example, Blob.from_bytes('\1\2\3\4'):slice(2, 3) == Blob.from_bytes('\2\3').

blob .. blob2

.. may be used to concatenate two blobs, e.g. Blob.from_bytes('\1') .. Blob.from_bytes('\2') == Blob.from_bytes('\1\2').

blob & blob2, blob | blob2, blob ~ blob2

&, |, and ~ may be used to take the bitwise and, the bitwise or, and the bitwise xor of two blobs. The blobs must be the same size, or else these operations will throw an exception.

~blob

~ may be used to take the bitwise not of a blob.

The cbor module

cbor.encode(object)

cbor.encode encodes (serializes) a Lua object into a Blob containing binary data in the CBOR format.

Encodable Lua objects include:

  • nil, which encodes to null.
  • Lua numbers and booleans, which encode to CBOR numbers and booleans.
  • Blobs, which encode to CBOR byte strings.
  • Lua strings, which encode to CBOR text strings.
    • CBOR text string should be valid UTF-8, while Lua strings may contain arbitrary binary data. To encode a Lua string representing potentially non-UTF-8 binary data, use Blob.from_bytes(string).
  • Lua tables, which encode to CBOR maps or CBOR array.
    • A table is encoded as an array if the set of keys is {1, 2, .., n} for some n.

A Lua object may use a custom encoder by adding an encoder function __tocbor to the metatable. There is no corresponding way to use a custom decoder.

For example,

null = setmetatable({}, { __tocbor = function(self) return cbor.encode(nil) end })

cbor.decode(data)

cbor.decode decodes (deserializes) a blob containing CBOR binary data, returning a Lua object that encodes to the given CBOR as defined in cbor.encode. CBOR byte strings decode to Blobs and CBOR nil decodes to null. The other CBOR types decode to primitive strings, numbers, booleans, and tables.

The json module

json.encode(object)

json.encode encodes (serializes) a Lua object into Lua string containing JSON data.

json.encode(object) returns the JSON corresponding to cbor.encode(object).

CBOR byte strings (Blobs) become base64-encoded JSON strings.

json.decode(data)

json.decode decodes (deserializes) a Lua string containing JSON data, returning a Lua object that encodes to the given JSON, as defined in json.encode.

JSON nil decodes to null. The other JSON types decode to primitive strings, numbers, booleans, and tables. JSON strings always decode to Lua strings, including base64-encoded JSON strings.

For example, encoding a Blob to JSON and then decoding the JSON will produce a base64-encoded Lua string. To get the original Blob, apply Blob.from_base64 to the decoded string.

The null object

This is an object that encodes to CBOR or JSON null. This is returned on success in methods like sobject:delete() that return no data. We do not return nil here so that assert(sobject:delete()) works.

The Array class

An Array is a table that always serializes to an JSON/CBOR array, not a map.

Non-numeric keys in an Array (more precisely, keys not included in ipairs) are ignored during serialization.

Motivation

The empty map {} and the empty array [] in JSON both deserialize to the empty Lua table {}.

The empty Lua table serializes to the empty map. Array() serializes to the empty array.

Constructors

Array() or Array {} is the empty array.

Array(table) returns setmetatable(table, Array).

E.g. assert(json.encode(Array { 1, 2, a = 3 }) == '[1,2]').

The Error class

This class is used for errors from the plugin APIs. If an error occurs, a function will return two values: nil and the error (this is a standard Lua idiom). An error has two properties: an HTTP status code number status and message string. Custom errors may be constructed using Error.new { status = <code>, message = "<message>" }.

For example,

function run()
  local result, error = Sobject { name = "nonexistent key" }
  assert(result) == nil
  assert(error.status = 404)

  -- return a custom 401 Unauthorized error to the plugin invoker
  return nil, Error.new { status = 401, message = "custom error" }
end

The function principal

principal() returns the EntityId of the entity that invoked the plugin.

For example,

function run()
  if principal():type() ~= 'user'
    return nil, Error.new { status = 401, message = "Plugin may only be invoked by users" }
  end
end

The function this_plugin

this_plugin() returns a Plugin object corresponding to the plugin being invoked.

For example,

function run()
  return "the name of this plugin is " .. this_plugin().name
end

The AuditLog module

AuditLog.log { message = message, severity = severity }

Write an audit log entry associated with this plugin with the given message and severity.

severity may be one of 'INFO', 'WARNING', 'ERROR', or 'CRITICAL'.

AuditLog.get_all { … }

Get audit log entries matching the requested filters. This corresponds to GET /sys/v1/logs in the REST API. Returns an array of audit log entries, or nil and an Error if the logs could not be fetched.

The Sobject class

This represents a security object, transient or persisted. The properties of this object are described in the REST API docs (it is named KeyObject there). sobject.value and sobject.pub_key are Blobs (unless they are nil).

sobject.creator is an EntityId object.

Like apps, plugin may create transient keys by adding transient = true to the create, import, unwrap, derive, or agree request. These transient keys only exist during the invocation of the plugin. As soon as the plugin’s function run returns, they become invalid.

Constructors

Sobject { id = ‘' } or Sobject { kid = '' }

This return the persisted security object with the given UUID, or nil and an Error if no such object exists.

For example,

local sobject = assert(Sobject { id = '123e4567-e89b-12d3-a456-426655440000' })

Sobject { name = ‘key name’ }

This returns the persisted security object with the given name, or nil and an Error if no such object exists.

Sobject.create { … }

Create a security object. This corresponds to POST /crypto/v1/keys in the REST API. The arguments to this function are described in the REST API documentation for SobjectRequest. This returns the created Sobject, or nil and an Error if the object could not be created.

For example,

local sobject = assert(Sobject.create { name = "my key", obj_type = "AES", key_size = 128 })

Sobject.import { … }

Import a security object. This corresponds to PUT /crypto/v1/keys in the REST API. The arguments to this function are described in the REST API documentation for SobjectRequest. This returns the import Sobject, or nil and an Error if the object could not be imported.

For example,

local sobject = assert(Sobject.import { name = "my key", obj_type = "AES", value = Blob.random { bits = 128 } })

Methods

sobject:update { … }

Update the properties of sobject. This corresponds to PATCH /crypto/v1/keys/<uuid> in the REST API. The arguments to this method are described in the REST API documentation for SobjectRequest. This method may not be called on a transient sobject. Returns null on success, or nil and an Error if the object could not be updated.

For example,

assert(sobject:update { name = "new key name" }) -- throw an exception if update fails

sobject:delete()

Delete the sobject. This corresponds to DELETE /crypto/v1/keys/<uuid> in the REST API. This method may not be called on a transient sobject. Returns null on success, or nil and an Error if the object could not be deleted.

For example,

assert(sobject:delete()) -- throw an exception if update fails

sobject:export()

Retrieve the value of sobject. This corresponds to GET /crypto/v1/keys/export in the REST API. This returns a Sobject which has a value property on success, or returns nil and an Error if the object could not be exported.

For example,

local exported_value = assert(Sobject { name = 'my key' }):export().value

sobject:descriptor()

Returns { kid = "<uuid>" } if sobject is persisted, or { transient_key = blob } if sobject is transient.

sobject:encrypt { … }

Encrypt data using sobject. This corresponds to POST /crypto/v1/encrypt in the REST API. The arguments to this method are described in the REST API docs for EncryptRequest. Returns a object corresponding to EncryptResponse in the REST API on success, or nil and an Error if the data could not be encrypted. In the returned object, cipher and iv are Blobs.

For example,

local sobject = assert(Sobject { name = "my aes key" })
local encrypt_response = assert(sobject:encrypt { plain = Blob.from_bytes("hello world"), mode = 'CBC' })
return encrypt_response.cipher:base64() .. ':' .. encrypt_response.iv:base64()

sobject:decrypt { … }

Decrypt data using sobject. This corresponds to POST /crypto/v1/decrypt in the REST API. The arguments to this method are described in the REST API docs for DecryptRequest. Returns an object corresponding to DecryptResponse in the REST API on success, or nil and an Error if the data could not be encrypted.

In the retuned object, plain is a Blob.

For example,

function encrypt(blob)
   local sobject = assert(Sobject { name = "my rsa key" })
   return assert(sobject:decrypt { cipher = blob }).plain
end

sobject:wrap { … }

Use sobject to wrap another security object. This corresponds to POST /crypto/v1/wrapkey in the REST API. The arguments to this method are described in the REST API docs for WrapKeyRequestEx. The subject argument may be a descriptor (as described in the REST API) or a Sobject. Returns an object corresponding to WrapKeyResponse in the REST API on success, or nil and an Error if the sobject key could not be wrapped.

For example,

local wrapping_key = assert(Sobject { name = "AES wrapping key" })
local generated_key = assert(Sobject.create { obj_type = 'AES', key_size = 128, transient = true })
local wrap_response = assert(wrapping_key:wrap { subject = generated_key, mode = 'CBC' })
local result = wrap_response.wrapped_key:base64() .. ':' .. wrap_response.iv:base64()

sobject:unwrap { … }

Use sobject to unwrap and import a wrapped key. This corresponds to POST /crypto/v1/unwrapkey in the REST API. The arguments to this method are described in the REST API docs for UnwrapKeyRequest. Returns the unwrapped Sobject, or nil and an Error if the wrapped key could not be unwrapped.

For example,

function run(input)
  local wrapping_key = assert(Sobject { name = "RSA wrapping key" })
  local unwrapped_key =
    assert(wrapping_key:unwrap { wrapped_key = input.wrapped_key, obj_type = 'AES', transient = true })
  return unwrapped_key:encrypt(input.encrypt_request)
end

sobject:agree { … }

Perform a key agreement algorithm using the private key sobject and the public key from another party. This corresponds to POST /crypto/v1/agree in the REST API. The arguments to this method are described in the REST API for AgreeKeyRequest. The public_key argument may be a descriptor (as described in the REST API) or a Sobject.

Returns the agreed Sobject, or nil and an Error if the key agreement could not be completed.

sobject:mac { … }

Use sobject to compute a cryptographic Message Authentication Code on a message. sobject must be a symmetric key. This corresponds to POST /crypto/v1/mac in the REST API. The arguments to this method are described in the REST API docs for MacGenerateRequest.

Returns an object corresponding to MacGenerateResponse from the REST API on success, or nil and an Error if the MAC could not be generated.

The EntityId class

This identifies an app, user, or plugin. It corresponds to CreatorType in the REST API docs.

Methods

entity_id:id()

Returns the UUID of the app, user, or plugin.

entity_id:type()

Returns "app", "user", or "plugin".

entity_id:entity()

Returns the appropriate App, User, or Plugin object, or nil and an Error if no such object exists.

For example,

local key = assert(Sobject { name = "my key" })
local creator = key.creator
return "Created by a " .. creator:type() .. " named " .. creator:entity().name

The App class

This represents an app. The properties of this object are described in the REST API docs.

app.creator is an EntityId object.

Constructors

App { id = “" }

This returns the app with the given UUID, or nil and an Error if no such app exists.

The Plugin class

This represents a Plugin.

The properties of this object are described in the REST API docs.

plugin.creator is an EntityId object.

Constructors

Plugin { id = “" }

This returns the plugin with the given UUID, or nil and an Error if no such object exists.

The User class

This represents a User. The properties of this object are described in the REST API docs.

Constructors

User { id = “" }

This returns the user with the given UUID, or nil and an Error if no such user exists.

The Group class

This represents a Group. The properties of this object are described in the REST API docs.

Constructors

Group { id = “" }

This returns the group with the given UUID, or nil and an Error if no such group exists.

The function digest

Compute the digest (hash) of the given data using the given algorithm. This corresponds to POST /crypto/v1/digest from the REST API.

digest returns an object corresponding to DigestResponse from the REST API on success, or nil and an Error if the digest could not be computed. The returned digest is a Blob.

For example,

local sha256_hash = assert(digest { data = Blob.from_bytes('Hello world'), alg = 'SHA256' }).digest
return sha256_hash:hex()