Browse Source

Got S3 working

forest johnson 2 years ago
parent
commit
e25ed0dcee
6 changed files with 502 additions and 41 deletions
  1. 5 1
      index.appcache.gotemplate
  2. 7 3
      index.html.gotemplate
  3. 28 5
      server.go
  4. 72 0
      static/application.css
  5. 179 32
      static/application.js
  6. 211 0
      static/awsClient.js

+ 5 - 1
index.appcache.gotemplate

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

+ 7 - 3
index.html.gotemplate

@ -1,13 +1,13 @@
1 1
<!DOCTYPE HTML>
2
<html manifest="index.appcache">
2
<html manifest="index.appcache?v={{.Version}}">
3 3
<head>
4 4
  <meta charset="UTF-8">
5 5
  <title>pwm</title>
6 6
  <script>
7 7
    // global namespace where we will be putting everything
8 8
    window.sequentialReadPasswordManager = {
9
      AWSAccessKeyId = {{.AWSAccessKeyId}}
10
      AWSSecretAccessKey = {{.AWSSecretAccessKey}}
9
      AWSAccessKeyId: "{{.AWSAccessKeyId}}",
10
      AWSSecretAccessKey: "{{.AWSSecretAccessKey}}"
11 11
    };
12 12
    // a fake implementation of AMD just so I can use use sjcl.js (Stanford JavaScript Crypto Library) without any modifications
13 13
    window.define = function(dependencies, factory) {
@ -16,6 +16,7 @@
16 16
  </script>
17 17
  <script src="static/vendor/sjcl.js?v={{.Version}}"></script>
18 18
  <script src="static/vendor/tenThousandMostCommonEnglishWords.js?v={{.Version}}"></script>
19
  <script src="static/awsClient.js?v={{.Version}}"></script>
19 20
  <link rel="stylesheet" type="text/css" href="static/application.css?v={{.Version}}">
20 21
</head>
21 22
<body>
@ -67,6 +68,9 @@
67 68
      <div id="modal-footer" style="height:50px;"></div>
68 69
    </div>
69 70
  </div>
71
  <div class="modal-container" id="progress-container" style="display:none;">
72
    <div class="loader">loading</div>
73
  </div>
70 74
71 75
  <script src="static/application.js?v={{.Version}}"></script>
72 76
</body>

+ 28 - 5
server.go

@ -93,6 +93,11 @@ func indexHtml(response http.ResponseWriter, request *http.Request) {
93 93
		fmt.Fprintf(response, "500 %s", err)
94 94
		return
95 95
	}
96
	response.Header().Set("Cache-Control", "max-age=0")
97
	response.Header().Set("Cache-Control", "must-revalidate")
98
	response.Header().Set("Cache-Control", "no-cache")
99
	response.Header().Set("Cache-Control", "no-store")
100
96 101
	io.Copy(response, &buffer)
97 102
}
98 103
@ -105,6 +110,11 @@ func cacheManifest(response http.ResponseWriter, request *http.Request) {
105 110
		return
106 111
	}
107 112
	response.Header().Set("Content-Type", "text/cache-manifest")
113
	response.Header().Set("Cache-Control", "max-age=0")
114
	response.Header().Set("Cache-Control", "must-revalidate")
115
	response.Header().Set("Cache-Control", "no-cache")
116
	response.Header().Set("Cache-Control", "no-store")
117
108 118
	io.Copy(response, &buffer)
109 119
}
110 120
@ -132,15 +142,12 @@ func hashFiles(filenames []string) string {
132 142
	return fmt.Sprintf("%x", hash.Sum(nil))
133 143
}
134 144
135
func main() {
136
137
	dataPath = filepath.Join(".", "data")
138
	os.MkdirAll(dataPath, os.ModePerm)
139
145
func reloadStaticFiles() {
140 146
	application.Version = hashFiles([]string{
141 147
		"index.html.gotemplate",
142 148
		"index.appcache.gotemplate",
143 149
		"static/application.js",
150
		"static/awsClient.js",
144 151
		"static/application.css",
145 152
		"static/vendor/sjcl.js",
146 153
		"static/vendor/tenThousandMostCommonEnglishWords.js",
@ -150,6 +157,14 @@ func main() {
150 157
151 158
	indexTemplate = loadTemplate("index.html.gotemplate")
152 159
	appcacheTemplate = loadTemplate("index.appcache.gotemplate")
160
}
161
162
func main() {
163
164
	dataPath = filepath.Join(".", "data")
165
	os.MkdirAll(dataPath, os.ModePerm)
166
167
	reloadStaticFiles()
153 168
154 169
	http.HandleFunc("/", indexHtml)
155 170
	http.HandleFunc("/index.appcache", cacheManifest)
@ -160,6 +175,14 @@ func main() {
160 175
161 176
	var headless = flag.Bool("headless", false, "headless server mode")
162 177
	if *headless == false {
178
179
		go func() {
180
			for true {
181
				reloadStaticFiles()
182
				time.Sleep(time.Second * 2)
183
			}
184
		}()
185
163 186
		go func() {
164 187
			var appUrl = "http://localhost:" + appPort
165 188
			var serverIsRunning = false

+ 72 - 0
static/application.css

@ -141,3 +141,75 @@ textarea {
141 141
.saved-status-indicator.error {
142 142
    background: red;
143 143
}
144
145
.yavascript {
146
  display: inline-block;
147
  
148
  width: 100px;
149
  height: 102px;
150
  background-repeat: no-repeat;
151
  background-image: url(data:image/gif;base64,R0lGODlhZABmALMPAPXy/CgkK2tmdMvG2K2nu0I9R7iyxdfS5ImDlJeRo8K8znp1hKOdsOTg8FpUYf///yH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS4wLWMwNjEgNjQuMTQwOTQ5LCAyMDEwLzEyLzA3LTEwOjU3OjAxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1LjEgV2luZG93cyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpGNjg0QTQ5ODBDRTQxMUU3OUMxMEM4MjlDNDgxNkEyMyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpGNjg0QTQ5OTBDRTQxMUU3OUMxMEM4MjlDNDgxNkEyMyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkY2ODRBNDk2MENFNDExRTc5QzEwQzgyOUM0ODE2QTIzIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkY2ODRBNDk3MENFNDExRTc5QzEwQzgyOUM0ODE2QTIzIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU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);
152
}
153
154
155
156
157
.loader,
158
.loader:before,
159
.loader:after {
160
  background: rgba(0,0,0,0.4);
161
  -webkit-animation: load1 1s infinite ease-in-out;
162
  animation: load1 1s infinite ease-in-out;
163
  width: 1em;
164
  height: 4em;
165
}
166
.loader:before,
167
.loader:after {
168
  position: absolute;
169
  top: 0;
170
  content: '';
171
}
172
.loader:before {
173
  left: -1.5em;
174
  -webkit-animation-delay: -0.32s;
175
  animation-delay: -0.32s;
176
}
177
.loader {
178
  color: rgba(0,0,0,0.4);
179
  text-indent: -9999em;
180
  margin: 160px auto;
181
  position: relative;
182
  font-size: 11px;
183
  -webkit-transform: translateZ(0);
184
  -ms-transform: translateZ(0);
185
  transform: translateZ(0);
186
  -webkit-animation-delay: -0.16s;
187
  animation-delay: -0.16s;
188
}
189
.loader:after {
190
  left: 1.5em;
191
}
192
@-webkit-keyframes load1 {
193
  0%,
194
  80%,
195
  100% {
196
    box-shadow: 0 0;
197
    height: 4em;
198
  }
199
  40% {
200
    box-shadow: 0 -2em;
201
    height: 5em;
202
  }
203
}
204
@keyframes load1 {
205
  0%,
206
  80%,
207
  100% {
208
    box-shadow: 0 0;
209
    height: 4em;
210
  }
211
  40% {
212
    box-shadow: 0 -2em;
213
    height: 5em;
214
  }
215
}

+ 179 - 32
static/application.js

@ -84,47 +84,164 @@
84 84
  })();
85 85
})(window.sequentialReadPasswordManager, window, document);
86 86
87
87 88
(function(app, window, document, undefined){
88
  app.storageService = new (function StorageService(cryptoService) {
89
  app.storageService = new (function StorageService(cryptoService, awsClient) {
89 90
90 91
    var baseUrl = "/storage";
91 92
92
    var request = (method, url, content) =>
93
    var s3InterceptorSymbol = "/awsS3";
94
95
    var localStorageKeyPrefix = "sequentialread-pwm:";
96
97
    var awsS3BucketName = 'sequentialread-pwm';
98
    var awsS3BucketRegion = 'us-west-2';
99
100
    var requestsCurrentlyInFlight = 0;
101
102
    function RequestFailure(httpRequest, isTimeout) {
103
      this.httpRequest = httpRequest;
104
      this.isTimeout = isTimeout
105
    }
106
107
    // request ALWAYS resolves, if it fails it will resolve a RequestFailure.
108
    var request = (method, url, headers, content) =>
93 109
      new Promise((resolve, reject) => {
110
111
        requestsCurrentlyInFlight += 1;
112
        document.getElementById('progress-container').style.display = 'block';
113
114
        var resolveAndPopInFlight = (result) => {
115
          requestsCurrentlyInFlight -= 1;
116
          if(requestsCurrentlyInFlight == 0) {
117
            document.getElementById('progress-container').style.display = 'none';
118
          }
119
          resolve(result);
120
        };
121
122
        headers = headers || {};
94 123
        var httpRequest = new XMLHttpRequest();
95
        httpRequest.onreadystatechange = () => {
96
          if (httpRequest.readyState === XMLHttpRequest.DONE) {
97
            if (httpRequest.status === 200) {
98
              if(httpRequest.responseText.length == 0) {
99
                resolve();
100
              } else {
101
                try {
102
                  resolve(JSON.parse(cryptoService.decrypt(httpRequest.responseText)));
103
                } catch (err) {
104
                  resolve(httpRequest.responseText);
105
                }
106
              }
124
        httpRequest.onloadend = () => {
125
          //console.log(`httpRequest.onloadend: ${httpRequest.status} ${url}`);
126
          if (httpRequest.status === 200) {
127
            if(httpRequest.responseText.length == 0) {
128
              resolveAndPopInFlight();
107 129
            } else {
108
              reject(httpRequest);
130
              try {
131
                resolveAndPopInFlight(JSON.parse(cryptoService.decrypt(httpRequest.responseText)));
132
              } catch (err) {
133
                resolveAndPopInFlight(httpRequest.responseText);
134
              }
109 135
            }
136
          } else if(httpRequest.status !== 0) {
137
            resolveAndPopInFlight(new RequestFailure(httpRequest, false));
110 138
          }
111 139
        };
140
        //httpRequest.onerror = () => {
141
        //  console.log(`httpRequest.onerror: ${httpRequest.status} ${url}`);
142
        //  resolveAndPopInFlight(new RequestFailure(httpRequest, false));
143
        //};
144
        httpRequest.ontimeout = () => {
145
          //console.log(`httpRequest.ontimeout: ${httpRequest.status} ${url}`);
146
          resolveAndPopInFlight(new RequestFailure(httpRequest, true));
147
        };
148
149
        // Encrypt Content first
150
        if(content) {
151
          if(typeof content == "object") {
152
            content = JSON.stringify(content, 0, 2);
153
          }
154
          content = cryptoService.encrypt(content);
155
        }
156
157
        // AWS S3 request interceptor
158
        if(url.startsWith(s3InterceptorSymbol)) {
159
          var path = url.replace(s3InterceptorSymbol, '');
160
          if(path.startsWith('/')){
161
            path = path.substring(1);
162
          }
163
          var s3Request = awsClient.s3Request(method, awsS3BucketRegion, awsS3BucketName, path, content);
164
          headers = s3Request.headers;
165
          url = s3Request.endpointUri;
166
167
          //console.log(url, headers);
168
        }
112 169
113 170
        httpRequest.open(method, url);
171
        httpRequest.timeout = 2000;
172
173
        Object.keys(headers)
174
          .filter(key => key.toLowerCase() != 'host' && key.toLowerCase() != 'content-length')
175
          .forEach(key => httpRequest.setRequestHeader(key, headers[key]));
114 176
        if(content) {
115
          httpRequest.setRequestHeader('Content-Type', 'text/plain');
116
          httpRequest.send(cryptoService.encrypt(JSON.stringify(content, 0, 2)));
177
          httpRequest.send(content);
117 178
        } else {
118 179
          httpRequest.send();
119 180
        }
120 181
      });
121 182
122
      var s3Request = 
183
    this.get = (id) => {
184
      // request() ALWAYS resolves, if it fails it will resolve a RequestFailure.
185
      return Promise.all([
186
        request('GET', `${baseUrl}/${id}`),
187
        request('GET', `${s3InterceptorSymbol}/${id}`)
188
      ]).then((results) => {
189
        return new Promise((resolve, reject) => {
190
          var localCopy = JSON.parse(window.localStorage[`${localStorageKeyPrefix}${id}`]);
191
          var sequentialreadCopy = results[0];
192
          var s3Copy = results[1];
193
          var allCopies = [];
194
          if(localCopy) {
195
            allCopies.push(localCopy);
196
          }
197
          if(!(sequentialreadCopy instanceof RequestFailure)) {
198
            allCopies.push(sequentialreadCopy);
199
          }
200
          if(!(s3Copy instanceof RequestFailure)) {
201
            allCopies.push(s3Copy);
202
          }
203
          if(allCopies.length == 0) {
204
            reject();
205
            return;
206
          }
207
208
          var latestCopy = {lastUpdated:0};
209
          allCopies.forEach(x => {
210
            if(x.lastUpdated && x.lastUpdated > latestCopy.lastUpdated) {
211
              latestCopy = x;
212
            }
213
          });
214
215
          if(latestCopy.lastUpdated == 0) {
216
            reject();
217
            return;
218
          }
123 219
124
    this.get = (id) => request('GET', `${baseUrl}/${id}`);
125
    this.put = (id, content) => request('PUT', `${baseUrl}/${id}`, content);
126
    this.delete = (id) => request('DELETE', `${baseUrl}/${id}`);
127
  })(app.cryptoService);
220
          if(allCopies.filter(x => x.lastUpdated < latestCopy.lastUpdated).length > 0) {
221
            this.put(id, latestCopy).then(() => resolve(latestCopy));
222
          } else {
223
            resolve(latestCopy);
224
          }
225
        });
226
      });
227
    };
228
    this.put = (id, content) => {
229
      content.lastUpdated = new Date().getTime();
230
      // request() ALWAYS resolves, if it fails it will resolve a RequestFailure.
231
      window.localStorage[`${localStorageKeyPrefix}${id}`] = JSON.stringify(content);
232
      return Promise.all([
233
        request('PUT', `${baseUrl}/${id}`, {'Content-Type': 'application/json'}, content),
234
        request('PUT', `${s3InterceptorSymbol}/${id}`, {'Content-Type': 'application/json'}, content)
235
      ]).then(() => content)
236
    };
237
    this.delete = (id) => {
238
      window.localStorage.removeItem(`${localStorageKeyPrefix}${id}`);
239
      return Promise.all([
240
        request('DELETE', `${baseUrl}/${id}`),
241
        request('DELETE', `${s3InterceptorSymbol}/${id}`)
242
      ]).then(() => null);
243
    };
244
  })(app.cryptoService, app.awsClient);
128 245
})(window.sequentialReadPasswordManager, window, document);
129 246
130 247
(function(app, document, undefined){
@ -169,6 +286,40 @@
169 286
  })();
170 287
})(window.sequentialReadPasswordManager, document);
171 288
289
290
(function(app, window, document, undefined){
291
  app.errorHandler = new (function ErrorHandler(modalService) {
292
293
    this.onError = (message, fileName, lineNumber, column, err) => {
294
      console.log(message, fileName, lineNumber, column, err);
295
      modalService.open(
296
        "JavaScript Error",
297
        `<div>
298
          <span class="yavascript"></span>
299
        </div>
300
        ${err ? err.name : ''}: ${message || err.message} at ${fileName}:${lineNumber}
301
        `,
302
        (resolve, reject) => {},
303
        [{
304
          innerHTML: "Ok",
305
          onclick: (resolve, reject) => resolve()
306
        }]
307
      )
308
    };
309
310
    window.onerror = this.onError;
311
    window.addEventListener("unhandledrejection", (unhandledPromiseRejectionEvent, promise) => {
312
      var err = unhandledPromiseRejectionEvent.reason;
313
      if(typeof err == "string") {
314
        err = new Error(err);
315
      }
316
      if(err) {
317
        this.onError(err.message, err.fileName, err.lineNumber, null, err);
318
      }
319
    });
320
  })(app.modalService);
321
})(window.sequentialReadPasswordManager, window);
322
172 323
(function(app, document, undefined){
173 324
  app.navController = new (function NavController() {
174 325
    var routes = [
@ -305,16 +456,12 @@
305 456
      storageService.get(cryptoService.getKeyId())
306 457
      .then(
307 458
        renderFileList,
308
        (xmlHttpRequest) => {
309
          if(xmlHttpRequest.status == 404) {
310
            storageService.put(cryptoService.getKeyId(), this.fileListDocument)
311
            .then(
312
              () => renderFileList(this.fileListDocument),
313
              () => null //TODO error handler
314
            );
315
          } else {
316
            //TODO error handler
317
          }
459
        () => {
460
          storageService.put(cryptoService.getKeyId(), this.fileListDocument)
461
          .then(
462
            () => renderFileList(this.fileListDocument),
463
            () => null //TODO error handler
464
          );
318 465
        }
319 466
      )
320 467
    };

+ 211 - 0
static/awsClient.js

@ -0,0 +1,211 @@
1
'use strict';
2
3
(function(app, document, undefined) {
4
  app.awsClient = new (function AWSClient(sjcl, awsAccessKeyId, awsSecretAccessKey ) {
5
    var EMPTY_BODY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
6
7
    var SCHEME = "AWS4";
8
    var ALGORITHM = "HMAC-SHA256";
9
    var TERMINATOR = "aws4_request";
10
11
    var X_Amz_Algorithm = "X-Amz-Algorithm";
12
    var X_Amz_Credential = "X-Amz-Credential";
13
    var X_Amz_SignedHeaders = "X-Amz-SignedHeaders";
14
    var X_Amz_Date = "X-Amz-Date";
15
    var X_Amz_Signature = "X-Amz-Signature";
16
    var X_Amz_Expires = "X-Amz-Expires";
17
    var X_Amz_Content_SHA256 = "X-Amz-Content-SHA256";
18
    var X_Amz_Decoded_Content_Length = "X-Amz-Decoded-Content-Length";
19
    var X_Amz_Meta_UUID = "X-Amz-Meta-UUID";
20
21
    var HMACSHA256 = "HMACSHA256";
22
23
    var CompressWhitespaceRegex = /\s+/g;
24
25
    var CanonicalRequestHashAlgorithm = "SHA-256";
26
27
    function canonicalizeHeaderNames(headers) {
28
      return Object.keys(headers).map(x => x.toLowerCase()).sort().join(";");
29
    }
30
31
    function canonicalizeHeaders(headers)
32
    {
33
        if (headers == null || Object.keys(headers).length == 0) {
34
          return "";
35
        }
36
37
        var headersLowerCase = {};
38
        Object.keys(headers).forEach(x => headersLowerCase[x.toLowerCase()] = headers[x]);
39
        return Object.keys(headersLowerCase)
40
          .sort()
41
          .map(x => `${x}:${headersLowerCase[x].replace(CompressWhitespaceRegex, " ").trim()}\n`)
42
          .join("");
43
    }
44
45
    function canonicalizeRequest(httpMethod,
46
                                 endpointUri,
47
                                 queryParameters,
48
                                 canonicalizedHeaders,
49
                                 canonicalizedHeaderNames,
50
                                 bodyHash) {
51
        return `${httpMethod}\n${CanonicalResourcePath(endpointUri)}\n${queryParameters}\n`
52
             + `${canonicalizedHeaders}\n${canonicalizedHeaderNames}\n${bodyHash}`;
53
    }
54
55
    var dummyElement = document.createElement('a');
56
    function CanonicalResourcePath(endpointUri) {
57
      dummyElement.href = endpointUri;
58
      if(!dummyElement.pathname) {
59
        return "/";
60
      }
61
      return encodeURI(dummyElement.pathname);
62
    }
63
64
    var utf8ToBits = sjcl.codec.utf8String.toBits;
65
    function deriveSigningKey(date, region, service) {
66
        var ksecret = `${SCHEME}${awsSecretAccessKey}`
67
68
        var hashDate = new sjcl.misc.hmac(utf8ToBits(ksecret), sjcl.hash.sha256).encrypt(utf8ToBits(date));
69
        var hashRegion = new sjcl.misc.hmac(hashDate, sjcl.hash.sha256).encrypt(utf8ToBits(region));
70
        var hashService = new sjcl.misc.hmac(hashRegion, sjcl.hash.sha256).encrypt(utf8ToBits(service));
71
        return new sjcl.misc.hmac(hashService, sjcl.hash.sha256).encrypt(utf8ToBits(TERMINATOR));
72
    }
73
74
    function toHexString(data, lowercase) {
75
        var toReturn = sjcl.codec.hex.fromBits(data);
76
        return lowercase ? toReturn : toReturn.toUpperCase();
77
    }
78
79
    //   var DateStringFormat = "yyyyMMdd";
80
    function dateStringFormat (date) {
81
      return `${date.getFullYear()}${lpad1(date.getMonth()+1)}${lpad1(date.getDate())}`;
82
    }
83
84
    // var ISO8601BasicFormat = "yyyyMMddTHHmmssZ";
85
    function ISO8601BasicFormat(date) {
86
      return `${date.toISOString().replace(/[-\.:]/g, '').substring(0,15)}Z`;
87
    }
88
89
    function lpad1(n) {
90
      return (n < 10) ? ("0" + String(n)) : String(n);
91
    }
92
93
    // returns the byte length of an utf8 string
94
    function byteLength(str) {
95
      var s = str.length;
96
      for (var i=str.length-1; i>=0; i--) {
97
        var code = str.charCodeAt(i);
98
        if (code > 0x7f && code <= 0x7ff) s++;
99
        else if (code > 0x7ff && code <= 0xffff) s+=2;
100
        if (code >= 0xDC00 && code <= 0xDFFF) i--; //trail surrogate
101
      }
102
      return s;
103
    }
104
105
    this.s3Request = (httpMethod, region, bucketName, objectKey, objectValue) => {
106
107
      var regionUrlPart = (region && region.toLowerCase() != "us-east-1") ? `-${region}` : "";
108
      var endpointUri = `https://${bucketName}.s3${regionUrlPart}.amazonaws.com/${objectKey}`;
109
110
      var headers = {};
111
112
      var bodyHash = EMPTY_BODY_SHA256;
113
      if(httpMethod == "PUT" || httpMethod == "POST") {
114
        if(typeof objectValue == "object") {
115
          headers["content-type"] = "application/json";
116
          objectValue = JSON.stringify(objectValue, 0, 2);
117
        } else {
118
          headers["content-type"] = "text/plain";
119
        }
120
        headers["content-length"] = String(byteLength(objectValue));
121
        bodyHash = toHexString(sjcl.hash.sha256.hash(sjcl.codec.utf8String.toBits(objectValue)), true);
122
      } else {
123
        headers["content-type"] = "text/plain";
124
      }
125
      headers[X_Amz_Content_SHA256] = bodyHash;
126
127
      var request = {
128
        service: 's3',
129
        region: region,
130
        httpMethod: httpMethod,
131
        endpointUri: endpointUri,
132
        queryParameters: '',
133
        headers: headers,
134
        bodyHash: bodyHash,
135
        bodyString: objectValue
136
      };
137
138
      headers["Authorization"] = this.awsSignatureV4AuthorizationHeader(request);
139
140
      return request;
141
    };
142
143
    // aws4Request is an object which has keys:
144
    //service, region, httpMethod, endpointUri, queryParameters, headers, bodyHash
145
    this.awsSignatureV4AuthorizationHeader = (aws4Request) => {
146
      // this was once used for testing against a known working implementation
147
      //var dateTime = new Date(1553021296000);
148
149
      // update the headers with required 'x-amz-date' and 'host' values
150
      var dateTime = new Date();
151
      var dateTimeStamp = ISO8601BasicFormat(dateTime);
152
      aws4Request.headers[X_Amz_Date] = dateTimeStamp;
153
154
      dummyElement.href = aws4Request.endpointUri;
155
      var hostHeader = dummyElement.host;
156
      if (dummyElement.port) {
157
        hostHeader = `${hostHeader}:${dummyElement.port}`
158
      }
159
      aws4Request.headers["Host"] = hostHeader;
160
161
      // canonicalize the headers; we need the set of header names as well as the
162
      // names and values to go into the signature process
163
      var canonicalizedHeaderNames = canonicalizeHeaderNames(aws4Request.headers);
164
      var canonicalizedHeaders = canonicalizeHeaders(aws4Request.headers);
165
166
      // if any query string parameters have been supplied, canonicalize them
167
      // (note this sample assumes any required url encoding has been done already)
168
      var canonicalizedQueryParameters = '';
169
      if (aws4Request.queryParameters) {
170
          var paramDictionary = {};
171
          aws4Request.queryParameters.split('&').map(p => p.split('='))
172
                                    .forEach(kv => paramDictionary[kv[0]] = kv.length > 1 ? kv[1] : "");
173
174
          var sortedKeys = Object.keys(paramDictionary).sort();
175
          return sortedKeys.map(x => `${x}=${paramDictionary[x]}`).join('&');
176
      }
177
178
      // canonicalize the various components of the request
179
      var canonicalRequest = canonicalizeRequest(aws4Request.httpMethod,
180
                                                 aws4Request.endpointUri,
181
                                                 canonicalizedQueryParameters,
182
                                                 canonicalizedHeaders,
183
                                                 canonicalizedHeaderNames,
184
                                                 aws4Request.bodyHash);
185
      //console.log(`\nCanonicalRequest:\n${canonicalRequest}`);
186
187
      // generate a hash of the canonical request, to go into signature computation
188
      var canonicalRequestHashBits = sjcl.hash.sha256.hash(sjcl.codec.utf8String.toBits(canonicalRequest));
189
190
      var dateStamp = dateStringFormat(dateTime);
191
      var scope = `${dateStamp}/${aws4Request.region}/${aws4Request.service}/${TERMINATOR}`;
192
      var stringToSign = `${SCHEME}-${ALGORITHM}\n${dateTimeStamp}\n${scope}\n${toHexString(canonicalRequestHashBits, true)}`;
193
194
      //console.log(`\nStringToSign:\n${stringToSign}`);
195
196
197
      var signingKey =  deriveSigningKey(dateStamp, aws4Request.region, aws4Request.service);
198
      //console.log(`\nKey:\n${toHexString(signingKey, true)}`);
199
200
      var signature = new sjcl.misc.hmac(signingKey, sjcl.hash.sha256).encrypt(utf8ToBits(stringToSign));
201
      var signatureString = toHexString(signature, true);
202
      //console.log(`\nSignature:\n${signatureString}`);
203
204
      var authorization = `${SCHEME}-${ALGORITHM} Credential=${awsAccessKeyId}/${scope}, `
205
                        + `SignedHeaders=${canonicalizedHeaderNames}, Signature=${signatureString}`;
206
207
      //console.log(`\nAuthorization:\n${authorization}`);
208
      return authorization;
209
    };
210
  })(app.sjcl, app.AWSAccessKeyId, app.AWSSecretAccessKey);
211
})(window.sequentialReadPasswordManager, document);