Class: Jekyll::Crypto
- Inherits:
-
Struct
- Object
- Struct
- Jekyll::Crypto
- Defined in:
- lib/jekyll/crypto.rb
Overview
The Crypto class encrypts the contents of a given Jekyll::Document using a password defined in the metadata or globally on the Jekyll::Site configuration.
Constant Summary collapse
- ITERATIONS =
20_000.freeze
Instance Attribute Summary collapse
-
#post ⇒ Object
Returns the value of attribute post.
-
#site ⇒ Object
Returns the value of attribute site.
Instance Method Summary collapse
-
#auth_data ⇒ String
Random authentication data.
-
#auth_tag ⇒ Object
Return the authentication tag, this ensures encrypted content hasn’t been tampered with.
-
#cipher ⇒ OpenSSL::Cipher::AES
Initializes AES-GCM-256 on encryption mode.
-
#ciphertext ⇒ Object
Encrypt the rendered content.
-
#derived_key ⇒ String
Derive a stronger key from the password using PBKDF2.
-
#encrypt! ⇒ String
Encrypts and replaces the rendered content of a Document.
-
#hash ⇒ OpenSSL::Digest::SHA256
Hashing function for key derivation.
-
#iterations ⇒ Integer
The amount of iterations for key derivation.
-
#iv ⇒ String
The Initialization Vector for encryption.
-
#password ⇒ String
Obtains the password for this Document or the global password for the site.
- #plain_content ⇒ Object
-
#salt ⇒ String
The salt is a random 16 bytes (128 bits) long String.
-
#to_html ⇒ String
Dumps the encrypted content as HTML.
-
#valid? ⇒ Boolean
Tries to decrypt to prove everything went well.
Instance Attribute Details
#post ⇒ Object
Returns the value of attribute post
9 10 11 |
# File 'lib/jekyll/crypto.rb', line 9 def post @post end |
#site ⇒ Object
Returns the value of attribute site
9 10 11 |
# File 'lib/jekyll/crypto.rb', line 9 def site @site end |
Instance Method Details
#auth_data ⇒ String
Random authentication data
123 124 125 |
# File 'lib/jekyll/crypto.rb', line 123 def auth_data @auth_data ||= cipher.auth_data = OpenSSL::Random.random_bytes(16) end |
#auth_tag ⇒ Object
Return the authentication tag, this ensures encrypted content hasn’t been tampered with.
73 74 75 |
# File 'lib/jekyll/crypto.rb', line 73 def auth_tag @auth_tag ||= cipher.auth_tag(16) end |
#cipher ⇒ OpenSSL::Cipher::AES
Initializes AES-GCM-256 on encryption mode
15 16 17 |
# File 'lib/jekyll/crypto.rb', line 15 def cipher @cipher ||= OpenSSL::Cipher::AES.new(256, :GCM).encrypt end |
#ciphertext ⇒ Object
Encrypt the rendered content
67 68 69 |
# File 'lib/jekyll/crypto.rb', line 67 def ciphertext @ciphertext ||= cipher.update(plain_content) + cipher.final end |
#derived_key ⇒ String
Derive a stronger key from the password using PBKDF2
22 23 24 25 26 27 28 |
# File 'lib/jekyll/crypto.rb', line 22 def derived_key @derived_key ||= OpenSSL::KDF.pbkdf2_hmac(password.to_s, salt: salt, iterations: iterations, length: hash.digest_length, hash: hash) end |
#encrypt! ⇒ String
Encrypts and replaces the rendered content of a Document.
33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/jekyll/crypto.rb', line 33 def encrypt! cipher.key = derived_key # Make these available to any theme that understands Jekyll::Crypto post.data['iv'] = iv.bytes post.data['salt'] = salt.bytes post.data['iterations'] = iterations post.data['data'] = auth_data # The content is replaced by an HTML representation of the # encryption parameters so Jekyll::Crypto can transparently # encrypt any theme. post.content = to_html end |
#hash ⇒ OpenSSL::Digest::SHA256
Hashing function for key derivation
109 110 111 |
# File 'lib/jekyll/crypto.rb', line 109 def hash @hash ||= OpenSSL::Digest::SHA256.new end |
#iterations ⇒ Integer
The amount of iterations for key derivation
100 101 102 103 104 |
# File 'lib/jekyll/crypto.rb', line 100 def iterations @iterations ||= post.data['iterations'] || site.config['iterations'] || ITERATIONS end |
#iv ⇒ String
The Initialization Vector for encryption
116 117 118 |
# File 'lib/jekyll/crypto.rb', line 116 def iv @iv ||= cipher.random_iv end |
#password ⇒ String
Obtains the password for this Document or the global password for the site. If any of them is missing an exception is thrown.
This is private information and is not made available as Document metadata.
91 92 93 94 95 |
# File 'lib/jekyll/crypto.rb', line 91 def password @password ||= post.data['password'] || site.config['password'] || raise(ArgumentError, 'Password missing, see jekyll-crypto documentation.') end |
#plain_content ⇒ Object
48 49 50 |
# File 'lib/jekyll/crypto.rb', line 48 def plain_content @plain_content ||= post.content.dup end |
#salt ⇒ String
The salt is a random 16 bytes (128 bits) long String
80 81 82 |
# File 'lib/jekyll/crypto.rb', line 80 def salt @salt ||= OpenSSL::Random.random_bytes(16) end |
#to_html ⇒ String
Dumps the encrypted content as HTML
The ciphertext for JS is a concatenation of encrypted text and a 16 bytes (128 bits) authentication tag that can later be decrypted by [SubtleCrypto](developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/decrypt).
TODO: Move to template and use Liquid renderer
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
# File 'lib/jekyll/crypto.rb', line 137 def to_html @to_html ||= <<~HTML <div class="jekyll-crypto" data-salt="#{JSON.dump salt.bytes}" data-iv="#{JSON.dump iv.bytes}" data-iterations="#{iterations}" data-auth-data="#{JSON.dump auth_data.bytes}"> <form> <input type="password" name="password" autocomplete="off" required /> <input type="submit" /> </form> <span class="ciphertext" style="word-wrap: break-word;display: none;"> #{JSON.dump ciphertext.bytes.concat(auth_tag.bytes)} </span> </div> <script type="text/javascript"> const jekyllCrypto = document.querySelectorAll('.jekyll-crypto'); const textEncoder = new TextEncoder(); const textDecoder = new TextDecoder(); const parseBytes = (bytesString) => Uint8Array.from(JSON.parse(bytesString)); jekyllCrypto.forEach(container => { container.querySelector('form').addEventListener('submit', event => { event.stopPropagation(); event.preventDefault(); const password = container.querySelector('input[name=password]').value; const salt = parseBytes(container.dataset.salt); const iv = parseBytes(container.dataset.iv); const iterations = parseInt(container.dataset.iterations); const authData = parseBytes(container.dataset.authData); const cipherText = parseBytes(container.querySelector('.ciphertext').innerText); window.crypto.subtle.importKey( "raw", textEncoder.encode(password), { name: "PBKDF2" }, false, [ "deriveBits", "deriveKey" ] ).then(keyMaterial => { window.crypto.subtle.deriveKey( { "name": "PBKDF2", salt: salt, "iterations": iterations, "hash": "SHA-256" }, keyMaterial, { "name": "AES-GCM", "length": 256 }, false, [ "encrypt", "decrypt" ] ).then(derivedKey => { window.crypto.subtle.decrypt( { name: "AES-GCM", iv: iv, additionalData: authData, tagLength: 128 }, derivedKey, cipherText ).then(decrypted => container.innerHTML = textDecoder.decode(decrypted)); }); }); }); }); </script> HTML end |
#valid? ⇒ Boolean
Tries to decrypt to prove everything went well
53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/jekyll/crypto.rb', line 53 def valid? decipher = OpenSSL::Cipher::AES.new(256, :GCM).decrypt decipher.key = derived_key decipher.iv = iv decipher.auth_tag = auth_tag decipher.auth_data = auth_data # This will throw an OpenSSL::Cipher::CipherError exception is # something goes wrong in the implementation. decipher.update(ciphertext) + decipher.final end |