+
+
The card contents will be here.
+
Expires after: exp
+ ${input}
+
+
})
+ ${copyText}
+
+
+
+
+`;
+}
diff --git a/android/pages/upload.js b/android/pages/upload.js
new file mode 100644
index 00000000..c98962dd
--- /dev/null
+++ b/android/pages/upload.js
@@ -0,0 +1,26 @@
+const html = require('choo/html');
+
+export default function progressBar(state, emit) {
+ let percent = 0;
+ if (state.transfer && state.transfer.progress) {
+ percent = Math.floor(state.transfer.progressRatio * 100);
+ }
+ function onclick(e) {
+ e.preventDefault();
+ if (state.uploading) {
+ emit('cancel');
+ }
+ emit('pushState', '/');
+ }
+ return html`
+
+
+
+
${percent}%
+
.
+
CANCEL
+
+
+
+ `;
+}
diff --git a/android/settings.gradle b/android/settings.gradle
new file mode 100644
index 00000000..e7b4def4
--- /dev/null
+++ b/android/settings.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/android/stores/intents.js b/android/stores/intents.js
new file mode 100644
index 00000000..ba757eaa
--- /dev/null
+++ b/android/stores/intents.js
@@ -0,0 +1,20 @@
+/* eslint-disable no-console */
+
+export default function intentHandler(state, emitter) {
+ window.addEventListener(
+ 'message',
+ event => {
+ if (typeof event.data !== 'string' || !event.data.startsWith('data:')) {
+ return;
+ }
+ fetch(event.data)
+ .then(res => res.blob())
+ .then(blob => {
+ emitter.emit('addFiles', { files: [blob] });
+ emitter.emit('upload', {});
+ })
+ .catch(e => console.error('ERROR ' + e + ' ' + e.stack));
+ },
+ false
+ );
+}
diff --git a/android/stores/state.js b/android/stores/state.js
new file mode 100644
index 00000000..a11058b5
--- /dev/null
+++ b/android/stores/state.js
@@ -0,0 +1,41 @@
+/* eslint-disable no-console */
+
+import User from '../user';
+import storage from '../../app/storage';
+
+export default function initialState(state, emitter) {
+ const files = [];
+
+ Object.assign(state, {
+ prefix: '/android_asset',
+ user: new User(storage),
+ getAsset(name) {
+ return `${state.prefix}/${name}`;
+ },
+ sentry: {
+ captureException: e => {
+ console.error('ERROR ' + e + ' ' + e.stack);
+ }
+ },
+ storage: {
+ files,
+ remove: function(fileId) {
+ console.log('REMOVE FILEID', fileId);
+ },
+ writeFile: function(file) {
+ console.log('WRITEFILE', file);
+ },
+ addFile: function(file) {
+ console.log('addfile' + JSON.stringify(file));
+ files.push(file);
+ emitter.emit('pushState', `/share/${file.id}`);
+ },
+ totalUploads: 0
+ },
+ transfer: null,
+ uploading: false,
+ settingPassword: false,
+ passwordSetError: null,
+ route: '/'
+ });
+}
diff --git a/android/user.js b/android/user.js
new file mode 100644
index 00000000..f9c056b6
--- /dev/null
+++ b/android/user.js
@@ -0,0 +1,30 @@
+/* global Android */
+import User from '../app/user';
+import { deriveFileListKey } from '../app/fxa';
+
+export default class AndroidUser extends User {
+ constructor(storage, limits) {
+ super(storage, limits);
+ }
+
+ async login() {
+ Android.beginOAuthFlow();
+ }
+
+ startAuthFlow() {
+ return Promise.resolve();
+ }
+
+ async finishLogin(accountInfo) {
+ const jwks = JSON.parse(accountInfo.keys);
+ const ikm = jwks['https://identity.mozilla.com/apps/send'].k;
+ const profile = {
+ displayName: accountInfo.displayName,
+ email: accountInfo.email,
+ avatar: accountInfo.avatar,
+ access_token: accountInfo.accessToken
+ };
+ profile.fileListKey = await deriveFileListKey(ikm);
+ this.info = profile;
+ }
+}
diff --git a/app/api.js b/app/api.js
new file mode 100644
index 00000000..2d1238c2
--- /dev/null
+++ b/app/api.js
@@ -0,0 +1,432 @@
+import { arrayToB64, b64ToArray, delay } from './utils';
+import { ECE_RECORD_SIZE } from './ece';
+
+let fileProtocolWssUrl = null;
+try {
+ fileProtocolWssUrl = localStorage.getItem('wssURL');
+} catch (e) {
+ // NOOP
+}
+if (!fileProtocolWssUrl) {
+ fileProtocolWssUrl = 'wss://send.firefox.com/api/ws';
+}
+
+export class ConnectionError extends Error {
+ constructor(cancelled, duration, size) {
+ super(cancelled ? '0' : 'connection closed');
+ this.cancelled = cancelled;
+ this.duration = duration;
+ this.size = size;
+ }
+}
+
+export function setFileProtocolWssUrl(url) {
+ localStorage && localStorage.setItem('wssURL', url);
+ fileProtocolWssUrl = url;
+}
+
+export function getFileProtocolWssUrl() {
+ return fileProtocolWssUrl;
+}
+
+let apiUrlPrefix = '';
+export function getApiUrl(path) {
+ return apiUrlPrefix + path;
+}
+
+export function setApiUrlPrefix(prefix) {
+ apiUrlPrefix = prefix;
+}
+
+function post(obj, bearerToken) {
+ const h = {
+ 'Content-Type': 'application/json'
+ };
+ if (bearerToken) {
+ h['Authorization'] = `Bearer ${bearerToken}`;
+ }
+ return {
+ method: 'POST',
+ headers: new Headers(h),
+ body: JSON.stringify(obj)
+ };
+}
+
+export function parseNonce(header) {
+ header = header || '';
+ return header.split(' ')[1];
+}
+
+async function fetchWithAuth(url, params, keychain) {
+ const result = {};
+ params = params || {};
+ const h = await keychain.authHeader();
+ params.headers = new Headers({
+ Authorization: h,
+ 'Content-Type': 'application/json'
+ });
+ const response = await fetch(url, params);
+ result.response = response;
+ result.ok = response.ok;
+ const nonce = parseNonce(response.headers.get('WWW-Authenticate'));
+ result.shouldRetry = response.status === 401 && nonce !== keychain.nonce;
+ keychain.nonce = nonce;
+ return result;
+}
+
+async function fetchWithAuthAndRetry(url, params, keychain) {
+ const result = await fetchWithAuth(url, params, keychain);
+ if (result.shouldRetry) {
+ return fetchWithAuth(url, params, keychain);
+ }
+ return result;
+}
+
+export async function del(id, owner_token) {
+ const response = await fetch(
+ getApiUrl(`/api/delete/${id}`),
+ post({ owner_token })
+ );
+ return response.ok;
+}
+
+export async function setParams(id, owner_token, bearerToken, params) {
+ const response = await fetch(
+ getApiUrl(`/api/params/${id}`),
+ post(
+ {
+ owner_token,
+ dlimit: params.dlimit
+ },
+ bearerToken
+ )
+ );
+ return response.ok;
+}
+
+export async function fileInfo(id, owner_token) {
+ const response = await fetch(
+ getApiUrl(`/api/info/${id}`),
+ post({ owner_token })
+ );
+
+ if (response.ok) {
+ const obj = await response.json();
+ return obj;
+ }
+
+ throw new Error(response.status);
+}
+
+export async function metadata(id, keychain) {
+ const result = await fetchWithAuthAndRetry(
+ getApiUrl(`/api/metadata/${id}`),
+ { method: 'GET' },
+ keychain
+ );
+ if (result.ok) {
+ const data = await result.response.json();
+ const meta = await keychain.decryptMetadata(b64ToArray(data.metadata));
+ return {
+ size: meta.size,
+ ttl: data.ttl,
+ iv: meta.iv,
+ name: meta.name,
+ type: meta.type,
+ manifest: meta.manifest
+ };
+ }
+ throw new Error(result.response.status);
+}
+
+export async function setPassword(id, owner_token, keychain) {
+ const auth = await keychain.authKeyB64();
+ const response = await fetch(
+ getApiUrl(`/api/password/${id}`),
+ post({ owner_token, auth })
+ );
+ return response.ok;
+}
+
+function asyncInitWebSocket(server) {
+ return new Promise((resolve, reject) => {
+ try {
+ const ws = new WebSocket(server);
+ ws.addEventListener('open', () => resolve(ws), { once: true });
+ } catch (e) {
+ reject(new ConnectionError(false));
+ }
+ });
+}
+
+function listenForResponse(ws, canceller) {
+ return new Promise((resolve, reject) => {
+ function handleClose(event) {
+ // a 'close' event before a 'message' event means the request failed
+ ws.removeEventListener('message', handleMessage);
+ reject(new ConnectionError(canceller.cancelled));
+ }
+ function handleMessage(msg) {
+ ws.removeEventListener('close', handleClose);
+ try {
+ const response = JSON.parse(msg.data);
+ if (response.error) {
+ throw new Error(response.error);
+ } else {
+ resolve(response);
+ }
+ } catch (e) {
+ reject(e);
+ }
+ }
+ ws.addEventListener('message', handleMessage, { once: true });
+ ws.addEventListener('close', handleClose, { once: true });
+ });
+}
+
+async function upload(
+ stream,
+ metadata,
+ verifierB64,
+ timeLimit,
+ dlimit,
+ bearerToken,
+ onprogress,
+ canceller
+) {
+ let size = 0;
+ const start = Date.now();
+ const host = window.location.hostname;
+ const port = window.location.port;
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+ const endpoint =
+ window.location.protocol === 'file:'
+ ? fileProtocolWssUrl
+ : `${protocol}//${host}${port ? ':' : ''}${port}/api/ws`;
+
+ const ws = await asyncInitWebSocket(endpoint);
+
+ try {
+ const metadataHeader = arrayToB64(new Uint8Array(metadata));
+ const fileMeta = {
+ fileMetadata: metadataHeader,
+ authorization: `send-v1 ${verifierB64}`,
+ bearer: bearerToken,
+ timeLimit,
+ dlimit
+ };
+ const uploadInfoResponse = listenForResponse(ws, canceller);
+ ws.send(JSON.stringify(fileMeta));
+ const uploadInfo = await uploadInfoResponse;
+
+ const completedResponse = listenForResponse(ws, canceller);
+
+ const reader = stream.getReader();
+ let state = await reader.read();
+ while (!state.done) {
+ if (canceller.cancelled) {
+ ws.close();
+ }
+ if (ws.readyState !== WebSocket.OPEN) {
+ break;
+ }
+ const buf = state.value;
+ ws.send(buf);
+ onprogress(size);
+ size += buf.length;
+ state = await reader.read();
+ while (
+ ws.bufferedAmount > ECE_RECORD_SIZE * 2 &&
+ ws.readyState === WebSocket.OPEN &&
+ !canceller.cancelled
+ ) {
+ await delay();
+ }
+ }
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.send(new Uint8Array([0])); //EOF
+ }
+
+ await completedResponse;
+ uploadInfo.duration = Date.now() - start;
+ return uploadInfo;
+ } catch (e) {
+ e.size = size;
+ e.duration = Date.now() - start;
+ throw e;
+ } finally {
+ if (![WebSocket.CLOSED, WebSocket.CLOSING].includes(ws.readyState)) {
+ ws.close();
+ }
+ }
+}
+
+export function uploadWs(
+ encrypted,
+ metadata,
+ verifierB64,
+ timeLimit,
+ dlimit,
+ bearerToken,
+ onprogress
+) {
+ const canceller = { cancelled: false };
+
+ return {
+ cancel: function() {
+ canceller.cancelled = true;
+ },
+
+ result: upload(
+ encrypted,
+ metadata,
+ verifierB64,
+ timeLimit,
+ dlimit,
+ bearerToken,
+ onprogress,
+ canceller
+ )
+ };
+}
+
+////////////////////////
+
+async function downloadS(id, keychain, signal) {
+ const auth = await keychain.authHeader();
+
+ const response = await fetch(getApiUrl(`/api/download/${id}`), {
+ signal: signal,
+ method: 'GET',
+ headers: { Authorization: auth }
+ });
+
+ const authHeader = response.headers.get('WWW-Authenticate');
+ if (authHeader) {
+ keychain.nonce = parseNonce(authHeader);
+ }
+
+ if (response.status !== 200) {
+ throw new Error(response.status);
+ }
+
+ return response.body;
+}
+
+async function tryDownloadStream(id, keychain, signal, tries = 2) {
+ try {
+ const result = await downloadS(id, keychain, signal);
+ return result;
+ } catch (e) {
+ if (e.message === '401' && --tries > 0) {
+ return tryDownloadStream(id, keychain, signal, tries);
+ }
+ if (e.name === 'AbortError') {
+ throw new Error('0');
+ }
+ throw e;
+ }
+}
+
+export function downloadStream(id, keychain) {
+ const controller = new AbortController();
+ function cancel() {
+ controller.abort();
+ }
+ return {
+ cancel,
+ result: tryDownloadStream(id, keychain, controller.signal)
+ };
+}
+
+//////////////////
+
+async function download(id, keychain, onprogress, canceller) {
+ const auth = await keychain.authHeader();
+ const xhr = new XMLHttpRequest();
+ canceller.oncancel = function() {
+ xhr.abort();
+ };
+ return new Promise(function(resolve, reject) {
+ xhr.addEventListener('loadend', function() {
+ canceller.oncancel = function() {};
+ const authHeader = xhr.getResponseHeader('WWW-Authenticate');
+ if (authHeader) {
+ keychain.nonce = parseNonce(authHeader);
+ }
+ if (xhr.status !== 200) {
+ return reject(new Error(xhr.status));
+ }
+
+ const blob = new Blob([xhr.response]);
+ resolve(blob);
+ });
+
+ xhr.addEventListener('progress', function(event) {
+ if (event.target.status === 200) {
+ onprogress(event.loaded);
+ }
+ });
+ xhr.open('get', getApiUrl(`/api/download/blob/${id}`));
+ xhr.setRequestHeader('Authorization', auth);
+ xhr.responseType = 'blob';
+ xhr.send();
+ onprogress(0);
+ });
+}
+
+async function tryDownload(id, keychain, onprogress, canceller, tries = 2) {
+ try {
+ const result = await download(id, keychain, onprogress, canceller);
+ return result;
+ } catch (e) {
+ if (e.message === '401' && --tries > 0) {
+ return tryDownload(id, keychain, onprogress, canceller, tries);
+ }
+ throw e;
+ }
+}
+
+export function downloadFile(id, keychain, onprogress) {
+ const canceller = {
+ oncancel: function() {} // download() sets this
+ };
+ function cancel() {
+ canceller.oncancel();
+ }
+ return {
+ cancel,
+ result: tryDownload(id, keychain, onprogress, canceller)
+ };
+}
+
+export async function getFileList(bearerToken, kid) {
+ const headers = new Headers({ Authorization: `Bearer ${bearerToken}` });
+ const response = await fetch(getApiUrl(`/api/filelist/${kid}`), { headers });
+ if (response.ok) {
+ const encrypted = await response.blob();
+ return encrypted;
+ }
+ throw new Error(response.status);
+}
+
+export async function setFileList(bearerToken, kid, data) {
+ const headers = new Headers({ Authorization: `Bearer ${bearerToken}` });
+ const response = await fetch(getApiUrl(`/api/filelist/${kid}`), {
+ headers,
+ method: 'POST',
+ body: data
+ });
+ return response.ok;
+}
+
+export async function getConstants() {
+ const response = await fetch(getApiUrl('/config'));
+
+ if (response.ok) {
+ const obj = await response.json();
+ return obj;
+ }
+
+ throw new Error(response.status);
+}
diff --git a/app/archive.js b/app/archive.js
new file mode 100644
index 00000000..683cc370
--- /dev/null
+++ b/app/archive.js
@@ -0,0 +1,84 @@
+import { blobStream, concatStream } from './streams';
+
+function isDupe(newFile, array) {
+ for (const file of array) {
+ if (
+ newFile.name === file.name &&
+ newFile.size === file.size &&
+ newFile.lastModified === file.lastModified
+ ) {
+ return true;
+ }
+ }
+ return false;
+}
+
+export default class Archive {
+ constructor(files = [], defaultTimeLimit = 86400, defaultDownloadLimit = 1) {
+ this.files = Array.from(files);
+ this.defaultTimeLimit = defaultTimeLimit;
+ this.defaultDownloadLimit = defaultDownloadLimit;
+ this.timeLimit = defaultTimeLimit;
+ this.dlimit = defaultDownloadLimit;
+ this.password = null;
+ }
+
+ get name() {
+ return this.files.length > 1 ? 'Send-Archive.zip' : this.files[0].name;
+ }
+
+ get type() {
+ return this.files.length > 1 ? 'send-archive' : this.files[0].type;
+ }
+
+ get size() {
+ return this.files.reduce((total, file) => total + file.size, 0);
+ }
+
+ get numFiles() {
+ return this.files.length;
+ }
+
+ get manifest() {
+ return {
+ files: this.files.map(file => ({
+ name: file.name,
+ size: file.size,
+ type: file.type
+ }))
+ };
+ }
+
+ get stream() {
+ return concatStream(this.files.map(file => blobStream(file)));
+ }
+
+ addFiles(files, maxSize, maxFiles) {
+ if (this.files.length + files.length > maxFiles) {
+ throw new Error('tooManyFiles');
+ }
+ const newFiles = files.filter(
+ file => file.size > 0 && !isDupe(file, this.files)
+ );
+ const newSize = newFiles.reduce((total, file) => total + file.size, 0);
+ if (this.size + newSize > maxSize) {
+ throw new Error('fileTooBig');
+ }
+ this.files = this.files.concat(newFiles);
+ return true;
+ }
+
+ remove(file) {
+ const index = this.files.indexOf(file);
+ if (index > -1) {
+ this.files.splice(index, 1);
+ }
+ }
+
+ clear() {
+ this.files = [];
+ this.dlimit = this.defaultDownloadLimit;
+ this.timeLimit = this.defaultTimeLimit;
+ this.password = null;
+ }
+}
diff --git a/app/capabilities.js b/app/capabilities.js
new file mode 100644
index 00000000..d43a6b10
--- /dev/null
+++ b/app/capabilities.js
@@ -0,0 +1,116 @@
+/* global AUTH_CONFIG */
+import { browserName, locale } from './utils';
+
+async function checkCrypto() {
+ try {
+ const key = await crypto.subtle.generateKey(
+ {
+ name: 'AES-GCM',
+ length: 128
+ },
+ true,
+ ['encrypt', 'decrypt']
+ );
+ await crypto.subtle.exportKey('raw', key);
+ await crypto.subtle.encrypt(
+ {
+ name: 'AES-GCM',
+ iv: crypto.getRandomValues(new Uint8Array(12)),
+ tagLength: 128
+ },
+ key,
+ new ArrayBuffer(8)
+ );
+ await crypto.subtle.importKey(
+ 'raw',
+ crypto.getRandomValues(new Uint8Array(16)),
+ 'PBKDF2',
+ false,
+ ['deriveKey']
+ );
+ await crypto.subtle.importKey(
+ 'raw',
+ crypto.getRandomValues(new Uint8Array(16)),
+ 'HKDF',
+ false,
+ ['deriveKey']
+ );
+ await crypto.subtle.generateKey(
+ {
+ name: 'ECDH',
+ namedCurve: 'P-256'
+ },
+ true,
+ ['deriveBits']
+ );
+ return true;
+ } catch (err) {
+ try {
+ window.asmCrypto = await import('asmcrypto.js');
+ await import('@dannycoates/webcrypto-liner/build/shim');
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+}
+
+function checkStreams() {
+ try {
+ new ReadableStream({
+ pull() {}
+ });
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
+
+async function polyfillStreams() {
+ try {
+ await import('@mattiasbuelens/web-streams-polyfill');
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
+
+export default async function getCapabilities() {
+ const browser = browserName();
+ const isMobile = /mobi|android/i.test(navigator.userAgent);
+ const serviceWorker = 'serviceWorker' in navigator && browser !== 'edge';
+ let crypto = await checkCrypto();
+ const nativeStreams = checkStreams();
+ let polyStreams = false;
+ if (!nativeStreams) {
+ polyStreams = await polyfillStreams();
+ }
+ let account = typeof AUTH_CONFIG !== 'undefined';
+ try {
+ account = account && !!localStorage;
+ } catch (e) {
+ account = false;
+ }
+ const share =
+ isMobile &&
+ typeof navigator.share === 'function' &&
+ locale().startsWith('en'); // en until strings merge
+
+ const standalone =
+ window.matchMedia('(display-mode: standalone)').matches ||
+ navigator.standalone;
+
+ const mobileFirefox = browser === 'firefox' && isMobile;
+
+ return {
+ account,
+ crypto,
+ serviceWorker,
+ streamUpload: nativeStreams || polyStreams,
+ streamDownload:
+ nativeStreams && serviceWorker && browser !== 'safari' && !mobileFirefox,
+ multifile: nativeStreams || polyStreams,
+ share,
+ standalone
+ };
+}
diff --git a/app/controller.js b/app/controller.js
new file mode 100644
index 00000000..8c6945ac
--- /dev/null
+++ b/app/controller.js
@@ -0,0 +1,309 @@
+import FileReceiver from './fileReceiver';
+import FileSender from './fileSender';
+import copyDialog from './ui/copyDialog';
+import faviconProgressbar from './ui/faviconProgressbar';
+import okDialog from './ui/okDialog';
+import shareDialog from './ui/shareDialog';
+import signupDialog from './ui/signupDialog';
+import surveyDialog from './ui/surveyDialog';
+import { bytes, locale } from './utils';
+import { copyToClipboard, delay, openLinksInNewTab, percent } from './utils';
+
+export default function(state, emitter) {
+ let lastRender = 0;
+ let updateTitle = false;
+
+ function render() {
+ emitter.emit('render');
+ }
+
+ async function checkFiles() {
+ const changes = await state.user.syncFileList();
+ const rerender = changes.incoming || changes.downloadCount;
+ if (rerender) {
+ render();
+ }
+ }
+
+ function updateProgress() {
+ if (updateTitle) {
+ emitter.emit('DOMTitleChange', percent(state.transfer.progressRatio));
+ }
+ faviconProgressbar.updateFavicon(state.transfer.progressRatio);
+ render();
+ }
+
+ emitter.on('DOMContentLoaded', () => {
+ document.addEventListener('blur', () => (updateTitle = true));
+ document.addEventListener('focus', () => {
+ updateTitle = false;
+ emitter.emit('DOMTitleChange', 'Send');
+ faviconProgressbar.updateFavicon(0);
+ });
+ checkFiles();
+ });
+
+ emitter.on('render', () => {
+ lastRender = Date.now();
+ });
+
+ emitter.on('login', email => {
+ state.user.login(email);
+ });
+
+ emitter.on('logout', async () => {
+ await state.user.logout();
+ emitter.emit('pushState', '/');
+ });
+
+ emitter.on('removeUpload', file => {
+ state.archive.remove(file);
+ if (state.archive.numFiles === 0) {
+ state.archive.clear();
+ }
+ render();
+ });
+
+ emitter.on('delete', async ownedFile => {
+ try {
+ state.storage.remove(ownedFile.id);
+ await ownedFile.del();
+ } catch (e) {
+ state.sentry.captureException(e);
+ }
+ render();
+ });
+
+ emitter.on('cancel', () => {
+ state.transfer.cancel();
+ faviconProgressbar.updateFavicon(0);
+ });
+
+ emitter.on('addFiles', async ({ files }) => {
+ if (files.length < 1) {
+ return;
+ }
+ const maxSize = state.user.maxSize;
+ try {
+ state.archive.addFiles(
+ files,
+ maxSize,
+ state.LIMITS.MAX_FILES_PER_ARCHIVE
+ );
+ } catch (e) {
+ state.modal = okDialog(
+ state.translate(e.message, {
+ size: bytes(maxSize),
+ count: state.LIMITS.MAX_FILES_PER_ARCHIVE
+ })
+ );
+ }
+ render();
+ });
+
+ emitter.on('signup-cta', source => {
+ const query = state.query;
+ state.user.startAuthFlow(source, {
+ campaign: query.utm_campaign,
+ content: query.utm_content,
+ medium: query.utm_medium,
+ source: query.utm_source,
+ term: query.utm_term
+ });
+ state.modal = signupDialog();
+ render();
+ });
+
+ emitter.on('authenticate', async (code, oauthState) => {
+ try {
+ await state.user.finishLogin(code, oauthState);
+ await state.user.syncFileList();
+ emitter.emit('replaceState', '/');
+ } catch (e) {
+ emitter.emit('replaceState', '/error');
+ setTimeout(render);
+ }
+ });
+
+ emitter.on('upload', async () => {
+ if (state.storage.files.length >= state.LIMITS.MAX_ARCHIVES_PER_USER) {
+ state.modal = okDialog(
+ state.translate('tooManyArchives', {
+ count: state.LIMITS.MAX_ARCHIVES_PER_USER
+ })
+ );
+ return render();
+ }
+ const archive = state.archive;
+ const sender = new FileSender();
+
+ sender.on('progress', updateProgress);
+ sender.on('encrypting', render);
+ sender.on('complete', render);
+ state.transfer = sender;
+ state.uploading = true;
+ render();
+
+ const links = openLinksInNewTab();
+ await delay(200);
+ try {
+ const ownedFile = await sender.upload(archive, state.user.bearerToken);
+ state.storage.totalUploads += 1;
+ faviconProgressbar.updateFavicon(0);
+
+ state.storage.addFile(ownedFile);
+ // TODO integrate password into /upload request
+ if (archive.password) {
+ emitter.emit('password', {
+ password: archive.password,
+ file: ownedFile
+ });
+ }
+ state.modal = state.capabilities.share
+ ? shareDialog(ownedFile.name, ownedFile.url)
+ : copyDialog(ownedFile.name, ownedFile.url);
+ } catch (err) {
+ if (err.message === '0') {
+ //cancelled. do nothing
+ render();
+ } else if (err.message === '401') {
+ const refreshed = await state.user.refresh();
+ if (refreshed) {
+ return emitter.emit('upload');
+ }
+ emitter.emit('pushState', '/error');
+ } else {
+ // eslint-disable-next-line no-console
+ console.error(err);
+ state.sentry.withScope(scope => {
+ scope.setExtra('duration', err.duration);
+ scope.setExtra('size', err.size);
+ state.sentry.captureException(err);
+ });
+ emitter.emit('pushState', '/error');
+ }
+ } finally {
+ openLinksInNewTab(links, false);
+ archive.clear();
+ state.uploading = false;
+ state.transfer = null;
+ await state.user.syncFileList();
+ render();
+ }
+ });
+
+ emitter.on('password', async ({ password, file }) => {
+ try {
+ state.settingPassword = true;
+ render();
+ await file.setPassword(password);
+ state.storage.writeFile(file);
+ await delay(1000);
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.error(err);
+ state.passwordSetError = err;
+ } finally {
+ state.settingPassword = false;
+ }
+ render();
+ });
+
+ emitter.on('getMetadata', async () => {
+ const file = state.fileInfo;
+
+ const receiver = new FileReceiver(file);
+ try {
+ await receiver.getMetadata();
+ state.transfer = receiver;
+ } catch (e) {
+ if (e.message === '401' || e.message === '404') {
+ file.password = null;
+ if (!file.requiresPassword) {
+ return emitter.emit('pushState', '/404');
+ }
+ } else {
+ console.error(e);
+ return emitter.emit('pushState', '/error');
+ }
+ }
+
+ render();
+ });
+
+ emitter.on('download', async () => {
+ state.transfer.on('progress', updateProgress);
+ state.transfer.on('decrypting', render);
+ state.transfer.on('complete', render);
+ const links = openLinksInNewTab();
+ try {
+ const dl = state.transfer.download({
+ stream: state.capabilities.streamDownload
+ });
+ render();
+ await dl;
+ state.storage.totalDownloads += 1;
+ faviconProgressbar.updateFavicon(0);
+ } catch (err) {
+ if (err.message === '0') {
+ // download cancelled
+ state.transfer.reset();
+ render();
+ } else {
+ // eslint-disable-next-line no-console
+ state.transfer = null;
+ const location = err.message === '404' ? '/404' : '/error';
+ if (location === '/error') {
+ state.sentry.withScope(scope => {
+ scope.setExtra('duration', err.duration);
+ scope.setExtra('size', err.size);
+ scope.setExtra('progress', err.progress);
+ state.sentry.captureException(err);
+ });
+ }
+ emitter.emit('pushState', location);
+ }
+ } finally {
+ openLinksInNewTab(links, false);
+ }
+ });
+
+ emitter.on('copy', ({ url }) => {
+ copyToClipboard(url);
+ });
+
+ emitter.on('closeModal', () => {
+ if (
+ state.PREFS.surveyUrl &&
+ ['copy', 'share'].includes(state.modal.type) &&
+ locale().startsWith('en') &&
+ (state.storage.totalUploads > 1 || state.storage.totalDownloads > 0) &&
+ !state.user.surveyed
+ ) {
+ state.user.surveyed = true;
+ state.modal = surveyDialog();
+ } else {
+ state.modal = null;
+ }
+ render();
+ });
+
+ setInterval(() => {
+ // poll for updates of the upload list
+ if (!state.modal && state.route === '/') {
+ checkFiles();
+ }
+ }, 2 * 60 * 1000);
+
+ setInterval(() => {
+ // poll for rerendering the file list countdown timers
+ if (
+ !state.modal &&
+ state.route === '/' &&
+ state.storage.files.length > 0 &&
+ Date.now() - lastRender > 30000
+ ) {
+ render();
+ }
+ }, 60000);
+}
diff --git a/app/dragManager.js b/app/dragManager.js
index a878853a..1379c004 100644
--- a/app/dragManager.js
+++ b/app/dragManager.js
@@ -6,18 +6,16 @@ export default function(state, emitter) {
}
});
document.body.addEventListener('drop', event => {
- if (state.route === '/' && !state.transfer) {
+ if (
+ state.route === '/' &&
+ !state.uploading &&
+ event.dataTransfer &&
+ event.dataTransfer.files
+ ) {
event.preventDefault();
- document.querySelector('.upload-window').classList.remove('ondrag');
- const target = event.dataTransfer;
- if (target.files.length === 0) {
- return;
- }
- if (target.files.length > 1 || target.files[0].size === 0) {
- return alert(state.translate('uploadPageMultipleFilesAlert'));
- }
- const file = target.files[0];
- emitter.emit('upload', { file, type: 'drop' });
+ emitter.emit('addFiles', {
+ files: Array.from(event.dataTransfer.files)
+ });
}
});
});
diff --git a/app/ece.js b/app/ece.js
new file mode 100644
index 00000000..4cd6b45e
--- /dev/null
+++ b/app/ece.js
@@ -0,0 +1,310 @@
+import 'buffer';
+import { transformStream } from './streams';
+
+const NONCE_LENGTH = 12;
+const TAG_LENGTH = 16;
+const KEY_LENGTH = 16;
+const MODE_ENCRYPT = 'encrypt';
+const MODE_DECRYPT = 'decrypt';
+export const ECE_RECORD_SIZE = 1024 * 64;
+
+const encoder = new TextEncoder();
+
+function generateSalt(len) {
+ const randSalt = new Uint8Array(len);
+ crypto.getRandomValues(randSalt);
+ return randSalt.buffer;
+}
+
+class ECETransformer {
+ constructor(mode, ikm, rs, salt) {
+ this.mode = mode;
+ this.prevChunk;
+ this.seq = 0;
+ this.firstchunk = true;
+ this.rs = rs;
+ this.ikm = ikm.buffer;
+ this.salt = salt;
+ }
+
+ async generateKey() {
+ const inputKey = await crypto.subtle.importKey(
+ 'raw',
+ this.ikm,
+ 'HKDF',
+ false,
+ ['deriveKey']
+ );
+
+ return crypto.subtle.deriveKey(
+ {
+ name: 'HKDF',
+ salt: this.salt,
+ info: encoder.encode('Content-Encoding: aes128gcm\0'),
+ hash: 'SHA-256'
+ },
+ inputKey,
+ {
+ name: 'AES-GCM',
+ length: 128
+ },
+ true, // Edge polyfill requires key to be extractable to encrypt :/
+ ['encrypt', 'decrypt']
+ );
+ }
+
+ async generateNonceBase() {
+ const inputKey = await crypto.subtle.importKey(
+ 'raw',
+ this.ikm,
+ 'HKDF',
+ false,
+ ['deriveKey']
+ );
+
+ const base = await crypto.subtle.exportKey(
+ 'raw',
+ await crypto.subtle.deriveKey(
+ {
+ name: 'HKDF',
+ salt: this.salt,
+ info: encoder.encode('Content-Encoding: nonce\0'),
+ hash: 'SHA-256'
+ },
+ inputKey,
+ {
+ name: 'AES-GCM',
+ length: 128
+ },
+ true,
+ ['encrypt', 'decrypt']
+ )
+ );
+
+ return Buffer.from(base.slice(0, NONCE_LENGTH));
+ }
+
+ generateNonce(seq) {
+ if (seq > 0xffffffff) {
+ throw new Error('record sequence number exceeds limit');
+ }
+ const nonce = Buffer.from(this.nonceBase);
+ const m = nonce.readUIntBE(nonce.length - 4, 4);
+ const xor = (m ^ seq) >>> 0; //forces unsigned int xor
+ nonce.writeUIntBE(xor, nonce.length - 4, 4);
+
+ return nonce;
+ }
+
+ pad(data, isLast) {
+ const len = data.length;
+ if (len + TAG_LENGTH >= this.rs) {
+ throw new Error('data too large for record size');
+ }
+
+ if (isLast) {
+ const padding = Buffer.alloc(1);
+ padding.writeUInt8(2, 0);
+ return Buffer.concat([data, padding]);
+ } else {
+ const padding = Buffer.alloc(this.rs - len - TAG_LENGTH);
+ padding.fill(0);
+ padding.writeUInt8(1, 0);
+ return Buffer.concat([data, padding]);
+ }
+ }
+
+ unpad(data, isLast) {
+ for (let i = data.length - 1; i >= 0; i--) {
+ if (data[i]) {
+ if (isLast) {
+ if (data[i] !== 2) {
+ throw new Error('delimiter of final record is not 2');
+ }
+ } else {
+ if (data[i] !== 1) {
+ throw new Error('delimiter of not final record is not 1');
+ }
+ }
+ return data.slice(0, i);
+ }
+ }
+ throw new Error('no delimiter found');
+ }
+
+ createHeader() {
+ const nums = Buffer.alloc(5);
+ nums.writeUIntBE(this.rs, 0, 4);
+ nums.writeUIntBE(0, 4, 1);
+ return Buffer.concat([Buffer.from(this.salt), nums]);
+ }
+
+ readHeader(buffer) {
+ if (buffer.length < 21) {
+ throw new Error('chunk too small for reading header');
+ }
+ const header = {};
+ header.salt = buffer.buffer.slice(0, KEY_LENGTH);
+ header.rs = buffer.readUIntBE(KEY_LENGTH, 4);
+ const idlen = buffer.readUInt8(KEY_LENGTH + 4);
+ header.length = idlen + KEY_LENGTH + 5;
+ return header;
+ }
+
+ async encryptRecord(buffer, seq, isLast) {
+ const nonce = this.generateNonce(seq);
+ const encrypted = await crypto.subtle.encrypt(
+ { name: 'AES-GCM', iv: nonce },
+ this.key,
+ this.pad(buffer, isLast)
+ );
+ return Buffer.from(encrypted);
+ }
+
+ async decryptRecord(buffer, seq, isLast) {
+ const nonce = this.generateNonce(seq);
+ const data = await crypto.subtle.decrypt(
+ {
+ name: 'AES-GCM',
+ iv: nonce,
+ tagLength: 128
+ },
+ this.key,
+ buffer
+ );
+
+ return this.unpad(Buffer.from(data), isLast);
+ }
+
+ async start(controller) {
+ if (this.mode === MODE_ENCRYPT) {
+ this.key = await this.generateKey();
+ this.nonceBase = await this.generateNonceBase();
+ controller.enqueue(this.createHeader());
+ } else if (this.mode !== MODE_DECRYPT) {
+ throw new Error('mode must be either encrypt or decrypt');
+ }
+ }
+
+ async transformPrevChunk(isLast, controller) {
+ if (this.mode === MODE_ENCRYPT) {
+ controller.enqueue(
+ await this.encryptRecord(this.prevChunk, this.seq, isLast)
+ );
+ this.seq++;
+ } else {
+ if (this.seq === 0) {
+ //the first chunk during decryption contains only the header
+ const header = this.readHeader(this.prevChunk);
+ this.salt = header.salt;
+ this.rs = header.rs;
+ this.key = await this.generateKey();
+ this.nonceBase = await this.generateNonceBase();
+ } else {
+ controller.enqueue(
+ await this.decryptRecord(this.prevChunk, this.seq - 1, isLast)
+ );
+ }
+ this.seq++;
+ }
+ }
+
+ async transform(chunk, controller) {
+ if (!this.firstchunk) {
+ await this.transformPrevChunk(false, controller);
+ }
+ this.firstchunk = false;
+ this.prevChunk = Buffer.from(chunk.buffer);
+ }
+
+ async flush(controller) {
+ //console.log('ece stream ends')
+ if (this.prevChunk) {
+ await this.transformPrevChunk(true, controller);
+ }
+ }
+}
+
+class StreamSlicer {
+ constructor(rs, mode) {
+ this.mode = mode;
+ this.rs = rs;
+ this.chunkSize = mode === MODE_ENCRYPT ? rs - 17 : 21;
+ this.partialChunk = new Uint8Array(this.chunkSize); //where partial chunks are saved
+ this.offset = 0;
+ }
+
+ send(buf, controller) {
+ controller.enqueue(buf);
+ if (this.chunkSize === 21 && this.mode === MODE_DECRYPT) {
+ this.chunkSize = this.rs;
+ }
+ this.partialChunk = new Uint8Array(this.chunkSize);
+ this.offset = 0;
+ }
+
+ //reslice input into record sized chunks
+ transform(chunk, controller) {
+ //console.log('Received chunk with %d bytes.', chunk.byteLength)
+ let i = 0;
+
+ if (this.offset > 0) {
+ const len = Math.min(chunk.byteLength, this.chunkSize - this.offset);
+ this.partialChunk.set(chunk.slice(0, len), this.offset);
+ this.offset += len;
+ i += len;
+
+ if (this.offset === this.chunkSize) {
+ this.send(this.partialChunk, controller);
+ }
+ }
+
+ while (i < chunk.byteLength) {
+ const remainingBytes = chunk.byteLength - i;
+ if (remainingBytes >= this.chunkSize) {
+ const record = chunk.slice(i, i + this.chunkSize);
+ i += this.chunkSize;
+ this.send(record, controller);
+ } else {
+ const end = chunk.slice(i, i + remainingBytes);
+ i += end.byteLength;
+ this.partialChunk.set(end);
+ this.offset = end.byteLength;
+ }
+ }
+ }
+
+ flush(controller) {
+ if (this.offset > 0) {
+ controller.enqueue(this.partialChunk.slice(0, this.offset));
+ }
+ }
+}
+
+/*
+input: a ReadableStream containing data to be transformed
+key: Uint8Array containing key of size KEY_LENGTH
+rs: int containing record size, optional
+salt: ArrayBuffer containing salt of KEY_LENGTH length, optional
+*/
+export function encryptStream(
+ input,
+ key,
+ rs = ECE_RECORD_SIZE,
+ salt = generateSalt(KEY_LENGTH)
+) {
+ const mode = 'encrypt';
+ const inputStream = transformStream(input, new StreamSlicer(rs, mode));
+ return transformStream(inputStream, new ECETransformer(mode, key, rs, salt));
+}
+
+/*
+input: a ReadableStream containing data to be transformed
+key: Uint8Array containing key of size KEY_LENGTH
+rs: int containing record size, optional
+*/
+export function decryptStream(input, key, rs = ECE_RECORD_SIZE) {
+ const mode = 'decrypt';
+ const inputStream = transformStream(input, new StreamSlicer(rs, mode));
+ return transformStream(inputStream, new ECETransformer(mode, key, rs));
+}
diff --git a/app/experiments.js b/app/experiments.js
index 37bce4e5..8e432e0a 100644
--- a/app/experiments.js
+++ b/app/experiments.js
@@ -1,25 +1,19 @@
import hash from 'string-hash';
+import Account from './ui/account';
const experiments = {
- 'SyI-hI7gT9agiH-f3f0BYg': {
- id: 'SyI-hI7gT9agiH-f3f0BYg',
- run: function(variant, state, emitter) {
- state.promo = variant === 1 ? 'body' : 'header';
- emitter.emit('render');
- },
+ signin_button_color: {
eligible: function() {
- return (
- !/firefox/i.test(navigator.userAgent) &&
- document.querySelector('html').lang === 'en-US'
- );
+ return true;
},
- variant: function(state) {
- return this.luckyNumber(state) > 0.5 ? 1 : 0;
+ variant: function() {
+ return ['white-primary', 'primary', 'white-violet', 'violet'][
+ Math.floor(Math.random() * 4)
+ ];
},
- luckyNumber: function(state) {
- return luckyNumber(
- `${this.id}:${state.storage.get('testpilot_ga__cid')}`
- );
+ run: function(variant, state) {
+ const account = state.cache(Account, 'account');
+ account.buttonClass = variant;
}
}
};
@@ -47,23 +41,12 @@ export default function initialize(state, emitter) {
xp.run(+state.query.v, state, emitter);
}
});
-
- if (!state.storage.get('testpilot_ga__cid')) {
- // first ever visit. check again after cid is assigned.
- emitter.on('DOMContentLoaded', () => {
- checkExperiments(state, emitter);
- });
+ const enrolled = state.storage.enrolled;
+ // single experiment per session for now
+ const id = Object.keys(enrolled)[0];
+ if (Object.keys(experiments).includes(id)) {
+ experiments[id].run(enrolled[id], state, emitter);
} else {
- const enrolled = state.storage.enrolled.filter(([id, variant]) => {
- const xp = experiments[id];
- if (xp) {
- xp.run(variant, state, emitter);
- }
- return !!xp;
- });
- // single experiment per session for now
- if (enrolled.length === 0) {
- checkExperiments(state, emitter);
- }
+ checkExperiments(state, emitter);
}
}
diff --git a/app/fileManager.js b/app/fileManager.js
deleted file mode 100644
index 588fd93a..00000000
--- a/app/fileManager.js
+++ /dev/null
@@ -1,249 +0,0 @@
-/* global EXPIRE_SECONDS */
-import FileSender from './fileSender';
-import FileReceiver from './fileReceiver';
-import { copyToClipboard, delay, fadeOut, percent } from './utils';
-import * as metrics from './metrics';
-
-function saveFile(file) {
- const dataView = new DataView(file.plaintext);
- const blob = new Blob([dataView], { type: file.type });
- const downloadUrl = URL.createObjectURL(blob);
-
- if (window.navigator.msSaveBlob) {
- return window.navigator.msSaveBlob(blob, file.name);
- }
- const a = document.createElement('a');
- a.href = downloadUrl;
- a.download = file.name;
- document.body.appendChild(a);
- a.click();
- URL.revokeObjectURL(downloadUrl);
-}
-
-function openLinksInNewTab(links, should = true) {
- links = links || Array.from(document.querySelectorAll('a:not([target])'));
- if (should) {
- links.forEach(l => {
- l.setAttribute('target', '_blank');
- l.setAttribute('rel', 'noopener noreferrer');
- });
- } else {
- links.forEach(l => {
- l.removeAttribute('target');
- l.removeAttribute('rel');
- });
- }
- return links;
-}
-
-function exists(id) {
- return new Promise((resolve, reject) => {
- const xhr = new XMLHttpRequest();
- xhr.onreadystatechange = () => {
- if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
- resolve(xhr.status === 200);
- }
- };
- xhr.onerror = () => resolve(false);
- xhr.ontimeout = () => resolve(false);
- xhr.open('get', '/api/exists/' + id);
- xhr.timeout = 2000;
- xhr.send();
- });
-}
-
-export default function(state, emitter) {
- let lastRender = 0;
- let updateTitle = false;
-
- function render() {
- emitter.emit('render');
- }
-
- async function checkFiles() {
- const files = state.storage.files;
- let rerender = false;
- for (const file of files) {
- const ok = await exists(file.id);
- if (!ok) {
- state.storage.remove(file.id);
- rerender = true;
- }
- }
- if (rerender) {
- render();
- }
- }
-
- function updateProgress() {
- if (updateTitle) {
- emitter.emit('DOMTitleChange', percent(state.transfer.progressRatio));
- }
- render();
- }
-
- emitter.on('DOMContentLoaded', () => {
- document.addEventListener('blur', () => (updateTitle = true));
- document.addEventListener('focus', () => {
- updateTitle = false;
- emitter.emit('DOMTitleChange', 'Firefox Send');
- });
- checkFiles();
- });
-
- emitter.on('navigate', checkFiles);
-
- emitter.on('render', () => {
- lastRender = Date.now();
- });
-
- emitter.on('delete', async ({ file, location }) => {
- try {
- metrics.deletedUpload({
- size: file.size,
- time: file.time,
- speed: file.speed,
- type: file.type,
- ttl: file.expiresAt - Date.now(),
- location
- });
- state.storage.remove(file.id);
- await FileSender.delete(file.id, file.deleteToken);
- } catch (e) {
- state.raven.captureException(e);
- }
- state.fileInfo = null;
- });
-
- emitter.on('cancel', () => {
- state.transfer.cancel();
- });
-
- emitter.on('upload', async ({ file, type }) => {
- const size = file.size;
- const sender = new FileSender(file);
- sender.on('progress', updateProgress);
- sender.on('encrypting', render);
- state.transfer = sender;
- render();
- const links = openLinksInNewTab();
- await delay(200);
- try {
- const start = Date.now();
- metrics.startedUpload({ size, type });
- const info = await sender.upload();
- const time = Date.now() - start;
- const speed = size / (time / 1000);
- metrics.completedUpload({ size, time, speed, type });
- document.getElementById('cancel-upload').hidden = 'hidden';
- await delay(1000);
- await fadeOut('upload-progress');
- info.name = file.name;
- info.size = size;
- info.type = type;
- info.time = time;
- info.speed = speed;
- info.createdAt = Date.now();
- info.url = `${info.url}#${info.secretKey}`;
- info.expiresAt = Date.now() + EXPIRE_SECONDS * 1000;
- state.fileInfo = info;
- state.storage.addFile(state.fileInfo);
- openLinksInNewTab(links, false);
- state.transfer = null;
- state.storage.totalUploads += 1;
- emitter.emit('pushState', `/share/${info.id}`);
- } catch (err) {
- console.error(err);
- state.transfer = null;
- if (err.message === '0') {
- //cancelled. do nothing
- metrics.cancelledUpload({ size, type });
- return render();
- }
- state.raven.captureException(err);
- metrics.stoppedUpload({ size, type, err });
- emitter.emit('pushState', '/error');
- }
- });
-
- emitter.on('password', async ({ password, file }) => {
- try {
- await FileSender.setPassword(password, file);
- metrics.addedPassword({ size: file.size });
- file.password = password;
- state.storage.writeFiles();
- } catch (e) {
- console.error(e);
- }
- render();
- });
-
- emitter.on('preview', async () => {
- const file = state.fileInfo;
- const url = `/api/download/${file.id}`;
- const receiver = new FileReceiver(url, file);
- receiver.on('progress', updateProgress);
- receiver.on('decrypting', render);
- state.transfer = receiver;
- try {
- await receiver.getMetadata(file.nonce);
- } catch (e) {
- if (e.message === '401') {
- file.password = null;
- if (!file.pwd) {
- return emitter.emit('pushState', '/404');
- }
- }
- }
- render();
- });
-
- emitter.on('download', async file => {
- state.transfer.on('progress', render);
- state.transfer.on('decrypting', render);
- const links = openLinksInNewTab();
- const size = file.size;
- try {
- const start = Date.now();
- metrics.startedDownload({ size: file.size, ttl: file.ttl });
- const f = await state.transfer.download(file.nonce);
- const time = Date.now() - start;
- const speed = size / (time / 1000);
- await delay(1000);
- await fadeOut('download-progress');
- saveFile(f);
- state.storage.totalDownloads += 1;
- state.transfer = null;
- metrics.completedDownload({ size, time, speed });
- emitter.emit('pushState', '/completed');
- } catch (err) {
- console.error(err);
- // TODO cancelled download
- const location = err.message === 'notfound' ? '/404' : '/error';
- if (location === '/error') {
- state.raven.captureException(err);
- metrics.stoppedDownload({ size, err });
- }
- emitter.emit('pushState', location);
- } finally {
- state.transfer = null;
- openLinksInNewTab(links, false);
- }
- });
-
- emitter.on('copy', ({ url, location }) => {
- copyToClipboard(url);
- metrics.copiedLink({ location });
- });
-
- setInterval(() => {
- // poll for rerendering the file list countdown timers
- if (
- state.route === '/' &&
- state.storage.files.length > 0 &&
- Date.now() - lastRender > 30000
- ) {
- render();
- }
- }, 60000);
-}
diff --git a/app/fileReceiver.js b/app/fileReceiver.js
index ee0cf5ca..85065429 100644
--- a/app/fileReceiver.js
+++ b/app/fileReceiver.js
@@ -1,110 +1,29 @@
import Nanobus from 'nanobus';
-import { arrayToB64, b64ToArray, bytes } from './utils';
+import Keychain from './keychain';
+import { delay, bytes, streamToArrayBuffer } from './utils';
+import { downloadFile, metadata, getApiUrl, reportLink } from './api';
+import { blobStream } from './streams';
+import Zip from './zip';
export default class FileReceiver extends Nanobus {
- constructor(url, file) {
+ constructor(fileInfo) {
super('FileReceiver');
- this.secretKeyPromise = window.crypto.subtle.importKey(
- 'raw',
- b64ToArray(file.key),
- 'HKDF',
- false,
- ['deriveKey']
- );
- this.encryptKeyPromise = this.secretKeyPromise.then(sk => {
- const encoder = new TextEncoder();
- return window.crypto.subtle.deriveKey(
- {
- name: 'HKDF',
- salt: new Uint8Array(),
- info: encoder.encode('encryption'),
- hash: 'SHA-256'
- },
- sk,
- {
- name: 'AES-GCM',
- length: 128
- },
- false,
- ['decrypt']
- );
- });
- if (file.pwd) {
- const encoder = new TextEncoder();
- this.authKeyPromise = window.crypto.subtle
- .importKey(
- 'raw',
- encoder.encode(file.password),
- { name: 'PBKDF2' },
- false,
- ['deriveKey']
- )
- .then(pwdKey =>
- window.crypto.subtle.deriveKey(
- {
- name: 'PBKDF2',
- salt: encoder.encode(file.url),
- iterations: 100,
- hash: 'SHA-256'
- },
- pwdKey,
- {
- name: 'HMAC',
- hash: 'SHA-256'
- },
- true,
- ['sign']
- )
- );
- } else {
- this.authKeyPromise = this.secretKeyPromise.then(sk => {
- const encoder = new TextEncoder();
- return window.crypto.subtle.deriveKey(
- {
- name: 'HKDF',
- salt: new Uint8Array(),
- info: encoder.encode('authentication'),
- hash: 'SHA-256'
- },
- sk,
- {
- name: 'HMAC',
- hash: { name: 'SHA-256' }
- },
- false,
- ['sign']
- );
- });
+ this.keychain = new Keychain(fileInfo.secretKey, fileInfo.nonce);
+ if (fileInfo.requiresPassword) {
+ this.keychain.setPassword(fileInfo.password, fileInfo.url);
}
- this.metaKeyPromise = this.secretKeyPromise.then(sk => {
- const encoder = new TextEncoder();
- return window.crypto.subtle.deriveKey(
- {
- name: 'HKDF',
- salt: new Uint8Array(),
- info: encoder.encode('metadata'),
- hash: 'SHA-256'
- },
- sk,
- {
- name: 'AES-GCM',
- length: 128
- },
- false,
- ['decrypt']
- );
- });
- this.file = file;
- this.url = url;
- this.msg = 'fileSizeProgress';
- this.state = 'initialized';
- this.progress = [0, 1];
+ this.fileInfo = fileInfo;
+ this.reset();
}
get progressRatio() {
return this.progress[0] / this.progress[1];
}
+ get progressIndefinite() {
+ return this.state !== 'downloading';
+ }
+
get sizes() {
return {
partialSize: bytes(this.progress[0]),
@@ -113,139 +32,207 @@ export default class FileReceiver extends Nanobus {
}
cancel() {
- // TODO
- }
-
- fetchMetadata(sig) {
- return new Promise((resolve, reject) => {
- const xhr = new XMLHttpRequest();
- xhr.onreadystatechange = () => {
- if (xhr.readyState === XMLHttpRequest.DONE) {
- const nonce = xhr.getResponseHeader('WWW-Authenticate').split(' ')[1];
- this.file.nonce = nonce;
- if (xhr.status === 200) {
- return resolve(xhr.response);
- }
- reject(new Error(xhr.status));
- }
- };
- xhr.onerror = () => reject(new Error(0));
- xhr.ontimeout = () => reject(new Error(0));
- xhr.open('get', `/api/metadata/${this.file.id}`);
- xhr.setRequestHeader('Authorization', `send-v1 ${arrayToB64(sig)}`);
- xhr.responseType = 'json';
- xhr.timeout = 2000;
- xhr.send();
- });
- }
-
- async getMetadata(nonce) {
- try {
- const authKey = await this.authKeyPromise;
- const sig = await window.crypto.subtle.sign(
- {
- name: 'HMAC'
- },
- authKey,
- b64ToArray(nonce)
- );
- const data = await this.fetchMetadata(new Uint8Array(sig));
- const metaKey = await this.metaKeyPromise;
- const json = await window.crypto.subtle.decrypt(
- {
- name: 'AES-GCM',
- iv: new Uint8Array(12),
- tagLength: 128
- },
- metaKey,
- b64ToArray(data.metadata)
- );
- const decoder = new TextDecoder();
- const meta = JSON.parse(decoder.decode(json));
- this.file.name = meta.name;
- this.file.type = meta.type;
- this.file.iv = meta.iv;
- this.file.size = data.size;
- this.file.ttl = data.ttl;
- this.state = 'ready';
- } catch (e) {
- this.state = 'invalid';
- throw e;
+ if (this.downloadRequest) {
+ this.downloadRequest.cancel();
}
}
- downloadFile(sig) {
+ reset() {
+ this.msg = 'fileSizeProgress';
+ this.state = 'initialized';
+ this.progress = [0, 1];
+ }
+
+ async getMetadata() {
+ const meta = await metadata(this.fileInfo.id, this.keychain);
+ this.fileInfo.name = meta.name;
+ this.fileInfo.type = meta.type;
+ this.fileInfo.iv = meta.iv;
+ this.fileInfo.size = +meta.size;
+ this.fileInfo.manifest = meta.manifest;
+ this.state = 'ready';
+ }
+
+ async reportLink(reason) {
+ await reportLink(this.fileInfo.id, this.keychain, reason);
+ }
+
+ sendMessageToSw(msg) {
return new Promise((resolve, reject) => {
- const xhr = new XMLHttpRequest();
+ const channel = new MessageChannel();
- xhr.onprogress = event => {
- if (event.lengthComputable && event.target.status !== 404) {
- this.progress = [event.loaded, event.total];
- this.emit('progress', this.progress);
+ channel.port1.onmessage = function(event) {
+ if (event.data === undefined) {
+ reject('bad response from serviceWorker');
+ } else if (event.data.error !== undefined) {
+ reject(event.data.error);
+ } else {
+ resolve(event.data);
}
};
- xhr.onload = event => {
- if (xhr.status === 404) {
- reject(new Error('notfound'));
- return;
- }
-
- if (xhr.status !== 200) {
- return reject(new Error(xhr.status));
- }
-
- const blob = new Blob([xhr.response]);
- const fileReader = new FileReader();
- fileReader.onload = function() {
- resolve(this.result);
- };
-
- fileReader.readAsArrayBuffer(blob);
- };
-
- xhr.open('get', this.url);
- xhr.setRequestHeader('Authorization', `send-v1 ${arrayToB64(sig)}`);
- xhr.responseType = 'blob';
- xhr.send();
+ navigator.serviceWorker.controller.postMessage(msg, [channel.port2]);
});
}
- async download(nonce) {
+ async downloadBlob(noSave = false) {
this.state = 'downloading';
- this.emit('progress', this.progress);
+ this.downloadRequest = await downloadFile(
+ this.fileInfo.id,
+ this.keychain,
+ p => {
+ this.progress = [p, this.fileInfo.size];
+ this.emit('progress');
+ }
+ );
try {
- const encryptKey = await this.encryptKeyPromise;
- const authKey = await this.authKeyPromise;
- const sig = await window.crypto.subtle.sign(
- {
- name: 'HMAC'
- },
- authKey,
- b64ToArray(nonce)
- );
- const ciphertext = await this.downloadFile(new Uint8Array(sig));
+ const ciphertext = await this.downloadRequest.result;
+ this.downloadRequest = null;
this.msg = 'decryptingFile';
+ this.state = 'decrypting';
this.emit('decrypting');
- const plaintext = await window.crypto.subtle.decrypt(
- {
- name: 'AES-GCM',
- iv: b64ToArray(this.file.iv),
- tagLength: 128
- },
- encryptKey,
- ciphertext
- );
+ let size = this.fileInfo.size;
+ let plainStream = this.keychain.decryptStream(blobStream(ciphertext));
+ if (this.fileInfo.type === 'send-archive') {
+ const zip = new Zip(this.fileInfo.manifest, plainStream);
+ plainStream = zip.stream;
+ size = zip.size;
+ }
+ const plaintext = await streamToArrayBuffer(plainStream, size);
+ if (!noSave) {
+ await saveFile({
+ plaintext,
+ name: decodeURIComponent(this.fileInfo.name),
+ type: this.fileInfo.type
+ });
+ }
this.msg = 'downloadFinish';
+ this.emit('complete');
this.state = 'complete';
- return {
- plaintext,
- name: decodeURIComponent(this.file.name),
- type: this.file.type
- };
} catch (e) {
- this.state = 'invalid';
+ this.downloadRequest = null;
throw e;
}
}
+
+ async downloadStream(noSave = false) {
+ const start = Date.now();
+ const onprogress = p => {
+ this.progress = [p, this.fileInfo.size];
+ this.emit('progress');
+ };
+
+ this.downloadRequest = {
+ cancel: () => {
+ this.sendMessageToSw({ request: 'cancel', id: this.fileInfo.id });
+ }
+ };
+
+ try {
+ this.state = 'downloading';
+
+ const info = {
+ request: 'init',
+ id: this.fileInfo.id,
+ filename: this.fileInfo.name,
+ type: this.fileInfo.type,
+ manifest: this.fileInfo.manifest,
+ key: this.fileInfo.secretKey,
+ requiresPassword: this.fileInfo.requiresPassword,
+ password: this.fileInfo.password,
+ url: this.fileInfo.url,
+ size: this.fileInfo.size,
+ nonce: this.keychain.nonce,
+ noSave
+ };
+ await this.sendMessageToSw(info);
+
+ onprogress(0);
+
+ if (noSave) {
+ const res = await fetch(getApiUrl(`/api/download/${this.fileInfo.id}`));
+ if (res.status !== 200) {
+ throw new Error(res.status);
+ }
+ } else {
+ const downloadPath = `/api/download/${this.fileInfo.id}`;
+ let downloadUrl = getApiUrl(downloadPath);
+ if (downloadUrl === downloadPath) {
+ downloadUrl = `${location.protocol}//${location.host}${downloadPath}`;
+ }
+ const a = document.createElement('a');
+ a.href = downloadUrl;
+ document.body.appendChild(a);
+ a.click();
+ }
+
+ let prog = 0;
+ let hangs = 0;
+ while (prog < this.fileInfo.size) {
+ const msg = await this.sendMessageToSw({
+ request: 'progress',
+ id: this.fileInfo.id
+ });
+ if (msg.progress === prog) {
+ hangs++;
+ } else {
+ hangs = 0;
+ }
+ if (hangs > 30) {
+ // TODO: On Chrome we don't get a cancel
+ // signal so one is indistinguishable from
+ // a hang. We may be able to detect
+ // which end is hung in the service worker
+ // to improve on this.
+ const e = new Error('hung download');
+ e.duration = Date.now() - start;
+ e.size = this.fileInfo.size;
+ e.progress = prog;
+ throw e;
+ }
+ prog = msg.progress;
+ onprogress(prog);
+ await delay(1000);
+ }
+
+ this.downloadRequest = null;
+ this.msg = 'downloadFinish';
+ this.emit('complete');
+ this.state = 'complete';
+ } catch (e) {
+ this.downloadRequest = null;
+ if (e === 'cancelled' || e.message === '400') {
+ throw new Error(0);
+ }
+ throw e;
+ }
+ }
+
+ download(options) {
+ if (options.stream) {
+ return this.downloadStream(options.noSave);
+ }
+ return this.downloadBlob(options.noSave);
+ }
+}
+
+async function saveFile(file) {
+ return new Promise(function(resolve, reject) {
+ const dataView = new DataView(file.plaintext);
+ const blob = new Blob([dataView], { type: file.type });
+
+ if (navigator.msSaveBlob) {
+ navigator.msSaveBlob(blob, file.name);
+ return resolve();
+ } else {
+ const downloadUrl = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = downloadUrl;
+ a.download = file.name;
+ document.body.appendChild(a);
+ a.click();
+ URL.revokeObjectURL(downloadUrl);
+ setTimeout(resolve, 100);
+ }
+ });
}
diff --git a/app/fileSender.js b/app/fileSender.js
index d5a00eec..e8bc6bea 100644
--- a/app/fileSender.js
+++ b/app/fileSender.js
@@ -1,48 +1,27 @@
import Nanobus from 'nanobus';
-import { arrayToB64, b64ToArray, bytes } from './utils';
+import OwnedFile from './ownedFile';
+import Keychain from './keychain';
+import { arrayToB64, bytes } from './utils';
+import { uploadWs } from './api';
+import { encryptedSize } from './utils';
export default class FileSender extends Nanobus {
- constructor(file) {
+ constructor() {
super('FileSender');
- this.file = file;
- this.msg = 'importingFile';
- this.progress = [0, 1];
- this.cancelled = false;
- this.iv = window.crypto.getRandomValues(new Uint8Array(12));
- this.uploadXHR = new XMLHttpRequest();
- this.rawSecret = window.crypto.getRandomValues(new Uint8Array(16));
- this.secretKey = window.crypto.subtle.importKey(
- 'raw',
- this.rawSecret,
- 'HKDF',
- false,
- ['deriveKey']
- );
- }
-
- static delete(id, token) {
- return new Promise((resolve, reject) => {
- if (!id || !token) {
- return reject();
- }
- const xhr = new XMLHttpRequest();
- xhr.open('POST', `/api/delete/${id}`);
- xhr.setRequestHeader('Content-Type', 'application/json');
-
- xhr.onreadystatechange = () => {
- if (xhr.readyState === XMLHttpRequest.DONE) {
- resolve();
- }
- };
-
- xhr.send(JSON.stringify({ delete_token: token }));
- });
+ this.keychain = new Keychain();
+ this.reset();
}
get progressRatio() {
return this.progress[0] / this.progress[1];
}
+ get progressIndefinite() {
+ return (
+ ['fileSizeProgress', 'notifyUploadEncryptDone'].indexOf(this.msg) === -1
+ );
+ }
+
get sizes() {
return {
partialSize: bytes(this.progress[0]),
@@ -50,242 +29,78 @@ export default class FileSender extends Nanobus {
};
}
+ reset() {
+ this.uploadRequest = null;
+ this.msg = 'importingFile';
+ this.progress = [0, 1];
+ this.cancelled = false;
+ }
+
cancel() {
this.cancelled = true;
- if (this.msg === 'fileSizeProgress') {
- this.uploadXHR.abort();
+ if (this.uploadRequest) {
+ this.uploadRequest.cancel();
}
}
- readFile() {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.readAsArrayBuffer(this.file);
- reader.onload = function(event) {
- const plaintext = new Uint8Array(this.result);
- resolve(plaintext);
- };
- reader.onerror = function(err) {
- reject(err);
- };
- });
- }
-
- uploadFile(encrypted, metadata, rawAuth) {
- return new Promise((resolve, reject) => {
- const dataView = new DataView(encrypted);
- const blob = new Blob([dataView], { type: 'application/octet-stream' });
- const fd = new FormData();
- fd.append('data', blob);
-
- const xhr = this.uploadXHR;
-
- xhr.upload.addEventListener('progress', e => {
- if (e.lengthComputable) {
- this.progress = [e.loaded, e.total];
- this.emit('progress', this.progress);
- }
- });
-
- xhr.onreadystatechange = () => {
- if (xhr.readyState === XMLHttpRequest.DONE) {
- if (xhr.status === 200) {
- const nonce = xhr
- .getResponseHeader('WWW-Authenticate')
- .split(' ')[1];
- this.progress = [1, 1];
- this.msg = 'notifyUploadDone';
- const responseObj = JSON.parse(xhr.responseText);
- return resolve({
- url: responseObj.url,
- id: responseObj.id,
- secretKey: arrayToB64(this.rawSecret),
- deleteToken: responseObj.delete,
- nonce
- });
- }
- this.msg = 'errorPageHeader';
- reject(new Error(xhr.status));
- }
- };
-
- xhr.open('post', '/api/upload', true);
- xhr.setRequestHeader(
- 'X-File-Metadata',
- arrayToB64(new Uint8Array(metadata))
- );
- xhr.setRequestHeader('Authorization', `send-v1 ${arrayToB64(rawAuth)}`);
- xhr.send(fd);
- this.msg = 'fileSizeProgress';
- });
- }
-
- async upload() {
- const encoder = new TextEncoder();
- const secretKey = await this.secretKey;
- const encryptKey = await window.crypto.subtle.deriveKey(
- {
- name: 'HKDF',
- salt: new Uint8Array(),
- info: encoder.encode('encryption'),
- hash: 'SHA-256'
- },
- secretKey,
- {
- name: 'AES-GCM',
- length: 128
- },
- false,
- ['encrypt']
- );
- const authKey = await window.crypto.subtle.deriveKey(
- {
- name: 'HKDF',
- salt: new Uint8Array(),
- info: encoder.encode('authentication'),
- hash: 'SHA-256'
- },
- secretKey,
- {
- name: 'HMAC',
- hash: 'SHA-256'
- },
- true,
- ['sign']
- );
- const metaKey = await window.crypto.subtle.deriveKey(
- {
- name: 'HKDF',
- salt: new Uint8Array(),
- info: encoder.encode('metadata'),
- hash: 'SHA-256'
- },
- secretKey,
- {
- name: 'AES-GCM',
- length: 128
- },
- false,
- ['encrypt']
- );
- const plaintext = await this.readFile();
+ async upload(archive, bearerToken) {
if (this.cancelled) {
throw new Error(0);
}
this.msg = 'encryptingFile';
this.emit('encrypting');
- const encrypted = await window.crypto.subtle.encrypt(
- {
- name: 'AES-GCM',
- iv: this.iv,
- tagLength: 128
- },
- encryptKey,
- plaintext
+ const totalSize = encryptedSize(archive.size);
+ const encStream = await this.keychain.encryptStream(archive.stream);
+ const metadata = await this.keychain.encryptMetadata(archive);
+ const authKeyB64 = await this.keychain.authKeyB64();
+
+ this.uploadRequest = uploadWs(
+ encStream,
+ metadata,
+ authKeyB64,
+ archive.timeLimit,
+ archive.dlimit,
+ bearerToken,
+ p => {
+ this.progress = [p, totalSize];
+ this.emit('progress');
+ }
);
- const metadata = await window.crypto.subtle.encrypt(
- {
- name: 'AES-GCM',
- iv: new Uint8Array(12),
- tagLength: 128
- },
- metaKey,
- encoder.encode(
- JSON.stringify({
- iv: arrayToB64(this.iv),
- name: this.file.name,
- type: this.file.type || 'application/octet-stream'
- })
- )
- );
- const rawAuth = await window.crypto.subtle.exportKey('raw', authKey);
+
if (this.cancelled) {
throw new Error(0);
}
- return this.uploadFile(encrypted, metadata, new Uint8Array(rawAuth));
- }
- static async setPassword(password, file) {
- const encoder = new TextEncoder();
- const secretKey = await window.crypto.subtle.importKey(
- 'raw',
- b64ToArray(file.secretKey),
- 'HKDF',
- false,
- ['deriveKey']
- );
- const authKey = await window.crypto.subtle.deriveKey(
- {
- name: 'HKDF',
- salt: new Uint8Array(),
- info: encoder.encode('authentication'),
- hash: 'SHA-256'
- },
- secretKey,
- {
- name: 'HMAC',
- hash: 'SHA-256'
- },
- true,
- ['sign']
- );
- const sig = await window.crypto.subtle.sign(
- {
- name: 'HMAC'
- },
- authKey,
- b64ToArray(file.nonce)
- );
- const pwdKey = await window.crypto.subtle.importKey(
- 'raw',
- encoder.encode(password),
- { name: 'PBKDF2' },
- false,
- ['deriveKey']
- );
- const newAuthKey = await window.crypto.subtle.deriveKey(
- {
- name: 'PBKDF2',
- salt: encoder.encode(file.url),
- iterations: 100,
- hash: 'SHA-256'
- },
- pwdKey,
- {
- name: 'HMAC',
- hash: 'SHA-256'
- },
- true,
- ['sign']
- );
- const rawAuth = await window.crypto.subtle.exportKey('raw', newAuthKey);
- return new Promise((resolve, reject) => {
- const xhr = new XMLHttpRequest();
- xhr.onreadystatechange = () => {
- if (xhr.readyState === XMLHttpRequest.DONE) {
- if (xhr.status === 200) {
- return resolve(xhr.response);
- }
- if (xhr.status === 401) {
- const nonce = xhr
- .getResponseHeader('WWW-Authenticate')
- .split(' ')[1];
- file.nonce = nonce;
- }
- reject(new Error(xhr.status));
- }
- };
- xhr.onerror = () => reject(new Error(0));
- xhr.ontimeout = () => reject(new Error(0));
- xhr.open('post', `/api/password/${file.id}`);
- xhr.setRequestHeader(
- 'Authorization',
- `send-v1 ${arrayToB64(new Uint8Array(sig))}`
- );
- xhr.setRequestHeader('Content-Type', 'application/json');
- xhr.responseType = 'json';
- xhr.timeout = 2000;
- xhr.send(JSON.stringify({ auth: arrayToB64(new Uint8Array(rawAuth)) }));
- });
+ this.msg = 'fileSizeProgress';
+ this.emit('progress'); // HACK to kick MS Edge
+ try {
+ const result = await this.uploadRequest.result;
+ this.msg = 'notifyUploadEncryptDone';
+ this.uploadRequest = null;
+ this.progress = [1, 1];
+ const secretKey = arrayToB64(this.keychain.rawSecret);
+ const ownedFile = new OwnedFile({
+ id: result.id,
+ url: `${result.url}#${secretKey}`,
+ name: archive.name,
+ size: archive.size,
+ manifest: archive.manifest,
+ time: result.duration,
+ speed: archive.size / (result.duration / 1000),
+ createdAt: Date.now(),
+ expiresAt: Date.now() + archive.timeLimit * 1000,
+ secretKey: secretKey,
+ nonce: this.keychain.nonce,
+ ownerToken: result.ownerToken,
+ dlimit: archive.dlimit,
+ timeLimit: archive.timeLimit
+ });
+
+ return ownedFile;
+ } catch (e) {
+ this.msg = 'errorPageHeader';
+ this.uploadRequest = null;
+ throw e;
+ }
}
}
diff --git a/app/fxa.js b/app/fxa.js
new file mode 100644
index 00000000..7827d33e
--- /dev/null
+++ b/app/fxa.js
@@ -0,0 +1,181 @@
+/* global AUTH_CONFIG */
+import { arrayToB64, b64ToArray } from './utils';
+
+const encoder = new TextEncoder();
+const decoder = new TextDecoder();
+
+function getOtherInfo(enc) {
+ const name = encoder.encode(enc);
+ const length = 256;
+ const buffer = new ArrayBuffer(name.length + 16);
+ const dv = new DataView(buffer);
+ const result = new Uint8Array(buffer);
+ let i = 0;
+ dv.setUint32(i, name.length);
+ i += 4;
+ result.set(name, i);
+ i += name.length;
+ dv.setUint32(i, 0);
+ i += 4;
+ dv.setUint32(i, 0);
+ i += 4;
+ dv.setUint32(i, length);
+ return result;
+}
+
+function concat(b1, b2) {
+ const result = new Uint8Array(b1.length + b2.length);
+ result.set(b1, 0);
+ result.set(b2, b1.length);
+ return result;
+}
+
+async function concatKdf(key, enc) {
+ if (key.length !== 32) {
+ throw new Error('unsupported key length');
+ }
+ const otherInfo = getOtherInfo(enc);
+ const buffer = new ArrayBuffer(4 + key.length + otherInfo.length);
+ const dv = new DataView(buffer);
+ const concat = new Uint8Array(buffer);
+ dv.setUint32(0, 1);
+ concat.set(key, 4);
+ concat.set(otherInfo, key.length + 4);
+ const result = await crypto.subtle.digest('SHA-256', concat);
+ return new Uint8Array(result);
+}
+
+export async function prepareScopedBundleKey(storage) {
+ const keys = await crypto.subtle.generateKey(
+ {
+ name: 'ECDH',
+ namedCurve: 'P-256'
+ },
+ true,
+ ['deriveBits']
+ );
+ const privateJwk = await crypto.subtle.exportKey('jwk', keys.privateKey);
+ const publicJwk = await crypto.subtle.exportKey('jwk', keys.publicKey);
+ const kid = await crypto.subtle.digest(
+ 'SHA-256',
+ encoder.encode(JSON.stringify(publicJwk))
+ );
+ privateJwk.kid = kid;
+ publicJwk.kid = kid;
+ storage.set('scopedBundlePrivateKey', JSON.stringify(privateJwk));
+ return arrayToB64(encoder.encode(JSON.stringify(publicJwk)));
+}
+
+export async function decryptBundle(storage, bundle) {
+ const privateJwk = JSON.parse(storage.get('scopedBundlePrivateKey'));
+ storage.remove('scopedBundlePrivateKey');
+ const privateKey = await crypto.subtle.importKey(
+ 'jwk',
+ privateJwk,
+ {
+ name: 'ECDH',
+ namedCurve: 'P-256'
+ },
+ false,
+ ['deriveBits']
+ );
+ const jweParts = bundle.split('.');
+ if (jweParts.length !== 5) {
+ throw new Error('invalid jwe');
+ }
+ const header = JSON.parse(decoder.decode(b64ToArray(jweParts[0])));
+ const additionalData = encoder.encode(jweParts[0]);
+ const iv = b64ToArray(jweParts[2]);
+ const ciphertext = b64ToArray(jweParts[3]);
+ const tag = b64ToArray(jweParts[4]);
+
+ if (header.alg !== 'ECDH-ES' || header.enc !== 'A256GCM') {
+ throw new Error('unsupported jwe type');
+ }
+
+ const publicKey = await crypto.subtle.importKey(
+ 'jwk',
+ header.epk,
+ {
+ name: 'ECDH',
+ namedCurve: 'P-256'
+ },
+ false,
+ []
+ );
+ const sharedBits = await crypto.subtle.deriveBits(
+ {
+ name: 'ECDH',
+ public: publicKey
+ },
+ privateKey,
+ 256
+ );
+
+ const rawSharedKey = await concatKdf(new Uint8Array(sharedBits), header.enc);
+ const sharedKey = await crypto.subtle.importKey(
+ 'raw',
+ rawSharedKey,
+ {
+ name: 'AES-GCM'
+ },
+ false,
+ ['decrypt']
+ );
+
+ const plaintext = await crypto.subtle.decrypt(
+ {
+ name: 'AES-GCM',
+ iv: iv,
+ additionalData: additionalData,
+ tagLength: tag.length * 8
+ },
+ sharedKey,
+ concat(ciphertext, tag)
+ );
+
+ return JSON.parse(decoder.decode(plaintext));
+}
+
+export async function preparePkce(storage) {
+ const verifier = arrayToB64(crypto.getRandomValues(new Uint8Array(64)));
+ storage.set('pkceVerifier', verifier);
+ const challenge = await crypto.subtle.digest(
+ 'SHA-256',
+ encoder.encode(verifier)
+ );
+ return arrayToB64(new Uint8Array(challenge));
+}
+
+export async function deriveFileListKey(ikm) {
+ const baseKey = await crypto.subtle.importKey(
+ 'raw',
+ b64ToArray(ikm),
+ { name: 'HKDF' },
+ false,
+ ['deriveKey']
+ );
+ const fileListKey = await crypto.subtle.deriveKey(
+ {
+ name: 'HKDF',
+ salt: new Uint8Array(),
+ info: encoder.encode('fileList'),
+ hash: 'SHA-256'
+ },
+ baseKey,
+ {
+ name: 'AES-GCM',
+ length: 128
+ },
+ true,
+ ['encrypt', 'decrypt']
+ );
+ const rawFileListKey = await crypto.subtle.exportKey('raw', fileListKey);
+ return arrayToB64(new Uint8Array(rawFileListKey));
+}
+
+export async function getFileListKey(storage, bundle) {
+ const jwks = await decryptBundle(storage, bundle);
+ const jwk = jwks[AUTH_CONFIG.key_scope];
+ return deriveFileListKey(jwk.k);
+}
diff --git a/app/keychain.js b/app/keychain.js
new file mode 100644
index 00000000..37951aa7
--- /dev/null
+++ b/app/keychain.js
@@ -0,0 +1,164 @@
+import { arrayToB64, b64ToArray } from './utils';
+import { decryptStream, encryptStream } from './ece.js';
+const encoder = new TextEncoder();
+const decoder = new TextDecoder();
+
+export default class Keychain {
+ constructor(secretKeyB64, nonce) {
+ this._nonce = nonce || 'yRCdyQ1EMSA3mo4rqSkuNQ==';
+ if (secretKeyB64) {
+ this.rawSecret = b64ToArray(secretKeyB64);
+ } else {
+ this.rawSecret = crypto.getRandomValues(new Uint8Array(16));
+ }
+ this.secretKeyPromise = crypto.subtle.importKey(
+ 'raw',
+ this.rawSecret,
+ 'HKDF',
+ false,
+ ['deriveKey']
+ );
+ this.metaKeyPromise = this.secretKeyPromise.then(function(secretKey) {
+ return crypto.subtle.deriveKey(
+ {
+ name: 'HKDF',
+ salt: new Uint8Array(),
+ info: encoder.encode('metadata'),
+ hash: 'SHA-256'
+ },
+ secretKey,
+ {
+ name: 'AES-GCM',
+ length: 128
+ },
+ false,
+ ['encrypt', 'decrypt']
+ );
+ });
+ this.authKeyPromise = this.secretKeyPromise.then(function(secretKey) {
+ return crypto.subtle.deriveKey(
+ {
+ name: 'HKDF',
+ salt: new Uint8Array(),
+ info: encoder.encode('authentication'),
+ hash: 'SHA-256'
+ },
+ secretKey,
+ {
+ name: 'HMAC',
+ hash: { name: 'SHA-256' }
+ },
+ true,
+ ['sign']
+ );
+ });
+ }
+
+ get nonce() {
+ return this._nonce;
+ }
+
+ set nonce(n) {
+ if (n && n !== this._nonce) {
+ this._nonce = n;
+ }
+ }
+
+ setPassword(password, shareUrl) {
+ this.authKeyPromise = crypto.subtle
+ .importKey('raw', encoder.encode(password), { name: 'PBKDF2' }, false, [
+ 'deriveKey'
+ ])
+ .then(passwordKey =>
+ crypto.subtle.deriveKey(
+ {
+ name: 'PBKDF2',
+ salt: encoder.encode(shareUrl),
+ iterations: 100,
+ hash: 'SHA-256'
+ },
+ passwordKey,
+ {
+ name: 'HMAC',
+ hash: 'SHA-256'
+ },
+ true,
+ ['sign']
+ )
+ );
+ }
+
+ setAuthKey(authKeyB64) {
+ this.authKeyPromise = crypto.subtle.importKey(
+ 'raw',
+ b64ToArray(authKeyB64),
+ {
+ name: 'HMAC',
+ hash: 'SHA-256'
+ },
+ true,
+ ['sign']
+ );
+ }
+
+ async authKeyB64() {
+ const authKey = await this.authKeyPromise;
+ const rawAuth = await crypto.subtle.exportKey('raw', authKey);
+ return arrayToB64(new Uint8Array(rawAuth));
+ }
+
+ async authHeader() {
+ const authKey = await this.authKeyPromise;
+ const sig = await crypto.subtle.sign(
+ {
+ name: 'HMAC'
+ },
+ authKey,
+ b64ToArray(this.nonce)
+ );
+ return `send-v1 ${arrayToB64(new Uint8Array(sig))}`;
+ }
+
+ async encryptMetadata(metadata) {
+ const metaKey = await this.metaKeyPromise;
+ const ciphertext = await crypto.subtle.encrypt(
+ {
+ name: 'AES-GCM',
+ iv: new Uint8Array(12),
+ tagLength: 128
+ },
+ metaKey,
+ encoder.encode(
+ JSON.stringify({
+ name: metadata.name,
+ size: metadata.size,
+ type: metadata.type || 'application/octet-stream',
+ manifest: metadata.manifest || {}
+ })
+ )
+ );
+ return ciphertext;
+ }
+
+ encryptStream(plainStream) {
+ return encryptStream(plainStream, this.rawSecret);
+ }
+
+ decryptStream(cryptotext) {
+ return decryptStream(cryptotext, this.rawSecret);
+ }
+
+ async decryptMetadata(ciphertext) {
+ const metaKey = await this.metaKeyPromise;
+ const plaintext = await crypto.subtle.decrypt(
+ {
+ name: 'AES-GCM',
+ iv: new Uint8Array(12),
+ tagLength: 128
+ },
+ metaKey,
+ ciphertext
+ );
+ return JSON.parse(decoder.decode(plaintext));
+ }
+}
diff --git a/app/locale.js b/app/locale.js
new file mode 100644
index 00000000..23dfdb7c
--- /dev/null
+++ b/app/locale.js
@@ -0,0 +1,26 @@
+import { FluentBundle, FluentResource } from '@fluent/bundle';
+
+function makeBundle(locale, ftl) {
+ const bundle = new FluentBundle(locale, { useIsolating: false });
+ bundle.addResource(new FluentResource(ftl));
+ return bundle;
+}
+
+export async function getTranslator(locale) {
+ const bundles = [];
+ const { default: en } = await import('../public/locales/en-US/send.ftl');
+ if (locale !== 'en-US') {
+ const { default: ftl } = await import(
+ `../public/locales/${locale}/send.ftl`
+ );
+ bundles.push(makeBundle(locale, ftl));
+ }
+ bundles.push(makeBundle('en-US', en));
+ return function(id, data) {
+ for (let bundle of bundles) {
+ if (bundle.hasMessage(id)) {
+ return bundle.formatPattern(bundle.getMessage(id).value, data);
+ }
+ }
+ };
+}
diff --git a/app/main.css b/app/main.css
new file mode 100644
index 00000000..6a42290e
--- /dev/null
+++ b/app/main.css
@@ -0,0 +1,412 @@
+@tailwind base;
+
+html {
+ line-height: 1.15;
+}
+
+@tailwind components;
+
+:not(input) {
+ user-select: none;
+}
+
+:root {
+ --violet-gradient: linear-gradient(
+ -180deg,
+ rgb(144 89 255 / 80%) 0%,
+ rgb(144 89 255 / 40%) 100%
+ );
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+a:focus {
+ outline: 1px dotted grey;
+}
+
+body {
+ background-image: url('../assets/bg.svg');
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: cover;
+ overflow-x: hidden;
+}
+
+.btn {
+ @apply bg-primary;
+ @apply text-white;
+ @apply cursor-pointer;
+ @apply py-4;
+ @apply px-6;
+ @apply font-semibold;
+}
+
+.btn:hover {
+ @apply bg-primary_accent;
+}
+
+.btn:focus {
+ @apply bg-primary_accent;
+}
+
+.checkbox {
+ @apply leading-normal;
+ @apply select-none;
+}
+
+.checkbox > input[type='checkbox'] {
+ @apply absolute;
+ @apply opacity-0;
+}
+
+.checkbox > label {
+ @apply cursor-pointer;
+}
+
+.checkbox > label::before {
+ /* @apply bg-grey-10; */
+ @apply border-default;
+ @apply rounded-sm;
+
+ content: '';
+ height: 1.5rem;
+ width: 1.5rem;
+ margin-right: 0.5rem;
+ float: left;
+}
+
+.checkbox > label:hover::before {
+ @apply border-primary;
+}
+
+.checkbox > input:focus + label::before {
+ @apply border-primary;
+}
+
+.checkbox > input:checked + label::before {
+ @apply bg-primary;
+ @apply border-primary;
+
+ background-image: url('../assets/lock.svg');
+ background-position: center;
+ background-size: 1.25rem;
+ background-repeat: no-repeat;
+}
+
+.checkbox > input:disabled + label {
+ cursor: auto;
+}
+
+.checkbox > input:disabled + label::before {
+ @apply bg-primary;
+ @apply border-primary;
+
+ background-image: url('../assets/lock.svg');
+ background-position: center;
+ background-size: 1.25rem;
+ background-repeat: no-repeat;
+ cursor: auto;
+}
+
+details {
+ overflow: hidden;
+}
+
+details > summary::marker {
+ display: none;
+}
+
+details > summary > svg {
+ transition: all 0.25s cubic-bezier(0.07, 0.95, 0, 1);
+}
+
+details[open] {
+ overflow-y: auto;
+}
+
+details[open] > summary > svg {
+ transform: rotate(90deg);
+}
+
+footer li a:hover {
+ text-decoration: underline;
+}
+
+.feedback-link {
+ background-color: #000;
+ background-image: url('../assets/feedback.svg');
+ background-position: 0.125rem 0.25rem;
+ background-repeat: no-repeat;
+ background-size: 1.125rem;
+ color: #fff;
+ display: block;
+ font-size: 0.75rem;
+ line-height: 0.75rem;
+ padding: 0.375rem 0.375rem 0.375rem 1.25rem;
+ text-indent: 0.125rem;
+ white-space: nowrap;
+}
+
+.link-primary {
+ @apply text-primary;
+}
+
+.link-primary:hover {
+ @apply text-primary_accent;
+}
+
+.link-primary:focus {
+ @apply text-primary_accent;
+}
+
+.main-header img {
+ height: 32px;
+ width: auto;
+}
+
+.text-underline {
+ text-decoration: underline;
+}
+
+.d-block {
+ display: block;
+}
+
+.d-inline-block {
+ display: inline-block;
+}
+
+.align-middle {
+ vertical-align: middle;
+}
+
+.main {
+ display: flex;
+ position: relative;
+ max-width: 64rem;
+ width: 100%;
+}
+
+.main > section {
+ @apply bg-white;
+}
+
+#password-msg::after {
+ content: '\200b';
+}
+
+progress {
+ @apply bg-grey-30;
+ @apply rounded-sm;
+ @apply w-full;
+ @apply h-1;
+}
+
+progress::-webkit-progress-bar {
+ @apply bg-grey-30;
+ @apply rounded-sm;
+ @apply w-full;
+ @apply h-1;
+}
+
+progress::-webkit-progress-value {
+ /* stylelint-disable */
+ background-image: -webkit-linear-gradient(
+ -45deg,
+ transparent 20%,
+ rgb(255 255 255 / 40%) 20%,
+ rgb(255 255 255 / 40%) 40%,
+ transparent 40%,
+ transparent 60%,
+ rgb(255 255 255 / 40%) 60%,
+ rgb(255 255 255 / 40%) 80%,
+ transparent 80%
+ ),
+ -webkit-linear-gradient(left, var(--color-primary), var(--color-primary));
+ /* stylelint-enable */
+ border-radius: 2px;
+ background-size: 21px 20px, 100% 100%, 100% 100%;
+}
+
+progress::-moz-progress-bar {
+ /* stylelint-disable */
+ background-image: -moz-linear-gradient(
+ 135deg,
+ transparent 20%,
+ rgb(255 255 255 / 40%) 20%,
+ rgb(255 255 255 / 40%) 40%,
+ transparent 40%,
+ transparent 60%,
+ rgb(255 255 255 / 40%) 60%,
+ rgb(255 255 255 / 40%) 80%,
+ transparent 80%
+ ),
+ -moz-linear-gradient(left, var(--color-primary), var(--color-primary));
+ /* stylelint-enable */
+ border-radius: 2px;
+ background-size: 21px 20px, 100% 100%, 100% 100%;
+ animation: animate-stripes 1s linear infinite;
+}
+
+@keyframes animate-stripes {
+ 100% {
+ background-position: -21px 0;
+ }
+}
+
+select {
+ background-image: url('../assets/select-arrow.svg');
+ background-position: calc(100% - 0.75rem);
+ background-repeat: no-repeat;
+}
+
+@screen md {
+ .main-header img {
+ height: 48px;
+ width: auto;
+ }
+
+ .main {
+ @apply flex-1;
+ @apply self-center;
+ @apply items-center;
+ @apply m-auto;
+ @apply py-8;
+
+ max-height: 42rem;
+ width: calc(100% - 3rem);
+ }
+}
+
+@screen dark {
+ body {
+ @apply text-grey-10;
+
+ background-image: unset;
+ }
+
+ .btn {
+ @apply bg-primary;
+ @apply text-white;
+ }
+
+ .btn:hover {
+ @apply bg-primary_accent;
+ }
+
+ .btn:focus {
+ @apply bg-primary_accent;
+ }
+
+ .link-primary {
+ @apply text-primary;
+ }
+
+ .link-primary:hover {
+ @apply text-primary_accent;
+ }
+
+ .link-primary:focus {
+ @apply text-primary_accent;
+ }
+
+ .main > section {
+ @apply bg-grey-90;
+ }
+
+ @screen md {
+ .main > section {
+ @apply border-default;
+ @apply border-grey-80;
+ }
+ }
+}
+
+@tailwind utilities;
+
+@responsive {
+ .shadow-light {
+ box-shadow: 0 0 8px 0 rgb(12 12 13 / 10%);
+ }
+
+ .shadow-big {
+ box-shadow: 0 12px 18px 2px rgb(34 0 51 / 4%),
+ 0 6px 22px 4px rgb(7 48 114 / 12%), 0 6px 10px -4px rgb(14 13 26 / 12%);
+ }
+}
+
+@variants focus {
+ .outline {
+ outline: 1px dotted grey;
+ }
+}
+
+.word-break-all {
+ word-break: break-all;
+ line-break: anywhere;
+}
+
+.signin {
+ backface-visibility: hidden;
+ border-radius: 6px;
+ transition-property: transform, background-color;
+ transition-duration: 250ms;
+ transition-timing-function: cubic-bezier(0.07, 0.95, 0, 1);
+}
+
+.signin:hover,
+.signin:focus {
+ transform: scale(1.0625);
+}
+
+.signin:hover:active {
+ transform: scale(0.9375);
+}
+
+/* begin signin button color experiment */
+
+.white-primary {
+ @apply border-primary;
+ @apply border-2;
+ @apply text-primary;
+}
+
+.white-primary:hover,
+.white-primary:focus {
+ @apply bg-primary;
+ @apply text-white;
+}
+
+.primary {
+ @apply bg-primary;
+ @apply text-white;
+}
+
+.white-violet {
+ @apply border-violet;
+ @apply border-2;
+ @apply text-violet;
+}
+
+.white-violet:hover,
+.white-violet:focus {
+ @apply bg-violet;
+ @apply text-white;
+
+ background-image: var(--violet-gradient);
+}
+
+.violet {
+ @apply bg-violet;
+ @apply text-white;
+}
+
+.violet:hover,
+.violet:focus {
+ background-image: var(--violet-gradient);
+}
+
+/* end signin button color experiment */
diff --git a/app/main.js b/app/main.js
index 2c55e0ed..c6a89dce 100644
--- a/app/main.js
+++ b/app/main.js
@@ -1,50 +1,74 @@
-import app from './routes';
-import locale from '../common/locales';
-import fileManager from './fileManager';
+/* global DEFAULTS LIMITS WEB_UI PREFS */
+import 'core-js';
+import 'fast-text-encoding'; // MS Edge support
+import 'intl-pluralrules';
+import choo from 'choo';
+import nanotiming from 'nanotiming';
+import routes from './routes';
+import getCapabilities from './capabilities';
+import controller from './controller';
import dragManager from './dragManager';
-import { canHasSend } from './utils';
-import assets from '../common/assets';
+import pasteManager from './pasteManager';
import storage from './storage';
-import metrics from './metrics';
import experiments from './experiments';
-import Raven from 'raven-js';
+import * as Sentry from '@sentry/browser';
+import './main.css';
+import User from './user';
+import { getTranslator } from './locale';
+import Archive from './archive';
+import { setTranslate, locale } from './utils';
-if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
- Raven.config(window.SENTRY_ID, window.RAVEN_CONFIG).install();
+if (navigator.doNotTrack !== '1' && window.SENTRY_CONFIG) {
+ Sentry.init(window.SENTRY_CONFIG);
}
-app.use((state, emitter) => {
- // init state
- state.transfer = null;
- state.fileInfo = null;
- state.translate = locale.getTranslator();
- state.storage = storage;
- state.raven = Raven;
- emitter.on('DOMContentLoaded', async () => {
- let reason = null;
- if (
- /firefox/i.test(navigator.userAgent) &&
- parseInt(navigator.userAgent.match(/firefox\/*([^\n\r]*)\./i)[1], 10) <=
- 49
- ) {
- reason = 'outdated';
- }
- if (/edge\/\d+/i.test(navigator.userAgent)) {
- reason = 'edge';
- }
- const ok = await canHasSend(assets.get('cryptofill.js'));
- if (!ok) {
- reason = /firefox/i.test(navigator.userAgent) ? 'outdated' : 'gcm';
- }
- if (reason) {
- setTimeout(() => emitter.emit('replaceState', `/unsupported/${reason}`));
- }
- });
-});
+if (process.env.NODE_ENV === 'production') {
+ nanotiming.disabled = true;
+}
-app.use(metrics);
-app.use(fileManager);
-app.use(dragManager);
-app.use(experiments);
+(async function start() {
+ const capabilities = await getCapabilities();
+ if (
+ !capabilities.crypto &&
+ window.location.pathname !== '/unsupported/crypto'
+ ) {
+ return window.location.assign('/unsupported/crypto');
+ }
+ if (capabilities.serviceWorker) {
+ try {
+ await navigator.serviceWorker.register('/serviceWorker.js');
+ await navigator.serviceWorker.ready;
+ } catch (e) {
+ // continue but disable streaming downloads
+ capabilities.streamDownload = false;
+ }
+ }
-app.mount('body');
+ const translate = await getTranslator(locale());
+ setTranslate(translate);
+ // eslint-disable-next-line require-atomic-updates
+ window.initialState = {
+ LIMITS,
+ DEFAULTS,
+ WEB_UI,
+ PREFS,
+ archive: new Archive([], DEFAULTS.EXPIRE_SECONDS, DEFAULTS.DOWNLOADS),
+ capabilities,
+ translate,
+ storage,
+ sentry: Sentry,
+ user: new User(storage, LIMITS, window.AUTH_CONFIG),
+ transfer: null,
+ fileInfo: null,
+ locale: locale()
+ };
+
+ const app = routes(choo({ hash: true }));
+ // eslint-disable-next-line require-atomic-updates
+ window.app = app;
+ app.use(experiments);
+ app.use(controller);
+ app.use(dragManager);
+ app.use(pasteManager);
+ app.mount('body');
+})();
diff --git a/app/metrics.js b/app/metrics.js
deleted file mode 100644
index 3f3adf4e..00000000
--- a/app/metrics.js
+++ /dev/null
@@ -1,283 +0,0 @@
-import testPilotGA from 'testpilot-ga/src/TestPilotGA';
-import storage from './storage';
-
-let hasLocalStorage = false;
-try {
- hasLocalStorage = typeof localStorage !== 'undefined';
-} catch (e) {
- // when disabled, any mention of localStorage throws an error
-}
-
-const analytics = new testPilotGA({
- an: 'Firefox Send',
- ds: 'web',
- tid: window.GOOGLE_ANALYTICS_ID
-});
-
-let appState = null;
-let experiment = null;
-
-export default function initialize(state, emitter) {
- appState = state;
- emitter.on('DOMContentLoaded', () => {
- // addExitHandlers();
- experiment = storage.enrolled[0];
- sendEvent(category(), 'visit', {
- cm5: storage.totalUploads,
- cm6: storage.files.length,
- cm7: storage.totalDownloads
- });
- //TODO restart handlers... somewhere
- });
- emitter.on('exit', evt => {
- exitEvent(evt);
- });
-}
-
-function category() {
- switch (appState.route) {
- case '/':
- case '/share/:id':
- return 'sender';
- case '/download/:id/:key':
- case '/download/:id':
- case '/completed':
- return 'recipient';
- default:
- return 'other';
- }
-}
-
-function sendEvent() {
- const args = Array.from(arguments);
- if (experiment && args[2]) {
- args[2].xid = experiment[0];
- args[2].xvar = experiment[1];
- }
- return (
- hasLocalStorage && analytics.sendEvent.apply(analytics, args).catch(() => 0)
- );
-}
-
-function urlToMetric(url) {
- switch (url) {
- case 'https://www.mozilla.org/':
- return 'mozilla';
- case 'https://www.mozilla.org/about/legal':
- return 'legal';
- case 'https://testpilot.firefox.com/about':
- return 'about';
- case 'https://testpilot.firefox.com/privacy':
- return 'privacy';
- case 'https://testpilot.firefox.com/terms':
- return 'terms';
- case 'https://www.mozilla.org/privacy/websites/#cookies':
- return 'cookies';
- case 'https://github.com/mozilla/send':
- return 'github';
- case 'https://twitter.com/FxTestPilot':
- return 'twitter';
- case 'https://www.mozilla.org/firefox/new/?scene=2':
- return 'download-firefox';
- case 'https://qsurvey.mozilla.com/s3/txp-firefox-send':
- return 'survey';
- case 'https://testpilot.firefox.com/':
- case 'https://testpilot.firefox.com/experiments/send':
- return 'testpilot';
- case 'https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com':
- return 'promo';
- default:
- return 'other';
- }
-}
-
-function setReferrer(state) {
- if (category() === 'sender') {
- if (state) {
- storage.referrer = `${state}-upload`;
- }
- } else if (category() === 'recipient') {
- if (state) {
- storage.referrer = `${state}-download`;
- }
- }
-}
-
-function externalReferrer() {
- if (/^https:\/\/testpilot\.firefox\.com/.test(document.referrer)) {
- return 'testpilot';
- }
- return 'external';
-}
-
-function takeReferrer() {
- const referrer = storage.referrer || externalReferrer();
- storage.referrer = null;
- return referrer;
-}
-
-function startedUpload(params) {
- return sendEvent('sender', 'upload-started', {
- cm1: params.size,
- cm5: storage.totalUploads,
- cm6: storage.files.length + 1,
- cm7: storage.totalDownloads,
- cd1: params.type,
- cd5: takeReferrer()
- });
-}
-
-function cancelledUpload(params) {
- setReferrer('cancelled');
- return sendEvent('sender', 'upload-stopped', {
- cm1: params.size,
- cm5: storage.totalUploads,
- cm6: storage.files.length,
- cm7: storage.totalDownloads,
- cd1: params.type,
- cd2: 'cancelled'
- });
-}
-
-function completedUpload(params) {
- return sendEvent('sender', 'upload-stopped', {
- cm1: params.size,
- cm2: params.time,
- cm3: params.speed,
- cm5: storage.totalUploads,
- cm6: storage.files.length,
- cm7: storage.totalDownloads,
- cd1: params.type,
- cd2: 'completed'
- });
-}
-
-function addedPassword(params) {
- return sendEvent('sender', 'password-added', {
- cm1: params.size,
- cm5: storage.totalUploads,
- cm6: storage.files.length,
- cm7: storage.totalDownloads
- });
-}
-
-function startedDownload(params) {
- return sendEvent('recipient', 'download-started', {
- cm1: params.size,
- cm4: params.ttl,
- cm5: storage.totalUploads,
- cm6: storage.files.length,
- cm7: storage.totalDownloads
- });
-}
-
-function stoppedDownload(params) {
- return sendEvent('recipient', 'download-stopped', {
- cm1: params.size,
- cm5: storage.totalUploads,
- cm6: storage.files.length,
- cm7: storage.totalDownloads,
- cd2: 'errored',
- cd6: params.err
- });
-}
-
-function cancelledDownload(params) {
- setReferrer('cancelled');
- return sendEvent('recipient', 'download-stopped', {
- cm1: params.size,
- cm5: storage.totalUploads,
- cm6: storage.files.length,
- cm7: storage.totalDownloads,
- cd2: 'cancelled'
- });
-}
-
-function stoppedUpload(params) {
- return sendEvent('sender', 'upload-stopped', {
- cm1: params.size,
- cm5: storage.totalUploads,
- cm6: storage.files.length,
- cm7: storage.totalDownloads,
- cd1: params.type,
- cd2: 'errored',
- cd6: params.err
- });
-}
-
-function completedDownload(params) {
- return sendEvent('recipient', 'download-stopped', {
- cm1: params.size,
- cm2: params.time,
- cm3: params.speed,
- cm5: storage.totalUploads,
- cm6: storage.files.length,
- cm7: storage.totalDownloads,
- cd2: 'completed'
- });
-}
-
-function deletedUpload(params) {
- return sendEvent(category(), 'upload-deleted', {
- cm1: params.size,
- cm2: params.time,
- cm3: params.speed,
- cm4: params.ttl,
- cm5: storage.totalUploads,
- cm6: storage.files.length,
- cm7: storage.totalDownloads,
- cd1: params.type,
- cd4: params.location
- });
-}
-
-function unsupported(params) {
- return sendEvent(category(), 'unsupported', {
- cd6: params.err
- });
-}
-
-function copiedLink(params) {
- return sendEvent('sender', 'copied', {
- cd4: params.location
- });
-}
-
-function exitEvent(target) {
- return sendEvent(category(), 'exited', {
- cd3: urlToMetric(target.currentTarget.href)
- });
-}
-
-// eslint-disable-next-line no-unused-vars
-function addExitHandlers() {
- const links = Array.from(document.querySelectorAll('a'));
- links.forEach(l => {
- if (/^http/.test(l.getAttribute('href'))) {
- l.addEventListener('click', exitEvent);
- }
- });
-}
-
-function restart(state) {
- setReferrer(state);
- return sendEvent(category(), 'restarted', {
- cd2: state
- });
-}
-
-export {
- copiedLink,
- startedUpload,
- cancelledUpload,
- stoppedUpload,
- completedUpload,
- deletedUpload,
- startedDownload,
- cancelledDownload,
- stoppedDownload,
- completedDownload,
- addedPassword,
- restart,
- unsupported
-};
diff --git a/app/ownedFile.js b/app/ownedFile.js
new file mode 100644
index 00000000..04b497d1
--- /dev/null
+++ b/app/ownedFile.js
@@ -0,0 +1,96 @@
+import Keychain from './keychain';
+import { arrayToB64 } from './utils';
+import { del, fileInfo, setParams, setPassword } from './api';
+
+export default class OwnedFile {
+ constructor(obj) {
+ if (!obj.manifest) {
+ throw new Error('invalid file object');
+ }
+ this.id = obj.id;
+ this.url = obj.url;
+ this.name = obj.name;
+ this.size = obj.size;
+ this.manifest = obj.manifest;
+ this.time = obj.time;
+ this.speed = obj.speed;
+ this.createdAt = obj.createdAt;
+ this.expiresAt = obj.expiresAt;
+ this.ownerToken = obj.ownerToken;
+ this.dlimit = obj.dlimit || 1;
+ this.dtotal = obj.dtotal || 0;
+ this.keychain = new Keychain(obj.secretKey, obj.nonce);
+ this._hasPassword = !!obj.hasPassword;
+ this.timeLimit = obj.timeLimit;
+ }
+
+ get hasPassword() {
+ return !!this._hasPassword;
+ }
+
+ get expired() {
+ return this.dlimit === this.dtotal || Date.now() > this.expiresAt;
+ }
+
+ async setPassword(password) {
+ try {
+ this.password = password;
+ this._hasPassword = true;
+ this.keychain.setPassword(password, this.url);
+ const result = await setPassword(this.id, this.ownerToken, this.keychain);
+ return result;
+ } catch (e) {
+ this.password = null;
+ this._hasPassword = false;
+ throw e;
+ }
+ }
+
+ del() {
+ return del(this.id, this.ownerToken);
+ }
+
+ changeLimit(dlimit, user = {}) {
+ if (this.dlimit !== dlimit) {
+ this.dlimit = dlimit;
+ return setParams(this.id, this.ownerToken, user.bearerToken, { dlimit });
+ }
+ return Promise.resolve(true);
+ }
+
+ async updateDownloadCount() {
+ const oldTotal = this.dtotal;
+ const oldLimit = this.dlimit;
+ try {
+ const result = await fileInfo(this.id, this.ownerToken);
+ this.dtotal = result.dtotal;
+ this.dlimit = result.dlimit;
+ } catch (e) {
+ if (e.message === '404') {
+ this.dtotal = this.dlimit;
+ }
+ // ignore other errors
+ }
+ return oldTotal !== this.dtotal || oldLimit !== this.dlimit;
+ }
+
+ toJSON() {
+ return {
+ id: this.id,
+ url: this.url,
+ name: this.name,
+ size: this.size,
+ manifest: this.manifest,
+ time: this.time,
+ speed: this.speed,
+ createdAt: this.createdAt,
+ expiresAt: this.expiresAt,
+ secretKey: arrayToB64(this.keychain.rawSecret),
+ ownerToken: this.ownerToken,
+ dlimit: this.dlimit,
+ dtotal: this.dtotal,
+ hasPassword: this.hasPassword,
+ timeLimit: this.timeLimit
+ };
+ }
+}
diff --git a/app/pasteManager.js b/app/pasteManager.js
new file mode 100644
index 00000000..c35c7f82
--- /dev/null
+++ b/app/pasteManager.js
@@ -0,0 +1,36 @@
+function getString(item) {
+ return new Promise(resolve => {
+ item.getAsString(resolve);
+ });
+}
+
+export default function(state, emitter) {
+ window.addEventListener('paste', async event => {
+ if (state.route !== '/' || state.uploading) return;
+ if (['password', 'text', 'email'].includes(event.target.type)) return;
+
+ const items = Array.from(event.clipboardData.items);
+ const transferFiles = items.filter(item => item.kind === 'file');
+ const strings = items.filter(item => item.kind === 'string');
+ if (transferFiles.length) {
+ const promises = transferFiles.map(async (f, i) => {
+ const blob = f.getAsFile();
+ if (!blob) {
+ return null;
+ }
+ const name = await getString(strings[i]);
+ const file = new File([blob], name, { type: blob.type });
+ return file;
+ });
+ const files = (await Promise.all(promises)).filter(f => !!f);
+ if (files.length) {
+ emitter.emit('addFiles', { files });
+ }
+ } else if (strings.length) {
+ strings[0].getAsString(s => {
+ const file = new File([s], 'pasted.txt', { type: 'text/plain' });
+ emitter.emit('addFiles', { files: [file] });
+ });
+ }
+ });
+}
diff --git a/app/qrcode.js b/app/qrcode.js
new file mode 100644
index 00000000..5d960df3
--- /dev/null
+++ b/app/qrcode.js
@@ -0,0 +1,2345 @@
+//---------------------------------------------------------------------
+//
+// QR Code Generator for JavaScript
+//
+// Copyright (c) 2009 Kazuhiko Arase
+//
+// URL: http://www.d-project.com/
+//
+// Licensed under the MIT license:
+// http://www.opensource.org/licenses/mit-license.php
+//
+// The word 'QR Code' is registered trademark of
+// DENSO WAVE INCORPORATED
+// http://www.denso-wave.com/qrcode/faqpatent-e.html
+//
+//---------------------------------------------------------------------
+
+var qrcode = (function() {
+ //---------------------------------------------------------------------
+ // qrcode
+ //---------------------------------------------------------------------
+
+ /**
+ * qrcode
+ * @param typeNumber 1 to 40
+ * @param errorCorrectionLevel 'L','M','Q','H'
+ */
+ var qrcode = function(typeNumber, errorCorrectionLevel) {
+ var PAD0 = 0xec;
+ var PAD1 = 0x11;
+
+ var _typeNumber = typeNumber;
+ var _errorCorrectionLevel = QRErrorCorrectionLevel[errorCorrectionLevel];
+ var _modules = null;
+ var _moduleCount = 0;
+ var _dataCache = null;
+ var _dataList = [];
+
+ var _this = {};
+
+ var makeImpl = function(test, maskPattern) {
+ _moduleCount = _typeNumber * 4 + 17;
+ _modules = (function(moduleCount) {
+ var modules = new Array(moduleCount);
+ for (var row = 0; row < moduleCount; row += 1) {
+ modules[row] = new Array(moduleCount);
+ for (var col = 0; col < moduleCount; col += 1) {
+ modules[row][col] = null;
+ }
+ }
+ return modules;
+ })(_moduleCount);
+
+ setupPositionProbePattern(0, 0);
+ setupPositionProbePattern(_moduleCount - 7, 0);
+ setupPositionProbePattern(0, _moduleCount - 7);
+ setupPositionAdjustPattern();
+ setupTimingPattern();
+ setupTypeInfo(test, maskPattern);
+
+ if (_typeNumber >= 7) {
+ setupTypeNumber(test);
+ }
+
+ if (_dataCache == null) {
+ _dataCache = createData(_typeNumber, _errorCorrectionLevel, _dataList);
+ }
+
+ mapData(_dataCache, maskPattern);
+ };
+
+ var setupPositionProbePattern = function(row, col) {
+ for (var r = -1; r <= 7; r += 1) {
+ if (row + r <= -1 || _moduleCount <= row + r) continue;
+
+ for (var c = -1; c <= 7; c += 1) {
+ if (col + c <= -1 || _moduleCount <= col + c) continue;
+
+ if (
+ (0 <= r && r <= 6 && (c == 0 || c == 6)) ||
+ (0 <= c && c <= 6 && (r == 0 || r == 6)) ||
+ (2 <= r && r <= 4 && 2 <= c && c <= 4)
+ ) {
+ _modules[row + r][col + c] = true;
+ } else {
+ _modules[row + r][col + c] = false;
+ }
+ }
+ }
+ };
+
+ var getBestMaskPattern = function() {
+ var minLostPoint = 0;
+ var pattern = 0;
+
+ for (var i = 0; i < 8; i += 1) {
+ makeImpl(true, i);
+
+ var lostPoint = QRUtil.getLostPoint(_this);
+
+ if (i == 0 || minLostPoint > lostPoint) {
+ minLostPoint = lostPoint;
+ pattern = i;
+ }
+ }
+
+ return pattern;
+ };
+
+ var setupTimingPattern = function() {
+ for (var r = 8; r < _moduleCount - 8; r += 1) {
+ if (_modules[r][6] != null) {
+ continue;
+ }
+ _modules[r][6] = r % 2 == 0;
+ }
+
+ for (var c = 8; c < _moduleCount - 8; c += 1) {
+ if (_modules[6][c] != null) {
+ continue;
+ }
+ _modules[6][c] = c % 2 == 0;
+ }
+ };
+
+ var setupPositionAdjustPattern = function() {
+ var pos = QRUtil.getPatternPosition(_typeNumber);
+
+ for (var i = 0; i < pos.length; i += 1) {
+ for (var j = 0; j < pos.length; j += 1) {
+ var row = pos[i];
+ var col = pos[j];
+
+ if (_modules[row][col] != null) {
+ continue;
+ }
+
+ for (var r = -2; r <= 2; r += 1) {
+ for (var c = -2; c <= 2; c += 1) {
+ if (
+ r == -2 ||
+ r == 2 ||
+ c == -2 ||
+ c == 2 ||
+ (r == 0 && c == 0)
+ ) {
+ _modules[row + r][col + c] = true;
+ } else {
+ _modules[row + r][col + c] = false;
+ }
+ }
+ }
+ }
+ }
+ };
+
+ var setupTypeNumber = function(test) {
+ var bits = QRUtil.getBCHTypeNumber(_typeNumber);
+
+ for (var i = 0; i < 18; i += 1) {
+ var mod = !test && ((bits >> i) & 1) == 1;
+ _modules[Math.floor(i / 3)][(i % 3) + _moduleCount - 8 - 3] = mod;
+ }
+
+ for (var i = 0; i < 18; i += 1) {
+ var mod = !test && ((bits >> i) & 1) == 1;
+ _modules[(i % 3) + _moduleCount - 8 - 3][Math.floor(i / 3)] = mod;
+ }
+ };
+
+ var setupTypeInfo = function(test, maskPattern) {
+ var data = (_errorCorrectionLevel << 3) | maskPattern;
+ var bits = QRUtil.getBCHTypeInfo(data);
+
+ // vertical
+ for (var i = 0; i < 15; i += 1) {
+ var mod = !test && ((bits >> i) & 1) == 1;
+
+ if (i < 6) {
+ _modules[i][8] = mod;
+ } else if (i < 8) {
+ _modules[i + 1][8] = mod;
+ } else {
+ _modules[_moduleCount - 15 + i][8] = mod;
+ }
+ }
+
+ // horizontal
+ for (var i = 0; i < 15; i += 1) {
+ var mod = !test && ((bits >> i) & 1) == 1;
+
+ if (i < 8) {
+ _modules[8][_moduleCount - i - 1] = mod;
+ } else if (i < 9) {
+ _modules[8][15 - i - 1 + 1] = mod;
+ } else {
+ _modules[8][15 - i - 1] = mod;
+ }
+ }
+
+ // fixed module
+ _modules[_moduleCount - 8][8] = !test;
+ };
+
+ var mapData = function(data, maskPattern) {
+ var inc = -1;
+ var row = _moduleCount - 1;
+ var bitIndex = 7;
+ var byteIndex = 0;
+ var maskFunc = QRUtil.getMaskFunction(maskPattern);
+
+ for (var col = _moduleCount - 1; col > 0; col -= 2) {
+ if (col == 6) col -= 1;
+
+ while (true) {
+ for (var c = 0; c < 2; c += 1) {
+ if (_modules[row][col - c] == null) {
+ var dark = false;
+
+ if (byteIndex < data.length) {
+ dark = ((data[byteIndex] >>> bitIndex) & 1) == 1;
+ }
+
+ var mask = maskFunc(row, col - c);
+
+ if (mask) {
+ dark = !dark;
+ }
+
+ _modules[row][col - c] = dark;
+ bitIndex -= 1;
+
+ if (bitIndex == -1) {
+ byteIndex += 1;
+ bitIndex = 7;
+ }
+ }
+ }
+
+ row += inc;
+
+ if (row < 0 || _moduleCount <= row) {
+ row -= inc;
+ inc = -inc;
+ break;
+ }
+ }
+ }
+ };
+
+ var createBytes = function(buffer, rsBlocks) {
+ var offset = 0;
+
+ var maxDcCount = 0;
+ var maxEcCount = 0;
+
+ var dcdata = new Array(rsBlocks.length);
+ var ecdata = new Array(rsBlocks.length);
+
+ for (var r = 0; r < rsBlocks.length; r += 1) {
+ var dcCount = rsBlocks[r].dataCount;
+ var ecCount = rsBlocks[r].totalCount - dcCount;
+
+ maxDcCount = Math.max(maxDcCount, dcCount);
+ maxEcCount = Math.max(maxEcCount, ecCount);
+
+ dcdata[r] = new Array(dcCount);
+
+ for (var i = 0; i < dcdata[r].length; i += 1) {
+ dcdata[r][i] = 0xff & buffer.getBuffer()[i + offset];
+ }
+ offset += dcCount;
+
+ var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount);
+ var rawPoly = qrPolynomial(dcdata[r], rsPoly.getLength() - 1);
+
+ var modPoly = rawPoly.mod(rsPoly);
+ ecdata[r] = new Array(rsPoly.getLength() - 1);
+ for (var i = 0; i < ecdata[r].length; i += 1) {
+ var modIndex = i + modPoly.getLength() - ecdata[r].length;
+ ecdata[r][i] = modIndex >= 0 ? modPoly.getAt(modIndex) : 0;
+ }
+ }
+
+ var totalCodeCount = 0;
+ for (var i = 0; i < rsBlocks.length; i += 1) {
+ totalCodeCount += rsBlocks[i].totalCount;
+ }
+
+ var data = new Array(totalCodeCount);
+ var index = 0;
+
+ for (var i = 0; i < maxDcCount; i += 1) {
+ for (var r = 0; r < rsBlocks.length; r += 1) {
+ if (i < dcdata[r].length) {
+ data[index] = dcdata[r][i];
+ index += 1;
+ }
+ }
+ }
+
+ for (var i = 0; i < maxEcCount; i += 1) {
+ for (var r = 0; r < rsBlocks.length; r += 1) {
+ if (i < ecdata[r].length) {
+ data[index] = ecdata[r][i];
+ index += 1;
+ }
+ }
+ }
+
+ return data;
+ };
+
+ var createData = function(typeNumber, errorCorrectionLevel, dataList) {
+ var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectionLevel);
+
+ var buffer = qrBitBuffer();
+
+ for (var i = 0; i < dataList.length; i += 1) {
+ var data = dataList[i];
+ buffer.put(data.getMode(), 4);
+ buffer.put(
+ data.getLength(),
+ QRUtil.getLengthInBits(data.getMode(), typeNumber)
+ );
+ data.write(buffer);
+ }
+
+ // calc num max data.
+ var totalDataCount = 0;
+ for (var i = 0; i < rsBlocks.length; i += 1) {
+ totalDataCount += rsBlocks[i].dataCount;
+ }
+
+ if (buffer.getLengthInBits() > totalDataCount * 8) {
+ throw 'code length overflow. (' +
+ buffer.getLengthInBits() +
+ '>' +
+ totalDataCount * 8 +
+ ')';
+ }
+
+ // end code
+ if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) {
+ buffer.put(0, 4);
+ }
+
+ // padding
+ while (buffer.getLengthInBits() % 8 != 0) {
+ buffer.putBit(false);
+ }
+
+ // padding
+ while (true) {
+ if (buffer.getLengthInBits() >= totalDataCount * 8) {
+ break;
+ }
+ buffer.put(PAD0, 8);
+
+ if (buffer.getLengthInBits() >= totalDataCount * 8) {
+ break;
+ }
+ buffer.put(PAD1, 8);
+ }
+
+ return createBytes(buffer, rsBlocks);
+ };
+
+ _this.addData = function(data, mode) {
+ mode = mode || 'Byte';
+
+ var newData = null;
+
+ switch (mode) {
+ case 'Numeric':
+ newData = qrNumber(data);
+ break;
+ case 'Alphanumeric':
+ newData = qrAlphaNum(data);
+ break;
+ case 'Byte':
+ newData = qr8BitByte(data);
+ break;
+ case 'Kanji':
+ newData = qrKanji(data);
+ break;
+ default:
+ throw 'mode:' + mode;
+ }
+
+ _dataList.push(newData);
+ _dataCache = null;
+ };
+
+ _this.isDark = function(row, col) {
+ if (row < 0 || _moduleCount <= row || col < 0 || _moduleCount <= col) {
+ throw row + ',' + col;
+ }
+ return _modules[row][col];
+ };
+
+ _this.getModuleCount = function() {
+ return _moduleCount;
+ };
+
+ _this.make = function() {
+ if (_typeNumber < 1) {
+ var typeNumber = 1;
+
+ for (; typeNumber < 40; typeNumber++) {
+ var rsBlocks = QRRSBlock.getRSBlocks(
+ typeNumber,
+ _errorCorrectionLevel
+ );
+ var buffer = qrBitBuffer();
+
+ for (var i = 0; i < _dataList.length; i++) {
+ var data = _dataList[i];
+ buffer.put(data.getMode(), 4);
+ buffer.put(
+ data.getLength(),
+ QRUtil.getLengthInBits(data.getMode(), typeNumber)
+ );
+ data.write(buffer);
+ }
+
+ var totalDataCount = 0;
+ for (var i = 0; i < rsBlocks.length; i++) {
+ totalDataCount += rsBlocks[i].dataCount;
+ }
+
+ if (buffer.getLengthInBits() <= totalDataCount * 8) {
+ break;
+ }
+ }
+
+ _typeNumber = typeNumber;
+ }
+
+ makeImpl(false, getBestMaskPattern());
+ };
+
+ _this.createTableTag = function(cellSize, margin) {
+ cellSize = cellSize || 2;
+ margin = typeof margin == 'undefined' ? cellSize * 4 : margin;
+
+ var qrHtml = '';
+
+ qrHtml += '
';
+ qrHtml += '';
+
+ for (var r = 0; r < _this.getModuleCount(); r += 1) {
+ qrHtml += '';
+
+ for (var c = 0; c < _this.getModuleCount(); c += 1) {
+ qrHtml += ' | ';
+ }
+
+ qrHtml += '
';
+ }
+
+ qrHtml += '';
+ qrHtml += '
';
+
+ return qrHtml;
+ };
+
+ _this.createSvgTag = function(cellSize, margin, alt, title) {
+ var opts = {};
+ if (typeof arguments[0] == 'object') {
+ // Called by options.
+ opts = arguments[0];
+ // overwrite cellSize and margin.
+ cellSize = opts.cellSize;
+ margin = opts.margin;
+ alt = opts.alt;
+ title = opts.title;
+ }
+
+ cellSize = cellSize || 2;
+ margin = typeof margin == 'undefined' ? cellSize * 4 : margin;
+
+ // Compose alt property surrogate
+ alt = typeof alt === 'string' ? { text: alt } : alt || {};
+ alt.text = alt.text || null;
+ alt.id = alt.text ? alt.id || 'qrcode-description' : null;
+
+ // Compose title property surrogate
+ title = typeof title === 'string' ? { text: title } : title || {};
+ title.text = title.text || null;
+ title.id = title.text ? title.id || 'qrcode-title' : null;
+
+ var size = _this.getModuleCount() * cellSize + margin * 2;
+ var c,
+ mc,
+ r,
+ mr,
+ qrSvg = '',
+ rect;
+
+ rect =
+ 'l' +
+ cellSize +
+ ',0 0,' +
+ cellSize +
+ ' -' +
+ cellSize +
+ ',0 0,-' +
+ cellSize +
+ 'z ';
+
+ qrSvg += '
';
+
+ return qrSvg;
+ };
+
+ _this.createDataURL = function(cellSize, margin) {
+ cellSize = cellSize || 2;
+ margin = typeof margin == 'undefined' ? cellSize * 4 : margin;
+
+ var size = _this.getModuleCount() * cellSize + margin * 2;
+ var min = margin;
+ var max = size - margin;
+
+ return createDataURL(size, size, function(x, y) {
+ if (min <= x && x < max && min <= y && y < max) {
+ var c = Math.floor((x - min) / cellSize);
+ var r = Math.floor((y - min) / cellSize);
+ return _this.isDark(r, c) ? 0 : 1;
+ } else {
+ return 1;
+ }
+ });
+ };
+
+ _this.createImgTag = function(cellSize, margin, alt) {
+ cellSize = cellSize || 2;
+ margin = typeof margin == 'undefined' ? cellSize * 4 : margin;
+
+ var size = _this.getModuleCount() * cellSize + margin * 2;
+
+ var img = '';
+ img += '
![]()
';
+
+ return img;
+ };
+
+ var escapeXml = function(s) {
+ var escaped = '';
+ for (var i = 0; i < s.length; i += 1) {
+ var c = s.charAt(i);
+ switch (c) {
+ case '<':
+ escaped += '<';
+ break;
+ case '>':
+ escaped += '>';
+ break;
+ case '&':
+ escaped += '&';
+ break;
+ case '"':
+ escaped += '"';
+ break;
+ default:
+ escaped += c;
+ break;
+ }
+ }
+ return escaped;
+ };
+
+ var _createHalfASCII = function(margin) {
+ var cellSize = 1;
+ margin = typeof margin == 'undefined' ? cellSize * 2 : margin;
+
+ var size = _this.getModuleCount() * cellSize + margin * 2;
+ var min = margin;
+ var max = size - margin;
+
+ var y, x, r1, r2, p;
+
+ var blocks = {
+ '██': '█',
+ '█ ': '▀',
+ ' █': '▄',
+ ' ': ' '
+ };
+
+ var blocksLastLineNoMargin = {
+ '██': '▀',
+ '█ ': '▀',
+ ' █': ' ',
+ ' ': ' '
+ };
+
+ var ascii = '';
+ for (y = 0; y < size; y += 2) {
+ r1 = Math.floor((y - min) / cellSize);
+ r2 = Math.floor((y + 1 - min) / cellSize);
+ for (x = 0; x < size; x += 1) {
+ p = '█';
+
+ if (
+ min <= x &&
+ x < max &&
+ min <= y &&
+ y < max &&
+ _this.isDark(r1, Math.floor((x - min) / cellSize))
+ ) {
+ p = ' ';
+ }
+
+ if (
+ min <= x &&
+ x < max &&
+ min <= y + 1 &&
+ y + 1 < max &&
+ _this.isDark(r2, Math.floor((x - min) / cellSize))
+ ) {
+ p += ' ';
+ } else {
+ p += '█';
+ }
+
+ // Output 2 characters per pixel, to create full square. 1 character per pixels gives only half width of square.
+ ascii +=
+ margin < 1 && y + 1 >= max ? blocksLastLineNoMargin[p] : blocks[p];
+ }
+
+ ascii += '\n';
+ }
+
+ if (size % 2 && margin > 0) {
+ return (
+ ascii.substring(0, ascii.length - size - 1) +
+ Array(size + 1).join('▀')
+ );
+ }
+
+ return ascii.substring(0, ascii.length - 1);
+ };
+
+ _this.createASCII = function(cellSize, margin) {
+ cellSize = cellSize || 1;
+
+ if (cellSize < 2) {
+ return _createHalfASCII(margin);
+ }
+
+ cellSize -= 1;
+ margin = typeof margin == 'undefined' ? cellSize * 2 : margin;
+
+ var size = _this.getModuleCount() * cellSize + margin * 2;
+ var min = margin;
+ var max = size - margin;
+
+ var y, x, r, p;
+
+ var white = Array(cellSize + 1).join('██');
+ var black = Array(cellSize + 1).join(' ');
+
+ var ascii = '';
+ var line = '';
+ for (y = 0; y < size; y += 1) {
+ r = Math.floor((y - min) / cellSize);
+ line = '';
+ for (x = 0; x < size; x += 1) {
+ p = 1;
+
+ if (
+ min <= x &&
+ x < max &&
+ min <= y &&
+ y < max &&
+ _this.isDark(r, Math.floor((x - min) / cellSize))
+ ) {
+ p = 0;
+ }
+
+ // Output 2 characters per pixel, to create full square. 1 character per pixels gives only half width of square.
+ line += p ? white : black;
+ }
+
+ for (r = 0; r < cellSize; r += 1) {
+ ascii += line + '\n';
+ }
+ }
+
+ return ascii.substring(0, ascii.length - 1);
+ };
+
+ _this.renderTo2dContext = function(context, cellSize) {
+ cellSize = cellSize || 2;
+ var length = _this.getModuleCount();
+ for (var row = 0; row < length; row++) {
+ for (var col = 0; col < length; col++) {
+ context.fillStyle = _this.isDark(row, col) ? 'black' : 'white';
+ context.fillRect(row * cellSize, col * cellSize, cellSize, cellSize);
+ }
+ }
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // qrcode.stringToBytes
+ //---------------------------------------------------------------------
+
+ qrcode.stringToBytesFuncs = {
+ default: function(s) {
+ var bytes = [];
+ for (var i = 0; i < s.length; i += 1) {
+ var c = s.charCodeAt(i);
+ bytes.push(c & 0xff);
+ }
+ return bytes;
+ }
+ };
+
+ qrcode.stringToBytes = qrcode.stringToBytesFuncs['default'];
+
+ //---------------------------------------------------------------------
+ // qrcode.createStringToBytes
+ //---------------------------------------------------------------------
+
+ /**
+ * @param unicodeData base64 string of byte array.
+ * [16bit Unicode],[16bit Bytes], ...
+ * @param numChars
+ */
+ qrcode.createStringToBytes = function(unicodeData, numChars) {
+ // create conversion map.
+
+ var unicodeMap = (function() {
+ var bin = base64DecodeInputStream(unicodeData);
+ var read = function() {
+ var b = bin.read();
+ if (b == -1) throw 'eof';
+ return b;
+ };
+
+ var count = 0;
+ var unicodeMap = {};
+ while (true) {
+ var b0 = bin.read();
+ if (b0 == -1) break;
+ var b1 = read();
+ var b2 = read();
+ var b3 = read();
+ var k = String.fromCharCode((b0 << 8) | b1);
+ var v = (b2 << 8) | b3;
+ unicodeMap[k] = v;
+ count += 1;
+ }
+ if (count != numChars) {
+ throw count + ' != ' + numChars;
+ }
+
+ return unicodeMap;
+ })();
+
+ var unknownChar = '?'.charCodeAt(0);
+
+ return function(s) {
+ var bytes = [];
+ for (var i = 0; i < s.length; i += 1) {
+ var c = s.charCodeAt(i);
+ if (c < 128) {
+ bytes.push(c);
+ } else {
+ var b = unicodeMap[s.charAt(i)];
+ if (typeof b == 'number') {
+ if ((b & 0xff) == b) {
+ // 1byte
+ bytes.push(b);
+ } else {
+ // 2bytes
+ bytes.push(b >>> 8);
+ bytes.push(b & 0xff);
+ }
+ } else {
+ bytes.push(unknownChar);
+ }
+ }
+ }
+ return bytes;
+ };
+ };
+
+ //---------------------------------------------------------------------
+ // QRMode
+ //---------------------------------------------------------------------
+
+ var QRMode = {
+ MODE_NUMBER: 1 << 0,
+ MODE_ALPHA_NUM: 1 << 1,
+ MODE_8BIT_BYTE: 1 << 2,
+ MODE_KANJI: 1 << 3
+ };
+
+ //---------------------------------------------------------------------
+ // QRErrorCorrectionLevel
+ //---------------------------------------------------------------------
+
+ var QRErrorCorrectionLevel = {
+ L: 1,
+ M: 0,
+ Q: 3,
+ H: 2
+ };
+
+ //---------------------------------------------------------------------
+ // QRMaskPattern
+ //---------------------------------------------------------------------
+
+ var QRMaskPattern = {
+ PATTERN000: 0,
+ PATTERN001: 1,
+ PATTERN010: 2,
+ PATTERN011: 3,
+ PATTERN100: 4,
+ PATTERN101: 5,
+ PATTERN110: 6,
+ PATTERN111: 7
+ };
+
+ //---------------------------------------------------------------------
+ // QRUtil
+ //---------------------------------------------------------------------
+
+ var QRUtil = (function() {
+ var PATTERN_POSITION_TABLE = [
+ [],
+ [6, 18],
+ [6, 22],
+ [6, 26],
+ [6, 30],
+ [6, 34],
+ [6, 22, 38],
+ [6, 24, 42],
+ [6, 26, 46],
+ [6, 28, 50],
+ [6, 30, 54],
+ [6, 32, 58],
+ [6, 34, 62],
+ [6, 26, 46, 66],
+ [6, 26, 48, 70],
+ [6, 26, 50, 74],
+ [6, 30, 54, 78],
+ [6, 30, 56, 82],
+ [6, 30, 58, 86],
+ [6, 34, 62, 90],
+ [6, 28, 50, 72, 94],
+ [6, 26, 50, 74, 98],
+ [6, 30, 54, 78, 102],
+ [6, 28, 54, 80, 106],
+ [6, 32, 58, 84, 110],
+ [6, 30, 58, 86, 114],
+ [6, 34, 62, 90, 118],
+ [6, 26, 50, 74, 98, 122],
+ [6, 30, 54, 78, 102, 126],
+ [6, 26, 52, 78, 104, 130],
+ [6, 30, 56, 82, 108, 134],
+ [6, 34, 60, 86, 112, 138],
+ [6, 30, 58, 86, 114, 142],
+ [6, 34, 62, 90, 118, 146],
+ [6, 30, 54, 78, 102, 126, 150],
+ [6, 24, 50, 76, 102, 128, 154],
+ [6, 28, 54, 80, 106, 132, 158],
+ [6, 32, 58, 84, 110, 136, 162],
+ [6, 26, 54, 82, 110, 138, 166],
+ [6, 30, 58, 86, 114, 142, 170]
+ ];
+ var G15 =
+ (1 << 10) |
+ (1 << 8) |
+ (1 << 5) |
+ (1 << 4) |
+ (1 << 2) |
+ (1 << 1) |
+ (1 << 0);
+ var G18 =
+ (1 << 12) |
+ (1 << 11) |
+ (1 << 10) |
+ (1 << 9) |
+ (1 << 8) |
+ (1 << 5) |
+ (1 << 2) |
+ (1 << 0);
+ var G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1);
+
+ var _this = {};
+
+ var getBCHDigit = function(data) {
+ var digit = 0;
+ while (data != 0) {
+ digit += 1;
+ data >>>= 1;
+ }
+ return digit;
+ };
+
+ _this.getBCHTypeInfo = function(data) {
+ var d = data << 10;
+ while (getBCHDigit(d) - getBCHDigit(G15) >= 0) {
+ d ^= G15 << (getBCHDigit(d) - getBCHDigit(G15));
+ }
+ return ((data << 10) | d) ^ G15_MASK;
+ };
+
+ _this.getBCHTypeNumber = function(data) {
+ var d = data << 12;
+ while (getBCHDigit(d) - getBCHDigit(G18) >= 0) {
+ d ^= G18 << (getBCHDigit(d) - getBCHDigit(G18));
+ }
+ return (data << 12) | d;
+ };
+
+ _this.getPatternPosition = function(typeNumber) {
+ return PATTERN_POSITION_TABLE[typeNumber - 1];
+ };
+
+ _this.getMaskFunction = function(maskPattern) {
+ switch (maskPattern) {
+ case QRMaskPattern.PATTERN000:
+ return function(i, j) {
+ return (i + j) % 2 == 0;
+ };
+ case QRMaskPattern.PATTERN001:
+ return function(i, j) {
+ return i % 2 == 0;
+ };
+ case QRMaskPattern.PATTERN010:
+ return function(i, j) {
+ return j % 3 == 0;
+ };
+ case QRMaskPattern.PATTERN011:
+ return function(i, j) {
+ return (i + j) % 3 == 0;
+ };
+ case QRMaskPattern.PATTERN100:
+ return function(i, j) {
+ return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 == 0;
+ };
+ case QRMaskPattern.PATTERN101:
+ return function(i, j) {
+ return ((i * j) % 2) + ((i * j) % 3) == 0;
+ };
+ case QRMaskPattern.PATTERN110:
+ return function(i, j) {
+ return (((i * j) % 2) + ((i * j) % 3)) % 2 == 0;
+ };
+ case QRMaskPattern.PATTERN111:
+ return function(i, j) {
+ return (((i * j) % 3) + ((i + j) % 2)) % 2 == 0;
+ };
+
+ default:
+ throw 'bad maskPattern:' + maskPattern;
+ }
+ };
+
+ _this.getErrorCorrectPolynomial = function(errorCorrectLength) {
+ var a = qrPolynomial([1], 0);
+ for (var i = 0; i < errorCorrectLength; i += 1) {
+ a = a.multiply(qrPolynomial([1, QRMath.gexp(i)], 0));
+ }
+ return a;
+ };
+
+ _this.getLengthInBits = function(mode, type) {
+ if (1 <= type && type < 10) {
+ // 1 - 9
+
+ switch (mode) {
+ case QRMode.MODE_NUMBER:
+ return 10;
+ case QRMode.MODE_ALPHA_NUM:
+ return 9;
+ case QRMode.MODE_8BIT_BYTE:
+ return 8;
+ case QRMode.MODE_KANJI:
+ return 8;
+ default:
+ throw 'mode:' + mode;
+ }
+ } else if (type < 27) {
+ // 10 - 26
+
+ switch (mode) {
+ case QRMode.MODE_NUMBER:
+ return 12;
+ case QRMode.MODE_ALPHA_NUM:
+ return 11;
+ case QRMode.MODE_8BIT_BYTE:
+ return 16;
+ case QRMode.MODE_KANJI:
+ return 10;
+ default:
+ throw 'mode:' + mode;
+ }
+ } else if (type < 41) {
+ // 27 - 40
+
+ switch (mode) {
+ case QRMode.MODE_NUMBER:
+ return 14;
+ case QRMode.MODE_ALPHA_NUM:
+ return 13;
+ case QRMode.MODE_8BIT_BYTE:
+ return 16;
+ case QRMode.MODE_KANJI:
+ return 12;
+ default:
+ throw 'mode:' + mode;
+ }
+ } else {
+ throw 'type:' + type;
+ }
+ };
+
+ _this.getLostPoint = function(qrcode) {
+ var moduleCount = qrcode.getModuleCount();
+
+ var lostPoint = 0;
+
+ // LEVEL1
+
+ for (var row = 0; row < moduleCount; row += 1) {
+ for (var col = 0; col < moduleCount; col += 1) {
+ var sameCount = 0;
+ var dark = qrcode.isDark(row, col);
+
+ for (var r = -1; r <= 1; r += 1) {
+ if (row + r < 0 || moduleCount <= row + r) {
+ continue;
+ }
+
+ for (var c = -1; c <= 1; c += 1) {
+ if (col + c < 0 || moduleCount <= col + c) {
+ continue;
+ }
+
+ if (r == 0 && c == 0) {
+ continue;
+ }
+
+ if (dark == qrcode.isDark(row + r, col + c)) {
+ sameCount += 1;
+ }
+ }
+ }
+
+ if (sameCount > 5) {
+ lostPoint += 3 + sameCount - 5;
+ }
+ }
+ }
+
+ // LEVEL2
+
+ for (var row = 0; row < moduleCount - 1; row += 1) {
+ for (var col = 0; col < moduleCount - 1; col += 1) {
+ var count = 0;
+ if (qrcode.isDark(row, col)) count += 1;
+ if (qrcode.isDark(row + 1, col)) count += 1;
+ if (qrcode.isDark(row, col + 1)) count += 1;
+ if (qrcode.isDark(row + 1, col + 1)) count += 1;
+ if (count == 0 || count == 4) {
+ lostPoint += 3;
+ }
+ }
+ }
+
+ // LEVEL3
+
+ for (var row = 0; row < moduleCount; row += 1) {
+ for (var col = 0; col < moduleCount - 6; col += 1) {
+ if (
+ qrcode.isDark(row, col) &&
+ !qrcode.isDark(row, col + 1) &&
+ qrcode.isDark(row, col + 2) &&
+ qrcode.isDark(row, col + 3) &&
+ qrcode.isDark(row, col + 4) &&
+ !qrcode.isDark(row, col + 5) &&
+ qrcode.isDark(row, col + 6)
+ ) {
+ lostPoint += 40;
+ }
+ }
+ }
+
+ for (var col = 0; col < moduleCount; col += 1) {
+ for (var row = 0; row < moduleCount - 6; row += 1) {
+ if (
+ qrcode.isDark(row, col) &&
+ !qrcode.isDark(row + 1, col) &&
+ qrcode.isDark(row + 2, col) &&
+ qrcode.isDark(row + 3, col) &&
+ qrcode.isDark(row + 4, col) &&
+ !qrcode.isDark(row + 5, col) &&
+ qrcode.isDark(row + 6, col)
+ ) {
+ lostPoint += 40;
+ }
+ }
+ }
+
+ // LEVEL4
+
+ var darkCount = 0;
+
+ for (var col = 0; col < moduleCount; col += 1) {
+ for (var row = 0; row < moduleCount; row += 1) {
+ if (qrcode.isDark(row, col)) {
+ darkCount += 1;
+ }
+ }
+ }
+
+ var ratio =
+ Math.abs((100 * darkCount) / moduleCount / moduleCount - 50) / 5;
+ lostPoint += ratio * 10;
+
+ return lostPoint;
+ };
+
+ return _this;
+ })();
+
+ //---------------------------------------------------------------------
+ // QRMath
+ //---------------------------------------------------------------------
+
+ var QRMath = (function() {
+ var EXP_TABLE = new Array(256);
+ var LOG_TABLE = new Array(256);
+
+ // initialize tables
+ for (var i = 0; i < 8; i += 1) {
+ EXP_TABLE[i] = 1 << i;
+ }
+ for (var i = 8; i < 256; i += 1) {
+ EXP_TABLE[i] =
+ EXP_TABLE[i - 4] ^
+ EXP_TABLE[i - 5] ^
+ EXP_TABLE[i - 6] ^
+ EXP_TABLE[i - 8];
+ }
+ for (var i = 0; i < 255; i += 1) {
+ LOG_TABLE[EXP_TABLE[i]] = i;
+ }
+
+ var _this = {};
+
+ _this.glog = function(n) {
+ if (n < 1) {
+ throw 'glog(' + n + ')';
+ }
+
+ return LOG_TABLE[n];
+ };
+
+ _this.gexp = function(n) {
+ while (n < 0) {
+ n += 255;
+ }
+
+ while (n >= 256) {
+ n -= 255;
+ }
+
+ return EXP_TABLE[n];
+ };
+
+ return _this;
+ })();
+
+ //---------------------------------------------------------------------
+ // qrPolynomial
+ //---------------------------------------------------------------------
+
+ function qrPolynomial(num, shift) {
+ if (typeof num.length == 'undefined') {
+ throw num.length + '/' + shift;
+ }
+
+ var _num = (function() {
+ var offset = 0;
+ while (offset < num.length && num[offset] == 0) {
+ offset += 1;
+ }
+ var _num = new Array(num.length - offset + shift);
+ for (var i = 0; i < num.length - offset; i += 1) {
+ _num[i] = num[i + offset];
+ }
+ return _num;
+ })();
+
+ var _this = {};
+
+ _this.getAt = function(index) {
+ return _num[index];
+ };
+
+ _this.getLength = function() {
+ return _num.length;
+ };
+
+ _this.multiply = function(e) {
+ var num = new Array(_this.getLength() + e.getLength() - 1);
+
+ for (var i = 0; i < _this.getLength(); i += 1) {
+ for (var j = 0; j < e.getLength(); j += 1) {
+ num[i + j] ^= QRMath.gexp(
+ QRMath.glog(_this.getAt(i)) + QRMath.glog(e.getAt(j))
+ );
+ }
+ }
+
+ return qrPolynomial(num, 0);
+ };
+
+ _this.mod = function(e) {
+ if (_this.getLength() - e.getLength() < 0) {
+ return _this;
+ }
+
+ var ratio = QRMath.glog(_this.getAt(0)) - QRMath.glog(e.getAt(0));
+
+ var num = new Array(_this.getLength());
+ for (var i = 0; i < _this.getLength(); i += 1) {
+ num[i] = _this.getAt(i);
+ }
+
+ for (var i = 0; i < e.getLength(); i += 1) {
+ num[i] ^= QRMath.gexp(QRMath.glog(e.getAt(i)) + ratio);
+ }
+
+ // recursive call
+ return qrPolynomial(num, 0).mod(e);
+ };
+
+ return _this;
+ }
+
+ //---------------------------------------------------------------------
+ // QRRSBlock
+ //---------------------------------------------------------------------
+
+ var QRRSBlock = (function() {
+ var RS_BLOCK_TABLE = [
+ // L
+ // M
+ // Q
+ // H
+
+ // 1
+ [1, 26, 19],
+ [1, 26, 16],
+ [1, 26, 13],
+ [1, 26, 9],
+
+ // 2
+ [1, 44, 34],
+ [1, 44, 28],
+ [1, 44, 22],
+ [1, 44, 16],
+
+ // 3
+ [1, 70, 55],
+ [1, 70, 44],
+ [2, 35, 17],
+ [2, 35, 13],
+
+ // 4
+ [1, 100, 80],
+ [2, 50, 32],
+ [2, 50, 24],
+ [4, 25, 9],
+
+ // 5
+ [1, 134, 108],
+ [2, 67, 43],
+ [2, 33, 15, 2, 34, 16],
+ [2, 33, 11, 2, 34, 12],
+
+ // 6
+ [2, 86, 68],
+ [4, 43, 27],
+ [4, 43, 19],
+ [4, 43, 15],
+
+ // 7
+ [2, 98, 78],
+ [4, 49, 31],
+ [2, 32, 14, 4, 33, 15],
+ [4, 39, 13, 1, 40, 14],
+
+ // 8
+ [2, 121, 97],
+ [2, 60, 38, 2, 61, 39],
+ [4, 40, 18, 2, 41, 19],
+ [4, 40, 14, 2, 41, 15],
+
+ // 9
+ [2, 146, 116],
+ [3, 58, 36, 2, 59, 37],
+ [4, 36, 16, 4, 37, 17],
+ [4, 36, 12, 4, 37, 13],
+
+ // 10
+ [2, 86, 68, 2, 87, 69],
+ [4, 69, 43, 1, 70, 44],
+ [6, 43, 19, 2, 44, 20],
+ [6, 43, 15, 2, 44, 16],
+
+ // 11
+ [4, 101, 81],
+ [1, 80, 50, 4, 81, 51],
+ [4, 50, 22, 4, 51, 23],
+ [3, 36, 12, 8, 37, 13],
+
+ // 12
+ [2, 116, 92, 2, 117, 93],
+ [6, 58, 36, 2, 59, 37],
+ [4, 46, 20, 6, 47, 21],
+ [7, 42, 14, 4, 43, 15],
+
+ // 13
+ [4, 133, 107],
+ [8, 59, 37, 1, 60, 38],
+ [8, 44, 20, 4, 45, 21],
+ [12, 33, 11, 4, 34, 12],
+
+ // 14
+ [3, 145, 115, 1, 146, 116],
+ [4, 64, 40, 5, 65, 41],
+ [11, 36, 16, 5, 37, 17],
+ [11, 36, 12, 5, 37, 13],
+
+ // 15
+ [5, 109, 87, 1, 110, 88],
+ [5, 65, 41, 5, 66, 42],
+ [5, 54, 24, 7, 55, 25],
+ [11, 36, 12, 7, 37, 13],
+
+ // 16
+ [5, 122, 98, 1, 123, 99],
+ [7, 73, 45, 3, 74, 46],
+ [15, 43, 19, 2, 44, 20],
+ [3, 45, 15, 13, 46, 16],
+
+ // 17
+ [1, 135, 107, 5, 136, 108],
+ [10, 74, 46, 1, 75, 47],
+ [1, 50, 22, 15, 51, 23],
+ [2, 42, 14, 17, 43, 15],
+
+ // 18
+ [5, 150, 120, 1, 151, 121],
+ [9, 69, 43, 4, 70, 44],
+ [17, 50, 22, 1, 51, 23],
+ [2, 42, 14, 19, 43, 15],
+
+ // 19
+ [3, 141, 113, 4, 142, 114],
+ [3, 70, 44, 11, 71, 45],
+ [17, 47, 21, 4, 48, 22],
+ [9, 39, 13, 16, 40, 14],
+
+ // 20
+ [3, 135, 107, 5, 136, 108],
+ [3, 67, 41, 13, 68, 42],
+ [15, 54, 24, 5, 55, 25],
+ [15, 43, 15, 10, 44, 16],
+
+ // 21
+ [4, 144, 116, 4, 145, 117],
+ [17, 68, 42],
+ [17, 50, 22, 6, 51, 23],
+ [19, 46, 16, 6, 47, 17],
+
+ // 22
+ [2, 139, 111, 7, 140, 112],
+ [17, 74, 46],
+ [7, 54, 24, 16, 55, 25],
+ [34, 37, 13],
+
+ // 23
+ [4, 151, 121, 5, 152, 122],
+ [4, 75, 47, 14, 76, 48],
+ [11, 54, 24, 14, 55, 25],
+ [16, 45, 15, 14, 46, 16],
+
+ // 24
+ [6, 147, 117, 4, 148, 118],
+ [6, 73, 45, 14, 74, 46],
+ [11, 54, 24, 16, 55, 25],
+ [30, 46, 16, 2, 47, 17],
+
+ // 25
+ [8, 132, 106, 4, 133, 107],
+ [8, 75, 47, 13, 76, 48],
+ [7, 54, 24, 22, 55, 25],
+ [22, 45, 15, 13, 46, 16],
+
+ // 26
+ [10, 142, 114, 2, 143, 115],
+ [19, 74, 46, 4, 75, 47],
+ [28, 50, 22, 6, 51, 23],
+ [33, 46, 16, 4, 47, 17],
+
+ // 27
+ [8, 152, 122, 4, 153, 123],
+ [22, 73, 45, 3, 74, 46],
+ [8, 53, 23, 26, 54, 24],
+ [12, 45, 15, 28, 46, 16],
+
+ // 28
+ [3, 147, 117, 10, 148, 118],
+ [3, 73, 45, 23, 74, 46],
+ [4, 54, 24, 31, 55, 25],
+ [11, 45, 15, 31, 46, 16],
+
+ // 29
+ [7, 146, 116, 7, 147, 117],
+ [21, 73, 45, 7, 74, 46],
+ [1, 53, 23, 37, 54, 24],
+ [19, 45, 15, 26, 46, 16],
+
+ // 30
+ [5, 145, 115, 10, 146, 116],
+ [19, 75, 47, 10, 76, 48],
+ [15, 54, 24, 25, 55, 25],
+ [23, 45, 15, 25, 46, 16],
+
+ // 31
+ [13, 145, 115, 3, 146, 116],
+ [2, 74, 46, 29, 75, 47],
+ [42, 54, 24, 1, 55, 25],
+ [23, 45, 15, 28, 46, 16],
+
+ // 32
+ [17, 145, 115],
+ [10, 74, 46, 23, 75, 47],
+ [10, 54, 24, 35, 55, 25],
+ [19, 45, 15, 35, 46, 16],
+
+ // 33
+ [17, 145, 115, 1, 146, 116],
+ [14, 74, 46, 21, 75, 47],
+ [29, 54, 24, 19, 55, 25],
+ [11, 45, 15, 46, 46, 16],
+
+ // 34
+ [13, 145, 115, 6, 146, 116],
+ [14, 74, 46, 23, 75, 47],
+ [44, 54, 24, 7, 55, 25],
+ [59, 46, 16, 1, 47, 17],
+
+ // 35
+ [12, 151, 121, 7, 152, 122],
+ [12, 75, 47, 26, 76, 48],
+ [39, 54, 24, 14, 55, 25],
+ [22, 45, 15, 41, 46, 16],
+
+ // 36
+ [6, 151, 121, 14, 152, 122],
+ [6, 75, 47, 34, 76, 48],
+ [46, 54, 24, 10, 55, 25],
+ [2, 45, 15, 64, 46, 16],
+
+ // 37
+ [17, 152, 122, 4, 153, 123],
+ [29, 74, 46, 14, 75, 47],
+ [49, 54, 24, 10, 55, 25],
+ [24, 45, 15, 46, 46, 16],
+
+ // 38
+ [4, 152, 122, 18, 153, 123],
+ [13, 74, 46, 32, 75, 47],
+ [48, 54, 24, 14, 55, 25],
+ [42, 45, 15, 32, 46, 16],
+
+ // 39
+ [20, 147, 117, 4, 148, 118],
+ [40, 75, 47, 7, 76, 48],
+ [43, 54, 24, 22, 55, 25],
+ [10, 45, 15, 67, 46, 16],
+
+ // 40
+ [19, 148, 118, 6, 149, 119],
+ [18, 75, 47, 31, 76, 48],
+ [34, 54, 24, 34, 55, 25],
+ [20, 45, 15, 61, 46, 16]
+ ];
+
+ var qrRSBlock = function(totalCount, dataCount) {
+ var _this = {};
+ _this.totalCount = totalCount;
+ _this.dataCount = dataCount;
+ return _this;
+ };
+
+ var _this = {};
+
+ var getRsBlockTable = function(typeNumber, errorCorrectionLevel) {
+ switch (errorCorrectionLevel) {
+ case QRErrorCorrectionLevel.L:
+ return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0];
+ case QRErrorCorrectionLevel.M:
+ return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1];
+ case QRErrorCorrectionLevel.Q:
+ return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2];
+ case QRErrorCorrectionLevel.H:
+ return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3];
+ default:
+ return undefined;
+ }
+ };
+
+ _this.getRSBlocks = function(typeNumber, errorCorrectionLevel) {
+ var rsBlock = getRsBlockTable(typeNumber, errorCorrectionLevel);
+
+ if (typeof rsBlock == 'undefined') {
+ throw 'bad rs block @ typeNumber:' +
+ typeNumber +
+ '/errorCorrectionLevel:' +
+ errorCorrectionLevel;
+ }
+
+ var length = rsBlock.length / 3;
+
+ var list = [];
+
+ for (var i = 0; i < length; i += 1) {
+ var count = rsBlock[i * 3 + 0];
+ var totalCount = rsBlock[i * 3 + 1];
+ var dataCount = rsBlock[i * 3 + 2];
+
+ for (var j = 0; j < count; j += 1) {
+ list.push(qrRSBlock(totalCount, dataCount));
+ }
+ }
+
+ return list;
+ };
+
+ return _this;
+ })();
+
+ //---------------------------------------------------------------------
+ // qrBitBuffer
+ //---------------------------------------------------------------------
+
+ var qrBitBuffer = function() {
+ var _buffer = [];
+ var _length = 0;
+
+ var _this = {};
+
+ _this.getBuffer = function() {
+ return _buffer;
+ };
+
+ _this.getAt = function(index) {
+ var bufIndex = Math.floor(index / 8);
+ return ((_buffer[bufIndex] >>> (7 - (index % 8))) & 1) == 1;
+ };
+
+ _this.put = function(num, length) {
+ for (var i = 0; i < length; i += 1) {
+ _this.putBit(((num >>> (length - i - 1)) & 1) == 1);
+ }
+ };
+
+ _this.getLengthInBits = function() {
+ return _length;
+ };
+
+ _this.putBit = function(bit) {
+ var bufIndex = Math.floor(_length / 8);
+ if (_buffer.length <= bufIndex) {
+ _buffer.push(0);
+ }
+
+ if (bit) {
+ _buffer[bufIndex] |= 0x80 >>> _length % 8;
+ }
+
+ _length += 1;
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // qrNumber
+ //---------------------------------------------------------------------
+
+ var qrNumber = function(data) {
+ var _mode = QRMode.MODE_NUMBER;
+ var _data = data;
+
+ var _this = {};
+
+ _this.getMode = function() {
+ return _mode;
+ };
+
+ _this.getLength = function(buffer) {
+ return _data.length;
+ };
+
+ _this.write = function(buffer) {
+ var data = _data;
+
+ var i = 0;
+
+ while (i + 2 < data.length) {
+ buffer.put(strToNum(data.substring(i, i + 3)), 10);
+ i += 3;
+ }
+
+ if (i < data.length) {
+ if (data.length - i == 1) {
+ buffer.put(strToNum(data.substring(i, i + 1)), 4);
+ } else if (data.length - i == 2) {
+ buffer.put(strToNum(data.substring(i, i + 2)), 7);
+ }
+ }
+ };
+
+ var strToNum = function(s) {
+ var num = 0;
+ for (var i = 0; i < s.length; i += 1) {
+ num = num * 10 + chatToNum(s.charAt(i));
+ }
+ return num;
+ };
+
+ var chatToNum = function(c) {
+ if ('0' <= c && c <= '9') {
+ return c.charCodeAt(0) - '0'.charCodeAt(0);
+ }
+ throw 'illegal char :' + c;
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // qrAlphaNum
+ //---------------------------------------------------------------------
+
+ var qrAlphaNum = function(data) {
+ var _mode = QRMode.MODE_ALPHA_NUM;
+ var _data = data;
+
+ var _this = {};
+
+ _this.getMode = function() {
+ return _mode;
+ };
+
+ _this.getLength = function(buffer) {
+ return _data.length;
+ };
+
+ _this.write = function(buffer) {
+ var s = _data;
+
+ var i = 0;
+
+ while (i + 1 < s.length) {
+ buffer.put(getCode(s.charAt(i)) * 45 + getCode(s.charAt(i + 1)), 11);
+ i += 2;
+ }
+
+ if (i < s.length) {
+ buffer.put(getCode(s.charAt(i)), 6);
+ }
+ };
+
+ var getCode = function(c) {
+ if ('0' <= c && c <= '9') {
+ return c.charCodeAt(0) - '0'.charCodeAt(0);
+ } else if ('A' <= c && c <= 'Z') {
+ return c.charCodeAt(0) - 'A'.charCodeAt(0) + 10;
+ } else {
+ switch (c) {
+ case ' ':
+ return 36;
+ case '$':
+ return 37;
+ case '%':
+ return 38;
+ case '*':
+ return 39;
+ case '+':
+ return 40;
+ case '-':
+ return 41;
+ case '.':
+ return 42;
+ case '/':
+ return 43;
+ case ':':
+ return 44;
+ default:
+ throw 'illegal char :' + c;
+ }
+ }
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // qr8BitByte
+ //---------------------------------------------------------------------
+
+ var qr8BitByte = function(data) {
+ var _mode = QRMode.MODE_8BIT_BYTE;
+ var _data = data;
+ var _bytes = qrcode.stringToBytes(data);
+
+ var _this = {};
+
+ _this.getMode = function() {
+ return _mode;
+ };
+
+ _this.getLength = function(buffer) {
+ return _bytes.length;
+ };
+
+ _this.write = function(buffer) {
+ for (var i = 0; i < _bytes.length; i += 1) {
+ buffer.put(_bytes[i], 8);
+ }
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // qrKanji
+ //---------------------------------------------------------------------
+
+ var qrKanji = function(data) {
+ var _mode = QRMode.MODE_KANJI;
+ var _data = data;
+
+ var stringToBytes = qrcode.stringToBytesFuncs['SJIS'];
+ if (!stringToBytes) {
+ throw 'sjis not supported.';
+ }
+ !(function(c, code) {
+ // self test for sjis support.
+ var test = stringToBytes(c);
+ if (test.length != 2 || ((test[0] << 8) | test[1]) != code) {
+ throw 'sjis not supported.';
+ }
+ })('\u53cb', 0x9746);
+
+ var _bytes = stringToBytes(data);
+
+ var _this = {};
+
+ _this.getMode = function() {
+ return _mode;
+ };
+
+ _this.getLength = function(buffer) {
+ return ~~(_bytes.length / 2);
+ };
+
+ _this.write = function(buffer) {
+ var data = _bytes;
+
+ var i = 0;
+
+ while (i + 1 < data.length) {
+ var c = ((0xff & data[i]) << 8) | (0xff & data[i + 1]);
+
+ if (0x8140 <= c && c <= 0x9ffc) {
+ c -= 0x8140;
+ } else if (0xe040 <= c && c <= 0xebbf) {
+ c -= 0xc140;
+ } else {
+ throw 'illegal char at ' + (i + 1) + '/' + c;
+ }
+
+ c = ((c >>> 8) & 0xff) * 0xc0 + (c & 0xff);
+
+ buffer.put(c, 13);
+
+ i += 2;
+ }
+
+ if (i < data.length) {
+ throw 'illegal char at ' + (i + 1);
+ }
+ };
+
+ return _this;
+ };
+
+ //=====================================================================
+ // GIF Support etc.
+ //
+
+ //---------------------------------------------------------------------
+ // byteArrayOutputStream
+ //---------------------------------------------------------------------
+
+ var byteArrayOutputStream = function() {
+ var _bytes = [];
+
+ var _this = {};
+
+ _this.writeByte = function(b) {
+ _bytes.push(b & 0xff);
+ };
+
+ _this.writeShort = function(i) {
+ _this.writeByte(i);
+ _this.writeByte(i >>> 8);
+ };
+
+ _this.writeBytes = function(b, off, len) {
+ off = off || 0;
+ len = len || b.length;
+ for (var i = 0; i < len; i += 1) {
+ _this.writeByte(b[i + off]);
+ }
+ };
+
+ _this.writeString = function(s) {
+ for (var i = 0; i < s.length; i += 1) {
+ _this.writeByte(s.charCodeAt(i));
+ }
+ };
+
+ _this.toByteArray = function() {
+ return _bytes;
+ };
+
+ _this.toString = function() {
+ var s = '';
+ s += '[';
+ for (var i = 0; i < _bytes.length; i += 1) {
+ if (i > 0) {
+ s += ',';
+ }
+ s += _bytes[i];
+ }
+ s += ']';
+ return s;
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // base64EncodeOutputStream
+ //---------------------------------------------------------------------
+
+ var base64EncodeOutputStream = function() {
+ var _buffer = 0;
+ var _buflen = 0;
+ var _length = 0;
+ var _base64 = '';
+
+ var _this = {};
+
+ var writeEncoded = function(b) {
+ _base64 += String.fromCharCode(encode(b & 0x3f));
+ };
+
+ var encode = function(n) {
+ if (n < 0) {
+ // error.
+ } else if (n < 26) {
+ return 0x41 + n;
+ } else if (n < 52) {
+ return 0x61 + (n - 26);
+ } else if (n < 62) {
+ return 0x30 + (n - 52);
+ } else if (n == 62) {
+ return 0x2b;
+ } else if (n == 63) {
+ return 0x2f;
+ }
+ throw 'n:' + n;
+ };
+
+ _this.writeByte = function(n) {
+ _buffer = (_buffer << 8) | (n & 0xff);
+ _buflen += 8;
+ _length += 1;
+
+ while (_buflen >= 6) {
+ writeEncoded(_buffer >>> (_buflen - 6));
+ _buflen -= 6;
+ }
+ };
+
+ _this.flush = function() {
+ if (_buflen > 0) {
+ writeEncoded(_buffer << (6 - _buflen));
+ _buffer = 0;
+ _buflen = 0;
+ }
+
+ if (_length % 3 != 0) {
+ // padding
+ var padlen = 3 - (_length % 3);
+ for (var i = 0; i < padlen; i += 1) {
+ _base64 += '=';
+ }
+ }
+ };
+
+ _this.toString = function() {
+ return _base64;
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // base64DecodeInputStream
+ //---------------------------------------------------------------------
+
+ var base64DecodeInputStream = function(str) {
+ var _str = str;
+ var _pos = 0;
+ var _buffer = 0;
+ var _buflen = 0;
+
+ var _this = {};
+
+ _this.read = function() {
+ while (_buflen < 8) {
+ if (_pos >= _str.length) {
+ if (_buflen == 0) {
+ return -1;
+ }
+ throw 'unexpected end of file./' + _buflen;
+ }
+
+ var c = _str.charAt(_pos);
+ _pos += 1;
+
+ if (c == '=') {
+ _buflen = 0;
+ return -1;
+ } else if (c.match(/^\s$/)) {
+ // ignore if whitespace.
+ continue;
+ }
+
+ _buffer = (_buffer << 6) | decode(c.charCodeAt(0));
+ _buflen += 6;
+ }
+
+ var n = (_buffer >>> (_buflen - 8)) & 0xff;
+ _buflen -= 8;
+ return n;
+ };
+
+ var decode = function(c) {
+ if (0x41 <= c && c <= 0x5a) {
+ return c - 0x41;
+ } else if (0x61 <= c && c <= 0x7a) {
+ return c - 0x61 + 26;
+ } else if (0x30 <= c && c <= 0x39) {
+ return c - 0x30 + 52;
+ } else if (c == 0x2b) {
+ return 62;
+ } else if (c == 0x2f) {
+ return 63;
+ } else {
+ throw 'c:' + c;
+ }
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // gifImage (B/W)
+ //---------------------------------------------------------------------
+
+ var gifImage = function(width, height) {
+ var _width = width;
+ var _height = height;
+ var _data = new Array(width * height);
+
+ var _this = {};
+
+ _this.setPixel = function(x, y, pixel) {
+ _data[y * _width + x] = pixel;
+ };
+
+ _this.write = function(out) {
+ //---------------------------------
+ // GIF Signature
+
+ out.writeString('GIF87a');
+
+ //---------------------------------
+ // Screen Descriptor
+
+ out.writeShort(_width);
+ out.writeShort(_height);
+
+ out.writeByte(0x80); // 2bit
+ out.writeByte(0);
+ out.writeByte(0);
+
+ //---------------------------------
+ // Global Color Map
+
+ // black
+ out.writeByte(0x00);
+ out.writeByte(0x00);
+ out.writeByte(0x00);
+
+ // white
+ out.writeByte(0xff);
+ out.writeByte(0xff);
+ out.writeByte(0xff);
+
+ //---------------------------------
+ // Image Descriptor
+
+ out.writeString(',');
+ out.writeShort(0);
+ out.writeShort(0);
+ out.writeShort(_width);
+ out.writeShort(_height);
+ out.writeByte(0);
+
+ //---------------------------------
+ // Local Color Map
+
+ //---------------------------------
+ // Raster Data
+
+ var lzwMinCodeSize = 2;
+ var raster = getLZWRaster(lzwMinCodeSize);
+
+ out.writeByte(lzwMinCodeSize);
+
+ var offset = 0;
+
+ while (raster.length - offset > 255) {
+ out.writeByte(255);
+ out.writeBytes(raster, offset, 255);
+ offset += 255;
+ }
+
+ out.writeByte(raster.length - offset);
+ out.writeBytes(raster, offset, raster.length - offset);
+ out.writeByte(0x00);
+
+ //---------------------------------
+ // GIF Terminator
+ out.writeString(';');
+ };
+
+ var bitOutputStream = function(out) {
+ var _out = out;
+ var _bitLength = 0;
+ var _bitBuffer = 0;
+
+ var _this = {};
+
+ _this.write = function(data, length) {
+ if (data >>> length != 0) {
+ throw 'length over';
+ }
+
+ while (_bitLength + length >= 8) {
+ _out.writeByte(0xff & ((data << _bitLength) | _bitBuffer));
+ length -= 8 - _bitLength;
+ data >>>= 8 - _bitLength;
+ _bitBuffer = 0;
+ _bitLength = 0;
+ }
+
+ _bitBuffer = (data << _bitLength) | _bitBuffer;
+ _bitLength = _bitLength + length;
+ };
+
+ _this.flush = function() {
+ if (_bitLength > 0) {
+ _out.writeByte(_bitBuffer);
+ }
+ };
+
+ return _this;
+ };
+
+ var getLZWRaster = function(lzwMinCodeSize) {
+ var clearCode = 1 << lzwMinCodeSize;
+ var endCode = (1 << lzwMinCodeSize) + 1;
+ var bitLength = lzwMinCodeSize + 1;
+
+ // Setup LZWTable
+ var table = lzwTable();
+
+ for (var i = 0; i < clearCode; i += 1) {
+ table.add(String.fromCharCode(i));
+ }
+ table.add(String.fromCharCode(clearCode));
+ table.add(String.fromCharCode(endCode));
+
+ var byteOut = byteArrayOutputStream();
+ var bitOut = bitOutputStream(byteOut);
+
+ // clear code
+ bitOut.write(clearCode, bitLength);
+
+ var dataIndex = 0;
+
+ var s = String.fromCharCode(_data[dataIndex]);
+ dataIndex += 1;
+
+ while (dataIndex < _data.length) {
+ var c = String.fromCharCode(_data[dataIndex]);
+ dataIndex += 1;
+
+ if (table.contains(s + c)) {
+ s = s + c;
+ } else {
+ bitOut.write(table.indexOf(s), bitLength);
+
+ if (table.size() < 0xfff) {
+ if (table.size() == 1 << bitLength) {
+ bitLength += 1;
+ }
+
+ table.add(s + c);
+ }
+
+ s = c;
+ }
+ }
+
+ bitOut.write(table.indexOf(s), bitLength);
+
+ // end code
+ bitOut.write(endCode, bitLength);
+
+ bitOut.flush();
+
+ return byteOut.toByteArray();
+ };
+
+ var lzwTable = function() {
+ var _map = {};
+ var _size = 0;
+
+ var _this = {};
+
+ _this.add = function(key) {
+ if (_this.contains(key)) {
+ throw 'dup key:' + key;
+ }
+ _map[key] = _size;
+ _size += 1;
+ };
+
+ _this.size = function() {
+ return _size;
+ };
+
+ _this.indexOf = function(key) {
+ return _map[key];
+ };
+
+ _this.contains = function(key) {
+ return typeof _map[key] != 'undefined';
+ };
+
+ return _this;
+ };
+
+ return _this;
+ };
+
+ var createDataURL = function(width, height, getPixel) {
+ var gif = gifImage(width, height);
+ for (var y = 0; y < height; y += 1) {
+ for (var x = 0; x < width; x += 1) {
+ gif.setPixel(x, y, getPixel(x, y));
+ }
+ }
+
+ var b = byteArrayOutputStream();
+ gif.write(b);
+
+ var base64 = base64EncodeOutputStream();
+ var bytes = b.toByteArray();
+ for (var i = 0; i < bytes.length; i += 1) {
+ base64.writeByte(bytes[i]);
+ }
+ base64.flush();
+
+ return 'data:image/gif;base64,' + base64;
+ };
+
+ //---------------------------------------------------------------------
+ // returns qrcode function.
+
+ return qrcode;
+})();
+
+// multibyte support
+!(function() {
+ qrcode.stringToBytesFuncs['UTF-8'] = function(s) {
+ // http://stackoverflow.com/questions/18729405/how-to-convert-utf8-string-to-byte-array
+ function toUTF8Array(str) {
+ var utf8 = [];
+ for (var i = 0; i < str.length; i++) {
+ var charcode = str.charCodeAt(i);
+ if (charcode < 0x80) utf8.push(charcode);
+ else if (charcode < 0x800) {
+ utf8.push(0xc0 | (charcode >> 6), 0x80 | (charcode & 0x3f));
+ } else if (charcode < 0xd800 || charcode >= 0xe000) {
+ utf8.push(
+ 0xe0 | (charcode >> 12),
+ 0x80 | ((charcode >> 6) & 0x3f),
+ 0x80 | (charcode & 0x3f)
+ );
+ }
+ // surrogate pair
+ else {
+ i++;
+ // UTF-16 encodes 0x10000-0x10FFFF by
+ // subtracting 0x10000 and splitting the
+ // 20 bits of 0x0-0xFFFFF into two halves
+ charcode =
+ 0x10000 +
+ (((charcode & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff));
+ utf8.push(
+ 0xf0 | (charcode >> 18),
+ 0x80 | ((charcode >> 12) & 0x3f),
+ 0x80 | ((charcode >> 6) & 0x3f),
+ 0x80 | (charcode & 0x3f)
+ );
+ }
+ }
+ return utf8;
+ }
+ return toUTF8Array(s);
+ };
+})();
+
+(function(factory) {
+ if (typeof define === 'function' && define.amd) {
+ define([], factory);
+ } else if (typeof exports === 'object') {
+ module.exports = factory();
+ }
+})(function() {
+ return qrcode;
+});
diff --git a/app/readme.md b/app/readme.md
new file mode 100644
index 00000000..b80e5246
--- /dev/null
+++ b/app/readme.md
@@ -0,0 +1,9 @@
+# Application Code
+
+`app/` contains the browser code that gets bundled into `app.[hash].js`. It's got all the logic, crypto, and UI. All of it gets used in the browser, and some of it by the server for server side rendering.
+
+The main entrypoint for the browser is [main.js](./main.js) and on the server [routes.js](./routes.js) is imported by [/server/routes/pages.js](../server/routes/pages.js)
+
+- `pages` contains display logic an markup for pages
+- `routes` contains route definitions and logic
+- `templates` contains ui elements smaller than pages
diff --git a/app/routes.js b/app/routes.js
new file mode 100644
index 00000000..6a259710
--- /dev/null
+++ b/app/routes.js
@@ -0,0 +1,21 @@
+const choo = require('choo');
+const download = require('./ui/download');
+const body = require('./ui/body');
+
+module.exports = function(app = choo({ hash: true })) {
+ app.route('/', body(require('./ui/home')));
+ app.route('/download/:id', body(download));
+ app.route('/download/:id/:key', body(download));
+ app.route('/unsupported/:reason', body(require('./ui/unsupported')));
+ app.route('/error', body(require('./ui/error')));
+ app.route('/blank', body(require('./ui/blank')));
+ app.route('/oauth', function(state, emit) {
+ emit('authenticate', state.query.code, state.query.state);
+ });
+ app.route('/login', function(state, emit) {
+ emit('replaceState', '/');
+ setTimeout(() => emit('render'));
+ });
+ app.route('*', body(require('./ui/notFound')));
+ return app;
+};
diff --git a/app/routes/download.js b/app/routes/download.js
deleted file mode 100644
index c0a47a4c..00000000
--- a/app/routes/download.js
+++ /dev/null
@@ -1,12 +0,0 @@
-const preview = require('../templates/preview');
-const download = require('../templates/download');
-
-module.exports = function(state, emit) {
- if (state.transfer) {
- const s = state.transfer.state;
- if (s === 'downloading' || s === 'complete') {
- return download(state, emit);
- }
- }
- return preview(state, emit);
-};
diff --git a/app/routes/home.js b/app/routes/home.js
deleted file mode 100644
index 0059ceb0..00000000
--- a/app/routes/home.js
+++ /dev/null
@@ -1,10 +0,0 @@
-const welcome = require('../templates/welcome');
-const upload = require('../templates/upload');
-
-module.exports = function(state, emit) {
- if (state.transfer && state.transfer.iv) {
- //TODO relying on 'iv' is gross
- return upload(state, emit);
- }
- return welcome(state, emit);
-};
diff --git a/app/routes/index.js b/app/routes/index.js
deleted file mode 100644
index 12b33c1d..00000000
--- a/app/routes/index.js
+++ /dev/null
@@ -1,43 +0,0 @@
-const choo = require('choo');
-const html = require('choo/html');
-const download = require('./download');
-const header = require('../templates/header');
-const footer = require('../templates/footer');
-const fxPromo = require('../templates/fxPromo');
-
-const app = choo();
-
-function body(template) {
- return function(state, emit) {
- const b = html`
- ${state.promo === 'header' ? fxPromo(state, emit) : ''}
- ${header(state)}
-
- ${footer(state)}
- `;
- if (state.layout) {
- return state.layout(state, b);
- }
- return b;
- };
-}
-
-app.route('/', body(require('./home')));
-app.route('/share/:id', body(require('../templates/share')));
-app.route('/download/:id', body(download));
-app.route('/download/:id/:key', body(download));
-app.route('/completed', body(require('../templates/completed')));
-app.route('/unsupported/:reason', body(require('../templates/unsupported')));
-app.route('/legal', body(require('../templates/legal')));
-app.route('/error', body(require('../templates/error')));
-app.route('/blank', body(require('../templates/blank')));
-app.route('*', body(require('../templates/notFound')));
-
-module.exports = app;
diff --git a/app/serviceWorker.js b/app/serviceWorker.js
new file mode 100644
index 00000000..34ae25b2
--- /dev/null
+++ b/app/serviceWorker.js
@@ -0,0 +1,174 @@
+import assets from '../common/assets';
+import { version } from '../package.json';
+import Keychain from './keychain';
+import { downloadStream } from './api';
+import { transformStream } from './streams';
+import Zip from './zip';
+import contentDisposition from 'content-disposition';
+
+let noSave = false;
+const map = new Map();
+const IMAGES = /.*\.(png|svg|jpg)$/;
+const VERSIONED_ASSET = /\.[A-Fa-f0-9]{8}\.(js|css|png|svg|jpg)(#\w+)?$/;
+const DOWNLOAD_URL = /\/api\/download\/([A-Fa-f0-9]{4,})/;
+const FONT = /\.woff2?$/;
+
+self.addEventListener('install', () => {
+ self.skipWaiting();
+});
+
+self.addEventListener('activate', event => {
+ event.waitUntil(self.clients.claim().then(precache));
+});
+
+async function decryptStream(id) {
+ const file = map.get(id);
+ if (!file) {
+ return new Response(null, { status: 400 });
+ }
+ try {
+ let size = file.size;
+ let type = file.type;
+ const keychain = new Keychain(file.key, file.nonce);
+ if (file.requiresPassword) {
+ keychain.setPassword(file.password, file.url);
+ }
+
+ file.download = downloadStream(id, keychain);
+
+ const body = await file.download.result;
+
+ const decrypted = keychain.decryptStream(body);
+
+ let zipStream = null;
+ if (file.type === 'send-archive') {
+ const zip = new Zip(file.manifest, decrypted);
+ zipStream = zip.stream;
+ type = 'application/zip';
+ size = zip.size;
+ }
+ const responseStream = transformStream(
+ zipStream || decrypted,
+ {
+ transform(chunk, controller) {
+ file.progress += chunk.length;
+ controller.enqueue(chunk);
+ }
+ },
+ function oncancel() {
+ // NOTE: cancel doesn't currently fire on chrome
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=638494
+ file.download.cancel();
+ map.delete(id);
+ }
+ );
+
+ const headers = {
+ 'Content-Disposition': contentDisposition(file.filename),
+ 'Content-Type': type,
+ 'Content-Length': size
+ };
+ return new Response(responseStream, { headers });
+ } catch (e) {
+ if (noSave) {
+ return new Response(null, { status: e.message });
+ }
+
+ return new Response(null, {
+ status: 302,
+ headers: {
+ Location: `/download/${id}/#${file.key}`
+ }
+ });
+ }
+}
+
+async function precache() {
+ try {
+ await cleanCache();
+ const cache = await caches.open(version);
+ const images = assets.match(IMAGES);
+ await cache.addAll(images);
+ } catch (e) {
+ console.error(e);
+ // cache will get populated on demand
+ }
+}
+
+async function cleanCache() {
+ const oldCaches = await caches.keys();
+ for (const c of oldCaches) {
+ if (c !== version) {
+ await caches.delete(c);
+ }
+ }
+}
+
+function cacheable(url) {
+ return VERSIONED_ASSET.test(url) || FONT.test(url);
+}
+
+async function cachedOrFetched(req) {
+ const cache = await caches.open(version);
+ const cached = await cache.match(req);
+ if (cached) {
+ return cached;
+ }
+ const fetched = await fetch(req);
+ if (fetched.ok && cacheable(req.url)) {
+ cache.put(req, fetched.clone());
+ }
+ return fetched;
+}
+
+self.onfetch = event => {
+ const req = event.request;
+ if (req.method !== 'GET') return;
+ const url = new URL(req.url);
+ const dlmatch = DOWNLOAD_URL.exec(url.pathname);
+ if (dlmatch) {
+ event.respondWith(decryptStream(dlmatch[1]));
+ } else if (cacheable(url.pathname)) {
+ event.respondWith(cachedOrFetched(req));
+ }
+};
+
+self.onmessage = event => {
+ if (event.data.request === 'init') {
+ noSave = event.data.noSave;
+ const info = {
+ key: event.data.key,
+ nonce: event.data.nonce,
+ filename: event.data.filename,
+ requiresPassword: event.data.requiresPassword,
+ password: event.data.password,
+ url: event.data.url,
+ type: event.data.type,
+ manifest: event.data.manifest,
+ size: event.data.size,
+ progress: 0
+ };
+ map.set(event.data.id, info);
+
+ event.ports[0].postMessage('file info received');
+ } else if (event.data.request === 'progress') {
+ const file = map.get(event.data.id);
+ if (!file) {
+ event.ports[0].postMessage({ error: 'cancelled' });
+ } else {
+ if (file.progress === file.size) {
+ map.delete(event.data.id);
+ }
+ event.ports[0].postMessage({ progress: file.progress });
+ }
+ } else if (event.data.request === 'cancel') {
+ const file = map.get(event.data.id);
+ if (file) {
+ if (file.download) {
+ file.download.cancel();
+ }
+ map.delete(event.data.id);
+ }
+ event.ports[0].postMessage('download cancelled');
+ }
+};
diff --git a/app/storage.js b/app/storage.js
index 27cba1cb..304759ea 100644
--- a/app/storage.js
+++ b/app/storage.js
@@ -1,4 +1,5 @@
-import { isFile } from './utils';
+import { arrayToB64, isFile } from './utils';
+import OwnedFile from './ownedFile';
class Mem {
constructor() {
@@ -37,23 +38,33 @@ class Storage {
}
loadFiles() {
- const fs = [];
+ const fs = new Map();
for (let i = 0; i < this.engine.length; i++) {
const k = this.engine.key(i);
if (isFile(k)) {
try {
- const f = JSON.parse(this.engine.getItem(k));
+ const f = new OwnedFile(JSON.parse(this.engine.getItem(k)));
if (!f.id) {
f.id = f.fileId;
}
- fs.push(f);
+
+ fs.set(f.id, f);
} catch (err) {
// obviously you're not a golfer
this.engine.removeItem(k);
}
}
}
- return fs.sort((a, b) => a.createdAt - b.createdAt);
+ return fs;
+ }
+
+ get id() {
+ let id = this.engine.getItem('device_id');
+ if (!id) {
+ id = arrayToB64(crypto.getRandomValues(new Uint8Array(16)));
+ this.engine.setItem('device_id', id);
+ }
+ return id;
}
get totalDownloads() {
@@ -75,44 +86,99 @@ class Storage {
this.engine.setItem('referrer', str);
}
get enrolled() {
- return JSON.parse(this.engine.getItem('experiments') || '[]');
+ return JSON.parse(this.engine.getItem('ab_experiments') || '{}');
}
enroll(id, variant) {
- const enrolled = this.enrolled;
- // eslint-disable-next-line no-unused-vars
- if (!enrolled.find(([i, v]) => i === id)) {
- enrolled.push([id, variant]);
- this.engine.setItem('experiments', JSON.stringify(enrolled));
- }
+ const enrolled = {};
+ enrolled[id] = variant;
+ this.engine.setItem('ab_experiments', JSON.stringify(enrolled));
}
get files() {
- return this._files;
+ return Array.from(this._files.values()).sort(
+ (a, b) => a.createdAt - b.createdAt
+ );
+ }
+
+ get user() {
+ try {
+ return JSON.parse(this.engine.getItem('user'));
+ } catch (e) {
+ return null;
+ }
+ }
+
+ set user(info) {
+ return this.engine.setItem('user', JSON.stringify(info));
}
getFileById(id) {
- return this._files.find(f => f.id === id);
+ return this._files.get(id);
}
get(id) {
return this.engine.getItem(id);
}
+ set(id, value) {
+ return this.engine.setItem(id, value);
+ }
+
remove(property) {
if (isFile(property)) {
- this._files.splice(this._files.findIndex(f => f.id === property), 1);
+ this._files.delete(property);
}
this.engine.removeItem(property);
}
addFile(file) {
- this._files.push(file);
+ this._files.set(file.id, file);
+ this.writeFile(file);
+ }
+
+ writeFile(file) {
this.engine.setItem(file.id, JSON.stringify(file));
}
writeFiles() {
- this._files.forEach(f => this.engine.setItem(f.id, JSON.stringify(f)));
+ this._files.forEach(f => this.writeFile(f));
+ }
+
+ clearLocalFiles() {
+ this._files.forEach(f => this.engine.removeItem(f.id));
+ this._files = new Map();
+ }
+
+ async merge(files = []) {
+ let incoming = false;
+ let outgoing = false;
+ let downloadCount = false;
+ for (const f of files) {
+ if (!this.getFileById(f.id)) {
+ this.addFile(new OwnedFile(f));
+ incoming = true;
+ }
+ }
+ const workingFiles = this.files.slice();
+ for (const f of workingFiles) {
+ const cc = await f.updateDownloadCount();
+ if (cc) {
+ await this.writeFile(f);
+ }
+ downloadCount = downloadCount || cc;
+ outgoing = outgoing || f.expired;
+ if (f.expired) {
+ this.remove(f.id);
+ } else if (!files.find(x => x.id === f.id)) {
+ outgoing = true;
+ }
+ }
+ return {
+ incoming,
+ outgoing,
+ downloadCount
+ };
}
}
diff --git a/app/streams.js b/app/streams.js
new file mode 100644
index 00000000..00159a24
--- /dev/null
+++ b/app/streams.js
@@ -0,0 +1,103 @@
+/* global TransformStream */
+
+export function transformStream(readable, transformer, oncancel) {
+ try {
+ return readable.pipeThrough(new TransformStream(transformer));
+ } catch (e) {
+ const reader = readable.getReader();
+ return new ReadableStream({
+ start(controller) {
+ if (transformer.start) {
+ return transformer.start(controller);
+ }
+ },
+ async pull(controller) {
+ let enqueued = false;
+ const wrappedController = {
+ enqueue(d) {
+ enqueued = true;
+ controller.enqueue(d);
+ }
+ };
+ while (!enqueued) {
+ const data = await reader.read();
+ if (data.done) {
+ if (transformer.flush) {
+ await transformer.flush(controller);
+ }
+ return controller.close();
+ }
+ await transformer.transform(data.value, wrappedController);
+ }
+ },
+ cancel(reason) {
+ readable.cancel(reason);
+ if (oncancel) {
+ oncancel(reason);
+ }
+ }
+ });
+ }
+}
+
+class BlobStreamController {
+ constructor(blob, size) {
+ this.blob = blob;
+ this.index = 0;
+ this.chunkSize = size || 1024 * 64;
+ }
+
+ pull(controller) {
+ return new Promise((resolve, reject) => {
+ const bytesLeft = this.blob.size - this.index;
+ if (bytesLeft <= 0) {
+ controller.close();
+ return resolve();
+ }
+ const size = Math.min(this.chunkSize, bytesLeft);
+ const slice = this.blob.slice(this.index, this.index + size);
+ const reader = new FileReader();
+ reader.onload = () => {
+ controller.enqueue(new Uint8Array(reader.result));
+ resolve();
+ };
+ reader.onerror = reject;
+ reader.readAsArrayBuffer(slice);
+ this.index += size;
+ });
+ }
+}
+
+export function blobStream(blob, size) {
+ return new ReadableStream(new BlobStreamController(blob, size));
+}
+
+class ConcatStreamController {
+ constructor(streams) {
+ this.streams = streams;
+ this.index = 0;
+ this.reader = null;
+ this.nextReader();
+ }
+
+ nextReader() {
+ const next = this.streams[this.index++];
+ this.reader = next && next.getReader();
+ }
+
+ async pull(controller) {
+ if (!this.reader) {
+ return controller.close();
+ }
+ const data = await this.reader.read();
+ if (data.done) {
+ this.nextReader();
+ return this.pull(controller);
+ }
+ controller.enqueue(data.value);
+ }
+}
+
+export function concatStream(streams) {
+ return new ReadableStream(new ConcatStreamController(streams));
+}
diff --git a/app/templates/blank.js b/app/templates/blank.js
deleted file mode 100644
index 080a3232..00000000
--- a/app/templates/blank.js
+++ /dev/null
@@ -1,6 +0,0 @@
-const html = require('choo/html');
-
-module.exports = function() {
- const div = html`
`;
- return div;
-};
diff --git a/app/templates/completed.js b/app/templates/completed.js
deleted file mode 100644
index 751b9de7..00000000
--- a/app/templates/completed.js
+++ /dev/null
@@ -1,35 +0,0 @@
-const html = require('choo/html');
-const progress = require('./progress');
-const { fadeOut } = require('../utils');
-const fxPromo = require('./fxPromo');
-
-module.exports = function(state, emit) {
- const div = html`
-
-
- ${state.promo === 'body' ? fxPromo(state, emit) : ''}
-
- `;
-
- async function sendNew(e) {
- e.preventDefault();
- await fadeOut('download');
- emit('pushState', '/');
- }
-
- return div;
-};
diff --git a/app/templates/download.js b/app/templates/download.js
deleted file mode 100644
index 7551eb5f..00000000
--- a/app/templates/download.js
+++ /dev/null
@@ -1,32 +0,0 @@
-const html = require('choo/html');
-const progress = require('./progress');
-const { bytes } = require('../utils');
-const fxPromo = require('./fxPromo');
-
-module.exports = function(state, emit) {
- const transfer = state.transfer;
- const div = html`
-
-
-
${state.translate(
- 'downloadingPageProgress',
- {
- filename: state.fileInfo.name,
- size: bytes(state.fileInfo.size)
- }
- )}
-
${state.translate('downloadingPageMessage')}
- ${progress(transfer.progressRatio)}
-
-
${state.translate(
- transfer.msg,
- transfer.sizes
- )}
-
-
- ${state.promo === 'body' ? fxPromo(state, emit) : ''}
-
- `;
-
- return div;
-};
diff --git a/app/templates/downloadPassword.js b/app/templates/downloadPassword.js
deleted file mode 100644
index 4ce13608..00000000
--- a/app/templates/downloadPassword.js
+++ /dev/null
@@ -1,56 +0,0 @@
-const html = require('choo/html');
-
-module.exports = function(state, emit) {
- const fileInfo = state.fileInfo;
- const label =
- fileInfo.password === null
- ? html`
-
`
- : html`
-
`;
- const div = html`
-
- ${label}
-
-
`;
-
- function inputChanged() {
- const input = document.getElementById('unlock-input');
- const btn = document.getElementById('unlock-btn');
- if (input.value.length > 0) {
- btn.classList.remove('btn-hidden');
- input.classList.remove('input-no-btn');
- } else {
- btn.classList.add('btn-hidden');
- input.classList.add('input-no-btn');
- }
- }
-
- function checkPassword(event) {
- event.preventDefault();
- const password = document.getElementById('unlock-input').value;
- if (password.length > 0) {
- document.getElementById('unlock-btn').disabled = true;
- state.fileInfo.url = window.location.href;
- state.fileInfo.password = password;
- emit('preview');
- }
- }
-
- return div;
-};
diff --git a/app/templates/error.js b/app/templates/error.js
deleted file mode 100644
index f7751faa..00000000
--- a/app/templates/error.js
+++ /dev/null
@@ -1,10 +0,0 @@
-const html = require('choo/html');
-const assets = require('../../common/assets');
-
-module.exports = function(state) {
- return html`
-
-
${state.translate('errorPageHeader')}
-
})
-
`;
-};
diff --git a/app/templates/file.js b/app/templates/file.js
deleted file mode 100644
index 7fc665e9..00000000
--- a/app/templates/file.js
+++ /dev/null
@@ -1,84 +0,0 @@
-const html = require('choo/html');
-const assets = require('../../common/assets');
-
-function timeLeft(milliseconds) {
- const minutes = Math.floor(milliseconds / 1000 / 60);
- const hours = Math.floor(minutes / 60);
- const seconds = Math.floor((milliseconds / 1000) % 60);
- if (hours >= 1) {
- return `${hours}h ${minutes % 60}m`;
- } else if (hours === 0) {
- return `${minutes}m ${seconds}s`;
- }
- return null;
-}
-
-module.exports = function(file, state, emit) {
- const ttl = file.expiresAt - Date.now();
- const remaining = timeLeft(ttl) || state.translate('linkExpiredAlt');
- const row = html`
-
- ${file.name} |
-
-
- ${state.translate(
- 'copiedUrl'
- )}
- |
- ${remaining} |
-
-
-
- |
-
- `;
-
- function copyClick(e) {
- emit('copy', { url: file.url, location: 'upload-list' });
- const icon = e.target;
- const text = e.target.nextSibling;
- icon.hidden = true;
- text.hidden = false;
- setTimeout(() => {
- icon.hidden = false;
- text.hidden = true;
- }, 500);
- }
-
- function showPopup() {
- const tr = document.getElementById(file.id);
- const popup = tr.querySelector('.popuptext');
- popup.classList.add('show');
- popup.focus();
- }
-
- function cancel(e) {
- e.stopPropagation();
- const tr = document.getElementById(file.id);
- const popup = tr.querySelector('.popuptext');
- popup.classList.remove('show');
- }
-
- function deleteFile() {
- emit('delete', { file, location: 'upload-list' });
- emit('render');
- }
-
- return row;
-};
diff --git a/app/templates/fileList.js b/app/templates/fileList.js
deleted file mode 100644
index eed2465f..00000000
--- a/app/templates/fileList.js
+++ /dev/null
@@ -1,32 +0,0 @@
-const html = require('choo/html');
-const file = require('./file');
-
-module.exports = function(state, emit) {
- let table = '';
- if (state.storage.files.length) {
- table = html`
-
-
-
- ${state.translate('uploadedFile')} |
- ${state.translate(
- 'copyFileList'
- )} |
- ${state.translate('expiryFileList')} |
- ${state.translate(
- 'deleteFileList'
- )} |
-
-
-
- ${state.storage.files.map(f => file(f, state, emit))}
-
-
- `;
- }
- return html`
-
- ${table}
-
- `;
-};
diff --git a/app/templates/footer.js b/app/templates/footer.js
deleted file mode 100644
index 795cdfda..00000000
--- a/app/templates/footer.js
+++ /dev/null
@@ -1,31 +0,0 @@
-const html = require('choo/html');
-const assets = require('../../common/assets');
-
-module.exports = function(state) {
- return html``;
-};
diff --git a/app/templates/fxPromo.js b/app/templates/fxPromo.js
deleted file mode 100644
index 5098314d..00000000
--- a/app/templates/fxPromo.js
+++ /dev/null
@@ -1,44 +0,0 @@
-const html = require('choo/html');
-const assets = require('../../common/assets');
-
-// function replaceLinks(str, urls) {
-// let i = -1;
-// const s = str.replace(/
([^<]+)<\/a>/g, (m, v) => {
-// i++;
-// return `${v}`;
-// });
-// return [`
${s}`];
-// }
-
-module.exports = function(state, emit) {
- // function close() {
- // document.querySelector('.banner').remove();
- // }
-
- function clicked(evt) {
- emit('exit', evt);
- }
-
- return html`
-
`;
-};
-
-/*
-
})
-*/
diff --git a/app/templates/header.js b/app/templates/header.js
deleted file mode 100644
index edd39d7a..00000000
--- a/app/templates/header.js
+++ /dev/null
@@ -1,21 +0,0 @@
-const html = require('choo/html');
-const assets = require('../../common/assets');
-
-module.exports = function(state) {
- return html``;
-};
diff --git a/app/templates/legal.js b/app/templates/legal.js
deleted file mode 100644
index af196ab9..00000000
--- a/app/templates/legal.js
+++ /dev/null
@@ -1,34 +0,0 @@
-const html = require('choo/html');
-
-function replaceLinks(str, urls) {
- let i = -1;
- const s = str.replace(/
([^<]+)<\/a>/g, (m, v) => {
- i++;
- return `${v}`;
- });
- return [`
${s}
`];
-}
-
-module.exports = function(state) {
- const div = html`
-
-
-
${state.translate('legalHeader')}
- ${html(
- replaceLinks(state.translate('legalNoticeTestPilot'), [
- 'https://testpilot.firefox.com/terms',
- 'https://testpilot.firefox.com/privacy',
- 'https://testpilot.firefox.com/experiments/send'
- ])
- )}
- ${html(
- replaceLinks(state.translate('legalNoticeMozilla'), [
- 'https://www.mozilla.org/privacy/websites/',
- 'https://www.mozilla.org/about/legal/terms/mozilla/'
- ])
- )}
-
-
- `;
- return div;
-};
diff --git a/app/templates/notFound.js b/app/templates/notFound.js
deleted file mode 100644
index c29bf194..00000000
--- a/app/templates/notFound.js
+++ /dev/null
@@ -1,21 +0,0 @@
-const html = require('choo/html');
-const assets = require('../../common/assets');
-
-module.exports = function(state) {
- const div = html`
-
`;
- return div;
-};
diff --git a/app/templates/preview.js b/app/templates/preview.js
deleted file mode 100644
index cb5352ac..00000000
--- a/app/templates/preview.js
+++ /dev/null
@@ -1,74 +0,0 @@
-const html = require('choo/html');
-const assets = require('../../common/assets');
-const notFound = require('./notFound');
-const downloadPassword = require('./downloadPassword');
-const { bytes } = require('../utils');
-const fxPromo = require('./fxPromo');
-
-function getFileFromDOM() {
- const el = document.getElementById('dl-file');
- if (!el) {
- return null;
- }
- return {
- nonce: el.getAttribute('data-nonce'),
- pwd: !!+el.getAttribute('data-requires-password')
- };
-}
-
-module.exports = function(state, emit) {
- state.fileInfo = state.fileInfo || getFileFromDOM();
- if (!state.fileInfo) {
- return notFound(state, emit);
- }
- state.fileInfo.id = state.params.id;
- state.fileInfo.key = state.params.key;
- const fileInfo = state.fileInfo;
- const size = fileInfo.size
- ? state.translate('downloadFileSize', { size: bytes(fileInfo.size) })
- : '';
- let action = html`
-
-
})
-
-
-
-
`;
- if (fileInfo.pwd && !fileInfo.password) {
- action = downloadPassword(state, emit);
- } else if (!state.transfer) {
- emit('preview');
- }
- const title = fileInfo.name
- ? state.translate('downloadFileName', { filename: fileInfo.name })
- : state.translate('downloadFileTitle');
- const div = html`
-
-
- ${state.promo === 'body' ? fxPromo(state, emit) : ''}
-
- `;
-
- function download(event) {
- event.preventDefault();
- emit('download', fileInfo);
- }
- return div;
-};
diff --git a/app/templates/progress.js b/app/templates/progress.js
deleted file mode 100644
index 997e05ba..00000000
--- a/app/templates/progress.js
+++ /dev/null
@@ -1,29 +0,0 @@
-const html = require('choo/html');
-
-const radius = 73;
-const oRadius = radius + 10;
-const oDiameter = oRadius * 2;
-const circumference = 2 * Math.PI * radius;
-
-module.exports = function(progressRatio) {
- const dashOffset = (1 - progressRatio) * circumference;
- const percent = Math.floor(progressRatio * 100);
- const div = html`
-
-
-
- `;
- return div;
-};
diff --git a/app/templates/share.js b/app/templates/share.js
deleted file mode 100644
index 3efeb6bd..00000000
--- a/app/templates/share.js
+++ /dev/null
@@ -1,90 +0,0 @@
-const html = require('choo/html');
-const assets = require('../../common/assets');
-const notFound = require('./notFound');
-const uploadPassword = require('./uploadPassword');
-const { allowedCopy, delay, fadeOut } = require('../utils');
-
-function passwordComplete(state, password) {
- const el = html([
- `
${state.translate('passwordResult', {
- password: '
'
- })}
`
- ]);
- el.lastElementChild.textContent = password;
- return el;
-}
-
-module.exports = function(state, emit) {
- const file = state.storage.getFileById(state.params.id);
- if (!file) {
- return notFound(state, emit);
- }
-
- file.password = file.password || '';
-
- const passwordSection = file.password
- ? passwordComplete(state, file.password)
- : uploadPassword(state, emit);
- const div = html`
-
-
${state.translate('uploadSuccessTimingHeader')}
-
-
- ${state.translate('copyUrlFormLabelWithName', {
- filename: file.name
- })}
-
-
-
-
- ${passwordSection}
-
-
${state.translate('sendAnotherFileLink')}
-
-
- `;
-
- async function sendNew(e) {
- e.preventDefault();
- await fadeOut('share-link');
- emit('pushState', '/');
- }
-
- async function copyLink() {
- if (allowedCopy()) {
- emit('copy', { url: file.url, location: 'success-screen' });
- const input = document.getElementById('link');
- input.disabled = true;
- const copyBtn = document.getElementById('copy-btn');
- copyBtn.disabled = true;
- copyBtn.classList.add('success');
- copyBtn.replaceChild(
- html`
})
`,
- copyBtn.firstChild
- );
- await delay(2000);
- input.disabled = false;
- if (!copyBtn.parentNode.classList.contains('wait-password')) {
- copyBtn.disabled = false;
- }
- copyBtn.classList.remove('success');
- copyBtn.textContent = state.translate('copyUrlFormButton');
- }
- }
-
- async function deleteFile() {
- emit('delete', { file, location: 'success-screen' });
- await fadeOut('share-link');
- emit('pushState', '/');
- }
- return div;
-};
diff --git a/app/templates/unsupported.js b/app/templates/unsupported.js
deleted file mode 100644
index f37c9bd5..00000000
--- a/app/templates/unsupported.js
+++ /dev/null
@@ -1,46 +0,0 @@
-const html = require('choo/html');
-const assets = require('../../common/assets');
-
-module.exports = function(state) {
- const msg =
- state.params.reason === 'outdated'
- ? html`
-
`
- : html`
-
`;
- const div = html`
${msg}
`;
- return div;
-};
diff --git a/app/templates/upload.js b/app/templates/upload.js
deleted file mode 100644
index a2d9edd2..00000000
--- a/app/templates/upload.js
+++ /dev/null
@@ -1,38 +0,0 @@
-const html = require('choo/html');
-const progress = require('./progress');
-const { bytes } = require('../utils');
-
-module.exports = function(state, emit) {
- const transfer = state.transfer;
-
- const div = html`
-
-
${state.translate(
- 'uploadingPageProgress',
- {
- filename: transfer.file.name,
- size: bytes(transfer.file.size)
- }
- )}
-
- ${progress(transfer.progressRatio)}
-
-
${state.translate(
- transfer.msg,
- transfer.sizes
- )}
-
-
-
- `;
-
- function cancel() {
- const btn = document.getElementById('cancel-upload');
- btn.disabled = true;
- btn.textContent = state.translate('uploadCancelNotification');
- emit('cancel');
- }
- return div;
-};
diff --git a/app/templates/uploadPassword.js b/app/templates/uploadPassword.js
deleted file mode 100644
index e84ebbb7..00000000
--- a/app/templates/uploadPassword.js
+++ /dev/null
@@ -1,65 +0,0 @@
-const html = require('choo/html');
-
-module.exports = function(state, emit) {
- const file = state.storage.getFileById(state.params.id);
- const div = html`
-
-
-
-
-
-
-
`;
-
- function inputChanged() {
- const input = document.getElementById('unlock-input');
- const btn = document.getElementById('unlock-btn');
- if (input.value.length > 0) {
- btn.classList.remove('btn-hidden');
- input.classList.remove('input-no-btn');
- } else {
- btn.classList.add('btn-hidden');
- input.classList.add('input-no-btn');
- }
- }
-
- function togglePasswordInput(e) {
- const unlockInput = document.getElementById('unlock-input');
- const boxChecked = e.target.checked;
- document
- .querySelector('.setPassword')
- .classList.toggle('hidden', !boxChecked);
- if (boxChecked) {
- unlockInput.focus();
- } else {
- unlockInput.value = '';
- }
- inputChanged();
- }
-
- function setPassword(event) {
- event.preventDefault();
- const password = document.getElementById('unlock-input').value;
- if (password.length > 0) {
- document.getElementById('copy').classList.remove('wait-password');
- document.getElementById('copy-btn').disabled = false;
- emit('password', { password, file });
- }
- }
-
- return div;
-};
diff --git a/app/templates/welcome.js b/app/templates/welcome.js
deleted file mode 100644
index 0f004608..00000000
--- a/app/templates/welcome.js
+++ /dev/null
@@ -1,73 +0,0 @@
-const html = require('choo/html');
-const assets = require('../../common/assets');
-const fileList = require('./fileList');
-const fxPromo = require('./fxPromo');
-const { fadeOut } = require('../utils');
-
-module.exports = function(state, emit) {
- const div = html`
-
-
${state.translate('uploadPageHeader')}
-
-
-
-
})
-
-
${state.translate('uploadPageDropMessage')}
-
- ${state.translate('uploadPageSizeMessage')}
-
-
-
-
- ${state.promo === 'body' ? fxPromo(state, emit) : ''}
- ${fileList(state, emit)}
-
- `;
-
- function dragover(event) {
- const div = document.querySelector('.upload-window');
- div.classList.add('ondrag');
- }
-
- function dragleave(event) {
- const div = document.querySelector('.upload-window');
- div.classList.remove('ondrag');
- }
-
- function onfocus(event) {
- event.target.classList.add('has-focus');
- }
-
- function onblur(event) {
- event.target.classList.remove('has-focus');
- }
-
- async function upload(event) {
- event.preventDefault();
- const target = event.target;
- const file = target.files[0];
- if (file.size === 0) {
- return;
- }
- await fadeOut('page-one');
- emit('upload', { file, type: 'click' });
- }
- return div;
-};
diff --git a/app/ui/account.js b/app/ui/account.js
new file mode 100644
index 00000000..9845a3a1
--- /dev/null
+++ b/app/ui/account.js
@@ -0,0 +1,111 @@
+const html = require('choo/html');
+const Component = require('choo/component');
+
+class Account extends Component {
+ constructor(name, state, emit) {
+ super(name);
+ this.state = state;
+ this.emit = emit;
+ this.enabled = state.capabilities.account;
+ this.local = state.components[name] = {};
+ this.buttonClass = '';
+ this.setLocal();
+ }
+
+ avatarClick(event) {
+ event.preventDefault();
+ const menu = document.getElementById('accountMenu');
+ menu.classList.toggle('invisible');
+ menu.focus();
+ }
+
+ hideMenu(event) {
+ event.stopPropagation();
+ const menu = document.getElementById('accountMenu');
+ menu.classList.add('invisible');
+ }
+
+ login(event) {
+ event.preventDefault();
+ this.emit('signup-cta', 'button');
+ }
+
+ logout(event) {
+ event.preventDefault();
+ this.emit('logout');
+ }
+
+ changed() {
+ return this.local.loggedIn !== this.state.user.loggedIn;
+ }
+
+ setLocal() {
+ const changed = this.changed();
+ if (changed) {
+ this.local.loggedIn = this.state.user.loggedIn;
+ }
+ return changed;
+ }
+
+ update() {
+ return this.setLocal();
+ }
+
+ createElement() {
+ if (!this.enabled) {
+ return html`
+
+ `;
+ }
+ const user = this.state.user;
+ const translate = this.state.translate;
+ this.setLocal();
+ if (user.loginRequired && !this.local.loggedIn) {
+ return html`
+
+ `;
+ }
+ if (!this.local.loggedIn) {
+ return html`
+
+
+
+ `;
+ }
+ return html`
+
+
+
+ - ${user.email}
+ -
+
+
+
+
+ `;
+ }
+}
+
+module.exports = Account;
diff --git a/app/ui/archiveTile.js b/app/ui/archiveTile.js
new file mode 100644
index 00000000..84ecf1f6
--- /dev/null
+++ b/app/ui/archiveTile.js
@@ -0,0 +1,645 @@
+/* global Android */
+
+const html = require('choo/html');
+const raw = require('choo/html/raw');
+const assets = require('../../common/assets');
+const {
+ bytes,
+ copyToClipboard,
+ list,
+ percent,
+ platform,
+ timeLeft
+} = require('../utils');
+const expiryOptions = require('./expiryOptions');
+
+function expiryInfo(translate, archive) {
+ const l10n = timeLeft(archive.expiresAt - Date.now());
+ return raw(
+ translate('archiveExpiryInfo', {
+ downloadCount: translate('downloadCount', {
+ num: archive.dlimit - archive.dtotal
+ }),
+ timespan: translate(l10n.id, l10n)
+ })
+ );
+}
+
+function password(state) {
+ const MAX_LENGTH = 4096;
+
+ return html`
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ function onPasswordPreviewButtonclicked(event) {
+ event.preventDefault();
+ const input = document.getElementById('password-input');
+ const eyeIcon = event.currentTarget.querySelector('img');
+
+ if (input.type === 'password') {
+ input.type = 'text';
+ eyeIcon.src = assets.get('eye-off.svg');
+ } else {
+ input.type = 'password';
+ eyeIcon.src = assets.get('eye.svg');
+ }
+
+ input.focus();
+ }
+
+ function togglePasswordInput(event) {
+ event.stopPropagation();
+ const checked = event.target.checked;
+ const input = document.getElementById('password-input');
+ const passwordPreviewButton = document.getElementById(
+ 'password-preview-button'
+ );
+ if (checked) {
+ input.classList.remove('invisible');
+ passwordPreviewButton.classList.remove('invisible');
+ input.focus();
+ } else {
+ input.classList.add('invisible');
+ passwordPreviewButton.classList.add('invisible');
+ input.value = '';
+ document.getElementById('password-msg').textContent = '';
+ state.archive.password = null;
+ }
+ }
+
+ function inputChanged() {
+ const passwordInput = document.getElementById('password-input');
+ const pwdmsg = document.getElementById('password-msg');
+ const password = passwordInput.value;
+ const length = password.length;
+
+ if (length === MAX_LENGTH) {
+ pwdmsg.textContent = state.translate('maxPasswordLength', {
+ length: MAX_LENGTH
+ });
+ } else {
+ pwdmsg.textContent = '';
+ }
+ state.archive.password = password;
+ }
+
+ function focused(event) {
+ event.preventDefault();
+ const el = document.getElementById('password-input');
+ if (el.placeholder !== state.translate('unlockInputPlaceholder')) {
+ el.placeholder = '';
+ }
+ }
+}
+
+function fileInfo(file, action) {
+ return html`
+
+
+
+
${file.name}
+ ${bytes(
+ file.size
+ )}
+
+ ${action}
+ `;
+}
+
+function archiveInfo(archive, action) {
+ return html`
+
+
+
+
${archive.name}
+
${bytes(
+ archive.size
+ )}
+
+ ${action}
+ `;
+}
+
+function archiveDetails(translate, archive) {
+ if (archive.manifest.files.length > 1) {
+ return html`
+
+
+
+ ${translate('fileCount', {
+ num: archive.manifest.files.length
+ })}
+
+ ${list(archive.manifest.files.map(f => fileInfo(f)))}
+
+ `;
+ }
+ function toggled(event) {
+ event.stopPropagation();
+ archive.open = event.target.open;
+ }
+}
+
+module.exports = function(state, emit, archive) {
+ const copyOrShare =
+ state.capabilities.share || platform() === 'android'
+ ? html`
+
+ `
+ : html`
+
+ `;
+ const dl =
+ platform() === 'web'
+ ? html`
+
+
+ ${state.translate('downloadButtonLabel')}
+
+ `
+ : html`
+
+ `;
+ return html`
+
+ ${archiveInfo(
+ archive,
+ html`
+
+ `
+ )}
+
+ ${expiryInfo(state.translate, archive)}
+
+ ${archiveDetails(state.translate, archive)}
+
+
+ ${dl} ${copyOrShare}
+
+
+ `;
+
+ function copy(event) {
+ event.stopPropagation();
+ copyToClipboard(archive.url);
+ const text = event.target.lastChild;
+ text.textContent = state.translate('copiedUrl');
+ setTimeout(
+ () => (text.textContent = state.translate('copyLinkButton')),
+ 1000
+ );
+ }
+
+ function del(event) {
+ event.stopPropagation();
+ emit('delete', archive);
+ }
+
+ async function share(event) {
+ event.stopPropagation();
+ if (platform() === 'android') {
+ Android.shareUrl(archive.url);
+ } else {
+ try {
+ await navigator.share({
+ title: state.translate('-send-brand'),
+ text: `Download "${archive.name}" with Send: simple, safe file sharing`,
+ //state.translate('shareMessage', { name }),
+ url: archive.url
+ });
+ } catch (e) {
+ // ignore
+ }
+ }
+ }
+};
+
+module.exports.wip = function(state, emit) {
+ return html`
+
+ ${list(
+ Array.from(state.archive.files)
+ .reverse()
+ .map(f =>
+ fileInfo(f, remove(f, state.translate('deleteButtonHover')))
+ ),
+ 'flex-shrink bg-grey-10 rounded-t overflow-y-auto px-6 py-4 md:h-full md:max-h-half-screen dark:bg-black',
+ 'bg-white px-2 my-2 shadow-light rounded-default dark:bg-grey-90 dark:border-default dark:border-grey-80'
+ )}
+
+
+
+
+
+ ${state.translate('totalSize', {
+ size: bytes(state.archive.size)
+ })}
+
+
+
+ ${expiryOptions(state, emit)} ${password(state, emit)}
+
+
+ `;
+
+ function focus(event) {
+ event.target.nextElementSibling.firstElementChild.classList.add('outline');
+ }
+
+ function blur(event) {
+ event.target.nextElementSibling.firstElementChild.classList.remove(
+ 'outline'
+ );
+ }
+
+ function upload(event) {
+ window.scrollTo(0, 0);
+ event.preventDefault();
+ event.target.disabled = true;
+ if (!state.uploading) {
+ emit('upload');
+ }
+ }
+
+ function add(event) {
+ event.preventDefault();
+ const newFiles = Array.from(event.target.files);
+
+ emit('addFiles', { files: newFiles });
+ setTimeout(() => {
+ document
+ .querySelector('#wip > ul > li:first-child')
+ .scrollIntoView({ block: 'center' });
+ });
+ }
+
+ function remove(file, desc) {
+ return html`
+
+ `;
+ function del(event) {
+ event.stopPropagation();
+ emit('removeUpload', file);
+ }
+ }
+};
+
+module.exports.uploading = function(state, emit) {
+ const progress = state.transfer.progressRatio;
+ const progressPercent = percent(progress);
+ const archive = state.archive;
+ return html`
+
+ ${archiveInfo(archive)}
+
+ ${expiryInfo(state.translate, {
+ dlimit: state.archive.dlimit,
+ dtotal: 0,
+ expiresAt: Date.now() + 500 + state.archive.timeLimit * 1000
+ })}
+
+
+ ${progressPercent}
+
+
+
+
+ `;
+
+ function cancel(event) {
+ event.stopPropagation();
+ event.target.disabled = true;
+ emit('cancel');
+ }
+};
+
+module.exports.empty = function(state, emit) {
+ const upsell =
+ state.user.loggedIn || !state.capabilities.account
+ ? ''
+ : html`
+
+ `;
+ const uploadNotice = state.WEB_UI.UPLOAD_AREA_NOTICE_HTML
+ ? html`
+
+ ${raw(state.WEB_UI.UPLOAD_AREA_NOTICE_HTML)}
+
+ `
+ : '';
+
+ return html`
+
+
+
+ ${state.translate('dragAndDropFiles')}
+
+
+ ${state.translate('orClickWithSize', {
+ size: bytes(state.user.maxSize)
+ })}
+
+
+
+ ${upsell} ${uploadNotice}
+
+ `;
+
+ function focus(event) {
+ event.target.nextElementSibling.classList.add('bg-primary', 'outline');
+ }
+
+ function blur(event) {
+ event.target.nextElementSibling.classList.remove('bg-primary', 'outline');
+ }
+
+ function add(event) {
+ event.preventDefault();
+ const newFiles = Array.from(event.target.files);
+
+ emit('addFiles', { files: newFiles });
+ }
+};
+
+module.exports.preview = function(state, emit) {
+ const archive = state.fileInfo;
+ if (archive.open === undefined) {
+ archive.open = true;
+ }
+ const single = archive.manifest.files.length === 1;
+ const details = single
+ ? ''
+ : html`
+
+ ${archiveDetails(state.translate, archive)}
+
+ `;
+ const notice = state.WEB_UI.DOWNLOAD_NOTICE_HTML
+ ? html`
+
+ ${raw(state.WEB_UI.DOWNLOAD_NOTICE_HTML)}
+
+ `
+ : '';
+ const sponsor = state.WEB_UI.SHOW_THUNDERBIRD_SPONSOR
+ ? html`
+
+
+ ${state.translate('sponsoredByThunderbird')}
+
+ `
+ : '';
+
+ return html`
+
+
+ ${archiveInfo(archive)} ${details}
+
+
+ ${notice} ${sponsor}
+
+ `;
+
+ function download(event) {
+ event.preventDefault();
+ event.target.disabled = true;
+ emit('download');
+ }
+};
+
+module.exports.downloading = function(state) {
+ const archive = state.fileInfo;
+ const progress = state.transfer.progressRatio;
+ const progressPercent = percent(progress);
+ return html`
+
+ ${archiveInfo(archive)}
+
+ ${progressPercent}
+
+
+
+ `;
+};
diff --git a/app/ui/blank.js b/app/ui/blank.js
new file mode 100644
index 00000000..d34f94e3
--- /dev/null
+++ b/app/ui/blank.js
@@ -0,0 +1,14 @@
+const html = require('choo/html');
+
+module.exports = function() {
+ return html`
+
+
+
+ `;
+};
diff --git a/app/ui/body.js b/app/ui/body.js
new file mode 100644
index 00000000..b717a9b1
--- /dev/null
+++ b/app/ui/body.js
@@ -0,0 +1,21 @@
+const html = require('choo/html');
+const Header = require('./header');
+const Footer = require('./footer');
+
+module.exports = function body(main) {
+ return function(state, emit) {
+ const b = html`
+
+ ${state.cache(Header, 'header').render()} ${main(state, emit)}
+ ${state.cache(Footer, 'footer').render()}
+
+ `;
+ if (state.layout) {
+ // server side only
+ return state.layout(state, b);
+ }
+ return b;
+ };
+};
diff --git a/app/ui/copyDialog.js b/app/ui/copyDialog.js
new file mode 100644
index 00000000..5f1f779e
--- /dev/null
+++ b/app/ui/copyDialog.js
@@ -0,0 +1,76 @@
+const html = require('choo/html');
+const { copyToClipboard } = require('../utils');
+const qr = require('./qr');
+
+module.exports = function(name, url) {
+ const dialog = function(state, emit, close) {
+ return html`
+
+
+ ${state.translate('notifyUploadEncryptDone')}
+
+
+ ${state.translate('copyLinkDescription')}
+ ${name}
+
+
+
+
+
+
+
+
+ `;
+
+ function toggleQR(event) {
+ event.stopPropagation();
+ const shareUrl = document.getElementById('share-url');
+ const qrBtn = document.getElementById('qr-btn');
+ if (shareUrl.classList.contains('hidden')) {
+ shareUrl.classList.replace('hidden', 'block');
+ qrBtn.classList.replace('w-48', 'w-16');
+ } else {
+ shareUrl.classList.replace('block', 'hidden');
+ qrBtn.classList.replace('w-16', 'w-48');
+ }
+ }
+
+ function copy(event) {
+ event.stopPropagation();
+ copyToClipboard(url);
+ event.target.textContent = state.translate('copiedUrl');
+ setTimeout(close, 1000);
+ }
+ };
+ dialog.type = 'copy';
+ return dialog;
+};
diff --git a/app/ui/download.js b/app/ui/download.js
new file mode 100644
index 00000000..b397fa8e
--- /dev/null
+++ b/app/ui/download.js
@@ -0,0 +1,96 @@
+/* global downloadMetadata */
+const html = require('choo/html');
+const archiveTile = require('./archiveTile');
+const modal = require('./modal');
+const noStreams = require('./noStreams');
+const notFound = require('./notFound');
+const downloadPassword = require('./downloadPassword');
+const downloadCompleted = require('./downloadCompleted');
+const BIG_SIZE = 1024 * 1024 * 256;
+
+function createFileInfo(state) {
+ return {
+ id: state.params.id,
+ secretKey: state.params.key,
+ nonce: downloadMetadata.nonce,
+ requiresPassword: downloadMetadata.pwd
+ };
+}
+
+function downloading(state, emit) {
+ return html`
+
+
+ ${state.translate('downloadingTitle')}
+
+ ${archiveTile.downloading(state, emit)}
+
+ `;
+}
+
+function preview(state, emit) {
+ if (!state.capabilities.streamDownload && state.fileInfo.size > BIG_SIZE) {
+ return noStreams(state, emit);
+ }
+ return html`
+
+
+ ${state.translate('downloadTitle')}
+
+
+ ${state.translate('downloadDescription')}
+
+ ${archiveTile.preview(state, emit)}
+
+ `;
+}
+
+module.exports = function(state, emit) {
+ let content = '';
+ if (!state.fileInfo) {
+ state.fileInfo = createFileInfo(state);
+ if (downloadMetadata.status === 404) {
+ return notFound(state);
+ }
+ if (!state.fileInfo.nonce) {
+ // coming from something like the browser back button
+ return location.reload();
+ }
+ }
+
+ if (!state.transfer && !state.fileInfo.requiresPassword) {
+ emit('getMetadata');
+ }
+
+ if (state.transfer) {
+ switch (state.transfer.state) {
+ case 'downloading':
+ case 'decrypting':
+ content = downloading(state, emit);
+ break;
+ case 'complete':
+ content = downloadCompleted(state);
+ break;
+ default:
+ content = preview(state, emit);
+ }
+ } else if (state.fileInfo.requiresPassword && !state.fileInfo.password) {
+ content = downloadPassword(state, emit);
+ }
+ return html`
+
+ ${state.modal && modal(state, emit)}
+
+
+ `;
+};
diff --git a/app/ui/downloadCompleted.js b/app/ui/downloadCompleted.js
new file mode 100644
index 00000000..acdd3044
--- /dev/null
+++ b/app/ui/downloadCompleted.js
@@ -0,0 +1,32 @@
+const html = require('choo/html');
+const assets = require('../../common/assets');
+
+module.exports = function(state) {
+ const btnText = state.user.loggedIn ? 'okButton' : 'sendYourFilesLink';
+ return html`
+
+ `;
+};
diff --git a/app/ui/downloadPassword.js b/app/ui/downloadPassword.js
new file mode 100644
index 00000000..99bbf434
--- /dev/null
+++ b/app/ui/downloadPassword.js
@@ -0,0 +1,98 @@
+const html = require('choo/html');
+
+module.exports = function(state, emit) {
+ const fileInfo = state.fileInfo;
+ const invalid = fileInfo.password === null;
+
+ const div = html`
+
+
+ ${state.translate('downloadTitle')}
+
+
+ ${state.translate('downloadDescription')}
+
+
+
+
+ `;
+
+ if (!(div instanceof String)) {
+ setTimeout(() => document.getElementById('password-input').focus());
+ }
+
+ function inputChanged(event) {
+ event.stopPropagation();
+ event.preventDefault();
+ const label = document.getElementById('password-error');
+ const input = document.getElementById('password-input');
+ const btn = document.getElementById('password-btn');
+ label.classList.add('invisible');
+ input.classList.remove('border-red', 'dark:border-red-40');
+ btn.classList.remove(
+ 'bg-red',
+ 'hover:bg-red',
+ 'focus:bg-red',
+ 'dark:bg-red-40'
+ );
+ }
+
+ function checkPassword(event) {
+ event.stopPropagation();
+ event.preventDefault();
+ const el = document.getElementById('password-input');
+ const password = el.value;
+ if (password.length > 0) {
+ document.getElementById('password-btn').disabled = true;
+ // Strip any url parameters between fileId and secretKey
+ const fileInfoUrl = window.location.href.replace(/\?.+#/, '#');
+ state.fileInfo.url = fileInfoUrl;
+ state.fileInfo.password = password;
+ emit('getMetadata');
+ }
+ return false;
+ }
+
+ return div;
+};
diff --git a/app/ui/error.js b/app/ui/error.js
new file mode 100644
index 00000000..241aac7f
--- /dev/null
+++ b/app/ui/error.js
@@ -0,0 +1,35 @@
+const html = require('choo/html');
+const assets = require('../../common/assets');
+const modal = require('./modal');
+
+module.exports = function(state, emit) {
+ const btnText = state.user.loggedIn ? 'okButton' : 'sendYourFilesLink';
+ return html`
+
+ ${state.modal && modal(state, emit)}
+
+
+ `;
+};
diff --git a/app/ui/expiryOptions.js b/app/ui/expiryOptions.js
new file mode 100644
index 00000000..2ee489f7
--- /dev/null
+++ b/app/ui/expiryOptions.js
@@ -0,0 +1,73 @@
+const html = require('choo/html');
+const raw = require('choo/html/raw');
+const { secondsToL10nId } = require('../utils');
+const selectbox = require('./selectbox');
+
+module.exports = function(state, emit) {
+ const el = html`
+
+ ${raw(
+ state.translate('archiveExpiryInfo', {
+ downloadCount:
+ '',
+ timespan: ''
+ })
+ )}
+
+ `;
+ if (el.__encoded) {
+ // we're rendering on the server
+ return el;
+ }
+
+ const counts = state.DEFAULTS.DOWNLOAD_COUNTS.filter(
+ i => state.capabilities.account || i <= state.user.maxDownloads
+ );
+
+ const dlCountSelect = el.querySelector('#dlCount');
+ el.replaceChild(
+ selectbox(
+ state.archive.dlimit,
+ counts,
+ num => state.translate('downloadCount', { num }),
+ value => {
+ const selected = parseInt(value);
+ state.archive.dlimit = selected;
+ emit('render');
+ if (selected > parseInt(state.user.maxDownloads || '0')) {
+ console.log('Chosen max download count is larger than the allowed limit', selected)
+ }
+ },
+ 'expire-after-dl-count-select'
+ ),
+ dlCountSelect
+ );
+
+ const expires = state.DEFAULTS.EXPIRE_TIMES_SECONDS.filter(
+ i => state.capabilities.account || i <= state.user.maxExpireSeconds
+ );
+
+ const timeSelect = el.querySelector('#timespan');
+ el.replaceChild(
+ selectbox(
+ state.archive.timeLimit,
+ expires,
+ num => {
+ const l10n = secondsToL10nId(num);
+ return state.translate(l10n.id, l10n);
+ },
+ value => {
+ const selected = parseInt(value);
+ state.archive.timeLimit = selected;
+ emit('render');
+ if (selected > parseInt(state.user.maxExpireSeconds || '0')) {
+ console.log('Chosen download expiration is larger than the allowed limit', selected)
+ }
+ },
+ 'expire-after-time-select'
+ ),
+ timeSelect
+ );
+
+ return el;
+};
diff --git a/app/ui/faviconProgressbar.js b/app/ui/faviconProgressbar.js
new file mode 100644
index 00000000..1faca4d9
--- /dev/null
+++ b/app/ui/faviconProgressbar.js
@@ -0,0 +1,46 @@
+/*global WEB_UI*/
+
+const { platform } = require('../utils');
+const assets = require('../../common/assets');
+
+const size = 32;
+const loaderWidth = 5;
+const loaderColor = WEB_UI.COLORS.PRIMARY;
+
+function drawCircle(canvas, context, color, lineWidth, outerWidth, percent) {
+ canvas.width = canvas.height = outerWidth;
+ context.translate(outerWidth * 0.5, outerWidth * 0.5);
+ context.rotate(-Math.PI * 0.5);
+ const radius = (outerWidth - lineWidth) * 0.5;
+ context.beginPath();
+ context.arc(0, 0, radius, 0, Math.PI * 2 * percent, false);
+ context.strokeStyle = color;
+ context.lineCap = 'square';
+ context.lineWidth = lineWidth;
+ context.stroke();
+}
+
+function drawNewFavicon(progressRatio) {
+ const canvas = document.createElement('canvas');
+ const context = canvas.getContext('2d');
+ drawCircle(canvas, context, '#efefef', loaderWidth, size, 1);
+ drawCircle(canvas, context, loaderColor, loaderWidth, size, progressRatio);
+ return canvas.toDataURL();
+}
+
+module.exports.updateFavicon = function(progressRatio) {
+ if (platform() === 'web') {
+ const link = document.querySelector("link[rel='icon'][sizes='32x32']");
+ const progress = progressRatio * 100;
+ if (progress === 0 || progress === 100) {
+ link.type = 'image/png';
+ link.href =
+ WEB_UI.CUSTOM_ASSETS.favicon_32px !== ''
+ ? WEB_UI.CUSTOM_ASSETS.favicon_32px
+ : assets.get('favicon-32x32.png');
+ return;
+ }
+
+ link.href = drawNewFavicon(progressRatio);
+ }
+};
diff --git a/app/ui/footer.js b/app/ui/footer.js
new file mode 100644
index 00000000..77a2b960
--- /dev/null
+++ b/app/ui/footer.js
@@ -0,0 +1,126 @@
+const html = require('choo/html');
+const Component = require('choo/component');
+
+class Footer extends Component {
+ constructor(name, state) {
+ super(name);
+ this.state = state;
+ }
+
+ update() {
+ return false;
+ }
+
+ createElement() {
+ const translate = this.state.translate;
+
+ // Add additional links from configuration if available
+ var links = [];
+ if (this.state != undefined && this.state.WEB_UI != undefined) {
+ const WEB_UI = this.state.WEB_UI;
+
+ if (WEB_UI.FOOTER_DONATE_URL != '') {
+ links.push(html`
+
+
+ ${translate('footerLinkDonate')}
+
+
+ `);
+ }
+ if (WEB_UI.FOOTER_CLI_URL != '') {
+ links.push(html`
+
+
+ ${translate('footerLinkCli')}
+
+
+ `);
+ }
+ if (WEB_UI.FOOTER_DMCA_URL != '') {
+ links.push(html`
+
+
+ ${translate('footerLinkDmca')}
+
+
+ `);
+ }
+ if (WEB_UI.FOOTER_SOURCE_URL != '') {
+ links.push(html`
+
+
+ ${translate('footerLinkSource')}
+
+
+ `);
+ }
+ } else {
+ links.push(html`
+
+
+ ${translate('footerLinkSource')}
+
+
+ `);
+ }
+
+ // Defining a custom footer
+ var footer = [];
+ if (this.state != undefined && this.state.WEB_UI != undefined) {
+ const WEB_UI = this.state.WEB_UI;
+
+ if (WEB_UI.CUSTOM_FOOTER_URL != '' && WEB_UI.CUSTOM_FOOTER_TEXT != '') {
+ footer.push(html`
+
+
+ ${WEB_UI.CUSTOM_FOOTER_TEXT}
+
+
+ `);
+ }
+ else if (WEB_UI.CUSTOM_FOOTER_URL != '') {
+ footer.push(html`
+
+
+ ${WEB_UI.CUSTOM_FOOTER_URL}
+
+
+ `);
+ }
+ else if (WEB_UI.CUSTOM_FOOTER_TEXT != '') {
+ footer.push(html`
+
+ ${WEB_UI.CUSTOM_FOOTER_TEXT}
+
+ `)
+ }
+ else {
+ footer.push(html`
+
+ ${translate('footerText')}
+
+ `);
+ }
+ }
+
+ return html`
+
+ `;
+ }
+}
+
+module.exports = Footer;
diff --git a/app/ui/header.js b/app/ui/header.js
new file mode 100644
index 00000000..679cb334
--- /dev/null
+++ b/app/ui/header.js
@@ -0,0 +1,65 @@
+const html = require('choo/html');
+const Component = require('choo/component');
+const Account = require('./account');
+const assets = require('../../common/assets');
+const { platform } = require('../utils');
+
+class Header extends Component {
+ constructor(name, state, emit) {
+ super(name);
+ this.state = state;
+ this.emit = emit;
+ this.account = state.cache(Account, 'account');
+ }
+
+ update() {
+ this.account.render();
+ return false;
+ }
+
+ createElement() {
+ let assetMap = {};
+ if (this.state.ui !== undefined) assetMap = this.state.ui.assets;
+ else
+ assetMap = {
+ icon:
+ this.state.WEB_UI.CUSTOM_ASSETS.icon !== ''
+ ? this.state.WEB_UI.CUSTOM_ASSETS.icon
+ : assets.get('icon.svg'),
+ wordmark:
+ this.state.WEB_UI.CUSTOM_ASSETS.wordmark !== ''
+ ? this.state.WEB_UI.CUSTOM_ASSETS.wordmark
+ : assets.get('wordmark.svg') + '#logo'
+ };
+ const title =
+ platform() === 'android'
+ ? html`
+
+
+
+
+ `
+ : html`
+
+
+
+
+ `;
+ return html`
+
+ ${title} ${this.account.render()}
+
+ `;
+ }
+}
+
+module.exports = Header;
diff --git a/app/ui/home.js b/app/ui/home.js
new file mode 100644
index 00000000..86550ff4
--- /dev/null
+++ b/app/ui/home.js
@@ -0,0 +1,73 @@
+const html = require('choo/html');
+const raw = require('choo/html/raw');
+const { list } = require('../utils');
+const archiveTile = require('./archiveTile');
+const modal = require('./modal');
+const intro = require('./intro');
+const assets = require('../../common/assets');
+
+module.exports = function(state, emit) {
+ const archives = state.storage.files
+ .filter(archive => !archive.expired)
+ .map(archive => archiveTile(state, emit, archive));
+ let left = '';
+ if (state.uploading) {
+ left = archiveTile.uploading(state, emit);
+ } else if (state.archive.numFiles > 0) {
+ left = archiveTile.wip(state, emit);
+ } else {
+ left = archiveTile.empty(state, emit);
+ }
+
+ if (archives.length > 0 && state.WEB_UI.UPLOADS_LIST_NOTICE_HTML) {
+ archives.push(html`
+
+ ${raw(state.WEB_UI.UPLOADS_LIST_NOTICE_HTML)}
+
+ `);
+ }
+
+ archives.reverse();
+
+ if (archives.length > 0 && state.WEB_UI.SHOW_THUNDERBIRD_SPONSOR) {
+ archives.push(html`
+
+
+ Sponsored by Thunderbird
+
+ `);
+ }
+
+ const right =
+ archives.length === 0
+ ? intro(state)
+ : list(archives, 'p-2 h-full overflow-y-auto w-full', 'mb-4 w-full');
+
+ return html`
+
+ ${state.modal && modal(state, emit)}
+
+
+ `;
+};
diff --git a/app/ui/intro.js b/app/ui/intro.js
new file mode 100644
index 00000000..9a53c404
--- /dev/null
+++ b/app/ui/intro.js
@@ -0,0 +1,55 @@
+const html = require('choo/html');
+const raw = require('choo/html/raw');
+const assets = require('../../common/assets');
+
+module.exports = function intro(state) {
+ const notice = state.WEB_UI.MAIN_NOTICE_HTML
+ ? html`
+
+ ${raw(state.WEB_UI.MAIN_NOTICE_HTML)}
+
+ `
+ : '';
+
+ const sponsor = state.WEB_UI.SHOW_THUNDERBIRD_SPONSOR
+ ? html`
+
+
+ Sponsored by Thunderbird
+
+ `
+ : '';
+
+ return html`
+
+ ${notice}
+
+
+ ${state.translate('introTitle')}
+
+
+ ${state.translate('introDescription')}
+
+
+ ${sponsor}
+
+ `;
+};
diff --git a/app/ui/modal.js b/app/ui/modal.js
new file mode 100644
index 00000000..3636af8a
--- /dev/null
+++ b/app/ui/modal.js
@@ -0,0 +1,25 @@
+const html = require('choo/html');
+
+module.exports = function(state, emit) {
+ return html`
+
+
+
+ ${state.modal(state, emit, close)}
+
+
+
+ `;
+
+ function close(event) {
+ if (event) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ emit('closeModal');
+ }
+};
diff --git a/app/ui/noStreams.js b/app/ui/noStreams.js
new file mode 100644
index 00000000..c3bc8673
--- /dev/null
+++ b/app/ui/noStreams.js
@@ -0,0 +1,108 @@
+const html = require('choo/html');
+const { bytes } = require('../utils');
+const assets = require('../../common/assets');
+
+module.exports = function(state, emit) {
+ const archive = state.fileInfo;
+ return html`
+
+
${state.translate(
+ 'downloadTitle'
+ )}
+
+ ⚠️ ${state.translate('noStreamsWarning')} ⚠️
+
+
+
+ `;
+
+ function optionChanged(event) {
+ event.stopPropagation();
+ const choice = event.target.value;
+ const button = event.currentTarget.nextElementSibling;
+ let title = button.title;
+ console.error(choice, title);
+ switch (choice) {
+ case 'copy':
+ title = state.translate('copyLinkButton');
+ break;
+ case 'firefox':
+ title = state.translate('downloadFirefox');
+ break;
+ case 'download':
+ title = state.translate('downloadButtonLabel');
+ break;
+ }
+ button.title = title;
+ button.value = title;
+ }
+
+ function submit(event) {
+ const action = document.querySelector('input[type="radio"]:checked').value;
+ switch (action) {
+ case 'copy':
+ emit('copy', { url: window.location.href });
+ document.querySelector('input[type="submit"]').value = state.translate(
+ 'copiedUrl'
+ );
+ break;
+ case 'firefox':
+ window.open(
+ 'https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com'
+ );
+ break;
+ case 'download':
+ emit('download');
+ break;
+ }
+ return false;
+ }
+};
diff --git a/app/ui/notFound.js b/app/ui/notFound.js
new file mode 100644
index 00000000..5cf1ce64
--- /dev/null
+++ b/app/ui/notFound.js
@@ -0,0 +1,35 @@
+const html = require('choo/html');
+const assets = require('../../common/assets');
+const modal = require('./modal');
+
+module.exports = function(state, emit) {
+ const btnText = state.user.loggedIn ? 'okButton' : 'sendYourFilesLink';
+ return html`
+
+ ${state.modal && modal(state, emit)}
+
+
+ `;
+};
diff --git a/app/ui/okDialog.js b/app/ui/okDialog.js
new file mode 100644
index 00000000..cb8c3c6a
--- /dev/null
+++ b/app/ui/okDialog.js
@@ -0,0 +1,20 @@
+const html = require('choo/html');
+
+module.exports = function(message) {
+ return function(state, emit, close) {
+ return html`
+
+
+ ${message}
+
+
+
+ `;
+ };
+};
diff --git a/app/ui/qr.js b/app/ui/qr.js
new file mode 100644
index 00000000..8f78865b
--- /dev/null
+++ b/app/ui/qr.js
@@ -0,0 +1,10 @@
+const raw = require('choo/html/raw');
+const qrcode = require('../qrcode');
+
+module.exports = function(url) {
+ const gen = qrcode(0, 'L');
+ gen.addData(url);
+ gen.make();
+ const qr = gen.createSvgTag({ scalable: true });
+ return raw(qr);
+};
diff --git a/app/ui/selectbox.js b/app/ui/selectbox.js
new file mode 100644
index 00000000..836332b8
--- /dev/null
+++ b/app/ui/selectbox.js
@@ -0,0 +1,35 @@
+const html = require('choo/html');
+
+module.exports = function(selected, options, translate, changed, htmlId) {
+ function choose(event) {
+ if (event.target.value != selected) {
+ console.log(
+ 'Selected new value from dropdown',
+ htmlId,
+ ':',
+ selected,
+ '->',
+ event.target.value
+ );
+ changed(event.target.value);
+ }
+ }
+
+ return html`
+
+ `;
+};
diff --git a/app/ui/shareDialog.js b/app/ui/shareDialog.js
new file mode 100644
index 00000000..86b17bd7
--- /dev/null
+++ b/app/ui/shareDialog.js
@@ -0,0 +1,61 @@
+const html = require('choo/html');
+
+module.exports = function(name, url) {
+ const dialog = function(state, emit, close) {
+ return html`
+
+
+ ${state.translate('notifyUploadEncryptDone')}
+
+
+ ${state.translate('shareLinkDescription')}
+ ${name}
+
+
+
+
+
+ `;
+
+ async function share(event) {
+ event.stopPropagation();
+ try {
+ await navigator.share({
+ title: state.translate('-send-brand'),
+ text: state.translate('shareMessage', { name }),
+ url
+ });
+ } catch (e) {
+ if (e.code === e.ABORT_ERR) {
+ return;
+ }
+ console.error(e);
+ }
+ close();
+ }
+ };
+ dialog.type = 'share';
+ return dialog;
+};
diff --git a/app/ui/signupDialog.js b/app/ui/signupDialog.js
new file mode 100644
index 00000000..596e446f
--- /dev/null
+++ b/app/ui/signupDialog.js
@@ -0,0 +1,89 @@
+const html = require('choo/html');
+const assets = require('../../common/assets');
+const { bytes } = require('../utils');
+
+module.exports = function() {
+ return function(state, emit, close) {
+ const DAYS = Math.floor(state.LIMITS.MAX_EXPIRE_SECONDS / 86400);
+ let submitting = false;
+ return html`
+
+
+
+
+ ${state.translate('accountBenefitTitle')}
+
+
+ -
+ ${state.translate('accountBenefitLargeFiles', {
+ size: bytes(state.LIMITS.MAX_FILE_SIZE)
+ })}
+
+ - ${state.translate('accountBenefitDownloadCount')}
+ -
+ ${state.translate('accountBenefitTimeLimit', { count: DAYS })}
+
+ - ${state.translate('accountBenefitSync')}
+
+
+
+
+ ${state.user.loginRequired
+ ? ''
+ : html`
+
+ `}
+
+
+ `;
+
+ function emailish(str) {
+ if (!str) {
+ return false;
+ }
+ // just check if it's the right shape
+ const a = str.split('@');
+ return a.length === 2 && a.every(s => s.length > 0);
+ }
+
+ function cancel(event) {
+ close(event);
+ }
+
+ function submitEmail(event) {
+ event.preventDefault();
+ if (submitting) {
+ return;
+ }
+ submitting = true;
+
+ const el = document.getElementById('email-input');
+ const email = el.value;
+ emit('login', emailish(email) ? email : null);
+ }
+ };
+};
diff --git a/app/ui/surveyDialog.js b/app/ui/surveyDialog.js
new file mode 100644
index 00000000..02c3236d
--- /dev/null
+++ b/app/ui/surveyDialog.js
@@ -0,0 +1,42 @@
+const html = require('choo/html');
+const version = require('../../package.json').version;
+const { browserName } = require('../utils');
+
+module.exports = function() {
+ return function(state, emit, close) {
+ const surveyUrl = `${
+ state.PREFS.surveyUrl
+ }?ver=${version}&browser=${browserName()}&anon=${
+ state.user.loggedIn
+ }&active_count=${state.storage.files.length}`;
+ return html`
+
+
+ Tell us what you think.
+
+
+ Love Send? Take a quick survey to let us know how we can make it
+ better.
+
+
+ Give feedback
+
+
+
+ `;
+ };
+};
diff --git a/app/ui/unsupported.js b/app/ui/unsupported.js
new file mode 100644
index 00000000..97a1eb5a
--- /dev/null
+++ b/app/ui/unsupported.js
@@ -0,0 +1,57 @@
+const html = require('choo/html');
+const modal = require('./modal');
+
+module.exports = function(state, emit) {
+ let strings = {};
+ let why = '';
+ let url = '';
+
+ if (state.params.reason !== 'outdated') {
+ strings = unsupportedStrings(state);
+ why = html`
+
+ ${state.translate('notSupportedLink')}
+
+ `;
+ url =
+ 'https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com';
+ } else {
+ strings = outdatedStrings(state);
+ url = 'https://support.mozilla.org/kb/update-firefox-latest-version';
+ }
+
+ return html`
+
+ ${state.modal && modal(state, emit)}
+
+ ${strings.header}
+ ${strings.description}
+ ${why}
+
+ ${strings.button}
+
+
+
+ `;
+};
+
+function outdatedStrings(state) {
+ return {
+ header: state.translate('notSupportedHeader'),
+ description: state.translate('notSupportedOutdatedDetail'),
+ button: state.translate('updateFirefox')
+ };
+}
+
+function unsupportedStrings(state) {
+ return {
+ header: state.translate('notSupportedHeader'),
+ description: state.translate('notSupportedDescription'),
+ button: state.translate('downloadFirefox')
+ };
+}
diff --git a/app/user.js b/app/user.js
new file mode 100644
index 00000000..be3aef2a
--- /dev/null
+++ b/app/user.js
@@ -0,0 +1,293 @@
+import assets from '../common/assets';
+import { getFileList, setFileList } from './api';
+import { encryptStream, decryptStream } from './ece';
+import { arrayToB64, b64ToArray, streamToArrayBuffer } from './utils';
+import { blobStream } from './streams';
+import { getFileListKey, prepareScopedBundleKey, preparePkce } from './fxa';
+import storage from './storage';
+
+const textEncoder = new TextEncoder();
+const textDecoder = new TextDecoder();
+const anonId = arrayToB64(crypto.getRandomValues(new Uint8Array(16)));
+
+async function hashId(id) {
+ const d = new Date();
+ const month = d.getUTCMonth();
+ const year = d.getUTCFullYear();
+ const encoded = textEncoder.encode(`${id}:${year}:${month}`);
+ const hash = await crypto.subtle.digest('SHA-256', encoded);
+ return arrayToB64(new Uint8Array(hash.slice(16)));
+}
+
+export default class User {
+ constructor(storage, limits, authConfig) {
+ this.authConfig = authConfig;
+ this.limits = limits;
+ this.storage = storage;
+ this.data = storage.user || {};
+ }
+
+ get info() {
+ return this.data || this.storage.user || {};
+ }
+
+ set info(data) {
+ this.data = data;
+ this.storage.user = data;
+ }
+
+ get firstAction() {
+ return this.storage.get('firstAction');
+ }
+
+ set firstAction(action) {
+ this.storage.set('firstAction', action);
+ }
+
+ get surveyed() {
+ return this.storage.get('surveyed');
+ }
+
+ set surveyed(yes) {
+ this.storage.set('surveyed', yes);
+ }
+
+ get avatar() {
+ const defaultAvatar = assets.get('user.svg');
+ if (this.info.avatarDefault) {
+ return defaultAvatar;
+ }
+ return this.info.avatar || defaultAvatar;
+ }
+
+ get name() {
+ return this.info.displayName;
+ }
+
+ get email() {
+ return this.info.email;
+ }
+
+ get loggedIn() {
+ return !!this.info.access_token;
+ }
+
+ get bearerToken() {
+ return this.info.access_token;
+ }
+
+ get refreshToken() {
+ return this.info.refresh_token;
+ }
+
+ get maxSize() {
+ return this.limits.MAX_FILE_SIZE;
+ }
+
+ get maxExpireSeconds() {
+ return this.limits.MAX_EXPIRE_SECONDS;
+ }
+
+ get maxDownloads() {
+ return this.limits.MAX_DOWNLOADS;
+ }
+
+ async metricId() {
+ return this.loggedIn ? hashId(this.info.uid) : undefined;
+ }
+
+ async deviceId() {
+ return this.loggedIn ? hashId(this.storage.id) : hashId(anonId);
+ }
+
+ async startAuthFlow(trigger, utms = {}) {
+ this.utms = utms;
+ this.trigger = trigger;
+ this.flowId = null;
+ this.flowBeginTime = null;
+ }
+
+ async login(email) {
+ const state = arrayToB64(crypto.getRandomValues(new Uint8Array(16)));
+ storage.set('oauthState', state);
+ const keys_jwk = await prepareScopedBundleKey(this.storage);
+ const code_challenge = await preparePkce(this.storage);
+ const options = {
+ action: 'email',
+ access_type: 'offline',
+ client_id: this.authConfig.client_id,
+ code_challenge,
+ code_challenge_method: 'S256',
+ response_type: 'code',
+ scope: `profile ${this.authConfig.key_scope}`,
+ state,
+ keys_jwk
+ };
+ if (email) {
+ options.email = email;
+ }
+ if (this.flowId && this.flowBeginTime) {
+ options.flow_id = this.flowId;
+ options.flow_begin_time = this.flowBeginTime;
+ }
+ if (this.trigger) {
+ options.entrypoint = `send-${this.trigger}`;
+ }
+ if (this.utms) {
+ options.utm_campaign = this.utms.campaign || 'none';
+ options.utm_content = this.utms.content || 'none';
+ options.utm_medium = this.utms.medium || 'none';
+ options.utm_source = this.utms.source || 'send';
+ options.utm_term = this.utms.term || 'none';
+ }
+ const params = new URLSearchParams(options);
+ location.assign(
+ `${this.authConfig.authorization_endpoint}?${params.toString()}`
+ );
+ }
+
+ async finishLogin(code, state) {
+ const localState = storage.get('oauthState');
+ storage.remove('oauthState');
+ if (state !== localState) {
+ throw new Error('state mismatch');
+ }
+ const tokenResponse = await fetch(this.authConfig.token_endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ code,
+ client_id: this.authConfig.client_id,
+ code_verifier: this.storage.get('pkceVerifier')
+ })
+ });
+ const auth = await tokenResponse.json();
+ const infoResponse = await fetch(this.authConfig.userinfo_endpoint, {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${auth.access_token}`
+ }
+ });
+ const userInfo = await infoResponse.json();
+ userInfo.access_token = auth.access_token;
+ userInfo.refresh_token = auth.refresh_token;
+ userInfo.fileListKey = await getFileListKey(this.storage, auth.keys_jwe);
+ this.info = userInfo;
+ this.storage.remove('pkceVerifier');
+ }
+
+ async refresh() {
+ if (!this.refreshToken) {
+ return false;
+ }
+ try {
+ const tokenResponse = await fetch(this.authConfig.token_endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ client_id: this.authConfig.client_id,
+ grant_type: 'refresh_token',
+ refresh_token: this.refreshToken
+ })
+ });
+ if (tokenResponse.ok) {
+ const auth = await tokenResponse.json();
+ const info = { ...this.info, access_token: auth.access_token };
+ this.info = info;
+ return true;
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ await this.logout();
+ return false;
+ }
+
+ async logout() {
+ try {
+ if (this.refreshToken) {
+ await fetch(this.authConfig.revocation_endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ refresh_token: this.refreshToken
+ })
+ });
+ }
+ if (this.bearerToken) {
+ await fetch(this.authConfig.revocation_endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ token: this.bearerToken
+ })
+ });
+ }
+ } catch (e) {
+ console.error(e);
+ // oh well, we tried
+ }
+ this.storage.clearLocalFiles();
+ this.info = {};
+ }
+
+ async syncFileList() {
+ let changes = { incoming: false, outgoing: false, downloadCount: false };
+ if (!this.loggedIn) {
+ return this.storage.merge();
+ }
+ let list = [];
+ const key = b64ToArray(this.info.fileListKey);
+ const sha = await crypto.subtle.digest('SHA-256', key);
+ const kid = arrayToB64(new Uint8Array(sha)).substring(0, 16);
+ const retry = async () => {
+ const refreshed = await this.refresh();
+ if (refreshed) {
+ return await this.syncFileList();
+ } else {
+ return { incoming: true };
+ }
+ };
+ try {
+ const encrypted = await getFileList(this.bearerToken, kid);
+ const decrypted = await streamToArrayBuffer(
+ decryptStream(blobStream(encrypted), key)
+ );
+ list = JSON.parse(textDecoder.decode(decrypted));
+ } catch (e) {
+ if (e.message === '401') {
+ return retry(e);
+ }
+ }
+ changes = await this.storage.merge(list);
+ if (!changes.outgoing) {
+ return changes;
+ }
+ try {
+ const blob = new Blob([
+ textEncoder.encode(JSON.stringify(this.storage.files))
+ ]);
+ const encrypted = await streamToArrayBuffer(
+ encryptStream(blobStream(blob), key)
+ );
+ await setFileList(this.bearerToken, kid, encrypted);
+ } catch (e) {
+ if (e.message === '401') {
+ return retry(e);
+ }
+ }
+ return changes;
+ }
+
+ toJSON() {
+ return this.info;
+ }
+}
diff --git a/app/utils.js b/app/utils.js
index 06f480f9..80ed462a 100644
--- a/app/utils.js
+++ b/app/utils.js
@@ -1,3 +1,10 @@
+/* global Android */
+let html;
+try {
+ html = require('choo/html');
+} catch (e) {
+ // running in the service worker
+}
const b64 = require('base64-js');
function arrayToB64(array) {
@@ -9,25 +16,11 @@ function arrayToB64(array) {
}
function b64ToArray(str) {
- str = (str + '==='.slice((str.length + 3) % 4))
- .replace(/-/g, '+')
- .replace(/_/g, '/');
- return b64.toByteArray(str);
+ return b64.toByteArray(str + '==='.slice((str.length + 3) % 4));
}
-function notify(str) {
- return str;
- /* TODO: enable once we have an opt-in ui element
- if (!('Notification' in window)) {
- return;
- } else if (Notification.permission === 'granted') {
- new Notification(str);
- } else if (Notification.permission !== 'denied') {
- Notification.requestPermission(function(permission) {
- if (permission === 'granted') new Notification(str);
- });
- }
- */
+function locale() {
+ return document.querySelector('html').lang;
}
function loadShim(polyfill) {
@@ -40,34 +33,8 @@ function loadShim(polyfill) {
});
}
-async function canHasSend(polyfill) {
- try {
- const key = await window.crypto.subtle.generateKey(
- {
- name: 'AES-GCM',
- length: 128
- },
- true,
- ['encrypt', 'decrypt']
- );
-
- await window.crypto.subtle.encrypt(
- {
- name: 'AES-GCM',
- iv: window.crypto.getRandomValues(new Uint8Array(12)),
- tagLength: 128
- },
- key,
- new ArrayBuffer(8)
- );
- return true;
- } catch (err) {
- return loadShim(polyfill);
- }
-}
-
function isFile(id) {
- return /^[0-9a-fA-F]{10}$/.test(id);
+ return /^[0-9a-fA-F]{10,16}$/.test(id);
}
function copyToClipboard(str) {
@@ -79,7 +46,7 @@ function copyToClipboard(str) {
if (navigator.userAgent.match(/iphone|ipad|ipod/i)) {
const range = document.createRange();
range.selectNodeContents(aux);
- const sel = window.getSelection();
+ const sel = getSelection();
sel.removeAllRanges();
sel.addRange(range);
aux.setSelectionRange(0, str.length);
@@ -98,33 +65,35 @@ const LOCALIZE_NUMBERS = !!(
typeof navigator === 'object'
);
-const UNITS = ['B', 'kB', 'MB', 'GB'];
+const UNITS = ['bytes', 'kb', 'mb', 'gb'];
function bytes(num) {
if (num < 1) {
return '0B';
}
const exponent = Math.min(Math.floor(Math.log10(num) / 3), UNITS.length - 1);
- const n = Number(num / Math.pow(1000, exponent));
- let nStr = n.toFixed(1);
+ const n = Number(num / Math.pow(1024, exponent));
+ const decimalDigits = Math.floor(n) === n ? 0 : 1;
+ let nStr = n.toFixed(decimalDigits);
if (LOCALIZE_NUMBERS) {
try {
- const locale = document.querySelector('html').lang;
- nStr = n.toLocaleString(locale, {
- minimumFractionDigits: 1,
- maximumFractionDigits: 1
+ nStr = n.toLocaleString(locale(), {
+ minimumFractionDigits: decimalDigits,
+ maximumFractionDigits: decimalDigits
});
} catch (e) {
// fall through
}
}
- return `${nStr}${UNITS[exponent]}`;
+ return translate('fileSize', {
+ num: nStr,
+ units: translate(UNITS[exponent])
+ });
}
function percent(ratio) {
if (LOCALIZE_NUMBERS) {
try {
- const locale = document.querySelector('html').lang;
- return ratio.toLocaleString(locale, { style: 'percent' });
+ return ratio.toLocaleString(locale(), { style: 'percent' });
} catch (e) {
// fall through
}
@@ -132,6 +101,13 @@ function percent(ratio) {
return `${Math.floor(ratio * 100)}%`;
}
+function number(n) {
+ if (LOCALIZE_NUMBERS) {
+ return n.toLocaleString(locale());
+ }
+ return n.toString();
+}
+
function allowedCopy() {
const support = !!document.queryCommandSupported;
return support ? document.queryCommandSupported('copy') : false;
@@ -141,26 +117,185 @@ function delay(delay = 100) {
return new Promise(resolve => setTimeout(resolve, delay));
}
-function fadeOut(id) {
- const classes = document.getElementById(id).classList;
- classes.remove('fadeIn');
- classes.add('fadeOut');
+function fadeOut(selector) {
+ const classes = document.querySelector(selector).classList;
+ classes.remove('effect--fadeIn');
+ classes.add('effect--fadeOut');
return delay(300);
}
-const ONE_DAY_IN_MS = 86400000;
+function openLinksInNewTab(links, should = true) {
+ links = links || Array.from(document.querySelectorAll('a:not([target])'));
+ if (should) {
+ links.forEach(l => {
+ l.setAttribute('target', '_blank');
+ l.setAttribute('rel', 'noopener noreferrer');
+ });
+ } else {
+ links.forEach(l => {
+ l.removeAttribute('target');
+ l.removeAttribute('rel');
+ });
+ }
+ return links;
+}
+
+function browserName() {
+ try {
+ // order of these matters
+ if (/firefox/i.test(navigator.userAgent)) {
+ return 'firefox';
+ }
+ if (/edge/i.test(navigator.userAgent)) {
+ return 'edge';
+ }
+ if (/edg/i.test(navigator.userAgent)) {
+ return 'edgium';
+ }
+ if (/trident/i.test(navigator.userAgent)) {
+ return 'ie';
+ }
+ if (/chrome/i.test(navigator.userAgent)) {
+ return 'chrome';
+ }
+ if (/safari/i.test(navigator.userAgent)) {
+ return 'safari';
+ }
+ if (/send android/i.test(navigator.userAgent)) {
+ return 'android-app';
+ }
+ return 'other';
+ } catch (e) {
+ return 'unknown';
+ }
+}
+
+async function streamToArrayBuffer(stream, size) {
+ const reader = stream.getReader();
+ let state = await reader.read();
+
+ if (size) {
+ const result = new Uint8Array(size);
+ let offset = 0;
+ while (!state.done) {
+ result.set(state.value, offset);
+ offset += state.value.length;
+ state = await reader.read();
+ }
+ return result.buffer;
+ }
+
+ const parts = [];
+ let len = 0;
+ while (!state.done) {
+ parts.push(state.value);
+ len += state.value.length;
+ state = await reader.read();
+ }
+ let offset = 0;
+ const result = new Uint8Array(len);
+ for (const part of parts) {
+ result.set(part, offset);
+ offset += part.length;
+ }
+ return result.buffer;
+}
+
+function list(items, ulStyle = '', liStyle = '') {
+ const lis = items.map(
+ i =>
+ html`
+
${i}
+ `
+ );
+ return html`
+
+ `;
+}
+
+function secondsToL10nId(seconds) {
+ if (seconds < 3600) {
+ return { id: 'timespanMinutes', num: Math.floor(seconds / 60) };
+ } else if (seconds < 86400) {
+ return { id: 'timespanHours', num: Math.floor(seconds / 3600) };
+ } else {
+ return { id: 'timespanDays', num: Math.floor(seconds / 86400) };
+ }
+}
+
+function timeLeft(milliseconds) {
+ if (milliseconds < 1) {
+ return { id: 'linkExpiredAlt' };
+ }
+ const minutes = Math.floor(milliseconds / 1000 / 60);
+ const hours = Math.floor(minutes / 60);
+ const days = Math.floor(hours / 24);
+ if (days >= 1) {
+ return {
+ id: 'expiresDaysHoursMinutes',
+ days,
+ hours: hours % 24,
+ minutes: minutes % 60
+ };
+ }
+ if (hours >= 1) {
+ return {
+ id: 'expiresHoursMinutes',
+ hours,
+ minutes: minutes % 60
+ };
+ } else if (hours === 0) {
+ if (minutes === 0) {
+ return { id: 'expiresMinutes', minutes: '< 1' };
+ }
+ return { id: 'expiresMinutes', minutes };
+ }
+ return null;
+}
+
+function platform() {
+ if (typeof Android === 'object') {
+ return 'android';
+ }
+ return 'web';
+}
+
+const ECE_RECORD_SIZE = 1024 * 64;
+const TAG_LENGTH = 16;
+function encryptedSize(size, rs = ECE_RECORD_SIZE, tagLength = TAG_LENGTH) {
+ const chunk_meta = tagLength + 1; // Chunk metadata, tag and delimiter
+ return 21 + size + chunk_meta * Math.ceil(size / (rs - chunk_meta));
+}
+
+let translate = function() {
+ throw new Error('uninitialized translate function. call setTranslate first');
+};
+function setTranslate(t) {
+ translate = t;
+}
module.exports = {
+ locale,
fadeOut,
delay,
allowedCopy,
bytes,
percent,
+ number,
copyToClipboard,
arrayToB64,
b64ToArray,
- notify,
- canHasSend,
+ loadShim,
isFile,
- ONE_DAY_IN_MS
+ openLinksInNewTab,
+ browserName,
+ streamToArrayBuffer,
+ list,
+ secondsToL10nId,
+ timeLeft,
+ platform,
+ encryptedSize,
+ setTranslate
};
diff --git a/app/zip.js b/app/zip.js
new file mode 100644
index 00000000..57d751cb
--- /dev/null
+++ b/app/zip.js
@@ -0,0 +1,186 @@
+import crc32 from 'crc/crc32';
+
+const encoder = new TextEncoder();
+
+function dosDateTime(dateTime = new Date()) {
+ const year = (dateTime.getFullYear() - 1980) << 9;
+ const month = (dateTime.getMonth() + 1) << 5;
+ const day = dateTime.getDate();
+ const date = year | month | day;
+ const hour = dateTime.getHours() << 11;
+ const minute = dateTime.getMinutes() << 5;
+ const second = Math.floor(dateTime.getSeconds() / 2);
+ const time = hour | minute | second;
+
+ return { date, time };
+}
+
+class File {
+ constructor(info) {
+ this.name = encoder.encode(info.name);
+ this.size = info.size;
+ this.bytesRead = 0;
+ this.crc = null;
+ this.dateTime = dosDateTime();
+ }
+
+ get header() {
+ const h = new ArrayBuffer(30 + this.name.byteLength);
+ const v = new DataView(h);
+ v.setUint32(0, 0x04034b50, true); // sig
+ v.setUint16(4, 20, true); // version
+ v.setUint16(6, 0x808, true); // bit flags (use data descriptor(8) + utf8-encoded(8 << 8))
+ v.setUint16(8, 0, true); // compression
+ v.setUint16(10, this.dateTime.time, true); // modified time
+ v.setUint16(12, this.dateTime.date, true); // modified date
+ v.setUint32(14, 0, true); // crc32 (in descriptor)
+ v.setUint32(18, 0, true); // compressed size (in descriptor)
+ v.setUint32(22, 0, true); // uncompressed size (in descriptor)
+ v.setUint16(26, this.name.byteLength, true); // name length
+ v.setUint16(28, 0, true); // extra field length
+ for (let i = 0; i < this.name.byteLength; i++) {
+ v.setUint8(30 + i, this.name[i]);
+ }
+ return new Uint8Array(h);
+ }
+
+ get dataDescriptor() {
+ const dd = new ArrayBuffer(16);
+ const v = new DataView(dd);
+ v.setUint32(0, 0x08074b50, true); // sig
+ v.setUint32(4, this.crc, true); // crc32
+ v.setUint32(8, this.size, true); // compressed size
+ v.setUint32(12, this.size, true); // uncompressed size
+ return new Uint8Array(dd);
+ }
+
+ directoryRecord(offset) {
+ const dr = new ArrayBuffer(46 + this.name.byteLength);
+ const v = new DataView(dr);
+ v.setUint32(0, 0x02014b50, true); // sig
+ v.setUint16(4, 20, true); // version made
+ v.setUint16(6, 20, true); // version required
+ v.setUint16(8, 0x808, true); // bit flags (use data descriptor(8) + utf8-encoded(8 << 8))
+ v.setUint16(10, 0, true); // compression
+ v.setUint16(12, this.dateTime.time, true); // modified time
+ v.setUint16(14, this.dateTime.date, true); // modified date
+ v.setUint32(16, this.crc, true); // crc
+ v.setUint32(20, this.size, true); // compressed size
+ v.setUint32(24, this.size, true); // uncompressed size
+ v.setUint16(28, this.name.byteLength, true); // name length
+ v.setUint16(30, 0, true); // extra length
+ v.setUint16(32, 0, true); // comment length
+ v.setUint16(34, 0, true); // disk number
+ v.setUint16(36, 0, true); // internal file attrs
+ v.setUint32(38, 0, true); // external file attrs
+ v.setUint32(42, offset, true); // file offset
+ for (let i = 0; i < this.name.byteLength; i++) {
+ v.setUint8(46 + i, this.name[i]);
+ }
+ return new Uint8Array(dr);
+ }
+
+ get byteLength() {
+ return this.size + this.name.byteLength + 30 + 16;
+ }
+
+ append(data, controller) {
+ this.bytesRead += data.byteLength;
+ const endIndex = data.byteLength - Math.max(this.bytesRead - this.size, 0);
+ const buf = data.slice(0, endIndex);
+ this.crc = crc32(buf, this.crc);
+ controller.enqueue(buf);
+ if (endIndex < data.byteLength) {
+ return data.slice(endIndex, data.byteLength);
+ }
+ }
+}
+
+function centralDirectory(files, controller) {
+ let directoryOffset = 0;
+ let directorySize = 0;
+ for (let i = 0; i < files.length; i++) {
+ const file = files[i];
+ const record = file.directoryRecord(directoryOffset);
+ directoryOffset += file.byteLength;
+ controller.enqueue(record);
+ directorySize += record.byteLength;
+ }
+ controller.enqueue(eod(files.length, directorySize, directoryOffset));
+}
+
+function eod(fileCount, directorySize, directoryOffset) {
+ const e = new ArrayBuffer(22);
+ const v = new DataView(e);
+ v.setUint32(0, 0x06054b50, true); // sig
+ v.setUint16(4, 0, true); // disk number
+ v.setUint16(6, 0, true); // directory disk
+ v.setUint16(8, fileCount, true); // number of records
+ v.setUint16(10, fileCount, true); // total records
+ v.setUint32(12, directorySize, true); // size of directory
+ v.setUint32(16, directoryOffset, true); // offset of directory
+ v.setUint16(20, 0, true); // comment length
+ return new Uint8Array(e);
+}
+
+class ZipStreamController {
+ constructor(files, source) {
+ this.files = files;
+ this.fileIndex = 0;
+ this.file = null;
+ this.reader = source.getReader();
+ this.nextFile();
+ this.extra = null;
+ }
+
+ nextFile() {
+ this.file = this.files[this.fileIndex++];
+ }
+
+ async pull(controller) {
+ if (!this.file) {
+ // end of archive
+ centralDirectory(this.files, controller);
+ return controller.close();
+ }
+ if (this.file.bytesRead === 0) {
+ // beginning of file
+ controller.enqueue(this.file.header);
+ if (this.extra) {
+ this.extra = this.file.append(this.extra, controller);
+ }
+ }
+ if (this.file.bytesRead >= this.file.size) {
+ // end of file
+ controller.enqueue(this.file.dataDescriptor);
+ this.nextFile();
+ return this.pull(controller);
+ }
+ const data = await this.reader.read();
+ if (data.done) {
+ this.nextFile();
+ return this.pull(controller);
+ }
+ this.extra = this.file.append(data.value, controller);
+ }
+}
+
+export default class Zip {
+ constructor(manifest, source) {
+ this.files = manifest.files.map(info => new File(info));
+ this.source = source;
+ }
+
+ get stream() {
+ return new ReadableStream(new ZipStreamController(this.files, this.source));
+ }
+
+ get size() {
+ const entries = this.files.reduce(
+ (total, file) => total + file.byteLength * 2 - file.size,
+ 0
+ );
+ const eod = 22;
+ return entries + eod;
+ }
+}
diff --git a/assets/add.svg b/assets/add.svg
new file mode 100644
index 00000000..84db8c30
--- /dev/null
+++ b/assets/add.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/addfiles.svg b/assets/addfiles.svg
new file mode 100644
index 00000000..3d6749d9
--- /dev/null
+++ b/assets/addfiles.svg
@@ -0,0 +1,9 @@
+
diff --git a/assets/android-chrome-192x192.png b/assets/android-chrome-192x192.png
new file mode 100644
index 00000000..044cba97
Binary files /dev/null and b/assets/android-chrome-192x192.png differ
diff --git a/assets/android-chrome-512x512.png b/assets/android-chrome-512x512.png
new file mode 100644
index 00000000..553bda11
Binary files /dev/null and b/assets/android-chrome-512x512.png differ
diff --git a/assets/apple-touch-icon.png b/assets/apple-touch-icon.png
new file mode 100644
index 00000000..ddb56ec1
Binary files /dev/null and b/assets/apple-touch-icon.png differ
diff --git a/assets/bg.svg b/assets/bg.svg
new file mode 100644
index 00000000..02457110
--- /dev/null
+++ b/assets/bg.svg
@@ -0,0 +1,31 @@
+
+
\ No newline at end of file
diff --git a/assets/blue_file.svg b/assets/blue_file.svg
new file mode 100644
index 00000000..2945487f
--- /dev/null
+++ b/assets/blue_file.svg
@@ -0,0 +1,24 @@
+
+
diff --git a/assets/check-16-blue.svg b/assets/check-16-blue.svg
deleted file mode 100644
index a2633292..00000000
--- a/assets/check-16-blue.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/check-16.svg b/assets/check-16.svg
deleted file mode 100644
index 4243e0dc..00000000
--- a/assets/check-16.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/close-16.svg b/assets/close-16.svg
index 21207a15..9a9da6d3 100644
--- a/assets/close-16.svg
+++ b/assets/close-16.svg
@@ -1 +1 @@
-
+
diff --git a/assets/completed.svg b/assets/completed.svg
new file mode 100644
index 00000000..4c693d3d
--- /dev/null
+++ b/assets/completed.svg
@@ -0,0 +1,260 @@
+
+
diff --git a/assets/copy-16.svg b/assets/copy-16.svg
index c5cee826..56bed4a8 100644
--- a/assets/copy-16.svg
+++ b/assets/copy-16.svg
@@ -1 +1,5 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/cryptofill.js b/assets/cryptofill.js
deleted file mode 100644
index db29a332..00000000
--- a/assets/cryptofill.js
+++ /dev/null
@@ -1,22 +0,0 @@
-var liner=function(e){function r(n){if(t[n])return t[n].exports;var a=t[n]={i:n,l:!1,exports:{}};return e[n].call(a.exports,a,a.exports,r),a.l=!0,a.exports}var t={};return r.m=e,r.c=t,r.i=function(e){return e},r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:n})},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},r.p="",r(r.s=16)}([function(e,r,t){"use strict";Object.defineProperty(r,"__esModule",{value:!0}),function(e){function n(e){for(var r=[],t=1;t
=0;s--){var c=i[s],u=c.arg.substring(1),h=c.index+1;a=a.substring(0,h)+arguments[+u]+a.substring(h+1+u.length)}return a=a.replace("%%","%")}function a(e){var r;r="string"==typeof e?{name:e}:e,h.checkAlgorithm(r);var t=e;return t.hash&&(t.hash=a(t.hash)),r}function o(e,r){if(!e)throw new s("Parameter '"+r+"' is required and cant be empty");if("undefined"!=typeof Buffer&&Buffer.isBuffer(e))return new Uint8Array(e);if(ArrayBuffer.isView(e))return new Uint8Array(e.buffer);if(e instanceof ArrayBuffer)return new Uint8Array(e);throw new s("Incoming parameter '"+r+"' has wrong data type. Must be ArrayBufferView or ArrayBuffer")}t.d(r,"WebCryptoError",function(){return s}),t.d(r,"AlgorithmError",function(){return c}),t.d(r,"CryptoKeyError",function(){return u}),t.d(r,"PrepareAlgorithm",function(){return a}),t.d(r,"PrepareData",function(){return o}),t.d(r,"BaseCrypto",function(){return h}),t.d(r,"AlgorithmNames",function(){return p}),t.d(r,"Base64Url",function(){return l}),t.d(r,"SubtleCrypto",function(){return W}),t.d(r,"Aes",function(){return m}),t.d(r,"AesAlgorithmError",function(){return A}),t.d(r,"AesWrapKey",function(){return w}),t.d(r,"AesEncrypt",function(){return v}),t.d(r,"AesECB",function(){return g}),t.d(r,"AesCBC",function(){return C}),t.d(r,"AesCTR",function(){return d}),t.d(r,"AesGCM",function(){return E}),t.d(r,"AesKW",function(){return P}),t.d(r,"RsaKeyGenParamsError",function(){return N}),t.d(r,"RsaHashedImportParamsError",function(){return G}),t.d(r,"Rsa",function(){return M}),t.d(r,"RsaSSA",function(){return B}),t.d(r,"RsaPSSParamsError",function(){return T}),t.d(r,"RsaPSS",function(){return D}),t.d(r,"RsaOAEPParamsError",function(){return x}),t.d(r,"RsaOAEP",function(){return H}),t.d(r,"EcKeyGenParamsError",function(){return S}),t.d(r,"Ec",function(){return _}),t.d(r,"EcAlgorithmError",function(){return U}),t.d(r,"EcDSA",function(){return L}),t.d(r,"EcDH",function(){return R}),t.d(r,"ShaAlgorithms",function(){return k}),t.d(r,"Sha",function(){return b});var i=t(6),s=function(e){function r(r){for(var t=[],a=1;a0&&e.length<=128))throw new A(A.PARAM_WRONG_VALUE,"length","number [1-128]")},r}(v);d.ALG_NAME=p.AesCTR;var E=function(e){function r(){return null!==e&&e.apply(this,arguments)||this}return t.i(i.a)(r,e),r.checkAlgorithmParams=function(e){if(this.checkAlgorithm(e),e.additionalData&&!(ArrayBuffer.isView(e.additionalData)||e.additionalData instanceof ArrayBuffer))throw new A(A.PARAM_WRONG_TYPE,"additionalData","ArrayBufferView or ArrayBuffer");if(!e.iv)throw new A(A.PARAM_REQUIRED,"iv");if(!(ArrayBuffer.isView(e.iv)||e.iv instanceof ArrayBuffer))throw new A(A.PARAM_WRONG_TYPE,"iv","ArrayBufferView or ArrayBuffer");if(e.tagLength){if(![32,64,96,104,112,120,128].some(function(r){return r===e.tagLength}))throw new A(A.PARAM_WRONG_VALUE,"tagLength","32, 64, 96, 104, 112, 120 or 128")}},r}(v);E.ALG_NAME=p.AesGCM;var P=function(e){function r(){return null!==e&&e.apply(this,arguments)||this}return t.i(i.a)(r,e),r.checkAlgorithmParams=function(e){this.checkAlgorithm(e)},r}(w);P.ALG_NAME=p.AesKW,P.KEY_USAGES=["wrapKey","unwrapKey"];var k=[p.Sha1,p.Sha256,p.Sha384,p.Sha512].join(" | "),b=function(e){function r(){return null!==e&&e.apply(this,arguments)||this}return t.i(i.a)(r,e),r.checkAlgorithm=function(r){var t;switch(t="string"==typeof r?{name:r}:r,e.checkAlgorithm.call(this,t),t.name.toUpperCase()){case p.Sha1:case p.Sha256:case p.Sha384:case p.Sha512:break;default:throw new c(c.WRONG_ALG_NAME,t.name,k)}},r.digest=function(e,r){var t=this;return new Promise(function(r,n){t.checkAlgorithm(e),r(void 0)})},r}(h),S=function(e){function r(){var r=null!==e&&e.apply(this,arguments)||this;return r.code=9,r}return t.i(i.a)(r,e),r}(c),_=function(e){function r(){return null!==e&&e.apply(this,arguments)||this}return t.i(i.a)(r,e),r.checkAlgorithm=function(e){if(e.name.toUpperCase()!==this.ALG_NAME.toUpperCase())throw new c(c.WRONG_ALG_NAME,e.name,this.ALG_NAME)},r.checkKeyGenParams=function(e){if(!e.namedCurve)throw new S(S.PARAM_REQUIRED,"namedCurve");if("string"!=typeof e.namedCurve)throw new S(S.PARAM_WRONG_TYPE,"namedCurve","string");switch(e.namedCurve.toUpperCase()){case"P-256":case"P-384":case"P-521":break;default:throw new S(S.PARAM_WRONG_VALUE,"namedCurve","P-256, P-384 or P-521")}},r.checkKeyGenUsages=function(e){var r=this;e.forEach(function(e){var t=0;for(t;t0&&e.length<=512))throw new c(c.PARAM_WRONG_VALUE,"length","more 0 and less than 512")},r.checkKeyGenUsages=function(e){var r=this;this.checkKeyUsages(e),e.forEach(function(e){var t=0;for(t;t=256&&r<=16384)||r%8)throw new N(N.PARAM_WRONG_VALUE,"modulusLength"," a multiple of 8 bits and >= 256 and <= 16384");var t=e.publicExponent;if(!t)throw new N(N.PARAM_REQUIRED,"publicExponent");if(!ArrayBuffer.isView(t))throw new N(N.PARAM_WRONG_TYPE,"publicExponent","ArrayBufferView");if(3!==t[0]&&(1!==t[0]||0!==t[1]||1!==t[2]))throw new N(N.PARAM_WRONG_VALUE,"publicExponent","Uint8Array([3]) | Uint8Array([1, 0, 1])");if(!e.hash)throw new N(N.PARAM_REQUIRED,"hash",k);b.checkAlgorithm(e.hash)},r.checkKeyGenUsages=function(e){var r=this;this.checkKeyUsages(e),e.forEach(function(e){var t=0;for(t;t>>16&65535,n=65535&e,a=r>>>16&65535,o=65535&r;return n*o+(t*o+n*a<<16>>>0)|0})},function(e,r,t){"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n=function(){function e(){}return e}();r.CryptoKey=n},function(e,r,t){"use strict";function n(e,r){function t(){this.constructor=e}a(e,r),e.prototype=null===r?Object.create(r):(t.prototype=r.prototype,new t)}r.a=n;/*! *****************************************************************************
-Copyright (c) Microsoft Corporation. All rights reserved.
-Licensed under the Apache License, Version 2.0 (the "License"); you may not use
-this file except in compliance with the License. You may obtain a copy of the
-License at http://www.apache.org/licenses/LICENSE-2.0
-
-THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
-WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
-MERCHANTABLITY OR NON-INFRINGEMENT.
-
-See the Apache Version 2.0 License for specific language governing permissions
-and limitations under the License.
-***************************************************************************** */
-var a=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,r){e.__proto__=r}||function(e,r){for(var t in r)r.hasOwnProperty(t)&&(e[t]=r[t])};Object.assign},function(e,r,t){"use strict";function n(e){for(var t in e)r.hasOwnProperty(t)||(r[t]=e[t])}Object.defineProperty(r,"__esModule",{value:!0}),n(t(4)),n(t(2))},function(e,r,t){"use strict";var n=this&&this.__extends||function(){var e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,r){e.__proto__=r}||function(e,r){for(var t in r)r.hasOwnProperty(t)&&(e[t]=r[t])};return function(r,t){function n(){this.constructor=r}e(r,t),r.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}}();Object.defineProperty(r,"__esModule",{value:!0});var a=t(0),o=t(1),i=t(5),s=t(3),c=t(4),u=function(e){function r(){return null!==e&&e.apply(this,arguments)||this}return n(r,e),r.generateKey=function(e,r,t){var n=this;return Promise.resolve().then(function(){n.checkModule();var a=c.nativeCrypto.getRandomValues(new Uint8Array(e.length/8)),o=new i.CryptoKey;return o.key=a,o.algorithm=e,o.extractable=r,o.type="secret",o.usages=t,o})},r.encrypt=function(e,r,t){return Promise.resolve().then(function(){var n;switch(e.name.toUpperCase()){case a.AlgorithmNames.AesECB:;n=asmCrypto.AES_ECB.encrypt(t,r.key,!0);break;case a.AlgorithmNames.AesCBC:var i=e;n=asmCrypto.AES_CBC.encrypt(t,r.key,void 0,a.PrepareData(i.iv,"iv"));break;case a.AlgorithmNames.AesGCM:var s=e;s.tagLength=s.tagLength||128;var c=void 0;s.additionalData&&(c=a.PrepareData(s.additionalData,"additionalData")),n=asmCrypto.AES_GCM.encrypt(t,r.key,s.iv,c,s.tagLength/8);break;default:throw new o.LinerError(a.AlgorithmError.UNSUPPORTED_ALGORITHM,e.name)}return n.buffer})},r.decrypt=function(e,r,t){return Promise.resolve().then(function(){var n;switch(e.name.toUpperCase()){case a.AlgorithmNames.AesECB:;n=asmCrypto.AES_ECB.decrypt(t,r.key,!0);break;case a.AlgorithmNames.AesCBC:var i=e;n=asmCrypto.AES_CBC.decrypt(t,r.key,void 0,a.PrepareData(i.iv,"iv"));break;case a.AlgorithmNames.AesGCM:var s=e;s.tagLength=s.tagLength||128;var c=void 0;s.additionalData&&(c=a.PrepareData(s.additionalData,"additionalData")),n=asmCrypto.AES_GCM.decrypt(t,r.key,s.iv,c,s.tagLength/8);break;default:throw new o.LinerError(a.AlgorithmError.UNSUPPORTED_ALGORITHM,e.name)}return n.buffer})},r.wrapKey=function(e,r,t,n){var a;return Promise.resolve().then(function(){return a=new h.Crypto,a.subtle.exportKey(e,r)}).then(function(e){var r;return r=e instanceof ArrayBuffer?new Uint8Array(e):s.string2buffer(JSON.stringify(e)),a.subtle.encrypt(n,t,r)})},r.unwrapKey=function(e,r,t,n,a,o,i){var c;return Promise.resolve().then(function(){return c=new h.Crypto,c.subtle.decrypt(n,t,r)}).then(function(r){var t;return t="jwk"===e.toLowerCase()?JSON.parse(s.buffer2string(new Uint8Array(r))):new Uint8Array(r),c.subtle.importKey(e,t,a,o,i)})},r.alg2jwk=function(e){return"A"+e.length+/-(\w+)/i.exec(e.name.toUpperCase())[1]},r.jwk2alg=function(e){throw new Error("Not implemented")},r.exportKey=function(e,r){var t=this;return Promise.resolve().then(function(){var n=r.key;if("jwk"===e.toLowerCase()){return{alg:t.alg2jwk(r.algorithm),ext:r.extractable,k:a.Base64Url.encode(n),key_ops:r.usages,kty:"oct"}}return n.buffer})},r.importKey=function(e,r,t,n,o){return Promise.resolve().then(function(){var n;if("jwk"===e.toLowerCase()){var s=r;n=a.Base64Url.decode(s.k)}else n=new Uint8Array(r);var c=new i.CryptoKey;return c.algorithm=t,c.type="secret",c.usages=o,c.key=n,c})},r.checkModule=function(){if("undefined"==typeof asmCrypto)throw new o.LinerError(o.LinerError.MODULE_NOT_FOUND,"asmCrypto","https://github.com/vibornoff/asmcrypto.js")},r}(a.BaseCrypto);r.AesCrypto=u;var h=t(2)},function(e,r,t){"use strict";function n(e){for(var r=new Uint8Array(e),t=[],n=0;n32?o>48?66:48:32,t.length32?o>48?66:48:32,t.length/232?o>48?66:48:32,a.length-1&&("public"===e.type||"secret"===e.type)&&e.usages.push(t)}),["sign","decrypt","unwrapKey","deriveKey","deriveBits"].forEach(function(t){r.indexOf(t)>-1&&("private"===e.type||"secret"===e.type)&&e.usages.push(t)})))})}function s(e,r,t){if(r&&A.BrowserInfo().name===A.Browser.IE){"extractable"in e&&(e.ext=e.extractable,delete e.extractable);var n=null;switch(r.name.toUpperCase()){case h.AlgorithmNames.RsaOAEP.toUpperCase():case h.AlgorithmNames.RsaPSS.toUpperCase():case h.AlgorithmNames.RsaSSA.toUpperCase():n=g.RsaCrypto;break;case h.AlgorithmNames.AesECB.toUpperCase():case h.AlgorithmNames.AesCBC.toUpperCase():case h.AlgorithmNames.AesGCM.toUpperCase():n=w.AesCrypto;break;default:throw new m.LinerError(m.LinerError.UNSUPPORTED_ALGORITHM,r.name.toUpperCase())}n&&!e.alg&&(e.alg=n.alg2jwk(r)),"key_ops"in e||(e.key_ops=t)}}function c(e){A.BrowserInfo().name===A.Browser.IE&&("ext"in e&&(e.extractable=e.ext,delete e.ext),delete e.key_ops,delete e.alg)}var u=this&&this.__extends||function(){var e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,r){e.__proto__=r}||function(e,r){for(var t in r)r.hasOwnProperty(t)&&(e[t]=r[t])};return function(r,t){function n(){this.constructor=r}e(r,t),r.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}}();Object.defineProperty(r,"__esModule",{value:!0});var h=t(0),p=t(0),f=t(0),l=t(4),y=t(2),m=t(1),A=t(3),w=t(8),v=t(11),g=t(10),C=t(9),d=[],E=function(e){function r(){return null!==e&&e.apply(this,arguments)||this}return u(r,e),r.prototype.generateKey=function(r,t,n){var o,s=arguments;return e.prototype.generateKey.apply(this,s).then(function(e){if(o=f.PrepareAlgorithm(r),(A.BrowserInfo().name!==A.Browser.Edge||o.name.toUpperCase()!==h.AlgorithmNames.AesGCM)&&l.nativeSubtle)try{return l.nativeSubtle.generateKey.apply(l.nativeSubtle,s).catch(function(e){console.warn("WebCrypto: native generateKey for "+o.name+" doesn't work.",e.message||"")})}catch(e){console.warn("WebCrypto: native generateKey for "+o.name+" doesn't work.",e.message||"")}}).then(function(e){if(e)return i(e,n),a(o,e),e;var r;switch(o.name.toLowerCase()){case h.AlgorithmNames.AesECB.toLowerCase():case h.AlgorithmNames.AesCBC.toLowerCase():case h.AlgorithmNames.AesGCM.toLowerCase():r=w.AesCrypto;break;case h.AlgorithmNames.EcDSA.toLowerCase():case h.AlgorithmNames.EcDH.toLowerCase():r=C.EcCrypto;break;case h.AlgorithmNames.RsaOAEP.toLowerCase():case h.AlgorithmNames.RsaPSS.toLowerCase():case h.AlgorithmNames.RsaSSA.toLowerCase():r=g.RsaCrypto;break;default:throw new m.LinerError(m.LinerError.UNSUPPORTED_ALGORITHM,o.name.toLowerCase())}return r.generateKey(o,t,n)})},r.prototype.digest=function(r,t){var n,a,o=arguments;return e.prototype.digest.apply(this,o).then(function(e){if(n=f.PrepareAlgorithm(r),a=f.PrepareData(t,"data"),l.nativeSubtle)try{return l.nativeSubtle.digest.apply(l.nativeSubtle,o).catch(function(e){console.warn("WebCrypto: native digest for "+n.name+" doesn't work.",e.message||"")})}catch(e){console.warn("WebCrypto: native digest for "+n.name+" doesn't work.",e.message||"")}}).then(function(e){return e||v.ShaCrypto.digest(n,a)})},r.prototype.sign=function(r,t,a){var i,s,c=arguments;return e.prototype.sign.apply(this,c).then(function(e){i=f.PrepareAlgorithm(r),s=f.PrepareData(a,"data");var n=o(t);if(n&&(c[0]=A.assign(i,n)),l.nativeSubtle)try{return l.nativeSubtle.sign.apply(l.nativeSubtle,c).catch(function(e){console.warn("WebCrypto: native sign for "+i.name+" doesn't work.",e.message||"")})}catch(e){console.warn("WebCrypto: native sign for "+i.name+" doesn't work.",e.message||"")}}).then(function(e){if(e)return e;var r;switch(i.name.toLowerCase()){case h.AlgorithmNames.EcDSA.toLowerCase():r=C.EcCrypto;break;case h.AlgorithmNames.RsaSSA.toLowerCase():case h.AlgorithmNames.RsaPSS.toLowerCase():r=g.RsaCrypto;break;default:throw new m.LinerError(m.LinerError.UNSUPPORTED_ALGORITHM,i.name.toLowerCase())}return n(t,r).then(function(e){return r.sign(i,e,s)})})},r.prototype.verify=function(r,t,a,i){var s,c,u,p=arguments;return e.prototype.verify.apply(this,p).then(function(e){s=f.PrepareAlgorithm(r),c=f.PrepareData(a,"data"),u=f.PrepareData(i,"data");var n=o(t);if(n&&(p[0]=A.assign(s,n)),l.nativeSubtle)try{return l.nativeSubtle.verify.apply(l.nativeSubtle,p).catch(function(e){console.warn("WebCrypto: native verify for "+s.name+" doesn't work.",e.message||"")})}catch(e){console.warn("WebCrypto: native verify for "+s.name+" doesn't work.",e.message||"")}}).then(function(e){if("boolean"==typeof e)return e;var r;switch(s.name.toLowerCase()){case h.AlgorithmNames.EcDSA.toLowerCase():r=C.EcCrypto;break;case h.AlgorithmNames.RsaSSA.toLowerCase():case h.AlgorithmNames.RsaPSS.toLowerCase():r=g.RsaCrypto;break;default:throw new m.LinerError(m.LinerError.UNSUPPORTED_ALGORITHM,s.name.toLowerCase())}return n(t,r).then(function(e){return r.verify(s,e,c,u)})})},r.prototype.deriveBits=function(r,t,n){var a,o=arguments;return e.prototype.deriveBits.apply(this,o).then(function(e){if(a=f.PrepareAlgorithm(r),l.nativeSubtle)try{return l.nativeSubtle.deriveBits.apply(l.nativeSubtle,o).catch(function(e){console.warn("WebCrypto: native deriveBits for "+a.name+" doesn't work.",e.message||"")})}catch(e){console.warn("WebCrypto: native deriveBits for "+a.name+" doesn't work.",e.message||"")}}).then(function(e){if(e)return e;var r;switch(a.name.toLowerCase()){case h.AlgorithmNames.EcDH.toLowerCase():r=C.EcCrypto;break;default:throw new m.LinerError(m.LinerError.NOT_SUPPORTED,"deriveBits")}return r.deriveBits(a,t,n)})},r.prototype.deriveKey=function(r,t,n,a,o){var s,c,u=arguments;return e.prototype.deriveKey.apply(this,u).then(function(e){if(s=f.PrepareAlgorithm(r),c=f.PrepareAlgorithm(n),l.nativeSubtle)try{return l.nativeSubtle.deriveKey.apply(l.nativeSubtle,u).catch(function(e){console.warn("WebCrypto: native deriveKey for "+s.name+" doesn't work.",e.message||"")})}catch(e){console.warn("WebCrypto: native deriveKey for "+s.name+" doesn't work.",e.message||"")}}).then(function(e){if(e)return i(e,o),e;var r;switch(s.name.toLowerCase()){case h.AlgorithmNames.EcDH.toLowerCase():r=C.EcCrypto;break;default:throw new m.LinerError(m.LinerError.NOT_SUPPORTED,"deriveBits")}return r.deriveKey(s,t,c,a,o)})},r.prototype.encrypt=function(r,t,a){var o,i,s=arguments;return e.prototype.encrypt.apply(this,s).then(function(e){if(o=f.PrepareAlgorithm(r),i=f.PrepareData(a,"data"),l.nativeSubtle)try{return l.nativeSubtle.encrypt.apply(l.nativeSubtle,s).catch(function(e){console.warn("WebCrypto: native 'encrypt' for "+o.name+" doesn't work.",e.message||"")})}catch(e){console.warn("WebCrypto: native 'encrypt' for "+o.name+" doesn't work.",e.message||"")}}).then(function(e){if(e){if(A.BrowserInfo().name===A.Browser.IE&&o.name.toUpperCase()===h.AlgorithmNames.AesGCM&&e.ciphertext){var r=new Uint8Array(e.ciphertext.byteLength+e.tag.byteLength),a=0;new Uint8Array(e.ciphertext).forEach(function(e){return r[a++]=e}),new Uint8Array(e.tag).forEach(function(e){return r[a++]=e}),e=r.buffer}return Promise.resolve(e)}var s;switch(o.name.toLowerCase()){case h.AlgorithmNames.AesECB.toLowerCase():case h.AlgorithmNames.AesCBC.toLowerCase():case h.AlgorithmNames.AesGCM.toLowerCase():s=w.AesCrypto;break;case h.AlgorithmNames.RsaOAEP.toLowerCase():s=g.RsaCrypto;break;default:throw new m.LinerError(m.LinerError.NOT_SUPPORTED,"encrypt")}return n(t,s).then(function(e){return s.encrypt(o,e,i)})})},r.prototype.decrypt=function(r,t,n){var a,o,i=arguments;return e.prototype.decrypt.apply(this,i).then(function(e){a=f.PrepareAlgorithm(r),o=f.PrepareData(n,"data");var i=o;if(A.BrowserInfo().name===A.Browser.IE&&a.name.toUpperCase()===h.AlgorithmNames.AesGCM){var s=o.byteLength-a.tagLength/8;i={ciphertext:o.buffer.slice(0,s),tag:o.buffer.slice(s,o.byteLength)}}if(t.key){var c=void 0;switch(a.name.toLowerCase()){case h.AlgorithmNames.AesECB.toLowerCase():case h.AlgorithmNames.AesCBC.toLowerCase():case h.AlgorithmNames.AesGCM.toLowerCase():c=w.AesCrypto;break;case h.AlgorithmNames.RsaOAEP.toLowerCase():c=g.RsaCrypto;break;default:throw new m.LinerError(m.LinerError.NOT_SUPPORTED,"decrypt")}return c.decrypt(a,t,o)}return l.nativeSubtle.decrypt.call(l.nativeSubtle,a,t,i)})},r.prototype.wrapKey=function(r,t,n,a){var o,i=arguments;return e.prototype.wrapKey.apply(this,i).then(function(e){if(o=f.PrepareAlgorithm(a),l.nativeSubtle)try{return l.nativeSubtle.wrapKey.apply(l.nativeSubtle,i).catch(function(e){console.warn("WebCrypto: native 'wrapKey' for "+o.name+" doesn't work.",e.message||"")})}catch(e){console.warn("WebCrypto: native 'wrapKey' for "+o.name+" doesn't work.",e.message||"")}}).then(function(e){if(e)return e;var a;switch(o.name.toLowerCase()){case h.AlgorithmNames.AesECB.toLowerCase():case h.AlgorithmNames.AesCBC.toLowerCase():case h.AlgorithmNames.AesGCM.toLowerCase():a=w.AesCrypto;break;case h.AlgorithmNames.RsaOAEP.toLowerCase():a=g.RsaCrypto;break;default:throw new m.LinerError(m.LinerError.NOT_SUPPORTED,"wrapKey")}return a.wrapKey(r,t,n,o)})},r.prototype.unwrapKey=function(r,t,n,a,o,s,c){var u,p,y,v=this,C=arguments;return e.prototype.unwrapKey.apply(this,C).then(function(e){if(u=f.PrepareAlgorithm(a),p=f.PrepareAlgorithm(o),y=f.PrepareData(t,"wrappedKey"),n.key){var d=void 0;switch(u.name.toLowerCase()){case h.AlgorithmNames.AesECB.toLowerCase():case h.AlgorithmNames.AesCBC.toLowerCase():case h.AlgorithmNames.AesGCM.toLowerCase():d=w.AesCrypto;break;case h.AlgorithmNames.RsaOAEP.toLowerCase():d=g.RsaCrypto;break;default:throw new m.LinerError(m.LinerError.NOT_SUPPORTED,"unwrapKey")}return d.unwrapKey(r,y,n,u,p,s,c)}return l.nativeSubtle.unwrapKey.apply(l.nativeSubtle,C).catch(function(e){return v.decrypt(u,n,t).then(function(e){var t;return t="jwk"===r?JSON.parse(A.buffer2string(new Uint8Array(e))):e,v.importKey(r,t,p,s,c)})}).then(function(e){if(e)return i(e,c),e}).catch(function(e){throw console.error(e),new Error("Cannot unwrap key from incoming data")})})},r.prototype.exportKey=function(r,t){var n=arguments;return e.prototype.exportKey.apply(this,n).then(function(){if(l.nativeSubtle)try{return l.nativeSubtle.exportKey.apply(l.nativeSubtle,n).catch(function(e){console.warn("WebCrypto: native 'exportKey' for "+t.algorithm.name+" doesn't work.",e.message||"")})}catch(e){console.warn("WebCrypto: native 'exportKey' for "+t.algorithm.name+" doesn't work.",e.message||"")}}).then(function(e){if(e){"jwk"===r&&e instanceof ArrayBuffer&&(e=A.buffer2string(new Uint8Array(e)),e=JSON.parse(e));var n=o(t);return n||(n=A.assign({},t.algorithm)),s(e,n,t.usages),Promise.resolve(e)}if(!t.key)throw new m.LinerError("Cannot export native CryptoKey from JS implementation");var a;switch(t.algorithm.name.toLowerCase()){case h.AlgorithmNames.AesECB.toLowerCase():case h.AlgorithmNames.AesCBC.toLowerCase():case h.AlgorithmNames.AesGCM.toLowerCase():a=w.AesCrypto;break;case h.AlgorithmNames.EcDH.toLowerCase():case h.AlgorithmNames.EcDSA.toLowerCase():a=C.EcCrypto;break;case h.AlgorithmNames.RsaSSA.toLowerCase():case h.AlgorithmNames.RsaPSS.toLowerCase():case h.AlgorithmNames.RsaOAEP.toLowerCase():a=g.RsaCrypto;break;default:throw new m.LinerError(m.LinerError.UNSUPPORTED_ALGORITHM,t.algorithm.name.toLowerCase())}return a.exportKey(r,t)})},r.prototype.importKey=function(r,t,n,o,s){var u,p,y=arguments;return e.prototype.importKey.apply(this,y).then(function(e){if(u=f.PrepareAlgorithm(n),p=t,A.BrowserInfo().name!==A.Browser.Safari&&A.BrowserInfo().name!==A.Browser.IE||(A.BrowserInfo().name===A.Browser.IE&&(t=A.assign({},t),c(t)),y[1]=A.string2buffer(JSON.stringify(t)).buffer),ArrayBuffer.isView(t)&&(p=f.PrepareData(t,"keyData")),l.nativeSubtle)try{return l.nativeSubtle.importKey.apply(l.nativeSubtle,y).catch(function(e){console.warn("WebCrypto: native 'importKey' for "+u.name+" doesn't work.",e.message||"")})}catch(e){console.warn("WebCrypto: native 'importKey' for "+u.name+" doesn't work.",e.message||"")}}).then(function(e){if(e)return a(u,e),i(e,s),Promise.resolve(e);var t;switch(u.name.toLowerCase()){case h.AlgorithmNames.AesECB.toLowerCase():case h.AlgorithmNames.AesCBC.toLowerCase():case h.AlgorithmNames.AesGCM.toLowerCase():t=w.AesCrypto;break;case h.AlgorithmNames.EcDH.toLowerCase():case h.AlgorithmNames.EcDSA.toLowerCase():t=C.EcCrypto;break;case h.AlgorithmNames.RsaSSA.toLowerCase():case h.AlgorithmNames.RsaPSS.toLowerCase():case h.AlgorithmNames.RsaOAEP.toLowerCase():t=g.RsaCrypto;break;default:throw new m.LinerError(m.LinerError.UNSUPPORTED_ALGORITHM,u.name.toLowerCase())}return t.importKey(r,p,u,o,s)})},r}(p.SubtleCrypto);r.SubtleCrypto=E,Uint8Array.prototype.forEach||(Uint8Array.prototype.forEach=function(e){for(var r=0;re;e++){var g=a.charCodeAt(e);if(b&&g>=55296&&56319>=g){if(++e>=c)throw new Error("Malformed string, low surrogate expected at position "+e);g=(55296^g)<<10|65536|56320^a.charCodeAt(e)}else if(!b&&g>>>8)throw new Error("Wide characters are not allowed.");!b||127>=g?d[f++]=g:2047>=g?(d[f++]=192|g>>6,d[f++]=128|63&g):65535>=g?(d[f++]=224|g>>12,d[f++]=128|g>>6&63,d[f++]=128|63&g):(d[f++]=240|g>>18,d[f++]=128|g>>12&63,d[f++]=128|g>>6&63,d[f++]=128|63&g)}return d.subarray(0,f)}function g(a){var b=a.length;1&b&&(a="0"+a,b++);for(var c=new Uint8Array(b>>1),d=0;b>d;d+=2)c[d>>1]=parseInt(a.substr(d,2),16);return c}function h(a){return f(atob(a))}function i(a,b){b=!!b;for(var c=a.length,d=new Array(c),e=0,f=0;c>e;e++){var g=a[e];if(!b||128>g)d[f++]=g;else if(g>=192&&224>g&&c>e+1)d[f++]=(31&g)<<6|63&a[++e];else if(g>=224&&240>g&&c>e+2)d[f++]=(15&g)<<12|(63&a[++e])<<6|63&a[++e];else{if(!(g>=240&&248>g&&c>e+3))throw new Error("Malformed UTF8 character at byte offset "+e);var h=(7&g)<<18|(63&a[++e])<<12|(63&a[++e])<<6|63&a[++e];65535>=h?d[f++]=h:(h^=65536,d[f++]=55296|h>>10,d[f++]=56320|1023&h)}}for(var i="",j=16384,e=0;f>e;e+=j)i+=String.fromCharCode.apply(String,d.slice(e,f>=e+j?e+j:f));return i}function j(a){for(var b="",c=0;c>>1,a|=a>>>2,a|=a>>>4,a|=a>>>8,a|=a>>>16,a+=1}function m(a){return"number"==typeof a}function n(a){return"string"==typeof a}function o(a){return a instanceof ArrayBuffer}function p(a){return a instanceof Uint8Array}function q(a){return a instanceof Int8Array||a instanceof Uint8Array||a instanceof Int16Array||a instanceof Uint16Array||a instanceof Int32Array||a instanceof Uint32Array||a instanceof Float32Array||a instanceof Float64Array}function r(a,b){var c=b.heap,d=c?c.byteLength:b.heapSize||65536;if(4095&d||0>=d)throw new Error("heap size must be a positive integer and a multiple of 4096");return c=c||new a(new ArrayBuffer(d))}function s(a,b,c,d,e){var f=a.length-b,g=e>f?f:e;return a.set(c.subarray(d,d+g),b),g}function t(a){a=a||{},this.heap=r(Uint8Array,a).subarray(Xa.HEAP_DATA),this.asm=a.asm||Xa(b,null,this.heap.buffer),this.mode=null,this.key=null,this.reset(a)}function u(a){if(void 0!==a){if(o(a)||p(a))a=new Uint8Array(a);else{if(!n(a))throw new TypeError("unexpected key type");a=f(a)}var b=a.length;if(16!==b&&24!==b&&32!==b)throw new d("illegal key size");var c=new DataView(a.buffer,a.byteOffset,a.byteLength);this.asm.set_key(b>>2,c.getUint32(0),c.getUint32(4),c.getUint32(8),c.getUint32(12),b>16?c.getUint32(16):0,b>16?c.getUint32(20):0,b>24?c.getUint32(24):0,b>24?c.getUint32(28):0),this.key=a}else if(!this.key)throw new Error("key is required")}function v(a){if(void 0!==a){if(o(a)||p(a))a=new Uint8Array(a);else{if(!n(a))throw new TypeError("unexpected iv type");a=f(a)}if(16!==a.length)throw new d("illegal iv size");var b=new DataView(a.buffer,a.byteOffset,a.byteLength);this.iv=a,this.asm.set_iv(b.getUint32(0),b.getUint32(4),b.getUint32(8),b.getUint32(12))}else this.iv=null,this.asm.set_iv(0,0,0,0)}function w(a){void 0!==a?this.padding=!!a:this.padding=!0}function x(a){return a=a||{},this.result=null,this.pos=0,this.len=0,u.call(this,a.key),this.hasOwnProperty("iv")&&v.call(this,a.iv),this.hasOwnProperty("padding")&&w.call(this,a.padding),this}function y(a){if(n(a)&&(a=f(a)),o(a)&&(a=new Uint8Array(a)),!p(a))throw new TypeError("data isn't of expected type");for(var b=this.asm,c=this.heap,d=Xa.ENC[this.mode],e=Xa.HEAP_DATA,g=this.pos,h=this.len,i=0,j=a.length||0,k=0,l=h+j&-16,m=0,q=new Uint8Array(l);j>0;)m=s(c,g+h,a,i,j),h+=m,i+=m,j-=m,m=b.cipher(d,e+g,h),m&&q.set(c.subarray(g,g+m),k),k+=m,h>m?(g+=m,h-=m):(g=0,h=0);return this.result=q,this.pos=g,this.len=h,this}function z(a){var b=null,c=0;void 0!==a&&(b=y.call(this,a).result,c=b.length);var e=this.asm,f=this.heap,g=Xa.ENC[this.mode],h=Xa.HEAP_DATA,i=this.pos,j=this.len,k=16-j%16,l=j;if(this.hasOwnProperty("padding")){if(this.padding){for(var m=0;k>m;++m)f[i+j+m]=k;j+=k,l=j}else if(j%16)throw new d("data length must be a multiple of the block size")}else j+=k;var n=new Uint8Array(c+l);return c&&n.set(b),j&&e.cipher(g,h+i,j),l&&n.set(f.subarray(i,i+l),c),this.result=n,this.pos=0,this.len=0,this}function A(a){this.nonce=null,this.counter=0,this.counterSize=0,t.call(this,a),this.mode="CTR"}function B(a){A.call(this,a)}function C(a,b,c){if(void 0!==c){if(8>c||c>48)throw new d("illegal counter size");this.counterSize=c;var e=Math.pow(2,c)-1;this.asm.set_mask(0,0,e/4294967296|0,0|e)}else this.counterSize=c=48,this.asm.set_mask(0,0,65535,4294967295);if(void 0===a)throw new Error("nonce is required");if(o(a)||p(a))a=new Uint8Array(a);else{if(!n(a))throw new TypeError("unexpected nonce type");a=f(a)}var g=a.length;if(!g||g>16)throw new d("illegal nonce size");this.nonce=a;var h=new DataView(new ArrayBuffer(16));if(new Uint8Array(h.buffer).set(a),this.asm.set_nonce(h.getUint32(0),h.getUint32(4),h.getUint32(8),h.getUint32(12)),void 0!==b){if(!m(b))throw new TypeError("unexpected counter type");if(0>b||b>=Math.pow(2,c))throw new d("illegal counter value");this.counter=b,this.asm.set_counter(0,0,b/4294967296|0,0|b)}else this.counter=b=0}function D(a){return a=a||{},x.call(this,a),C.call(this,a.nonce,a.counter,a.counterSize),this}function E(a){for(var b=this.heap,c=this.asm,d=0,e=a.length||0,f=0;e>0;){for(f=s(b,0,a,d,e),d+=f,e-=f;15&f;)b[f++]=0;c.mac(Xa.MAC.GCM,Xa.HEAP_DATA,f)}}function F(a){this.nonce=null,this.adata=null,this.iv=null,this.counter=1,this.tagSize=16,t.call(this,a),this.mode="GCM"}function G(a){F.call(this,a)}function H(a){F.call(this,a)}function I(a){a=a||{},x.call(this,a);var b=this.asm,c=this.heap;b.gcm_init();var e=a.tagSize;if(void 0!==e){if(!m(e))throw new TypeError("tagSize must be a number");if(4>e||e>16)throw new d("illegal tagSize value");this.tagSize=e}else this.tagSize=16;var g=a.nonce;if(void 0===g)throw new Error("nonce is required");if(p(g)||o(g))g=new Uint8Array(g);else{if(!n(g))throw new TypeError("unexpected nonce type");g=f(g)}this.nonce=g;var h=g.length||0,i=new Uint8Array(16);12!==h?(E.call(this,g),c[0]=c[1]=c[2]=c[3]=c[4]=c[5]=c[6]=c[7]=c[8]=c[9]=c[10]=0,c[11]=h>>>29,c[12]=h>>>21&255,c[13]=h>>>13&255,c[14]=h>>>5&255,c[15]=h<<3&255,b.mac(Xa.MAC.GCM,Xa.HEAP_DATA,16),b.get_iv(Xa.HEAP_DATA),b.set_iv(),i.set(c.subarray(0,16))):(i.set(g),i[15]=1);var j=new DataView(i.buffer);this.gamma0=j.getUint32(12),b.set_nonce(j.getUint32(0),j.getUint32(4),j.getUint32(8),0),b.set_mask(0,0,0,4294967295);var k=a.adata;if(void 0!==k&&null!==k){if(p(k)||o(k))k=new Uint8Array(k);else{if(!n(k))throw new TypeError("unexpected adata type");k=f(k)}if(k.length>$a)throw new d("illegal adata length");k.length?(this.adata=k,E.call(this,k)):this.adata=null}else this.adata=null;var l=a.counter;if(void 0!==l){if(!m(l))throw new TypeError("counter must be a number");if(1>l||l>4294967295)throw new RangeError("counter must be a positive 32-bit integer");this.counter=l,b.set_counter(0,0,0,this.gamma0+l|0)}else this.counter=1,b.set_counter(0,0,0,this.gamma0+1|0);var q=a.iv;if(void 0!==q){if(!m(l))throw new TypeError("counter must be a number");this.iv=q,v.call(this,q)}return this}function J(a){if(n(a)&&(a=f(a)),o(a)&&(a=new Uint8Array(a)),!p(a))throw new TypeError("data isn't of expected type");var b=0,c=a.length||0,d=this.asm,e=this.heap,g=this.counter,h=this.pos,i=this.len,j=0,k=i+c&-16,l=0;if((g-1<<4)+i+c>$a)throw new RangeError("counter overflow");for(var m=new Uint8Array(k);c>0;)l=s(e,h+i,a,b,c),i+=l,b+=l,c-=l,l=d.cipher(Xa.ENC.CTR,Xa.HEAP_DATA+h,i),l=d.mac(Xa.MAC.GCM,Xa.HEAP_DATA+h,l),l&&m.set(e.subarray(h,h+l),j),g+=l>>>4,j+=l,i>l?(h+=l,i-=l):(h=0,i=0);return this.result=m,this.counter=g,this.pos=h,this.len=i,this}function K(){var a=this.asm,b=this.heap,c=this.counter,d=this.tagSize,e=this.adata,f=this.pos,g=this.len,h=new Uint8Array(g+d);a.cipher(Xa.ENC.CTR,Xa.HEAP_DATA+f,g+15&-16),g&&h.set(b.subarray(f,f+g));for(var i=g;15&i;i++)b[f+i]=0;a.mac(Xa.MAC.GCM,Xa.HEAP_DATA+f,i);var j=null!==e?e.length:0,k=(c-1<<4)+g;return b[0]=b[1]=b[2]=0,b[3]=j>>>29,b[4]=j>>>21,b[5]=j>>>13&255,b[6]=j>>>5&255,b[7]=j<<3&255,b[8]=b[9]=b[10]=0,b[11]=k>>>29,b[12]=k>>>21&255,b[13]=k>>>13&255,b[14]=k>>>5&255,b[15]=k<<3&255,a.mac(Xa.MAC.GCM,Xa.HEAP_DATA,16),a.get_iv(Xa.HEAP_DATA),a.set_counter(0,0,0,this.gamma0),a.cipher(Xa.ENC.CTR,Xa.HEAP_DATA,16),h.set(b.subarray(0,d),g),this.result=h,this.counter=1,this.pos=0,this.len=0,this}function L(a){var b=J.call(this,a).result,c=K.call(this).result,d=new Uint8Array(b.length+c.length);return b.length&&d.set(b),c.length&&d.set(c,b.length),this.result=d,this}function M(a){if(n(a)&&(a=f(a)),o(a)&&(a=new Uint8Array(a)),!p(a))throw new TypeError("data isn't of expected type");var b=0,c=a.length||0,d=this.asm,e=this.heap,g=this.counter,h=this.tagSize,i=this.pos,j=this.len,k=0,l=j+c>h?j+c-h&-16:0,m=j+c-l,q=0;if((g-1<<4)+j+c>$a)throw new RangeError("counter overflow");for(var r=new Uint8Array(l);c>m;)q=s(e,i+j,a,b,c-m),j+=q,b+=q,c-=q,q=d.mac(Xa.MAC.GCM,Xa.HEAP_DATA+i,q),q=d.cipher(Xa.DEC.CTR,Xa.HEAP_DATA+i,q),q&&r.set(e.subarray(i,i+q),k),g+=q>>>4,k+=q,i=0,j=0;return c>0&&(j+=s(e,0,a,b,c)),this.result=r,this.counter=g,this.pos=i,this.len=j,this}function N(){var a=this.asm,b=this.heap,d=this.tagSize,f=this.adata,g=this.counter,h=this.pos,i=this.len,j=i-d,k=0;if(d>i)throw new c("authentication tag not found");for(var l=new Uint8Array(j),m=new Uint8Array(b.subarray(h+j,h+i)),n=j;15&n;n++)b[h+n]=0;k=a.mac(Xa.MAC.GCM,Xa.HEAP_DATA+h,n),k=a.cipher(Xa.DEC.CTR,Xa.HEAP_DATA+h,n),j&&l.set(b.subarray(h,h+j));var o=null!==f?f.length:0,p=(g-1<<4)+i-d;b[0]=b[1]=b[2]=0,b[3]=o>>>29,b[4]=o>>>21,b[5]=o>>>13&255,b[6]=o>>>5&255,b[7]=o<<3&255,b[8]=b[9]=b[10]=0,b[11]=p>>>29,b[12]=p>>>21&255,b[13]=p>>>13&255,b[14]=p>>>5&255,b[15]=p<<3&255,a.mac(Xa.MAC.GCM,Xa.HEAP_DATA,16),a.get_iv(Xa.HEAP_DATA),a.set_counter(0,0,0,this.gamma0),a.cipher(Xa.ENC.CTR,Xa.HEAP_DATA,16);for(var q=0,n=0;d>n;++n)q|=m[n]^b[n];if(q)throw new e("data integrity check failed");return this.result=l,this.counter=1,this.pos=0,this.len=0,this}function O(a){var b=M.call(this,a).result,c=N.call(this).result,d=new Uint8Array(b.length+c.length);return b.length&&d.set(b),c.length&&d.set(c,b.length),this.result=d,this}function P(a,b,c,d,e){if(void 0===a)throw new SyntaxError("data required");if(void 0===b)throw new SyntaxError("key required");if(void 0===c)throw new SyntaxError("nonce required");return new F({heap:cb,asm:db,key:b,nonce:c,adata:d,tagSize:e}).encrypt(a).result}function Q(a,b,c,d,e){if(void 0===a)throw new SyntaxError("data required");if(void 0===b)throw new SyntaxError("key required");if(void 0===c)throw new SyntaxError("nonce required");return new F({heap:cb,asm:db,key:b,nonce:c,adata:d,tagSize:e}).decrypt(a).result}function R(){return this.result=null,this.pos=0,this.len=0,this.asm.reset(),this}function S(a){if(null!==this.result)throw new c("state must be reset before processing new data");if(n(a)&&(a=f(a)),o(a)&&(a=new Uint8Array(a)),!p(a))throw new TypeError("data isn't of expected type");for(var b=this.asm,d=this.heap,e=this.pos,g=this.len,h=0,i=a.length,j=0;i>0;)j=s(d,e+g,a,h,i),g+=j,h+=j,i-=j,j=b.process(e,g),e+=j,g-=j,g||(e=0);return this.pos=e,this.len=g,this}function T(){if(null!==this.result)throw new c("state must be reset before processing new data");return this.asm.finish(this.pos,this.len,0),this.result=new Uint8Array(this.HASH_SIZE),this.result.set(this.heap.subarray(0,this.HASH_SIZE)),this.pos=0,this.len=0,this}function U(a,b,c){"use asm";var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0;var n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0;var D=new a.Uint8Array(c);function E(Q,R,S,T,U,V,W,X,Y,Z,$,_,aa,ba,ca,da){Q=Q|0;R=R|0;S=S|0;T=T|0;U=U|0;V=V|0;W=W|0;X=X|0;Y=Y|0;Z=Z|0;$=$|0;_=_|0;aa=aa|0;ba=ba|0;ca=ca|0;da=da|0;var ea=0,fa=0,ga=0,ha=0,ia=0,ja=0,ka=0,la=0,ma=0;ea=d;fa=e;ga=f;ha=g;ia=h;ja=i;ka=j;la=k;ma=Q+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+1116352408|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;ma=R+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+1899447441|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;ma=S+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+3049323471|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;ma=T+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+3921009573|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;ma=U+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+961987163|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;ma=V+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+1508970993|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;ma=W+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+2453635748|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;ma=X+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+2870763221|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;ma=Y+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+3624381080|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;ma=Z+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+310598401|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;ma=$+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+607225278|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;ma=_+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+1426881987|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;ma=aa+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+1925078388|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;ma=ba+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+2162078206|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;ma=ca+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+2614888103|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;ma=da+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+3248222580|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;Q=ma=(R>>>7^R>>>18^R>>>3^R<<25^R<<14)+(ca>>>17^ca>>>19^ca>>>10^ca<<15^ca<<13)+Q+Z|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+3835390401|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;R=ma=(S>>>7^S>>>18^S>>>3^S<<25^S<<14)+(da>>>17^da>>>19^da>>>10^da<<15^da<<13)+R+$|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+4022224774|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;S=ma=(T>>>7^T>>>18^T>>>3^T<<25^T<<14)+(Q>>>17^Q>>>19^Q>>>10^Q<<15^Q<<13)+S+_|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+264347078|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;T=ma=(U>>>7^U>>>18^U>>>3^U<<25^U<<14)+(R>>>17^R>>>19^R>>>10^R<<15^R<<13)+T+aa|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+604807628|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;U=ma=(V>>>7^V>>>18^V>>>3^V<<25^V<<14)+(S>>>17^S>>>19^S>>>10^S<<15^S<<13)+U+ba|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+770255983|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;V=ma=(W>>>7^W>>>18^W>>>3^W<<25^W<<14)+(T>>>17^T>>>19^T>>>10^T<<15^T<<13)+V+ca|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+1249150122|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;W=ma=(X>>>7^X>>>18^X>>>3^X<<25^X<<14)+(U>>>17^U>>>19^U>>>10^U<<15^U<<13)+W+da|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+1555081692|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;X=ma=(Y>>>7^Y>>>18^Y>>>3^Y<<25^Y<<14)+(V>>>17^V>>>19^V>>>10^V<<15^V<<13)+X+Q|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+1996064986|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;Y=ma=(Z>>>7^Z>>>18^Z>>>3^Z<<25^Z<<14)+(W>>>17^W>>>19^W>>>10^W<<15^W<<13)+Y+R|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+2554220882|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;Z=ma=($>>>7^$>>>18^$>>>3^$<<25^$<<14)+(X>>>17^X>>>19^X>>>10^X<<15^X<<13)+Z+S|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+2821834349|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;$=ma=(_>>>7^_>>>18^_>>>3^_<<25^_<<14)+(Y>>>17^Y>>>19^Y>>>10^Y<<15^Y<<13)+$+T|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+2952996808|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;_=ma=(aa>>>7^aa>>>18^aa>>>3^aa<<25^aa<<14)+(Z>>>17^Z>>>19^Z>>>10^Z<<15^Z<<13)+_+U|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+3210313671|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;aa=ma=(ba>>>7^ba>>>18^ba>>>3^ba<<25^ba<<14)+($>>>17^$>>>19^$>>>10^$<<15^$<<13)+aa+V|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+3336571891|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;ba=ma=(ca>>>7^ca>>>18^ca>>>3^ca<<25^ca<<14)+(_>>>17^_>>>19^_>>>10^_<<15^_<<13)+ba+W|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+3584528711|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;ca=ma=(da>>>7^da>>>18^da>>>3^da<<25^da<<14)+(aa>>>17^aa>>>19^aa>>>10^aa<<15^aa<<13)+ca+X|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+113926993|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;da=ma=(Q>>>7^Q>>>18^Q>>>3^Q<<25^Q<<14)+(ba>>>17^ba>>>19^ba>>>10^ba<<15^ba<<13)+da+Y|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+338241895|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;Q=ma=(R>>>7^R>>>18^R>>>3^R<<25^R<<14)+(ca>>>17^ca>>>19^ca>>>10^ca<<15^ca<<13)+Q+Z|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+666307205|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;R=ma=(S>>>7^S>>>18^S>>>3^S<<25^S<<14)+(da>>>17^da>>>19^da>>>10^da<<15^da<<13)+R+$|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+773529912|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;S=ma=(T>>>7^T>>>18^T>>>3^T<<25^T<<14)+(Q>>>17^Q>>>19^Q>>>10^Q<<15^Q<<13)+S+_|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+1294757372|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;T=ma=(U>>>7^U>>>18^U>>>3^U<<25^U<<14)+(R>>>17^R>>>19^R>>>10^R<<15^R<<13)+T+aa|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+1396182291|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;U=ma=(V>>>7^V>>>18^V>>>3^V<<25^V<<14)+(S>>>17^S>>>19^S>>>10^S<<15^S<<13)+U+ba|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+1695183700|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;V=ma=(W>>>7^W>>>18^W>>>3^W<<25^W<<14)+(T>>>17^T>>>19^T>>>10^T<<15^T<<13)+V+ca|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+1986661051|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;W=ma=(X>>>7^X>>>18^X>>>3^X<<25^X<<14)+(U>>>17^U>>>19^U>>>10^U<<15^U<<13)+W+da|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+2177026350|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;X=ma=(Y>>>7^Y>>>18^Y>>>3^Y<<25^Y<<14)+(V>>>17^V>>>19^V>>>10^V<<15^V<<13)+X+Q|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+2456956037|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;Y=ma=(Z>>>7^Z>>>18^Z>>>3^Z<<25^Z<<14)+(W>>>17^W>>>19^W>>>10^W<<15^W<<13)+Y+R|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+2730485921|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;Z=ma=($>>>7^$>>>18^$>>>3^$<<25^$<<14)+(X>>>17^X>>>19^X>>>10^X<<15^X<<13)+Z+S|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+2820302411|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;$=ma=(_>>>7^_>>>18^_>>>3^_<<25^_<<14)+(Y>>>17^Y>>>19^Y>>>10^Y<<15^Y<<13)+$+T|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+3259730800|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;_=ma=(aa>>>7^aa>>>18^aa>>>3^aa<<25^aa<<14)+(Z>>>17^Z>>>19^Z>>>10^Z<<15^Z<<13)+_+U|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+3345764771|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;aa=ma=(ba>>>7^ba>>>18^ba>>>3^ba<<25^ba<<14)+($>>>17^$>>>19^$>>>10^$<<15^$<<13)+aa+V|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+3516065817|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;ba=ma=(ca>>>7^ca>>>18^ca>>>3^ca<<25^ca<<14)+(_>>>17^_>>>19^_>>>10^_<<15^_<<13)+ba+W|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+3600352804|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;ca=ma=(da>>>7^da>>>18^da>>>3^da<<25^da<<14)+(aa>>>17^aa>>>19^aa>>>10^aa<<15^aa<<13)+ca+X|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+4094571909|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;da=ma=(Q>>>7^Q>>>18^Q>>>3^Q<<25^Q<<14)+(ba>>>17^ba>>>19^ba>>>10^ba<<15^ba<<13)+da+Y|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+275423344|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;Q=ma=(R>>>7^R>>>18^R>>>3^R<<25^R<<14)+(ca>>>17^ca>>>19^ca>>>10^ca<<15^ca<<13)+Q+Z|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+430227734|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;R=ma=(S>>>7^S>>>18^S>>>3^S<<25^S<<14)+(da>>>17^da>>>19^da>>>10^da<<15^da<<13)+R+$|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+506948616|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;S=ma=(T>>>7^T>>>18^T>>>3^T<<25^T<<14)+(Q>>>17^Q>>>19^Q>>>10^Q<<15^Q<<13)+S+_|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+659060556|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;T=ma=(U>>>7^U>>>18^U>>>3^U<<25^U<<14)+(R>>>17^R>>>19^R>>>10^R<<15^R<<13)+T+aa|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+883997877|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;U=ma=(V>>>7^V>>>18^V>>>3^V<<25^V<<14)+(S>>>17^S>>>19^S>>>10^S<<15^S<<13)+U+ba|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+958139571|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;V=ma=(W>>>7^W>>>18^W>>>3^W<<25^W<<14)+(T>>>17^T>>>19^T>>>10^T<<15^T<<13)+V+ca|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+1322822218|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;W=ma=(X>>>7^X>>>18^X>>>3^X<<25^X<<14)+(U>>>17^U>>>19^U>>>10^U<<15^U<<13)+W+da|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+1537002063|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;X=ma=(Y>>>7^Y>>>18^Y>>>3^Y<<25^Y<<14)+(V>>>17^V>>>19^V>>>10^V<<15^V<<13)+X+Q|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+1747873779|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;Y=ma=(Z>>>7^Z>>>18^Z>>>3^Z<<25^Z<<14)+(W>>>17^W>>>19^W>>>10^W<<15^W<<13)+Y+R|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+1955562222|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;Z=ma=($>>>7^$>>>18^$>>>3^$<<25^$<<14)+(X>>>17^X>>>19^X>>>10^X<<15^X<<13)+Z+S|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+2024104815|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;$=ma=(_>>>7^_>>>18^_>>>3^_<<25^_<<14)+(Y>>>17^Y>>>19^Y>>>10^Y<<15^Y<<13)+$+T|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+2227730452|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;_=ma=(aa>>>7^aa>>>18^aa>>>3^aa<<25^aa<<14)+(Z>>>17^Z>>>19^Z>>>10^Z<<15^Z<<13)+_+U|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+2361852424|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;aa=ma=(ba>>>7^ba>>>18^ba>>>3^ba<<25^ba<<14)+($>>>17^$>>>19^$>>>10^$<<15^$<<13)+aa+V|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+2428436474|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;ba=ma=(ca>>>7^ca>>>18^ca>>>3^ca<<25^ca<<14)+(_>>>17^_>>>19^_>>>10^_<<15^_<<13)+ba+W|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+2756734187|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;ca=ma=(da>>>7^da>>>18^da>>>3^da<<25^da<<14)+(aa>>>17^aa>>>19^aa>>>10^aa<<15^aa<<13)+ca+X|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+3204031479|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;da=ma=(Q>>>7^Q>>>18^Q>>>3^Q<<25^Q<<14)+(ba>>>17^ba>>>19^ba>>>10^ba<<15^ba<<13)+da+Y|0;ma=ma+la+(ia>>>6^ia>>>11^ia>>>25^ia<<26^ia<<21^ia<<7)+(ka^ia&(ja^ka))+3329325298|0;la=ka;ka=ja;ja=ia;ia=ha+ma|0;ha=ga;ga=fa;fa=ea;ea=ma+(fa&ga^ha&(fa^ga))+(fa>>>2^fa>>>13^fa>>>22^fa<<30^fa<<19^fa<<10)|0;d=d+ea|0;e=e+fa|0;f=f+ga|0;g=g+ha|0;h=h+ia|0;i=i+ja|0;j=j+ka|0;k=k+la|0}function F(Q){Q=Q|0;E(D[Q|0]<<24|D[Q|1]<<16|D[Q|2]<<8|D[Q|3],D[Q|4]<<24|D[Q|5]<<16|D[Q|6]<<8|D[Q|7],D[Q|8]<<24|D[Q|9]<<16|D[Q|10]<<8|D[Q|11],D[Q|12]<<24|D[Q|13]<<16|D[Q|14]<<8|D[Q|15],D[Q|16]<<24|D[Q|17]<<16|D[Q|18]<<8|D[Q|19],D[Q|20]<<24|D[Q|21]<<16|D[Q|22]<<8|D[Q|23],D[Q|24]<<24|D[Q|25]<<16|D[Q|26]<<8|D[Q|27],D[Q|28]<<24|D[Q|29]<<16|D[Q|30]<<8|D[Q|31],D[Q|32]<<24|D[Q|33]<<16|D[Q|34]<<8|D[Q|35],D[Q|36]<<24|D[Q|37]<<16|D[Q|38]<<8|D[Q|39],D[Q|40]<<24|D[Q|41]<<16|D[Q|42]<<8|D[Q|43],D[Q|44]<<24|D[Q|45]<<16|D[Q|46]<<8|D[Q|47],D[Q|48]<<24|D[Q|49]<<16|D[Q|50]<<8|D[Q|51],D[Q|52]<<24|D[Q|53]<<16|D[Q|54]<<8|D[Q|55],D[Q|56]<<24|D[Q|57]<<16|D[Q|58]<<8|D[Q|59],D[Q|60]<<24|D[Q|61]<<16|D[Q|62]<<8|D[Q|63])}function G(Q){Q=Q|0;D[Q|0]=d>>>24;D[Q|1]=d>>>16&255;D[Q|2]=d>>>8&255;D[Q|3]=d&255;D[Q|4]=e>>>24;D[Q|5]=e>>>16&255;D[Q|6]=e>>>8&255;D[Q|7]=e&255;D[Q|8]=f>>>24;D[Q|9]=f>>>16&255;D[Q|10]=f>>>8&255;D[Q|11]=f&255;D[Q|12]=g>>>24;D[Q|13]=g>>>16&255;D[Q|14]=g>>>8&255;D[Q|15]=g&255;D[Q|16]=h>>>24;D[Q|17]=h>>>16&255;D[Q|18]=h>>>8&255;D[Q|19]=h&255;D[Q|20]=i>>>24;D[Q|21]=i>>>16&255;D[Q|22]=i>>>8&255;D[Q|23]=i&255;D[Q|24]=j>>>24;D[Q|25]=j>>>16&255;D[Q|26]=j>>>8&255;D[Q|27]=j&255;D[Q|28]=k>>>24;D[Q|29]=k>>>16&255;D[Q|30]=k>>>8&255;D[Q|31]=k&255}function H(){d=1779033703;e=3144134277;f=1013904242;g=2773480762;h=1359893119;i=2600822924;j=528734635;k=1541459225;l=m=0}function I(Q,R,S,T,U,V,W,X,Y,Z){Q=Q|0;R=R|0;S=S|0;T=T|0;U=U|0;V=V|0;W=W|0;X=X|0;Y=Y|0;Z=Z|0;d=Q;e=R;f=S;g=T;h=U;i=V;j=W;k=X;l=Y;m=Z}function J(Q,R){Q=Q|0;R=R|0;var S=0;if(Q&63)return-1;while((R|0)>=64){F(Q);Q=Q+64|0;R=R-64|0;S=S+64|0}l=l+S|0;if(l>>>0>>0)m=m+1|0;return S|0}function K(Q,R,S){Q=Q|0;R=R|0;S=S|0;var T=0,U=0;if(Q&63)return-1;if(~S)if(S&31)return-1;if((R|0)>=64){T=J(Q,R)|0;if((T|0)==-1)return-1;Q=Q+T|0;R=R-T|0}T=T+R|0;l=l+R|0;if(l>>>0>>0)m=m+1|0;D[Q|R]=128;if((R|0)>=56){for(U=R+1|0;(U|0)<64;U=U+1|0)D[Q|U]=0;F(Q);R=0;D[Q|0]=0}for(U=R+1|0;(U|0)<59;U=U+1|0)D[Q|U]=0;D[Q|56]=m>>>21&255;D[Q|57]=m>>>13&255;D[Q|58]=m>>>5&255;D[Q|59]=m<<3&255|l>>>29;D[Q|60]=l>>>21&255;D[Q|61]=l>>>13&255;D[Q|62]=l>>>5&255;D[Q|63]=l<<3&255;F(Q);if(~S)G(S);return T|0}function L(){d=n;e=o;f=p;g=q;h=r;i=s;j=t;k=u;l=64;m=0}function M(){d=v;e=w;f=x;g=y;h=z;i=A;j=B;k=C;l=64;m=0}function N(Q,R,S,T,U,V,W,X,Y,Z,$,_,aa,ba,ca,da){Q=Q|0;R=R|0;S=S|0;T=T|0;U=U|0;V=V|0;W=W|0;X=X|0;Y=Y|0;Z=Z|0;$=$|0;_=_|0;aa=aa|0;ba=ba|0;ca=ca|0;da=da|0;H();E(Q^1549556828,R^1549556828,S^1549556828,T^1549556828,U^1549556828,V^1549556828,W^1549556828,X^1549556828,Y^1549556828,Z^1549556828,$^1549556828,_^1549556828,aa^1549556828,ba^1549556828,ca^1549556828,da^1549556828);v=d;w=e;x=f;y=g;z=h;A=i;B=j;C=k;H();E(Q^909522486,R^909522486,S^909522486,T^909522486,U^909522486,V^909522486,W^909522486,X^909522486,Y^909522486,Z^909522486,$^909522486,_^909522486,aa^909522486,ba^909522486,ca^909522486,da^909522486);
-
-n=d;o=e;p=f;q=g;r=h;s=i;t=j;u=k;l=64;m=0}function O(Q,R,S){Q=Q|0;R=R|0;S=S|0;var T=0,U=0,V=0,W=0,X=0,Y=0,Z=0,$=0,_=0;if(Q&63)return-1;if(~S)if(S&31)return-1;_=K(Q,R,-1)|0;T=d,U=e,V=f,W=g,X=h,Y=i,Z=j,$=k;M();E(T,U,V,W,X,Y,Z,$,2147483648,0,0,0,0,0,0,768);if(~S)G(S);return _|0}function P(Q,R,S,T,U){Q=Q|0;R=R|0;S=S|0;T=T|0;U=U|0;var V=0,W=0,X=0,Y=0,Z=0,$=0,_=0,aa=0,ba=0,ca=0,da=0,ea=0,fa=0,ga=0,ha=0,ia=0;if(Q&63)return-1;if(~U)if(U&31)return-1;D[Q+R|0]=S>>>24;D[Q+R+1|0]=S>>>16&255;D[Q+R+2|0]=S>>>8&255;D[Q+R+3|0]=S&255;O(Q,R+4|0,-1)|0;V=ba=d,W=ca=e,X=da=f,Y=ea=g,Z=fa=h,$=ga=i,_=ha=j,aa=ia=k;T=T-1|0;while((T|0)>0){L();E(ba,ca,da,ea,fa,ga,ha,ia,2147483648,0,0,0,0,0,0,768);ba=d,ca=e,da=f,ea=g,fa=h,ga=i,ha=j,ia=k;M();E(ba,ca,da,ea,fa,ga,ha,ia,2147483648,0,0,0,0,0,0,768);ba=d,ca=e,da=f,ea=g,fa=h,ga=i,ha=j,ia=k;V=V^d;W=W^e;X=X^f;Y=Y^g;Z=Z^h;$=$^i;_=_^j;aa=aa^k;T=T-1|0}d=V;e=W;f=X;g=Y;h=Z;i=$;j=_;k=aa;if(~U)G(U);return 0}return{reset:H,init:I,process:J,finish:K,hmac_reset:L,hmac_init:N,hmac_finish:O,pbkdf2_generate_block:P}}function V(a){a=a||{},this.heap=r(Uint8Array,a),this.asm=a.asm||U({Uint8Array:Uint8Array},null,this.heap.buffer),this.BLOCK_SIZE=eb,this.HASH_SIZE=fb,this.reset()}function W(){return null===hb&&(hb=new V({heapSize:1048576})),hb}function X(a){if(void 0===a)throw new SyntaxError("data required");return W().reset().process(a).finish().result}function Y(a){var b=X(a);return j(b)}function Z(a){var b=X(a);return k(b)}function $(a){if(a=a||{},!a.hash)throw new SyntaxError("option 'hash' is required");if(!a.hash.HASH_SIZE)throw new SyntaxError("option 'hash' supplied doesn't seem to be a valid hash function");return this.hash=a.hash,this.BLOCK_SIZE=this.hash.BLOCK_SIZE,this.HMAC_SIZE=this.hash.HASH_SIZE,this.key=null,this.verify=null,this.result=null,(void 0!==a.password||void 0!==a.verify)&&this.reset(a),this}function _(a,b){if(o(b)&&(b=new Uint8Array(b)),n(b)&&(b=f(b)),!p(b))throw new TypeError("password isn't of expected type");var c=new Uint8Array(a.BLOCK_SIZE);return c.set(b.length>a.BLOCK_SIZE?a.reset().process(b).finish().result:b),c}function aa(a){if(o(a)||p(a))a=new Uint8Array(a);else{if(!n(a))throw new TypeError("verify tag isn't of expected type");a=f(a)}if(a.length!==this.HMAC_SIZE)throw new d("illegal verification tag size");this.verify=a}function ba(a){a=a||{};var b=a.password;if(null===this.key&&!n(b)&&!b)throw new c("no key is associated with the instance");this.result=null,this.hash.reset(),(b||n(b))&&(this.key=_(this.hash,b));for(var d=new Uint8Array(this.key),e=0;e=g;++g){var h=(g-1)*this.hmac.HMAC_SIZE,i=(f>g?0:e%this.hmac.HMAC_SIZE)||this.hmac.HMAC_SIZE,j=new Uint8Array(this.hmac.reset().process(a).process(new Uint8Array([g>>>24&255,g>>>16&255,g>>>8&255,255&g])).finish().result);this.result.set(j.subarray(0,i),h);for(var k=1;b>k;++k){j=new Uint8Array(this.hmac.reset().process(j).finish().result);for(var l=0;i>l;++l)this.result[h+l]^=j[l]}}return this}function oa(a){return a=a||{},a.hmac instanceof ea||(a.hmac=ha()),la.call(this,a),this}function pa(a,b,e){if(null!==this.result)throw new c("state must be reset before processing new data");if(!a&&!n(a))throw new d("bad 'salt' value");b=b||this.count,e=e||this.length,this.result=new Uint8Array(e);for(var f=Math.ceil(e/this.hmac.HMAC_SIZE),g=1;f>=g;++g){var h=(g-1)*this.hmac.HMAC_SIZE,i=(f>g?0:e%this.hmac.HMAC_SIZE)||this.hmac.HMAC_SIZE;this.hmac.reset().process(a),this.hmac.hash.asm.pbkdf2_generate_block(this.hmac.hash.pos,this.hmac.hash.len,g,b,0),this.result.set(this.hmac.hash.heap.subarray(0,i),h)}return this}function qa(){return null===ob&&(ob=new oa),ob}function ra(){if(void 0!==ub)d=new Uint8Array(32),nb.call(ub,d),xb(d);else{var a,c,d=new Wa(3);d[0]=sb(),d[1]=rb(),d[2]=vb(),d=new Uint8Array(d.buffer);var e="";void 0!==b.location?e+=b.location.href:void 0!==b.process&&(e+=b.process.pid+b.process.title);var f=qa();for(a=0;100>a;a++)d=f.reset({password:d}).generate(e,1e3,32).result,c=vb(),d[0]^=c>>>24,d[1]^=c>>>16,d[2]^=c>>>8,d[3]^=c;xb(d)}yb=0,zb=!0}function sa(a){if(!o(a)&&!q(a))throw new TypeError("bad seed type");var b=a.byteOffset||0,c=a.byteLength||a.length,d=new Uint8Array(a.buffer||a,b,c);xb(d),yb=0;for(var e=0,f=0;f=Cb}function ta(a){if(zb||ra(),!Ab&&void 0===ub){if(!Db)throw new e("No strong PRNGs available. Use asmCrypto.random.seed().");void 0!==qb&&qb.error("No strong PRNGs available; your security is greatly lowered. Use asmCrypto.random.seed().")}if(!Eb&&!Ab&&void 0!==ub&&void 0!==qb){var b=(new Error).stack;Fb[b]|=0,Fb[b]++||qb.warn("asmCrypto PRNG not seeded; your security relies on your system PRNG. If this is not acceptable, use asmCrypto.random.seed().")}if(!o(a)&&!q(a))throw new TypeError("unexpected buffer type");var c,d,f=a.byteOffset||0,g=a.byteLength||a.length,h=new Uint8Array(a.buffer||a,f,g);for(void 0!==ub&&nb.call(ub,h),c=0;g>c;c++)0===(3&c)&&(yb>=1099511627776&&ra(),d=wb(),yb++),h[c]^=d,d>>>=8;return a}function ua(){(!zb||yb>=1099511627776)&&ra();var a=(1048576*wb()+(wb()>>>12))/4503599627370496;return yb+=2,a}function va(a,b,c){"use asm";var d=0;var e=new a.Uint32Array(c);var f=a.Math.imul;function g(u){u=u|0;d=u=u+31&-32;return u|0}function h(u){u=u|0;var v=0;v=d;d=v+(u+31&-32)|0;return v|0}function i(u){u=u|0;d=d-(u+31&-32)|0}function j(u,v,w){u=u|0;v=v|0;w=w|0;var x=0;if((v|0)>(w|0)){for(;(x|0)<(u|0);x=x+4|0){e[w+x>>2]=e[v+x>>2]}}else{for(x=u-4|0;(x|0)>=0;x=x-4|0){e[w+x>>2]=e[v+x>>2]}}}function k(u,v,w){u=u|0;v=v|0;w=w|0;var x=0;for(;(x|0)<(u|0);x=x+4|0){e[w+x>>2]=v}}function l(u,v,w,x){u=u|0;v=v|0;w=w|0;x=x|0;var y=0,z=0,A=0,B=0,C=0;if((x|0)<=0)x=v;if((x|0)<(v|0))v=x;z=1;for(;(C|0)<(v|0);C=C+4|0){y=~e[u+C>>2];A=(y&65535)+z|0;B=(y>>>16)+(A>>>16)|0;e[w+C>>2]=B<<16|A&65535;z=B>>>16}for(;(C|0)<(x|0);C=C+4|0){e[w+C>>2]=z-1|0}return z|0}function m(u,v,w,x){u=u|0;v=v|0;w=w|0;x=x|0;var y=0,z=0,A=0;if((v|0)>(x|0)){for(A=v-4|0;(A|0)>=(x|0);A=A-4|0){if(e[u+A>>2]|0)return 1}}else{for(A=x-4|0;(A|0)>=(v|0);A=A-4|0){if(e[w+A>>2]|0)return-1}}for(;(A|0)>=0;A=A-4|0){y=e[u+A>>2]|0,z=e[w+A>>2]|0;if(y>>>0>>0)return-1;if(y>>>0>z>>>0)return 1}return 0}function n(u,v){u=u|0;v=v|0;var w=0;for(w=v-4|0;(w|0)>=0;w=w-4|0){if(e[u+w>>2]|0)return w+4|0}return 0}function o(u,v,w,x,y,z){u=u|0;v=v|0;w=w|0;x=x|0;y=y|0;z=z|0;var A=0,B=0,C=0,D=0,E=0,F=0;if((v|0)<(x|0)){D=u,u=w,w=D;D=v,v=x,x=D}if((z|0)<=0)z=v+4|0;if((z|0)<(x|0))v=x=z;for(;(F|0)<(x|0);F=F+4|0){A=e[u+F>>2]|0;B=e[w+F>>2]|0;D=((A&65535)+(B&65535)|0)+C|0;E=((A>>>16)+(B>>>16)|0)+(D>>>16)|0;e[y+F>>2]=D&65535|E<<16;C=E>>>16}for(;(F|0)<(v|0);F=F+4|0){A=e[u+F>>2]|0;D=(A&65535)+C|0;E=(A>>>16)+(D>>>16)|0;e[y+F>>2]=D&65535|E<<16;C=E>>>16}for(;(F|0)<(z|0);F=F+4|0){e[y+F>>2]=C|0;C=0}return C|0}function p(u,v,w,x,y,z){u=u|0;v=v|0;w=w|0;x=x|0;y=y|0;z=z|0;var A=0,B=0,C=0,D=0,E=0,F=0;if((z|0)<=0)z=(v|0)>(x|0)?v+4|0:x+4|0;if((z|0)<(v|0))v=z;if((z|0)<(x|0))x=z;if((v|0)<(x|0)){for(;(F|0)<(v|0);F=F+4|0){A=e[u+F>>2]|0;B=e[w+F>>2]|0;D=((A&65535)-(B&65535)|0)+C|0;E=((A>>>16)-(B>>>16)|0)+(D>>16)|0;e[y+F>>2]=D&65535|E<<16;C=E>>16}for(;(F|0)<(x|0);F=F+4|0){B=e[w+F>>2]|0;D=C-(B&65535)|0;E=(D>>16)-(B>>>16)|0;e[y+F>>2]=D&65535|E<<16;C=E>>16}}else{for(;(F|0)<(x|0);F=F+4|0){A=e[u+F>>2]|0;B=e[w+F>>2]|0;D=((A&65535)-(B&65535)|0)+C|0;E=((A>>>16)-(B>>>16)|0)+(D>>16)|0;e[y+F>>2]=D&65535|E<<16;C=E>>16}for(;(F|0)<(v|0);F=F+4|0){A=e[u+F>>2]|0;D=(A&65535)+C|0;E=(A>>>16)+(D>>16)|0;e[y+F>>2]=D&65535|E<<16;C=E>>16}}for(;(F|0)<(z|0);F=F+4|0){e[y+F>>2]=C|0}return C|0}function q(u,v,w,x,y,z){u=u|0;v=v|0;w=w|0;x=x|0;y=y|0;z=z|0;var A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0,P=0,Q=0,R=0,S=0,T=0,U=0,V=0,W=0,X=0,Y=0,Z=0,$=0,_=0,aa=0,ba=0,ca=0,da=0,ea=0,fa=0,ga=0,ha=0,ia=0,ja=0,ka=0,la=0,ma=0,na=0,oa=0,pa=0,qa=0,ra=0,sa=0,ta=0,ua=0,va=0,wa=0,xa=0,ya=0,za=0,Aa=0,Ba=0,Ca=0;if((v|0)>(x|0)){ua=u,va=v;u=w,v=x;w=ua,x=va}xa=v+x|0;if((z|0)>(xa|0)|(z|0)<=0)z=xa;if((z|0)<(v|0))v=z;if((z|0)<(x|0))x=z;for(;(ya|0)<(v|0);ya=ya+32|0){za=u+ya|0;I=e[(za|0)>>2]|0,J=e[(za|4)>>2]|0,K=e[(za|8)>>2]|0,L=e[(za|12)>>2]|0,M=e[(za|16)>>2]|0,N=e[(za|20)>>2]|0,O=e[(za|24)>>2]|0,P=e[(za|28)>>2]|0,A=I&65535,B=J&65535,C=K&65535,D=L&65535,E=M&65535,F=N&65535,G=O&65535,H=P&65535,I=I>>>16,J=J>>>16,K=K>>>16,L=L>>>16,M=M>>>16,N=N>>>16,O=O>>>16,P=P>>>16;ma=na=oa=pa=qa=ra=sa=ta=0;for(Aa=0;(Aa|0)<(x|0);Aa=Aa+32|0){Ba=w+Aa|0;Ca=y+(ya+Aa|0)|0;Y=e[(Ba|0)>>2]|0,Z=e[(Ba|4)>>2]|0,$=e[(Ba|8)>>2]|0,_=e[(Ba|12)>>2]|0,aa=e[(Ba|16)>>2]|0,ba=e[(Ba|20)>>2]|0,ca=e[(Ba|24)>>2]|0,da=e[(Ba|28)>>2]|0,Q=Y&65535,R=Z&65535,S=$&65535,T=_&65535,U=aa&65535,V=ba&65535,W=ca&65535,X=da&65535,Y=Y>>>16,Z=Z>>>16,$=$>>>16,_=_>>>16,aa=aa>>>16,ba=ba>>>16,ca=ca>>>16,da=da>>>16;ea=e[(Ca|0)>>2]|0,fa=e[(Ca|4)>>2]|0,ga=e[(Ca|8)>>2]|0,ha=e[(Ca|12)>>2]|0,ia=e[(Ca|16)>>2]|0,ja=e[(Ca|20)>>2]|0,ka=e[(Ca|24)>>2]|0,la=e[(Ca|28)>>2]|0;ua=((f(A,Q)|0)+(ma&65535)|0)+(ea&65535)|0;va=((f(I,Q)|0)+(ma>>>16)|0)+(ea>>>16)|0;wa=((f(A,Y)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(I,Y)|0)+(va>>>16)|0)+(wa>>>16)|0;ea=wa<<16|ua&65535;ua=((f(A,R)|0)+(xa&65535)|0)+(fa&65535)|0;va=((f(I,R)|0)+(xa>>>16)|0)+(fa>>>16)|0;wa=((f(A,Z)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(I,Z)|0)+(va>>>16)|0)+(wa>>>16)|0;fa=wa<<16|ua&65535;ua=((f(A,S)|0)+(xa&65535)|0)+(ga&65535)|0;va=((f(I,S)|0)+(xa>>>16)|0)+(ga>>>16)|0;wa=((f(A,$)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(I,$)|0)+(va>>>16)|0)+(wa>>>16)|0;ga=wa<<16|ua&65535;ua=((f(A,T)|0)+(xa&65535)|0)+(ha&65535)|0;va=((f(I,T)|0)+(xa>>>16)|0)+(ha>>>16)|0;wa=((f(A,_)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(I,_)|0)+(va>>>16)|0)+(wa>>>16)|0;ha=wa<<16|ua&65535;ua=((f(A,U)|0)+(xa&65535)|0)+(ia&65535)|0;va=((f(I,U)|0)+(xa>>>16)|0)+(ia>>>16)|0;wa=((f(A,aa)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(I,aa)|0)+(va>>>16)|0)+(wa>>>16)|0;ia=wa<<16|ua&65535;ua=((f(A,V)|0)+(xa&65535)|0)+(ja&65535)|0;va=((f(I,V)|0)+(xa>>>16)|0)+(ja>>>16)|0;wa=((f(A,ba)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(I,ba)|0)+(va>>>16)|0)+(wa>>>16)|0;ja=wa<<16|ua&65535;ua=((f(A,W)|0)+(xa&65535)|0)+(ka&65535)|0;va=((f(I,W)|0)+(xa>>>16)|0)+(ka>>>16)|0;wa=((f(A,ca)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(I,ca)|0)+(va>>>16)|0)+(wa>>>16)|0;ka=wa<<16|ua&65535;ua=((f(A,X)|0)+(xa&65535)|0)+(la&65535)|0;va=((f(I,X)|0)+(xa>>>16)|0)+(la>>>16)|0;wa=((f(A,da)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(I,da)|0)+(va>>>16)|0)+(wa>>>16)|0;la=wa<<16|ua&65535;ma=xa;ua=((f(B,Q)|0)+(na&65535)|0)+(fa&65535)|0;va=((f(J,Q)|0)+(na>>>16)|0)+(fa>>>16)|0;wa=((f(B,Y)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(J,Y)|0)+(va>>>16)|0)+(wa>>>16)|0;fa=wa<<16|ua&65535;ua=((f(B,R)|0)+(xa&65535)|0)+(ga&65535)|0;va=((f(J,R)|0)+(xa>>>16)|0)+(ga>>>16)|0;wa=((f(B,Z)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(J,Z)|0)+(va>>>16)|0)+(wa>>>16)|0;ga=wa<<16|ua&65535;ua=((f(B,S)|0)+(xa&65535)|0)+(ha&65535)|0;va=((f(J,S)|0)+(xa>>>16)|0)+(ha>>>16)|0;wa=((f(B,$)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(J,$)|0)+(va>>>16)|0)+(wa>>>16)|0;ha=wa<<16|ua&65535;ua=((f(B,T)|0)+(xa&65535)|0)+(ia&65535)|0;va=((f(J,T)|0)+(xa>>>16)|0)+(ia>>>16)|0;wa=((f(B,_)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(J,_)|0)+(va>>>16)|0)+(wa>>>16)|0;ia=wa<<16|ua&65535;ua=((f(B,U)|0)+(xa&65535)|0)+(ja&65535)|0;va=((f(J,U)|0)+(xa>>>16)|0)+(ja>>>16)|0;wa=((f(B,aa)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(J,aa)|0)+(va>>>16)|0)+(wa>>>16)|0;ja=wa<<16|ua&65535;ua=((f(B,V)|0)+(xa&65535)|0)+(ka&65535)|0;va=((f(J,V)|0)+(xa>>>16)|0)+(ka>>>16)|0;wa=((f(B,ba)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(J,ba)|0)+(va>>>16)|0)+(wa>>>16)|0;ka=wa<<16|ua&65535;ua=((f(B,W)|0)+(xa&65535)|0)+(la&65535)|0;va=((f(J,W)|0)+(xa>>>16)|0)+(la>>>16)|0;wa=((f(B,ca)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(J,ca)|0)+(va>>>16)|0)+(wa>>>16)|0;la=wa<<16|ua&65535;ua=((f(B,X)|0)+(xa&65535)|0)+(ma&65535)|0;va=((f(J,X)|0)+(xa>>>16)|0)+(ma>>>16)|0;wa=((f(B,da)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(J,da)|0)+(va>>>16)|0)+(wa>>>16)|0;ma=wa<<16|ua&65535;na=xa;ua=((f(C,Q)|0)+(oa&65535)|0)+(ga&65535)|0;va=((f(K,Q)|0)+(oa>>>16)|0)+(ga>>>16)|0;wa=((f(C,Y)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(K,Y)|0)+(va>>>16)|0)+(wa>>>16)|0;ga=wa<<16|ua&65535;ua=((f(C,R)|0)+(xa&65535)|0)+(ha&65535)|0;va=((f(K,R)|0)+(xa>>>16)|0)+(ha>>>16)|0;wa=((f(C,Z)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(K,Z)|0)+(va>>>16)|0)+(wa>>>16)|0;ha=wa<<16|ua&65535;ua=((f(C,S)|0)+(xa&65535)|0)+(ia&65535)|0;va=((f(K,S)|0)+(xa>>>16)|0)+(ia>>>16)|0;wa=((f(C,$)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(K,$)|0)+(va>>>16)|0)+(wa>>>16)|0;ia=wa<<16|ua&65535;ua=((f(C,T)|0)+(xa&65535)|0)+(ja&65535)|0;va=((f(K,T)|0)+(xa>>>16)|0)+(ja>>>16)|0;wa=((f(C,_)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(K,_)|0)+(va>>>16)|0)+(wa>>>16)|0;ja=wa<<16|ua&65535;ua=((f(C,U)|0)+(xa&65535)|0)+(ka&65535)|0;va=((f(K,U)|0)+(xa>>>16)|0)+(ka>>>16)|0;wa=((f(C,aa)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(K,aa)|0)+(va>>>16)|0)+(wa>>>16)|0;ka=wa<<16|ua&65535;ua=((f(C,V)|0)+(xa&65535)|0)+(la&65535)|0;va=((f(K,V)|0)+(xa>>>16)|0)+(la>>>16)|0;wa=((f(C,ba)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(K,ba)|0)+(va>>>16)|0)+(wa>>>16)|0;la=wa<<16|ua&65535;ua=((f(C,W)|0)+(xa&65535)|0)+(ma&65535)|0;va=((f(K,W)|0)+(xa>>>16)|0)+(ma>>>16)|0;wa=((f(C,ca)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(K,ca)|0)+(va>>>16)|0)+(wa>>>16)|0;ma=wa<<16|ua&65535;ua=((f(C,X)|0)+(xa&65535)|0)+(na&65535)|0;va=((f(K,X)|0)+(xa>>>16)|0)+(na>>>16)|0;wa=((f(C,da)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(K,da)|0)+(va>>>16)|0)+(wa>>>16)|0;na=wa<<16|ua&65535;oa=xa;ua=((f(D,Q)|0)+(pa&65535)|0)+(ha&65535)|0;va=((f(L,Q)|0)+(pa>>>16)|0)+(ha>>>16)|0;wa=((f(D,Y)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(L,Y)|0)+(va>>>16)|0)+(wa>>>16)|0;ha=wa<<16|ua&65535;ua=((f(D,R)|0)+(xa&65535)|0)+(ia&65535)|0;va=((f(L,R)|0)+(xa>>>16)|0)+(ia>>>16)|0;wa=((f(D,Z)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(L,Z)|0)+(va>>>16)|0)+(wa>>>16)|0;ia=wa<<16|ua&65535;ua=((f(D,S)|0)+(xa&65535)|0)+(ja&65535)|0;va=((f(L,S)|0)+(xa>>>16)|0)+(ja>>>16)|0;wa=((f(D,$)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(L,$)|0)+(va>>>16)|0)+(wa>>>16)|0;ja=wa<<16|ua&65535;ua=((f(D,T)|0)+(xa&65535)|0)+(ka&65535)|0;va=((f(L,T)|0)+(xa>>>16)|0)+(ka>>>16)|0;wa=((f(D,_)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(L,_)|0)+(va>>>16)|0)+(wa>>>16)|0;ka=wa<<16|ua&65535;ua=((f(D,U)|0)+(xa&65535)|0)+(la&65535)|0;va=((f(L,U)|0)+(xa>>>16)|0)+(la>>>16)|0;wa=((f(D,aa)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(L,aa)|0)+(va>>>16)|0)+(wa>>>16)|0;la=wa<<16|ua&65535;ua=((f(D,V)|0)+(xa&65535)|0)+(ma&65535)|0;va=((f(L,V)|0)+(xa>>>16)|0)+(ma>>>16)|0;wa=((f(D,ba)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(L,ba)|0)+(va>>>16)|0)+(wa>>>16)|0;ma=wa<<16|ua&65535;ua=((f(D,W)|0)+(xa&65535)|0)+(na&65535)|0;va=((f(L,W)|0)+(xa>>>16)|0)+(na>>>16)|0;wa=((f(D,ca)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(L,ca)|0)+(va>>>16)|0)+(wa>>>16)|0;na=wa<<16|ua&65535;ua=((f(D,X)|0)+(xa&65535)|0)+(oa&65535)|0;va=((f(L,X)|0)+(xa>>>16)|0)+(oa>>>16)|0;wa=((f(D,da)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(L,da)|0)+(va>>>16)|0)+(wa>>>16)|0;oa=wa<<16|ua&65535;pa=xa;ua=((f(E,Q)|0)+(qa&65535)|0)+(ia&65535)|0;va=((f(M,Q)|0)+(qa>>>16)|0)+(ia>>>16)|0;wa=((f(E,Y)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(M,Y)|0)+(va>>>16)|0)+(wa>>>16)|0;ia=wa<<16|ua&65535;ua=((f(E,R)|0)+(xa&65535)|0)+(ja&65535)|0;va=((f(M,R)|0)+(xa>>>16)|0)+(ja>>>16)|0;wa=((f(E,Z)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(M,Z)|0)+(va>>>16)|0)+(wa>>>16)|0;ja=wa<<16|ua&65535;ua=((f(E,S)|0)+(xa&65535)|0)+(ka&65535)|0;va=((f(M,S)|0)+(xa>>>16)|0)+(ka>>>16)|0;wa=((f(E,$)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(M,$)|0)+(va>>>16)|0)+(wa>>>16)|0;ka=wa<<16|ua&65535;ua=((f(E,T)|0)+(xa&65535)|0)+(la&65535)|0;va=((f(M,T)|0)+(xa>>>16)|0)+(la>>>16)|0;wa=((f(E,_)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(M,_)|0)+(va>>>16)|0)+(wa>>>16)|0;la=wa<<16|ua&65535;ua=((f(E,U)|0)+(xa&65535)|0)+(ma&65535)|0;va=((f(M,U)|0)+(xa>>>16)|0)+(ma>>>16)|0;wa=((f(E,aa)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(M,aa)|0)+(va>>>16)|0)+(wa>>>16)|0;ma=wa<<16|ua&65535;ua=((f(E,V)|0)+(xa&65535)|0)+(na&65535)|0;va=((f(M,V)|0)+(xa>>>16)|0)+(na>>>16)|0;wa=((f(E,ba)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(M,ba)|0)+(va>>>16)|0)+(wa>>>16)|0;na=wa<<16|ua&65535;ua=((f(E,W)|0)+(xa&65535)|0)+(oa&65535)|0;va=((f(M,W)|0)+(xa>>>16)|0)+(oa>>>16)|0;wa=((f(E,ca)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(M,ca)|0)+(va>>>16)|0)+(wa>>>16)|0;oa=wa<<16|ua&65535;ua=((f(E,X)|0)+(xa&65535)|0)+(pa&65535)|0;va=((f(M,X)|0)+(xa>>>16)|0)+(pa>>>16)|0;wa=((f(E,da)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(M,da)|0)+(va>>>16)|0)+(wa>>>16)|0;pa=wa<<16|ua&65535;qa=xa;ua=((f(F,Q)|0)+(ra&65535)|0)+(ja&65535)|0;va=((f(N,Q)|0)+(ra>>>16)|0)+(ja>>>16)|0;wa=((f(F,Y)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(N,Y)|0)+(va>>>16)|0)+(wa>>>16)|0;ja=wa<<16|ua&65535;ua=((f(F,R)|0)+(xa&65535)|0)+(ka&65535)|0;va=((f(N,R)|0)+(xa>>>16)|0)+(ka>>>16)|0;wa=((f(F,Z)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(N,Z)|0)+(va>>>16)|0)+(wa>>>16)|0;ka=wa<<16|ua&65535;ua=((f(F,S)|0)+(xa&65535)|0)+(la&65535)|0;va=((f(N,S)|0)+(xa>>>16)|0)+(la>>>16)|0;wa=((f(F,$)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(N,$)|0)+(va>>>16)|0)+(wa>>>16)|0;la=wa<<16|ua&65535;ua=((f(F,T)|0)+(xa&65535)|0)+(ma&65535)|0;va=((f(N,T)|0)+(xa>>>16)|0)+(ma>>>16)|0;wa=((f(F,_)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(N,_)|0)+(va>>>16)|0)+(wa>>>16)|0;ma=wa<<16|ua&65535;ua=((f(F,U)|0)+(xa&65535)|0)+(na&65535)|0;va=((f(N,U)|0)+(xa>>>16)|0)+(na>>>16)|0;wa=((f(F,aa)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(N,aa)|0)+(va>>>16)|0)+(wa>>>16)|0;na=wa<<16|ua&65535;ua=((f(F,V)|0)+(xa&65535)|0)+(oa&65535)|0;va=((f(N,V)|0)+(xa>>>16)|0)+(oa>>>16)|0;wa=((f(F,ba)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(N,ba)|0)+(va>>>16)|0)+(wa>>>16)|0;oa=wa<<16|ua&65535;ua=((f(F,W)|0)+(xa&65535)|0)+(pa&65535)|0;va=((f(N,W)|0)+(xa>>>16)|0)+(pa>>>16)|0;wa=((f(F,ca)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(N,ca)|0)+(va>>>16)|0)+(wa>>>16)|0;pa=wa<<16|ua&65535;ua=((f(F,X)|0)+(xa&65535)|0)+(qa&65535)|0;va=((f(N,X)|0)+(xa>>>16)|0)+(qa>>>16)|0;wa=((f(F,da)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(N,da)|0)+(va>>>16)|0)+(wa>>>16)|0;qa=wa<<16|ua&65535;ra=xa;ua=((f(G,Q)|0)+(sa&65535)|0)+(ka&65535)|0;va=((f(O,Q)|0)+(sa>>>16)|0)+(ka>>>16)|0;wa=((f(G,Y)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(O,Y)|0)+(va>>>16)|0)+(wa>>>16)|0;ka=wa<<16|ua&65535;ua=((f(G,R)|0)+(xa&65535)|0)+(la&65535)|0;va=((f(O,R)|0)+(xa>>>16)|0)+(la>>>16)|0;wa=((f(G,Z)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(O,Z)|0)+(va>>>16)|0)+(wa>>>16)|0;la=wa<<16|ua&65535;ua=((f(G,S)|0)+(xa&65535)|0)+(ma&65535)|0;va=((f(O,S)|0)+(xa>>>16)|0)+(ma>>>16)|0;wa=((f(G,$)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(O,$)|0)+(va>>>16)|0)+(wa>>>16)|0;ma=wa<<16|ua&65535;ua=((f(G,T)|0)+(xa&65535)|0)+(na&65535)|0;va=((f(O,T)|0)+(xa>>>16)|0)+(na>>>16)|0;wa=((f(G,_)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(O,_)|0)+(va>>>16)|0)+(wa>>>16)|0;na=wa<<16|ua&65535;ua=((f(G,U)|0)+(xa&65535)|0)+(oa&65535)|0;va=((f(O,U)|0)+(xa>>>16)|0)+(oa>>>16)|0;wa=((f(G,aa)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(O,aa)|0)+(va>>>16)|0)+(wa>>>16)|0;oa=wa<<16|ua&65535;ua=((f(G,V)|0)+(xa&65535)|0)+(pa&65535)|0;va=((f(O,V)|0)+(xa>>>16)|0)+(pa>>>16)|0;wa=((f(G,ba)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(O,ba)|0)+(va>>>16)|0)+(wa>>>16)|0;pa=wa<<16|ua&65535;ua=((f(G,W)|0)+(xa&65535)|0)+(qa&65535)|0;va=((f(O,W)|0)+(xa>>>16)|0)+(qa>>>16)|0;wa=((f(G,ca)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(O,ca)|0)+(va>>>16)|0)+(wa>>>16)|0;qa=wa<<16|ua&65535;ua=((f(G,X)|0)+(xa&65535)|0)+(ra&65535)|0;va=((f(O,X)|0)+(xa>>>16)|0)+(ra>>>16)|0;wa=((f(G,da)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(O,da)|0)+(va>>>16)|0)+(wa>>>16)|0;ra=wa<<16|ua&65535;sa=xa;ua=((f(H,Q)|0)+(ta&65535)|0)+(la&65535)|0;va=((f(P,Q)|0)+(ta>>>16)|0)+(la>>>16)|0;wa=((f(H,Y)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(P,Y)|0)+(va>>>16)|0)+(wa>>>16)|0;la=wa<<16|ua&65535;ua=((f(H,R)|0)+(xa&65535)|0)+(ma&65535)|0;va=((f(P,R)|0)+(xa>>>16)|0)+(ma>>>16)|0;wa=((f(H,Z)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(P,Z)|0)+(va>>>16)|0)+(wa>>>16)|0;ma=wa<<16|ua&65535;ua=((f(H,S)|0)+(xa&65535)|0)+(na&65535)|0;va=((f(P,S)|0)+(xa>>>16)|0)+(na>>>16)|0;wa=((f(H,$)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(P,$)|0)+(va>>>16)|0)+(wa>>>16)|0;na=wa<<16|ua&65535;ua=((f(H,T)|0)+(xa&65535)|0)+(oa&65535)|0;va=((f(P,T)|0)+(xa>>>16)|0)+(oa>>>16)|0;wa=((f(H,_)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(P,_)|0)+(va>>>16)|0)+(wa>>>16)|0;oa=wa<<16|ua&65535;ua=((f(H,U)|0)+(xa&65535)|0)+(pa&65535)|0;va=((f(P,U)|0)+(xa>>>16)|0)+(pa>>>16)|0;wa=((f(H,aa)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(P,aa)|0)+(va>>>16)|0)+(wa>>>16)|0;pa=wa<<16|ua&65535;ua=((f(H,V)|0)+(xa&65535)|0)+(qa&65535)|0;va=((f(P,V)|0)+(xa>>>16)|0)+(qa>>>16)|0;wa=((f(H,ba)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(P,ba)|0)+(va>>>16)|0)+(wa>>>16)|0;qa=wa<<16|ua&65535;ua=((f(H,W)|0)+(xa&65535)|0)+(ra&65535)|0;va=((f(P,W)|0)+(xa>>>16)|0)+(ra>>>16)|0;wa=((f(H,ca)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(P,ca)|0)+(va>>>16)|0)+(wa>>>16)|0;ra=wa<<16|ua&65535;ua=((f(H,X)|0)+(xa&65535)|0)+(sa&65535)|0;va=((f(P,X)|0)+(xa>>>16)|0)+(sa>>>16)|0;wa=((f(H,da)|0)+(va&65535)|0)+(ua>>>16)|0;xa=((f(P,da)|0)+(va>>>16)|0)+(wa>>>16)|0;sa=wa<<16|ua&65535;ta=xa;e[(Ca|0)>>2]=ea,e[(Ca|4)>>2]=fa,e[(Ca|8)>>2]=ga,e[(Ca|12)>>2]=ha,e[(Ca|16)>>2]=ia,e[(Ca|20)>>2]=ja,e[(Ca|24)>>2]=ka,e[(Ca|28)>>2]=la}Ca=y+(ya+Aa|0)|0;e[(Ca|0)>>2]=ma,e[(Ca|4)>>2]=na,e[(Ca|8)>>2]=oa,e[(Ca|12)>>2]=pa,e[(Ca|16)>>2]=qa,e[(Ca|20)>>2]=ra,e[(Ca|24)>>2]=sa,e[(Ca|28)>>2]=ta}}function r(u,v,w){u=u|0;v=v|0;w=w|0;var x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0,P=0,Q=0,R=0,S=0,T=0,U=0,V=0,W=0,X=0,Y=0,Z=0,$=0,_=0,aa=0,ba=0,ca=0,da=0,ea=0,fa=0,ga=0,ha=0,ia=0,ja=0,ka=0,la=0,ma=0,na=0,oa=0,pa=0,qa=0,ra=0,sa=0,ta=0,ua=0,va=0,wa=0,xa=0,ya=0,za=0,Aa=0,Ba=0,Ca=0,Da=0,Ea=0,Fa=0,Ga=0;for(;(Ba|0)<(v|0);Ba=Ba+4|0){Ga=w+(Ba<<1)|0;F=e[u+Ba>>2]|0,x=F&65535,F=F>>>16;ra=f(x,x)|0;sa=(f(x,F)|0)+(ra>>>17)|0;ta=(f(F,F)|0)+(sa>>>15)|0;e[Ga>>2]=sa<<17|ra&131071;e[(Ga|4)>>2]=ta}for(Aa=0;(Aa|0)<(v|0);Aa=Aa+8|0){Ea=u+Aa|0,Ga=w+(Aa<<1)|0;F=e[Ea>>2]|0,x=F&65535,F=F>>>16;V=e[(Ea|4)>>2]|0,N=V&65535,V=V>>>16;ra=f(x,N)|0;sa=(f(x,V)|0)+(ra>>>16)|0;ta=(f(F,N)|0)+(sa&65535)|0;wa=((f(F,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;xa=e[(Ga|4)>>2]|0;ra=(xa&65535)+((ra&65535)<<1)|0;ta=((xa>>>16)+((ta&65535)<<1)|0)+(ra>>>16)|0;e[(Ga|4)>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[(Ga|8)>>2]|0;ra=((xa&65535)+((wa&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(wa>>>16<<1)|0)+(ra>>>16)|0;e[(Ga|8)>>2]=ta<<16|ra&65535;ua=ta>>>16;if(ua){xa=e[(Ga|12)>>2]|0;ra=(xa&65535)+ua|0;ta=(xa>>>16)+(ra>>>16)|0;e[(Ga|12)>>2]=ta<<16|ra&65535}}for(Aa=0;(Aa|0)<(v|0);Aa=Aa+16|0){Ea=u+Aa|0,Ga=w+(Aa<<1)|0;F=e[Ea>>2]|0,x=F&65535,F=F>>>16,G=e[(Ea|4)>>2]|0,y=G&65535,G=G>>>16;V=e[(Ea|8)>>2]|0,N=V&65535,V=V>>>16,W=e[(Ea|12)>>2]|0,O=W&65535,W=W>>>16;ra=f(x,N)|0;sa=f(F,N)|0;ta=((f(x,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ba=ta<<16|ra&65535;ra=(f(x,O)|0)+(wa&65535)|0;sa=(f(F,O)|0)+(wa>>>16)|0;ta=((f(x,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;ca=ta<<16|ra&65535;da=wa;ra=(f(y,N)|0)+(ca&65535)|0;sa=(f(G,N)|0)+(ca>>>16)|0;ta=((f(y,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ca=ta<<16|ra&65535;ra=((f(y,O)|0)+(da&65535)|0)+(wa&65535)|0;sa=((f(G,O)|0)+(da>>>16)|0)+(wa>>>16)|0;ta=((f(y,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;da=ta<<16|ra&65535;ea=wa;xa=e[(Ga|8)>>2]|0;ra=(xa&65535)+((ba&65535)<<1)|0;ta=((xa>>>16)+(ba>>>16<<1)|0)+(ra>>>16)|0;e[(Ga|8)>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[(Ga|12)>>2]|0;ra=((xa&65535)+((ca&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ca>>>16<<1)|0)+(ra>>>16)|0;e[(Ga|12)>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[(Ga|16)>>2]|0;ra=((xa&65535)+((da&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(da>>>16<<1)|0)+(ra>>>16)|0;e[(Ga|16)>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[(Ga|20)>>2]|0;ra=((xa&65535)+((ea&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ea>>>16<<1)|0)+(ra>>>16)|0;e[(Ga|20)>>2]=ta<<16|ra&65535;ua=ta>>>16;for(Da=24;!!ua&(Da|0)<32;Da=Da+4|0){xa=e[(Ga|Da)>>2]|0;ra=(xa&65535)+ua|0;ta=(xa>>>16)+(ra>>>16)|0;e[(Ga|Da)>>2]=ta<<16|ra&65535;ua=ta>>>16}}for(Aa=0;(Aa|0)<(v|0);Aa=Aa+32|0){Ea=u+Aa|0,Ga=w+(Aa<<1)|0;F=e[Ea>>2]|0,x=F&65535,F=F>>>16,G=e[(Ea|4)>>2]|0,y=G&65535,G=G>>>16,H=e[(Ea|8)>>2]|0,z=H&65535,H=H>>>16,I=e[(Ea|12)>>2]|0,A=I&65535,I=I>>>16;V=e[(Ea|16)>>2]|0,N=V&65535,V=V>>>16,W=e[(Ea|20)>>2]|0,O=W&65535,W=W>>>16,X=e[(Ea|24)>>2]|0,P=X&65535,X=X>>>16,Y=e[(Ea|28)>>2]|0,Q=Y&65535,Y=Y>>>16;ra=f(x,N)|0;sa=f(F,N)|0;ta=((f(x,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ba=ta<<16|ra&65535;ra=(f(x,O)|0)+(wa&65535)|0;sa=(f(F,O)|0)+(wa>>>16)|0;ta=((f(x,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;ca=ta<<16|ra&65535;ra=(f(x,P)|0)+(wa&65535)|0;sa=(f(F,P)|0)+(wa>>>16)|0;ta=((f(x,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;da=ta<<16|ra&65535;ra=(f(x,Q)|0)+(wa&65535)|0;sa=(f(F,Q)|0)+(wa>>>16)|0;ta=((f(x,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;ea=ta<<16|ra&65535;fa=wa;ra=(f(y,N)|0)+(ca&65535)|0;sa=(f(G,N)|0)+(ca>>>16)|0;ta=((f(y,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ca=ta<<16|ra&65535;ra=((f(y,O)|0)+(da&65535)|0)+(wa&65535)|0;sa=((f(G,O)|0)+(da>>>16)|0)+(wa>>>16)|0;ta=((f(y,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;da=ta<<16|ra&65535;ra=((f(y,P)|0)+(ea&65535)|0)+(wa&65535)|0;sa=((f(G,P)|0)+(ea>>>16)|0)+(wa>>>16)|0;ta=((f(y,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;ea=ta<<16|ra&65535;ra=((f(y,Q)|0)+(fa&65535)|0)+(wa&65535)|0;sa=((f(G,Q)|0)+(fa>>>16)|0)+(wa>>>16)|0;ta=((f(y,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;fa=ta<<16|ra&65535;ga=wa;ra=(f(z,N)|0)+(da&65535)|0;sa=(f(H,N)|0)+(da>>>16)|0;ta=((f(z,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;da=ta<<16|ra&65535;ra=((f(z,O)|0)+(ea&65535)|0)+(wa&65535)|0;sa=((f(H,O)|0)+(ea>>>16)|0)+(wa>>>16)|0;ta=((f(z,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;ea=ta<<16|ra&65535;ra=((f(z,P)|0)+(fa&65535)|0)+(wa&65535)|0;sa=((f(H,P)|0)+(fa>>>16)|0)+(wa>>>16)|0;ta=((f(z,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;fa=ta<<16|ra&65535;ra=((f(z,Q)|0)+(ga&65535)|0)+(wa&65535)|0;sa=((f(H,Q)|0)+(ga>>>16)|0)+(wa>>>16)|0;ta=((f(z,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;ga=ta<<16|ra&65535;ha=wa;ra=(f(A,N)|0)+(ea&65535)|0;sa=(f(I,N)|0)+(ea>>>16)|0;ta=((f(A,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ea=ta<<16|ra&65535;ra=((f(A,O)|0)+(fa&65535)|0)+(wa&65535)|0;sa=((f(I,O)|0)+(fa>>>16)|0)+(wa>>>16)|0;ta=((f(A,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;fa=ta<<16|ra&65535;ra=((f(A,P)|0)+(ga&65535)|0)+(wa&65535)|0;sa=((f(I,P)|0)+(ga>>>16)|0)+(wa>>>16)|0;ta=((f(A,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;ga=ta<<16|ra&65535;ra=((f(A,Q)|0)+(ha&65535)|0)+(wa&65535)|0;sa=((f(I,Q)|0)+(ha>>>16)|0)+(wa>>>16)|0;ta=((f(A,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;ha=ta<<16|ra&65535;ia=wa;xa=e[(Ga|16)>>2]|0;ra=(xa&65535)+((ba&65535)<<1)|0;ta=((xa>>>16)+(ba>>>16<<1)|0)+(ra>>>16)|0;e[(Ga|16)>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[(Ga|20)>>2]|0;ra=((xa&65535)+((ca&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ca>>>16<<1)|0)+(ra>>>16)|0;e[(Ga|20)>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[(Ga|24)>>2]|0;ra=((xa&65535)+((da&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(da>>>16<<1)|0)+(ra>>>16)|0;e[(Ga|24)>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[(Ga|28)>>2]|0;ra=((xa&65535)+((ea&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ea>>>16<<1)|0)+(ra>>>16)|0;e[(Ga|28)>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[Ga+32>>2]|0;ra=((xa&65535)+((fa&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(fa>>>16<<1)|0)+(ra>>>16)|0;e[Ga+32>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[Ga+36>>2]|0;ra=((xa&65535)+((ga&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ga>>>16<<1)|0)+(ra>>>16)|0;e[Ga+36>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[Ga+40>>2]|0;ra=((xa&65535)+((ha&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ha>>>16<<1)|0)+(ra>>>16)|0;e[Ga+40>>2]=ta<<16|ra&65535;ua=ta>>>16;xa=e[Ga+44>>2]|0;ra=((xa&65535)+((ia&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ia>>>16<<1)|0)+(ra>>>16)|0;e[Ga+44>>2]=ta<<16|ra&65535;ua=ta>>>16;for(Da=48;!!ua&(Da|0)<64;Da=Da+4|0){xa=e[Ga+Da>>2]|0;ra=(xa&65535)+ua|0;ta=(xa>>>16)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16}}for(ya=32;(ya|0)<(v|0);ya=ya<<1){za=ya<<1;for(Aa=0;(Aa|0)<(v|0);Aa=Aa+za|0){Ga=w+(Aa<<1)|0;va=0;for(Ba=0;(Ba|0)<(ya|0);Ba=Ba+32|0){Ea=(u+Aa|0)+Ba|0;F=e[Ea>>2]|0,x=F&65535,F=F>>>16,G=e[(Ea|4)>>2]|0,y=G&65535,G=G>>>16,H=e[(Ea|8)>>2]|0,z=H&65535,H=H>>>16,I=e[(Ea|12)>>2]|0,A=I&65535,I=I>>>16,J=e[(Ea|16)>>2]|0,B=J&65535,J=J>>>16,K=e[(Ea|20)>>2]|0,C=K&65535,K=K>>>16,L=e[(Ea|24)>>2]|0,D=L&65535,L=L>>>16,M=e[(Ea|28)>>2]|0,E=M&65535,M=M>>>16;ja=ka=la=ma=na=oa=pa=qa=ua=0;for(Ca=0;(Ca|0)<(ya|0);Ca=Ca+32|0){Fa=((u+Aa|0)+ya|0)+Ca|0;V=e[Fa>>2]|0,N=V&65535,V=V>>>16,W=e[(Fa|4)>>2]|0,O=W&65535,W=W>>>16,X=e[(Fa|8)>>2]|0,P=X&65535,X=X>>>16,Y=e[(Fa|12)>>2]|0,Q=Y&65535,Y=Y>>>16,Z=e[(Fa|16)>>2]|0,R=Z&65535,Z=Z>>>16,$=e[(Fa|20)>>2]|0,S=$&65535,$=$>>>16,_=e[(Fa|24)>>2]|0,T=_&65535,_=_>>>16,aa=e[(Fa|28)>>2]|0,U=aa&65535,aa=aa>>>16;ba=ca=da=ea=fa=ga=ha=ia=0;ra=((f(x,N)|0)+(ba&65535)|0)+(ja&65535)|0;sa=((f(F,N)|0)+(ba>>>16)|0)+(ja>>>16)|0;ta=((f(x,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ba=ta<<16|ra&65535;ra=((f(x,O)|0)+(ca&65535)|0)+(wa&65535)|0;sa=((f(F,O)|0)+(ca>>>16)|0)+(wa>>>16)|0;
-
-ta=((f(x,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;ca=ta<<16|ra&65535;ra=((f(x,P)|0)+(da&65535)|0)+(wa&65535)|0;sa=((f(F,P)|0)+(da>>>16)|0)+(wa>>>16)|0;ta=((f(x,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;da=ta<<16|ra&65535;ra=((f(x,Q)|0)+(ea&65535)|0)+(wa&65535)|0;sa=((f(F,Q)|0)+(ea>>>16)|0)+(wa>>>16)|0;ta=((f(x,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;ea=ta<<16|ra&65535;ra=((f(x,R)|0)+(fa&65535)|0)+(wa&65535)|0;sa=((f(F,R)|0)+(fa>>>16)|0)+(wa>>>16)|0;ta=((f(x,Z)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,Z)|0)+(sa>>>16)|0)+(ta>>>16)|0;fa=ta<<16|ra&65535;ra=((f(x,S)|0)+(ga&65535)|0)+(wa&65535)|0;sa=((f(F,S)|0)+(ga>>>16)|0)+(wa>>>16)|0;ta=((f(x,$)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,$)|0)+(sa>>>16)|0)+(ta>>>16)|0;ga=ta<<16|ra&65535;ra=((f(x,T)|0)+(ha&65535)|0)+(wa&65535)|0;sa=((f(F,T)|0)+(ha>>>16)|0)+(wa>>>16)|0;ta=((f(x,_)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,_)|0)+(sa>>>16)|0)+(ta>>>16)|0;ha=ta<<16|ra&65535;ra=((f(x,U)|0)+(ia&65535)|0)+(wa&65535)|0;sa=((f(F,U)|0)+(ia>>>16)|0)+(wa>>>16)|0;ta=((f(x,aa)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(F,aa)|0)+(sa>>>16)|0)+(ta>>>16)|0;ia=ta<<16|ra&65535;ja=wa;ra=((f(y,N)|0)+(ca&65535)|0)+(ka&65535)|0;sa=((f(G,N)|0)+(ca>>>16)|0)+(ka>>>16)|0;ta=((f(y,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ca=ta<<16|ra&65535;ra=((f(y,O)|0)+(da&65535)|0)+(wa&65535)|0;sa=((f(G,O)|0)+(da>>>16)|0)+(wa>>>16)|0;ta=((f(y,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;da=ta<<16|ra&65535;ra=((f(y,P)|0)+(ea&65535)|0)+(wa&65535)|0;sa=((f(G,P)|0)+(ea>>>16)|0)+(wa>>>16)|0;ta=((f(y,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;ea=ta<<16|ra&65535;ra=((f(y,Q)|0)+(fa&65535)|0)+(wa&65535)|0;sa=((f(G,Q)|0)+(fa>>>16)|0)+(wa>>>16)|0;ta=((f(y,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;fa=ta<<16|ra&65535;ra=((f(y,R)|0)+(ga&65535)|0)+(wa&65535)|0;sa=((f(G,R)|0)+(ga>>>16)|0)+(wa>>>16)|0;ta=((f(y,Z)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,Z)|0)+(sa>>>16)|0)+(ta>>>16)|0;ga=ta<<16|ra&65535;ra=((f(y,S)|0)+(ha&65535)|0)+(wa&65535)|0;sa=((f(G,S)|0)+(ha>>>16)|0)+(wa>>>16)|0;ta=((f(y,$)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,$)|0)+(sa>>>16)|0)+(ta>>>16)|0;ha=ta<<16|ra&65535;ra=((f(y,T)|0)+(ia&65535)|0)+(wa&65535)|0;sa=((f(G,T)|0)+(ia>>>16)|0)+(wa>>>16)|0;ta=((f(y,_)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,_)|0)+(sa>>>16)|0)+(ta>>>16)|0;ia=ta<<16|ra&65535;ra=((f(y,U)|0)+(ja&65535)|0)+(wa&65535)|0;sa=((f(G,U)|0)+(ja>>>16)|0)+(wa>>>16)|0;ta=((f(y,aa)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(G,aa)|0)+(sa>>>16)|0)+(ta>>>16)|0;ja=ta<<16|ra&65535;ka=wa;ra=((f(z,N)|0)+(da&65535)|0)+(la&65535)|0;sa=((f(H,N)|0)+(da>>>16)|0)+(la>>>16)|0;ta=((f(z,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;da=ta<<16|ra&65535;ra=((f(z,O)|0)+(ea&65535)|0)+(wa&65535)|0;sa=((f(H,O)|0)+(ea>>>16)|0)+(wa>>>16)|0;ta=((f(z,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;ea=ta<<16|ra&65535;ra=((f(z,P)|0)+(fa&65535)|0)+(wa&65535)|0;sa=((f(H,P)|0)+(fa>>>16)|0)+(wa>>>16)|0;ta=((f(z,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;fa=ta<<16|ra&65535;ra=((f(z,Q)|0)+(ga&65535)|0)+(wa&65535)|0;sa=((f(H,Q)|0)+(ga>>>16)|0)+(wa>>>16)|0;ta=((f(z,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;ga=ta<<16|ra&65535;ra=((f(z,R)|0)+(ha&65535)|0)+(wa&65535)|0;sa=((f(H,R)|0)+(ha>>>16)|0)+(wa>>>16)|0;ta=((f(z,Z)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,Z)|0)+(sa>>>16)|0)+(ta>>>16)|0;ha=ta<<16|ra&65535;ra=((f(z,S)|0)+(ia&65535)|0)+(wa&65535)|0;sa=((f(H,S)|0)+(ia>>>16)|0)+(wa>>>16)|0;ta=((f(z,$)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,$)|0)+(sa>>>16)|0)+(ta>>>16)|0;ia=ta<<16|ra&65535;ra=((f(z,T)|0)+(ja&65535)|0)+(wa&65535)|0;sa=((f(H,T)|0)+(ja>>>16)|0)+(wa>>>16)|0;ta=((f(z,_)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,_)|0)+(sa>>>16)|0)+(ta>>>16)|0;ja=ta<<16|ra&65535;ra=((f(z,U)|0)+(ka&65535)|0)+(wa&65535)|0;sa=((f(H,U)|0)+(ka>>>16)|0)+(wa>>>16)|0;ta=((f(z,aa)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(H,aa)|0)+(sa>>>16)|0)+(ta>>>16)|0;ka=ta<<16|ra&65535;la=wa;ra=((f(A,N)|0)+(ea&65535)|0)+(ma&65535)|0;sa=((f(I,N)|0)+(ea>>>16)|0)+(ma>>>16)|0;ta=((f(A,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ea=ta<<16|ra&65535;ra=((f(A,O)|0)+(fa&65535)|0)+(wa&65535)|0;sa=((f(I,O)|0)+(fa>>>16)|0)+(wa>>>16)|0;ta=((f(A,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;fa=ta<<16|ra&65535;ra=((f(A,P)|0)+(ga&65535)|0)+(wa&65535)|0;sa=((f(I,P)|0)+(ga>>>16)|0)+(wa>>>16)|0;ta=((f(A,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;ga=ta<<16|ra&65535;ra=((f(A,Q)|0)+(ha&65535)|0)+(wa&65535)|0;sa=((f(I,Q)|0)+(ha>>>16)|0)+(wa>>>16)|0;ta=((f(A,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;ha=ta<<16|ra&65535;ra=((f(A,R)|0)+(ia&65535)|0)+(wa&65535)|0;sa=((f(I,R)|0)+(ia>>>16)|0)+(wa>>>16)|0;ta=((f(A,Z)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,Z)|0)+(sa>>>16)|0)+(ta>>>16)|0;ia=ta<<16|ra&65535;ra=((f(A,S)|0)+(ja&65535)|0)+(wa&65535)|0;sa=((f(I,S)|0)+(ja>>>16)|0)+(wa>>>16)|0;ta=((f(A,$)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,$)|0)+(sa>>>16)|0)+(ta>>>16)|0;ja=ta<<16|ra&65535;ra=((f(A,T)|0)+(ka&65535)|0)+(wa&65535)|0;sa=((f(I,T)|0)+(ka>>>16)|0)+(wa>>>16)|0;ta=((f(A,_)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,_)|0)+(sa>>>16)|0)+(ta>>>16)|0;ka=ta<<16|ra&65535;ra=((f(A,U)|0)+(la&65535)|0)+(wa&65535)|0;sa=((f(I,U)|0)+(la>>>16)|0)+(wa>>>16)|0;ta=((f(A,aa)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(I,aa)|0)+(sa>>>16)|0)+(ta>>>16)|0;la=ta<<16|ra&65535;ma=wa;ra=((f(B,N)|0)+(fa&65535)|0)+(na&65535)|0;sa=((f(J,N)|0)+(fa>>>16)|0)+(na>>>16)|0;ta=((f(B,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(J,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;fa=ta<<16|ra&65535;ra=((f(B,O)|0)+(ga&65535)|0)+(wa&65535)|0;sa=((f(J,O)|0)+(ga>>>16)|0)+(wa>>>16)|0;ta=((f(B,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(J,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;ga=ta<<16|ra&65535;ra=((f(B,P)|0)+(ha&65535)|0)+(wa&65535)|0;sa=((f(J,P)|0)+(ha>>>16)|0)+(wa>>>16)|0;ta=((f(B,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(J,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;ha=ta<<16|ra&65535;ra=((f(B,Q)|0)+(ia&65535)|0)+(wa&65535)|0;sa=((f(J,Q)|0)+(ia>>>16)|0)+(wa>>>16)|0;ta=((f(B,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(J,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;ia=ta<<16|ra&65535;ra=((f(B,R)|0)+(ja&65535)|0)+(wa&65535)|0;sa=((f(J,R)|0)+(ja>>>16)|0)+(wa>>>16)|0;ta=((f(B,Z)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(J,Z)|0)+(sa>>>16)|0)+(ta>>>16)|0;ja=ta<<16|ra&65535;ra=((f(B,S)|0)+(ka&65535)|0)+(wa&65535)|0;sa=((f(J,S)|0)+(ka>>>16)|0)+(wa>>>16)|0;ta=((f(B,$)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(J,$)|0)+(sa>>>16)|0)+(ta>>>16)|0;ka=ta<<16|ra&65535;ra=((f(B,T)|0)+(la&65535)|0)+(wa&65535)|0;sa=((f(J,T)|0)+(la>>>16)|0)+(wa>>>16)|0;ta=((f(B,_)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(J,_)|0)+(sa>>>16)|0)+(ta>>>16)|0;la=ta<<16|ra&65535;ra=((f(B,U)|0)+(ma&65535)|0)+(wa&65535)|0;sa=((f(J,U)|0)+(ma>>>16)|0)+(wa>>>16)|0;ta=((f(B,aa)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(J,aa)|0)+(sa>>>16)|0)+(ta>>>16)|0;ma=ta<<16|ra&65535;na=wa;ra=((f(C,N)|0)+(ga&65535)|0)+(oa&65535)|0;sa=((f(K,N)|0)+(ga>>>16)|0)+(oa>>>16)|0;ta=((f(C,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(K,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ga=ta<<16|ra&65535;ra=((f(C,O)|0)+(ha&65535)|0)+(wa&65535)|0;sa=((f(K,O)|0)+(ha>>>16)|0)+(wa>>>16)|0;ta=((f(C,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(K,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;ha=ta<<16|ra&65535;ra=((f(C,P)|0)+(ia&65535)|0)+(wa&65535)|0;sa=((f(K,P)|0)+(ia>>>16)|0)+(wa>>>16)|0;ta=((f(C,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(K,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;ia=ta<<16|ra&65535;ra=((f(C,Q)|0)+(ja&65535)|0)+(wa&65535)|0;sa=((f(K,Q)|0)+(ja>>>16)|0)+(wa>>>16)|0;ta=((f(C,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(K,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;ja=ta<<16|ra&65535;ra=((f(C,R)|0)+(ka&65535)|0)+(wa&65535)|0;sa=((f(K,R)|0)+(ka>>>16)|0)+(wa>>>16)|0;ta=((f(C,Z)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(K,Z)|0)+(sa>>>16)|0)+(ta>>>16)|0;ka=ta<<16|ra&65535;ra=((f(C,S)|0)+(la&65535)|0)+(wa&65535)|0;sa=((f(K,S)|0)+(la>>>16)|0)+(wa>>>16)|0;ta=((f(C,$)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(K,$)|0)+(sa>>>16)|0)+(ta>>>16)|0;la=ta<<16|ra&65535;ra=((f(C,T)|0)+(ma&65535)|0)+(wa&65535)|0;sa=((f(K,T)|0)+(ma>>>16)|0)+(wa>>>16)|0;ta=((f(C,_)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(K,_)|0)+(sa>>>16)|0)+(ta>>>16)|0;ma=ta<<16|ra&65535;ra=((f(C,U)|0)+(na&65535)|0)+(wa&65535)|0;sa=((f(K,U)|0)+(na>>>16)|0)+(wa>>>16)|0;ta=((f(C,aa)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(K,aa)|0)+(sa>>>16)|0)+(ta>>>16)|0;na=ta<<16|ra&65535;oa=wa;ra=((f(D,N)|0)+(ha&65535)|0)+(pa&65535)|0;sa=((f(L,N)|0)+(ha>>>16)|0)+(pa>>>16)|0;ta=((f(D,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(L,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ha=ta<<16|ra&65535;ra=((f(D,O)|0)+(ia&65535)|0)+(wa&65535)|0;sa=((f(L,O)|0)+(ia>>>16)|0)+(wa>>>16)|0;ta=((f(D,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(L,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;ia=ta<<16|ra&65535;ra=((f(D,P)|0)+(ja&65535)|0)+(wa&65535)|0;sa=((f(L,P)|0)+(ja>>>16)|0)+(wa>>>16)|0;ta=((f(D,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(L,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;ja=ta<<16|ra&65535;ra=((f(D,Q)|0)+(ka&65535)|0)+(wa&65535)|0;sa=((f(L,Q)|0)+(ka>>>16)|0)+(wa>>>16)|0;ta=((f(D,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(L,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;ka=ta<<16|ra&65535;ra=((f(D,R)|0)+(la&65535)|0)+(wa&65535)|0;sa=((f(L,R)|0)+(la>>>16)|0)+(wa>>>16)|0;ta=((f(D,Z)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(L,Z)|0)+(sa>>>16)|0)+(ta>>>16)|0;la=ta<<16|ra&65535;ra=((f(D,S)|0)+(ma&65535)|0)+(wa&65535)|0;sa=((f(L,S)|0)+(ma>>>16)|0)+(wa>>>16)|0;ta=((f(D,$)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(L,$)|0)+(sa>>>16)|0)+(ta>>>16)|0;ma=ta<<16|ra&65535;ra=((f(D,T)|0)+(na&65535)|0)+(wa&65535)|0;sa=((f(L,T)|0)+(na>>>16)|0)+(wa>>>16)|0;ta=((f(D,_)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(L,_)|0)+(sa>>>16)|0)+(ta>>>16)|0;na=ta<<16|ra&65535;ra=((f(D,U)|0)+(oa&65535)|0)+(wa&65535)|0;sa=((f(L,U)|0)+(oa>>>16)|0)+(wa>>>16)|0;ta=((f(D,aa)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(L,aa)|0)+(sa>>>16)|0)+(ta>>>16)|0;oa=ta<<16|ra&65535;pa=wa;ra=((f(E,N)|0)+(ia&65535)|0)+(qa&65535)|0;sa=((f(M,N)|0)+(ia>>>16)|0)+(qa>>>16)|0;ta=((f(E,V)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(M,V)|0)+(sa>>>16)|0)+(ta>>>16)|0;ia=ta<<16|ra&65535;ra=((f(E,O)|0)+(ja&65535)|0)+(wa&65535)|0;sa=((f(M,O)|0)+(ja>>>16)|0)+(wa>>>16)|0;ta=((f(E,W)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(M,W)|0)+(sa>>>16)|0)+(ta>>>16)|0;ja=ta<<16|ra&65535;ra=((f(E,P)|0)+(ka&65535)|0)+(wa&65535)|0;sa=((f(M,P)|0)+(ka>>>16)|0)+(wa>>>16)|0;ta=((f(E,X)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(M,X)|0)+(sa>>>16)|0)+(ta>>>16)|0;ka=ta<<16|ra&65535;ra=((f(E,Q)|0)+(la&65535)|0)+(wa&65535)|0;sa=((f(M,Q)|0)+(la>>>16)|0)+(wa>>>16)|0;ta=((f(E,Y)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(M,Y)|0)+(sa>>>16)|0)+(ta>>>16)|0;la=ta<<16|ra&65535;ra=((f(E,R)|0)+(ma&65535)|0)+(wa&65535)|0;sa=((f(M,R)|0)+(ma>>>16)|0)+(wa>>>16)|0;ta=((f(E,Z)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(M,Z)|0)+(sa>>>16)|0)+(ta>>>16)|0;ma=ta<<16|ra&65535;ra=((f(E,S)|0)+(na&65535)|0)+(wa&65535)|0;sa=((f(M,S)|0)+(na>>>16)|0)+(wa>>>16)|0;ta=((f(E,$)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(M,$)|0)+(sa>>>16)|0)+(ta>>>16)|0;na=ta<<16|ra&65535;ra=((f(E,T)|0)+(oa&65535)|0)+(wa&65535)|0;sa=((f(M,T)|0)+(oa>>>16)|0)+(wa>>>16)|0;ta=((f(E,_)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(M,_)|0)+(sa>>>16)|0)+(ta>>>16)|0;oa=ta<<16|ra&65535;ra=((f(E,U)|0)+(pa&65535)|0)+(wa&65535)|0;sa=((f(M,U)|0)+(pa>>>16)|0)+(wa>>>16)|0;ta=((f(E,aa)|0)+(sa&65535)|0)+(ra>>>16)|0;wa=((f(M,aa)|0)+(sa>>>16)|0)+(ta>>>16)|0;pa=ta<<16|ra&65535;qa=wa;Da=ya+(Ba+Ca|0)|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((ba&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ba>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((ca&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ca>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((da&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(da>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((ea&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ea>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((fa&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(fa>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((ga&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ga>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((ha&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ha>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((ia&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ia>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16}Da=ya+(Ba+Ca|0)|0;xa=e[Ga+Da>>2]|0;ra=(((xa&65535)+((ja&65535)<<1)|0)+ua|0)+va|0;ta=((xa>>>16)+(ja>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((ka&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ka>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((la&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(la>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((ma&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(ma>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((na&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(na>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((oa&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(oa>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((pa&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(pa>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;ua=ta>>>16;Da=Da+4|0;xa=e[Ga+Da>>2]|0;ra=((xa&65535)+((qa&65535)<<1)|0)+ua|0;ta=((xa>>>16)+(qa>>>16<<1)|0)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;va=ta>>>16}for(Da=Da+4|0;!!va&(Da|0)>2]|0;ra=(xa&65535)+va|0;ta=(xa>>>16)+(ra>>>16)|0;e[Ga+Da>>2]=ta<<16|ra&65535;va=ta>>>16}}}}function s(u,v,w,x,y){u=u|0;v=v|0;w=w|0;x=x|0;y=y|0;var z=0,A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0,O=0,P=0,Q=0,R=0;for(P=v-1&-4;(P|0)>=0;P=P-4|0){z=e[u+P>>2]|0;if(z){v=P;break}}for(P=x-1&-4;(P|0)>=0;P=P-4|0){A=e[w+P>>2]|0;if(A){x=P;break}}while((A&2147483648)==0){A=A<<1;B=B+1|0}D=e[u+v>>2]|0;if(B){C=D>>>(32-B|0);for(P=v-4|0;(P|0)>=0;P=P-4|0){z=e[u+P>>2]|0;e[u+P+4>>2]=D<>>(32-B|0):0);D=z}e[u>>2]=D<>2]|0;for(P=x-4|0;(P|0)>=0;P=P-4|0){A=e[w+P>>2]|0;e[w+P+4>>2]=E<>>(32-B|0);E=A}e[w>>2]=E<>2]|0;F=E>>>16,G=E&65535;for(P=v;(P|0)>=(x|0);P=P-4|0){Q=P-x|0;D=e[u+P>>2]|0;H=(C>>>0)/(F>>>0)|0,J=(C>>>0)%(F>>>0)|0,L=f(H,G)|0;while((H|0)==65536|L>>>0>(J<<16|D>>>16)>>>0){H=H-1|0,J=J+F|0,L=L-G|0;if((J|0)>=65536)break}N=0,O=0;for(R=0;(R|0)<=(x|0);R=R+4|0){A=e[w+R>>2]|0;L=(f(H,A&65535)|0)+(N>>>16)|0;M=(f(H,A>>>16)|0)+(L>>>16)|0;A=N&65535|L<<16;N=M;z=e[u+Q+R>>2]|0;L=((z&65535)-(A&65535)|0)+O|0;M=((z>>>16)-(A>>>16)|0)+(L>>16)|0;e[u+Q+R>>2]=M<<16|L&65535;O=M>>16}L=((C&65535)-(N&65535)|0)+O|0;M=((C>>>16)-(N>>>16)|0)+(L>>16)|0;C=M<<16|L&65535;O=M>>16;if(O){H=H-1|0;O=0;for(R=0;(R|0)<=(x|0);R=R+4|0){A=e[w+R>>2]|0;z=e[u+Q+R>>2]|0;L=(z&65535)+O|0;M=(z>>>16)+A+(L>>>16)|0;e[u+Q+R>>2]=M<<16|L&65535;O=M>>>16}C=C+O|0}D=e[u+P>>2]|0;z=C<<16|D>>>16;I=(z>>>0)/(F>>>0)|0,K=(z>>>0)%(F>>>0)|0,L=f(I,G)|0;while((I|0)==65536|L>>>0>(K<<16|D&65535)>>>0){I=I-1|0,K=K+F|0,L=L-G|0;if((K|0)>=65536)break}N=0,O=0;for(R=0;(R|0)<=(x|0);R=R+4|0){A=e[w+R>>2]|0;L=(f(I,A&65535)|0)+(N&65535)|0;M=((f(I,A>>>16)|0)+(L>>>16)|0)+(N>>>16)|0;A=L&65535|M<<16;N=M>>>16;z=e[u+Q+R>>2]|0;L=((z&65535)-(A&65535)|0)+O|0;M=((z>>>16)-(A>>>16)|0)+(L>>16)|0;O=M>>16;e[u+Q+R>>2]=M<<16|L&65535}L=((C&65535)-(N&65535)|0)+O|0;M=((C>>>16)-(N>>>16)|0)+(L>>16)|0;O=M>>16;if(O){I=I-1|0;O=0;for(R=0;(R|0)<=(x|0);R=R+4|0){A=e[w+R>>2]|0;z=e[u+Q+R>>2]|0;L=((z&65535)+(A&65535)|0)+O|0;M=((z>>>16)+(A>>>16)|0)+(L>>>16)|0;O=M>>>16;e[u+Q+R>>2]=L&65535|M<<16}}e[y+Q>>2]=H<<16|I;C=e[u+P>>2]|0}if(B){D=e[u>>2]|0;for(P=4;(P|0)<=(x|0);P=P+4|0){z=e[u+P>>2]|0;e[u+P-4>>2]=z<<(32-B|0)|D>>>B;D=z}e[u+x>>2]=D>>>B}}function t(u,v,w,x,y,z){u=u|0;v=v|0;w=w|0;x=x|0;y=y|0;z=z|0;var A=0,B=0,C=0,D=0,E=0,F=0,G=0,H=0,I=0,J=0,K=0,L=0,M=0,N=0;A=h(x<<1)|0;k(x<<1,0,A);j(v,u,A);for(L=0;(L|0)<(x|0);L=L+4|0){C=e[A+L>>2]|0,D=C&65535,C=C>>>16;F=y>>>16,E=y&65535;G=f(D,E)|0,H=((f(D,F)|0)+(f(C,E)|0)|0)+(G>>>16)|0;D=G&65535,C=H&65535;K=0;for(M=0;(M|0)<(x|0);M=M+4|0){N=L+M|0;F=e[w+M>>2]|0,E=F&65535,F=F>>>16;J=e[A+N>>2]|0;G=((f(D,E)|0)+(K&65535)|0)+(J&65535)|0;H=((f(D,F)|0)+(K>>>16)|0)+(J>>>16)|0;I=((f(C,E)|0)+(H&65535)|0)+(G>>>16)|0;K=((f(C,F)|0)+(I>>>16)|0)+(H>>>16)|0;J=I<<16|G&65535;e[A+N>>2]=J}N=L+M|0;J=e[A+N>>2]|0;G=((J&65535)+(K&65535)|0)+B|0;H=((J>>>16)+(K>>>16)|0)+(G>>>16)|0;e[A+N>>2]=H<<16|G&65535;B=H>>>16}j(x,A+x|0,z);i(x<<1);if(B|(m(w,x,z,x)|0)<=0){p(z,x,w,x,z,x)|0}}return{sreset:g,salloc:h,sfree:i,z:k,tst:n,neg:l,cmp:m,add:o,sub:p,mul:q,sqr:r,div:s,mredc:t}}function wa(a){return a instanceof ya}function xa(a,b){return a*b|0}function ya(a){var b=Kb,c=0,d=0;if(n(a)&&(a=f(a)),o(a)&&(a=new Uint8Array(a)),void 0===a);else if(m(a)){var e=Math.abs(a);e>4294967295?(b=new Uint32Array(2),b[0]=0|e,b[1]=e/4294967296|0,c=52):e>0?(b=new Uint32Array(1),b[0]=e,c=32):(b=Kb,c=0),d=0>a?-1:1}else if(p(a)){for(var g=0;!a[g];g++);if(c=8*(a.length-g),!c)return Mb;b=new Uint32Array(c+31>>5);for(var h=a.length-4;h>=g;h-=4)b[a.length-4-h>>2]=a[h]<<24|a[h+1]<<16|a[h+2]<<8|a[h+3];g-h===3?b[b.length-1]=a[g]:g-h===2?b[b.length-1]=a[g]<<8|a[g+1]:g-h===1&&(b[b.length-1]=a[g]<<16|a[g+1]<<8|a[g+2]),d=1}else{if("object"!=typeof a||null===a)throw new TypeError("number is of unexpected type");b=new Uint32Array(a.limbs),c=a.bitLength,d=a.sign}this.limbs=b,this.bitLength=c,this.sign=d}function za(a){a=a||16;var b=this.limbs,c=this.bitLength,e="";if(16!==a)throw new d("bad radix");for(var f=(c+31>>5)-1;f>=0;f--){var g=b[f].toString(16);e+="00000000".substr(g.length),e+=g}return e=e.replace(/^0+/,""),e.length||(e="0"),this.sign<0&&(e="-"+e),e}function Aa(){var a=this.bitLength,b=this.limbs;if(0===a)return new Uint8Array(0);for(var c=a+7>>3,d=new Uint8Array(c),e=0;c>e;e++){var f=c-e-1;d[e]=b[f>>2]>>((3&f)<<3)}return d}function Ba(){var a=this.limbs,b=this.bitLength,c=this.sign;if(!c)return 0;if(32>=b)return c*(a[0]>>>0);if(52>=b)return c*(4294967296*(a[1]>>>0)+(a[0]>>>0));var d,e,f=0;for(d=a.length-1;d>=0;d--)if(0!==(e=a[d])){for(;0===(e<>>0):c*(1048576*((a[d]<>>32-f:0))>>>0)+((a[d-1]<1?a[d-2]>>>32-f:0))>>>12))*Math.pow(2,32*d-f-52)}function Ca(a){var b=this.limbs,c=this.bitLength;if(a>=c)return this;var d=new ya,e=a+31>>5,f=a%32;return d.limbs=new Uint32Array(b.subarray(0,e)),d.bitLength=a,d.sign=this.sign,f&&(d.limbs[e-1]&=-1>>>32-f),d}function Da(a,b){if(!m(a))throw new TypeError("TODO");if(void 0!==b&&!m(b))throw new TypeError("TODO");var c=this.limbs,d=this.bitLength;if(0>a)throw new RangeError("TODO");if(a>=d)return Mb;(void 0===b||b>d-a)&&(b=d-a);var e,f=new ya,g=a>>5,h=a+b+31>>5,i=b+31>>5,j=a%32,k=b%32;if(e=new Uint32Array(i),j){for(var l=0;h-g-1>l;l++)e[l]=c[g+l]>>>j|c[g+l+1]<<32-j;e[l]=c[g+l]>>>j}else e.set(c.subarray(g,h));return k&&(e[i-1]&=-1>>>32-k),f.limbs=e,f.bitLength=b,f.sign=this.sign,f}function Ea(){var a=new ya;return a.limbs=this.limbs,a.bitLength=this.bitLength,a.sign=-1*this.sign,a}function Fa(a){wa(a)||(a=new ya(a));var b=this.limbs,c=b.length,d=a.limbs,e=d.length,f=0;return this.signa.sign?1:(Jb.set(b,0),Jb.set(d,c),f=Hb.cmp(0,c<<2,c<<2,e<<2),f*this.sign)}function Ga(a){if(wa(a)||(a=new ya(a)),!this.sign)return a;if(!a.sign)return this;var b,c,d,e,f=this.bitLength,g=this.limbs,h=g.length,i=this.sign,j=a.bitLength,k=a.limbs,l=k.length,m=a.sign,n=new ya;b=(f>j?f:j)+(i*m>0?1:0),c=b+31>>5,Hb.sreset();var o=Hb.salloc(h<<2),p=Hb.salloc(l<<2),q=Hb.salloc(c<<2);return Hb.z(q-o+(c<<2),0,o),Jb.set(g,o>>2),Jb.set(k,p>>2),i*m>0?(Hb.add(o,h<<2,p,l<<2,q,c<<2),d=i):i>m?(e=Hb.sub(o,h<<2,p,l<<2,q,c<<2),d=e?m:i):(e=Hb.sub(p,l<<2,o,h<<2,q,c<<2),d=e?i:m),e&&Hb.neg(q,c<<2,q,c<<2),0===Hb.tst(q,c<<2)?Mb:(n.limbs=new Uint32Array(Jb.subarray(q>>2,(q>>2)+c)),n.bitLength=b,n.sign=d,n)}function Ha(a){return wa(a)||(a=new ya(a)),this.add(a.negate())}function Ia(a){if(wa(a)||(a=new ya(a)),!this.sign||!a.sign)return Mb;var b,c,d=this.bitLength,e=this.limbs,f=e.length,g=a.bitLength,h=a.limbs,i=h.length,j=new ya;b=d+g,c=b+31>>5,Hb.sreset();var k=Hb.salloc(f<<2),l=Hb.salloc(i<<2),m=Hb.salloc(c<<2);return Hb.z(m-k+(c<<2),0,k),Jb.set(e,k>>2),Jb.set(h,l>>2),Hb.mul(k,f<<2,l,i<<2,m,c<<2),j.limbs=new Uint32Array(Jb.subarray(m>>2,(m>>2)+c)),j.sign=this.sign*a.sign,j.bitLength=b,j}function Ja(){if(!this.sign)return Mb;var a,b,c=this.bitLength,d=this.limbs,e=d.length,f=new ya;a=c<<1,b=a+31>>5,Hb.sreset();var g=Hb.salloc(e<<2),h=Hb.salloc(b<<2);return Hb.z(h-g+(b<<2),0,g),Jb.set(d,g>>2),Hb.sqr(g,e<<2,h),f.limbs=new Uint32Array(Jb.subarray(h>>2,(h>>2)+b)),f.bitLength=a,f.sign=1,f}function Ka(a){wa(a)||(a=new ya(a));var b,c,d=this.bitLength,e=this.limbs,f=e.length,g=a.bitLength,h=a.limbs,i=h.length,j=Mb,k=Mb;Hb.sreset();var l=Hb.salloc(f<<2),m=Hb.salloc(i<<2),n=Hb.salloc(f<<2);return Hb.z(n-l+(f<<2),0,l),Jb.set(e,l>>2),Jb.set(h,m>>2),Hb.div(l,f<<2,m,i<<2,n),b=Hb.tst(n,f<<2)>>2,b&&(j=new ya,j.limbs=new Uint32Array(Jb.subarray(n>>2,(n>>2)+b)),j.bitLength=b<<5>d?d:b<<5,j.sign=this.sign*a.sign),c=Hb.tst(l,i<<2)>>2,c&&(k=new ya,k.limbs=new Uint32Array(Jb.subarray(l>>2,(l>>2)+c)),k.bitLength=c<<5>g?g:c<<5,k.sign=this.sign),{quotient:j,remainder:k}}function La(a,b){var c,d,e,f,g=0>a?-1:1,h=0>b?-1:1,i=1,j=0,k=0,l=1;for(a*=g,b*=h,f=b>a,f&&(e=a,a=b,b=e,e=g,g=h,h=e),d=Math.floor(a/b),c=a-d*b;c;)e=i-d*j,i=j,j=e,e=k-d*l,k=l,l=e,a=b,b=c,d=Math.floor(a/b),c=a-d*b;return j*=g,l*=h,f&&(e=j,j=l,l=e),{gcd:b,x:j,y:l}}function Ma(a,b){wa(a)||(a=new ya(a)),wa(b)||(b=new ya(b));var c=a.sign,d=b.sign;0>c&&(a=a.negate()),0>d&&(b=b.negate());var e=a.compare(b);if(0>e){var f=a;a=b,b=f,f=c,c=d,d=f}var g,h,i,j=Nb,k=Mb,l=b.bitLength,m=Mb,n=Nb,o=a.bitLength;for(g=a.divide(b);(h=g.remainder)!==Mb;)i=g.quotient,g=j.subtract(i.multiply(k).clamp(l)).clamp(l),j=k,k=g,g=m.subtract(i.multiply(n).clamp(o)).clamp(o),m=n,n=g,a=b,b=h,g=a.divide(b);if(0>c&&(k=k.negate()),0>d&&(n=n.negate()),0>e){var f=k;k=n,n=f}return{gcd:b,x:k,y:n}}function Na(){if(ya.apply(this,arguments),this.valueOf()<1)throw new RangeError;if(!(this.bitLength<=32)){var a;if(1&this.limbs[0]){var b=(this.bitLength+31&-32)+1,c=new Uint32Array(b+31>>5);c[c.length-1]=1,a=new ya,a.sign=1,a.bitLength=b,a.limbs=c;var d=La(4294967296,this.limbs[0]).y;this.coefficient=0>d?-d:4294967296-d,this.comodulus=a,this.comodulusRemainder=a.divide(this).remainder,this.comodulusRemainderSquare=a.square().divide(this).remainder}}}function Oa(a){return wa(a)||(a=new ya(a)),a.bitLength<=32&&this.bitLength<=32?new ya(a.valueOf()%this.valueOf()):a.compare(this)<0?a:a.divide(this).remainder}function Pa(a){a=this.reduce(a);var b=Ma(this,a);return 1!==b.gcd.valueOf()?null:(b=b.y,b.sign<0&&(b=b.add(this).clamp(this.bitLength)),b)}function Qa(a,b){wa(a)||(a=new ya(a)),wa(b)||(b=new ya(b));for(var c=0,d=0;d>>=1;var f=8;b.bitLength<=4536&&(f=7),b.bitLength<=1736&&(f=6),b.bitLength<=630&&(f=5),b.bitLength<=210&&(f=4),b.bitLength<=60&&(f=3),b.bitLength<=12&&(f=2),1<=c&&(f=1),a=Ra(this.reduce(a).multiply(this.comodulusRemainderSquare),this);var g=Ra(a.square(),this),h=new Array(1<d;d++)h[d]=Ra(h[d-1].multiply(g),this);for(var i=this.comodulusRemainder,j=i,d=b.limbs.length-1;d>=0;d--)for(var e=b.limbs[d],k=32;k>0;)if(2147483648&e){for(var l=e>>>32-f,m=f;0===(1&l);)l>>>=1,m--;for(var n=h[l>>>1];l;)l>>>=1,j!==i&&(j=Ra(j.square(),this));j=j!==i?Ra(j.multiply(n),this):n,e<<=m,k-=m}else j!==i&&(j=Ra(j.square(),this)),e<<=1,k--;return j=Ra(j,this)}function Ra(a,b){var c=a.limbs,d=c.length,e=b.limbs,f=e.length,g=b.coefficient;Hb.sreset();var h=Hb.salloc(d<<2),i=Hb.salloc(f<<2),j=Hb.salloc(f<<2);Hb.z(j-h+(f<<2),0,h),Jb.set(c,h>>2),Jb.set(e,i>>2),Hb.mredc(h,d<<2,i,f<<2,g,j);var k=new ya;return k.limbs=new Uint32Array(Jb.subarray(j>>2,(j>>2)+f)),k.bitLength=b.bitLength,k.sign=1,k}function Sa(a){var b=new ya(this),c=0;for(b.limbs[0]-=1;0===b.limbs[c>>5];)c+=32;for(;0===(b.limbs[c>>5]>>(31&c)&1);)c++;b=b.slice(c);for(var d=new Na(this),e=this.subtract(Nb),f=new ya(this),g=this.limbs.length-1;0===f.limbs[g];)g--;for(;--a>=0;){for(ta(f.limbs),f.limbs[0]<2&&(f.limbs[0]+=2);f.compare(e)>=0;)f.limbs[g]>>>=1;var h=d.power(f,b);if(0!==h.compare(Nb)&&0!==h.compare(e)){for(var i=c;--i>0;){if(h=h.square().divide(d).remainder,0===h.compare(Nb))return!1;if(0===h.compare(e))break}if(0===i)return!1}}return!0}function Ta(a){a=a||80;var b=this.limbs,c=0;if(0===(1&b[0]))return!1;if(1>=a)return!0;var d=0,e=0,f=0;for(c=0;c>>=2;for(var h=b[c];h;)e+=3&h,h>>>=2,e-=3&h,h>>>=2;for(var i=b[c];i;)f+=15&i,i>>>=4,f-=15&i,i>>>=4}return d%3&&e%5&&f%17?2>=a?!0:Sa.call(this,a>>>1):!1}function Ua(a){if(Pb.length>=a)return Pb.slice(0,a);for(var b=Pb[Pb.length-1]+2;Pb.length=d*d&&b%d!=0;d=Pb[++c]);d*d>b&&Pb.push(b)}return Pb}function Va(a,c){var d=a+31>>5,e=new ya({sign:1,bitLength:a,limbs:d}),f=e.limbs,g=1e4;512>=a&&(g=2200),256>=a&&(g=600);var h=Ua(g),i=new Uint32Array(g),j=a*b.Math.LN2|0,k=27;for(a>=250&&(k=12),a>=450&&(k=6),a>=850&&(k=3),a>=1300&&(k=2);;){ta(f),f[0]|=1,f[d-1]|=1<<(a-1&31),31&a&&(f[d-1]&=l(a+1&31)-1),i[0]=1;for(var m=1;g>m;m++)i[m]=e.divide(h[m]).remainder.valueOf();a:for(var n=0;j>n;n+=2,f[0]+=2){for(var m=1;g>m;m++)if((i[m]+n)%h[m]===0)continue a;if(("function"!=typeof c||c(e))&&Sa.call(e,k))return e}}}c.prototype=Object.create(Error.prototype,{name:{value:"IllegalStateError"}}),d.prototype=Object.create(Error.prototype,{name:{value:"IllegalArgumentError"}}),e.prototype=Object.create(Error.prototype,{name:{value:"SecurityError"}});var Wa=b.Float64Array||b.Float32Array;a.string_to_bytes=f,a.hex_to_bytes=g,a.base64_to_bytes=h,a.bytes_to_string=i,a.bytes_to_hex=j,a.bytes_to_base64=k,b.IllegalStateError=c,b.IllegalArgumentError=d,b.SecurityError=e;var Xa=function(){"use strict";function a(){e=[],f=[];var a,b,c=1;for(a=0;255>a;a++)e[a]=c,b=128&c,c<<=1,c&=255,128===b&&(c^=27),c^=e[a],f[e[a]]=a;e[255]=e[0],f[0]=0,k=!0}function b(a,b){var c=e[(f[a]+f[b])%255];return(0===a||0===b)&&(c=0),c}function c(a){var b=e[255-f[a]];return 0===a&&(b=0),b}function d(){function d(a){var b,d,e;for(d=e=c(a),b=0;4>b;b++)d=255&(d<<1|d>>>7),e^=d;return e^=99}k||a(),g=[],h=[],i=[[],[],[],[]],j=[[],[],[],[]];for(var e=0;256>e;e++){var f=d(e);g[e]=f,h[f]=e,i[0][e]=b(2,f)<<24|f<<16|f<<8|b(3,f),j[0][f]=b(14,e)<<24|b(9,e)<<16|b(13,e)<<8|b(11,e);for(var l=1;4>l;l++)i[l][e]=i[l-1][e]>>>8|i[l-1][e]<<24,j[l][f]=j[l-1][f]>>>8|j[l-1][f]<<24}}var e,f,g,h,i,j,k=!1,l=!1,m=function(a,b,c){function e(a,b,c,d,e,h,i,k,l){var m=f.subarray(0,60),o=f.subarray(256,316);m.set([b,c,d,e,h,i,k,l]);for(var p=a,q=1;4*a+28>p;p++){var r=m[p-1];(p%a===0||8===a&&p%a===4)&&(r=g[r>>>24]<<24^g[r>>>16&255]<<16^g[r>>>8&255]<<8^g[255&r]),p%a===0&&(r=r<<8^r>>>24^q<<24,q=q<<1^(128&q?27:0)),m[p]=m[p-a]^r}for(var s=0;p>s;s+=4)for(var t=0;4>t;t++){var r=m[p-(4+s)+(4-t)%4];4>s||s>=p-4?o[s+t]=r:o[s+t]=j[0][g[r>>>24]]^j[1][g[r>>>16&255]]^j[2][g[r>>>8&255]]^j[3][g[255&r]]}n.set_rounds(a+5)}l||d();var f=new Uint32Array(c);f.set(g,512),f.set(h,768);for(var k=0;4>k;k++)f.set(i[k],4096+1024*k>>2),f.set(j[k],8192+1024*k>>2);var m={Uint8Array:Uint8Array,Uint32Array:Uint32Array},n=function(a,b,c){"use asm";var d=0,e=0,f=0,g=0,h=0,i=0,j=0,k=0,l=0,m=0,n=0,o=0,p=0,q=0,r=0,s=0,t=0,u=0,v=0,w=0,x=0;var y=new a.Uint32Array(c),z=new a.Uint8Array(c);function A(X,Y,Z,$,_,aa,ba,ca){X=X|0;Y=Y|0;Z=Z|0;$=$|0;_=_|0;aa=aa|0;ba=ba|0;ca=ca|0;var da=0,ea=0,fa=0,ga=0,ha=0,ia=0,ja=0,ka=0;da=Z|1024,ea=Z|2048,fa=Z|3072;_=_^y[(X|0)>>2],aa=aa^y[(X|4)>>2],ba=ba^y[(X|8)>>2],ca=ca^y[(X|12)>>2];for(ka=16;(ka|0)<=$<<4;ka=ka+16|0){ga=y[(Z|_>>22&1020)>>2]^y[(da|aa>>14&1020)>>2]^y[(ea|ba>>6&1020)>>2]^y[(fa|ca<<2&1020)>>2]^y[(X|ka|0)>>2],ha=y[(Z|aa>>22&1020)>>2]^y[(da|ba>>14&1020)>>2]^y[(ea|ca>>6&1020)>>2]^y[(fa|_<<2&1020)>>2]^y[(X|ka|4)>>2],ia=y[(Z|ba>>22&1020)>>2]^y[(da|ca>>14&1020)>>2]^y[(ea|_>>6&1020)>>2]^y[(fa|aa<<2&1020)>>2]^y[(X|ka|8)>>2],ja=y[(Z|ca>>22&1020)>>2]^y[(da|_>>14&1020)>>2]^y[(ea|aa>>6&1020)>>2]^y[(fa|ba<<2&1020)>>2]^y[(X|ka|12)>>2];_=ga,aa=ha,ba=ia,ca=ja}d=y[(Y|_>>22&1020)>>2]<<24^y[(Y|aa>>14&1020)>>2]<<16^y[(Y|ba>>6&1020)>>2]<<8^y[(Y|ca<<2&1020)>>2]^y[(X|ka|0)>>2],e=y[(Y|aa>>22&1020)>>2]<<24^y[(Y|ba>>14&1020)>>2]<<16^y[(Y|ca>>6&1020)>>2]<<8^y[(Y|_<<2&1020)>>2]^y[(X|ka|4)>>2],f=y[(Y|ba>>22&1020)>>2]<<24^y[(Y|ca>>14&1020)>>2]<<16^y[(Y|_>>6&1020)>>2]<<8^y[(Y|aa<<2&1020)>>2]^y[(X|ka|8)>>2],g=y[(Y|ca>>22&1020)>>2]<<24^y[(Y|_>>14&1020)>>2]<<16^y[(Y|aa>>6&1020)>>2]<<8^y[(Y|ba<<2&1020)>>2]^y[(X|ka|12)>>2]}function B(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;A(0,2048,4096,x,X,Y,Z,$)}function C(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;var _=0;A(1024,3072,8192,x,X,$,Z,Y);_=e,e=g,g=_}function D(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;A(0,2048,4096,x,h^X,i^Y,j^Z,k^$);h=d,i=e,j=f,k=g}function E(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;var _=0;A(1024,3072,8192,x,X,$,Z,Y);_=e,e=g,g=_;d=d^h,e=e^i,f=f^j,g=g^k;h=X,i=Y,j=Z,k=$}function F(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;A(0,2048,4096,x,h,i,j,k);h=d=d^X,i=e=e^Y,j=f=f^Z,k=g=g^$}function G(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;A(0,2048,4096,x,h,i,j,k);d=d^X,e=e^Y,f=f^Z,g=g^$;h=X,i=Y,j=Z,k=$}function H(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;A(0,2048,4096,x,h,i,j,k);h=d,i=e,j=f,k=g;d=d^X,e=e^Y,f=f^Z,g=g^$}function I(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;A(0,2048,4096,x,l,m,n,o);o=~s&o|s&o+1,n=~r&n|r&n+((o|0)==0),m=~q&m|q&m+((n|0)==0),l=~p&l|p&l+((m|0)==0);d=d^X,e=e^Y,f=f^Z,g=g^$}function J(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;var _=0,aa=0,ba=0,ca=0,da=0,ea=0,fa=0,ga=0,ha=0,ia=0;X=X^h,Y=Y^i,Z=Z^j,$=$^k;_=t|0,aa=u|0,ba=v|0,ca=w|0;for(;(ha|0)<128;ha=ha+1|0){if(_>>>31){da=da^X,ea=ea^Y,fa=fa^Z,ga=ga^$}_=_<<1|aa>>>31,aa=aa<<1|ba>>>31,ba=ba<<1|ca>>>31,ca=ca<<1;ia=$&1;$=$>>>1|Z<<31,Z=Z>>>1|Y<<31,Y=Y>>>1|X<<31,X=X>>>1;if(ia)X=X^3774873600}h=da,i=ea,j=fa,k=ga}function K(X){X=X|0;x=X}function L(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;d=X,e=Y,f=Z,g=$}function M(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;h=X,i=Y,j=Z,k=$}function N(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;l=X,m=Y,n=Z,o=$}function O(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;p=X,q=Y,r=Z,s=$}function P(X,Y,Z,$){X=X|0;Y=Y|0;Z=Z|0;$=$|0;o=~s&o|s&$,n=~r&n|r&Z,m=~q&m|q&Y,l=~p&l|p&X}function Q(X){X=X|0;if(X&15)return-1;z[X|0]=d>>>24,z[X|1]=d>>>16&255,z[X|2]=d>>>8&255,z[X|3]=d&255,z[X|4]=e>>>24,z[X|5]=e>>>16&255,z[X|6]=e>>>8&255,z[X|7]=e&255,z[X|8]=f>>>24,z[X|9]=f>>>16&255,z[X|10]=f>>>8&255,z[X|11]=f&255,z[X|12]=g>>>24,z[X|13]=g>>>16&255,z[X|14]=g>>>8&255,z[X|15]=g&255;return 16}function R(X){X=X|0;if(X&15)return-1;z[X|0]=h>>>24,z[X|1]=h>>>16&255,z[X|2]=h>>>8&255,z[X|3]=h&255,z[X|4]=i>>>24,z[X|5]=i>>>16&255,z[X|6]=i>>>8&255,z[X|7]=i&255,z[X|8]=j>>>24,z[X|9]=j>>>16&255,z[X|10]=j>>>8&255,z[X|11]=j&255,z[X|12]=k>>>24,z[X|13]=k>>>16&255,z[X|14]=k>>>8&255,z[X|15]=k&255;return 16}function S(){B(0,0,0,0);t=d,u=e,v=f,w=g}function T(X,Y,Z){X=X|0;Y=Y|0;Z=Z|0;var $=0;if(Y&15)return-1;while((Z|0)>=16){V[X&7](z[Y|0]<<24|z[Y|1]<<16|z[Y|2]<<8|z[Y|3],z[Y|4]<<24|z[Y|5]<<16|z[Y|6]<<8|z[Y|7],z[Y|8]<<24|z[Y|9]<<16|z[Y|10]<<8|z[Y|11],z[Y|12]<<24|z[Y|13]<<16|z[Y|14]<<8|z[Y|15]);z[Y|0]=d>>>24,z[Y|1]=d>>>16&255,z[Y|2]=d>>>8&255,z[Y|3]=d&255,z[Y|4]=e>>>24,z[Y|5]=e>>>16&255,z[Y|6]=e>>>8&255,z[Y|7]=e&255,z[Y|8]=f>>>24,z[Y|9]=f>>>16&255,z[Y|10]=f>>>8&255,z[Y|11]=f&255,z[Y|12]=g>>>24,z[Y|13]=g>>>16&255,z[Y|14]=g>>>8&255,z[Y|15]=g&255;$=$+16|0,Y=Y+16|0,Z=Z-16|0}return $|0}function U(X,Y,Z){X=X|0;Y=Y|0;Z=Z|0;var $=0;if(Y&15)return-1;while((Z|0)>=16){W[X&1](z[Y|0]<<24|z[Y|1]<<16|z[Y|2]<<8|z[Y|3],z[Y|4]<<24|z[Y|5]<<16|z[Y|6]<<8|z[Y|7],z[Y|8]<<24|z[Y|9]<<16|z[Y|10]<<8|z[Y|11],z[Y|12]<<24|z[Y|13]<<16|z[Y|14]<<8|z[Y|15]);$=$+16|0,Y=Y+16|0,Z=Z-16|0}return $|0}var V=[B,C,D,E,F,G,H,I];var W=[D,J];return{set_rounds:K,set_state:L,
-set_iv:M,set_nonce:N,set_mask:O,set_counter:P,get_state:Q,get_iv:R,gcm_init:S,cipher:T,mac:U}}(m,b,c);return n.set_key=e,n};return m.ENC={ECB:0,CBC:2,CFB:4,OFB:6,CTR:7},m.DEC={ECB:1,CBC:3,CFB:5,OFB:6,CTR:7},m.MAC={CBC:0,GCM:1},m.HEAP_DATA=16384,m}(),Ya=A.prototype;Ya.BLOCK_SIZE=16,Ya.reset=D,Ya.encrypt=z,Ya.decrypt=z;var Za=B.prototype;Za.BLOCK_SIZE=16,Za.reset=D,Za.process=y,Za.finish=z;var $a=68719476704,_a=F.prototype;_a.BLOCK_SIZE=16,_a.reset=I,_a.encrypt=L,_a.decrypt=O;var ab=G.prototype;ab.BLOCK_SIZE=16,ab.reset=I,ab.process=J,ab.finish=K;var bb=H.prototype;bb.BLOCK_SIZE=16,bb.reset=I,bb.process=M,bb.finish=N;var cb=new Uint8Array(1048576),db=Xa(b,null,cb.buffer);a.AES_GCM=F,a.AES_GCM.encrypt=P,a.AES_GCM.decrypt=Q,a.AES_GCM.Encrypt=G,a.AES_GCM.Decrypt=H;var eb=64,fb=32;V.BLOCK_SIZE=eb,V.HASH_SIZE=fb;var gb=V.prototype;gb.reset=R,gb.process=S,gb.finish=T;var hb=null;V.bytes=X,V.hex=Y,V.base64=Z,a.SHA256=V;var ib=$.prototype;ib.reset=ba,ib.process=ca,ib.finish=da,ea.BLOCK_SIZE=V.BLOCK_SIZE,ea.HMAC_SIZE=V.HASH_SIZE;var jb=ea.prototype;jb.reset=fa,jb.process=ca,jb.finish=ga;var kb=null;ea.bytes=ia,ea.hex=ja,ea.base64=ka,a.HMAC_SHA256=ea;var lb=la.prototype;lb.reset=ma,lb.generate=na;var mb=oa.prototype;mb.reset=ma,mb.generate=pa;var nb,ob=null,pb=function(){function a(){function a(){b^=d<<11,l=l+b|0,d=d+f|0,d^=f>>>2,m=m+d|0,f=f+l|0,f^=l<<8,n=n+f|0,l=l+m|0,l^=m>>>16,o=o+l|0,m=m+n|0,m^=n<<10,p=p+m|0,n=n+o|0,n^=o>>>4,b=b+n|0,o=o+p|0,o^=p<<8,d=d+o|0,p=p+b|0,p^=b>>>9,f=f+p|0,b=b+d|0}var b,d,f,l,m,n,o,p;h=i=j=0,b=d=f=l=m=n=o=p=2654435769;for(var q=0;4>q;q++)a();for(var q=0;256>q;q+=8)b=b+g[0|q]|0,d=d+g[1|q]|0,f=f+g[2|q]|0,l=l+g[3|q]|0,m=m+g[4|q]|0,n=n+g[5|q]|0,o=o+g[6|q]|0,p=p+g[7|q]|0,a(),e.set([b,d,f,l,m,n,o,p],q);for(var q=0;256>q;q+=8)b=b+e[0|q]|0,d=d+e[1|q]|0,f=f+e[2|q]|0,l=l+e[3|q]|0,m=m+e[4|q]|0,n=n+e[5|q]|0,o=o+e[6|q]|0,p=p+e[7|q]|0,a(),e.set([b,d,f,l,m,n,o,p],q);c(1),k=256}function b(b){var c,d,e,h,i;if(q(b))b=new Uint8Array(b.buffer);else if(m(b))h=new Wa(1),h[0]=b,b=new Uint8Array(h.buffer);else if(n(b))b=f(b);else{if(!o(b))throw new TypeError("bad seed type");b=new Uint8Array(b)}for(i=b.length,d=0;i>d;d+=1024){for(e=d,c=0;1024>c&&i>e;e=d|++c)g[c>>2]^=b[e]<<((3&c)<<3);a()}}function c(a){a=a||1;for(var b,c,d;a--;)for(j=j+1|0,i=i+j|0,b=0;256>b;b+=4)h^=h<<13,h=e[b+128&255]+h|0,c=e[0|b],e[0|b]=d=e[c>>>2&255]+(h+i|0)|0,g[0|b]=i=e[d>>>10&255]+c|0,h^=h>>>6,h=e[b+129&255]+h|0,c=e[1|b],e[1|b]=d=e[c>>>2&255]+(h+i|0)|0,g[1|b]=i=e[d>>>10&255]+c|0,h^=h<<2,h=e[b+130&255]+h|0,c=e[2|b],e[2|b]=d=e[c>>>2&255]+(h+i|0)|0,g[2|b]=i=e[d>>>10&255]+c|0,h^=h>>>16,h=e[b+131&255]+h|0,c=e[3|b],e[3|b]=d=e[c>>>2&255]+(h+i|0)|0,g[3|b]=i=e[d>>>10&255]+c|0}function d(){return k--||(c(1),k=255),g[k]}var e=new Uint32Array(256),g=new Uint32Array(256),h=0,i=0,j=0,k=0;return{seed:b,prng:c,rand:d}}(),qb=b.console,rb=b.Date.now,sb=b.Math.random,tb=b.performance,ub=b.crypto||b.msCrypto;void 0!==ub&&(nb=ub.getRandomValues);var vb,wb=pb.rand,xb=pb.seed,yb=0,zb=!1,Ab=!1,Bb=0,Cb=256,Db=!1,Eb=!1,Fb={};if(void 0!==tb)vb=function(){return 1e3*tb.now()|0};else{var Gb=1e3*rb()|0;vb=function(){return 1e3*rb()-Gb|0}}a.random=ua,a.random.seed=sa,Object.defineProperty(ua,"allowWeak",{get:function(){return Db},set:function(a){Db=a}}),Object.defineProperty(ua,"skipSystemRNGWarning",{get:function(){return Eb},set:function(a){Eb=a}}),a.getRandomValues=ta,a.getRandomValues.seed=sa,Object.defineProperty(ta,"allowWeak",{get:function(){return Db},set:function(a){Db=a}}),Object.defineProperty(ta,"skipSystemRNGWarning",{get:function(){return Eb},set:function(a){Eb=a}}),b.Math.random=ua,void 0===b.crypto&&(b.crypto={}),b.crypto.getRandomValues=ta;var Hb,Ib={Uint32Array:Uint32Array,Math:b.Math},Jb=new Uint32Array(1048576);void 0===Ib.Math.imul?(Ib.Math.imul=xa,Hb=va(Ib,null,Jb.buffer),delete Ib.Math.imul):Hb=va(Ib,null,Jb.buffer);var Kb=new Uint32Array(0),Lb=ya.prototype=new Number;Lb.toString=za,Lb.toBytes=Aa,Lb.valueOf=Ba,Lb.clamp=Ca,Lb.slice=Da,Lb.negate=Ea,Lb.compare=Fa,Lb.add=Ga,Lb.subtract=Ha,Lb.multiply=Ia,Lb.square=Ja,Lb.divide=Ka;var Mb=new ya(0),Nb=new ya(1);Object.freeze(Mb),Object.freeze(Nb);var Ob=Na.prototype=new ya;Ob.reduce=Oa,Ob.inverse=Pa,Ob.power=Qa;var Pb=[2,3];return Lb.isProbablePrime=Ta,ya.randomProbablePrime=Va,ya.ZERO=Mb,ya.ONE=Nb,ya.extGCD=Ma,a.BigNumber=ya,a.Modulus=Na,"function"==typeof define&&define.amd?define([],function(){return a}):"object"==typeof module&&module.exports?module.exports=a:b.asmCrypto=a,a}({},this);
diff --git a/assets/dl.svg b/assets/dl.svg
new file mode 100644
index 00000000..fc57a36a
--- /dev/null
+++ b/assets/dl.svg
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/assets/error.svg b/assets/error.svg
new file mode 100644
index 00000000..0d478816
--- /dev/null
+++ b/assets/error.svg
@@ -0,0 +1,258 @@
+
+
diff --git a/assets/eye-off.svg b/assets/eye-off.svg
new file mode 100644
index 00000000..6edb1745
--- /dev/null
+++ b/assets/eye-off.svg
@@ -0,0 +1 @@
+
diff --git a/assets/eye.svg b/assets/eye.svg
new file mode 100644
index 00000000..059dd2e9
--- /dev/null
+++ b/assets/eye.svg
@@ -0,0 +1 @@
+
diff --git a/assets/favicon-120.png b/assets/favicon-120.png
deleted file mode 100644
index 1fa60cd8..00000000
Binary files a/assets/favicon-120.png and /dev/null differ
diff --git a/assets/favicon-128.png b/assets/favicon-128.png
deleted file mode 100644
index 45f37fce..00000000
Binary files a/assets/favicon-128.png and /dev/null differ
diff --git a/assets/favicon-144.png b/assets/favicon-144.png
deleted file mode 100644
index 5f822567..00000000
Binary files a/assets/favicon-144.png and /dev/null differ
diff --git a/assets/favicon-152.png b/assets/favicon-152.png
deleted file mode 100644
index afa2e978..00000000
Binary files a/assets/favicon-152.png and /dev/null differ
diff --git a/assets/favicon-167.png b/assets/favicon-167.png
deleted file mode 100644
index d39f8a0c..00000000
Binary files a/assets/favicon-167.png and /dev/null differ
diff --git a/assets/favicon-16x16.png b/assets/favicon-16x16.png
new file mode 100644
index 00000000..1926e1bf
Binary files /dev/null and b/assets/favicon-16x16.png differ
diff --git a/assets/favicon-180.png b/assets/favicon-180.png
deleted file mode 100644
index 67a23392..00000000
Binary files a/assets/favicon-180.png and /dev/null differ
diff --git a/assets/favicon-195.png b/assets/favicon-195.png
deleted file mode 100644
index bb763ea5..00000000
Binary files a/assets/favicon-195.png and /dev/null differ
diff --git a/assets/favicon-196.png b/assets/favicon-196.png
deleted file mode 100644
index 8ca0f0b9..00000000
Binary files a/assets/favicon-196.png and /dev/null differ
diff --git a/assets/favicon-228.png b/assets/favicon-228.png
deleted file mode 100644
index 3df9e05c..00000000
Binary files a/assets/favicon-228.png and /dev/null differ
diff --git a/assets/favicon-32.png b/assets/favicon-32.png
deleted file mode 100644
index 045edd37..00000000
Binary files a/assets/favicon-32.png and /dev/null differ
diff --git a/assets/favicon-32x32.png b/assets/favicon-32x32.png
new file mode 100644
index 00000000..f00f4ee7
Binary files /dev/null and b/assets/favicon-32x32.png differ
diff --git a/assets/favicon-96.png b/assets/favicon-96.png
deleted file mode 100644
index ab851902..00000000
Binary files a/assets/favicon-96.png and /dev/null differ
diff --git a/assets/github-icon.svg b/assets/github-icon.svg
deleted file mode 100644
index 44e074c8..00000000
--- a/assets/github-icon.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/icon-64x64.png b/assets/icon-64x64.png
new file mode 100644
index 00000000..e3a6b2a7
Binary files /dev/null and b/assets/icon-64x64.png differ
diff --git a/assets/icon.svg b/assets/icon.svg
new file mode 100644
index 00000000..e4bfb3d1
--- /dev/null
+++ b/assets/icon.svg
@@ -0,0 +1,34 @@
+
+
diff --git a/assets/illustration_download.svg b/assets/illustration_download.svg
deleted file mode 100644
index 9425fc28..00000000
--- a/assets/illustration_download.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/illustration_error.svg b/assets/illustration_error.svg
deleted file mode 100644
index cba8d2e0..00000000
--- a/assets/illustration_error.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/illustration_expired.svg b/assets/illustration_expired.svg
deleted file mode 100644
index 1d569f08..00000000
--- a/assets/illustration_expired.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/lock.svg b/assets/lock.svg
new file mode 100644
index 00000000..d4e16c96
--- /dev/null
+++ b/assets/lock.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/assets/main.css b/assets/main.css
deleted file mode 100644
index 0ff37f08..00000000
--- a/assets/main.css
+++ /dev/null
@@ -1,1096 +0,0 @@
-/*** index.html ***/
-html {
- background: url('./send_bg.svg');
- font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'segoe ui',
- 'helvetica neue', helvetica, ubuntu, roboto, noto, arial, sans-serif;
- font-weight: 200;
- background-size: 110%;
- background-repeat: no-repeat;
- background-position: center top;
- height: 100%;
- margin: auto;
-}
-
-body {
- font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'segoe ui',
- 'helvetica neue', helvetica, ubuntu, roboto, noto, arial, sans-serif;
- display: flex;
- flex-direction: column;
- margin: 0;
- min-height: 100vh;
- position: relative;
-}
-
-#progress circle {
- stroke: #eee;
- stroke-width: 0.75em;
-}
-
-#progress #bar {
- transition: stroke-dashoffset 300ms linear;
- stroke: #3b9dff;
-}
-
-.header {
- align-items: flex-start;
- box-sizing: border-box;
- display: flex;
- justify-content: space-between;
- padding: 31px;
- width: 100%;
-}
-
-.send-logo {
- display: flex;
- position: relative;
- align-items: center;
-}
-
-.send-logo h1 {
- transition: color 50ms;
-}
-
-.send-logo h1:hover {
- color: #0297f8;
-}
-
-.send-logo > a {
- display: flex;
- flex-direction: row;
-}
-
-.site-title {
- color: #3e3d40;
- font-size: 32px;
- font-weight: 500;
- margin: 0;
- position: relative;
- top: -1px;
- letter-spacing: 1px;
- margin-left: 8px;
-}
-
-.site-subtitle {
- color: #3e3d40;
- font-size: 12px;
- margin: 0 8px;
-}
-
-.site-subtitle a {
- font-weight: bold;
- color: #3e3d40;
- transition: color 50ms;
-}
-
-.site-subtitle a:hover {
- color: #0297f8;
-}
-
-.feedback {
- background-color: #0297f8;
- background-image: url('./feedback.svg');
- background-position: 2px 4px;
- background-repeat: no-repeat;
- background-size: 18px;
- border-radius: 3px;
- border: 1px solid #0297f8;
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
- color: #fff;
- cursor: pointer;
- display: block;
- float: right;
- font-size: 12px;
- line-height: 12px;
- opacity: 0.9;
- padding: 5px;
- overflow: hidden;
- min-width: 12px;
- max-width: 12px;
- text-indent: 17px;
- transition: all 250ms ease-in-out;
- white-space: nowrap;
-}
-
-.feedback:hover,
-.feedback:focus {
- min-width: 30px;
- max-width: 300px;
- text-indent: 2px;
- padding: 5px 5px 5px 20px;
- background-color: #0287e8;
-}
-
-.feedback:active {
- background-color: #0277d8;
-}
-
-.all {
- flex: 1;
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
- max-width: 650px;
- margin: 0 auto;
- padding: 0 20px;
- box-sizing: border-box;
- width: 96%;
-}
-
-pre,
-input,
-select,
-textarea,
-button {
- font-family: inherit;
- margin: 0;
-}
-
-pre {
- font-weight: 600;
- display: inline-block;
-}
-
-a {
- text-decoration: none;
-}
-
-.btn {
- font-weight: 500;
-}
-
-/** page-one **/
-
-.fadeOut {
- opacity: 0;
- animation: fadeout 200ms linear;
-}
-
-@keyframes fadeout {
- 0% {
- opacity: 1;
- }
-
- 100% {
- opacity: 0;
- }
-}
-
-.fadeIn {
- opacity: 1;
- animation: fadein 200ms linear;
-}
-
-@keyframes fadein {
- 0% {
- opacity: 0;
- }
-
- 100% {
- opacity: 1;
- }
-}
-
-.title {
- font-size: 33px;
- line-height: 40px;
- margin: 20px auto;
- text-align: center;
- max-width: 520px;
- font-family: 'SF Pro Text', sans-serif;
- word-wrap: break-word;
-}
-
-.description {
- font-size: 15px;
- line-height: 23px;
- max-width: 630px;
- text-align: center;
- margin: 0 auto 60px;
- color: #0c0c0d;
- width: 92%;
-}
-
-.upload-window {
- border: 3px dashed rgba(0, 148, 251, 0.5);
- margin: 0 auto 10px;
- height: 255px;
- border-radius: 4px;
- display: flex;
- justify-content: center;
- align-items: center;
- flex-direction: column;
- text-align: center;
- transition: transform 150ms;
- padding: 15px;
-}
-
-.upload-window.ondrag {
- border: 5px dashed rgba(0, 148, 251, 0.5);
- height: 251px;
- transform: scale(1.04);
- border-radius: 4.2px;
- display: flex;
- justify-content: center;
- align-items: center;
- flex-direction: column;
- text-align: center;
-}
-
-.link {
- color: #0094fb;
- text-decoration: none;
-}
-
-.link:hover {
- color: #0287e8;
-}
-
-#upload-text {
- font-size: 22px;
- color: #737373;
- margin: 20px 0 10px;
- font-family: 'SF Pro Text', sans-serif;
-}
-
-.browse {
- background: #0297f8;
- border-radius: 5px;
- font-size: 20px;
- color: #fff;
- min-width: 240px;
- height: 60px;
- display: flex;
- justify-content: center;
- align-items: center;
- cursor: pointer;
- padding: 0 10px;
-}
-
-.browse:hover {
- background-color: #0287e8;
-}
-
-input[type='file'] {
- opacity: 0;
- overflow: hidden;
- position: absolute;
- z-index: -1;
-}
-
-input[type='file'].has-focus + #browse,
-input[type='file']:focus + #browse {
- background-color: #0287e8;
- outline: 1px dotted #000;
- outline: -webkit-focus-ring-color auto 5px;
-}
-
-#file-size-msg {
- font-size: 12px;
- line-height: 16px;
- color: #737373;
- margin-bottom: 22px;
-}
-
-/** file-list **/
-th {
- font-size: 16px;
- color: #858585;
- font-weight: lighter;
- text-align: left;
- background: rgba(0, 148, 251, 0.05);
- height: 40px;
- border-top: 1px solid rgba(0, 148, 251, 0.1);
- padding: 0 19px;
- white-space: nowrap;
-}
-
-td {
- font-size: 15px;
- vertical-align: top;
- color: #4a4a4a;
- padding: 17px 19px 0;
- line-height: 23px;
- position: relative;
-}
-
-table {
- border-collapse: collapse;
- font-family: 'Segoe UI', 'SF Pro Text', sans-serif;
-}
-
-tbody {
- word-wrap: break-word;
- word-break: break-all;
-}
-
-#uploaded-files {
- margin: 45.3px auto;
- table-layout: fixed;
-}
-
-#uploaded-file {
- width: 35%;
-}
-
-#copy-file-list {
- width: 25%;
-}
-
-#expiry-file-list {
- width: 21%;
-}
-
-#delete-file-list {
- width: 12%;
-}
-
-.overflow-col {
- text-overflow: ellipsis;
- max-width: 0;
- overflow: hidden;
- white-space: nowrap;
-}
-
-.center-col {
- text-align: center;
-}
-
-.icon-delete,
-.icon-copy,
-.icon-check {
- cursor: pointer;
-}
-
-.icon-copy[disabled='disabled'] {
- pointer-events: none;
- opacity: 0.3;
-}
-
-.text-copied {
- color: #0a8dff;
-}
-
-/* Popup container */
-.popup {
- position: absolute;
- display: inline-block;
-}
-
-/* The actual popup (appears on top) */
-.popup .popuptext {
- visibility: hidden;
- min-width: 204px;
- min-height: 105px;
- background-color: #fff;
- color: #000;
- border: 1px solid #d7d7db;
- padding: 15px 24px;
- box-sizing: content-box;
- text-align: center;
- border-radius: 5px;
- position: absolute;
- z-index: 1;
- bottom: 20px;
- left: -40px;
- transition: opacity 0.5s;
- opacity: 0;
- outline: 0;
- box-shadow: 3px 3px 7px rgba(136, 136, 136, 0.3);
-}
-
-/* Popup arrow */
-.popup .popuptext::after {
- content: '';
- position: absolute;
- bottom: -11px;
- left: 20px;
- background-color: #fff;
- display: block;
- width: 20px;
- height: 20px;
- transform: rotate(45deg);
- border-radius: 0 0 5px;
- border-right: 1px solid #d7d7db;
- border-bottom: 1px solid #d7d7db;
-}
-
-.popup .show {
- visibility: visible;
- opacity: 1;
-}
-
-.popup-message {
- height: 40px;
- display: flex;
- justify-content: center;
- align-items: center;
- border-bottom: 1px #ebebeb solid;
- color: #0c0c0d;
- font-size: 15px;
- font-weight: normal;
- padding-bottom: 15px;
- white-space: nowrap;
- width: calc(100% + 48px);
- margin-left: -24px;
-}
-
-.popup-action {
- margin-top: 15px;
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: space-between;
-}
-
-.popup-yes {
- color: #fff;
- background-color: #0297f8;
- border-radius: 5px;
- padding: 5px 25px;
- font-weight: normal;
- cursor: pointer;
- min-width: 94px;
- box-sizing: border-box;
- white-space: nowrap;
- margin-left: 12px;
-}
-
-.popup-yes:hover {
- background-color: #0287e8;
-}
-
-.popup-no {
- color: #4a4a4a;
- background-color: #fbfbfb;
- border: 1px #c1c1c1 solid;
- border-radius: 5px;
- padding: 5px 25px;
- font-weight: normal;
- min-width: 94px;
- box-sizing: border-box;
- cursor: pointer;
- white-space: nowrap;
-}
-
-.popup-no:hover {
- background-color: #efeff1;
-}
-
-/** upload-progress **/
-.progress-bar {
- margin-top: 3px;
- display: flex;
- justify-content: center;
- align-items: center;
- text-align: center;
- position: relative;
-}
-
-.percentage {
- letter-spacing: -0.78px;
- font-family: 'Segoe UI', 'SF Pro Text', sans-serif;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
-
-.percent-number {
- font-size: 43.2px;
- line-height: 58px;
-}
-
-.percent-sign {
- font-size: 28.8px;
- stroke: none;
- fill: #686868;
-}
-
-.upload {
- margin: 0 auto;
- display: flex;
- justify-content: center;
- align-items: center;
- flex-direction: column;
- text-align: center;
- font-size: 15px;
-}
-
-.progress-text {
- color: rgba(0, 0, 0, 0.5);
- letter-spacing: -0.4px;
- margin-top: 24px;
- margin-bottom: 74px;
-}
-
-#cancel-upload {
- color: #d70022;
- background: #fff;
- font-size: 15px;
- border: 0;
- cursor: pointer;
- text-decoration: underline;
-}
-
-#cancel-upload:disabled {
- text-decoration: none;
- cursor: auto;
-}
-
-/** share-link **/
-#share-window {
- margin: 0 auto;
- display: flex;
- justify-content: center;
- align-items: center;
- flex-direction: column;
- width: 100%;
- max-width: 640px;
-}
-
-#share-window-r > div {
- font-size: 12px;
- padding-bottom: 10px;
-}
-
-#copy {
- display: flex;
- flex-wrap: nowrap;
- width: 100%;
-}
-
-#copy.wait-password #link,
-#copy.wait-password #copy-btn {
- opacity: 0.5;
-}
-
-#copy-text {
- align-self: flex-start;
- margin-top: 60px;
- margin-bottom: 10px;
- color: #0c0c0d;
- max-width: 614px;
- word-wrap: break-word;
-}
-
-#link {
- flex: 1;
- height: 56px;
- border: 1px solid #0297f8;
- border-radius: 6px 0 0 6px;
- font-size: 20px;
- color: #737373;
- font-family: 'SF Pro Text', sans-serif;
- letter-spacing: 0;
- line-height: 23px;
- font-weight: 300;
- padding-left: 10px;
- padding-right: 10px;
-}
-
-#link:disabled {
- border: 1px solid #05a700;
- background: #fff;
-}
-
-#copy-btn {
- flex: 0 1 165px;
- background: #0297f8;
- border-radius: 0 6px 6px 0;
- border: 1px solid #0297f8;
- color: white;
- cursor: pointer;
- font-size: 15px;
- padding-left: 10px;
- padding-right: 10px;
- white-space: nowrap;
-}
-
-#copy-btn:not(:disabled):hover {
- background-color: #0287e8;
-}
-
-#copy-btn.success {
- background: #05a700;
- border: 1px solid #05a700;
-}
-
-#copy-btn:disabled {
- cursor: auto;
-}
-
-#delete-file {
- width: 176px;
- height: 44px;
- background: #fff;
- border: 1px solid rgba(12, 12, 13, 0.3);
- border-radius: 5px;
- font-size: 15px;
- margin-top: 50px;
- margin-bottom: 12px;
- cursor: pointer;
- color: #313131;
-}
-
-#delete-file:hover {
- background: #efeff1;
-}
-
-.send-new {
- font-size: 15px;
- margin: auto;
- text-align: center;
- color: #0094fb;
- cursor: pointer;
- text-decoration: underline;
-}
-
-.send-new:hover,
-.send-new:focus,
-.send-new:active {
- color: #0287e8;
-}
-
-.hidden {
- visibility: hidden;
-}
-
-.selectPassword {
- padding: 10px 0;
- align-self: left;
- max-width: 100%;
- overflow-wrap: break-word;
-}
-
-.setPassword {
- align-self: left;
- display: flex;
- flex-wrap: nowrap;
- width: 80%;
- padding: 10px 5px;
-}
-
-/* upload-error */
-#upload-error {
- display: flex;
- justify-content: center;
- align-items: center;
- flex-direction: column;
- text-align: center;
-}
-
-#upload-error[hidden],
-#unsupported-browser[hidden] {
- display: none;
-}
-
-#upload-error-img {
- margin: 51px 0 71px;
-}
-
-/* unsupported-browser */
-#unsupported-browser {
- display: flex;
- justify-content: center;
- align-items: center;
- flex-direction: column;
-}
-
-.unsupported-description {
- font-size: 13px;
- line-height: 23px;
- text-align: center;
- color: #7d7d7d;
- margin: 0 auto 23px;
-}
-
-.firefox-logo {
- width: 70px;
-}
-
-.firefox-logo-small {
- width: 24px;
-}
-
-#dl-firefox,
-#update-firefox {
- margin-bottom: 181px;
- height: 80px;
- background: #98e02b;
- border-radius: 3px;
- cursor: pointer;
- border: 0;
- box-shadow: 0 5px 3px rgb(234, 234, 234);
- font-family: 'Fira Sans';
- font-weight: 500;
- color: #fff;
- font-size: 26px;
- display: flex;
- justify-content: center;
- align-items: center;
- line-height: 1;
- padding: 0 25px;
-}
-
-.unsupported-button-text {
- text-align: left;
- margin-left: 20.4px;
-}
-
-.unsupported-button-text > span {
- font-family: 'Fira Sans';
- font-weight: 300;
- font-size: 18px;
- letter-spacing: -0.69px;
-}
-
-/** download.html **/
-#download-btn {
- font-size: 15px;
- color: white;
- width: 180px;
- height: 44px;
- margin-top: 20px;
- margin-bottom: 30px;
- text-align: center;
- background: #0297f8;
- border: 1px solid #0297f8;
- border-radius: 5px;
- cursor: pointer;
-}
-
-#download-btn:hover {
- background-color: #0287e8;
-}
-
-#download-btn:disabled {
- background: #47b04b;
- cursor: auto;
-}
-
-#download {
- margin: 0 auto 30px;
- display: flex;
- justify-content: center;
- align-items: center;
- flex-direction: column;
- text-align: center;
-}
-
-#expired-img {
- margin: 51px 0 71px;
-}
-
-.expired-description {
- font-size: 15px;
- line-height: 23px;
- text-align: center;
- color: #7d7d7d;
- margin: 0 auto 23px;
-}
-
-#download-progress {
- width: 590px;
-}
-
-#download-progress[hidden] {
- display: none;
-}
-
-#download-img {
- width: 283px;
- height: 196px;
-}
-
-.enterPassword {
- text-align: left;
- padding: 40px;
-}
-
-.red {
- color: red;
-}
-
-#unlock {
- display: flex;
- flex-wrap: nowrap;
- width: 100%;
- padding: 10px 0;
-}
-
-.unlock-input {
- flex: 1;
- height: 46px;
- border: 1px solid #0297f8;
- border-radius: 6px 0 0 6px;
- font-size: 20px;
- color: #737373;
- font-family: 'SF Pro Text', sans-serif;
- letter-spacing: 0;
- line-height: 23px;
- font-weight: 300;
- padding-left: 10px;
- padding-right: 10px;
-}
-
-#unlock-btn {
- flex: 0 1 165px;
- background: #0297f8;
- border-radius: 0 6px 6px 0;
- border: 1px solid #0297f8;
- color: white;
- cursor: pointer;
- font-size: 15px;
- padding-left: 10px;
- padding-right: 10px;
- white-space: nowrap;
-}
-
-#unlock-btn:hover {
- background-color: #0287e8;
-}
-
-.btn-hidden {
- visibility: hidden;
-}
-
-.input-no-btn {
- border-radius: 6px;
-}
-
-/* footer */
-.footer {
- right: 0;
- bottom: 0;
- left: 0;
- font-size: 13px;
- display: flex;
- align-items: flex-end;
- flex-direction: row;
- justify-content: space-between;
- padding: 50px 31px 41px;
- width: 100%;
- box-sizing: border-box;
-}
-
-.mozilla-logo {
- width: 112px;
- height: 32px;
- margin-bottom: -5px;
-}
-
-.legal-links {
- max-width: 81vw;
- display: flex;
- align-items: center;
- flex-direction: row;
-}
-
-.legal-links > a {
- color: #858585;
- opacity: 0.9;
- white-space: nowrap;
- margin-right: 2vw;
-}
-
-.legal-links > a:hover {
- opacity: 1;
-}
-
-.legal-links > a:visited {
- color: #858585;
-}
-
-.social-links {
- display: flex;
- justify-content: space-between;
- width: 94px;
-}
-
-.social-links > a {
- opacity: 0.9;
-}
-
-.social-links > a:hover {
- opacity: 1;
-}
-
-.github,
-.twitter {
- width: 32px;
- height: 32px;
- margin-bottom: -5px;
-}
-
-#addPasswordWrapper {
- min-height: 24px;
-}
-
-#addPassword {
- position: absolute;
- visibility: collapse;
-}
-
-#addPasswordWrapper label {
- line-height: 20px;
- cursor: pointer;
- position: relative;
- opacity: 0.6;
-}
-
-#addPassword:checked + label {
- opacity: 1;
-}
-
-#addPasswordWrapper label::before {
- content: '';
- height: 20px;
- width: 20px;
- margin-right: 10px;
- margin-left: 5px;
- float: left;
- border: 1px solid rgba(12, 12, 13, 0.3);
- border-radius: 2px;
-}
-
-#addPassword:checked + label::before {
- background-image: url('./check-16-blue.svg');
- background-position: 2px 1px;
-}
-
-.banner {
- padding: 0 15px;
- height: 48px;
- background-color: #efeff1;
- color: #4a4a4f;
- font-size: 13px;
- display: flex;
- flex-direction: row;
- align-content: center;
- align-items: center;
- justify-content: center;
-}
-
-.banner > div {
- display: flex;
- align-items: center;
- margin: 0 auto;
-}
-
-.banner > div > span {
- margin-left: 10px;
-}
-
-@media (max-device-width: 992px), (max-width: 992px) {
- .popup .popuptext {
- left: auto;
- right: -40px;
- }
-
- .popup .popuptext::after {
- left: auto;
- right: 36px;
- }
-}
-
-@media (max-device-width: 768px), (max-width: 768px) {
- .description {
- margin: 0 auto 25px;
- }
-
- #copy {
- width: 100%;
- }
-
- #link {
- font-size: 18px;
- }
-
- .footer {
- flex-direction: column;
- justify-content: flex-start;
- align-items: flex-start;
- max-width: 630px;
- margin: auto;
- }
-
- .mozilla-logo {
- margin-left: -7px;
- }
-
- .legal-links {
- flex-direction: column;
- margin: auto;
- width: 100%;
- max-width: 100%;
- }
-
- .legal-links > * {
- display: block;
- padding: 10px 0;
- align-self: flex-start;
- }
-
- .social-links {
- margin-top: 20px;
- align-self: flex-start;
- }
-}
-
-@media (max-device-width: 520px), (max-width: 520px) {
- .header {
- flex-direction: column;
- justify-content: flex-start;
- }
-
- .feedback {
- margin-top: 10px;
- min-width: 30px;
- max-width: 300px;
- text-indent: 2px;
- padding: 5px 5px 5px 20px;
- }
-
- #copy,
- .setPassword,
- #unlock {
- width: 100%;
- flex-direction: column;
- padding-left: 0;
- }
-
- .selectPassword {
- align-self: center;
- min-width: 95%;
- }
-
- #addPasswordWrapper label::before {
- margin-left: 0;
- }
-
- #link,
- #unlock-input {
- font-size: 22px;
- padding: 15px 10px;
- border-radius: 6px 6px 0 0;
- }
-
- #copy-btn,
- #unlock-btn {
- border-radius: 0 0 6px 6px;
- flex: 0 1 65px;
- }
-
- #copy-text {
- text-align: center;
- }
-
- th {
- font-size: 14px;
- padding: 0 5px;
- }
-
- td {
- font-size: 13px;
- padding: 17px 5px 0;
- }
-}
diff --git a/assets/master-logo.svg b/assets/master-logo.svg
new file mode 100644
index 00000000..ec8e6f8f
--- /dev/null
+++ b/assets/master-logo.svg
@@ -0,0 +1,99 @@
+
diff --git a/assets/mozilla-logo.svg b/assets/mozilla-logo.svg
deleted file mode 100644
index 3ea2e868..00000000
--- a/assets/mozilla-logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/notFound.svg b/assets/notFound.svg
new file mode 100644
index 00000000..24e04000
--- /dev/null
+++ b/assets/notFound.svg
@@ -0,0 +1,274 @@
+
+
diff --git a/assets/safari-pinned-tab.svg b/assets/safari-pinned-tab.svg
new file mode 100644
index 00000000..d1fec797
--- /dev/null
+++ b/assets/safari-pinned-tab.svg
@@ -0,0 +1,34 @@
+
+
diff --git a/assets/select-arrow.svg b/assets/select-arrow.svg
new file mode 100644
index 00000000..d43db4d0
--- /dev/null
+++ b/assets/select-arrow.svg
@@ -0,0 +1,19 @@
+
+
\ No newline at end of file
diff --git a/assets/send-fb.jpg b/assets/send-fb.jpg
index 784fe51f..a9dcbdbf 100644
Binary files a/assets/send-fb.jpg and b/assets/send-fb.jpg differ
diff --git a/assets/send-twitter.jpg b/assets/send-twitter.jpg
index d13d5f90..ae30ad54 100644
Binary files a/assets/send-twitter.jpg and b/assets/send-twitter.jpg differ
diff --git a/assets/send_bg.svg b/assets/send_bg.svg
deleted file mode 100644
index a4557acc..00000000
--- a/assets/send_bg.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/send_logo.svg b/assets/send_logo.svg
deleted file mode 100644
index b13d138f..00000000
--- a/assets/send_logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/share-24.svg b/assets/share-24.svg
new file mode 100644
index 00000000..f85064c9
--- /dev/null
+++ b/assets/share-24.svg
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/assets/thunderbird-icon.svg b/assets/thunderbird-icon.svg
new file mode 100644
index 00000000..4f555086
--- /dev/null
+++ b/assets/thunderbird-icon.svg
@@ -0,0 +1,65 @@
+
\ No newline at end of file
diff --git a/assets/twitter-icon.svg b/assets/twitter-icon.svg
deleted file mode 100644
index 8816009a..00000000
--- a/assets/twitter-icon.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/upload.svg b/assets/upload.svg
deleted file mode 100644
index f98e6b2e..00000000
--- a/assets/upload.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/user.svg b/assets/user.svg
new file mode 100644
index 00000000..3d052d07
--- /dev/null
+++ b/assets/user.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/wordmark.svg b/assets/wordmark.svg
new file mode 100644
index 00000000..bc4841c6
--- /dev/null
+++ b/assets/wordmark.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets_src/apple-touch-icon.xcf b/assets_src/apple-touch-icon.xcf
new file mode 100644
index 00000000..71584e92
Binary files /dev/null and b/assets_src/apple-touch-icon.xcf differ
diff --git a/assets_src/completed.svg b/assets_src/completed.svg
new file mode 100644
index 00000000..7d4736cb
--- /dev/null
+++ b/assets_src/completed.svg
@@ -0,0 +1,284 @@
+
+
diff --git a/assets_src/error.svg b/assets_src/error.svg
new file mode 100644
index 00000000..cea8a7ac
--- /dev/null
+++ b/assets_src/error.svg
@@ -0,0 +1,284 @@
+
+
diff --git a/assets_src/icon.svg b/assets_src/icon.svg
new file mode 100644
index 00000000..efa7b48e
--- /dev/null
+++ b/assets_src/icon.svg
@@ -0,0 +1,76 @@
+
+
diff --git a/assets_src/notFound.svg b/assets_src/notFound.svg
new file mode 100644
index 00000000..ce74ddb8
--- /dev/null
+++ b/assets_src/notFound.svg
@@ -0,0 +1,300 @@
+
+
diff --git a/assets_src/safari-pinned-tab.svg b/assets_src/safari-pinned-tab.svg
new file mode 100644
index 00000000..4f7a6f54
--- /dev/null
+++ b/assets_src/safari-pinned-tab.svg
@@ -0,0 +1,76 @@
+
+
diff --git a/assets_src/send-header.xcf b/assets_src/send-header.xcf
new file mode 100644
index 00000000..bff45898
Binary files /dev/null and b/assets_src/send-header.xcf differ
diff --git a/browserconfig.xml b/browserconfig.xml
deleted file mode 100644
index a1ff7e28..00000000
--- a/browserconfig.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
- #0297F8
-
-
-
\ No newline at end of file
diff --git a/browserslist b/browserslist
index d4ca6a4f..6ba761ca 100644
--- a/browserslist
+++ b/browserslist
@@ -1,5 +1,6 @@
last 2 chrome versions
last 2 firefox versions
+last 2 safari versions
+last 2 edge versions
+edge 18
firefox esr
-ie >= 9
-safari >= 9
diff --git a/build/android_index_plugin.js b/build/android_index_plugin.js
new file mode 100644
index 00000000..7f37d608
--- /dev/null
+++ b/build/android_index_plugin.js
@@ -0,0 +1,50 @@
+const path = require('path');
+const html = require('choo/html');
+const NAME = 'AndroidIndexPlugin';
+
+function chunkFileNames(compilation) {
+ const names = {};
+ for (const chunk of compilation.chunks) {
+ for (const file of chunk.files) {
+ if (!/\.map$/.test(file)) {
+ names[`${chunk.name}${path.extname(file)}`] = file;
+ }
+ }
+ }
+ return names;
+}
+class AndroidIndexPlugin {
+ apply(compiler) {
+ compiler.hooks.emit.tap(NAME, compilation => {
+ const files = chunkFileNames(compilation);
+ const page = html`
+
+
+ Send
+
+
+
+
+
+
+
+
+ `
+ .toString()
+ .replace(/\n\s{6}/g, '\n');
+ compilation.assets['android.html'] = {
+ source() {
+ return page;
+ },
+ size() {
+ return page.length;
+ }
+ };
+ });
+ }
+}
+
+module.exports = AndroidIndexPlugin;
diff --git a/build/fluent_loader.js b/build/fluent_loader.js
deleted file mode 100644
index 8ba41928..00000000
--- a/build/fluent_loader.js
+++ /dev/null
@@ -1,38 +0,0 @@
-const { MessageContext } = require('fluent');
-
-function toJSON(map) {
- return JSON.stringify(Array.from(map));
-}
-
-module.exports = function(source) {
- const localeExp = this.options.locale || /([^/]+)\/[^/]+\.ftl$/;
- const result = localeExp.exec(this.resourcePath);
- const locale = result && result[1];
- // pre-parse the ftl
- const context = new MessageContext(locale);
- context.addMessages(source);
- if (!locale) {
- throw new Error(`couldn't find locale in: ${this.resourcePath}`);
- }
- return `
-module.exports = \`
-if (typeof window === 'undefined') {
- var fluent = require('fluent');
-}
-var ctx = new fluent.MessageContext('${locale}', {useIsolating: false});
-ctx._messages = new Map(${toJSON(context._messages)});
-function translate(id, data) {
- var msg = ctx.getMessage(id);
- if (typeof(msg) !== 'string' && !msg.val && msg.attrs) {
- msg = msg.attrs.title || msg.attrs.alt
- }
- return ctx.format(msg, data);
-}
-if (typeof window === 'undefined') {
- module.exports = translate;
-}
-else {
- window.translate = translate;
-}
-\``;
-};
diff --git a/build/generate_l10n_map.js b/build/generate_l10n_map.js
deleted file mode 100644
index 57f73ab4..00000000
--- a/build/generate_l10n_map.js
+++ /dev/null
@@ -1,22 +0,0 @@
-const fs = require('fs');
-const path = require('path');
-
-function kv(d) {
- return `"${d}": require('../public/locales/${d}/send.ftl')`;
-}
-
-module.exports = function() {
- const dirs = fs.readdirSync(path.join(__dirname, '..', 'public', 'locales'));
- const code = `
- module.exports = {
- translate: function (id, data) { return window.translate(id, data) },
- ${dirs.map(kv).join(',\n')}
- };`;
- return {
- code,
- dependencies: dirs.map(d =>
- require.resolve(`../public/locales/${d}/send.ftl`)
- ),
- cacheable: false
- };
-};
diff --git a/build/package_json_loader.js b/build/package_json_loader.js
deleted file mode 100644
index a03678f5..00000000
--- a/build/package_json_loader.js
+++ /dev/null
@@ -1,11 +0,0 @@
-const commit = require('git-rev-sync').short();
-
-module.exports = function(source) {
- const pkg = JSON.parse(source);
- const version = {
- commit,
- source: pkg.homepage,
- version: process.env.CIRCLE_TAG || `v${pkg.version}`
- };
- return `module.exports = '${JSON.stringify(version)}'`;
-};
diff --git a/build/readme.md b/build/readme.md
new file mode 100644
index 00000000..b8fc18d7
--- /dev/null
+++ b/build/readme.md
@@ -0,0 +1,14 @@
+# Custom Loaders
+
+## Android Index Plugin
+
+Generates the `index.html` page for the native android client
+
+## Version Plugin
+
+Creates a `version.json` file that gets exposed by the `/__version__` route from the `package.json` file and current git commit hash.
+
+# See Also
+
+- [docs/build.md](../docs/build.md)
+- [webpack.config.js](../webpack.config.js)
\ No newline at end of file
diff --git a/build/version_plugin.js b/build/version_plugin.js
new file mode 100644
index 00000000..af351a3a
--- /dev/null
+++ b/build/version_plugin.js
@@ -0,0 +1,33 @@
+const gitRevSync = require('git-rev-sync');
+const pkg = require('../package.json');
+
+let commit = 'unknown';
+
+try {
+ commit = gitRevSync.short();
+} catch (e) {
+ console.warn('Error fetching current git commit: ' + e);
+}
+
+const version = JSON.stringify({
+ commit,
+ source: pkg.homepage,
+ version: process.env.CIRCLE_TAG || `v${pkg.version}`
+});
+
+class VersionPlugin {
+ apply(compiler) {
+ compiler.hooks.emit.tap('VersionPlugin', compilation => {
+ compilation.assets['version.json'] = {
+ source() {
+ return version;
+ },
+ size() {
+ return version.length;
+ }
+ };
+ });
+ }
+}
+
+module.exports = VersionPlugin;
diff --git a/circle.yml b/circle.yml
deleted file mode 100644
index af534362..00000000
--- a/circle.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-machine:
- node:
- version: 8
- services:
- - docker
- - redis
- environment:
- PATH: "/home/ubuntu/send/firefox:$PATH"
-
-dependencies:
- pre:
- - npm i -g get-firefox geckodriver nsp
- - get-firefox --platform linux --extract --target /home/ubuntu/send
-
-deployment:
- latest:
- branch: master
- commands:
- - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
- - docker build -t mozilla/send:latest .
- - docker push mozilla/send:latest
- tags:
- tag: /.*/
- owner: mozilla
- commands:
- - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
- - docker build -t mozilla/send:$CIRCLE_TAG .
- - docker push mozilla/send:$CIRCLE_TAG
-
-test:
- override:
- - npm run build
- - npm run lint
- - npm test
- - nsp check
diff --git a/common/assets.js b/common/assets.js
index b1db9acf..f1a4657e 100644
--- a/common/assets.js
+++ b/common/assets.js
@@ -1,6 +1,6 @@
-const genmap = require('../build/generate_asset_map');
+const genmap = require('./generate_asset_map');
const isServer = typeof genmap === 'function';
-const prefix = isServer ? '/' : '';
+let prefix = '';
let manifest = {};
try {
//eslint-disable-next-line node/no-missing-require
@@ -15,15 +15,38 @@ function getAsset(name) {
return prefix + assets[name];
}
+function setPrefix(name) {
+ prefix = name;
+}
+
+function getMatches(match) {
+ return Object.keys(assets)
+ .filter(k => match.test(k))
+ .map(getAsset);
+}
+
const instance = {
+ setPrefix: setPrefix,
get: getAsset,
+ match: getMatches,
setMiddleware: function(middleware) {
+ function getManifest() {
+ return JSON.parse(
+ middleware.fileSystem.readFileSync(
+ middleware.getFilenameFromUrl('/manifest.json')
+ )
+ );
+ }
if (middleware) {
instance.get = function getAssetWithMiddleware(name) {
- const f = middleware.fileSystem.readFileSync(
- middleware.getFilenameFromUrl('/manifest.json')
- );
- return prefix + JSON.parse(f)[name];
+ const m = getManifest();
+ return prefix + m[name];
+ };
+ instance.match = function matchAssetWithMiddleware(match) {
+ const m = getManifest();
+ return Object.keys(m)
+ .filter(k => match.test(k))
+ .map(k => prefix + m[k]);
};
}
}
diff --git a/build/generate_asset_map.js b/common/generate_asset_map.js
similarity index 53%
rename from build/generate_asset_map.js
rename to common/generate_asset_map.js
index d1efebb1..6289e54c 100644
--- a/build/generate_asset_map.js
+++ b/common/generate_asset_map.js
@@ -1,3 +1,14 @@
+/*
+ This code is included by both the server and frontend via
+ common/assets.js
+
+ When included from the server the export will be the function.
+
+ When included from the frontend (via webpack) the export will
+ be an object mapping file names to hashed file names. Example:
+ "send_logo.svg": "send_logo.5fcfdf0e.svg"
+*/
+
const fs = require('fs');
const path = require('path');
@@ -8,12 +19,11 @@ function kv(f) {
module.exports = function() {
const files = fs.readdirSync(path.join(__dirname, '..', 'assets'));
const code = `module.exports = {
- "package.json": require('../package.json'),
${files.map(kv).join(',\n')}
};`;
return {
code,
dependencies: files.map(f => require.resolve('../assets/' + f)),
- cacheable: false
+ cacheable: true
};
};
diff --git a/common/locales.js b/common/locales.js
deleted file mode 100644
index 17eb9c2a..00000000
--- a/common/locales.js
+++ /dev/null
@@ -1,51 +0,0 @@
-const gen = require('../build/generate_l10n_map');
-
-const isServer = typeof gen === 'function';
-const prefix = isServer ? '/' : '';
-let manifest = {};
-try {
- //eslint-disable-next-line node/no-missing-require
- manifest = require('../dist/manifest.json');
-} catch (e) {
- // use middleware
-}
-
-const locales = isServer ? manifest : gen;
-
-function getLocale(name) {
- return prefix + locales[`public/locales/${name}/send.ftl`];
-}
-
-function serverTranslator(name) {
- return require(`../dist/${locales[`public/locales/${name}/send.ftl`]}`);
-}
-
-function browserTranslator() {
- return locales.translate;
-}
-
-const translator = isServer ? serverTranslator : browserTranslator;
-
-const instance = {
- get: getLocale,
- getTranslator: translator,
- setMiddleware: function(middleware) {
- if (middleware) {
- const _eval = require('require-from-string');
- instance.get = function getLocaleWithMiddleware(name) {
- const f = middleware.fileSystem.readFileSync(
- middleware.getFilenameFromUrl('/manifest.json')
- );
- return prefix + JSON.parse(f)[`public/locales/${name}/send.ftl`];
- };
- instance.getTranslator = function(name) {
- const f = middleware.fileSystem.readFileSync(
- middleware.getFilenameFromUrl(instance.get(name))
- );
- return _eval(f.toString());
- };
- }
- }
-};
-
-module.exports = instance;
diff --git a/common/readme.md b/common/readme.md
new file mode 100644
index 00000000..0aaad7ac
--- /dev/null
+++ b/common/readme.md
@@ -0,0 +1,7 @@
+# Common Code
+
+This directory contains code loaded by both the frontend `app` and backend `server`. The code here can be challenging to understand at first because the contexts for the two (three counting the dev server) environments that include them are quite different, but the purpose of these modules are quite simple, to provide mappings from the source assets (`copy-16.png`) to the concrete production assets (`copy-16.db66e0bf.svg`).
+
+## Generate Asset Map
+
+This loader enumerates all the files in `assets/` so that `common/assets.js` can provide mappings from the source filename to the hashed filename used on the site.
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 37fb258e..8ad9fc6f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -8,6 +8,12 @@ services:
- "1443:1443"
environment:
- REDIS_HOST=redis
- - NODE_ENV=production
redis:
image: redis:alpine
+ selenium-firefox:
+ image: b4handjr/selenium-firefox
+ ports:
+ - "${VNC_PORT:-5900}:5900"
+ shm_size: 2g
+ volumes:
+ - .:/code
diff --git a/docs/AWS.md b/docs/AWS.md
new file mode 100644
index 00000000..999dba85
--- /dev/null
+++ b/docs/AWS.md
@@ -0,0 +1,236 @@
+# Deployment to AWS
+
+This document describes how to do a deployment of Send in AWS
+
+## AWS requirements
+
+### Security groups (2)
+
+* ALB:
+ - inbound: allow traffic from anywhere on port 80 and 443
+ - ountbound: allow traffic to the instance security group on port `8080`
+
+* Instance:
+ - inbound: allow SSH from your public IP or a bastion (changing the default SSH port is a good idea)
+ - inbound: allow traffic from the ALB security group on port `8080`
+ - ountbound: allow all traffic to anywhere
+
+### Resources
+
+* An S3 bucket (block all public access)
+
+* A private EC2 instance running Ubuntu `20.04` (you can use the [Amazon EC2 AMI Locator](https://cloud-images.ubuntu.com/locator/ec2/) to find the latest)
+
+ Attach an IAM role to the instance with the following inline policy:
+
+ ```json
+ {
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Action": [
+ "s3:ListAllMyBuckets"
+ ],
+ "Resource": [
+ "*"
+ ],
+ "Effect": "Allow"
+ },
+ {
+ "Action": [
+ "s3:ListBucket",
+ "s3:GetBucketLocation",
+ "s3:ListBucketMultipartUploads"
+ ],
+ "Resource": [
+ "arn:aws:s3:::"
+ ],
+ "Effect": "Allow"
+ },
+ {
+ "Action": [
+ "s3:GetObject",
+ "s3:GetObjectVersion",
+ "s3:ListMultipartUploadParts",
+ "s3:PutObject",
+ "s3:AbortMultipartUpload",
+ "s3:DeleteObject",
+ "s3:DeleteObjectVersion"
+ ],
+ "Resource": [
+ "arn:aws:s3:::/*"
+ ],
+ "Effect": "Allow"
+ }
+ ]
+ }
+ ```
+
+* A public ALB:
+
+ - Create a target group with the instance registered (HTTP on port `8080` and path `/`)
+ - Configure HTTP (port 80) to redirect to HTTPS (port 443)
+ - HTTPS (port 443) using the latest security policy and an ACM certificate like `send.mydomain.com`
+
+* A Route53 public record, alias from `send.mydomain.com` to the ALB
+
+## Software requirements
+
+* Git
+* NodeJS `15.x` LTS
+* Local Redis server
+
+### Prerequisite packages
+
+```bash
+sudo apt update
+sudo apt install -y apt-transport-https ca-certificates curl software-properties-common
+```
+
+### Add repositories
+
+* NodeJS `15.x` LTS (checkout [package.json](../package.json)):
+
+```bash
+curl -fsSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | sudo apt-key add -
+echo 'deb [arch=amd64] https://deb.nodesource.com/node_15.x focal main' | sudo tee /etc/apt/sources.list.d/nodejs.list
+```
+
+* Git (latest)
+
+```bash
+sudo add-apt-repository ppa:git-core/ppa
+```
+
+* Redis (latest)
+
+```bash
+sudo add-apt-repository ppa:redislabs/redis
+```
+
+### Install required packages
+
+```bash
+sudo apt update
+sudo apt install git nodejs redis-server telnet
+```
+
+### Redis server
+
+#### Password (optional)
+
+Generate a strong password:
+
+```bash
+makepasswd --chars=100
+```
+
+Edit Redis configuration file `/etc/redis/redis.conf`:
+
+```bash
+requirepass
+```
+
+_Note: documentation on securing Redis https://redis.io/topics/security_
+
+#### Systemd
+
+Enable and (re)start the Redis server service:
+
+```bash
+sudo systemctl enable redis-server
+sudo systemctl restart redis-server
+sudo systemctl status redis-server
+```
+
+## Website directory
+
+Setup a directory for the data
+
+```
+sudo mkdir -pv /var/www/send
+sudo chown www-data:www-data /var/www/send
+sudo 750 /var/www/send
+```
+
+### NodeJS
+
+Update npm:
+
+```bash
+sudo npm install -g npm
+```
+
+Checkout current NodeJS and npm versions:
+
+```bash
+node --version
+npm --version
+```
+
+Clone repository, install JavaScript packages and compiles the assets:
+
+```bash
+sudo su -l www-data -s /bin/bash
+cd /var/www/send
+git clone https://gitlab.com/timvisee/send.git .
+npm install
+npm run build
+exit
+```
+
+Create the file `/var/www/send/.env` used by Systemd with your environment variables
+(checkout [config.js](../server/config.js) for more configuration environment variables):
+
+```
+BASE_URL='https://send.mydomain.com'
+NODE_ENV='production'
+PORT='8080'
+REDIS_PASSWORD=''
+S3_BUCKET=''
+```
+
+Lower files and folders permissions to user and group `www-data`:
+
+```
+sudo find /var/www/send -type d -exec chmod 750 {} \;
+sudo find /var/www/send -type f -exec chmod 640 {} \;
+sudo find -L /var/www/send/node_modules/.bin/ -exec chmod 750 {} \;
+```
+
+### Systemd
+
+Create the file `/etc/systemd/system/send.service` with `root` user and `644` mode:
+
+```
+[Unit]
+Description=Send
+After=network.target
+Requires=redis-server.service
+Documentation=https://gitlab.com/timvisee/send
+
+[Service]
+Type=simple
+ExecStart=/usr/bin/npm run prod
+EnvironmentFile=/var/www/send/.env
+WorkingDirectory=/var/www/send
+User=www-data
+Group=www-data
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
+```
+
+_Note: could be better tuner to secure the service by restricting system permissions,
+check with `systemd-analyze security send`_
+
+Enable and start the Send service, check logs:
+
+```
+sudo systemctl daemon-reload
+sudo systemctl enable send
+sudo systemctl start send
+sudo systemctl status send
+journalctl -fu send
+```
diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS
index 548da409..8291a567 100644
--- a/docs/CODEOWNERS
+++ b/docs/CODEOWNERS
@@ -1,2 +1,2 @@
-# flod as main contact for string changes
-public/locales/en-US/*.ftl @flodolo
+# timvisee as main contact for string changes
+public/locales/en-US/*.ftl @timvisee
diff --git a/docs/acceptance-mobile.md b/docs/acceptance-mobile.md
new file mode 100644
index 00000000..08d0d9c6
--- /dev/null
+++ b/docs/acceptance-mobile.md
@@ -0,0 +1,78 @@
+# Send V2 UX Mobile Acceptance and Spec Annotations
+
+`Date Created: 8/20/2018`
+
+## Acceptance Criteria
+
+Adapted from [this spreadsheet](https://airtable.com/shrkcBPOLkvNFOrpp)
+
+- [ ] It should look and feel of an Android App
+- [ ] It should look and feel like the Send Web Client
+
+### Main Screen
+- [ ] It should clearly Indicate the name of the product
+- [ ] If user has no existing Sends, it should make clear the primary benefits of the service (private, e2e encrypted, self-destructing file sharing)
+- [ ] It should allow users to access the file picker to create Send links
+- [ ] If the user has existing Sends, it should display a card-based list view of each [see Cards section below]
+
+### Non-Authenticated Users
+- [ ] It should make clear the benefits of a Firefox Account
+- [ ] It should allow users to log into or create a Firefox account
+- [ ] It should allow users to select and send multiple files in one URL
+- [ ] It should limit the sendable file size to 1GB
+- [ ] It should allow users to set an expiration time of 5 minutes, 1 hour, or 24 hours
+- [ ] It should allow users to set a download count of 1 downloads
+
+### Authenticated Users
+- [ ] It should indicate that the user is signed in via Firefox Account
+- [ ] It should allow the user to sign out
+- [ ] It should allow users to select and send multiple files in one URL
+- [ ] It should limit users to sending 2.5GB per Send
+- [ ] It should allow users to extend Send times up to 1 Week
+- [ ] It should allow users to extend Send download counts up to 100 times
+
+### Cards
+- [ ] It should display the name of the sent file/files
+- [ ] It should display the time remaining before expiration
+- [ ] It should display the number of downloads remaining before expiration
+- [ ] It should have a button that lets the user copy the send link to their clipboard
+- [ ] It should show a preview icon (not a thumbnail) that has some relationship to the file types or content being sent* (see 5.1 in spec)
+- [ ] It should have an overflow (meatball) menu that when triggered, gives the user share or delete buttons
+- [ ] While encrypting / pushing to server, it should display a progress meter and a cancel button
+- [ ] For authenticated users, it should be expandable to display all files in a send (5.1.1)
+- [ ] If user cancels Send, or Upload fails, it should display a warning in the card
+- [ ] It should display expired Sends below current sends with their UI greyed out and an expiration warning for 24 hours after expiration
+- [ ] It should remove expired cards from display after 24 hours
+- [ ] It should let users permanently delete records expired sends
+- [ ] It should display a visual indicator when a Send is password protected
+- [ ] It should allow the user to share via a native Android share sheet
+- [ ] It should allow me to create Send links through intents from other apps
+
+### General/other
+- [ ] It should allow users to set passwords to protect their Sends
+- [ ] It should warn users when they are trying to upload files larger than their share limit
+
+### Stretch
+- [ ] It should allow users to use the photo gallery to create Send links
+- [ ] It should allow users to use their camera to create Send links
+- [ ] It should allow users to opt into notification when a share link expires
+- [ ] It should allow users to opt into notifications when their link is downloaded
+
+## Annotations on Mobile Spec
+This document tracks differences between the UX spec for Send and the intended MVP.
+
+[Spec Link](https://mozilla.invisionapp.com/share/GNN6KKOQ5XS)
+
+* 1.1: Spec describes toolbar which may not be possible given the application framework we're using. In particular, issues with the spec include the color, logo and different font weights may be an issue.
+* 1.2: Spec's treatment of FxA UI may be difficult to match. We should use the default OAuth implementation and re-evaluate UX once we see an implementation demo. Also, the landing page UI should display a log-in CTA directly and not require users click into the hamburger menu.
+* 2.1: MVP will only include file picker. Signed in users will be able to select multiple files. File selection flow will be Android-native. Probably don't have the ability to add notifications as in the last screen on this page.
+* 2.1: @fzzzy will provide screenshots of this flow for UX evaluation and comment.
+* 3.1.4: The spec shows deleting the last item in an unshared set returning the user to the picker menu. Instead, it should return to the app home page.
+* 3.1.5: Same as 3.1.5 notes. Both cases should show the warning dialog.
+* 4.1: We may not be able to do a thumbnail here. Instead we should specify a set of icons to be displayed.
+* 6.3: We're not going to allow cards to be edited. This page is deprecated.
+* 6.4: Swiping cards to delete is stretched.
+* 6.5: We're not 100% sure what happens on network connectivity errors, we should test this and adapt UX as necessary.
+* 7.1: The last screen on this page depicts a network error notification on the selection screen. Instead the user should hit the send button, be taken back to the cards and display the card as in 5.1.2
+* 7.3: May not be necessary...we can ask for permissions on install.
+* 8.1: Notifications do not block launch
diff --git a/docs/acceptance-web.md b/docs/acceptance-web.md
new file mode 100644
index 00000000..859ef2fd
--- /dev/null
+++ b/docs/acceptance-web.md
@@ -0,0 +1,73 @@
+# Send V2 UX Web Acceptance Criteria
+
+## General
+
+- [ ] It should match the spec provided.
+- [ ] It should have a feedback button
+- [ ] It should provide links to relevant legal documentation
+
+### Non-Authenticated Users
+
+- [ ] It should make clear the benefits of a Firefox Account
+- [ ] It should allow users to log into or create a Firefox account
+- [ ] It should allow users to select and send multiple files in one URL
+- [ ] It should limit the sendable file size to 1GB
+- [ ] It should allow users to set an expiration time of 5 minutes, 1 hour, or 24 hours
+- [ ] It should allow users to set an download count of 1 downloads
+
+### Authenticated Users
+
+- [ ] It should indicate that the user is signed in via Firefox Account
+- [ ] It should allow the user to sign out
+- [ ] It should allow users to select and send multiple files in one URL
+- [ ] It should limit users to sending 2.5GB per Send
+- [ ] It should allow users to extend Send times up to 1 Week
+- [ ] It should allow users to extend Send download counts up to 100 times
+
+### Main Screen
+
+- [ ] It should clearly indicate the name of the product
+- [ ] If user has no existing Sends, it should make clear the primary benefits of the service (private, e2e encrypted, self-destructing file sharing)
+- [ ] It should allow users to access the file picker to create Send links
+- [ ] It should allow users to drag and drop files
+- [ ] It should provide affordances to sign in to Send
+- [ ] If the user has existing Sends, it should display a card-based list view of each
+
+### Upload UI
+
+- [ ] It should allow users to continue to add files to their upload up to a set limit
+- [ ] It should allow users to set a password
+- [ ] It should let users delete items from their upload bundle
+
+### Uploading UI
+
+- [ ] It should display an affordance to demonstrate the status of an upload
+
+### Share UI
+
+- [ ] It should provide a copiable URL to the bundle
+
+### Download UI
+
+- [ ] It should prompt the user for a password if one is required
+- [ ] It should provide feedback for incorrect passwords
+- [ ] It should provide a description of Send to make clear what this service is
+- [ ] It should let the user see the files they are downloading
+- [ ] It should let the user download their files
+
+### Download Complete UI
+
+- [ ] It should indicate that a download is complete
+- [ ] It should provide a description of the Send service
+- [ ] It should provide a link back to the upload UI
+
+### Expiry UI
+
+- [ ] It should provide a generic message indicating a share has expired
+- [ ] It should allow the user to navigate back to the upload page
+
+### In Memory DL Page
+
+- [ ] It should show in case a user tries to download a large file on a suboptimal client
+- [ ] It should suggest the user use Firefox
+- [ ] It should let the user copy the download url
\ No newline at end of file
diff --git a/docs/build.md b/docs/build.md
new file mode 100644
index 00000000..f440f410
--- /dev/null
+++ b/docs/build.md
@@ -0,0 +1,22 @@
+Send has two build configurations, development and production. Both can be run via `npm` scripts, `npm start` for development and `npm run build` for production. Webpack is our only build tool and all configuration lives in [webpack.config.js](../webpack.config.js).
+
+# Development
+
+`npm start` launches a `webpack-dev-server` on port 8080 that compiles the assets and watches files for changes. It also serves the backend API and frontend unit tests via the `server/bin/dev.js` entrypoint. The frontend tests can be run in the browser by navigating to http://localhost:8080/test and will rerun automatically as the watched files are saved with changes.
+
+# Production
+
+`npm run build` compiles the assets and writes the files to the `dist/` directory. `npm run prod` launches an Express server on port 1443 that serves the backend API and frontend static assets from `dist/` via the `server/bin/prod.js` entrypoint.
+
+# Notable differences
+
+- Development compiles assets in memory, so no `dist/` directory is generated
+- Development does not enable CSP headers
+- Development frontend source is instrumented for code coverage
+- Only development includes sourcemaps
+- Only development exposes the `/test` route
+- Production sets Cache-Control immutable headers on the hashed static assets
+
+# Custom Loaders
+
+The `build/` directory contains custom webpack loaders specific to Send. See [build/readme.md](../build/readme.md) for details on each loader.
\ No newline at end of file
diff --git a/docs/deployment.md b/docs/deployment.md
new file mode 100644
index 00000000..e4f6f60c
--- /dev/null
+++ b/docs/deployment.md
@@ -0,0 +1,96 @@
+## Requirements
+
+This document describes how to do a full deployment of Send on your own Linux server. You will need:
+
+* A working (and ideally somewhat recent) installation of NodeJS and npm
+* Git
+* Apache webserver
+* Optionally telnet, to be able to quickly check your installation
+
+For example in Debian/Ubuntu systems:
+
+```bash
+sudo apt install git apache2 nodejs npm telnet
+```
+
+## Building
+
+* We assume an already configured virtual-host on your webserver with an existing empty htdocs folder
+* First, remove that htdocs folder - we will replace it with Send's version now
+* git clone https://github.com/timvisee/send.git htdocs
+* Make now sure you are NOT root but rather the user your webserver is serving files under (e.g. "su www-data" or whoever the owner of your htdocs folder is)
+* npm install
+* npm run build
+
+## Running
+
+To have a permanently running version of Send as a background process:
+
+* Create a file `run.sh` with:
+
+```bash
+#!/bin/bash
+nohup su www-data -c "npm run prod" 2>/dev/null &
+```
+
+* Execute the script:
+
+```bash
+chmod +x run.sh
+./run.sh
+```
+
+Now the Send backend should be running on port 1443. You can check with:
+
+```bash
+telnet localhost 1443
+```
+
+## Reverse Proxy
+
+Of course, we don't want to expose the service on port 1443. Instead we want our normal webserver to forward all requests to Send ("Reverse proxy").
+
+# Apache webserver
+
+* Enable Apache required modules:
+
+```bash
+sudo a2enmod headers
+sudo a2enmod proxy
+sudo a2enmod proxy_http
+sudo a2enmod proxy_wstunnel
+sudo a2enmod rewrite
+```
+
+* Edit your Apache virtual host configuration file, insert this:
+
+```
+# Enable rewrite engine
+RewriteEngine on
+
+# Make sure the original domain name is forwarded to Send
+# Otherwise the generated URLs will be wrong
+ProxyPreserveHost on
+
+# Make sure the generated URL is https://
+RequestHeader set X-Forwarded-Proto https
+
+# If it's a normal file (e.g. PNG, CSS) just return it
+RewriteCond %{REQUEST_FILENAME} -f
+RewriteRule .* - [L]
+
+# If it's a websocket connection, redirect it to a Send WS connection
+RewriteCond %{HTTP:Upgrade} =websocket [NC]
+RewriteRule /(.*) ws://127.0.0.1:1443/$1 [P,L]
+
+# Otherwise redirect it to a normal HTTP connection
+RewriteRule ^/(.*)$ http://127.0.0.1:1443/$1 [P,QSA]
+ProxyPassReverse "/" "http://127.0.0.1:1443"
+```
+
+* Test configuration and restart Apache:
+
+```bash
+sudo apache2ctl configtest
+sudo systemctl restart apache2
+```
diff --git a/docs/docker.md b/docs/docker.md
index 9003412b..265485a7 100644
--- a/docs/docker.md
+++ b/docs/docker.md
@@ -1,35 +1,160 @@
-## Setup
+## Docker Quickstart
-Before building the Docker image, you must build the production assets:
+Use `registry.gitlab.com/timvisee/send:latest` from [`timvisee/send`'s Gitlab image registry](https://gitlab.com/timvisee/send/container_registry) for the latest Docker image.
-```sh
-npm run build
+```bash
+docker pull registry.gitlab.com/timvisee/send:latest
+
+# example quickstart (point REDIS_HOST to an already-running redis server)
+docker run -v $PWD/uploads:/uploads -p 1443:1443 \
+ -e 'DETECT_BASE_URL=true' \
+ -e 'REDIS_HOST=localhost' \
+ -e 'FILE_DIR=/uploads' \
+ registry.gitlab.com/timvisee/send:latest
```
-Then you can run either `docker build` or `docker-compose up`.
+Or clone this repo and run `docker build -t send:latest .` to build an image locally.
+*Note: for Docker Compose, see: https://github.com/timvisee/send-docker-compose*
-## Environment variables:
+## Environment Variables
-| Name | Description
+All the available config options and their defaults can be found here: https://github.com/timvisee/send/blob/master/server/config.js
+
+Config options should be set as unquoted environment variables. Boolean options should be `true`/`false`, time/duration should be integers (seconds), and filesize values should be integers (bytes).
+
+Config options expecting array values (e.g. `EXPIRE_TIMES_SECONDS`, `DOWNLOAD_COUNTS`) should be in unquoted CSV format. UI dropdowns will default to the first value in the CSV, e.g. `DOWNLOAD_COUNTS=5,1,10,100` will show four dropdown options, with `5` selected by the default.
+
+#### Server Configuration
+
+| Name | Description |
|------------------|-------------|
-| `PORT` | Port the server will listen on (defaults to 1443).
-| `S3_BUCKET` | The S3 bucket name.
-| `REDIS_HOST` | Host name of the Redis server.
-| `GOOGLE_ANALYTICS_ID` | Google Analytics ID
-| `SENTRY_CLIENT` | Sentry Client ID
-| `SENTRY_DSN` | Sentry DSN
-| `MAX_FILE_SIZE` | in bytes (defaults to 2147483648)
-| `NODE_ENV` | "production"
+| `BASE_URL` | The HTTPS URL where traffic will be served (e.g. `https://send.firefox.com`)
+| `DETECT_BASE_URL` | Autodetect the base URL using browser if `BASE_URL` is unset (defaults to `false`)
+| `PORT` | Port the server will listen on (defaults to `1443`)
+| `NODE_ENV` | Run in `development` mode (unsafe) or `production` mode (the default)
+| `SEND_FOOTER_DMCA_URL` | A URL to a contact page for DMCA requests (empty / not shown by default)
+| `SENTRY_CLIENT`, `SENTRY_DSN` | Sentry Client ID and DSN for error tracking (optional, disabled by default)
-## Example:
+*Note: more options can be found here: https://github.com/timvisee/send/blob/master/server/config.js*
-```sh
-$ docker run --net=host -e 'NODE_ENV=production' \
+#### Upload and Download Limits
+
+Configure the limits for uploads and downloads. Long expiration times are risky on public servers as people may use you as free hosting for copyrighted content or malware (which is why Mozilla shut down their `send` service). It's advised to only expose your service on a LAN/intranet, password protect it with a proxy/gateway, or make sure to set `SEND_FOOTER_DMCA_URL` above so you can respond to takedown requests.
+
+| Name | Description |
+|------------------|-------------|
+| `MAX_FILE_SIZE` | Maximum upload file size in bytes (defaults to `2147483648` aka 2GB)
+| `MAX_FILES_PER_ARCHIVE` | Maximum number of files per archive (defaults to `64`)
+| `MAX_EXPIRE_SECONDS` | Maximum upload expiry time in seconds (defaults to `604800` aka 7 days)
+| `MAX_DOWNLOADS` | Maximum number of downloads (defaults to `100`)
+| `DOWNLOAD_COUNTS` | Download limit options to show in UI dropdown, e.g. `10,1,2,5,10,15,25,50,100,1000`
+| `EXPIRE_TIMES_SECONDS` | Expire time options to show in UI dropdown, e.g. `3600,86400,604800,2592000,31536000`
+| `DEFAULT_DOWNLOADS` | Default download limit in UI (defaults to `1`)
+| `DEFAULT_EXPIRE_SECONDS` | Default expire time in UI (defaults to `86400`)
+
+*Note: more options can be found here: https://github.com/timvisee/send/blob/master/server/config.js*
+
+#### Storage Backend Options
+
+Pick how you want to store uploaded files and set these config options accordingly:
+
+- Local filesystem (the default): set `FILE_DIR` to the local path used inside the container for storage (or leave the default)
+- S3-compatible object store: set `S3_BUCKET`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` (and `S3_ENDPOINT` if using something other than AWS)
+- Google Cloud Storage: set `GCS_BUCKET` to the name of a GCS bucket (auth should be set up using [Application Default Credentials](https://cloud.google.com/docs/authentication/production#auth-cloud-implicit-nodejs))
+
+Redis is used as the metadata database for the backend and is required no matter which storage method you use.
+
+| Name | Description |
+|------------------|-------------|
+| `REDIS_HOST`, `REDIS_PORT`, `REDIS_USER`, `REDIS_PASSWORD`, `REDIS_DB` | Host name, port, and pass of the Redis server (defaults to `localhost`, `6379`, and no password)
+| `FILE_DIR` | Directory for storage inside the Docker container (defaults to `/uploads`)
+| `S3_BUCKET` | The S3 bucket name to use (only set if using S3 for storage)
+| `S3_ENDPOINT` | An optional custom endpoint to use for S3 (defaults to AWS)
+| `S3_USE_PATH_STYLE_ENDPOINT`| Whether to force [path style URLs](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html#s3ForcePathStyle-property) for S3 objects (defaults to `false`)
+| `AWS_ACCESS_KEY_ID` | S3 access key ID (only set if using S3 for storage)
+| `AWS_SECRET_ACCESS_KEY` | S3 secret access key ID (only set if using S3 for storage)
+| `GCS_BUCKET` | Google Cloud Storage bucket (only set if using GCP for storage)
+
+*Note: more options can be found here: https://github.com/timvisee/send/blob/master/server/config.js*
+
+## Branding
+
+To change the look the colors aswell as some graphics can be changed via environment variables.
+See the table below for the variables and their default values.
+
+| Name | Default | Description |
+|---|---|---|
+| UI_COLOR_PRIMARY | #0a84ff | The primary color |
+| UI_COLOR_ACCENT | #003eaa | The accent color (eg. for hover-effects) |
+| UI_CUSTOM_ASSETS_ANDROID_CHROME_192PX | | A custom icon for Android (192x192px) |
+| UI_CUSTOM_ASSETS_ANDROID_CHROME_512PX | | A custom icon for Android (512x512px) |
+| UI_CUSTOM_ASSETS_APPLE_TOUCH_ICON | | A custom icon for Apple |
+| UI_CUSTOM_ASSETS_FAVICON_16PX | | A custom favicon (16x16px) |
+| UI_CUSTOM_ASSETS_FAVICON_32PX | | A custom favicon (32x32px) |
+| UI_CUSTOM_ASSETS_ICON | | A custom icon (Logo on the top-left of the UI) |
+| UI_CUSTOM_ASSETS_SAFARI_PINNED_TAB | | A custom icon for Safari |
+| UI_CUSTOM_ASSETS_FACEBOOK | | A custom header image for Facebook |
+| UI_CUSTOM_ASSETS_TWITTER | | A custom header image for Twitter |
+| UI_CUSTOM_ASSETS_WORDMARK | | A custom wordmark (Text next to the logo) |
+| UI_CUSTOM_CSS | | Allows you to define a custom CSS file for custom styling |
+| CUSTOM_FOOTER_TEXT | | Allows you to define a custom footer |
+| CUSTOM_FOOTER_URL | | Allows you to define a custom URL in your footer |
+
+Side note: If you define a custom URL and a custom footer, only the footer text will display, but will be hyperlinked to the URL.
+
+## Examples
+
+**Run using an Amazon Elasticache for the Redis DB, Amazon S3 for the storage backend, and Sentry for error reporting.**
+
+```bash
+$ docker run -p 1443:1443 \
-e 'S3_BUCKET=testpilot-p2p-dev' \
-e 'REDIS_HOST=dyf9s2r4vo3.bolxr4.0001.usw2.cache.amazonaws.com' \
- -e 'GOOGLE_ANALYTICS_ID=UA-35433268-78' \
-e 'SENTRY_CLIENT=https://51e23d7263e348a7a3b90a5357c61cb2@sentry.prod.mozaws.net/168' \
-e 'SENTRY_DSN=https://51e23d7263e348a7a3b90a5357c61cb2:65e23d7263e348a7a3b90a5357c61c44@sentry.prod.mozaws.net/168' \
- mozilla/send:latest
+ -e 'BASE_URL=https://send.example.com' \
+ registry.gitlab.com/timvisee/send:latest
```
+
+*Note: make sure to replace the example values above with your real values before running.*
+
+
+**Run totally self-hosted using the current filesystem directry (`$PWD`) to store the Redis data and file uploads, with a `5GB` upload limit, 1 month expiry, and contact URL set.**
+
+```bash
+# create a network for the send backend and redis containers to talk to each other
+$ docker network create timviseesend
+
+# start the redis container
+$ docker run --net=timviseesend -v $PWD/redis:/data redis-server --appendonly yes
+
+# start the send backend container
+$ docker run --net=timviseesend -v $PWD/uploads:/uploads -p 1443:1443 \
+ -e 'BASE_URL=http://localhost:1443' \
+ -e 'MAX_FILE_SIZE=5368709120' \
+ -e 'MAX_EXPIRE_SECONDS=2592000' \
+ -e 'SEND_FOOTER_DMCA_URL=https://example.com/dmca-contact-info' \
+ registry.gitlab.com/timvisee/send:latest
+```
+Then open http://localhost:1443 to view the UI. (change the `localhost` to your IP or hostname above to serve the UI to others)
+
+To run with HTTPS, you will need to set up a reverse proxy with SSL termination in front of the backend. See Docker Compose below for an example setup.
+
+
+**Run with custom branding.**
+
+```bash
+$ docker run -p 1443:1443 \
+ -v $PWD/custom_assets:/app/dist/custom_assets \
+ -e 'UI_COLOR_PRIMARY=#f00' \
+ -e 'UI_COLOR_ACCENT=#a00' \
+ -e 'UI_CUSTOM_ASSETS_ICON=custom_assets/logo.svg' \
+ registry.gitlab.com/timvisee/send:latest
+```
+
+## Docker Compose
+
+For a Docker compose configuration example, see:
+
+https://github.com/timvisee/send-docker-compose
diff --git a/docs/encryption.md b/docs/encryption.md
new file mode 100644
index 00000000..b41d37de
--- /dev/null
+++ b/docs/encryption.md
@@ -0,0 +1,46 @@
+# File Encryption
+
+Send uses 128-bit AES-GCM encryption via the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) to encrypt files in the browser before uploading them to the server. The code is in [app/keychain.js](../app/keychain.js).
+
+## Steps
+
+### Uploading
+
+1. A new secret key is generated with `crypto.getRandomValues`
+2. The secret key is used to derive more keys via HKDF SHA-256
+ - a series of encryption keys for the file, via [ECE](https://tools.ietf.org/html/rfc8188) (AES-GCM)
+ - an encryption key for the file metadata (AES-GCM)
+ - a signing key for request authentication (HMAC SHA-256)
+3. The file and metadata are encrypted with their corresponding keys
+4. The encrypted data and signing key are uploaded to the server
+5. An owner token and the share url are returned by the server and stored in local storage
+6. The secret key is appended to the share url as a [#fragment](https://en.wikipedia.org/wiki/Fragment_identifier) and presented to the UI
+
+### Downloading
+
+1. The browser loads the share url page, which includes an authentication nonce
+2. The browser imports the secret key from the url fragment
+3. The same 3 keys as above are derived
+4. The browser signs the nonce with its signing key and requests the metadata
+5. The encrypted metadata is decrypted and presented on the page
+6. The browser makes another authenticated request to download the encrypted file
+7. The browser downloads and decrypts the file
+8. The file prompts the save dialog or automatically saves depending on the browser settings
+
+### Passwords
+
+A password may optionally be set to authenticate the download request. When a password is set the following steps occur.
+
+#### Sender
+
+1. The original signing key derived from the secret key is discarded
+2. A new signing key is generated via PBKDF2 from the user entered password and the full share url (including secret key fragment)
+3. The new key is sent to the server, authenticated by the owner token
+4. The server stores the new key and marks the record as needing a password
+
+#### Downloader
+
+1. The browser loads the share url page, which includes an authentication nonce and indicator that the file requires a password
+2. The user is prompted for the password and the signing key is derived
+3. The browser requests the metadata using the key to sign the nonce
+4. If the password was correct the metadata is returned, otherwise a 401
diff --git a/docs/experiments.md b/docs/experiments.md
new file mode 100644
index 00000000..f627204b
--- /dev/null
+++ b/docs/experiments.md
@@ -0,0 +1,84 @@
+# A/B experiment testing
+
+We're using Google Analytics Experiments for A/B testing.
+
+## Creating an experiment
+
+Navigate to the Behavior > Experiments section of Google Analytics and click the "Create experiment" button.
+
+The "Objective for this experiment" is the most complicated part. See the "Promo click (Goal ID 4 / Goal Set 1)" for an example.
+
+In step 2 add as many variants as you plan to test. The urls are not important since we aren't using their js library to choose the variants. The name will show up in the report so choose good ones. "Original page" becomes variant 0 and each variant increments by one. We'll use the numbers in our `app/experiments.js` code.
+
+Step 3 contains some script that we'll ignore. The important thing here is the **Experiment ID**. This is the value we need to name our experiment in `app/experiments.js`. Save the changes so far and wait until the code containing the experiment has been deployed to production **before** starting the experiment.
+
+## Experiment code
+
+Code for experiments live in [app/experiments.js](../app/experiments.js). There's an `experiments` object that contains the logic for deciding whether an experiment should run, which variant to use, and what to do. Each object needs to have these functions:
+
+### `eligible` function
+
+This function returns a boolean of whether this experiment should be active for this session. Any data available to the page can be used determine the result.
+
+### `variant` function
+
+This function returns which experimental group this session is placed in. The variant values need to match the values set up in Google Analytics, usually 0 thru N-1. This value is usually picked at random based on what percentage of each variant is desired.
+
+### `run` function
+
+This function gets the `variant` value chosen by the variant function and the `state` and `emitter` objects from the app. This function can do anything needed to change the app based on the experiment. A common pattern is to set or change a value on `state` that will be picked up by other parts of the app, like ui templates, to change how it looks or behaves.
+
+### Example
+
+Here's a full example of the experiment object:
+
+```js
+const experiments = {
+ S9wqVl2SQ4ab2yZtqDI3Dw: { // The Experiment ID from Google Analytics
+ id: 'S9wqVl2SQ4ab2yZtqDI3Dw',
+ run: function(variant, state, emitter) {
+ switch (variant) {
+ case 1:
+ state.promo = 'blue';
+ break;
+ case 2:
+ state.promo = 'pink';
+ break;
+ default:
+ state.promo = 'grey';
+ }
+ emitter.emit('render');
+ },
+ eligible: function() {
+ return (
+ !/firefox|fxios/i.test(navigator.userAgent) &&
+ document.querySelector('html').lang === 'en-US'
+ );
+ },
+ variant: function(state) {
+ const n = this.luckyNumber(state);
+ if (n < 0.33) {
+ return 0;
+ }
+ return n < 0.66 ? 1 : 2;
+ },
+ luckyNumber: function(state) {
+ return luckyNumber(
+ `${this.id}:${state.storage.get('testpilot_ga__cid')}`
+ );
+ }
+ }
+};
+```
+
+## Reporting results
+
+All metrics pings will include the variant and experiment id, but it's usually important to trigger a specific event to be counted as the experiment goal (the "Objective for this experiment" part from setup). Use an 'experiment' event to do this. For example:
+
+```js
+emit('experiment', { cd3: 'promo' });
+```
+
+where `emit` is the app emitter function passed to the [route handler](https://github.com/choojs/choo#approuteroutename-handlerstate-emit)
+
+The second argument can be an object with any additional parameters. It usually includes a custom dimension that we chose to filter on while creating the experiment in Google Analytics.
\ No newline at end of file
diff --git a/docs/faq.md b/docs/faq.md
index 7809e48f..657042de 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -1,12 +1,12 @@
-## How big of a file can I transfer with Firefox Send?
+## How big of a file can I transfer with Send?
-There is a 2GB file size limit built in to Send, however, in practice you may
-be unable to send files that large. Send encrypts and decrypts the files in
-the browser which is great for security but will tax your system resources. In
-particular you can expect to see your memory usage go up by at least the size
-of the file when the transfer is processing. You can see [the results of some
-testing](https://github.com/mozilla/send/issues/170#issuecomment-314107793).
-For the most reliable operation on common computers, it’s probably best to stay
+There is a 2GB file size limit built in to Send, but this may be changed by the
+hoster. Send encrypts and decrypts the files in the browser which is great for
+security but will tax your system resources. In particular you can expect to
+see your memory usage go up by at least the size of the file when the transfer
+is processing. You can see [the results of some
+testing](https://github.com/mozilla/send/issues/170#issuecomment-314107793). For
+the most reliable operation on common computers, it’s probably best to stay
under a few hundred megabytes.
## Why is my browser not supported?
@@ -17,26 +17,25 @@ Many browsers support this standard and should work fine, but some have not
implemented it yet (mobile browsers lag behind on this, in
particular).
-## Why does Firefox Send require JavaScript?
+## Why does Send require JavaScript?
-Firefox Send uses JavaScript to:
+Send uses JavaScript to:
- Encrypt and decrypt files locally on the client instead of the server.
- Render the user interface.
-- Manage translations on the website into [various different languages](https://github.com/mozilla/send#localization).
+- Manage translations on the website into [various different languages](https://github.com/timvisee/send#localization).
- Collect data to help us improve Send in accordance with our [Terms & Privacy](https://send.firefox.com/legal).
-Since Send is an open source project, you can see all of the cool ways we use JavaScript by [examining our code](https://github.com/mozilla/send/).
+Since Send is an open source project, you can see all of the cool ways we use JavaScript by [examining our code](https://github.com/timvisee/send/).
## How long are files available for?
Files are available to be downloaded for 24 hours, after which they are removed
-from the server. They are also removed immediately after a download completes.
+from the server. They are also removed immediately once the download limit is reached.
## Can a file be downloaded more than once?
-Not currently, but we're considering multiple download support in a future
-release.
+Yes, once a file is submitted to Send you can select the download limit.
*Disclaimer: Send is an experiment and under active development. The answers
diff --git a/docs/localization.md b/docs/localization.md
new file mode 100644
index 00000000..1cd92d41
--- /dev/null
+++ b/docs/localization.md
@@ -0,0 +1,29 @@
+# Localization
+
+Send is localized in over 50 languages. We use the [fluent](http://projectfluent.org/) library and store our translations in [FTL](http://projectfluent.org/fluent/guide/) files in `public/locales/`. `en-US` is our base language.
+
+## Process
+
+Strings are added or removed from [public/locales/en-US/send.ftl] as needed. Strings **MUST NOT** be *changed* after they've been commited and pushed to master. Changing a string requires creating a new ID with a new name (preferably descriptive instead of incremented) and deletion of the obsolete ID. It's often useful to add a comment above the string with info about how and where the string is used.
+
+Once new strings are commited to master they are available for translators in Pontoon. All languages other than `en-US` should be edited via Pontoon. Translations get automatically commited to the github master branch.
+
+### Activation
+
+The development environment includes all locales in `public/locales` via the `L10N_DEV` environment variable. Production uses `package.json` as the list of locales to use. Once a locale has enough string coverage it should be added to `package.json`.
+
+## Code
+
+In `app/` we use the `state.translate()` function to translate strings to the best matching language base on the user's `Accept-Language` header. It's a wrapper around fluent's [FluentBundle.format](http://projectfluent.org/fluent.js/fluent/FluentBundle.html). It works the same for both server and client side rendering.
+
+### Examples
+
+```js
+// simple string
+const finishedString = state.translate('downloadFinish')
+// with parameters
+const progressString = state.translate('downloadingPageProgress', {
+ filename: state.fileInfo.name,
+ size: bytes(state.fileInfo.size)
+})
+```
diff --git a/docs/metrics.md b/docs/metrics.md
deleted file mode 100644
index 9006b347..00000000
--- a/docs/metrics.md
+++ /dev/null
@@ -1,144 +0,0 @@
-# Send Metrics
-The metrics collection and analysis plan for Send, a forthcoming Test Pilot experiment.
-
-## Analysis
-Data collected by Send will be used to answer the following high-level questions:
-
-- Do users send files?
- - How often? How many?
- - What is the retention?
- - What is the distribution of senders?
-- How do recipients interact with promotional UI elements?
- - Are file recipients converted to file senders?
- - Are non-Firefox users converted to Firefox users?
-- Where does it go wrong?
- - How often are there errors in uploading or downloading files?
- - What types of errors to users commonly see?
- - At what point do errors affect retention?
-
-## Collection
-Data will be collected with Google Analytics and follow [Test Pilot standards](https://github.com/mozilla/testpilot/blob/master/docs/experiments/ga.md) for reporting.
-
-### Custom Metrics
-- `cm1` - the size of the file, in bytes.
-- `cm2` - the amount of time it took to complete the file transfer, in milliseconds. Only include if the file completed transferring (ref: `cd2`).
-- `cm3` - the rate of the file transfer, in bytes per second. This is computed by dividing `cm1` by `cm2`, not by monitoring transfer speeds. Only include if the file completed transferring (ref: `cd2`).
-- `cm4` - the amount of time until the file will expire, in milliseconds.
-- `cm5` - the number of files the user has ever uploaded.
-- `cm6` - the number of unexpired files the user has uploaded.
-- `cm7` - the number of files the user has ever downloaded.
-
-### Custom Dimensions
-- `cd1` - the method by which the user initiated an upload. One of `drag`, `click`.
-- `cd2` - the reason that the file transfer stopped. One of `completed`, `errored`, `cancelled`.
-- `cd3` - the destination of a link click. One of `experiment-page`, `download-firefox`, `twitter`, `github`, `cookies`, `terms`, `privacy`, `about`, `legal`, `mozilla`.
-- `cd4` - the location from which the user copied the URL to an upload file. One of `success-screen`, `upload-list`.
-- `cd5` - the referring location. One of `completed-download`, `errored-download`, `cancelled-download`, `completed-upload`, `errored-upload`, `cancelled-upload`, `testpilot`, `external`.
-- `cd6` - identifying information about an error. Exclude if there is no error involved. **TODO:** enumerate a list of possibilities.
-
-### Events
-
-_NB:_ due to how files are being tracked, there are no events indicating file expiry. This carries some risk: most notably, we can only derive expiration rates by looking at download rates, which is prone to skew if there are problems in data collection.
-
-#### `upload-started`
-Triggered whenever a user begins uploading a file. Includes:
-
-- `ec` - `sender`
-- `ea` - `upload-started`
-- `cm1`
-- `cm5`
-- `cm6`
-- `cm7`
-- `cd1`
-- `cd5`
-
-#### `upload-stopped`
-Triggered whenever a user stops uploading a file. Includes:
-
-- `ec` - `sender`
-- `ea` - `upload-stopped`
-- `cm1`
-- `cm2`
-- `cm3`
-- `cm5`
-- `cm6`
-- `cm7`
-- `cd1`
-- `cd2`
-- `cd6`
-
-#### `password-added`
-Triggered whenever a password is added to a file. Includes:
-
-- `cm1`
-- `cm5`
-- `cm6`
-- `cm7`
-
-#### `download-started`
-Triggered whenever a user begins downloading a file. Includes:
-
-- `ec` - `recipient`
-- `ea` - `download-started`
-- `cm1`
-- `cm4`
-- `cm5`
-- `cm6`
-- `cm7`
-
-#### `download-stopped`
-Triggered whenever a user stops downloading a file.
-
-- `ec` - `recipient`
-- `ea` - `download-stopped`
-- `cm1`
-- `cm2` (if possible and applicable)
-- `cm3` (if possible and applicable)
-- `cm5`
-- `cm6`
-- `cm7`
-- `cd2`
-- `cd6`
-
-#### `exited`
-Fired whenever a user follows a link external to Send.
-
-- `ec` - `recipient`, `sender`, or `other`, as applicable.
-- `ea` - `exited`
-- `cd3`
-
-#### `upload-deleted`
-Fired whenever a user deletes a file they’ve uploaded.
-
-- `ec` - `sender`
-- `ea` - `upload-deleted`
-- `cm1`
-- `cm2`
-- `cm3`
-- `cm4`
-- `cm5`
-- `cm6`
-- `cm7`
-- `cd1`
-- `cd4`
-
-#### `copied`
-Fired whenever a user copies the URL of an upload file.
-
-- `ec` - `sender`
-- `ea` - `copied`
-- `cd4`
-
-#### `restarted`
-Fired whenever the user interrupts any part of funnel to return to the start of it (e.g. with a “send another file” or “send your own files” link).
-
-- `ec` - `recipient`, `sender`, or `other`, as applicable.
-- `ea` - `restarted`
-- `cd2`
-
-#### `unsupported`
-Fired whenever a user is presented a message saying that their browser is unsupported due to missing crypto APIs.
-
-- `ec` - `recipient` or `sender`, as applicable.
-- `ea` - `unsupported`
-- `cd6`
diff --git a/docs/notes/streams.md b/docs/notes/streams.md
new file mode 100644
index 00000000..ec0c0b89
--- /dev/null
+++ b/docs/notes/streams.md
@@ -0,0 +1,34 @@
+# Web Streams
+
+- API
+ - https://developer.mozilla.org/en-US/docs/Web/API/Streams_API
+- Reference Implementation
+ - https://github.com/whatwg/streams/tree/master/reference-implementation
+- Examples
+ - https://github.com/mdn/dom-examples/tree/master/streams
+- Polyfill
+ - https://github.com/MattiasBuelens/web-streams-polyfill
+
+# Encrypted Content Encoding
+
+- Spec
+ - https://trac.tools.ietf.org/html/rfc8188
+- node.js implementation
+ - https://github.com/web-push-libs/encrypted-content-encoding/tree/master/nodejs
+
+# Other APIs
+
+- Blobs
+ - https://developer.mozilla.org/en-US/docs/Web/API/Blob
+- ArrayBuffers, etc
+ - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
+ - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
+- FileReader
+ - https://developer.mozilla.org/en-US/docs/Web/API/FileReader
+
+# Other
+
+- node.js Buffer browser library
+ - https://github.com/feross/buffer
+- StreamSaver
+ - https://github.com/jimmywarting/StreamSaver.js
diff --git a/docs/takedowns.md b/docs/takedowns.md
new file mode 100644
index 00000000..da09c2ee
--- /dev/null
+++ b/docs/takedowns.md
@@ -0,0 +1,19 @@
+## Take-down process
+
+In cases of a DMCA notice, or other abuse yet to be determined, a file has to be removed from the service.
+
+Files can be delisted and made inaccessible by removing their record from Redis.
+
+Send share links contain the `id` of the file, for example `https://send.firefox.com/download/3d9d2bb9a1`
+
+From a host with access to the Redis server run a `DEL` command with the file id.
+
+For example:
+
+```sh
+redis-cli DEL 3d9d2bb9a1
+```
+
+Other redis-cli parameters like `-h` may also be required. See [redis-cli docs](https://redis.io/topics/rediscli) for more info.
+
+The encrypted file resides on S3 as the same `id` under the bucket that the app was configured with as `S3_BUCKET`. The file can be managed if it has not already expired with the [AWS cli](https://docs.aws.amazon.com/cli/latest/reference/s3/index.html) or AWS web console.
\ No newline at end of file
diff --git a/ios/generate-bundle.js b/ios/generate-bundle.js
new file mode 100644
index 00000000..70f60c3d
--- /dev/null
+++ b/ios/generate-bundle.js
@@ -0,0 +1,21 @@
+const child_process = require('child_process');
+const fs = require('fs');
+const path = require('path');
+
+child_process.execSync('npm run build');
+
+const prefix = path.join('..', 'dist');
+const json_string = fs.readFileSync(path.join(prefix, 'manifest.json'));
+const manifest = JSON.parse(json_string);
+
+const ios_filename = manifest['ios.js'];
+fs.writeFileSync(
+ 'send-ios/assets/ios.js',
+ fs.readFileSync(`${prefix}${ios_filename}`)
+);
+
+const vendor_filename = manifest['vendor.js'];
+fs.writeFileSync(
+ 'send-ios/assets/vendor.js',
+ fs.readFileSync(`${prefix}${vendor_filename}`)
+);
diff --git a/ios/ios.js b/ios/ios.js
new file mode 100644
index 00000000..23b7c4a3
--- /dev/null
+++ b/ios/ios.js
@@ -0,0 +1,158 @@
+/* global window, document, fetch */
+
+const MAXFILESIZE = 1024 * 1024 * 1024 * 2;
+
+const EventEmitter = require('events');
+const emitter = new EventEmitter();
+
+function dom(tagName, attributes, children = []) {
+ const node = document.createElement(tagName);
+ for (const name in attributes) {
+ if (name.indexOf('on') === 0) {
+ node[name] = attributes[name];
+ } else if (name === 'htmlFor') {
+ node.htmlFor = attributes.htmlFor;
+ } else if (name === 'className') {
+ node.className = attributes.className;
+ } else {
+ node.setAttribute(name, attributes[name]);
+ }
+ }
+ if (!(children instanceof Array)) {
+ children = [children];
+ }
+ for (let child of children) {
+ if (typeof child === 'string') {
+ child = document.createTextNode(child);
+ }
+ node.appendChild(child);
+ }
+ return node;
+}
+
+function uploadComplete(file) {
+ document.body.innerHTML = '';
+ const input = dom('input', { id: 'url', value: file.url });
+ const copy = dom(
+ 'button',
+ {
+ id: 'copy-button',
+ className: 'button',
+ onclick: () => {
+ window.webkit.messageHandlers['copy'].postMessage(input.value);
+ copy.textContent = 'Copied!';
+ setTimeout(function() {
+ copy.textContent = 'Copy to clipboard';
+ }, 2000);
+ }
+ },
+ 'Copy to clipboard'
+ );
+ const node = dom(
+ 'div',
+ { id: 'striped' },
+ dom('div', { id: 'white' }, [
+ input,
+ copy,
+ dom(
+ 'button',
+ { id: 'send-another', className: 'button', onclick: render },
+ 'Send another file'
+ )
+ ])
+ );
+ document.body.appendChild(node);
+}
+
+const state = {
+ storage: {
+ files: [],
+ remove: function(fileId) {
+ console.log('REMOVE FILEID', fileId);
+ },
+ writeFile: function(file) {
+ console.log('WRITEFILE', file);
+ },
+ addFile: uploadComplete,
+ totalUploads: 0
+ },
+ transfer: null,
+ uploading: false,
+ settingPassword: false,
+ passwordSetError: null,
+ route: '/'
+};
+
+function upload(event) {
+ console.log('UPLOAD');
+ event.preventDefault();
+ const target = event.target;
+ const file = target.files[0];
+ if (file.size === 0) {
+ return;
+ }
+ if (file.size > MAXFILESIZE) {
+ console.log('file too big (no bigger than ' + MAXFILESIZE + ')');
+ return;
+ }
+
+ emitter.emit('upload', { file: file, type: 'click' });
+}
+
+function render() {
+ document.body.innerHTML = '';
+ const striped = dom(
+ 'div',
+ { id: 'striped' },
+ dom('div', { id: 'white' }, [
+ dom('label', { id: 'label', htmlFor: 'input' }, 'Choose file'),
+ dom('input', {
+ id: 'input',
+ type: 'file',
+ name: 'input',
+ onchange: upload
+ })
+ ])
+ );
+ document.body.appendChild(striped);
+}
+
+emitter.on('render', function() {
+ document.body.innerHTML = '';
+ const percent =
+ (state.transfer.progress[0] / state.transfer.progress[1]) * 100;
+ const node = dom(
+ 'div',
+ { style: 'background-color: white; width: 100%' },
+ dom('span', {
+ style: `display: inline-block; width: ${percent}%; background-color: blue`
+ })
+ );
+ document.body.appendChild(node);
+});
+
+emitter.on('pushState', function(path) {
+ console.log('pushState ' + path + ' ' + JSON.stringify(state));
+});
+
+const controller = require('../app/controller').default;
+try {
+ controller(state, emitter);
+} catch (e) {
+ console.error('error' + e);
+ console.error(e);
+}
+
+function sendBase64EncodedFromSwift(encoded) {
+ fetch(encoded)
+ .then(res => res.blob())
+ .then(blob => {
+ emitter.emit('upload', { file: blob, type: 'share' });
+ });
+}
+
+window.sendBase64EncodedFromSwift = sendBase64EncodedFromSwift;
+
+render();
+
+window.webkit.messageHandlers['loaded'].postMessage('');
diff --git a/ios/send-ios-action-extension/ActionViewController.swift b/ios/send-ios-action-extension/ActionViewController.swift
new file mode 100644
index 00000000..9b202adb
--- /dev/null
+++ b/ios/send-ios-action-extension/ActionViewController.swift
@@ -0,0 +1,77 @@
+//
+// ActionViewController.swift
+// send-ios-action-extension
+//
+// Created by Donovan Preston on 7/26/18.
+//
+
+import UIKit
+import WebKit
+import MobileCoreServices
+
+var typesToLoad = [("com.adobe.pdf", "application/pdf"), ("public.png", "image/png"),
+ ("public.jpeg", "image/jpeg"), ("public.jpeg-2000", "image/jp2"),
+ ("com.compuserve.gif", "image/gif"), ("com.microsoft.bmp", "image/bmp"),
+ ("public.plain-text", "text/plain")]
+
+class ActionViewController: UIViewController, WKScriptMessageHandler {
+
+ @IBOutlet var webView: WKWebView!
+ var typeToSend: String?
+ var dataToSend: Data?
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ self.webView.frame = self.view.bounds
+ self.webView?.configuration.userContentController.add(self, name: "loaded")
+ self.webView?.configuration.userContentController.add(self, name: "copy")
+
+ if let url = Bundle.main.url(
+ forResource: "index",
+ withExtension: "html",
+ subdirectory: "assets") {
+ self.webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
+ }
+ // Get the item[s] we're handling from the extension context.
+
+ for item in self.extensionContext!.inputItems as! [NSExtensionItem] {
+ for provider in item.attachments! as! [NSItemProvider] {
+ for (type, mimeType) in typesToLoad {
+ if provider.hasItemConformingToTypeIdentifier(type) {
+ provider.loadDataRepresentation(forTypeIdentifier: type, completionHandler: { (data, error) in
+ OperationQueue.main.addOperation {
+ self.typeToSend = mimeType
+ self.dataToSend = data
+ }
+ })
+ return
+ }
+ }
+ }
+ }
+ }
+
+ public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
+ print("Message received: \(message.name) with body: \(message.body)")
+ if (message.name == "loaded") {
+ let stringToSend = "window.sendBase64EncodedFromSwift('data:\(self.typeToSend ?? "application/octet-stream");base64,\(self.dataToSend?.base64EncodedString() ?? "")')";
+ self.webView.evaluateJavaScript(stringToSend) { (object: Any?, error: Error?) -> Void in
+ print("completed")
+ }
+ } else if (message.name == "copy") {
+ UIPasteboard.general.string = "\(message.body)"
+ }
+ }
+
+ override func didReceiveMemoryWarning() {
+ super.didReceiveMemoryWarning()
+ // Dispose of any resources that can be recreated.
+ }
+
+ @IBAction func done() {
+ // Return any edited content to the host app.
+ // This template doesn't do anything, so we just echo the passed in items.
+ self.extensionContext!.completeRequest(returningItems: self.extensionContext!.inputItems, completionHandler: nil)
+ }
+
+}
diff --git a/ios/send-ios-action-extension/Base.lproj/MainInterface.storyboard b/ios/send-ios-action-extension/Base.lproj/MainInterface.storyboard
new file mode 100644
index 00000000..5939032d
--- /dev/null
+++ b/ios/send-ios-action-extension/Base.lproj/MainInterface.storyboard
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/send-ios-action-extension/Info.plist b/ios/send-ios-action-extension/Info.plist
new file mode 100644
index 00000000..10caa73e
--- /dev/null
+++ b/ios/send-ios-action-extension/Info.plist
@@ -0,0 +1,53 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ send-ios-action-extension
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ XPC!
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ NSExtension
+
+ NSExtensionAttributes
+
+ NSExtensionActivationRule
+ SUBQUERY (
+ extensionItems,
+ $extensionItem,
+ SUBQUERY (
+ $extensionItem.attachments,
+ $attachment,
+ (
+ ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.adobe.pdf"
+ || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image"
+ || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.plain-text"
+ || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.png"
+ || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.jpeg"
+ || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.jpeg-2000"
+ || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.compuserve.gif"
+ || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.microsoft.bmp"
+ )
+ ).@count == 1
+ ).@count == 1
+
+ NSExtensionMainStoryboard
+ MainInterface
+ NSExtensionPointIdentifier
+ com.apple.ui-services
+
+
+
diff --git a/ios/send-ios.xcodeproj/project.pbxproj b/ios/send-ios.xcodeproj/project.pbxproj
new file mode 100644
index 00000000..83f460b2
--- /dev/null
+++ b/ios/send-ios.xcodeproj/project.pbxproj
@@ -0,0 +1,526 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 50;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ E34149C621017A3A00930775 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34149C521017A3A00930775 /* AppDelegate.swift */; };
+ E34149C821017A3A00930775 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34149C721017A3A00930775 /* ViewController.swift */; };
+ E34149CB21017A3A00930775 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E34149C921017A3A00930775 /* Main.storyboard */; };
+ E34149CD21017A3D00930775 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E34149CC21017A3D00930775 /* Assets.xcassets */; };
+ E34149D021017A3D00930775 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E34149CE21017A3D00930775 /* LaunchScreen.storyboard */; };
+ E355478521028193009D206E /* help.html in Resources */ = {isa = PBXBuildFile; fileRef = E355478421028193009D206E /* help.html */; };
+ E355478921092E22009D206E /* assets in Resources */ = {isa = PBXBuildFile; fileRef = E355478821092E22009D206E /* assets */; };
+ E355478C210A534F009D206E /* ios.js in Resources */ = {isa = PBXBuildFile; fileRef = E355478B210A534F009D206E /* ios.js */; };
+ E355478E210A5357009D206E /* generate-bundle.js in Resources */ = {isa = PBXBuildFile; fileRef = E355478D210A5357009D206E /* generate-bundle.js */; };
+ E397A0B2210A641C00A978D4 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E397A0B1210A641C00A978D4 /* ActionViewController.swift */; };
+ E397A0B5210A641C00A978D4 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E397A0B3210A641C00A978D4 /* MainInterface.storyboard */; };
+ E397A0B9210A641C00A978D4 /* send-ios-action-extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E397A0AF210A641C00A978D4 /* send-ios-action-extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ E397A0BF210A6B5500A978D4 /* assets in Resources */ = {isa = PBXBuildFile; fileRef = E397A0BE210A6B5500A978D4 /* assets */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ E397A0B7210A641C00A978D4 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = E34149BA21017A3900930775 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = E397A0AE210A641C00A978D4;
+ remoteInfo = "send-ios-action-extension";
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ E397A0BD210A641C00A978D4 /* Embed App Extensions */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 13;
+ files = (
+ E397A0B9210A641C00A978D4 /* send-ios-action-extension.appex in Embed App Extensions */,
+ );
+ name = "Embed App Extensions";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ E34149C221017A3900930775 /* send-ios.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "send-ios.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+ E34149C521017A3A00930775 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ E34149C721017A3A00930775 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
+ E34149CA21017A3A00930775 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ E34149CC21017A3D00930775 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ E34149CF21017A3D00930775 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ E34149D121017A3D00930775 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ E355478421028193009D206E /* help.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = help.html; sourceTree = ""; };
+ E355478821092E22009D206E /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = assets; sourceTree = ""; };
+ E355478B210A534F009D206E /* ios.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = ios.js; sourceTree = SOURCE_ROOT; };
+ E355478D210A5357009D206E /* generate-bundle.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "generate-bundle.js"; sourceTree = SOURCE_ROOT; };
+ E397A0AF210A641C00A978D4 /* send-ios-action-extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "send-ios-action-extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
+ E397A0B1210A641C00A978D4 /* ActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionViewController.swift; sourceTree = ""; };
+ E397A0B4210A641C00A978D4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; };
+ E397A0B6210A641C00A978D4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ E397A0BE210A6B5500A978D4 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = "send-ios/assets"; sourceTree = SOURCE_ROOT; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ E34149BF21017A3900930775 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ E397A0AC210A641C00A978D4 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ E34149B921017A3900930775 = {
+ isa = PBXGroup;
+ children = (
+ E34149C421017A3900930775 /* send-ios */,
+ E397A0B0210A641C00A978D4 /* send-ios-action-extension */,
+ E34149C321017A3900930775 /* Products */,
+ );
+ sourceTree = "";
+ };
+ E34149C321017A3900930775 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ E34149C221017A3900930775 /* send-ios.app */,
+ E397A0AF210A641C00A978D4 /* send-ios-action-extension.appex */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ E34149C421017A3900930775 /* send-ios */ = {
+ isa = PBXGroup;
+ children = (
+ E355478D210A5357009D206E /* generate-bundle.js */,
+ E355478B210A534F009D206E /* ios.js */,
+ E34149C521017A3A00930775 /* AppDelegate.swift */,
+ E34149C721017A3A00930775 /* ViewController.swift */,
+ E34149C921017A3A00930775 /* Main.storyboard */,
+ E34149CC21017A3D00930775 /* Assets.xcassets */,
+ E34149CE21017A3D00930775 /* LaunchScreen.storyboard */,
+ E34149D121017A3D00930775 /* Info.plist */,
+ E355478421028193009D206E /* help.html */,
+ E355478821092E22009D206E /* assets */,
+ );
+ path = "send-ios";
+ sourceTree = "";
+ };
+ E397A0B0210A641C00A978D4 /* send-ios-action-extension */ = {
+ isa = PBXGroup;
+ children = (
+ E397A0BE210A6B5500A978D4 /* assets */,
+ E397A0B1210A641C00A978D4 /* ActionViewController.swift */,
+ E397A0B3210A641C00A978D4 /* MainInterface.storyboard */,
+ E397A0B6210A641C00A978D4 /* Info.plist */,
+ );
+ path = "send-ios-action-extension";
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ E34149C121017A3900930775 /* send-ios */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = E34149D421017A3D00930775 /* Build configuration list for PBXNativeTarget "send-ios" */;
+ buildPhases = (
+ E355478A210A4C43009D206E /* ShellScript */,
+ E34149BE21017A3900930775 /* Sources */,
+ E34149BF21017A3900930775 /* Frameworks */,
+ E34149C021017A3900930775 /* Resources */,
+ E397A0BD210A641C00A978D4 /* Embed App Extensions */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ E397A0B8210A641C00A978D4 /* PBXTargetDependency */,
+ );
+ name = "send-ios";
+ productName = "send-ios";
+ productReference = E34149C221017A3900930775 /* send-ios.app */;
+ productType = "com.apple.product-type.application";
+ };
+ E397A0AE210A641C00A978D4 /* send-ios-action-extension */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = E397A0BC210A641C00A978D4 /* Build configuration list for PBXNativeTarget "send-ios-action-extension" */;
+ buildPhases = (
+ E397A0AB210A641C00A978D4 /* Sources */,
+ E397A0AC210A641C00A978D4 /* Frameworks */,
+ E397A0AD210A641C00A978D4 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = "send-ios-action-extension";
+ productName = "send-ios-action-extension";
+ productReference = E397A0AF210A641C00A978D4 /* send-ios-action-extension.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ E34149BA21017A3900930775 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastSwiftUpdateCheck = 0940;
+ LastUpgradeCheck = 0940;
+ ORGANIZATIONNAME = "Donovan Preston";
+ TargetAttributes = {
+ E34149C121017A3900930775 = {
+ CreatedOnToolsVersion = 9.4.1;
+ };
+ E397A0AE210A641C00A978D4 = {
+ CreatedOnToolsVersion = 9.4.1;
+ };
+ };
+ };
+ buildConfigurationList = E34149BD21017A3900930775 /* Build configuration list for PBXProject "send-ios" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = E34149B921017A3900930775;
+ productRefGroup = E34149C321017A3900930775 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ E34149C121017A3900930775 /* send-ios */,
+ E397A0AE210A641C00A978D4 /* send-ios-action-extension */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ E34149C021017A3900930775 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E355478C210A534F009D206E /* ios.js in Resources */,
+ E355478921092E22009D206E /* assets in Resources */,
+ E355478E210A5357009D206E /* generate-bundle.js in Resources */,
+ E34149D021017A3D00930775 /* LaunchScreen.storyboard in Resources */,
+ E355478521028193009D206E /* help.html in Resources */,
+ E34149CD21017A3D00930775 /* Assets.xcassets in Resources */,
+ E34149CB21017A3A00930775 /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ E397A0AD210A641C00A978D4 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E397A0B5210A641C00A978D4 /* MainInterface.storyboard in Resources */,
+ E397A0BF210A6B5500A978D4 /* assets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ E355478A210A4C43009D206E /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "node generate-bundle.js\n";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ E34149BE21017A3900930775 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E34149C821017A3A00930775 /* ViewController.swift in Sources */,
+ E34149C621017A3A00930775 /* AppDelegate.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ E397A0AB210A641C00A978D4 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E397A0B2210A641C00A978D4 /* ActionViewController.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ E397A0B8210A641C00A978D4 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = E397A0AE210A641C00A978D4 /* send-ios-action-extension */;
+ targetProxy = E397A0B7210A641C00A978D4 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ E34149C921017A3A00930775 /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ E34149CA21017A3A00930775 /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ E34149CE21017A3D00930775 /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ E34149CF21017A3D00930775 /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+ E397A0B3210A641C00A978D4 /* MainInterface.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ E397A0B4210A641C00A978D4 /* Base */,
+ );
+ name = MainInterface.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ E34149D221017A3D00930775 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.4;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ E34149D321017A3D00930775 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.4;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ E34149D521017A3D00930775 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_STYLE = Automatic;
+ INFOPLIST_FILE = "send-ios/Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "com.mozilla.send-ios";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 4.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ E34149D621017A3D00930775 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_STYLE = Automatic;
+ INFOPLIST_FILE = "send-ios/Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "com.mozilla.send-ios";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 4.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ E397A0BA210A641C00A978D4 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ INFOPLIST_FILE = "send-ios-action-extension/Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "com.mozilla.send-ios.send-ios-action-extension";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SWIFT_VERSION = 4.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ E397A0BB210A641C00A978D4 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ INFOPLIST_FILE = "send-ios-action-extension/Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "com.mozilla.send-ios.send-ios-action-extension";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SWIFT_VERSION = 4.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ E34149BD21017A3900930775 /* Build configuration list for PBXProject "send-ios" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ E34149D221017A3D00930775 /* Debug */,
+ E34149D321017A3D00930775 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ E34149D421017A3D00930775 /* Build configuration list for PBXNativeTarget "send-ios" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ E34149D521017A3D00930775 /* Debug */,
+ E34149D621017A3D00930775 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ E397A0BC210A641C00A978D4 /* Build configuration list for PBXNativeTarget "send-ios-action-extension" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ E397A0BA210A641C00A978D4 /* Debug */,
+ E397A0BB210A641C00A978D4 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = E34149BA21017A3900930775 /* Project object */;
+}
diff --git a/ios/send-ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/send-ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..883bc6f4
--- /dev/null
+++ b/ios/send-ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/ios/send-ios.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/send-ios.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000..18d98100
--- /dev/null
+++ b/ios/send-ios.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/ios/send-ios.xcodeproj/project.xcworkspace/xcuserdata/donovan.xcuserdatad/UserInterfaceState.xcuserstate b/ios/send-ios.xcodeproj/project.xcworkspace/xcuserdata/donovan.xcuserdatad/UserInterfaceState.xcuserstate
new file mode 100644
index 00000000..ccfe2abd
Binary files /dev/null and b/ios/send-ios.xcodeproj/project.xcworkspace/xcuserdata/donovan.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/ios/send-ios.xcodeproj/xcuserdata/donovan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/ios/send-ios.xcodeproj/xcuserdata/donovan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
new file mode 100644
index 00000000..a53fd357
--- /dev/null
+++ b/ios/send-ios.xcodeproj/xcuserdata/donovan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
diff --git a/ios/send-ios.xcodeproj/xcuserdata/donovan.xcuserdatad/xcschemes/xcschememanagement.plist b/ios/send-ios.xcodeproj/xcuserdata/donovan.xcuserdatad/xcschemes/xcschememanagement.plist
new file mode 100644
index 00000000..566d89e1
--- /dev/null
+++ b/ios/send-ios.xcodeproj/xcuserdata/donovan.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -0,0 +1,19 @@
+
+
+
+
+ SchemeUserState
+
+ send-ios-action-extension.xcscheme
+
+ orderHint
+ 1
+
+ send-ios.xcscheme
+
+ orderHint
+ 0
+
+
+
+
diff --git a/ios/send-ios/AppDelegate.swift b/ios/send-ios/AppDelegate.swift
new file mode 100644
index 00000000..bfb62d7a
--- /dev/null
+++ b/ios/send-ios/AppDelegate.swift
@@ -0,0 +1,45 @@
+//
+// AppDelegate.swift
+// send-ios
+//
+// Created by Donovan Preston on 7/19/18.
+//
+
+import UIKit
+
+@UIApplicationMain
+class AppDelegate: UIResponder, UIApplicationDelegate {
+
+ var window: UIWindow?
+
+
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
+ // Override point for customization after application launch.
+ return true
+ }
+
+ func applicationWillResignActive(_ application: UIApplication) {
+ // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
+ // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
+ }
+
+ func applicationDidEnterBackground(_ application: UIApplication) {
+ // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
+ // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
+ }
+
+ func applicationWillEnterForeground(_ application: UIApplication) {
+ // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
+ }
+
+ func applicationDidBecomeActive(_ application: UIApplication) {
+ // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
+ }
+
+ func applicationWillTerminate(_ application: UIApplication) {
+ // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
+ }
+
+
+}
+
diff --git a/ios/send-ios/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/send-ios/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000..d8db8d65
--- /dev/null
+++ b/ios/send-ios/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,98 @@
+{
+ "images" : [
+ {
+ "idiom" : "iphone",
+ "size" : "20x20",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "20x20",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "29x29",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "29x29",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "40x40",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "40x40",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "60x60",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "60x60",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "20x20",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "20x20",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "29x29",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "29x29",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "40x40",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "40x40",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "76x76",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "76x76",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "83.5x83.5",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ios-marketing",
+ "size" : "1024x1024",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/ios/send-ios/Assets.xcassets/Contents.json b/ios/send-ios/Assets.xcassets/Contents.json
new file mode 100644
index 00000000..da4a164c
--- /dev/null
+++ b/ios/send-ios/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/ios/send-ios/Base.lproj/LaunchScreen.storyboard b/ios/send-ios/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 00000000..f83f6fd5
--- /dev/null
+++ b/ios/send-ios/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/send-ios/Base.lproj/Main.storyboard b/ios/send-ios/Base.lproj/Main.storyboard
new file mode 100644
index 00000000..09ae84fd
--- /dev/null
+++ b/ios/send-ios/Base.lproj/Main.storyboard
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/send-ios/Info.plist b/ios/send-ios/Info.plist
new file mode 100644
index 00000000..16be3b68
--- /dev/null
+++ b/ios/send-ios/Info.plist
@@ -0,0 +1,45 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UIRequiredDeviceCapabilities
+
+ armv7
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+
+
diff --git a/ios/send-ios/ViewController.swift b/ios/send-ios/ViewController.swift
new file mode 100644
index 00000000..289f07c4
--- /dev/null
+++ b/ios/send-ios/ViewController.swift
@@ -0,0 +1,39 @@
+//
+// ViewController.swift
+// send-ios
+//
+// Created by Donovan Preston on 7/19/18.
+//
+
+import UIKit
+import WebKit
+
+class ViewController: UIViewController, WKScriptMessageHandler {
+ @IBOutlet var webView: WKWebView!
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ self.webView.frame = self.view.bounds
+ self.webView?.configuration.userContentController.add(self, name: "loaded")
+ self.webView?.configuration.userContentController.add(self, name: "copy")
+ if let url = Bundle.main.url(
+ forResource: "index",
+ withExtension: "html",
+ subdirectory: "assets") {
+ webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
+ }
+ }
+
+ public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
+ print("Message received: \(message.name) with body: \(message.body)")
+ UIPasteboard.general.string = "\(message.body)"
+ }
+
+ override func didReceiveMemoryWarning() {
+ super.didReceiveMemoryWarning()
+ // Dispose of any resources that can be recreated.
+ }
+
+
+}
+
diff --git a/ios/send-ios/assets/background_1.jpg b/ios/send-ios/assets/background_1.jpg
new file mode 100644
index 00000000..c92d3fb1
Binary files /dev/null and b/ios/send-ios/assets/background_1.jpg differ
diff --git a/ios/send-ios/assets/index.css b/ios/send-ios/assets/index.css
new file mode 100644
index 00000000..3dd18d2d
--- /dev/null
+++ b/ios/send-ios/assets/index.css
@@ -0,0 +1,84 @@
+body {
+ background: url('background_1.jpg');
+ display: flex;
+ flex-direction: row;
+ flex: auto;
+ justify-content: center;
+ align-items: center;
+ padding: 0 20px;
+ box-sizing: border-box;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+
+#striped {
+ background-image: repeating-linear-gradient(
+ 45deg,
+ white,
+ white 5px,
+ #ea000e 5px,
+ #ea000e 25px,
+ white 25px,
+ white 30px,
+ #0083ff 30px,
+ #0083ff 50px
+ );
+ height: 350px;
+ width: 480px;
+}
+
+#white {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ height: 100%;
+ background-color: white;
+ margin: 0 10px;
+ padding: 1px 10px 0 10px;
+}
+
+#label {
+ background: #0297f8;
+ border: 1px solid #0297f8;
+ color: white;
+ font-size: 24px;
+ font-weight: 500;
+ height: 60px;
+ width: 200px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+#input {
+ display: none;
+}
+
+#url {
+ flex: 1;
+ width: 100%;
+ height: 32px;
+ font-size: 24px;
+ margin-top: 1em;
+}
+
+.button {
+ flex: 1;
+ display: block;
+ background: #0297f8;
+ border: 1px solid #0297f8;
+ color: white;
+ font-size: 24px;
+ font-weight: 500;
+ width: 95%;
+ height: 32px;
+ margin-top: 1em;
+}
+
+#send-another {
+ margin-bottom: 1em;
+}
diff --git a/ios/send-ios/assets/index.html b/ios/send-ios/assets/index.html
new file mode 100644
index 00000000..4579be02
--- /dev/null
+++ b/ios/send-ios/assets/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+ Send
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/send-ios/help.html b/ios/send-ios/help.html
new file mode 100644
index 00000000..afbd39d5
--- /dev/null
+++ b/ios/send-ios/help.html
@@ -0,0 +1,5 @@
+
+
+ HELLO WORLD
+
+