feat: allow to preview notarization.

Often the user may want to see what the notarization result will look like without
spending time on the 2PC protocol. This adds a "Preview" menu item to allow to do that.
This commit is contained in:
themighty1
2022-01-20 17:39:14 +03:00
parent 63e877cba3
commit 03084cf6a5
23 changed files with 697 additions and 345 deletions

View File

@@ -13,6 +13,7 @@ import {Socket} from './Socket.js';
import {TLS, getExpandedKeys, decrypt_tls_responseV6} from './TLS.js';
import {verifyNotary, getURLFetcherDoc} from './oracles.js';
import {TLSNotarySession} from './TLSNotarySession.js';
import {TLSprobe} from './TLSprobe.js';
import {ProgressMonitor} from './ProgressMonitor.js';
import {FirstTimeSetup} from './FirstTimeSetup.js';
@@ -81,8 +82,8 @@ export class Main{
// Some preferences may not exist if we are upgrading from
// a previous PageSigner version. Create the preferences.
if (await getPref('firstTimeInitCompleted') === null){
await addNewPreference('firstTimeInitCompleted', false);
if (await getPref('firstTimeInitCompletedv2') === null){
await addNewPreference('firstTimeInitCompletedv2', false);
await addNewPreference('parsedCircuits', null);
}
if (await getPref('trustedOracle') === null){
@@ -331,8 +332,8 @@ export class Main{
}
async prepareNotarization(after_click) {
if (!this.trustedOracleReady) {
async prepareNotarization(after_click = false, isPreview = false) {
if (!isPreview && !this.trustedOracleReady) {
this.sendAlert({
title: 'PageSigner error.',
text: 'Cannot notarize because something is wrong with PageSigner server. Please try again later'
@@ -435,7 +436,7 @@ export class Main{
oBSH_details['requestBody'] = oBR_details.requestBody;
}
const rv = this.getHeaders(oBSH_details);
this.startNotarization(rv.headers, rv.server, rv.port)
this.startNotarization(rv.headers, rv.server, rv.port, isPreview)
.catch(err => {
console.log('Notarization aborted.', err);
console.trace();
@@ -527,20 +528,20 @@ export class Main{
console.log('ext got msg', data);
switch (data.message){
case 'rename':
await renameSession(data.args.dir, data.args.newname);
await renameSession(data.sid, data.newname);
this.sendSessions(await getAllSessions());
break;
case 'delete':
await deleteSession(data.args.dir);
await deleteSession(data.sid);
this.sendSessions(await getAllSessions());
break;
case 'import':
// data is js array
this.importPgsgAndShow(new Uint8Array(data.args.data));
this.importPgsgAndShow(new Uint8Array(data.data));
break;
case 'export':
this.sendToManager({'pgsg': JSON.stringify(await this.getPGSG(data.args.dir)),
'name': (await getSession(data.args.dir)).sessionName}, 'export');
this.sendToManager({'pgsg': JSON.stringify(await this.getPGSG(data.sid)),
'name': (await getSession(data.sid)).sessionName}, 'export');
break;
case 'notarize':
this.prepareNotarization(false);
@@ -548,6 +549,9 @@ export class Main{
case 'notarizeAfter':
this.prepareNotarization(true);
break;
case 'preview':
this.prepareNotarization(false, true);
break;
case 'manage':
this.openManager();
break;
@@ -557,19 +561,16 @@ export class Main{
case 'openLink1':
chrome.tabs.create({url: 'https://www.tlsnotary.org'});
break;
case 'donate link':
chrome.tabs.create({url: 'https://www.tlsnotary.org/#Donate'});
case 'showSession':
this.showSession(data.sid);
break;
case 'viewdata':
this.openViewer(data.args.dir);
case 'showDetails':
this.openDetails(data.sid);
break;
case 'viewraw':
this.openDetails(data.args.dir, false);
case 'showPreviewDetails':
this.openPreviewDetails(data.serverName, data.request, data.response);
break;
case 'raw editor':
this.openDetails(data.args.dir, true);
break;
case 'file picker':
case 'fileChooser':
this.openFileChooser();
break;
case 'openChromeExtensions':
@@ -582,7 +583,7 @@ export class Main{
this.openPythonScript();
break;
case 'pendingAction':
this.pendingAction = data.args;
this.pendingAction = data.action;
break;
case 'useNotaryNoSandbox':
this.useNotaryNoSandbox(data.IP);
@@ -592,10 +593,17 @@ export class Main{
}
}
async startNotarization(headers, server, port) {
// startNotarization starts a TLSNotary session, saves the session result
// and displays it to the user. If we are in the preview mode, then we send
// the request directly to the server and display the result.
async startNotarization(headers, server, port, isPreview=false) {
if (isPreview){
await this.startPreview(headers, server, port);
return;
}
this.notarization_in_progress = true;
this.pm.init();
this.isFirstTimeSetupNeeded = ! await getPref('firstTimeInitCompleted');
this.isFirstTimeSetupNeeded = ! await getPref('firstTimeInitCompletedv2');
chrome.runtime.sendMessage({
destination: 'popup',
message: 'notarization_in_progress',
@@ -606,7 +614,7 @@ export class Main{
console.time('setPref');
await setPref('parsedCircuits', obj);
console.timeEnd('setPref');
await setPref('firstTimeInitCompleted', true);
await setPref('firstTimeInitCompletedv2', true);
}
const circuits = await getPref('parsedCircuits');
const session = new TLSNotarySession(
@@ -624,6 +632,18 @@ export class Main{
this.showSession(date);
}
// startPreview does not perfrom a TLSNotary session but simply fetches the
// resource from the webserver and shows the user a preview of what the
// notarization result will look like, were the user to initiate a notarization.
// This is especially useful when the the user picks which headers/resources
// to include in the notarization and wants to have a quick preview.
async startPreview(headers, server, port) {
const preview = new TLSprobe(server, port, headers, globals.sessionOptions);
const response = await preview.start();
console.log('response was: ', response);
await this.openViewer(server, headers, response);
}
loadDefaultIcon(){
const url = chrome.extension.getURL('ui/img/icon.png');
chrome.browserAction.setIcon({path: url});
@@ -632,11 +652,13 @@ export class Main{
// opens a tab showing the session. sid is a unique session id
// creation time is sid.
async showSession (sid){
await this.openViewer(sid);
const data = await getSession(sid);
const blob = await getSessionBlob(sid);
if (data === null) {throw('failed to get index', sid);}
await this.openViewer(data.serverName, blob.request, blob.response, sid);
this.sendSessions( await getAllSessions()); // refresh manager
}
openPythonScript(){
const url = chrome.extension.getURL('pagesigner.py');
chrome.tabs.create({url: url}, function(t){
@@ -885,13 +907,10 @@ export class Main{
}
async openViewer(sid) {
const data = await getSession(sid);
const blob = await getSessionBlob(sid);
if (data === null) {throw('failed to get index', sid);}
const commonName = data.serverName;
const request = blob.request;
const response = blob.response;
// openViewer opens a new browser tab (or reuses the import tab, if importing
// happened), waits for the tab to fully load and sends data for the viewer
// to display.
async openViewer(serverName, request, response, sid) {
let tabId = null;// the id of the tab that we will be sending to
const url = chrome.extension.getURL('ui/html/viewer.html');
@@ -933,25 +952,32 @@ export class Main{
}
console.log('send to viewer');
// the tab is either an already opened import tab or a fully-loaded new viewer tab
// the tab is either an already opened import tab or a fully-loaded new viewer tab.
// if sid was not set, then this is a preview tab.
// We already checked that the new viewer's tab DOM was loaded. Proceed to send the data
chrome.runtime.sendMessage({
destination: 'viewer',
message: 'show',
tabId: tabId,
data: {
request: request,
response: response,
sessionId: sid,
serverName: commonName
}
request: request,
response: response,
sessionId: sid,
serverName: serverName
});
}
async openDetails(sid, isEditor) {
openPreviewDetails(serverName, request, response){
this.doOpenDetails(serverName, request, response);
}
async openDetails(sid){
const data = await getSession(sid);
const blob = await getSessionBlob(sid);
const url = chrome.extension.getURL('ui/html/rawviewer.html');
this.doOpenDetails(data.serverName, blob.request, blob.response, sid);
}
async doOpenDetails(serverName, request, response, sid) {
const url = chrome.extension.getURL('ui/html/detailsViewer.html');
let tabId = null; // id of the tab to which we will send the data
const myTabs = [];
@@ -964,21 +990,19 @@ export class Main{
await new Promise(function(resolve) {
chrome.tabs.create({url: url}, async function(t){
tabId = t.id;
await that.checkIfTabOpened(t, 'isRawViewer', myTabs);
await that.checkIfTabOpened(t, 'isDetailsViewer', myTabs);
resolve();
});
});
chrome.runtime.sendMessage({
destination: 'rawviewer',
message: isEditor ? 'edit' : 'show',
destination: 'detailsViewer',
message: 'show',
tabId: tabId,
data: {
request: blob.request,
response: blob.response,
sessionId: sid,
serverName: data.serverName
}
request: request,
response: response,
sessionId: sid,
serverName: serverName
});
}

View File

@@ -358,8 +358,7 @@ export class TLS {
return this.rsaSig;
}
// TODO this description is incorrect
// sendClientFinished accepts encrypted Client Finished (CF), auth tag for CF, verify_data for CF.
// sendClientFinished accepts encrypted Client Finished (CF).
// It then sends Client Key Exchange, Change Cipher Spec and encrypted Client Finished
async sendClientFinished(encCF, tagCF){
const cke_tls_record_header = new Uint8Array([0x16, 0x03, 0x03, 0x00, 0x46]); // Type: Handshake, Version: TLS 1.2, Length

View File

@@ -27,10 +27,8 @@ export class TLSNotarySession{
await this.twopc.init();
await this.tls.buildAndSendClientHello();
const serverEcPubkey = await this.tls.receiveAndParseServerHello();
const serverX = serverEcPubkey.slice(1, 33);
const serverY = serverEcPubkey.slice(33, 65);
if ( this.pm) this.pm.update('last_stage', {'current': 3, 'total': 10});
const [pmsShare, cpubBytes] = await this.twopc.getECDHShare(serverX, serverY);
const [pmsShare, cpubBytes] = await this.twopc.getECDHShare(serverEcPubkey);
if ( this.pm) this.pm.update('last_stage', {'current': 4, 'total': 10});
await this.tls.buildClientKeyExchange(cpubBytes);

204
core/TLSprobe.js Normal file
View File

@@ -0,0 +1,204 @@
/* global bcuNode, ECSimple */
import {TLS, decrypt_tls_responseV6} from './TLS.js';
var bcu;
if (typeof(window) !== 'undefined'){
import('./third-party/math.js').then((module) => {
bcu = module;
});
} else {
// we are in node. bcuNode must have been made global
bcu = bcuNode;
}
import {assert, concatTA, int2ba, str2ba, ba2int, getRandom, eq, sha256, ba2str}
from './utils.js';
// class TLSprobe is used when we want to give a preview of notarization.
// It creates a TLS connection and fetches a resource.
export class TLSprobe extends TLS{
constructor(server, port, request, sessionOptions){
super(server, port, request, sessionOptions);
}
async start(){
await super.buildAndSendClientHello();
const serverEcPubkey = await super.receiveAndParseServerHello();
const [pms, cpubBytes] = this.generatePMS(serverEcPubkey);
await super.buildClientKeyExchange(cpubBytes);
const [cr, sr] = await super.getRandoms();
const [cwk, swk, civ, siv, MS_CryptoKey] = await this.getExpandedKeys(pms, cr, sr);
const verifyData = await this.computeVerifyDataCF(MS_CryptoKey, super.getAllHandshakes());
const cf = concatTA(new Uint8Array([0x14, 0x00, 0x00, 0x0c]), verifyData);
super.updateAllHandshakes(cf);
const [encCF, tagCF] = await this.encryptClientFinished(cf, cwk, civ);
await super.sendClientFinished(encCF, tagCF);
const encSF = await super.receiveServerFinished();
await this.checkServerFinished(encSF, super.getAllHandshakes(), MS_CryptoKey, swk, siv);
const encReq = await this.encryptRequest(str2ba(this.headers), cwk, civ);
this.sendRequest([encReq]);
const serverRecords = await super.receiveServerResponse();
const plaintextRecs = await decrypt_tls_responseV6(serverRecords, swk, siv);
let plaintextFlat = '';
for (const rec of plaintextRecs){
plaintextFlat += ba2str(rec);
}
return plaintextFlat;
}
// serverEcPubkey is webserver's ephemeral pubkey from the Server Key Exchange
generatePMS(serverEcPubkey) {
const x = serverEcPubkey.slice(1, 33);
const y = serverEcPubkey.slice(33, 65);
this.secp256r1 = new ECSimple.Curve(
0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFCn,
0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604Bn,
0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551n,
(2n ** 224n) * (2n ** 32n - 1n) + 2n ** 192n + 2n ** 96n - 1n,
new ECSimple.ModPoint(
0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296n,
0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5n,
),
);
const Q_server = new ECSimple.ModPoint(ba2int(x), ba2int(y));
const d = bcu.randBetween(this.secp256r1.n - 1n, 1n);
const Q = this.secp256r1.multiply(this.secp256r1.g, d);
const ECpoint = this.secp256r1.multiply(Q_server, d);
const pms = ECpoint.x;
const pubBytes = concatTA(int2ba(Q.x, 32), int2ba(Q.y, 32));
return [int2ba(pms, 32), pubBytes];
}
async getExpandedKeys(preMasterSecret, cr, sr){
const Secret_CryptoKey = await crypto.subtle.importKey(
'raw',
preMasterSecret.buffer,
{name: 'HMAC', hash:'SHA-256'},
true,
['sign']);
// calculate Master Secret and expanded keys
const seed = concatTA(str2ba('master secret'), cr, sr);
const a0 = seed;
const a1 = new Uint8Array (await crypto.subtle.sign('HMAC', Secret_CryptoKey, a0.buffer));
const a2 = new Uint8Array (await crypto.subtle.sign('HMAC', Secret_CryptoKey, a1.buffer));
const p1 = new Uint8Array (await crypto.subtle.sign('HMAC', Secret_CryptoKey, concatTA(a1, seed).buffer));
const p2 = new Uint8Array (await crypto.subtle.sign('HMAC', Secret_CryptoKey, concatTA(a2, seed).buffer));
const ms = concatTA(p1, p2).slice(0, 48);
const MS_CryptoKey = await crypto.subtle.importKey('raw', ms.buffer, {name: 'HMAC', hash:'SHA-256'}, true, ['sign']);
// Expand keys
const eseed = concatTA(str2ba('key expansion'), sr, cr);
const ea0 = eseed;
const ea1 = new Uint8Array (await crypto.subtle.sign('HMAC', MS_CryptoKey, ea0.buffer));
const ea2 = new Uint8Array (await crypto.subtle.sign('HMAC', MS_CryptoKey, ea1.buffer));
const ep1 = new Uint8Array (await crypto.subtle.sign('HMAC', MS_CryptoKey, concatTA(ea1, eseed).buffer));
const ep2 = new Uint8Array (await crypto.subtle.sign('HMAC', MS_CryptoKey, concatTA(ea2, eseed).buffer));
const ek = concatTA(ep1, ep2).slice(0, 40);
// GCM doesnt need MAC keys
const client_write_key = ek.slice(0, 16);
const server_write_key = ek.slice(16, 32);
const client_write_IV = ek.slice(32, 36);
const server_write_IV = ek.slice(36, 40);
return [client_write_key, server_write_key, client_write_IV, server_write_IV, MS_CryptoKey];
}
// returns verify_data for Client Finished
async computeVerifyDataCF(MS_CryptoKey, allHandshakes){
const hshash = await sha256(allHandshakes);
const seed = concatTA(str2ba('client finished'), hshash);
const a0 = seed;
const a1 = new Uint8Array (await crypto.subtle.sign('HMAC', MS_CryptoKey, a0.buffer));
const p1 = new Uint8Array (await crypto.subtle.sign('HMAC', MS_CryptoKey, concatTA(a1, seed).buffer));
const verifyData = p1.slice(0, 12);
return verifyData;
}
// encrypt Client Finished and return the encrypted CF (without the 8-byte nonce)
// and the auth tag
async encryptClientFinished(cf, cwk, civ){
const explicit_nonce = int2ba(1, 8); //explicit nonce is hard-coded to 1 for now
//const explicit_nonce = getRandom(8);
const nonce = concatTA(civ, explicit_nonce);
const seq_num = 0;
const tmp = [];
tmp.push(...Array.from(int2ba(seq_num, 8)));
tmp.push(0x16); // type 0x16 = Handshake
tmp.push(0x03, 0x03); // TLS Version 1.2
tmp.push(0x00, 0x10); // 16 bytes of unencrypted data
//additional authenticated data
const aad = new Uint8Array(tmp);
const key = await crypto.subtle.importKey('raw', cwk.buffer, 'AES-GCM',
true, ['encrypt']);
const ciphertext = new Uint8Array(await crypto.subtle.encrypt({name: 'AES-GCM',
iv: nonce.buffer, additionalData: aad.buffer}, key, cf.buffer));
console.log('len in encryptClientFinished is ', ciphertext.length);
return [ciphertext.slice(0, -16), ciphertext.slice(-16)];
}
// encrypt Server Finished using the explicit_nonce
// return ciphertext (without the nonce) and the tag
async encryptServerFinished(sf, explicit_nonce, swk, siv){
const nonce = concatTA(siv, explicit_nonce);
const seq_num = 0;
const tmp = [];
tmp.push(...Array.from(int2ba(seq_num, 8)));
tmp.push(0x16, 0x03, 0x03); // Handshake, TLS Version 1.2
tmp.push(0x00, 0x10); // 16 bytes of unencrypted data
//additional authenticated data
const aad = new Uint8Array(tmp);
const key = await crypto.subtle.importKey('raw', swk.buffer, 'AES-GCM',
true, ['encrypt']);
const ciphertext = new Uint8Array(await crypto.subtle.encrypt({name: 'AES-GCM',
iv: nonce.buffer, additionalData: aad.buffer}, key, sf.buffer));
return [ciphertext.slice(0, -16), ciphertext.slice(-16)];
}
async checkServerFinished(encSF, allHandshakes, MS_CryptoKey, swk, siv){
const hshash = await sha256(allHandshakes);
const seed = concatTA(str2ba('server finished'), hshash);
const a0 = seed;
const a1 = new Uint8Array (await crypto.subtle.sign('HMAC', MS_CryptoKey, a0.buffer));
const p1 = new Uint8Array (await crypto.subtle.sign('HMAC', MS_CryptoKey, concatTA(a1, seed).buffer));
const verifyData = p1.slice(0, 12);
const sf = concatTA(new Uint8Array([0x14, 0x00, 0x00, 0x0c]), verifyData);
const nonceFromWire = encSF.slice(0, 8);
const [ct, tag] = await this.encryptServerFinished(sf, nonceFromWire, swk, siv);
assert(eq(concatTA(nonceFromWire, ct, tag), encSF));
}
async encryptRequest(req, cwk, civ){
const explicit_nonce = getRandom(8);
const nonce = concatTA(civ, explicit_nonce);
let seq_num = 1;
const tmp = [];
tmp.push(...Array.from(int2ba(seq_num, 8)));
tmp.push(0x17, 0x03, 0x03); // Application data, TLS Version 1.2
tmp.push(...Array.from(int2ba(req.length, 2))); // bytelength of unencrypted data
//additional authenticated data
const aad = new Uint8Array(tmp);
const key = await crypto.subtle.importKey('raw', cwk.buffer, 'AES-GCM',
true, ['encrypt']);
return concatTA(explicit_nonce, new Uint8Array(
await crypto.subtle.encrypt({name: 'AES-GCM', iv: nonce.buffer,
additionalData: aad.buffer}, key, req.buffer)));
}
sendRequest(records){
let appdata = new Uint8Array();
for (let i=0; i< records.length; i++){
appdata = concatTA(
appdata,
new Uint8Array([0x17, 0x03, 0x03]), // Type: Application data, TLS Version 1.2
int2ba(records[i].length, 2), // 2-byte length of encrypted data
records[i]);
}
console.log('sending http request');
this.sckt.send(appdata);
}
}

View File

@@ -2,8 +2,8 @@ export const globals = {
// defaultNotaryIP/Port is the default IP address/port of the notary server.
// If this IP address becomes unavailable, Pagesigner will query backupUrl for
// a new notary's IP address and will save the new IP address in the preferences.
//defaultNotaryIP: '127.0.0.1',
defaultNotaryIP: '18.207.130.193',
defaultNotaryIP: '127.0.0.1',
//defaultNotaryIP: '18.207.130.193',
defaultNotaryPort: 10011,
// backupUrl is the URL to query to get the IP address of another notary
// server in case if defaultNotaryIP is unreachable
@@ -26,6 +26,6 @@ export const globals = {
// if useNotaryNoSandbox is set to true, then we fetch notary's pubkey by
// querying /getPubKey and trust it. This is only useful when notary runs
// in a non-sandbox environment.
useNotaryNoSandbox: false
//useNotaryNoSandbox: true
//useNotaryNoSandbox: false
useNotaryNoSandbox: true
};

View File

@@ -12,6 +12,12 @@ import {OTSender} from './OTSender.js';
import {OTReceiver} from './OTReceiver.js';
import {GHASH} from './GHASH.js';
// class TWOPC implement two-party computation techniques used in the TLSNotary
// session. Paillier 2PC, Gabrled Circuits, Oblivious Transfer.
// The description of each step of the TLS PRF computation, both inside the
// garbled circuit and outside of it:
// [REF 1] https://github.com/tlsnotary/circuits/blob/master/README
export class TWOPC {
constructor(notary, plaintextLen, circuits, progressMonitor) {
@@ -38,10 +44,18 @@ export class TWOPC {
this.uid = Math.random().toString(36).slice(-10);
// cs is an array of circuits, where each circuit is an object with the fields:
// gatesBlob, gatesCount, wiresCount, notaryInputSize, clientInputSize,
// outputSize, andGateCount
// outputSize, andGateCount, outputsSizes
this.cs = Object.keys(circuits).map(k => circuits[k]);
// start count of circuits with 1, push an empy element to index 0
// start count of circuits with 1, push an empty element to index 0
this.cs.splice(0, 0, undefined);
// The output of a circuit is actually multiple concatenated values. We need
// to know how many bits each output value has in order to parse the output
this.cs[1]['outputsSizes'] = [256, 256];
this.cs[2]['outputsSizes'] = [256, 256];
this.cs[3]['outputsSizes'] = [128, 128, 32, 32, 128, 128, 128];
this.cs[4]['outputsSizes'] = [128, 128, 128, 96];
this.cs[5]['outputsSizes'] = [128];
this.cs[6]['outputsSizes'] = [128];
// output is the output of the circuit as array of bytes
this.output = Array(this.cs.length);
// Commits are used to ensure malicious security of garbled circuits
@@ -113,8 +127,11 @@ export class TWOPC {
}
}
async getECDHShare(x, y){
const paillier = new Paillier2PC(x, y);
// serverEcPubkey is webserver's ephemeral pubkey from the Server Key Exchange
async getECDHShare(serverEcPubkey){
const serverX = serverEcPubkey.slice(1, 33);
const serverY = serverEcPubkey.slice(33, 65);
const paillier = new Paillier2PC(serverX, serverY);
const step1Resp = await this.send('step1', paillier.step1());
const step2Promise = this.send('step2', paillier.step2(step1Resp), true);
// while Notary is responding to step2 we can do some more computations
@@ -141,25 +158,26 @@ export class TWOPC {
}
const c1Mask = getRandom(32);
const c1Out = await this.runCircuit([this.clientPMSShare, c1Mask], 1);
// unmask the output
const innerState1Masked = c1Out[0].slice(0, 32);
const innerState1 = xor(innerState1Masked, c1Mask);
// [REF 1] Step 2
const c1Out = (await this.runCircuit([this.clientPMSShare, c1Mask], 1))[0];
// unmask only the outputs relevant to the client
const pmsInnerHashState = xor(c1Out.slice(32, 64), c1Mask);
const [c2_p2, c2_p1inner] = await this.phase2(innerState1);
// [REF 1] Steps 3,5,7,9
const [c2_p2, c2_p1inner] = await this.phase2(pmsInnerHashState);
const c2Mask = getRandom(32);
const input2 = [c2_p1inner, c2_p2.subarray(0, 16), c2Mask];
const c2Out = await this.runCircuit(input2, 2);
// [REF 1] Step 10, 12
const c2Out = (await this.runCircuit(input2, 2))[0];
// unmask the output
const innerState2Masked = c2Out[0].slice(0, 32);
const innerState2 = xor(innerState2Masked, c2Mask);
const [c3_p1inner_vd, c3_p2inner, c3_p1inner] = await this.phase3(innerState2);
// for readability, mask indexing starts from 1
const masks3 = [0, 16, 16, 4, 4, 16, 16, 16, 12].map(x => getRandom(x));
const input3 = [c3_p1inner, c3_p2inner, c3_p1inner_vd, ...masks3.slice(1)];
const c3Out = await this.runCircuit(input3, 3);
const [encFinished, tag, verify_data] = await this.phase4(c3Out, masks3);
const msInnerHashState = xor(c2Out.slice(32, 64), c2Mask);
// [REF 1] Steps 13,15,17,20,22
const [verify_data, c3_p2inner, c3_p1inner] = await this.phase3(msInnerHashState);
// for readability, indexing of masks starts from 1
const masks3 = [0, 16, 16, 4, 4, 16, 16, 16].map(x => getRandom(x));
const input3 = [c3_p1inner, c3_p2inner, ...masks3.slice(1)];
const c3Out = (await this.runCircuit(input3, 3))[0];
const [encFinished, tag] = await this.phase4(c3Out, masks3, verify_data);
return [encFinished, tag, verify_data];
}
@@ -174,28 +192,31 @@ export class TWOPC {
const seed = concatTA(str2ba('server finished'), hshash);
const a0 = seed;
// [REF 1] Step 25
const a1inner = innerHash(this.innerState_MS, a0);
const a1 = await this.send('c4_pre1', a1inner);
// [REF 1] Step 27
const p1inner = innerHash(this.innerState_MS, concatTA(a1, seed));
// for readability, mask indexing starts from 1
const masks4 = [0, 16, 16, 16, 12].map(x => getRandom(x));
const input4 = [p1inner, this.swkShare, this.sivShare, sf_nonce,
...masks4.slice(1)];
const outArray = await this.runCircuit(input4, 4);
const c4_output = outArray[0];
console.log('c4Output.length', c4_output.length);
// [REF 1] Step 28
const c4out = (await this.runCircuit(input4, 4))[0];
let o = 0; // offset
const verify_dataMasked = c4_output.slice(o, o+=12);
const verify_data = xor(verify_dataMasked, masks4[4]);
const encCounterMasked = c4_output.slice(o, o+=16);
const encCounter = xor(encCounterMasked, masks4[3]);
const gctrSFMasked = c4_output.slice(o, o+=16);
const gctrShare = xor(gctrSFMasked, masks4[2]);
const H1MaskedTwice = c4_output.slice(o, o+=16);
// parse outputs
const H1MaskedTwice = c4out.slice(o, o+=16);
const gctrSFMasked = c4out.slice(o, o+=16);
const encCounterMasked = c4out.slice(o, o+=16);
const verify_dataMasked = c4out.slice(o, o+=12);
// unmask outputs
// H1 xor-masked by notary is our share of H^1, the mask is notary's share of H^1
const H1share = xor(H1MaskedTwice, masks4[1]);
const gctrShare = xor(gctrSFMasked, masks4[2]);
const encCounter = xor(encCounterMasked, masks4[3]);
const verify_data = xor(verify_dataMasked, masks4[4]);
const sf = concatTA(new Uint8Array([0x14, 0x00, 0x00, 0x0c]), verify_data);
const encSF = xor(sf, encCounter);
assert(eq(sf_pure, encSF));
@@ -230,10 +251,10 @@ export class TWOPC {
input5 = [].concat(input5, [this.cwkShare, this.civShare, masks5[i],
fixedNonce, 10, counter, 10]);
}
const outArray = await this.runCircuit(input5, 5);
const c5out = await this.runCircuit(input5, 5);
const encCounters = [];
for (let i=0; i < this.C5Count; i++){
encCounters.push(xor(outArray[i], masks5[i]));
encCounters.push(xor(c5out[i], masks5[i]));
}
return encCounters;
}
@@ -249,7 +270,7 @@ export class TWOPC {
masks6.push(getRandom(16));
input6 = [].concat(input6, [this.cwkShare, this.civShare, masks6[i], nonce]);
}
const outArray = await this.runCircuit(input6, 6);
const c6out = await this.runCircuit(input6, 6);
const c6CommitSalt = await this.send('checkC6Commit', this.myCommit[6]);
// the commit which notary computed on their side must be equal to our commit
const saltedCommit = await sha256(concatTA(this.myCommit[6], c6CommitSalt));
@@ -257,7 +278,7 @@ export class TWOPC {
const gctrBlocks = [];
for (let i=0; i < this.C6Count; i++){
gctrBlocks.push(xor(outArray[i], masks6[i]));
gctrBlocks.push(xor(c6out[i], masks6[i]));
}
return gctrBlocks;
}
@@ -537,7 +558,6 @@ export class TWOPC {
}
async phase2(innerStateUint8) {
const innerState = new Int32Array(8);
for (let i = 0; i < 8; i++) {
var hex = ba2hex(innerStateUint8).slice(i * 8, (i + 1) * 8);
@@ -554,14 +574,17 @@ export class TWOPC {
// ms = (p1+p2)[0:48]
const seed = concatTA(str2ba('master secret'), this.client_random, this.server_random);
// [REF 1] Step 3
const a1inner = innerHash(innerState, seed);
const a1 = await this.send('c1_step3', a1inner);
// [REF 1] Step 5
const a2inner = innerHash(innerState, a1);
const a2 = await this.send('c1_step4', a2inner);
// [REF 1] Step 7
const p2inner = innerHash(innerState, concatTA(a2, seed));
const p2 = await this.send('c1_step5', p2inner);
// [REF 1] Step 9
const p1inner = innerHash(innerState, concatTA(a1, seed));
return [p2, p1inner];
}
@@ -585,42 +608,43 @@ export class TWOPC {
const seed = concatTA(str2ba('key expansion'), this.server_random, this.client_random);
// at the same time also compute verify_data for Client Finished
const seed_vd = concatTA(str2ba('client finished'), this.hs_hash);
// [REF 1] Step 13
const a1inner = innerHash(this.innerState_MS, seed);
// [REF 1] Step 20
const a1inner_vd = innerHash(this.innerState_MS, seed_vd);
const resp4 = await this.send('c2_step3', concatTA(a1inner, a1inner_vd));
const a1 = resp4.subarray(0, 32);
const a1_vd = resp4.subarray(32, 64);
// [REF 1] Step 15
const a2inner = innerHash(this.innerState_MS, a1);
// [REF 1] Step 22
const p1inner_vd = innerHash(this.innerState_MS, concatTA(a1_vd, seed_vd));
const resp5 = await this.send('c2_step4', concatTA(a2inner, p1inner_vd));
const a2 = resp5.subarray(0, 32);
const verify_data = resp5.subarray(32, 44);
// [REF 1] Step 17
const p1inner = innerHash(this.innerState_MS, concatTA(a1, seed));
const p2inner = innerHash(this.innerState_MS, concatTA(a2, seed));
return [p1inner_vd, p2inner, p1inner];
return [verify_data, p2inner, p1inner];
}
async phase4(outArray, masks3) {
const c3_output = outArray[0];
async phase4(c3out, masks3, verify_data) {
let o = 0; // offset
const verify_dataMasked = c3_output.slice(o, o+=12);
const verify_data = xor(verify_dataMasked, masks3[8]);
const encCounterMasked = c3_output.slice(o, o+=16);
const encCounter = xor(encCounterMasked, masks3[7]);
const gctrMaskedTwice = c3_output.slice(o, o+=16);
const myGctrShare = xor(gctrMaskedTwice, masks3[6]);
const H1MaskedTwice = c3_output.slice(o, o+=16);
const civMaskedTwice = c3_output.slice(o, o+=4);
this.civShare = xor(civMaskedTwice, masks3[4]);
const sivMaskedTwice = c3_output.slice(o, o+=4);
this.sivShare = xor(sivMaskedTwice, masks3[3]);
const cwkMaskedTwice = c3_output.slice(o, o+=16);
const swkMaskedTwice = c3_output.slice(o, o+=16);
this.cwkShare = xor(cwkMaskedTwice, masks3[2]);
// parse all outputs
const swkMaskedTwice = c3out.slice(o, o+=16);
const cwkMaskedTwice = c3out.slice(o, o+=16);
const sivMaskedTwice = c3out.slice(o, o+=4);
const civMaskedTwice = c3out.slice(o, o+=4);
const H1MaskedTwice = c3out.slice(o, o+=16);
const gctrMaskedTwice = c3out.slice(o, o+=16);
const encCounterMasked = c3out.slice(o, o+=16);
// unmask all outputs
this.swkShare = xor(swkMaskedTwice, masks3[1]);
this.cwkShare = xor(cwkMaskedTwice, masks3[2]);
this.sivShare = xor(sivMaskedTwice, masks3[3]);
this.civShare = xor(civMaskedTwice, masks3[4]);
const myGctrShare = xor(gctrMaskedTwice, masks3[6]);
const encCounter = xor(encCounterMasked, masks3[7]);
// H1 xor-masked by notary is our (i.e. the client's) share of H^1,
// notary's mask is his share of H^1.
@@ -640,7 +664,7 @@ export class TWOPC {
const tagShare = this.ghash.processFinResponse(otResp, [aad, encCF, lenAlenC]);
// notary's gctr share is already included in notaryTagShare
const tagFromPowersOfH = xor(xor(notaryTagShare, tagShare), myGctrShare);
return [encCF, tagFromPowersOfH, verify_data];
return [encCF, tagFromPowersOfH];
}
// getClientFinishedResumed runs a circuit to obtain data needed to construct the
@@ -669,9 +693,10 @@ export class TWOPC {
return [this.ephemeralKey, this.eValidFrom, this.eValidUntil, this.eSigByMasterKey];
}
// eslint-disable-next-line class-methods-use-this
// optionally send extra data
async runCircuit(inputs, cNo, extraData = new Uint8Array(0)) {
// runCircuit evaluates a circuit and returns the circuit's output. It exchanges
// OT messages in order to get it's input labels and send to notary his input labels.
// The notary is also evaluating this circuit on his end.
async runCircuit(inputs, cNo) {
console.log('in runCircuit', cNo);
// inputs is an array of inputs in the order in which the inputs appear in the c*.casm files.
// The circuit expects the least bit of the input to be the first bit
@@ -688,12 +713,12 @@ export class TWOPC {
}
const c = this.cs[cNo];
// how many times to repeat evaluation (> 1 only for circuits 5&7 )
const repeatCount = [0, 1, 1, 1, 1, this.C5Count, 1, this.C6Count][cNo];
const repeatCount = [0, 1, 1, 1, 1, this.C5Count, this.C6Count][cNo];
// for circuit 1, there was no previous commit
const prevCommit = cNo > 1 ? this.myCommit[cNo-1] : new Uint8Array(0);
const otReq = this.otR.createRequest(inputBits);
const blob1 = await this.send(`c${cNo}_step1`, concatTA(prevCommit, otReq, extraData));
const blob1 = await this.send(`c${cNo}_step1`, concatTA(prevCommit, otReq));
let o = 0;
if (cNo > 1){
@@ -711,8 +736,8 @@ export class TWOPC {
assert(blob1.length == o);
const allClientLabels = this.otR.parseResponse(inputBits, hisOtResp);
const senderMsg = this.g.getNotaryLabels(cNo);
const otResp = this.otS.processRequest(hisOtReq, senderMsg);
const notaryLabels = this.g.getNotaryLabels(cNo);
const otResp = this.otS.processRequest(hisOtReq, notaryLabels);
const clientLabels = this.g.getClientLabels(inputBits, cNo);
const sendPromise = this.send(`c${cNo}_step2`, concatTA(otResp, clientLabels), true);
@@ -748,7 +773,9 @@ export class TWOPC {
console.log('evaluator output does not match the garbled outputs');
}
}
const out = bitsToBytes(bits);
// reverse output bits so that the values of the output be placed in
// the same order as they appear in the *.casm files
const out = this.parseOutputBits(cNo, bits);
this.output[cNo] = out;
output.push(out);
}
@@ -757,11 +784,21 @@ export class TWOPC {
const cStep2Out = await sendPromise;
this.hisSaltedCommit[cNo] = cStep2Out.subarray(0, 32);
const extraDataOut = cStep2Out.subarray(32);
output.push(extraDataOut);
return output;
}
// parseOutputBits converts the output bits of the circuit number "cNo" into
// a slice of output values in the same order as they appear in the *.casm files
parseOutputBits(cNo, outBits){
let o = 0; //offset
let outBytes = new Uint8Array(0);
for (const outSize of this.cs[cNo]['outputsSizes']){
outBytes = concatTA(outBytes, bitsToBytes(outBits.slice(o, o+=outSize)));
}
assert(o == this.cs[cNo].outputSize);
return outBytes;
}
// eslint-disable-next-line class-methods-use-this
processBlob(blob) {

31
ui/DetailsViewer.js Normal file
View File

@@ -0,0 +1,31 @@
/* global chrome*/
import {decode_str} from './utils.js';
// DetailsViewer show session details: raw request, response, notarization time
class DetailsViewer{
constructor(){
window.tabid = null; // allow the extension to put the id of the tab which opened this page
window.isDetailsViewer = true;
// isReady will be se to true after message listener is installed
window.isReady = false;
}
main(){
chrome.runtime.onMessage.addListener(function(obj) {
if (obj.destination !== 'detailsViewer') return;
console.log('got obj', obj);
if (obj.tabId != window.tabid) return;
document.getElementById('request').textContent = decode_str(obj.request);
document.getElementById('response').textContent = decode_str(obj.response);
if (obj.sessionId != undefined){
document.getElementById('notarization_time').textContent = obj.sessionId;
}
});
window.isReady = true;
}
}
window.detailsViewer = new DetailsViewer();
window.detailsViewer.main();

View File

@@ -25,11 +25,9 @@ export class FileChooser{
const import_label = document.getElementById('import_label');
import_label.classList.toggle('m-fadeOut');
chrome.runtime.sendMessage({
'destination': 'extension',
'message': 'import',
'args': {
'data': Array.from(new Uint8Array(e.target.result))
}
destination: 'extension',
message: 'import',
data: Array.from(new Uint8Array(e.target.result))
});
// don't close the window, we reuse it to display html
}

View File

@@ -11,7 +11,6 @@ class Manager{
window.isManager = true;
// isReady will be se to true after message listener is installed
window.isReady = false;
}
main() {
@@ -36,8 +35,8 @@ class Manager{
});
window.isReady = true;
chrome.runtime.sendMessage({
'destination': 'extension',
'message': 'refresh'
destination: 'extension',
message: 'refresh'
});
}
@@ -67,7 +66,7 @@ class Manager{
addRow(args) {
const that = this;
const session = args.creationTime;
const sid = args.creationTime;
const tb = document.getElementById('tableBody');
const row = tb.insertRow(tb.rows.length);
@@ -112,11 +111,9 @@ class Manager{
},
function() {
chrome.runtime.sendMessage({
'destination': 'extension',
'message': 'export',
'args': {
'dir': session
}
destination: 'extension',
message: 'export',
sid: sid
});
});
};
@@ -131,7 +128,7 @@ class Manager{
imgRen.style.marginLeft = 10;
imgRen.title = 'Give the session a more memorable name';
imgRen.onclick = function(event) {
that.doRename(event.target, session);
that.doRename(event.target, sid);
};
iconDiv.appendChild(imgRen);
@@ -150,11 +147,9 @@ class Manager{
},
function() {
chrome.runtime.sendMessage({
'destination': 'extension',
'message': 'delete',
'args': {
'dir': session
}
destination: 'extension',
message: 'delete',
sid: sid
});
});
};
@@ -181,11 +176,9 @@ class Manager{
input1.className = 'btn';
input1.onclick = function() {
chrome.runtime.sendMessage({
'destination': 'extension',
'message': 'viewdata',
'args': {
'dir': session
}
destination: 'extension',
message: 'showSession',
sid: sid
});
};
input1.value = 'HTML';
@@ -196,11 +189,9 @@ class Manager{
input2.className = 'btn';
input2.onclick = function() {
chrome.runtime.sendMessage({
'destination': 'extension',
'message': 'viewraw',
'args': {
'dir': session
}
destination: 'extension',
message: 'showDetails',
sid: sid
});
};
input2.value = 'Details';
@@ -211,11 +202,9 @@ class Manager{
input3.className = 'btn';
input3.onclick = function() {
chrome.runtime.sendMessage({
'destination': 'extension',
'message': 'raw editor',
'args': {
'dir': session
}
destination: 'extension',
message: 'raw editor',
sid: sid
});
};
input3.value = 'Edit';
@@ -237,7 +226,7 @@ class Manager{
row.appendChild(td3);
}
doRename(t, dir) {
doRename(t, sid) {
var isValid = (function() {
var rg1 = /^[^\\/:\*\?"<>\|]+$/; // forbidden characters \ / : * ? " < > |
var rg2 = /^\./; // cannot start with dot (.)
@@ -266,12 +255,10 @@ class Manager{
} else if (new_name === null) return; // escape pressed
else {
chrome.runtime.sendMessage({
'destination': 'extension',
'message': 'rename',
'args': {
'dir': dir,
'newname': new_name
}
destination: 'extension',
message: 'rename',
sid: sid,
newname: new_name
});
}
});

View File

@@ -3,8 +3,11 @@
export class NotificationBar{
constructor(){}
show(sessionId, serverName, hideButton) {
// show shows the notification bar at the top of the page.
show(sessionId, serverName, request, response, hideButton) {
hideButton = hideButton || false;
// if sessionId was not passed, it means we are in the preview mode
const isPreview = sessionId != undefined ? false : true;
const table = document.createElement('table');
table.style.position = 'fixed';
@@ -30,25 +33,40 @@ export class NotificationBar{
img.height = 24;
img.width = 24;
const text = document.createElement('text');
text.textContent = 'PageSigner verified that this page was received from ';
const domain = document.createElement('text');
domain.id = 'domainName';
domain.textContent = serverName;
if (isPreview){
text.textContent = 'This is a preview of what the notarization result will look like. Press Details to preview the raw HTML data.';
} else {
text.textContent = 'PageSigner verified that this page was received from ';
domain.textContent = serverName;
}
const button = document.createElement('button');
button.id = 'viewRaw';
button.textContent = 'Details';
button.style.MozBorderRadius = '4px';
button.style.WebkitBorderRadius = '4px';
button.style.borderRadius = '4px';
button.onclick = function() {
chrome.runtime.sendMessage({
destination: 'extension',
message: 'viewraw',
args: {
dir: sessionId
}
});
};
if (isPreview){
button.onclick = function() {
chrome.runtime.sendMessage({
destination: 'extension',
message: 'showPreviewDetails',
serverName: serverName,
request: request,
response: response
});
};
} else {
button.onclick = function() {
chrome.runtime.sendMessage({
destination: 'extension',
message: 'showDetails',
sid: sessionId
});
};
}
if (hideButton) {
button.hidden = true;
}

View File

@@ -24,8 +24,13 @@ class Popup{
document.getElementById('notarize').addEventListener('click', function(){
that.notarizeClicked(false);
});
document.getElementById('notarizeAfter').addEventListener('click', function(){
that.notarizeClicked(true);
document.getElementById('notarizeNow').addEventListener('click', function(){
that.notarizeNowClicked(false);
});
document.getElementById('preview').addEventListener('click', function(){
that.previewClicked(false);
});
document.getElementById('manage').addEventListener('click', function() {
@@ -39,7 +44,7 @@ class Popup{
document.getElementById('import').addEventListener('click', function() {
chrome.runtime.sendMessage({
destination: 'extension',
message: 'file picker'
message: 'fileChooser'
});
window.close();
});
@@ -67,8 +72,25 @@ class Popup{
});
}
// notarizeClicked is triggered when Notarize of Notariza after click was pressed
notarizeClicked(isAfterClick){
// notarizeClicked triggers a dropdown menu when "Notarize this page" was pressed
notarizeClicked(){
const nn = document.getElementById('notarizeNow');
const p = document.getElementById('preview');
const pe = document.getElementById('previewExplanation');
if (nn.hidden == true){
nn.hidden = false;
p.hidden = false;
pe.hidden = false;
} else {
nn.hidden = true;
p.hidden = true;
pe.hidden = true;
}
return;
}
// notarizeNowClicked is triggered when "Notarize now" was pressed
notarizeNowClicked(isAfterClick){
isAfterClick = isAfterClick || false;
const msg = isAfterClick ? 'notarizeAfter' : 'notarize';
if (this.is_firefox && ! this.hasPermission && this.currentUrl.startsWith('https://')){
@@ -81,7 +103,7 @@ class Popup{
chrome.runtime.sendMessage({
destination: 'extension',
message: 'pendingAction',
args: msg
action: msg
});
browser.permissions.request({origins: [this.currentUrl]});
}
@@ -96,6 +118,14 @@ class Popup{
}
}
previewClicked(){
chrome.runtime.sendMessage({
destination: 'extension',
message: 'preview'
});
window.close();
}
processMessages(data) {
console.log('popup got message', data);
if (data.destination !== 'popup') return;

View File

@@ -1,30 +0,0 @@
/* global chrome*/
import {decode_str} from './utils.js';
class RawViewer{
constructor(){
window.tabid = null; // allow the extension to put the id of the tab which opened this page
window.isRawViewer = true;
// isReady will be se to true after message listener is installed
window.isReady = false;
}
main(){
chrome.runtime.onMessage.addListener(function(obj) {
if (obj.destination !== 'rawviewer') return;
console.log('got obj', obj);
if (obj.tabId != window.tabid) return;
const request = decode_str(obj.data.request);
const response = decode_str(obj.data.response);
document.getElementById('request').textContent = request;
document.getElementById('response').textContent = response;
document.getElementById('notarization_time').textContent = obj.data.sessionId;
});
window.isReady = true;
}
}
window.rawViewer = new RawViewer();
window.rawViewer.main();

View File

@@ -34,13 +34,12 @@ class Viewer{
throw 'unexpected message';
}
console.log('got data in viewer');
var hideButton = false;
var text = msg.data.response;
console.log('text size is', text.length);
let hideButton = false;
console.log('text size is', msg.response.length);
// remove the HTTP headers
var http_body = text.split('\r\n\r\n').splice(1).join('\r\n\r\n');
let http_body = msg.response.split('\r\n\r\n').splice(1).join('\r\n\r\n');
let type = that.getType(text);
let type = that.getType(msg.response);
if (['html', 'json', 'xml', 'txt'].indexOf(type) > -1) {
// add CSP to prevent loading any resources from the page
const csp = '<meta http-equiv=\'Content-Security-Policy\' content="default-src \'none\'; img-src data:"></meta>\r\n';
@@ -53,9 +52,11 @@ class Viewer{
that.view_file(str2ba(http_body), msg.serverName + '.' + type);};
document.getElementById('view file').removeAttribute('hidden');
}
console.log('msg.data.sessionId', msg.sessionId);
setTimeout(function(){
// we need timeout because the body may not yet be available
new NotificationBar().show(msg.data.sessionId, msg.data.serverName, hideButton);
new NotificationBar().show(msg.sessionId, msg.serverName, msg.request,
msg.response, hideButton);
document.body.style.marginTop = '30px';
}, 1000);
});

View File

@@ -1,4 +1,4 @@
<script type="module" src="../RawViewer.js"></script>
<script type="module" src="../DetailsViewer.js"></script>
<style>
.btn{

View File

@@ -93,12 +93,12 @@
line-height: 20px;
color: white;
}
</style>
</head>
<body>
<div style='padding: 5px; word-wrap: break-word;' id='aboutWindow' hidden>
<h1 align="center">PageSigner v3.0</h1>
<p align="center">Email the developers: </p>
@@ -111,19 +111,31 @@
<h3>Please allow PageSigner to access this page.</h3>
</div>
<div id='managerTable'>
<table style="width: 16em;" id='menu' hidden>
<tr class="border_bottom">
<td class="pointer" id="notarize">
<img class='menu_img' src="../img/icon.svg"></img>
<text style='padding-right: 5px;'>Notarize this page</text>
</td>
<!-- notarizeAfter is not currently in use -->
<td id="notarizeAfter" hidden>
<img style="cursor:pointer;width:12px;height:12px;position:fixed;right:0px;top:2px" title="Click this tiny cursor icon if you want to make a click on the page first. As soon as you click on the page, PageSigner will start notarizing" class='menu_img'
src="../img/arrow16.png"></img>
</td>
<div id='menu' hidden>
<table style="width: 16em;">
<tr class="border_bottom">
<td class="pointer" id="notarize">
<img class='menu_img' src="../img/icon.svg"></img>
<text style='padding-right: 5px;'>Notarize this page</text>
</td>
</tr>
<tr class="border_bottom">
<td class="pointer" id="notarizeNow" hidden>
<img class='menu_img' src="../img/icon.svg"></img>
<text style='padding-right: 5px;'>Notarize now</text>
</td>
</tr>
<tr class="border_bottom">
<td class="pointer" id="preview" hidden>
<img class='menu_img' src="../img/preview.svg"></img>
<text style='padding-right: 5px;'>Preview</text>
</td>
</tr>
<tr>
<td id="previewExplanation" hidden>
<text style='padding-right: 5px;'>Select "Preview" if you do not want to perform the notarization right now, but instead want a quick preview of what the final result will look like.</text>
</td>
</tr>
<tr class="border_bottom">
<td class="pointer" id="manage">
<img class='menu_img' src="../img/files.svg"></img>
@@ -134,13 +146,15 @@
<td class="pointer" id="import">
<img class='menu_img' src="../img/import.svg"></img>
<text>Import session</text>
</td>
</tr>
<tr>
<td class="pointer" id="about">
<img class='menu_img' src="../img/icon.svg"></img>
<text>About</text>
</td>
</tr>
</table>
</table>
</div>
<div style="width: 14em;" id='appNotInstalledChrome' class='warning' hidden>

42
ui/img/SOURCES Normal file
View File

@@ -0,0 +1,42 @@
preview.svg
AUTHOR: salesforce
https://www.svgrepo.com/svg/375035/preview
LICENSE: Creative Commons Attribution License https://www.svgrepo.com/page/licensing creativecommons.org
unmodified
import.svg
AUTHOR: SVG Repo
https://www.svgrepo.com/svg/17434/import
LICENSE: CC0 License https://www.svgrepo.com/page/licensing creativecommons.org
unmodified
files.svg
AUTHOR: SVG Repo
https://www.svgrepo.com/svg/107125/files
LICENSE: CC0 License https://www.svgrepo.com/page/licensing creativecommons.org
unmodified
delete.svg
AUTHOR: SVG Repo
https://www.svgrepo.com/svg/162728/delete
LICENSE: CC0 License https://www.svgrepo.com/page/licensing creativecommons.org
unmodified
export.svg
AUTHOR: zurb
https://www.svgrepo.com/svg/373072/page-export
LICENSE: PD License https://www.svgrepo.com/page/licensing creativecommons.org
unmodified
rename.svg
AUTHOR: SVG Repo
https://www.svgrepo.com/svg/6755/edit
LICENSE: CC0 License https://www.svgrepo.com/page/licensing creativecommons.org
unmodified
Name the creator (either as identified on the work, or as noted in instructions to downstream users)
Provide a URL for the work (either as identified on the work, or as noted in instructions to downstream users)
Name the license
Provide a URL for the license
Note whether the work has been modified

View File

@@ -1 +1,45 @@
<svg height="512pt" viewBox="-57 0 512 512" width="512pt" xmlns="http://www.w3.org/2000/svg"><path d="m156.371094 30.90625h85.570312v14.398438h30.902344v-16.414063c.003906-15.929687-12.949219-28.890625-28.871094-28.890625h-89.632812c-15.921875 0-28.875 12.960938-28.875 28.890625v16.414063h30.90625zm0 0"/><path d="m344.210938 167.75h-290.109376c-7.949218 0-14.207031 6.78125-13.566406 14.707031l24.253906 299.90625c1.351563 16.742188 15.316407 29.636719 32.09375 29.636719h204.542969c16.777344 0 30.742188-12.894531 32.09375-29.640625l24.253907-299.902344c.644531-7.925781-5.613282-14.707031-13.5625-14.707031zm-219.863282 312.261719c-.324218.019531-.648437.03125-.96875.03125-8.101562 0-14.902344-6.308594-15.40625-14.503907l-15.199218-246.207031c-.523438-8.519531 5.957031-15.851562 14.472656-16.375 8.488281-.515625 15.851562 5.949219 16.375 14.472657l15.195312 246.207031c.527344 8.519531-5.953125 15.847656-14.46875 16.375zm90.433594-15.421875c0 8.53125-6.917969 15.449218-15.453125 15.449218s-15.453125-6.917968-15.453125-15.449218v-246.210938c0-8.535156 6.917969-15.453125 15.453125-15.453125 8.53125 0 15.453125 6.917969 15.453125 15.453125zm90.757812-245.300782-14.511718 246.207032c-.480469 8.210937-7.292969 14.542968-15.410156 14.542968-.304688 0-.613282-.007812-.921876-.023437-8.519531-.503906-15.019531-7.816406-14.515624-16.335937l14.507812-246.210938c.5-8.519531 7.789062-15.019531 16.332031-14.515625 8.519531.5 15.019531 7.816406 14.519531 16.335937zm0 0"/><path d="m397.648438 120.0625-10.148438-30.421875c-2.675781-8.019531-10.183594-13.429687-18.640625-13.429687h-339.410156c-8.453125 0-15.964844 5.410156-18.636719 13.429687l-10.148438 30.421875c-1.957031 5.867188.589844 11.851562 5.34375 14.835938 1.9375 1.214843 4.230469 1.945312 6.75 1.945312h372.796876c2.519531 0 4.816406-.730469 6.75-1.949219 4.753906-2.984375 7.300781-8.96875 5.34375-14.832031zm0 0"/></svg>
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 473 473" style="enable-background:new 0 0 473 473;" xml:space="preserve">
<g>
<path d="M324.285,215.015V128h20V38h-98.384V0H132.669v38H34.285v90h20v305h161.523c23.578,24.635,56.766,40,93.477,40
c71.368,0,129.43-58.062,129.43-129.43C438.715,277.276,388.612,222.474,324.285,215.015z M294.285,215.015
c-18.052,2.093-34.982,7.911-50,16.669V128h50V215.015z M162.669,30h53.232v8h-53.232V30z M64.285,68h250v30h-250V68z M84.285,128
h50v275h-50V128z M164.285,403V128h50v127.768c-21.356,23.089-34.429,53.946-34.429,87.802c0,21.411,5.231,41.622,14.475,59.43
H164.285z M309.285,443c-54.826,0-99.429-44.604-99.429-99.43s44.604-99.429,99.429-99.429s99.43,44.604,99.43,99.429
S364.111,443,309.285,443z"/>
<polygon points="342.248,289.395 309.285,322.358 276.323,289.395 255.11,310.608 288.073,343.571 255.11,376.533 276.323,397.746
309.285,364.783 342.248,397.746 363.461,376.533 330.498,343.571 363.461,310.608 "/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1 +0,0 @@
<svg id="Capa_1" enable-background="new 0 0 512 512" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><g><path d="m512 207.011-73.768-73.685-20.766 20.743v-59.065l-94.805-95.004h-322.661v512h417.466v-210.562zm-265.543 160.302 31.293 31.257-13.397 13.382h-31.293v-31.257zm52.529 10.045-31.292-31.258 109.094-108.971 31.293 31.258zm170.539-170.347-40.208 40.163-31.293-31.258 40.208-40.163zm-138.082-155.715 34.929 35.002h-34.929zm55.989 430.704h-357.398v-452h271.376v86.298h86.023v67.771l-184.407 184.199v73.684h73.767l110.639-110.514z"/><path d="m59.59 146.298h178.201v30h-178.201z"/><path d="m59.59 86.298h138.156v30h-138.156z"/></g></svg>

Before

Width:  |  Height:  |  Size: 673 B

View File

@@ -1,59 +1,17 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 366.999 366.999" style="enable-background:new 0 0 366.999 366.999;" xml:space="preserve">
<path id="XMLID_223_" d="M363.598,247.01c0.146-0.177,0.272-0.365,0.409-0.547c0.157-0.209,0.319-0.414,0.464-0.632
c0.145-0.216,0.27-0.441,0.402-0.662c0.118-0.198,0.243-0.392,0.352-0.596c0.121-0.225,0.223-0.458,0.332-0.688
c0.101-0.213,0.207-0.423,0.298-0.643c0.092-0.223,0.167-0.451,0.248-0.678c0.085-0.235,0.175-0.467,0.248-0.708
c0.068-0.226,0.118-0.454,0.176-0.683c0.062-0.246,0.131-0.49,0.181-0.741c0.052-0.261,0.082-0.524,0.12-0.788
c0.032-0.221,0.074-0.439,0.096-0.664c0.048-0.485,0.073-0.973,0.074-1.46c0-0.007,0.001-0.013,0.001-0.02
c0-0.008-0.001-0.017-0.001-0.025c0-0.486-0.025-0.971-0.073-1.455c-0.022-0.225-0.064-0.442-0.096-0.664
c-0.038-0.263-0.068-0.526-0.12-0.787c-0.05-0.253-0.12-0.499-0.182-0.747c-0.057-0.226-0.107-0.453-0.174-0.677
c-0.073-0.242-0.164-0.476-0.25-0.713c-0.081-0.225-0.155-0.452-0.246-0.673c-0.092-0.221-0.199-0.432-0.3-0.647
c-0.108-0.229-0.209-0.459-0.329-0.683c-0.11-0.206-0.236-0.401-0.355-0.6c-0.131-0.221-0.256-0.443-0.4-0.658
c-0.147-0.219-0.31-0.424-0.467-0.635c-0.136-0.182-0.262-0.368-0.407-0.544c-0.299-0.365-0.616-0.714-0.948-1.049
c-0.016-0.016-0.029-0.034-0.045-0.05l-37.499-37.501c-5.857-5.857-15.355-5.858-21.213-0.001c-5.858,5.858-5.858,15.355,0,21.213
l11.894,11.895L270,222.501v-78.605c0.003-0.133,0.02-0.263,0.02-0.396c0-3.606-1.287-6.903-3.407-9.49
c-0.021-0.026-0.042-0.053-0.064-0.079c-0.276-0.332-0.567-0.65-0.871-0.958c-0.043-0.044-0.087-0.089-0.131-0.133
c-0.132-0.131-0.255-0.272-0.393-0.398L155.609,22.896c-0.005-0.004-0.01-0.009-0.015-0.014c-0.307-0.306-0.627-0.593-0.955-0.868
c-0.104-0.087-0.212-0.169-0.318-0.253c-0.24-0.19-0.483-0.374-0.733-0.548c-0.125-0.088-0.251-0.174-0.379-0.258
c-0.263-0.172-0.53-0.333-0.802-0.487c-0.112-0.063-0.22-0.132-0.334-0.193c-0.363-0.194-0.733-0.372-1.109-0.534
c-0.154-0.067-0.311-0.124-0.467-0.186c-0.25-0.099-0.501-0.192-0.756-0.277c-0.175-0.058-0.35-0.114-0.527-0.166
c-0.289-0.084-0.581-0.158-0.875-0.225c-0.131-0.029-0.259-0.066-0.392-0.093c-0.42-0.084-0.844-0.146-1.27-0.193
c-0.13-0.015-0.262-0.023-0.393-0.035c-0.353-0.031-0.706-0.048-1.06-0.054C145.148,18.51,145.076,18.5,145,18.5H15
c-8.284,0-15,6.716-15,15v300c0,8.284,6.716,15,15,15h240c8.284,0,15-6.716,15-15v-80.999l45.786,0.001l-11.893,11.893
c-5.858,5.858-5.858,15.355,0,21.213c2.929,2.929,6.768,4.394,10.606,4.394s7.678-1.464,10.606-4.394l37.499-37.499
c0.008-0.008,0.014-0.016,0.021-0.023C362.968,247.742,363.292,247.383,363.598,247.01z M160,69.713l58.787,58.787H160V69.713z
M240,318.5H30v-270h100v95c0,8.284,6.716,15,15,15h95v64.001l-65.001-0.001c-8.284,0-15,6.716-15,15c0,8.284,6.716,15,15,15
L240,252.501V318.5z"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="100px" height="100px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve">
<g>
<path d="M94.284,65.553L75.825,52.411c-0.389-0.276-0.887-0.312-1.312-0.093c-0.424,0.218-0.684,0.694-0.685,1.173l0.009,6.221
H57.231c-0.706,0-1.391,0.497-1.391,1.204v11.442c0,0.707,0.685,1.194,1.391,1.194h16.774v6.27c0,0.478,0.184,0.917,0.609,1.136
c0.425,0.219,0.853,0.182,1.242-0.096l18.432-13.228c0.335-0.239,0.477-0.626,0.477-1.038c0-0.002,0-0.002,0-0.002
C94.765,66.179,94.621,65.793,94.284,65.553z"/>
<path d="M64.06,78.553h-6.49h0c-0.956,0-1.73,0.774-1.73,1.73h-0.007v3.01H15.191V36.16h17.723c0.956,0,1.73-0.774,1.73-1.73
V16.707h21.188l0,36.356h0.011c0.021,0.937,0.784,1.691,1.726,1.691h6.49c0.943,0,1.705-0.754,1.726-1.691h0.004v-0.038
c0,0,0-0.001,0-0.001l0-0.001l0-40.522h-0.005V8.48c0-0.956-0.774-1.73-1.73-1.73h-2.45v0H32.914v0h-1.73L5.235,32.7v2.447v1.013
v52.912v2.447c0,0.956,0.774,1.73,1.73,1.73h1.582h53.925h1.582c0.956,0,1.73-0.774,1.73-1.73v-2.448h0.005l0-8.789l0-0.001
C65.79,79.328,65.015,78.553,64.06,78.553z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,14 +1,17 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 469.333 469.333" style="enable-background:new 0 0 469.333 469.333;" xml:space="preserve">
<g>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 454.072 454.072" style="enable-background:new 0 0 454.072 454.072;" xml:space="preserve">
<g id="XMLID_7_">
<g>
<g>
<path d="M330.667,0h-256C51.093,0,32,19.093,32,42.667v298.667h42.667V42.667h256V0z"/>
<path d="M394.667,85.333H160c-23.573,0-42.667,19.093-42.667,42.667v298.667c0,23.573,19.093,42.667,42.667,42.667h234.667
c23.573,0,42.667-19.093,42.667-42.667V128C437.333,104.427,418.24,85.333,394.667,85.333z M394.667,426.667H160V128h234.667
V426.667z"/>
<path d="M364.639,0H192.671c-21.836,0-39.601,17.765-39.601,39.601v49.817h27.408V39.601c0-6.723,5.47-12.193,12.193-12.193
H364.64c6.723,0,12.193,5.47,12.193,12.193v265.335c0,6.723-5.47,12.192-12.193,12.192h-40.085v27.408h40.085
c21.836,0,39.6-17.765,39.6-39.6V39.601C404.239,17.765,386.475,0,364.639,0z"/>
<path d="M261.401,109.536H89.432c-21.836,0-39.6,17.765-39.6,39.601v265.334c0,21.836,17.764,39.601,39.6,39.601h171.969
c21.836,0,39.601-17.765,39.601-39.601V149.137C301.002,127.301,283.237,109.536,261.401,109.536z M273.594,414.471
L273.594,414.471c-0.001,6.723-5.471,12.193-12.194,12.193H89.432c-6.723,0-12.193-5.47-12.193-12.193V149.137
c0-6.723,5.47-12.193,12.193-12.193h171.969c6.723,0,12.193,5.47,12.193,12.193V414.471z"/>
</g>
</g>
</g>

Before

Width:  |  Height:  |  Size: 943 B

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,29 +1,16 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 330 330" style="enable-background:new 0 0 330 330;" xml:space="preserve">
<path id="XMLID_227_" d="M315,204h-45v-78.604c0.003-0.133,0.02-0.263,0.02-0.396c0-3.606-1.287-6.903-3.407-9.49
c-0.021-0.026-0.042-0.053-0.064-0.079c-0.276-0.332-0.567-0.65-0.871-0.958c-0.044-0.044-0.087-0.089-0.131-0.133
c-0.133-0.131-0.255-0.272-0.393-0.398L155.609,4.396c-0.004-0.004-0.01-0.009-0.015-0.014c-0.307-0.307-0.628-0.593-0.956-0.869
c-0.104-0.087-0.211-0.168-0.316-0.253c-0.241-0.191-0.485-0.375-0.736-0.55c-0.124-0.087-0.249-0.173-0.376-0.256
c-0.264-0.173-0.534-0.335-0.807-0.49c-0.11-0.063-0.217-0.131-0.33-0.191c-0.364-0.194-0.734-0.372-1.111-0.535
c-0.152-0.066-0.31-0.124-0.465-0.185c-0.25-0.099-0.502-0.192-0.757-0.277c-0.175-0.058-0.35-0.114-0.527-0.166
c-0.289-0.084-0.581-0.158-0.875-0.225c-0.131-0.029-0.259-0.066-0.392-0.093c-0.42-0.084-0.844-0.146-1.27-0.193
c-0.13-0.015-0.262-0.023-0.394-0.035c-0.352-0.031-0.705-0.048-1.06-0.054C145.148,0.01,145.076,0,145,0H15C6.716,0,0,6.716,0,15
v300c0,8.284,6.716,15,15,15h240c8.284,0,15-6.716,15-15v-81h45c8.284,0,15-6.716,15-15C330,210.716,323.284,204,315,204z
M160,51.213L218.787,110H160V51.213z M240,300H30V30h100v95c0,8.284,6.716,15,15,15h95v63.999l-58.786-0.001l11.893-11.892
c5.858-5.858,5.858-15.355,0-21.213c-5.858-5.857-15.355-5.858-21.213-0.001l-37.5,37.499c-0.056,0.056-0.104,0.118-0.158,0.175
c-0.285,0.294-0.564,0.593-0.823,0.909c-0.095,0.115-0.176,0.24-0.268,0.357c-0.209,0.27-0.419,0.539-0.609,0.823
c-0.067,0.1-0.122,0.206-0.187,0.307c-0.199,0.313-0.395,0.627-0.572,0.955c-0.039,0.073-0.07,0.15-0.108,0.224
c-0.186,0.362-0.365,0.728-0.522,1.105c-0.02,0.049-0.035,0.101-0.055,0.15c-0.162,0.402-0.313,0.81-0.44,1.229
c-0.017,0.055-0.027,0.111-0.043,0.166c-0.12,0.412-0.23,0.828-0.315,1.253c-0.025,0.124-0.036,0.251-0.057,0.376
c-0.062,0.357-0.125,0.714-0.161,1.08c-0.05,0.496-0.076,0.995-0.076,1.497c0,0.503,0.026,1.002,0.076,1.498
c0.036,0.365,0.099,0.722,0.161,1.08c0.021,0.124,0.032,0.251,0.057,0.374c0.086,0.431,0.197,0.852,0.319,1.268
c0.014,0.049,0.023,0.101,0.039,0.15c0.128,0.421,0.279,0.832,0.442,1.237c0.019,0.047,0.033,0.096,0.053,0.143
c0.159,0.382,0.339,0.752,0.528,1.118c0.036,0.069,0.065,0.142,0.102,0.211c0.178,0.333,0.377,0.652,0.58,0.969
c0.061,0.097,0.114,0.198,0.178,0.293c0.197,0.295,0.415,0.575,0.632,0.855c0.083,0.107,0.158,0.221,0.244,0.326
c0.288,0.35,0.594,0.685,0.912,1.007c0.024,0.025,0.046,0.053,0.071,0.078l37.5,37.501c2.93,2.929,6.768,4.394,10.607,4.394
c3.838,0,7.678-1.465,10.606-4.393c5.858-5.858,5.858-15.355,0-21.213l-11.894-11.895L240,233.999V300z"/>
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 60.903 60.903" style="enable-background:new 0 0 60.903 60.903;" xml:space="preserve">
<g>
<path d="M49.561,16.464H39.45v6h10.111c3.008,0,5.341,1.535,5.341,2.857v26.607c0,1.321-2.333,2.858-5.341,2.858H11.34
c-3.007,0-5.34-1.537-5.34-2.858V25.324c0-1.322,2.333-2.858,5.34-2.858h10.11v-6H11.34C4.981,16.466,0,20.357,0,25.324v26.605
c0,4.968,4.981,8.857,11.34,8.857h38.223c6.357,0,11.34-3.891,11.34-8.857V25.324C60.902,20.355,55.921,16.464,49.561,16.464z"/>
<path d="M39.529,29.004c-0.768,0-1.535,0.294-2.121,0.88l-3.756,3.755V20.612v-6V3.117c0-1.656-1.343-3-3-3s-3,1.344-3,3v11.494v6
v13.23l-3.959-3.958c-0.586-0.586-1.354-0.88-2.121-0.88s-1.535,0.294-2.121,0.88c-1.172,1.17-1.172,3.07,0,4.241l8.957,8.957
c0.586,0.586,1.354,0.877,2.12,0.877c0.008,0,0.016,0,0.023,0s0.015,0,0.022,0c0.768,0,1.534-0.291,2.12-0.877l8.957-8.957
c1.172-1.171,1.172-3.071,0-4.241C41.064,29.298,40.298,29.004,39.529,29.004z"/>
</g>
<g>
</g>
<g>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

8
ui/img/preview.svg Normal file
View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
width="52px" height="52px" viewBox="0 0 52 52" enable-background="new 0 0 52 52" xml:space="preserve">
<g>
<path d="M51.8,25.1C47.1,15.6,37.3,9,26,9S4.9,15.6,0.2,25.1c-0.3,0.6-0.3,1.3,0,1.8C4.9,36.4,14.7,43,26,43
s21.1-6.6,25.8-16.1C52.1,26.3,52.1,25.7,51.8,25.1z M26,37c-6.1,0-11-4.9-11-11s4.9-11,11-11s11,4.9,11,11S32.1,37,26,37z"/>
<path d="M26,19c-3.9,0-7,3.1-7,7s3.1,7,7,7s7-3.1,7-7S29.9,19,26,19z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 488 B

View File

@@ -1,21 +1,21 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 477.873 477.873" style="enable-background:new 0 0 477.873 477.873;" xml:space="preserve">
viewBox="0 0 490.337 490.337" style="enable-background:new 0 0 490.337 490.337;" xml:space="preserve">
<g>
<g>
<path d="M392.533,238.937c-9.426,0-17.067,7.641-17.067,17.067V426.67c0,9.426-7.641,17.067-17.067,17.067H51.2
c-9.426,0-17.067-7.641-17.067-17.067V85.337c0-9.426,7.641-17.067,17.067-17.067H256c9.426,0,17.067-7.641,17.067-17.067
S265.426,34.137,256,34.137H51.2C22.923,34.137,0,57.06,0,85.337V426.67c0,28.277,22.923,51.2,51.2,51.2h307.2
c28.277,0,51.2-22.923,51.2-51.2V256.003C409.6,246.578,401.959,238.937,392.533,238.937z"/>
</g>
</g>
<g>
<g>
<path d="M458.742,19.142c-12.254-12.256-28.875-19.14-46.206-19.138c-17.341-0.05-33.979,6.846-46.199,19.149L141.534,243.937
c-1.865,1.879-3.272,4.163-4.113,6.673l-34.133,102.4c-2.979,8.943,1.856,18.607,10.799,21.585
c1.735,0.578,3.552,0.873,5.38,0.875c1.832-0.003,3.653-0.297,5.393-0.87l102.4-34.133c2.515-0.84,4.8-2.254,6.673-4.13
l224.802-224.802C484.25,86.023,484.253,44.657,458.742,19.142z"/>
<path d="M229.9,145.379l-47.5,47.5c-17.5,17.5-35.1,35-52.5,52.7c-4.1,4.2-7.2,9.8-8.4,15.3c-6.3,28.9-12.4,57.8-18.5,86.7
l-3.4,16c-1.6,7.8,0.5,15.6,5.8,20.9c4.1,4.1,9.8,6.4,15.8,6.4c1.7,0,3.4-0.2,5.1-0.5l17.6-3.7c28-5.9,56.1-11.9,84.1-17.7
c6.5-1.4,12-4.3,16.7-9c78.6-78.7,157.2-157.3,235.8-235.8c5.8-5.8,9-12.7,9.8-21.2c0.1-1.4,0-2.8-0.3-4.1c-0.5-2-0.9-4.1-1.4-6.1
c-1.1-5.1-2.3-10.9-4.7-16.5l0,0c-14.7-33.6-39.1-57.6-72.5-71.1c-6.7-2.7-13.8-3.6-20-4.4l-1.7-0.2c-9-1.1-17.2,1.9-24.3,9.1
C320.4,54.879,275.1,100.179,229.9,145.379z M386.4,24.679c0.2,0,0.3,0,0.5,0l1.7,0.2c5.2,0.6,10,1.2,13.8,2.8
c27.2,11,47.2,30.6,59.3,58.2c1.4,3.2,2.3,7.3,3.2,11.6c0.3,1.6,0.7,3.2,1,4.8c-0.4,1.8-1.1,3-2.5,4.3
c-78.7,78.5-157.3,157.2-235.9,235.8c-1.3,1.3-2.5,1.9-4.3,2.3c-28.1,5.9-56.1,11.8-84.2,17.7l-14.8,3.1l2.8-13.1
c6.1-28.8,12.2-57.7,18.4-86.5c0.2-0.9,1-2.3,1.9-3.3c17.4-17.6,34.8-35.1,52.3-52.5l47.5-47.5c45.3-45.3,90.6-90.6,135.8-136
C384.8,24.979,385.7,24.679,386.4,24.679z"/>
<path d="M38.9,109.379h174.6c6.8,0,12.3-5.5,12.3-12.3s-5.5-12.3-12.3-12.3H38.9c-21.5,0-38.9,17.5-38.9,38.9v327.4
c0,21.5,17.5,38.9,38.9,38.9h327.3c21.5,0,38.9-17.5,38.9-38.9v-167.5c0-6.8-5.5-12.3-12.3-12.3s-12.3,5.5-12.3,12.3v167.5
c0,7.9-6.5,14.4-14.4,14.4H38.9c-7.9,0-14.4-6.5-14.4-14.4v-327.3C24.5,115.879,31,109.379,38.9,109.379z"/>
</g>
</g>
<g>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB