Ui/transit key actions textareas (#8462)

* feat: update transit textareas to codeblocks & automatically encode plaintext to base64 unless marked as encoded
This commit is contained in:
Chelsea Shaw 2020-03-04 11:36:37 -06:00 committed by GitHub
parent c7986d119c
commit 6f6f1d9fd8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 230 additions and 62 deletions

View File

@ -12,6 +12,7 @@
/.storybook/
/.yarn/
/stories/
/storybook-static/
# misc
/coverage/

View File

@ -4,6 +4,7 @@ import { assert } from '@ember/debug';
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { set, get, computed } from '@ember/object';
import { encodeString } from 'vault/utils/b64';
const TRANSIT_PARAMS = {
hash_algorithm: 'sha2-256',
@ -25,6 +26,7 @@ const TRANSIT_PARAMS = {
random_bytes: null,
signature: null,
sum: null,
encodedBase64: false,
exportKeyType: null,
exportKeyVersion: null,
wrappedToken: null,
@ -185,7 +187,16 @@ export default Component.extend(TRANSIT_PARAMS, {
doSubmit(data, options = {}) {
const { backend, id } = this.getModelInfo();
const action = this.get('selectedAction');
let payload = data ? this.compactData(data) : null;
const { encodedBase64, ...formData } = data || {};
if (!encodedBase64) {
if (action === 'encrypt' && !!formData.plaintext) {
formData.plaintext = encodeString(formData.plaintext);
}
if ((action === 'hmac' || action === 'verify') && !!formData.input) {
formData.input = encodeString(formData.input);
}
}
let payload = formData ? this.compactData(formData) : null;
this.setProperties({
errors: null,
result: null,

View File

@ -5,8 +5,18 @@
<label for="plaintext" class="is-label">
Plaintext
</label>
<div class="control is-relative">
{{textarea id="plaintext" value=plaintext readonly=true class="textarea" data-test-transit-input="plaintext"}}
<div id="plaintext-control" class="control is-relative">
{{ivy-codemirror
value=plaintext
options=(hash
readOnly=true
lineNumbers=true
tabSize=2
mode='ruby'
theme='hashi'
)
data-test-transit-input="plaintext"
}}
{{b64-toggle value=plaintext isInput=false initialEncoding="base64" data-test-transit-b64-toggle="plaintext"}}
</div>
</div>
@ -32,8 +42,18 @@
<div class="box is-sideless is-fullwidth is-marginless">
<div class="field">
<label for="ciphertext" class="is-label">Ciphertext</label>
<div class="control">
{{textarea id="ciphertext" name="ciphertext" value=ciphertext class="textarea" data-test-transit-input="ciphertext"}}
<div id="ciphertext-control" class="control">
{{ivy-codemirror
value=ciphertext
valueUpdated=(action (mut ciphertext))
options=(hash
lineNumbers=true
tabSize=2
mode='ruby'
theme='hashi'
)
data-test-transit-input="ciphertext"
}}
</div>
</div>
{{#if key.derived}}

View File

@ -1,17 +1,27 @@
<form {{action 'doSubmit' (hash plaintext=plaintext context=context nonce=nonce key_version=key_version) on="submit"}}>
<form {{action 'doSubmit' (hash plaintext=plaintext context=context nonce=nonce key_version=key_version encodedBase64=encodedBase64) on="submit"}}>
{{#if (and plaintext ciphertext)}}
<div class="box is-sideless is-fullwidth is-marginless">
<div class="field">
<label for="ciphertext" class="is-label">Ciphertext</label>
<div class="control is-expanded">
<textarea readonly class="textarea" id="ciphertext" data-test-transit-input="ciphertext">{{ciphertext}}</textarea>
<div id="ciphertext-control" class="control is-expanded">
{{ivy-codemirror
value=ciphertext
options=(hash
readOnly=true
lineNumbers=true
tabSize=2
mode='ruby'
theme='hashi'
)
data-test-transit-input="ciphertext"
}}
</div>
</div>
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
{{#copy-button
clipboardTarget="#ciphertext"
clipboardText=ciphertext
class="button is-primary"
buttonType="button"
success=(action (set-flash-message 'Ciphertext copied!'))
@ -20,7 +30,7 @@
{{/copy-button}}
</div>
<div class="control">
<button {{action "onClear"}} type="button" class="button">
<button {{action "onClear"}} type="button" class="button" data-test-encrypt-back-button>
Back
</button>
</div>
@ -37,11 +47,24 @@
<label for="plaintext" class="is-label">
Plaintext
</label>
<div class="control is-relative">
{{textarea id="plaintext" value=plaintext class="textarea" data-test-transit-input="plaintext"}}
{{b64-toggle value=plaintext isInput=false data-test-transit-b64-toggle="plaintext"}}
<div id="plaintext-control" class="control is-relative">
{{ivy-codemirror
value=plaintext
valueUpdated=(action (mut plaintext))
options=(hash
lineNumbers=true
tabSize=2
mode='ruby'
theme='hashi'
)
data-test-transit-input="plaintext"
}}
</div>
</div>
<div class="field">
{{input type="checkbox" id="encodedBase64" checked=encodedBase64 data-test-transit-input="encodedBase64"}}
<label for="encodedBase64">This data is already encoded in base64</label>
</div>
{{#if key.derived}}
<div class="field">
<label for="context" class="is-label">

View File

@ -3,15 +3,24 @@
<div class="box is-sideless is-fullwidth is-marginless">
<div class="field">
<label for="hmac" class="is-label">HMAC</label>
<div class="control">
<textarea readonly class="textarea" id="hmac">{{hmac}}</textarea>
<div id="hmac-control" class="control">
{{ivy-codemirror
value=hmac
options=(hash
readOnly=true
lineNumbers=true
tabSize=2
mode='ruby'
theme='hashi'
)
}}
</div>
</div>
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
{{#copy-button
clipboardTarget="#hmac"
clipboardText=hmac
class="button is-primary"
buttonType="button"
success=(action (set-flash-message 'HMAC copied!'))
@ -37,11 +46,28 @@
<label for="input" class="is-label">
Input
</label>
<div class="control is-relative">
{{textarea id="input" name="input" value=input class="textarea" data-test-transit-input="input"}}
{{b64-toggle value=input isInput=false data-test-transit-b64-toggle="input"}}
<div id="input-control" class="control is-relative">
{{ivy-codemirror
value=input
valueUpdated=(action (mut input))
options=(hash
lineNumbers=true
tabSize=2
mode='ruby'
theme='hashi'
)
data-test-transit-input="input"
}}
</div>
</div>
<div class="field">
{{input
type="checkbox"
id="encodedBase64"
checked=encodedBase64
data-test-transit-input="encodedBase64" }}
<label for="encodedBase64">This data is already encoded in base64</label>
</div>
<div class="field">
<label for="algorithm" class="is-label">Hash Algorithm</label>
<div class="control is-expanded">

View File

@ -9,7 +9,16 @@
<div class="field">
<label for="ciphertext" class="is-label">Ciphertext</label>
<div class="control is-expanded">
{{textarea name="ciphertext" class="textarea" id="ciphertext" value=ciphertext}}
{{ivy-codemirror
value=ciphertext
valueUpdated=(action (mut ciphertext))
options=(hash
lineNumbers=true
tabSize=2
mode='ruby'
theme='hashi'
)
}}
</div>
</div>
{{#if key.derived}}

View File

@ -1,4 +1,4 @@
<form {{action "doSubmit" (hash input=input signature=signature signature_algorithm=signature_algorithm hmac=hmac hash_algorithm=hash_algorithm context=context prehashed=prehashed) on="submit"}}>
<form {{action "doSubmit" (hash input=input signature=signature signature_algorithm=signature_algorithm hmac=hmac hash_algorithm=hash_algorithm context=context prehashed=prehashed encodedBase64=encodedBase64) on="submit"}}>
{{#if (not-eq valid null)}}
<div class="box is-sideless is-fullwidth is-marginless">
<h4 class="is-label">Verified</h4>
@ -25,10 +25,24 @@
Input
</label>
<div class="control is-relative">
{{textarea id="input" name="input" value=input class="textarea" data-test-transit-input="input"}}
{{b64-toggle value=input isInput=false data-test-transit-b64-toggle="input"}}
{{ivy-codemirror
id="input"
value=input
valueUpdated=(action (mut input))
options=(hash
lineNumbers=true
tabSize=2
mode='ruby'
theme='hashi'
)
data-test-transit-input="input"
}}
</div>
</div>
<div class="field">
{{input type="checkbox" id="encodedBase64" checked=encodedBase64 data-test-transit-input="encodedBase64"}}
<label for="encodedBase64">This data is already encoded in base64</label>
</div>
{{#if (and key.supportsSigning key.derived (not hmac))}}
<div class="field">
<label for="context" class="is-label">
@ -76,7 +90,12 @@
<div class="level-right">
{{#unless (eq verification 'HMAC')}}
<div class="control is-flex">
{{input id="prehashed" type="checkbox" name="prehashed" class="switch is-rounded is-success is-small" checked=prehashed }}
{{input
id="prehashed"
type="checkbox"
name="prehashed"
class="switch is-rounded is-success is-small"
checked=prehashed }}
<label for="prehashed">Prehashed</label>
</div>
{{/unless}}
@ -126,14 +145,34 @@
<div class="field is-flex-column is-flex-1">
<label for="hmac" class="is-label">HMAC</label>
<div class="control is-flex-column is-flex-1">
{{textarea class="textarea is-flex-1" id="hmac" value=hmac}}
{{ivy-codemirror
id="hmac"
value=hmac
valueUpdated=(action (mut hmac))
options=(hash
lineNumbers=true
tabSize=2
mode='ruby'
theme='hashi'
)
}}
</div>
</div>
{{else}}
<div class="field is-flex-column is-flex-1">
<label for="signature" class="is-label">Signature</label>
<div class="control is-flex-column is-flex-1">
{{textarea id="signature" class="textarea is-flex-1" value=signature}}
{{ivy-codemirror
id="signature"
value=signature
valueUpdated=(action (mut signature))
options=(hash
lineNumbers=true
tabSize=2
mode='ruby'
theme='hashi'
)
}}
</div>
</div>
{{/if}}
@ -143,7 +182,17 @@
<div class="field">
<label for="hmac" class="is-label">HMAC</label>
<div class="control">
{{textarea class="textarea" id="hmac" value=hmac}}
{{ivy-codemirror
id="hmac"
value=hmac
valueUpdated=(action (mut hmac))
options=(hash
lineNumbers=true
tabSize=2
mode='ruby'
theme='hashi'
)
}}
</div>
</div>
<div class="field">

View File

@ -97,7 +97,7 @@ const testConvergentEncryption = async function(assert, keyName) {
decodeAfterDecrypt: false,
assertAfterEncrypt: key => {
assert.ok(
/vault:/.test(find('[data-test-transit-input="ciphertext"]').value),
/vault:/.test(find('#ciphertext-control .CodeMirror').CodeMirror.getValue()),
`${key}: ciphertext shows a vault-prefixed ciphertext`
);
},
@ -111,12 +111,11 @@ const testConvergentEncryption = async function(assert, keyName) {
},
assertAfterDecrypt: key => {
assert
.dom('[data-test-transit-input="plaintext"]')
.hasValue(
'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=',
`${key}: the ui shows the base64-encoded plaintext`
);
assert.equal(
find('#plaintext-control .CodeMirror').CodeMirror.getValue(),
'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=',
`${key}: the ui shows the base64-encoded plaintext`
);
},
},
// raw bytes for plaintext, string for context
@ -128,7 +127,7 @@ const testConvergentEncryption = async function(assert, keyName) {
decodeAfterDecrypt: false,
assertAfterEncrypt: key => {
assert.ok(
/vault:/.test(find('[data-test-transit-input="ciphertext"]').value),
/vault:/.test(find('#ciphertext-control .CodeMirror').CodeMirror.getValue()),
`${key}: ciphertext shows a vault-prefixed ciphertext`
);
},
@ -138,12 +137,11 @@ const testConvergentEncryption = async function(assert, keyName) {
.hasValue(encodeString('context'), `${key}: the ui shows the input context`);
},
assertAfterDecrypt: key => {
assert
.dom('[data-test-transit-input="plaintext"]')
.hasValue(
'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=',
`${key}: the ui shows the base64-encoded plaintext`
);
assert.equal(
find('#plaintext-control .CodeMirror').CodeMirror.getValue(),
'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=',
`${key}: the ui shows the base64-encoded plaintext`
);
},
},
// base64 input
@ -155,7 +153,7 @@ const testConvergentEncryption = async function(assert, keyName) {
decodeAfterDecrypt: true,
assertAfterEncrypt: key => {
assert.ok(
/vault:/.test(find('[data-test-transit-input="ciphertext"]').value),
/vault:/.test(find('#ciphertext-control .CodeMirror').CodeMirror.getValue()),
`${key}: ciphertext shows a vault-prefixed ciphertext`
);
},
@ -165,9 +163,11 @@ const testConvergentEncryption = async function(assert, keyName) {
.hasValue(encodeString('context'), `${key}: the ui shows the input context`);
},
assertAfterDecrypt: key => {
assert
.dom('[data-test-transit-input="plaintext"]')
.hasValue('This is the secret', `${key}: the ui decodes plaintext`);
assert.equal(
find('#plaintext-control .CodeMirror').CodeMirror.getValue(),
'This is the secret',
`${key}: the ui decodes plaintext`
);
},
},
@ -181,7 +181,7 @@ const testConvergentEncryption = async function(assert, keyName) {
assertAfterEncrypt: key => {
assert.ok(find('[data-test-transit-input="ciphertext"]'), `${key}: ciphertext box shows`);
assert.ok(
/vault:/.test(find('[data-test-transit-input="ciphertext"]').value),
/vault:/.test(find('#ciphertext-control .CodeMirror').CodeMirror.getValue()),
`${key}: ciphertext shows a vault-prefixed ciphertext`
);
},
@ -192,19 +192,22 @@ const testConvergentEncryption = async function(assert, keyName) {
},
assertAfterDecrypt: key => {
assert.ok(find('[data-test-transit-input="plaintext"]'), `${key}: plaintext box shows`);
assert
.dom('[data-test-transit-input="plaintext"]')
.hasValue('There are many secrets 🤐', `${key}: the ui decodes plaintext`);
assert.equal(
find('#plaintext-control .CodeMirror').CodeMirror.getValue(),
'There are many secrets 🤐',
`${key}: the ui decodes plaintext`
);
},
},
];
for (let testCase of tests) {
await click('[data-test-transit-action-link="encrypt"]');
await fillIn('[data-test-transit-input="plaintext"]', testCase.plaintext);
find('#plaintext-control .CodeMirror').CodeMirror.setValue(testCase.plaintext);
await fillIn('[data-test-transit-input="context"]', testCase.context);
if (testCase.encodePlaintext) {
await click('[data-test-transit-b64-toggle="plaintext"]');
if (!testCase.encodePlaintext) {
// If value is already encoded, check the box
await click('input[data-test-transit-input="encodedBase64"]');
}
if (testCase.encodeContext) {
await click('[data-test-transit-b64-toggle="context"]');

View File

@ -119,8 +119,7 @@ module('Integration | Component | transit key actions', function(hooks) {
this.set('storeService.keyActionReturnVal', { ciphertext: 'secret' });
await render(hbs`{{transit-key-actions selectedAction=selectedAction key=key}}`);
await fillIn('#plaintext', 'plaintext');
await click('[data-test-transit-b64-toggle="plaintext"]');
find('#plaintext-control .CodeMirror').CodeMirror.setValue('plaintext');
await click('button[type="submit"]');
assert.deepEqual(
this.get('storeService.callArgs'),
@ -135,7 +134,28 @@ module('Integration | Component | transit key actions', function(hooks) {
'passes expected args to the adapter'
);
assert.equal(find('#ciphertext').value, 'secret');
assert.equal(find('#ciphertext-control .CodeMirror').CodeMirror.getValue(), 'secret');
const preEncodedValue = encodeString('plaintext');
// Click back button
await click('[data-test-encrypt-back-button]');
// Encrypt again, with pre-encoded value and checkbox selected
find('#plaintext-control .CodeMirror').CodeMirror.setValue(preEncodedValue);
await click('input[data-test-transit-input="encodedBase64"]');
await click('button[type="submit"]');
assert.deepEqual(
this.get('storeService.callArgs'),
{
action: 'encrypt',
backend: 'transit',
id: 'akey',
payload: {
plaintext: preEncodedValue,
},
},
'passes expected args to the adapter'
);
}
test('it encrypts', doEncrypt);
@ -148,8 +168,7 @@ module('Integration | Component | transit key actions', function(hooks) {
this.set('storeService.keyActionReturnVal', { ciphertext: 'secret' });
await render(hbs`{{transit-key-actions selectedAction="encrypt" key=key}}`);
await fillIn('#plaintext', 'plaintext');
await click('[data-test-transit-b64-toggle="plaintext"]');
findAll('.CodeMirror')[0].CodeMirror.setValue('plaintext');
assert.equal(findAll('#key_version').length, 1, 'it renders the key version selector');
await triggerEvent('#key_version', 'change');
@ -177,9 +196,8 @@ module('Integration | Component | transit key actions', function(hooks) {
this.set('storeService.keyActionReturnVal', { ciphertext: 'secret' });
await render(hbs`{{transit-key-actions selectedAction="encrypt" key=key}}`);
await fillIn('#plaintext', 'plaintext');
await click('[data-test-transit-b64-toggle="plaintext"]');
// await fillIn('#plaintext', 'plaintext');
find('#plaintext-control .CodeMirror').CodeMirror.setValue('plaintext');
assert.equal(
findAll('#key_version').length,
0,
@ -193,10 +211,18 @@ module('Integration | Component | transit key actions', function(hooks) {
this.set('storeService.keyActionReturnVal', { plaintext });
this.set('selectedAction', 'decrypt');
assert.equal(find('#ciphertext').value, 'secret', 'keeps ciphertext value');
assert.equal(
find('#ciphertext-control .CodeMirror').CodeMirror.getValue(),
'secret',
'keeps ciphertext value'
);
await click('button[type="submit"]');
assert.equal(find('#plaintext').value, plaintext, 'renders decrypted value');
assert.equal(
find('#plaintext-control .CodeMirror').CodeMirror.getValue(),
plaintext,
'renders decrypted value'
);
});
const setupExport = async function() {