浏览代码

Initial commit

Signed-off-by: Izumiko <yosoro@outlook.com>
Izumiko 5 年之前
当前提交
aa39e8e2b5
共有 10 个文件被更改,包括 414 次插入0 次删除
  1. 8 0
      .gitignore
  2. 123 0
      README.md
  3. 3 0
      example_site/config.toml
  4. 13 0
      example_site/content/post/example.md
  5. 8 0
      go.mod
  6. 14 0
      go.sum
  7. 82 0
      hugo-encrypt.go
  8. 11 0
      i18n/en-us.toml
  9. 11 0
      i18n/zh-cn.toml
  10. 141 0
      shortcodes/hugo-encryptor.html

+ 8 - 0
.gitignore

@@ -0,0 +1,8 @@
+/archetypes/
+/content/
+/layouts/
+/public/
+/static/
+/themes/
+/config.toml
+hugo.exe

+ 123 - 0
README.md

@@ -0,0 +1,123 @@
+# Hugo Encrypt
+
+**Hugo-Encrypt** is a golang port of [Hugo Encryptor](https://github.com/Li4n0/hugo_encryptor)
+   
+**Hugo-Encrypt** is a tool to protect your [Hugo](https://gohugo.io) posts. It uses AES-256-GCM to encrypt the contents of your posts, and inserts a snippet of `<script>` code to verify whether the password is correct or not in readers' browser. Without a correct key, nobody can decrypt your private posts.
+
+## Installation
+
+Environmental dependence: go 1.11+
+
+**Step 1: Install Hugo-Encrypt.**
+
+    $ go get github.com/Izumiko/hugo_encrypt.git
+
+**Step 2: Place `hugo-encrypt` in the root directory of your blog, or add `hugo-encrypt` to `PATH`**
+
+    $ cp hugo-encrypt /path/to/your/blog/
+
+    or
+
+    $ export PATH=/path/of/hugo-encrypt/:$PATH
+
+**Step 3: Place `shortcodes/hugo-encryptor.html` in the shortcode directory of your blog:**
+
+    $ mkdir -p /path/to/your/blog/layouts/shortcodes
+    $ cp shortcodes/hugo-encryptor.html /path/to/your/blog/layouts/shortcodes/hugo-encryptor.html
+
+**Step 4: Merge i18n translation files and/or add your own language.**
+
+    $ cp -r i18n /path/to/your/blog/
+
+## Usage
+
+**Step 1: Use `hugo-encryptor` tag surround the text you want to encrypt **
+
+**Attention! There must be some text and the `<!--more-->` tag before the hugo-encryptor:**
+
+```markdown
+---
+title: "This Is An Encrypted Post"
+---
+
+**There must be some text, and the summary tag is also needed:**
+<!--more-->
+{{% hugo-encryptor "PASSWORD" %}}
+
+# You cannot see me unless you've got the password!
+
+This is the content you want to encrypt!
+
+**Do remember to close the `hugo-encryptor` shortcodes tag:**
+
+{{% /hugo-encryptor %}}
+```
+
+**Step 2: Generate your site as usual**
+
+It may be something like:
+
+    $ hugo
+
+**Step 3: Get the encryption done!**
+
+    $ ./hugo-encrypt
+
+    or (if added to PATH)
+
+    $ hugo-encrypt
+
+
+Then all the private posts in your `public` directory would be encrypted thoroughly, congrats!
+
+## Configuration
+
+Although the **Hugo-Encrypt** can run without any configure, we provide some settings params to help you configure **Hugo-Encrypt** to your liking.
+
+### Language
+
+**Hugo-Encrypt** uses i18n settings to display. You can change it by param below. Be sure to add the corresponding language file to the i18n folder.
+
+```toml
+[params]
+ 		 ......
+  DefaultContentLanguage = "zh-cn"
+```
+
+### The way of client password storage
+
+As default,**Hugo-Encrypt** use `localStorage` to storage the password in client. By adding `hugoEncryptorStorage` param in your blog's config file, you can change the storage method into `sessionStorage`. Such as below:
+
+```toml
+[params]
+ 		 ......
+  hugoEncryptorStorage = "session" # or "local"
+```
+
+For the difference of two storage ways:
+
+* **localStorage**:
+
+  Once a reader input the correct password,the authorization status will not expire, the reader can read the article at any time without having to enter the password again. Unless you change the password or the reader clean his browser cache.
+
+* **sessionStorage**:
+
+  If a reader input the correct password, he could read the article without having to enter the password again until the user close his browser.
+
+### Style
+
+As default, **Hugo-Encrypt** has no style,but we have already give all the visual element a class name, you can add style for them in your css files.
+
+## Attentions
+
+* Do remember to keep the source code of your encrypted posts private. Never push your blog directory into a public repository.
+
+* Every time when you generate your site, you should run `$ hugo-encrypt` again to encrypt the posts which you want to be protected. If you are worried about you will forgot that, it's a good idea to use a shell script to take the place of  `$ hugo` ,such as below:
+
+  ```bash
+  #!/bin/bash
+  hugo && hugo-encrypt
+  ```
+
+  
+

+ 3 - 0
example_site/config.toml

@@ -0,0 +1,3 @@
+DefaultContentLanguage = "zh-cn"
+[params]
+    hugoEncryptorStorage = "session" # or "local"

+ 13 - 0
example_site/content/post/example.md

@@ -0,0 +1,13 @@
+---
+title: "Example"
+date: 2019-07-22
+tags: ["go", "golang", "hugo", "example"]
+draft: false
+---
+
+#Content before encryption.
+
+<!--more-->
+{{% hugo-encryptor "PASSWORD" %}}
+##Encrypted content.
+{{% /hugo-encryptor %}}

+ 8 - 0
go.mod

@@ -0,0 +1,8 @@
+module hugo-encrypt
+
+go 1.12
+
+require (
+	github.com/PuerkitoBio/goquery v1.5.0
+	golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
+)

+ 14 - 0
go.sum

@@ -0,0 +1,14 @@
+github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
+github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
+github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
+github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
+golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

+ 82 - 0
hugo-encrypt.go

@@ -0,0 +1,82 @@
+package main
+
+import (
+	"bytes"
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/rand"
+	"crypto/sha256"
+	"encoding/hex"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/PuerkitoBio/goquery"
+	"golang.org/x/crypto/pbkdf2"
+)
+
+func deriveKey(passphrase string, salt []byte) ([]byte, []byte) {
+	if salt == nil {
+		salt = make([]byte, 8)
+		// http://www.ietf.org/rfc/rfc2898.txt
+		// Salt.
+		rand.Read(salt)
+	}
+	return pbkdf2.Key([]byte(passphrase), salt, 1000, 32, sha256.New), salt
+}
+
+func encrypt(passphrase, plaintext string) string {
+	key, salt := deriveKey(passphrase, nil)
+	iv := make([]byte, 12)
+	// http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
+	// Section 8.2
+	rand.Read(iv)
+	b, _ := aes.NewCipher(key)
+	aesgcm, _ := cipher.NewGCM(b)
+	data := aesgcm.Seal(nil, iv, []byte(plaintext), nil)
+	return hex.EncodeToString(salt) + "-" + hex.EncodeToString(iv) + "-" + hex.EncodeToString(data)
+}
+
+func encryptPage(path string) {
+	content, err := ioutil.ReadFile(path)
+	if err != nil {
+		panic(err)
+	}
+	doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(content))
+	if err != nil {
+		panic(err)
+	}
+	block := doc.Find("cipher-text")
+	if len(block.Nodes) == 1 {
+		fmt.Printf("Processing %s\n", path)
+
+		password, _ := block.Attr("data-password")
+		blockhtml, _ := block.Html()
+		enchtml := encrypt(password, blockhtml)
+		block.RemoveAttr("data-password")
+		block.SetHtml(enchtml)
+		wholehtml, _ := doc.Html()
+		ioutil.WriteFile(path, []byte(wholehtml), 0644)
+	}
+}
+
+func main() {
+	err := filepath.Walk("./public", func(path string, f os.FileInfo, err error) error {
+		if f == nil {
+			return err
+		}
+		if f.IsDir() {
+			return nil
+		}
+		ok := strings.HasSuffix(f.Name(), ".html")
+		if ok {
+			encryptPage(path)
+		}
+		return nil
+	})
+	if err != nil {
+		fmt.Printf("filepath.Walk() returned %v\n", err)
+	}
+}

+ 11 - 0
i18n/en-us.toml

@@ -0,0 +1,11 @@
+[protectedbypwd]
+other = "The following content is password protected."
+
+[inputpassword]
+other = "Please input the password"
+
+[submit]
+other = "Submit"
+
+[wrongpwd]
+other = "Wrong password"

+ 11 - 0
i18n/zh-cn.toml

@@ -0,0 +1,11 @@
+[protectedbypwd]
+other = "以下内容被密码保护"
+
+[inputpassword]
+other = "请输入密码"
+
+[submit]
+other = "提交"
+
+[wrongpwd]
+other = "密码错误"

+ 141 - 0
shortcodes/hugo-encryptor.html

@@ -0,0 +1,141 @@
+{{/*
+    ## Hugo Encrypt
+    ### Params:
+    - `password`:
+
+        require param
+    - Simple
+
+        {{% hugo-encryptor "your password" %}}
+your content
+{{% /hugo-encryptor %}}
+
+*/}}
+{{/* DEFAULTS */}}
+{{ $_hugo_config := `{ "version": 1 }` }}
+
+<hugo-encryptor>
+  <p>{{ i18n "protectedbypwd" }}</p>
+  
+  <div class='hugo-encryptor-form'>
+    <input
+      class="hugo-encryptor-input"
+      id="hugo-encryptor-password"
+      placeholder='{{ i18n "inputpassword" }}'
+    />
+    <input
+      class="hugo-encryptor-button"
+      type="button"
+      value='{{ i18n "submit" }}'
+      onclick="hugoDecrypt(document.getElementById('hugo-encryptor-password').value,'input')"
+    />
+  </div>
+  <cipher-text data-password="{{ .Get 0 }}" style="display:none;">
+    <p id="verifyText" style="display:none;">
+      The quick brown fox jumps over the lazy dog
+    </p>
+    {{ .Inner }}
+  </cipher-text>
+  <script>
+      let cipher = document.getElementsByTagName("cipher-text")[0];
+      const storageKey = location.pathname + "password";
+      const userStorage = {{ if .Site.Params.hugoEncryptorStorage }} window['{{.Site.Params.hugoEncryptorStorage}}Storage'] {{ else }} localStorage {{ end }};
+      /**
+       * Encodes a utf8 string as a byte array.
+       * @param {String} str 
+       * @returns {Uint8Array}
+       */
+      function str2buf(str) {
+        return new TextEncoder("utf-8").encode(str);
+      }
+      /**
+       * Decodes a byte array as a utf8 string.
+       * @param {Uint8Array} buffer 
+       * @returns {String}
+       */
+      function buf2str(buffer) {
+        return new TextDecoder("utf-8").decode(buffer);
+      }
+      /**
+       * Decodes a string of hex to a byte array.
+       * @param {String} hexStr
+       * @returns {Uint8Array} 
+       */
+      function hex2buf(hexStr) {
+        return new Uint8Array(hexStr.match(/.{2}/g).map(h => parseInt(h, 16)));
+      }
+      /**
+       * Given a passphrase, this generates a crypto key
+       * using `PBKDF2` with SHA256 and 1000 iterations.
+       * If no salt is given, a new one is generated.
+       * The return value is an array of `[key, salt]`.
+       * @param {String} passphrase 
+       * @param {UInt8Array} salt [salt=random bytes]
+       * @returns {Promise<[CryptoKey,UInt8Array]>} 
+       */
+      function deriveKey(passphrase, salt) {
+        salt = salt || crypto.getRandomValues(new Uint8Array(8));
+        return crypto.subtle
+          .importKey("raw", str2buf(passphrase), "PBKDF2", false, ["deriveKey"])
+          .then(key =>
+            crypto.subtle.deriveKey(
+              { name: "PBKDF2", salt, iterations: 1000, hash: "SHA-256" },
+              key,
+              { name: "AES-GCM", length: 256 },
+              false,
+              ["encrypt", "decrypt"],
+            ),
+          )
+          .then(key => [key, salt]);
+      }
+      /**
+       * Given a key and ciphertext (in the form of a string) as given by `encrypt`,
+       * this decrypts the ciphertext and returns the original plaintext
+       * @param {String} passphrase 
+       * @param {String} saltIvCipherHex 
+       * @returns {Promise<String>}
+       */
+      function decrypt(passphrase, saltIvCipherHex) {
+        const [salt, iv, data] = saltIvCipherHex.split("-").map(hex2buf);
+        return deriveKey(passphrase, salt)
+          .then(([key]) => crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, data))
+          .then(v => buf2str(new Uint8Array(v)));
+      }
+      /**
+       * @name:hugoDecrypt
+       * @description: judge the password ,and decrypt post
+       * @param {String} password
+       * @param {String} type
+       */
+      const hugoDecrypt = function(password, type) {
+        try {
+          decrypt(password, cipher.innerText).then(function(res) {
+            if ( res.includes("The quick brown fox jumps over the lazy dog") ) {
+              cipher.parentElement.outerHTML = res;
+              userStorage.setItem(storageKey, password);
+              document.getElementById("verifyText").outerHTML = "";
+            } else {
+              if (type === "input") {
+                alert('{{ i18n "wrongpwd" }}');
+              } else if (type === "storage") {
+                userStorage.removeItem(storageKey);
+              }
+            }
+          });
+        } catch (error) {
+          if (type === "input") {
+            alert('{{ i18n "wrongpwd" }}');
+          } else if (type === "storage") {
+            userStorage.removeItem(location.pathname + "password");
+          }
+        }
+      };
+  </script>
+  <script>
+    window.onload = () => {
+      if (userStorage.getItem(storageKey)) {
+        hugoDecrypt(userStorage.getItem(storageKey), "storage");
+      }
+    };
+  </script>
+</hugo-encryptor>