Skip to content
This repository was archived by the owner on Mar 2, 2022. It is now read-only.

Post sealed box & Decrypt response #19

Merged
merged 39 commits into from
Jun 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
8de6e71
more interfaces
jongbonga May 13, 2021
8c0acd9
post messageData
jongbonga May 13, 2021
eee5f6f
post and decrypt box
jongbonga May 14, 2021
d016d72
secret key getter, sealbox action
jongbonga May 14, 2021
6533625
decrypt message
jongbonga May 14, 2021
1e6a8f9
moved post encryted box method to vuex
jongbonga May 20, 2021
4893d73
save decrypted msg
jongbonga May 20, 2021
9e1e2e3
update mutation nomenclature
jongbonga May 20, 2021
b31b8f0
show msg dialog and reset on close
jongbonga May 20, 2021
c2674dd
stop loading after box is sealed and posted
jongbonga May 21, 2021
b8112a5
action nomanclature
jongbonga May 21, 2021
a66cde3
no return from action
jongbonga May 22, 2021
5d5f8bf
use ourSecretKey's state instead of getters
jongbonga May 25, 2021
ded93bb
// TODO: Error Handling
jongbonga May 25, 2021
8853f7a
check if ourSecretKey is null
jongbonga May 25, 2021
cf0b2ec
catch requestExecutionToken
jongbonga May 25, 2021
b667b97
refactor(store): making all the action return types explicit
jongbonga May 26, 2021
57cebfd
refactor(AccessForm): use notifyErrors around requestExecutionToken
jongbonga May 26, 2021
7b160bc
refactor(AccessForm): change create to request
jongbonga May 26, 2021
4d143be
throw instead of returning error
jongbonga May 26, 2021
587dfe7
chore(AccessForm): remove TODO
PiDelport May 27, 2021
941c574
change decryptedMessage to executionToken
jongbonga May 26, 2021
63d0de8
refactor(store): use axios validateStatus to check the status code
PiDelport May 27, 2021
a4817aa
refactor(store,AccessForm): rename setDecryptedMsg → setExecutionToke…
PiDelport May 27, 2021
f00c6ce
text(AccessForm): tweak execution token message box title
PiDelport May 27, 2021
4364cb1
docs(store): add note about execution token format
PiDelport May 27, 2021
061bc93
refactor(store): tweak requestExecutionToken, throw on error
PiDelport May 27, 2021
d2949ae
fix(store): add await for postAccessForm dispatch
PiDelport May 27, 2021
31a65a1
lint(cryptography): yarn lint
PiDelport May 27, 2021
8127c3f
refactor(cryptography): pull fields back into EncryptedMessage
PiDelport May 27, 2021
47d6912
docs(cryptography): add TODO comment for better local key management
PiDelport May 27, 2021
777c231
refactor(store): drop unused code: decryptResponse
PiDelport May 27, 2021
ff3a078
refactor(models): rename data upload types to "sealed" types
PiDelport May 27, 2021
952f893
feat(utils): add checkDefined helper
PiDelport May 27, 2021
3f63d9d
refactor(store): use checkDefined for null checks
PiDelport May 27, 2021
4a8938f
refactor(AccessForm): remove unused store accessors
PiDelport May 27, 2021
eff97b7
refactor(store): pull out UploadResult type
PiDelport May 27, 2021
2c44236
refactor(store): factor out a common sealedPost helper
PiDelport May 27, 2021
7c17610
refactor(store): drop unused secret key from store state
PiDelport May 27, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 38 additions & 6 deletions src/components/AccessForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,24 @@
></el-input-number>
</el-form-item>
<el-form-item style="margin-top: 15px">
<el-button type="primary" @click="onSubmit('accessForm')"
>Create</el-button
<el-button
type="primary"
:loading="loading"
@click="onSubmit('accessForm')"
>
Request
</el-button>
</el-form-item>
</el-form>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import { encryptJson } from "@/utils/cryptography";
import elForm from "element-plus/lib/el-form";
import { ElMessageBox } from "element-plus";
import { mapActions, mapMutations, mapState } from "vuex";
import { notifyErrors } from "@/utils/error-notification";
import util from "tweetnacl-util";

export default defineComponent({
data() {
Expand Down Expand Up @@ -73,24 +80,49 @@ export default defineComponent({
trigger: "change"
}
]
}
},
loading: false
};
},
computed: {
...mapState(["executionToken"])
},
methods: {
...mapActions(["requestExecutionToken"]),
...mapMutations(["setExecutionToken"]),
onSubmit(formName: string) {
(this.$refs[formName] as typeof elForm).validate(
async (valid: boolean) => {
if (valid) {
const enclavePublicKey = this.$store.getters.enclavePublicKey;
this.loading = true;
// Get the unproxied form data before encrypting, for clarity.
const data = Object.assign({}, this.form);
await encryptJson(data, enclavePublicKey);
await notifyErrors("Execution token request failed", () => {
return this.requestExecutionToken(data);
});
this.loading = false;
} else {
console.log("error submit!!");
return false;
}
}
);
},
async handleMsgDisplay(executionToken: Uint8Array) {
await ElMessageBox({
title: "Please copy and save this execution token",
message: util.encodeBase64(executionToken),
beforeClose: () => {
this.setExecutionToken(null);
}
});
}
},
watch: {
executionToken(newState: Uint8Array | null) {
if (newState) {
this.handleMsgDisplay(newState);
}
}
}
});
Expand Down
8 changes: 6 additions & 2 deletions src/models/data-upload.ts → src/models/sealed-calls.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export interface Request {
/**
* Types for dealing with sealed API calls.
*/

export interface SealedRequest {
readonly metadata: Metadata;
// Base64 encoded Uint8Array
readonly payload: string;
Expand All @@ -11,7 +15,7 @@ export interface Metadata {
readonly nonce: string;
}

export interface Response {
export interface SealedResponse {
// Base64 encoded Uint8Array
readonly ciphertext: string;
// Base64 encoded Uint8Array
Expand Down
163 changes: 98 additions & 65 deletions src/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import { checkDefined } from "@/utils/checks";
import { createLogger, createStore } from "vuex";
import { encryptBlob, Base64, decryptMessage } from "@/utils/cryptography";
import {
encryptBlob,
Base64,
decryptMessage,
EncryptedMessage,
encryptJson
} from "@/utils/cryptography";
import { verifyToken } from "@/utils/jwt";
import { AttestationToken } from "@/utils/attestation-token";
import base64url from "base64url";
import {
Request as UploadRequest,
Response as UploadResponse
} from "@/models/data-upload";
import { SealedRequest, SealedResponse } from "@/models/sealed-calls";
import axios from "axios";

interface UploadResult {
accessKey: string;
uuid: Uint8Array;
}

export interface State {
jwtToken: string | null;
ourSecretKey: Base64 | null;
attestationResult: AttestationToken | null;
uploadResult: { accessKey: string; uuid: Uint8Array } | null;
uploadResult: UploadResult | null;
executionToken: Uint8Array | null;
}

// TODO: add typescript typings for Vuex
Expand All @@ -22,9 +31,9 @@ export default createStore<State>({
plugins: process.env.NODE_ENV !== "production" ? [createLogger()] : [],
state: {
jwtToken: null,
ourSecretKey: null,
attestationResult: null,
uploadResult: null
uploadResult: null,
executionToken: null
},
getters: {
enclavePublicKey(state) {
Expand All @@ -42,14 +51,13 @@ export default createStore<State>({
saveAttestationResult(state, attestationResult) {
state.attestationResult = attestationResult;
},
saveSecretKey(state, secretKey: Base64) {
state.ourSecretKey = secretKey;
},
saveUploadResult(
state,
uploadResult: { accessKey: string; uuid: Uint8Array }
) {
saveUploadResult(state, uploadResult: UploadResult) {
state.uploadResult = uploadResult;
},
// XXX: The execution token will probably be a JWT value later,
// but for now, just save it as raw bytes.
setExecutionToken(state: State, executionToken: Uint8Array): void {
state.executionToken = executionToken;
}
},
actions: {
Expand All @@ -62,59 +70,33 @@ export default createStore<State>({
saveToken({ commit }, token) {
commit("saveToken", token);
},
async encryptAndUploadFile({ commit, dispatch, getters }, message: File) {
const enclavePubKey = getters.enclavePublicKey;
if (!enclavePubKey) {
return null;
}
const { ourData, messageData } = await encryptBlob(
message,
enclavePubKey as Base64

/**
* Upload a data file, and save the resulting UUID and access token.
*/
async encryptAndUploadFile(context, message: File): Promise<void> {
const uploadResult = parseUploadResponse(
await sealedPost(
"https://rtc-data.registree.io/data/uploads",
message,
checkDefined(context.getters.enclavePublicKey),
encryptBlob
)
);
commit("saveSecretKey", ourData.ourSecretKey);
dispatch("uploadFile", {
metadata: {
nonce: messageData.nonce,
uploader_pub_key: messageData.ourPublicKey
},
payload: messageData.ciphertext
});
context.commit("saveUploadResult", uploadResult);
},
async uploadFile({ dispatch }, request: UploadRequest) {
const res = await axios.post<UploadResponse>(

/**
* Submit an execution token request.
*/
async requestExecutionToken(context, executionTokenRequest): Promise<void> {
const executionToken = await sealedPost(
"https://rtc-data.registree.io/data/uploads",
request
);
if (res.status !== 200) {
return "error";
}
await dispatch("decryptUploadResponse", res.data);
},
async decryptUploadResponse(
{ state, dispatch, getters },
response: UploadResponse
) {
const enclavePubKey = getters.enclavePublicKey;
if (!enclavePubKey || !state.ourSecretKey) {
return "error";
}
const msg = decryptMessage(
response.ciphertext,
response.nonce,
enclavePubKey,
state.ourSecretKey
executionTokenRequest,
checkDefined(context.getters.enclavePublicKey),
encryptJson
);
if (!msg) {
// TODO: Error Handling
return "error";
}
await dispatch("parseUploadMessage", msg);
},
async parseUploadMessage({ commit }, message: Uint8Array) {
console.log("parseUploadMessage:", message);
const accessKey = btoa(message.slice(0, 24).toString());
const uuid = message.slice(24);
commit("saveUploadResult", { accessKey, uuid });
context.commit("setExecutionToken", executionToken);
}
},
modules: {}
Expand Down Expand Up @@ -154,3 +136,54 @@ async function fetchAttestationToken(): Promise<string> {
}
}
}

/**
* Helper: Post a sealed request, and unseal the response.
*
* @param url - The endpoint URL to post to
* @param value - The unsealed value to seal and post
* @param enclavePubKey - The remote enclave's public key
* @param encrypt - Function to encrypt (seal) the value with
* @returns - The unsealed response bytes
*/
async function sealedPost<T>(
url: string,
value: T,
enclavePubKey: Base64,
encrypt: (value: T, theirPublicKey: Base64) => Promise<EncryptedMessage>
): Promise<Uint8Array> {
// Seal:
const {
ourData: { ourSecretKey: ephemeralSecretKey },
messageData
}: EncryptedMessage = await encrypt(value, enclavePubKey);
const requestBody: SealedRequest = {
metadata: {
nonce: messageData.nonce,
uploader_pub_key: messageData.ourPublicKey
},
payload: messageData.ciphertext
};

// Post:
const axiosResponse = await axios.post<SealedResponse>(url, requestBody, {
validateStatus: status => status === 200
});
const responseBody: SealedResponse = axiosResponse.data;

// Unseal:
const { nonce, ciphertext } = responseBody;
return checkDefined(
decryptMessage(ciphertext, nonce, enclavePubKey, ephemeralSecretKey)
);
}

/**
* Helper: Parse raw data upload result to a data access key and UUID.
*/
function parseUploadResponse(bytes: Uint8Array): UploadResult {
return {
accessKey: btoa(bytes.slice(0, 24).toString()),
uuid: bytes.slice(24)
};
}
13 changes: 13 additions & 0 deletions src/utils/checks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Check that a value is defined.
*
* @throws Error if `value` is `null` or `undefined`
*/
export function checkDefined<T>(value: T | null | undefined): T {
if (value === null) {
throw new Error("unexpected null value");
} else if (value === undefined) {
throw new Error("unexpected undefined value");
}
return value;
}
7 changes: 3 additions & 4 deletions src/utils/cryptography.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import util from "tweetnacl-util";
export type Base64 = string;

export interface EncryptedMessage {
// TODO: Remove the secret key from this interface, once we have better local key management?
readonly ourData: {
ourSecretKey: Base64;
};
Expand Down Expand Up @@ -111,10 +112,8 @@ function encryptBox(
ourPublicKey: Uint8Array;
ourSecretKey: Uint8Array;
} {
const {
publicKey: ourPublicKey,
secretKey: ourSecretKey
} = nacl.box.keyPair();
const { publicKey: ourPublicKey, secretKey: ourSecretKey } =
nacl.box.keyPair();
const nonce = nacl.randomBytes(nacl.box.nonceLength);
const ciphertext = nacl.box(plaintext, nonce, theirPublicKey, ourSecretKey);
return { ciphertext, nonce, ourPublicKey, ourSecretKey };
Expand Down