Datatypes

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:

  • A unique class, either named or anonymous
  • Structure, either explicitly as fields or implicitly as a closure
  • Fields that can have type hints and can be primitive
  • Immutability on by default
  • Unification with maps (via records)
  • Optional implementations of abstract methods specified in protocols or interfaces

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))))
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.4.179