Simple generically-derived cryptographic digestion

GitHub Workflow

Gastronomy

Gastronomy provides a range of common cryptographic operations through a simple, typesafe and immutable API.

Features

  • hashing of simple and primitive Scala types
  • generically-derived digests for all product and coproduct types
  • supports SHA-256, SHA-1 and MD5 hash algorithms
  • symmetric encryption with AES
  • asymmetric encryption/decryption using RSA
  • signing with DSA
  • AES, RSA and DSA key generation
  • calculation of HMACs for SHA-256, SHA-1 and MD5
  • encoding into Hex, BASE-64, and URL-safe BASE-64
  • serializers and parsers for PEM-encoded data

Availability

The current latest release of Gastronomy is 0.4.0.

Getting Started

Gastronomy provides representations of public, private and symmetric keys which offer a number of cryptographic methods:

  • PublicKey provides:
    • verify for verifying signatures
    • encrypt for encrypting data
    • pem to provide the public key as a PEM-encoded string
  • PrivateKey provides:
    • sign for signing data
    • decrypt for decrypting encrypted data
    • public to derive a PublicKey from the PrivateKey
    • pem to provide the private key as a PEM-encoded string
  • SymmetricKey provides verify, encrypt, pem, sign and decrypt in a single key.

Additionally, the extension methods, digest and hmac are provided for any value which can be serialized to bytes.

The objects PrivateKey and SymmetricKey both have generate methods which will generate new random keys.

Signing

Given, for example, a PrivateKey[Dsa[1024]] instance, key, data may be signed with, for example,

val signature: Signature[Dsa[1024]] = key.sign(data)

This works for any value, data, that has an appropriate ByteCodec instance. The type parameter of the signature will depend on the type parameter of the private key.

Verifying a signature

A public key, pubKey, which could, for example, be derived from the private key in the previous example,

val pubKey = key.public

may be used to verify a signature of type Signature[Dsa[1024]] with:

val valid: Boolean = pubKey.verify(data, signature)

Here, data must be the same object that was used (with the private key) to produce the signature, and may be any type that has a contextual ByteCodec instance.

Encryption

A public key instance, for example, pubKey of type PublicKey[Rsa[2048]], can encrypt some data by calling,

val encrypted: Message[Rsa[2048]] = pubKey.encrypt(data)

Decryption

An encrypted message may conversely be decrypted using the corresponding PrivateKey[Rsa[2048]] instance, key:

val data: String = key.decrypt[String](encrypted)

The return type (String in the above example) must be specified as a parameter to the decrypt method, and may be any type for which a corresponding ByteCodec exists in context. However, the type should be the same as the type of the object that was originally encrypted, otherwise it may fail to decode.

Digests

A cryptographic digest (or hash) of any value may be calculated by calling digest[A] on that value, for an appropriate choice of A, provided a Hashable instance is in context for that type of object. Hashable instances exist for Strings, primitive types, sequences of these types, and product and coproduct types consisting of just other hashable types.

Cryptographic digests have the type Digest[A] where A is the algorithm type.

For example,

val digest: Digest[Sha2[384]] = (10, "alpha", 'z').digest[Sha2[384]]

HMACs

Any value whose type has a corresponding ByteCodec instance in context may have an HMAC value calculated, of type Hmac[A] (where A is the cryptographic algorithm). As a parameter, this needs an IArray[Byte] representing (in some form) the key to be used.

Here is an example using SHA-512:

val hmac: Hmac[Sha2[512]] = "Hello world".hmac("secret".bytes)

Type inference

Whenever an expression is used in a position with an expected type, the type parameters of the methods decrypt, digest and hmac may be omitted, for example given the case class,

case class Block(digest: Digest[Sha2[256]], json: Json, hmac: Hmac[Sha2[512]])

we can instantiate it with just,

val block = Block(data.digest, data.decrypt, value.hmac)

Alternatively, a particular given may be imported directly into the current scope to prioritize it, such that it may be used in preference to the alternatives.

Byte data

Representations of binary data are common with low-level cryptographic operations. All operations in Gastronomy use the immutable IArray[Byte] type as the underlying representation of binary data, but typically wrap the data in a type which more precisely indicates the content of that data.

These types include the key types, PublicKey, PrivateKey and SymmetricKey, and result types, Signature, Hmac, Digest and Message.

These types are all further refined with a type parameter representing the cryptographic algorithm associated with that data. For example, an MD5 digest is typed as, Digest[Md5] and a 384-bit SHA-2 HMAC has the type, Hmac[Sha2[384]].

In order to make it easier to share these values, they can be encoded to and from Strings using a number of different encodings:

  • binary (Binary)
  • hexadecimal (Hex)
  • BASE-64 (Base64)
  • URL-safe BASE-64 (Base64Url)

The encode method, which exists as an extension on IArray[Byte], as well as (directly) on all types representing byte data. It takes one of these as a type parameter to produce a String of that data, encoded with the specified encoding.

Algorithms

Gastronomy's cryptographic functions are implemented through different algorithms, which are represented by types. Their names follow the conventions of other Scala types, hence:

Additionally, the types Base64, Base64Url, Hex and Binary represent non-cryptographic byte-to-string encodings.

Generating keys

The PrivateKey object provides the generate[A]() method, where A is Rsa[B], Dsa[B] or Aes[B] for an appropriate choice of B.

The algorithm Aes[B] can also be used with the SymmetricKey object to get a symmetric key which has the functionality of both a public and private key.

Byte codecs

Any object which can be serialized to bytes may be digested, signed, verified, HMACked or encrypted, and can be returned from a decryption operation, provided a corresponding ByteCodec instance is available for that type.

ByteCodecs are provided for IArray[Byte] (trivially) and for Strings (assuming a UTF-8 encoding).

PEM encoding

The Privacy-Enhanced Mail format is commonly used for exchanging cryptographic values safely in ASCII-only environments.

A Pem type is provided for reading, writing and representing this data. The case class Pem has two fields: kind, which is the label that appears after the words BEGIN and END in the serialized format, and data, which is an IArray[Byte] of the byte data.

The serialize method will produce a String of the data, properly encoded as BASE-64, and delimited.

The method Pem.parse will attempt to parse a String containing PEM-encoded data.

All Gastronomy's key types offer a pem method which will return an appropriately-labelled Pem value containing that key, however to avoid the risk of accidentally exposing a private key, the pem method of PrivateKey must be called with a special singleton value, like so:

privateKey.pem(RevealSecretKey)

Other Cryptographic Algorithms

Gastronomy may be easily extended to support other cryptographic algorithms. The existing implementations of Rsa, Dsa, Aes, Sha1, Sha2 and Md5 should be studied to investigate this possibility.

Related Projects

The following Scala One libraries are dependencies of Gastronomy:

Gossamer  

The following Scala One libraries are dependents of Gastronomy:

Jovian   Scintillate  

Status

Gastronomy is classified as maturescent. Propensive defines the following five stability levels for open-source projects:

  • embryonic: for experimental or demonstrative purposes only, without any guarantees of longevity
  • fledgling: of proven utility, seeking contributions, but liable to significant redesigns
  • maturescent: major design decisions broady settled, seeking probatory adoption and refinement
  • dependable: production-ready, subject to controlled ongoing maintenance and enhancement; tagged as version 1.0 or later
  • adamantine: proven, reliable and production-ready, with no further breaking changes ever anticipated

Gastronomy is designed to be small. Its entire source code currently consists of 441 lines of code.

Building

Gastronomy can be built on Linux or Mac OS with Irk, by running the irk script in the root directory:

./irk

This script will download irk the first time it is run, start a daemon process, and run the build. Subsequent invocations will be near-instantaneous.

Contributing

Contributors to Gastronomy are welcome and encouraged. New contributors may like to look for issues marked label: good first issue.

We suggest that all contributors read the Contributing Guide to make the process of contributing to Gastronomy easier.

Please do not contact project maintainers privately with questions. While it can be tempting to repsond to such questions, private answers cannot be shared with a wider audience, and it can result in duplication of effort.

Author

Gastronomy was designed and developed by Jon Pretty, and commercial support and training is available from Propensive OÜ.

Name

Gastronomy is named after the art and science of "good eating", which leads to digestion, since the library consumes data to produce digests (but has subsequently grown in scope).

License

Gastronomy is copyright © 2018-22 Jon Pretty & Propensive OÜ, and is made available under the Apache 2.0 License.