From d8fbab52bc6ed5fb94d7d6778a5328194534cf25 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Thu, 17 May 2018 12:32:05 -0500 Subject: [PATCH] Password wizard wip --- .../javascripts/app/controllers/footer.js | 7 + .../app/directives/views/accountMenu.js | 205 ++---------------- .../app/directives/views/passwordWizard.js | 165 ++++++++++++++ .../app/services/archiveManager.js | 117 ++++++++++ app/assets/stylesheets/app/_ui.scss | 4 + .../directives/account-menu.html.haml | 50 ++--- .../directives/password-wizard.html.haml | 65 ++++++ package-lock.json | 6 +- package.json | 2 +- 9 files changed, 398 insertions(+), 223 deletions(-) create mode 100644 app/assets/javascripts/app/directives/views/passwordWizard.js create mode 100644 app/assets/javascripts/app/services/archiveManager.js create mode 100644 app/assets/templates/directives/password-wizard.html.haml diff --git a/app/assets/javascripts/app/controllers/footer.js b/app/assets/javascripts/app/controllers/footer.js index 2721dcfc1..285a8aa47 100644 --- a/app/assets/javascripts/app/controllers/footer.js +++ b/app/assets/javascripts/app/controllers/footer.js @@ -162,5 +162,12 @@ angular.module('app') room.showRoom = !room.showRoom; } + // 002 Update + + this.securityUpdateAvailable = function() { + var keys = authManager.keys() + return keys && !keys.ak; + } + }); diff --git a/app/assets/javascripts/app/directives/views/accountMenu.js b/app/assets/javascripts/app/directives/views/accountMenu.js index 1344712e6..4ba475b5c 100644 --- a/app/assets/javascripts/app/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/directives/views/accountMenu.js @@ -9,13 +9,18 @@ class AccountMenu { }; } - controller($scope, $rootScope, authManager, modelManager, syncManager, dbManager, passcodeManager, $timeout, storageManager) { + controller($scope, $rootScope, authManager, modelManager, syncManager, dbManager, passcodeManager, + $timeout, storageManager, $compile, archiveManager) { 'ngInject'; $scope.formData = {mergeLocal: true, url: syncManager.serverURL, ephemeral: false}; $scope.user = authManager.user; $scope.server = syncManager.serverURL; + $timeout(() => { + $scope.openPasswordWizard("change-pw"); + }, 0) + $scope.close = function() { $timeout(() => { $scope.closeFunction()(); @@ -29,63 +34,6 @@ class AccountMenu { $scope.canAddPasscode = !authManager.isEphemeralSession(); $scope.syncStatus = syncManager.syncStatus; - $scope.newPasswordData = {}; - - $scope.showPasswordChangeForm = function() { - $scope.newPasswordData.showForm = true; - } - - $scope.submitPasswordChange = function() { - - let newPass = $scope.newPasswordData.newPassword; - - if(!newPass || newPass.length == 0) { - return; - } - - if(newPass != $scope.newPasswordData.newPasswordConfirmation) { - alert("Your new password does not match its confirmation."); - $scope.newPasswordData.status = null; - return; - } - - var email = $scope.user.email; - if(!email) { - alert("We don't have your email stored. Please log out then log back in to fix this issue."); - $scope.newPasswordData.status = null; - return; - } - - $scope.newPasswordData.status = "Generating New Keys..."; - $scope.newPasswordData.showForm = false; - - // perform a sync beforehand to pull in any last minutes changes before we change the encryption key (and thus cant decrypt new changes) - syncManager.sync(function(response){ - authManager.changePassword(email, newPass, function(response){ - if(response.error) { - alert("There was an error changing your password. Please try again."); - $scope.newPasswordData.status = null; - return; - } - - // re-encrypt all items - $scope.newPasswordData.status = "Re-encrypting all items with your new key..."; - - modelManager.setAllItemsDirty(); - syncManager.sync(function(response){ - if(response.error) { - alert("There was an error re-encrypting your items. Your password was changed, but not all your items were properly re-encrypted and synced. You should try syncing again. If all else fails, you should restore your notes from backup.") - return; - } - $scope.newPasswordData.status = "Successfully changed password and re-encrypted all items."; - $timeout(function(){ - alert("Your password has been changed, and your items successfully re-encrypted and synced. You must sign out of all other signed in applications and sign in again, or else you may corrupt your data.") - $scope.newPasswordData = {}; - }, 1000) - }); - }) - }, null, "submitPasswordChange") - } $scope.submitMfaForm = function() { var params = {}; @@ -204,6 +152,13 @@ class AccountMenu { } } + $scope.openPasswordWizard = function(type) { + var scope = $rootScope.$new(true); + scope.type = type; + var el = $compile( "" )(scope); + angular.element(document.body).append(el); + } + // Allows indexeddb unencrypted logs to be deleted // clearAllModels will remove data from backing store, but not from working memory // See: https://github.com/standardnotes/desktop/issues/131 @@ -345,142 +300,10 @@ class AccountMenu { Export */ - function loadZip(callback) { - if(window.zip) { - callback(); - return; - } - - var scriptTag = document.createElement('script'); - scriptTag.src = "/assets/zip/zip.js"; - scriptTag.async = false; - var headTag = document.getElementsByTagName('head')[0]; - headTag.appendChild(scriptTag); - scriptTag.onload = function() { - zip.workerScriptsPath = "assets/zip/"; - callback(); - } - } - - function downloadZippedNotes(notes) { - loadZip(function(){ - - zip.createWriter(new zip.BlobWriter("application/zip"), function(zipWriter) { - - var index = 0; - function nextFile() { - var note = notes[index]; - var blob = new Blob([note.text], {type: 'text/plain'}); - zipWriter.add(`${note.title}-${note.uuid}.txt`, new zip.BlobReader(blob), function() { - index++; - if(index < notes.length) { - nextFile(); - } else { - zipWriter.close(function(blob) { - downloadData(blob, `Notes Txt Archive - ${new Date()}.zip`) - zipWriter = null; - }); - } - }); - } - - nextFile(); - }, onerror); - }) - } - - var textFile = null; - - function hrefForData(data) { - // If we are replacing a previously generated file we need to - // manually revoke the object URL to avoid memory leaks. - if (textFile !== null) { - window.URL.revokeObjectURL(textFile); - } - - textFile = window.URL.createObjectURL(data); - - // returns a URL you can use as a href - return textFile; - } - - function downloadData(data, fileName) { - var link = document.createElement('a'); - link.setAttribute('download', fileName); - link.href = hrefForData(data); - document.body.appendChild(link); - link.click(); - link.remove(); - } - $scope.downloadDataArchive = function() { - // download in Standard File format - var keys, authParams, protocolVersion; - if($scope.archiveFormData.encrypted) { - if(authManager.offline() && passcodeManager.hasPasscode()) { - keys = passcodeManager.keys(); - authParams = passcodeManager.passcodeAuthParams(); - protocolVersion = authParams.version; - } else { - keys = authManager.keys(); - authParams = authManager.getAuthParams(); - protocolVersion = authManager.protocolVersion(); - } - } - var data = $scope.itemsData(keys, authParams, protocolVersion); - downloadData(data, `SN Archive - ${new Date()}.txt`); - - // download as zipped plain text files - if(!keys) { - var notes = modelManager.allItemsMatchingTypes(["Note"]); - downloadZippedNotes(notes); - } + archiveManager.downloadBackup($scope.archiveFormData.encrypted); } - $scope.itemsData = function(keys, authParams, protocolVersion) { - let data = modelManager.getAllItemsJSONData(keys, authParams, protocolVersion); - let blobData = new Blob([data], {type: 'text/json'}); - return blobData; - } - - - - // Advanced - - $scope.reencryptPressed = function() { - if(!confirm("Are you sure you want to re-encrypt and sync all your items? This is useful when updates are made to our encryption specification. You should have been instructed to come here from our website.")) { - return; - } - - if(!confirm("It is highly recommended that you download a backup of your data before proceeding. Press cancel to go back. Note that this procedure can take some time, depending on the number of items you have. Do not close the app during process.")) { - return; - } - - modelManager.setAllItemsDirty(); - syncManager.sync(function(response){ - if(response.error) { - alert("There was an error re-encrypting your items. You should try syncing again. If all else fails, you should restore your notes from backup.") - return; - } - - $timeout(function(){ - alert("Your items have been successfully re-encrypted and synced. You must sign out of all other signed in applications (mobile, desktop, web) and sign in again, or else you may corrupt your data.") - $scope.newPasswordData = {}; - }, 1000) - }, null, "reencryptPressed"); - - } - - - - // 002 Update - - $scope.securityUpdateAvailable = function() { - var keys = authManager.keys() - return keys && !keys.ak; - } - - /* Encryption Status */ diff --git a/app/assets/javascripts/app/directives/views/passwordWizard.js b/app/assets/javascripts/app/directives/views/passwordWizard.js new file mode 100644 index 000000000..7072f3b01 --- /dev/null +++ b/app/assets/javascripts/app/directives/views/passwordWizard.js @@ -0,0 +1,165 @@ +class PasswordWizard { + + constructor() { + this.restrict = "E"; + this.templateUrl = "directives/password-wizard.html"; + this.scope = { + type: "=" + }; + } + + controller($scope, modelManager, archiveManager, $timeout) { + 'ngInject'; + + const IntroStep = 0; + const BackupStep = 1; + const SignoutStep = 2; + const PasswordStep = 3; + const SyncStep = 4; + const FinishStep = 5; + + let DefaultContinueTitle = "Continue"; + $scope.continueTitle = DefaultContinueTitle; + + $scope.step = PasswordStep; + + $scope.titleForStep = function(step) { + switch (step) { + case BackupStep: + return "Download a backup of your data"; + case SignoutStep: + return "Sign out of all your devices"; + case PasswordStep: + return $scope.changePassword ? "Enter password information" : "Enter your current password"; + case SyncStep: + return "Encrypt and sync data with new keys"; + default: + return null; + } + } + + $scope.configure = function() { + if($scope.type == "change-pw") { + $scope.title = "Change Password"; + $scope.changePassword = true; + } else if($scope.type == "upgrade-security") { + $scope.title = "Security Update"; + $scope.securityUpdate = true; + } + }(); + + $scope.continue = function() { + let next = () => { + $scope.step += 1; + $scope.initializeStep($scope.step); + } + + var preprocessor = $scope.preprocessorForStep($scope.step); + if(preprocessor) { + preprocessor(() => { + next(); + }) + } else { + next(); + } + } + + $scope.downloadBackup = function(encrypted) { + archiveManager.downloadBackup(encrypted); + } + + $scope.preprocessorForStep = function(step) { + if(step == PasswordStep) { + return (callback) => { + $scope.showSpinner = true; + $scope.continueTitle = "Generating Keys..."; + $timeout(() => { + $scope.validatePasswordInformation(() => { + $scope.showSpinner = false; + $scope.continueTitle = DefaultContinueTitle; + callback(); + }); + }) + } + } + } + + $scope.initializeStep = function(step) { + if(step == SyncStep) { + $scope.lockContinue = true; + $scope.resyncData(() => { + $scope.lockContinue = false; + }) + } + } + + $scope.validatePasswordInformation = function(callback) { + $timeout(() => { + callback(); + }, 1000) + } + + $scope.resyncData = function(callback) { + $timeout(() => { + callback(); + }, 2000) + } + + $scope.submitPasswordChange = function() { + + let newPass = $scope.newPasswordData.newPassword; + let currentPass = $scope.newPasswordData.currentPassword; + + if(!newPass || newPass.length == 0) { + return; + } + + if(newPass != $scope.newPasswordData.newPasswordConfirmation) { + alert("Your new password does not match its confirmation."); + $scope.newPasswordData.status = null; + return; + } + + var email = $scope.user.email; + if(!email) { + alert("We don't have your email stored. Please log out then log back in to fix this issue."); + $scope.newPasswordData.status = null; + return; + } + + $scope.newPasswordData.status = "Generating New Keys..."; + $scope.newPasswordData.showForm = false; + + // perform a sync beforehand to pull in any last minutes changes before we change the encryption key (and thus cant decrypt new changes) + syncManager.sync(function(response){ + authManager.changePassword(email, currentPass, newPass, function(response){ + if(response.error) { + alert("There was an error changing your password. Please try again."); + $scope.newPasswordData.status = null; + return; + } + + // re-encrypt all items + $scope.newPasswordData.status = "Re-encrypting all items with your new key..."; + + modelManager.setAllItemsDirty(); + syncManager.sync(function(response){ + if(response.error) { + alert("There was an error re-encrypting your items. Your password was changed, but not all your items were properly re-encrypted and synced. You should try syncing again. If all else fails, you should restore your notes from backup.") + return; + } + $scope.newPasswordData.status = "Successfully changed password and re-encrypted all items."; + $timeout(function(){ + alert("Your password has been changed, and your items successfully re-encrypted and synced. You must sign out of all other signed in applications and sign in again, or else you may corrupt your data.") + $scope.newPasswordData = {}; + }, 1000) + }); + }) + }, null, "submitPasswordChange") + } + + } + +} + +angular.module('app').directive('passwordWizard', () => new PasswordWizard); diff --git a/app/assets/javascripts/app/services/archiveManager.js b/app/assets/javascripts/app/services/archiveManager.js new file mode 100644 index 000000000..cc0bf7dc7 --- /dev/null +++ b/app/assets/javascripts/app/services/archiveManager.js @@ -0,0 +1,117 @@ +class ArchiveManager { + + constructor(passcodeManager, authManager, modelManager) { + this.passcodeManager = passcodeManager; + this.authManager = authManager; + this.modelManager = modelManager; + } + + /* + Public + */ + + downloadBackup(encrypted) { + // download in Standard File format + var keys, authParams, protocolVersion; + if(encrypted) { + if(this.authManager.offline() && this.passcodeManager.hasPasscode()) { + keys = this.passcodeManager.keys(); + authParams = this.passcodeManager.passcodeAuthParams(); + protocolVersion = authParams.version; + } else { + keys = this.authManager.keys(); + authParams = this.authManager.getAuthParams(); + protocolVersion = this.authManager.protocolVersion(); + } + } + var data = this.__itemsData(keys, authParams, protocolVersion); + this.__downloadData(data, `SN Archive - ${new Date()}.txt`); + + // download as zipped plain text files + if(!keys) { + var notes = this.modelManager.allItemsMatchingTypes(["Note"]); + this.__downloadZippedNotes(notes); + } + } + + /* + Private + */ + + __itemsData(keys, authParams, protocolVersion) { + let data = this.modelManager.getAllItemsJSONData(keys, authParams, protocolVersion); + let blobData = new Blob([data], {type: 'text/json'}); + return blobData; + } + + __loadZip(callback) { + if(window.zip) { + callback(); + return; + } + + var scriptTag = document.createElement('script'); + scriptTag.src = "/assets/zip/zip.js"; + scriptTag.async = false; + var headTag = document.getElementsByTagName('head')[0]; + headTag.appendChild(scriptTag); + scriptTag.onload = function() { + zip.workerScriptsPath = "assets/zip/"; + callback(); + } + } + + __downloadZippedNotes(notes) { + this.__loadZip(() => { + zip.createWriter(new zip.BlobWriter("application/zip"), (zipWriter) => { + var index = 0; + + let nextFile = () => { + var note = notes[index]; + var blob = new Blob([note.text], {type: 'text/plain'}); + + zipWriter.add(`${note.title}-${note.uuid}.txt`, new zip.BlobReader(blob), () => { + index++; + if(index < notes.length) { + nextFile(); + } else { + zipWriter.close((blob) => { + this.__downloadData(blob, `Notes Txt Archive - ${new Date()}.zip`) + zipWriter = null; + }); + } + }); + } + + nextFile(); + }, onerror); + }) + } + + + __hrefForData(data) { + // If we are replacing a previously generated file we need to + // manually revoke the object URL to avoid memory leaks. + if (this.textFile !== null) { + window.URL.revokeObjectURL(this.textFile); + } + + this.textFile = window.URL.createObjectURL(data); + + // returns a URL you can use as a href + return this.textFile; + } + + __downloadData(data, fileName) { + var link = document.createElement('a'); + link.setAttribute('download', fileName); + link.href = this.__hrefForData(data); + document.body.appendChild(link); + link.click(); + link.remove(); + } + + +} + +angular.module('app').service('archiveManager', ArchiveManager); diff --git a/app/assets/stylesheets/app/_ui.scss b/app/assets/stylesheets/app/_ui.scss index 91b0be017..f952bb177 100644 --- a/app/assets/stylesheets/app/_ui.scss +++ b/app/assets/stylesheets/app/_ui.scss @@ -82,6 +82,10 @@ $screen-md-max: ($screen-lg-min - 1) !default; display: block !important; } +.inline { + display: inline-block; +} + .wrap { word-wrap: break-word; word-break: break-all; diff --git a/app/assets/templates/directives/account-menu.html.haml b/app/assets/templates/directives/account-menu.html.haml index 12129291a..477915a79 100644 --- a/app/assets/templates/directives/account-menu.html.haml +++ b/app/assets/templates/directives/account-menu.html.haml @@ -74,35 +74,29 @@ .panel-row - %a.panel-row.condensed{"ng-click" => "newPasswordData.changePassword = !newPasswordData.changePassword"} Change Password - .notification.warning{"ng-if" => "newPasswordData.changePassword"} - %h1.title Change Password - .text - %p Since your encryption key is based on your password, changing your password requires all your notes and tags to be re-encrypted using your new key. - %p If you have thousands of items, this can take several minutes — you must keep the application window open during this process. - %p After changing your password, you must log out of all other applications currently signed in to your account. - %p.bold It is highly recommended you download a backup of your data before proceeding. - .panel-row{"ng-if" => "!newPasswordData.status"} - .horizontal-group{"ng-if" => "!newPasswordData.showForm"} - %a.red{"ng-click" => "showPasswordChangeForm()"} Continue - %a{"ng-click" => "newPasswordData.changePassword = false; newPasswordData.showForm = false"} Cancel - .panel-row{"ng-if" => "newPasswordData.showForm"} - %form.panel-form.stretch - %input{:type => 'password', "ng-model" => "newPasswordData.newPassword", "placeholder" => "Enter new password"} - %input{:type => 'password', "ng-model" => "newPasswordData.newPasswordConfirmation", "placeholder" => "Confirm new password"} - .button-group.stretch.panel-row.form-submit - .button.info{"type" => "submit", "ng-click" => "submitPasswordChange()"} - .label Submit - %a{"ng-click" => "newPasswordData.changePassword = false; newPasswordData.showForm = false"} Cancel - - %p.italic.mt-10{"ng-if" => "newPasswordData.status"} {{newPasswordData.status}} - - - - %a.panel-row.condensed{"ng-click" => "showAdvanced = !showAdvanced"} Advanced - %div{"ng-if" => "showAdvanced"} - %a.panel-row{"ng-click" => "reencryptPressed()"} Resync All Items + %a.panel-row.condensed{"ng-click" => "openPasswordWizard('change-pw')"} Change Password + -# .notification.warning{"ng-if" => "newPasswordData.changePassword"} + -# %h1.title Change Password + -# .text + -# %p Since your encryption key is based on your password, changing your password requires all your notes and tags to be re-encrypted using your new key. + -# %p If you have thousands of items, this can take several minutes — you must keep the application window open during this process. + -# %p After changing your password, you must log out of all other applications currently signed in to your account. + -# %p.bold It is highly recommended you download a backup of your data before proceeding. + -# .panel-row{"ng-if" => "!newPasswordData.status"} + -# .horizontal-group{"ng-if" => "!newPasswordData.showForm"} + -# %a.red{"ng-click" => "showPasswordChangeForm()"} Continue + -# %a{"ng-click" => "newPasswordData.changePassword = false; newPasswordData.showForm = false"} Cancel + -# .panel-row{"ng-if" => "newPasswordData.showForm"} + -# %form.panel-form.stretch + -# %input{:type => 'password', "ng-model" => "newPasswordData.newPassword", "placeholder" => "Enter new password"} + -# %input{:type => 'password', "ng-model" => "newPasswordData.newPasswordConfirmation", "placeholder" => "Confirm new password"} + -# .button-group.stretch.panel-row.form-submit + -# .button.info{"type" => "submit", "ng-click" => "submitPasswordChange()"} + -# .label Submit + -# %a{"ng-click" => "newPasswordData.changePassword = false; newPasswordData.showForm = false"} Cancel + -# + -# %p.italic.mt-10{"ng-if" => "newPasswordData.status"} {{newPasswordData.status}} %a.panel-row.condensed{"ng-if" => "securityUpdateAvailable()", "ng-click" => "clickedSecurityUpdate()"} Security Update Available .notification.default{"ng-if" => "securityUpdateData.showForm"} diff --git a/app/assets/templates/directives/password-wizard.html.haml b/app/assets/templates/directives/password-wizard.html.haml new file mode 100644 index 000000000..27d0dc2a1 --- /dev/null +++ b/app/assets/templates/directives/password-wizard.html.haml @@ -0,0 +1,65 @@ +.modal.medium + .content + .sn-component + .panel + .header + %h1.title {{title}} + %a.close-button{"ng-click" => "close()"} Close + .content + + %div{"ng-if" => "step == 0"} + %div{"ng-if" => "changePassword"} + %p Since your encryption key is based on your password, changing your password requires all your notes and tags to be re-encrypted using your new key. + %p If you have thousands of items, this can take several minutes—you must keep the application window open during this process. + %div{"ng-if" => "securityUpdate"} + %p Welcome to the security update process. + + %p.info Press Continue to proceed. + + .panel-section{"ng-if" => "step > 0"} + + %h3.title.panel-row Step {{step}} — {{titleForStep(step)}} + + %div{"ng-if" => "step == 1"} + %p.panel-row + The entirety of your data will be re-encrypted and re-uploaded to your account. This is a generally safe process, + but unforeseen factors like poor network connectivity or a sudden shutdown of your computer may cause this process to fail. + It's best to be on the safe side before large operations like this. + .panel-row + .button-group + .button.info{"ng-click" => "downloadBackup(true)"} + .label Download Encrypted Backup + .button.info{"ng-click" => "downloadBackup(false)"} + .label Download Decrypted Backup + + %div{"ng-if" => "step == 2"} + %p.panel-row + As a result of this process, your encryption keys will change. + Any devices on which you use Standard Notes will need to end their session. After this process completes, you'll be asked to sign back in. + + %p.bold.panel-row Please sign out of all applications (excluding this one), including desktop, web, and mobile (iOS and Android). + %p.panel-row Press Continue only when you have completed signing out of all your devices. + + + %div{"ng-if" => "step == 3"} + %div{"ng-if" => "changePassword"} + %div{"ng-if" => "securityUpdate"} + %p.panel-row Enter your current password. We'll run this through our encryption scheme to generate strong new encryption keys. + + .panel-row + %form + %input.form-control{:type => 'password', "ng-model" => "formData.currentPassword", "placeholder" => "Current Password", "sn-autofocus" => "true", "should-focus" => "true"} + + %input.form-control{"ng-if" => "changePassword", :type => 'password', "ng-model" => "formData.newPassword", "placeholder" => "New Password"} + %input.form-control{"ng-if" => "changePassword", :type => 'password', "ng-model" => "formData.newPasswordConfirmation", "placeholder" => "Confirm New Password"} + + %div{"ng-if" => "step == 4"} + %p.panel-row + Your data is being re-encrypted with your new keys and synced to your account. + %p.panel-row.danger + Do not close this window until this process completes. + + .footer + %a.right{"ng-click" => "continue()", "ng-class" => "{'disabled' : lockContinue}"} + .spinner.small.inline.info{"ng-if" => "showSpinner", "style" => "margin-right: 5px;"} + {{continueTitle}} diff --git a/package-lock.json b/package-lock.json index e9e909042..78566d128 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5785,9 +5785,9 @@ "dev": true }, "sn-stylekit": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/sn-stylekit/-/sn-stylekit-1.0.12.tgz", - "integrity": "sha512-CkCGj6pS2FB9tF5uVbqMlmKpQ1wJFCcXAri2B4nCq8aAv2D5frUhhPFJ1oCnFgBssB9LxnbW2+C2CQojVCL9ig==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/sn-stylekit/-/sn-stylekit-1.0.14.tgz", + "integrity": "sha512-0jx2hJOw8Qer/aqcyXSses5g1m+nNDtDkwFkonolyVheTyhJXZrCw4kIIV9tbeOspwqsVjR12pk7L+R2mnl61Q==", "dev": true }, "snake-case": { diff --git a/package.json b/package.json index fa7121001..45b302d31 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "karma-cli": "^1.0.1", "karma-jasmine": "^1.1.0", "karma-phantomjs-launcher": "^1.0.2", - "sn-stylekit": "1.0.12", + "sn-stylekit": "1.0.14", "standard-file-js": "file:~/Desktop/sf/sfjs" }, "license": "GPL-3.0"