├── .gitignore ├── Makefile ├── static ├── robots.txt ├── styles.css ├── main.js └── pure-min.css ├── templates ├── error.tpl ├── empty.tpl ├── create.tpl ├── preshow.tpl ├── layout.tpl ├── new.tpl ├── show.tpl └── 404.html ├── go.mod ├── recaptcha.go ├── templates.go ├── go.sum ├── store.go ├── README.md └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | crypt 2 | crypt.db 3 | config.json 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | 3 | build: 4 | go build . 5 | -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: /$ 3 | Disallow: / 4 | -------------------------------------------------------------------------------- /templates/error.tpl: -------------------------------------------------------------------------------- 1 |
2 |

Something went wrong

3 | Create something 4 |
5 | -------------------------------------------------------------------------------- /templates/empty.tpl: -------------------------------------------------------------------------------- 1 |
2 |

There is nothing, and there was nothing all the time.

3 | Create something 4 |
5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Termina1/crypt 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/boltdb/bolt v1.3.1 7 | github.com/google/uuid v1.3.0 8 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 9 | ) 10 | 11 | require golang.org/x/sys v0.0.0-20180727230415-bd9dbc187b6e // indirect 12 | -------------------------------------------------------------------------------- /templates/create.tpl: -------------------------------------------------------------------------------- 1 |
2 |
3 | This link will be deleted after first access 4 | 5 | Create another 6 |
7 | secret qr code 8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /static/styles.css: -------------------------------------------------------------------------------- 1 | .splash-title { 2 | padding: 20px 50px; 3 | border: 1px #000 dashed; 4 | max-width: 400px; 5 | margin: auto; 6 | margin: 10px auto 30px auto; 7 | } 8 | .container { 9 | max-width: 600px; 10 | margin: auto; 11 | padding: 10px; 12 | } 13 | .center { 14 | text-align: center; 15 | } 16 | .error { 17 | color: red; 18 | } 19 | .nothing { 20 | text-align: center; 21 | } 22 | .another { 23 | margin-top: 10px; 24 | } 25 | .top-container { 26 | min-height: 100%; 27 | display: flex; 28 | align-items: center; 29 | } 30 | body, html { 31 | height: 100%; 32 | } 33 | -------------------------------------------------------------------------------- /templates/preshow.tpl: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |
9 | 10 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /recaptcha.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/url" 7 | ) 8 | 9 | const googleApi string = "https://www.google.com/recaptcha/api/siteverify" 10 | 11 | type GoogleResponse struct { 12 | Success bool `json:"success"` 13 | Hostname string `json:"hostname"` 14 | } 15 | 16 | func checkRecaptcha(secretKey string, response string) bool { 17 | resp, err := http.PostForm(googleApi, url.Values{ 18 | "response": {response}, 19 | "secret": {secretKey}, 20 | }) 21 | if err != nil { 22 | return false 23 | } 24 | target := GoogleResponse{} 25 | json.NewDecoder(resp.Body).Decode(&target) 26 | return target.Success 27 | } 28 | -------------------------------------------------------------------------------- /templates.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "html/template" 7 | ) 8 | 9 | //go:embed templates/* 10 | var templates embed.FS 11 | var tplCache map[string]*template.Template = make(map[string]*template.Template) 12 | 13 | func loadTemplate(name string) *template.Template { 14 | tpl, ok := tplCache[name] 15 | if ok { 16 | return tpl 17 | } 18 | file, err := templates.ReadFile("templates/" + name) 19 | if err != nil { 20 | panic(fmt.Sprintf("Unable to load template %s", err.Error())) 21 | } 22 | tpl, err = template.New(name).Parse(string(file)) 23 | if err != nil { 24 | panic(fmt.Sprintf("Unable to parse template %s", err.Error())) 25 | } 26 | tplCache[name] = tpl 27 | return tpl 28 | } 29 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= 2 | github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= 3 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 4 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= 6 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= 7 | golang.org/x/sys v0.0.0-20180727230415-bd9dbc187b6e h1:3dQ4fR8k5KugjVKO0oqSd1odxuk2yaE2CIfxWP2WarQ= 8 | golang.org/x/sys v0.0.0-20180727230415-bd9dbc187b6e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 9 | -------------------------------------------------------------------------------- /templates/layout.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Crypt keeps a secret 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 |

Crypt. One time notes

19 |
20 |
21 | {{.}} 22 |
23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /templates/new.tpl: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | We will generate a link for one-time sharing 5 | 6 |
7 | 9 |
10 |
11 | 12 | 13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 | -------------------------------------------------------------------------------- /templates/show.tpl: -------------------------------------------------------------------------------- 1 |
2 |
3 | This secret was deleted, you can never access this information again 4 |
5 |
6 | 8 |
9 | 10 |
11 | 12 | 13 | 14 | 15 | Create another 16 |
17 |
18 |
19 |
20 | 21 | -------------------------------------------------------------------------------- /store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/boltdb/bolt" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | const bucket string = "secrets" 9 | 10 | func storeAndLink(db *bolt.DB, secret string, salt string) (string, error) { 11 | 12 | uid := uuid.NewString() 13 | errbd := db.Update(func(tx *bolt.Tx) error { 14 | b, berr := tx.CreateBucketIfNotExists([]byte(bucket)) 15 | if berr != nil { 16 | return berr 17 | } 18 | err := b.Put([]byte(uid), []byte(secret)) 19 | if err != nil { 20 | return err 21 | } 22 | err = b.Put([]byte(uid+"_salt"), []byte(salt)) 23 | return err 24 | }) 25 | return uid, errbd 26 | } 27 | 28 | func readAndDelete(db *bolt.DB, uid string) (string, string, error) { 29 | var copyDest []byte 30 | var copySalt []byte 31 | err := db.Batch(func(tx *bolt.Tx) error { 32 | b, berr := tx.CreateBucketIfNotExists([]byte(bucket)) 33 | if berr != nil { 34 | return berr 35 | } 36 | result := b.Get([]byte(uid)) 37 | salt := b.Get([]byte(uid + "_salt")) 38 | err := b.Delete([]byte(uid)) 39 | // bolt will reuse memory after transation, we need to topy it 40 | copyDest = make([]byte, len(result), (cap(result)+1)*2) 41 | copySalt = make([]byte, len(salt), (cap(salt)+1)*2) 42 | copy(copyDest, result) 43 | copy(copySalt, salt) 44 | return err 45 | }) 46 | return string(copyDest), string(copySalt), err 47 | } 48 | -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found 6 | 7 | 54 | 55 | 56 |

Page Not Found

57 |

Sorry, but the page you were trying to view does not exist.

58 | 59 | 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crypt 2 | 3 | This is the simplest possible service for leaving one-time notes. 4 | Screenshot 2022-12-18 at 16 02 44 5 | 6 | ## Features 7 | 8 | 1. Generate link that will be available only for one view (and QR code for it) 9 | 2. Links protected with reCAPTCHA, so brute-force is not possible (reCAPTCHA js file is included on a separate pre-show page and never on the same page where you enter or reveal secret) 10 | 3. Optional E2E encryption. Secret encrypted in the browser and is sent to the server encrypted. 11 | 4. Simplistic uncluttered responsive UI with full mobile support 12 | 13 | Crypt can be compiled into a single binary, all batteries included: 14 | 15 | 1. Embedded DB (boltdb) 16 | 2. All static files are compiled to binary 17 | 18 | Trying to keep dependencies to bare minimum, only use: 19 | 20 | 1. google uuid library 21 | 2. boltdb for storing secrets 22 | 3. QR code library for qr code generation 23 | 24 | Frontend follows the same ideology: 25 | 26 | 1. No JS frameworks. The only JS file is responsible for E2E encryption and autoresizing textarea 27 | 2. Everything is rendered server-side (with native Go module). Works without JS perfectly via forms (long forgotten technology), except for reCAPTCHA. 28 | 3. Pure CSS is only 3-rd party dependency for grid and styling forms, inputs, etc. 29 | 30 | ## TODO 31 | 32 | Would be nice to have those features: 33 | 34 | 1. Customizable expiration for links 35 | 2. Customizable amount of possible views for links 36 | 37 | ## Installation 38 | 39 | You need to have go and make tools installed: 40 | 41 | 1. Clone this repository 42 | 2. Run `make` 43 | 44 | ## Configuration 45 | 46 | - `-config` — path to configuration file 47 | 48 | Possible contents of a configuration file is listed below: 49 | 50 | ``` 51 | { 52 | "port": "which port should be used to start server, default: 8080", 53 | "domain": "domain that this server is binded to, need this to generate correct link, required", 54 | "db_location": "location of the db file on disk, default: crypt.db", 55 | "client_key": "ReCAPTCHA site key, required", 56 | "secret_key": "ReCAPTCHA secret key, required" 57 | } 58 | ``` 59 | 60 | - `-prefix` — prefix for env variables name, default: crypt 61 | 62 | You can override any option with env variable, even those that are required. So, you can totally skip config file and use only env vars. 63 | To do that you have to set env varibale with name `{prefix}\_{option}`. 64 | 65 | Option name should be spelled uppercase without any symbols. 66 | For example, to override secret key you have to set `{prefix}\_SECRETKEY` variable. 67 | -------------------------------------------------------------------------------- /static/main.js: -------------------------------------------------------------------------------- 1 | function str2ab(str) { 2 | var buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char 3 | var bufView = new Uint16Array(buf); 4 | for (var i = 0, strLen = str.length; i < strLen; i++) { 5 | bufView[i] = str.charCodeAt(i); 6 | } 7 | return buf; 8 | } 9 | 10 | function ab2str(buf) { 11 | var bufView = new Uint16Array(buf); 12 | return bufView.toString(); 13 | } 14 | 15 | function toRealString(buf) { 16 | return String.fromCharCode.apply(null, new Uint16Array(buf)); 17 | } 18 | 19 | function init() { 20 | var text = document.querySelector("._content_area"); 21 | function resize() { 22 | text.style.height = "auto"; 23 | text.style.height = text.scrollHeight + 5 + "px"; 24 | } 25 | /* 0-timeout to get the already changed text */ 26 | function delayedResize() { 27 | window.setTimeout(resize, 0); 28 | } 29 | text.addEventListener("input", resize, false); 30 | text.focus(); 31 | text.select(); 32 | resize(); 33 | } 34 | 35 | function deab2str(str) { 36 | var arr = str.split(",").map(function (i) { 37 | return parseInt(i); 38 | }); 39 | var buf = new ArrayBuffer(arr.length * 2); 40 | var bufView = new Uint16Array(buf); 41 | arr.forEach(function (v, i) { 42 | bufView[i] = v; 43 | }); 44 | return buf; 45 | } 46 | 47 | function initalizeEncrypt(form) { 48 | form.addEventListener("submit", encryptSecret); 49 | } 50 | 51 | function initializeDecryptor(decryptor) { 52 | decryptor.addEventListener("click", decryptSecret); 53 | } 54 | 55 | var cipherType = { 56 | name: "AES-CTR", 57 | length: 256, 58 | }; 59 | 60 | function keyFromPass(pass, salt) { 61 | var bufPass = str2ab(pass); 62 | return crypto.subtle 63 | .importKey("raw", bufPass, { name: "PBKDF2" }, false, ["deriveKey"]) 64 | .then(function (key) { 65 | return crypto.subtle.deriveKey( 66 | { 67 | name: "PBKDF2", 68 | salt: salt, 69 | iterations: 1000, 70 | hash: { name: "SHA-1" }, 71 | }, 72 | key, 73 | cipherType, 74 | false, 75 | ["encrypt", "decrypt"] 76 | ); 77 | }); 78 | } 79 | 80 | function encrypt(secret, key) { 81 | var bufSecret = str2ab(secret); 82 | return crypto.subtle 83 | .encrypt( 84 | { 85 | name: "AES-CTR", 86 | counter: new Uint8Array(16), 87 | length: 128, 88 | }, 89 | key, 90 | bufSecret 91 | ) 92 | .then(function (cipher) { 93 | return ab2str(cipher); 94 | }); 95 | } 96 | 97 | function decrypt(cipher, key) { 98 | var bufCipher = deab2str(cipher); 99 | return crypto.subtle 100 | .decrypt( 101 | { 102 | name: "AES-CTR", 103 | counter: new Uint8Array(16), 104 | length: 128, 105 | }, 106 | key, 107 | bufCipher 108 | ) 109 | .then(function (bufPlain) { 110 | return toRealString(bufPlain); 111 | }); 112 | } 113 | 114 | var cipherText = ""; 115 | 116 | function decryptSecret(ev) { 117 | var secret = document.querySelector("._secret_show"); 118 | var pass = document.querySelector("._decrypt_pass"); 119 | var saltEl = document.querySelector("._salt"); 120 | pass.nextElementSibling.setAttribute("hidden", ""); 121 | 122 | var originalCipher = cipherText || secret.value; 123 | cipherText = originalCipher; 124 | var salt = saltEl.value.split(",").map(function (i) { 125 | return parseInt(i); 126 | }); 127 | salt = new Uint8Array(salt); 128 | keyFromPass(pass.value, salt) 129 | .then(function (key) { 130 | return decrypt(originalCipher, key); 131 | }) 132 | .then(function (plaintext) { 133 | secret.value = plaintext; 134 | secret.style.height = "auto"; 135 | secret.style.height = secret.scrollHeight + "px"; 136 | }) 137 | .catch(function (err) { 138 | pass.nextElementSibling.removeAttribute("hidden"); 139 | }); 140 | } 141 | 142 | function encryptSecret(ev) { 143 | var pass = document.querySelector("._encrypt_pass"); 144 | var secret = document.querySelector("._create_secret"); 145 | var saltEl = document.querySelector("._salt"); 146 | var salt = window.crypto.getRandomValues(new Uint8Array(16)); 147 | pass.nextElementSibling.setAttribute("hidden", ""); 148 | saltEl.value = salt.toString(); 149 | if (pass.value) { 150 | keyFromPass(pass.value, salt) 151 | .then(function (key) { 152 | return encrypt(secret.value, key); 153 | }) 154 | .then(function (cipher) { 155 | secret.value = cipher; 156 | ev.target.removeEventListener("submit", encryptSecret); 157 | ev.target.submit(); 158 | }) 159 | .catch(function (error) { 160 | pass.nextElementSibling.removeAttribute("hidden"); 161 | pass.value = ""; 162 | saltEl.value = ""; 163 | }); 164 | ev.preventDefault(); 165 | } 166 | } 167 | 168 | (function () { 169 | document.addEventListener("DOMContentLoaded", function () { 170 | var form = document.querySelector("._submit_new"); 171 | var decryptor = document.querySelector("._decryptor"); 172 | if (form) { 173 | initalizeEncrypt(form); 174 | } 175 | 176 | if (decryptor) { 177 | initializeDecryptor(decryptor); 178 | } 179 | }); 180 | })(); 181 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "embed" 6 | "encoding/json" 7 | "flag" 8 | "fmt" 9 | "html/template" 10 | "net/http" 11 | "os" 12 | "reflect" 13 | "strings" 14 | 15 | "github.com/boltdb/bolt" 16 | "github.com/skip2/go-qrcode" 17 | ) 18 | 19 | //go:embed static/* 20 | var static embed.FS 21 | 22 | type Config struct { 23 | Domain string `required:"true" json:"domain"` 24 | SecretKey string `required:"true" json:"secret_key"` 25 | ClientKey string `required:"true" json:"client_key"` 26 | DbLocation string `default:"crypt.db" json:"db_location"` 27 | Port string `default:"8080" json:"port"` 28 | } 29 | 30 | func handler(w http.ResponseWriter, r *http.Request, db *bolt.DB, config Config) { 31 | var action = r.URL.Path[1:] 32 | switch action { 33 | case "": 34 | var tplRes bytes.Buffer 35 | loadTemplate("new.tpl").Execute(&tplRes, nil) 36 | loadTemplate("layout.tpl").Execute(w, template.HTML(tplRes.String())) 37 | case "create": 38 | var tplRes bytes.Buffer 39 | err := r.ParseForm() 40 | if err != nil { 41 | loadTemplate("error.tpl").Execute(&tplRes, err) 42 | } else { 43 | body := r.FormValue("secret") 44 | salt := r.FormValue("salt") 45 | link, storeErr := storeAndLink(db, body, salt) 46 | link = config.Domain + "/show?uid=" + link 47 | if storeErr != nil { 48 | loadTemplate("error.tpl").Execute(&tplRes, nil) 49 | } else { 50 | loadTemplate("create.tpl").Execute(&tplRes, link) 51 | } 52 | } 53 | loadTemplate("layout.tpl").Execute(w, template.HTML(tplRes.String())) 54 | 55 | case "show": 56 | var tplRes bytes.Buffer 57 | err := r.ParseForm() 58 | if err != nil { 59 | loadTemplate("error.tpl").Execute(&tplRes, err) 60 | } else { 61 | uid := r.FormValue("uid") 62 | recaptcha := r.FormValue("g-recaptcha-response") 63 | 64 | if recaptcha != "" && checkRecaptcha(config.SecretKey, recaptcha) { 65 | secret, salt, readErr := readAndDelete(db, uid) 66 | if readErr != nil { 67 | loadTemplate("error.tpl").Execute(&tplRes, nil) 68 | } else if secret == "" { 69 | loadTemplate("empty.tpl").Execute(&tplRes, nil) 70 | } else { 71 | loadTemplate("show.tpl").Execute(&tplRes, map[string]string{ 72 | "secret": secret, 73 | "salt": salt, 74 | }) 75 | } 76 | } else { 77 | loadTemplate("preshow.tpl").Execute(&tplRes, map[string]string{ 78 | "uid": uid, 79 | "clientKey": config.ClientKey, 80 | }) 81 | } 82 | } 83 | loadTemplate("layout.tpl").Execute(w, template.HTML(tplRes.String())) 84 | case "qr.png": 85 | uid := r.URL.Query().Get("uid") 86 | data, err := qrcode.Encode(fmt.Sprintf("%s/show?uid=%s", config.Domain, uid), qrcode.Medium, 256) 87 | if err != nil { 88 | w.WriteHeader(http.StatusInternalServerError) 89 | w.Header().Set("Content-Type", "image/png") 90 | w.Write([]byte("")) 91 | } else { 92 | w.WriteHeader(http.StatusOK) 93 | w.Write(data) 94 | } 95 | default: 96 | file, err := static.ReadFile("static/" + action) 97 | if err != nil { 98 | w.WriteHeader(http.StatusNotFound) 99 | loadTemplate("404.html").Execute(w, "") 100 | } else { 101 | if strings.HasSuffix(action, ".css") { 102 | w.Header().Set("Content-Type", "text/css") 103 | } 104 | w.WriteHeader(http.StatusOK) 105 | w.Write(file) 106 | } 107 | } 108 | } 109 | 110 | func getEnv(prefix, name string) (string, bool) { 111 | envName := strings.ToUpper(fmt.Sprintf("%s_%s", prefix, name)) 112 | return os.LookupEnv(envName) 113 | } 114 | 115 | func overrideConfig(prefix string, config any) error { 116 | type_ := reflect.TypeOf(config) 117 | if type_.Kind() != reflect.Ptr { 118 | return fmt.Errorf("config struct must be pointer to a struct") 119 | } 120 | type_ = type_.Elem() 121 | if type_.Kind() != reflect.Struct { 122 | return fmt.Errorf("config struct must be pointer to a struct") 123 | } 124 | value_ := reflect.ValueOf(config).Elem() 125 | fields := reflect.VisibleFields(type_) 126 | for _, field := range fields { 127 | if field.Type.Kind() != reflect.String { 128 | return fmt.Errorf("only string config parameters are supported, for %s given %s", field.Name, field.Type.String()) 129 | } 130 | v, ok := getEnv(prefix, field.Name) 131 | fieldValue := value_.FieldByIndex(field.Index) 132 | if ok && fieldValue.CanSet() { 133 | fieldValue.SetString(v) 134 | } 135 | required, ok := field.Tag.Lookup("required") 136 | if ok && required == "true" && fieldValue.String() == "" { 137 | return fmt.Errorf("field %s is required but not set", field.Name) 138 | } 139 | def, ok := field.Tag.Lookup("default") 140 | if ok && fieldValue.String() == "" && fieldValue.CanSet() { 141 | fieldValue.SetString(def) 142 | } 143 | } 144 | return nil 145 | } 146 | 147 | func main() { 148 | config := Config{} 149 | var configLoc = flag.String("config", "", "config file for crypt in JSON format") 150 | var prefix = flag.String("prefix", "crypt", "env prefix for overrides") 151 | flag.Parse() 152 | if configLoc != nil && *configLoc != "" { 153 | file, err := os.ReadFile(*configLoc) 154 | if err != nil { 155 | panic(fmt.Sprintf("can't read config file %s", *configLoc)) 156 | } 157 | json.Unmarshal(file, &config) 158 | } 159 | err := overrideConfig(*prefix, &config) 160 | if err != nil { 161 | panic(fmt.Sprintf("error while parsing config: %s", err.Error())) 162 | } 163 | db, err := bolt.Open(config.DbLocation, 0600, nil) 164 | if err != nil { 165 | panic(err) 166 | } 167 | defer db.Close() 168 | 169 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 170 | handler(w, r, db, config) 171 | }) 172 | fmt.Printf("Listening on port %s\n", config.Port) 173 | http.ListenAndServe(":"+config.Port, nil) 174 | } 175 | -------------------------------------------------------------------------------- /static/pure-min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Pure v3.0.0 3 | Copyright 2013 Yahoo! 4 | Licensed under the BSD License. 5 | https://github.com/pure-css/pure/blob/master/LICENSE 6 | */ 7 | /*! 8 | normalize.css v | MIT License | https://necolas.github.io/normalize.css/ 9 | Copyright (c) Nicolas Gallagher and Jonathan Neal 10 | */ 11 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}html{font-family:sans-serif}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{display:flex;flex-flow:row wrap;align-content:flex-start}.pure-u{display:inline-block;vertical-align:top}.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.pure-button{display:inline-block;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;user-select:none;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-0.43em}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:rgba(0,0,0,.8);border:none transparent;background-color:#e6e6e6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:focus,.pure-button:hover{background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000}.pure-button-disabled,.pure-button-disabled:active,.pure-button-disabled:focus,.pure-button-disabled:hover,.pure-button[disabled]{border:none;background-image:none;opacity:.4;cursor:not-allowed;box-shadow:none;pointer-events:none}.pure-button-hidden{display:none}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-button-group .pure-button{margin:0;border-radius:0;border-right:1px solid rgba(0,0,0,.2)}.pure-button-group .pure-button:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.pure-button-group .pure-button:last-child{border-top-right-radius:2px;border-bottom-right-radius:2px;border-right:none}.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=color]:focus,.pure-form input[type=date]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=email]:focus,.pure-form input[type=month]:focus,.pure-form input[type=number]:focus,.pure-form input[type=password]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=text]:focus,.pure-form input[type=time]:focus,.pure-form input[type=url]:focus,.pure-form input[type=week]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129fea}.pure-form input:not([type]):focus{outline:0;border-color:#129fea}.pure-form input[type=checkbox]:focus,.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus{outline:thin solid #129FEA;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=color][disabled],.pure-form input[type=date][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=email][disabled],.pure-form input[type=month][disabled],.pure-form input[type=number][disabled],.pure-form input[type=password][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=text][disabled],.pure-form input[type=time][disabled],.pure-form input[type=url][disabled],.pure-form input[type=week][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form select:focus:invalid,.pure-form textarea:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=checkbox]:focus:invalid:focus,.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=color],.pure-form-stacked input[type=date],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=email],.pure-form-stacked input[type=file],.pure-form-stacked input[type=month],.pure-form-stacked input[type=number],.pure-form-stacked input[type=password],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=text],.pure-form-stacked input[type=time],.pure-form-stacked input[type=url],.pure-form-stacked input[type=week],.pure-form-stacked label,.pure-form-stacked select,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned select,.pure-form-aligned textarea,.pure-form-message-inline{display:inline-block;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form .pure-input-rounded,.pure-form input.pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-3-4{width:75%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=color],.pure-group input[type=date],.pure-group input[type=datetime-local],.pure-group input[type=datetime],.pure-group input[type=email],.pure-group input[type=month],.pure-group input[type=number],.pure-group input[type=password],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=text],.pure-group input[type=time],.pure-group input[type=url],.pure-group input[type=week]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0 0}.pure-form-message,.pure-form-message-inline{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-item,.pure-menu-list{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-heading,.pure-menu-link{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-separator{display:inline-block;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-active>.pure-menu-children,.pure-menu-allow-hover:hover>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;padding:.5em 0}.pure-menu-horizontal .pure-menu-children .pure-menu-separator,.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{display:block;width:auto}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-heading,.pure-menu-link{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent;cursor:default}.pure-menu-active>.pure-menu-link,.pure-menu-link:focus,.pure-menu-link:hover{background-color:#eee}.pure-menu-selected>.pure-menu-link,.pure-menu-selected>.pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0} --------------------------------------------------------------------------------