We’ve shown how to extend existing types to implement new abstractions with protocols, but what if we want to create a new type in Clojure? That’s where datatypes come in.
A datatype provides the following:
We will use the deftype macro to define a new datatype, called CryptoVault, that will implement two protocols, including IOFactory.
Now that gulp and expectorate support several existing Java classes, let’s create a new supported type, CryptoVault. You’ll create an instance of a CryptoVault by passing in an argument that implements the clojure.java.io.IOFactory protocol (not the one we’ve defined here), a path to a cryptographic key store, and a password. The contents expectorated into the CryptoVault will be encrypted and written to the IOFactory object and then decrypted when gulped back in.
We’ll use deftype to create the new type.
| (deftype name [& fields] & opts+specs) |
It takes the name of the type and a vector of fields contained by the type. The naming convention for datatypes is the same as used by Java classes, i.e., CamelCase.
| user=> (deftype CryptoVault [filename keystore password]) |
| user.CryptoVault |
Once the type has been defined, we can create an instance of our CryptoVault:
| user=> (def vault (->CryptoVault "vault-file" "keystore" "toomanysecrets")) |
| #'user/vault |
And its fields can be accessed using the same prefix-dot syntax used to access fields in Java objects.
| user=> (.filename vault) |
| "vault-file" |
| |
| user=> (.keystore vault) |
| "keystore" |
| |
| user=> (.password vault) |
| "toomanysecrets" |
Now that we’ve defined the basic CryptoVault type, let’s add behavior with some methods. Datatypes can implement only those methods that have been specified in either a protocol or an interface, so let’s first create a Vault protocol.
| (defprotocol Vault |
| (init-vault [vault]) |
| (vault-output-stream [vault]) |
| (vault-input-stream [vault])) |
The protocol includes three functions—init-vault, vault-output-stream, and vault-input-stream—that every Vault must implement.
We can define our new type’s methods inline with deftype; we just pass the type name and vector of fields as before, followed by a protocol name and one or more method bodies:
| (ns examples.cryptovault |
| (:require [examples.io :refer [IOFactory make-reader make-writer]]) |
| (:require [clojure.java.io :as io]) |
| (:import (java.security KeyStore KeyStore$SecretKeyEntry |
| KeyStore$PasswordProtection) |
| (javax.crypto KeyGenerator Cipher CipherOutputStream |
| CipherInputStream) |
| (java.io FileOutputStream))) |
| (deftype CryptoVault [filename keystore password] |
| Vault |
| (init-vault [vault] |
| ... define method body here ...) |
| |
| (vault-output-stream [vault] |
| ... define method body here ...) |
| |
| (vault-input-stream [vault] |
| ... define method body here ...) |
| |
| IOFactory |
| (make-reader [vault] |
| (make-reader (vault-input-stream vault))) |
| (make-writer [vault] |
| (make-writer (vault-output-stream vault)))) |
Notice that the methods for more than one protocol can be defined inline; we’ve defined the methods for the Vault and IOFactory protocols together, although the bodies of the Vault methods have been elided and will be described next.
The init-vault method will generate an Advanced Encryption Standard (AES) key, place it in a java.security.KeyStore, write the keystore data to the file specified by the keystore field in the CryptoVault, and then password-protect it.
| (init-vault [vault] |
| (let [password (.toCharArray (.password vault)) |
| key (.generateKey (KeyGenerator/getInstance "AES")) |
| keystore (doto (KeyStore/getInstance "JCEKS") |
| (.load nil password) |
| (.setEntry "vault-key" |
| (KeyStore$SecretKeyEntry. key) |
| (KeyStore$PasswordProtection. password)))] |
| (with-open [fos (FileOutputStream. (.keystore vault))] |
| (.store keystore fos password)))) |
Both the vault-output-stream and vault-input-stream methods will use a function, vault-key, to load the keystore associated with the CryptoVault and extract the AES key used to encrypt and decrypt the contents of the vault.
| (defn vault-key [vault] |
| (let [password (.toCharArray (.password vault))] |
| (with-open [fis (FileInputStream. (.keystore vault))] |
| (-> (doto (KeyStore/getInstance "JCEKS") |
| (.load fis password)) |
| (.getKey "vault-key" password))))) |
The vault-output-stream method uses the vault-key method to initialize an AES cipher object, creates an OutputStream from the Vault’s filename, and then uses the cipher and OutputStream to create an instance of a CipherOutputStream.
| (vault-output-stream [vault] |
| (let [cipher (doto (Cipher/getInstance "AES") |
| (.init Cipher/ENCRYPT_MODE (vault-key vault)))] |
| (CipherOutputStream. (io/output-stream (.filename vault)) cipher))) |
vault-input-stream works like vault-output-stream, but returns a CipherInputStream.
| (vault-input-stream [vault] |
| (let [cipher (doto (Cipher/getInstance "AES") |
| (.init Cipher/DECRYPT_MODE (vault-key vault)))] |
| (CipherInputStream. (io/input-stream (.filename vault)) cipher))) |
To create an instance of a CryptoVault, just pass the location where data should be stored, the keystore filename, and the password protecting the keystore. If the keystore hasn’t been initialized, then call the init-vault method:
| user=> (def vault (->CryptoVault "vault-file" "keystore" "toomanysecrets")) |
| #'user/vault |
| |
| user=> (init-vault vault) |
| nil |
Then use the CryptoVault like any other source/destination used by gulp and expectorate.
| user=> (expectorate vault "This is a test of the CryptoVault") |
| nil |
| |
| user=> (gulp vault) |
| "This is a test of the CryptoVault" |
We can use the CryptoVault with the built-in spit and slurp functions by extending it to support the clojure.java.io/IOFactory protocol. This version of the IOFactory has four methods, instead of two like ours, and there are default method implementations defined in a map called default-streams-impl. We’ll override just two of its methods, make-input-stream and make-output-stream, by assoc’ing our new implementations into this map and passing it to the extend function.
| (extend CryptoVault |
| clojure.java.io/IOFactory |
| (assoc clojure.java.io/default-streams-impl |
| :make-input-stream (fn [x opts] (vault-input-stream x)) |
| :make-output-stream (fn [x opts] (vault-output-stream x)))) |
That’s it; now we can read and write to a CryptoVault using slurp and spit.
| user=> (spit vault "This is a test of the CryptoVault using spit and slurp") |
| nil |
| |
| user=> (slurp vault) |
| "This is a test of the CryptoVault using spit and slurp" |
Let’s put all the pieces together in a .clj file. Make a src/examples/datatypes subdirectory within your project directory, and create a file called vault.clj.
| (ns examples.cryptovault-complete |
| (:require [clojure.java.io :as io] |
| [examples.protocols.io :as proto]) |
| (:import (java.security KeyStore KeyStore$SecretKeyEntry |
| KeyStore$PasswordProtection) |
| (javax.crypto Cipher KeyGenerator CipherOutputStream |
| CipherInputStream) |
| (java.io FileInputStream FileOutputStream))) |
| (defprotocol Vault |
| (init-vault [vault]) |
| (vault-output-stream [vault]) |
| (vault-input-stream [vault])) |
| (defn vault-key [vault] |
| (let [password (.toCharArray (.password vault))] |
| (with-open [fis (FileInputStream. (.keystore vault))] |
| (-> (doto (KeyStore/getInstance "JCEKS") |
| (.load fis password)) |
| (.getKey "vault-key" password))))) |
| (deftype CryptoVault [filename keystore password] |
| Vault |
| (init-vault [vault] |
| (let [password (.toCharArray (.password vault)) |
| key (.generateKey (KeyGenerator/getInstance "AES")) |
| keystore (doto (KeyStore/getInstance "JCEKS") |
| (.load nil password) |
| (.setEntry "vault-key" |
| (KeyStore$SecretKeyEntry. key) |
| (KeyStore$PasswordProtection. password)))] |
| (with-open [fos (FileOutputStream. (.keystore vault))] |
| (.store keystore fos password)))) |
| |
| (vault-output-stream [vault] |
| (let [cipher (doto (Cipher/getInstance "AES") |
| (.init Cipher/ENCRYPT_MODE (vault-key vault)))] |
| (CipherOutputStream. (io/output-stream (.filename vault)) cipher))) |
| |
| (vault-input-stream [vault] |
| (let [cipher (doto (Cipher/getInstance "AES") |
| (.init Cipher/DECRYPT_MODE (vault-key vault)))] |
| (CipherInputStream. (io/input-stream (.filename vault)) cipher))) |
| |
| proto/IOFactory |
| (make-reader [vault] |
| (proto/make-reader (vault-input-stream vault))) |
| (make-writer [vault] |
| (proto/make-writer (vault-output-stream vault)))) |
| |
| (extend CryptoVault |
| clojure.java.io/IOFactory |
| (assoc io/default-streams-impl |
| :make-input-stream (fn [x opts] (vault-input-stream x)) |
| :make-output-stream (fn [x opts] (vault-output-stream x)))) |