5 Commits 49baf48ba4 ... b03eef2794

Author SHA1 Message Date
  forest b03eef2794 fix bugs and make sure to cache all files locally to deliver on offline 1 month ago
  forest 9654783bf7 remove double logging oops 1 month ago
  forest 62d61defa1 update readme for 1.2.0 1 month ago
  forest 3ed7f1edbe update build for 1.2.0 1 month ago
  forest 81b34c780a first shot at replacing application cache with service worker 1 month ago
10 changed files with 204 additions and 62 deletions
  1. 2 0
      .gitignore
  2. 1 1
      Dockerfile
  3. 2 3
      ReadMe.md
  4. 2 2
      build.sh
  5. 0 14
      index.appcache.gotemplate
  6. 6 6
      index.html.gotemplate
  7. 32 29
      server.go
  8. 2 2
      static/application.css
  9. 72 5
      static/application.js
  10. 85 0
      static/serviceworker.js

+ 2 - 0
.gitignore

@ -1,2 +1,4 @@
1 1
data
2 2
sequentialread-password-manager
3
test-server.key
4
test-server.pem

+ 1 - 1
Dockerfile

@ -1,6 +1,6 @@
1 1
FROM alpine
2 2
3
COPY sequentialread-password-manager index.appcache.gotemplate index.html.gotemplate ./
3
COPY sequentialread-password-manager LICENSE ReadMe.md index.html.gotemplate ./
4 4
COPY static static
5 5
6 6
EXPOSE 8073

+ 2 - 3
ReadMe.md

@ -1,7 +1,6 @@
1 1
2 2
# SequentialRead Password Manager
3 3
4
5 4
This is a Golang / HTML5  / vanilla JavaScript web-application which stores encrypted text files in three places:
6 5
7 6
 - `localStorage` in the browser
@ -37,7 +36,7 @@ It was designed that way to strengthen the claim that "everything it sends out f
37 36
38 37
## High Avaliability by Design
39 38
40
 It uses the [HTML5 Application Cache](https://alistapart.com/article/application-cache-is-a-douchebag/) to ensure that even if my server goes down, the app still loads.
39
 It uses the [Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers) to ensure that even if my server goes down, the app still loads.
41 40
42 41
 It also has its own AWS Credential with access to the bucket, so you can still access S3 if my server goes down.
43 42
@ -69,7 +68,7 @@ Casual remote attackers probably won't have access to the ciphertext since they
69 68
70 69
## Hosting it yourself
71 70
72
You should create a separate IAM user in AWS which only has access to the bucket. This is the policy I used for that user:
71
You should create a separate IAM user in AWS which only has access to the bucket. This is the policy I used for that user. Note that the user does not have the capability to list the bucket contents, only to get and put objects. This is very important for security reasons, otherwise a remote attacker would have a much easier time trying to break the encryption, because they would have easy access to the ciphertext. 
73 72
74 73
```
75 74
{

+ 2 - 2
build.sh

@ -1,4 +1,4 @@
1 1
#!/bin/bash
2 2
3
GOOS=linux GOARCH=amd64 go build -v -o sequentialread-password-manager server.go \
4
  && docker build -t sequentialread/sequentialread-password-manager:1.1.0 .
3
GOOS=linux GOARCH=amd64 go build -v -o sequentialread-password-manager -tags netgo  server.go \
4
  && docker build -t sequentialread/sequentialread-password-manager:1.2.0 .

+ 0 - 14
index.appcache.gotemplate

@ -1,14 +0,0 @@
1
CACHE MANIFEST
2
# VERSION {{.Version}}
3
/?v={{.Version}}
4
/static/application.css?v={{.Version}}
5
/static/application.js?v={{.Version}}
6
/static/awsClient.js?v={{.Version}}
7
/static/vendor/sjcl.js?v={{.Version}}
8
/static/vendor/tenThousandMostCommonEnglishWords.js?v={{.Version}}
9
10
NETWORK:
11
*
12
13
FALLBACK:
14
/ /?v={{.Version}}

+ 6 - 6
index.html.gotemplate

@ -1,5 +1,5 @@
1 1
<!DOCTYPE HTML>
2
<html manifest="index.appcache?v={{.Version}}">
2
<html>
3 3
<head>
4 4
  <meta charset="UTF-8">
5 5
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
@ -17,10 +17,10 @@
17 17
      window.sequentialReadPasswordManager.sjcl = factory();
18 18
    };
19 19
  </script>
20
  <script src="static/vendor/sjcl.js?v={{.Version}}"></script>
21
  <script src="static/vendor/tenThousandMostCommonEnglishWords.js?v={{.Version}}"></script>
22
  <script src="static/awsClient.js?v={{.Version}}"></script>
23
  <link rel="stylesheet" type="text/css" href="static/application.css?v={{.Version}}">
20
  <script src="static/vendor/sjcl.js"></script> 
21
  <script src="static/vendor/tenThousandMostCommonEnglishWords.js"></script>
22
  <script src="static/awsClient.js"></script>
23
  <link rel="stylesheet" type="text/css" href="static/application.css">
24 24
</head>
25 25
<body>
26 26
  <div class="header">
@ -90,6 +90,6 @@
90 90
    <div class="loader">loading</div>
91 91
  </div>
92 92
93
  <script src="static/application.js?v={{.Version}}"></script>
93
  <script src="static/application.js"></script>
94 94
</body>
95 95
</html>

+ 32 - 29
server.go

@ -3,6 +3,7 @@ package main
3 3
import (
4 4
	"bytes"
5 5
	"crypto/sha256"
6
	"crypto/tls"
6 7
	"flag"
7 8
	"fmt"
8 9
	"io"
@ -21,7 +22,6 @@ import (
21 22
var appPort = "8073"
22 23
var dataPath string
23 24
var indexTemplate *template.Template
24
var appcacheTemplate *template.Template
25 25
var application Application
26 26
27 27
type Application struct {
@ -104,27 +104,7 @@ func indexHtml(response http.ResponseWriter, request *http.Request) {
104 104
		fmt.Fprintf(response, "500 %s", err)
105 105
		return
106 106
	}
107
	response.Header().Set("Cache-Control", "max-age=0")
108
	response.Header().Set("Cache-Control", "must-revalidate")
109
	response.Header().Set("Cache-Control", "no-cache")
110
	response.Header().Set("Cache-Control", "no-store")
111
112
	io.Copy(response, &buffer)
113
}
114
115
func cacheManifest(response http.ResponseWriter, request *http.Request) {
116
	var buffer bytes.Buffer
117
	err := appcacheTemplate.Execute(&buffer, application)
118
	if err != nil {
119
		response.WriteHeader(500)
120
		fmt.Fprintf(response, "500 %s", err)
121
		return
122
	}
123
	response.Header().Set("Content-Type", "text/cache-manifest")
124
	response.Header().Set("Cache-Control", "max-age=0")
125
	response.Header().Set("Cache-Control", "must-revalidate")
126
	response.Header().Set("Cache-Control", "no-cache")
127
	response.Header().Set("Cache-Control", "no-store")
107
	response.Header().Set("Etag", application.Version)
128 108
129 109
	io.Copy(response, &buffer)
130 110
}
@ -156,8 +136,8 @@ func hashFiles(filenames []string) string {
156 136
func reloadStaticFiles() {
157 137
	application.Version = hashFiles([]string{
158 138
		"index.html.gotemplate",
159
		"index.appcache.gotemplate",
160 139
		"static/application.js",
140
		"static/serviceworker.js",
161 141
		"static/awsClient.js",
162 142
		"static/application.css",
163 143
		"static/vendor/sjcl.js",
@ -169,7 +149,6 @@ func reloadStaticFiles() {
169 149
	application.S3BucketRegion = os.ExpandEnv("$SEQUENTIAL_READ_PWM_S3_BUCKET_REGION")
170 150
171 151
	indexTemplate = loadTemplate("index.html.gotemplate")
172
	appcacheTemplate = loadTemplate("index.appcache.gotemplate")
173 152
}
174 153
175 154
func main() {
@ -180,18 +159,29 @@ func main() {
180 159
	reloadStaticFiles()
181 160
182 161
	http.HandleFunc("/", indexHtml)
162
163
	http.HandleFunc("/serviceworker.js", func(response http.ResponseWriter, request *http.Request) {
164
		file, _ := os.OpenFile("static/serviceworker.js", os.O_RDONLY, 0755)
165
		defer file.Close()
166
		response.Header().Set("Etag", application.Version)
167
		response.Header().Set("Content-Type", "application/javascript")
168
		io.Copy(response, file)
169
	})
170
183 171
	http.HandleFunc("/version", func(response http.ResponseWriter, request *http.Request) {
184 172
		response.WriteHeader(200)
185 173
		fmt.Fprint(response, application.Version)
186 174
	})
187
	http.HandleFunc("/index.appcache", cacheManifest)
188 175
189 176
	http.HandleFunc("/storage/", storage)
190 177
191 178
	http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
192 179
193 180
	var headless = flag.Bool("headless", false, "headless server mode")
194
	if *headless == false {
181
	var tlsFlag = flag.String("tls", "", "path to tlsFlag cert/key (for example, entering \"test\" will resolve \"./test.key\" and \"./test.pem\"")
182
	flag.Parse()
183
184
	if headless == nil || *headless == false {
195 185
196 186
		go func() {
197 187
			for true {
@ -202,12 +192,20 @@ func main() {
202 192
203 193
		go func() {
204 194
			var appUrl = "http://localhost:" + appPort
195
			if tlsFlag != nil && *tlsFlag != "" {
196
				appUrl = "https://localhost:" + appPort
197
			}
205 198
			var serverIsRunning = false
206 199
			var attempts = 0
207 200
			for !serverIsRunning && attempts < 15 {
208 201
				attempts += 1
209 202
				time.Sleep(time.Millisecond * 500)
210
				response, err := http.Get(appUrl)
203
				tr := &http.Transport{
204
					TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
205
				}
206
				client := &http.Client{Transport: tr}
207
				response, err := client.Get(appUrl)
208
211 209
				if err == nil && response.StatusCode == 200 {
212 210
					serverIsRunning = true
213 211
				}
@ -216,8 +214,13 @@ func main() {
216 214
		}()
217 215
	}
218 216
219
	//http.ListenAndServeTLS(":443", "test-server.pem", "test-server.key", nil)
220
	http.ListenAndServe(":"+appPort, nil)
217
	if tlsFlag != nil && *tlsFlag != "" {
218
		err := http.ListenAndServeTLS(":"+appPort, fmt.Sprintf("%s.pem", *tlsFlag), fmt.Sprintf("%s.key", *tlsFlag), nil)
219
		panic(err)
220
	} else {
221
		err := http.ListenAndServe(":"+appPort, nil)
222
		panic(err)
223
	}
221 224
222 225
}
223 226

+ 2 - 2
static/application.css

@ -20,7 +20,7 @@ h3 {
20 20
}
21 21
input, button {
22 22
  border-radius: 2px;
23
  border: 1px solid #dddddd;
23
  border: 1px solid #bbb;
24 24
  padding: 5px;
25 25
  margin:5px;
26 26
}
@ -44,7 +44,7 @@ button:hover {
44 44
}
45 45
button:active {
46 46
  background: #ffffff;
47
  border: 1px solid #dddddd;
47
  border: 1px solid #aaa;
48 48
}
49 49
input[type="text"]{
50 50
  color:  #666666;

+ 72 - 5
static/application.js

@ -1,14 +1,47 @@
1 1
'use strict';
2 2
3 3
4
(function(window, undefined){
4
(function(window, navigator, undefined){
5 5
6 6
  window.sequentialReadPasswordManager = window.sequentialReadPasswordManager || {};
7 7
  window.sequentialReadPasswordManager.localStorageKeyPrefix = "sequentialread-pwm:";
8 8
  window.sequentialReadPasswordManager.s3InterceptorSymbol =  "/awsS3/";
9 9
  window.sequentialReadPasswordManager.storageBaseUrl = "/storage";
10 10
11
})(window);
11
  window.addEventListener('load', () => {
12
    if (!('serviceWorker' in navigator)) {
13
      console.log('service workers not supported 😣')
14
      return
15
    }
16
  
17
    navigator.serviceWorker.register('/serviceworker.js').then(
18
      (reg) => {
19
        if(reg.installing) {
20
          console.log('Service worker installing, will reload');
21
          
22
          document.getElementById('progress-container').style.display = 'block';
23
          window.setTimeout(function(){
24
            window.location = window.location.origin;
25
          }, 2000);
26
        } else if(reg.waiting) {
27
          console.log('Service worker installed');
28
          
29
        } else if(reg.active) {
30
          console.log('Service worker active');
31
        }
32
      },
33
      err => {
34
        console.error('service worker registration failed! 😱', err)
35
      }
36
    );
37
    navigator.serviceWorker.addEventListener('message', event => {
38
      if(event.data.log) {
39
        console.log(event.data.log);
40
      }
41
    });
42
  });
43
44
})(window, navigator);
12 45
13 46
14 47
(function(app, window, document, undefined){
@ -220,6 +253,10 @@
220 253
221 254
      });
222 255
256
    this.isStoredLocally = (id) => {
257
      return !!window.localStorage[`${localStorageKeyPrefix}${id}`];
258
    };
259
223 260
    this.get = (id) => {
224 261
      return Promise.all([
225 262
        httpButAlwaysResolves('GET', `${storageBaseUrl}/${id}`, {'Accept': 'application/json'}),
@ -552,7 +589,26 @@
552 589
    this.load = () => {
553 590
      storageService.get(cryptoService.getKeyId())
554 591
      .then(
555
        renderFileList,
592
        fileListDocument => {
593
          // attempt to load all the files that are not already stored locally
594
          // this way if the user tries to open a file that they have never opened before
595
          // when they are offline, it should work.
596
597
          // console.log(fileListDocument.files
598
          //   .filter(x => !storageService.isStoredLocally(x.id)).map(x => x.name))
599
600
          // console.log(fileListDocument.files
601
          //   .filter(x => !storageService.isStoredLocally(x.id)).map(x => x.id))
602
603
          fileListDocument.files
604
            .filter(x => !storageService.isStoredLocally(x.id))
605
            .map(file => storageService.get(file.id).then(
606
              () => {}, 
607
              err => console.log(`could not cache file "${file.name}"`, err)
608
            ));
609
610
          return renderFileList(fileListDocument);
611
        },
556 612
        () => {
557 613
          modalService.open(
558 614
            "New Index File",
@ -669,9 +725,20 @@
669 725
    .then(
670 726
      (currentVersion) => {
671 727
        var lastVersion = window.localStorage[`${localStorageKeyPrefix}version`];
672
        if(currentVersion != lastVersion) {
673
          console.log(`reloading in 1 second due to new app version: ${currentVersion}`)
728
        if(currentVersion && currentVersion != lastVersion) {
729
          
730
          console.log(`reloading service worker due to new app version: ${currentVersion}`)
674 731
          window.localStorage[`${localStorageKeyPrefix}version`] = currentVersion;
732
          if(navigator.serviceWorker && navigator.serviceWorker.controller) {
733
            navigator.serviceWorker.controller.postMessage({clearCache: true});
734
          }
735
          navigator.serviceWorker.getRegistrations().then(registrations => {
736
            registrations.forEach(registration => {
737
              registration.unregister()
738
            }) 
739
          });
740
741
          document.getElementById('progress-container').style.display = 'block';
675 742
          window.setTimeout(function(){
676 743
            window.location = window.location.origin;
677 744
          }, 1000);

+ 85 - 0
static/serviceworker.js

@ -0,0 +1,85 @@
1
2
const cacheVersion = 'v1';
3
4
self.addEventListener('install', event => {
5
6
  event.waitUntil(clients.get(event.clientId).then(client => {
7
    // if(client) {
8
    //   client.postMessage({ log: "asd222" });
9
    // }
10
    return caches.open(cacheVersion).then(cache => {
11
      return cache.addAll([
12
        '/',
13
        '/static/application.css',
14
        '/static/application.js',
15
        '/static/awsClient.js',
16
        '/static/vendor/sjcl.js',
17
        '/static/vendor/tenThousandMostCommonEnglishWords.js',
18
      ]);
19
    })
20
  }));
21
});
22
23
self.addEventListener('message', async event => {
24
25
  if(event.source) {
26
    event.source.postMessage({ log: `sw got message: ${JSON.stringify(event.data)}` });
27
  } else if(event.clientId) {
28
    const client = await clients.get(event.clientId);
29
    client.postMessage({ log: `sw got message: ${JSON.stringify(event.data)}` });
30
  }
31
32
  if(event.data.clearCache) {
33
    caches.delete(cacheVersion);
34
  }
35
});
36
37
self.addEventListener('fetch', event => {
38
39
  event.respondWith(clients.get(event.clientId).then((client) => {
40
    return caches.match(event.request).then(response => {
41
      // caches.match() always resolves
42
      // but in case of success response will have value
43
      if (response !== undefined) {
44
  
45
        
46
        if(client) {
47
          client.postMessage({ log: `cache hit: ${event.request.method}, ${event.request.url}` });
48
        }
49
        return response;
50
      } else {
51
        return fetch(event.request).then(response => {
52
          const url = new URL(event.request.url);
53
          const isServerStorage = url.pathname.startsWith('/storage');
54
          const isVersion = url.pathname == "/version";
55
          const isAws = url.host.includes('s3') && (url.host.includes('aws') || url.host.includes('amazon'));
56
          const isPut = event.request.method == "PUT";
57
58
          if(!isServerStorage && !isVersion && !isAws && !isPut) {
59
            // response may be used only once
60
            // we need to save clone to put one copy in cache
61
            // and serve second one
62
            let responseClone = response.clone();
63
            
64
            caches.open(cacheVersion).then((cache) => {
65
              cache.put(event.request, responseClone);
66
            });
67
            if(client) {
68
              client.postMessage({ log: `cache miss: ${event.request.method}, ${event.request.url}` });
69
            }
70
          } else if(client) {
71
            client.postMessage({ log: `ignored: ${event.request.method}, ${event.request.url}` });
72
          }
73
  
74
          return response;
75
        }).catch( e => {
76
          if(client) {
77
            client.postMessage({ log: `fetch failed in serviceworker! ${event.request.method}, ${event.request.url}: ${e} ` });
78
          }
79
          return null;
80
        });
81
      }
82
    });
83
  }));
84
  
85
});