Skip to content

Commit

Permalink
Wasm bindings for KeyIdStorage (#1147)
Browse files Browse the repository at this point in the history
  • Loading branch information
abdulmth authored Mar 20, 2023
1 parent 20d5c54 commit 94db021
Show file tree
Hide file tree
Showing 8 changed files with 338 additions and 5 deletions.
46 changes: 46 additions & 0 deletions bindings/wasm/docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ and resolution of DID documents in Alias Outputs.</p>
<dt><a href="#MethodData">MethodData</a></dt>
<dd><p>Supported verification method data formats.</p>
</dd>
<dt><a href="#MethodDigest">MethodDigest</a></dt>
<dd></dd>
<dt><a href="#MethodScope">MethodScope</a></dt>
<dd><p>Supported verification method types.</p>
</dd>
Expand Down Expand Up @@ -2939,6 +2941,50 @@ Deserializes an instance from a JSON object.
| --- | --- |
| json | <code>any</code> |

<a name="MethodDigest"></a>

## MethodDigest
**Kind**: global class

* [MethodDigest](#MethodDigest)
* [new MethodDigest(verification_method)](#new_MethodDigest_new)
* _instance_
* [.pack()](#MethodDigest+pack) ⇒ <code>Uint8Array</code>
* [.clone()](#MethodDigest+clone)[<code>MethodDigest</code>](#MethodDigest)
* _static_
* [.unpack(bytes)](#MethodDigest.unpack)[<code>MethodDigest</code>](#MethodDigest)

<a name="new_MethodDigest_new"></a>

### new MethodDigest(verification_method)

| Param | Type |
| --- | --- |
| verification_method | [<code>VerificationMethod</code>](#VerificationMethod) |

<a name="MethodDigest+pack"></a>

### methodDigest.pack() ⇒ <code>Uint8Array</code>
Packs `MethodDigest` into bytes.

**Kind**: instance method of [<code>MethodDigest</code>](#MethodDigest)
<a name="MethodDigest+clone"></a>

### methodDigest.clone() ⇒ [<code>MethodDigest</code>](#MethodDigest)
Deep clones the object.

**Kind**: instance method of [<code>MethodDigest</code>](#MethodDigest)
<a name="MethodDigest.unpack"></a>

### MethodDigest.unpack(bytes) ⇒ [<code>MethodDigest</code>](#MethodDigest)
Unpacks bytes into [`MethodDigest`].

**Kind**: static method of [<code>MethodDigest</code>](#MethodDigest)

| Param | Type |
| --- | --- |
| bytes | <code>Uint8Array</code> |

<a name="MethodScope"></a>

## MethodScope
Expand Down
39 changes: 38 additions & 1 deletion bindings/wasm/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
// SPDX-License-Identifier: Apache-2.0

use identity_iota::resolver;
use identity_iota::storage::key_id_storage::KeyIdStorageError;
use identity_iota::storage::key_id_storage::KeyIdStorageErrorKind;
use identity_iota::storage::key_id_storage::KeyIdStorageResult;
use identity_iota::storage::key_storage::KeyStorageError;
use identity_iota::storage::key_storage::KeyStorageErrorKind;
use identity_iota::storage::key_storage::KeyStorageResult;
use std::borrow::Cow;
use std::fmt::Debug;
use std::fmt::Display;
use std::result::Result as StdResult;
use wasm_bindgen::JsValue;
Expand Down Expand Up @@ -190,6 +194,24 @@ impl From<identity_iota::credential::CompoundPresentationValidationError> for Wa
}
}

impl From<identity_iota::core::SingleStructError<KeyStorageErrorKind>> for WasmError<'_> {
fn from(error: identity_iota::core::SingleStructError<KeyStorageErrorKind>) -> Self {
Self {
name: Cow::Borrowed("KeyStorageError"),
message: Cow::Owned(format!("{}", error)),
}
}
}

impl From<identity_iota::core::SingleStructError<KeyIdStorageErrorKind>> for WasmError<'_> {
fn from(error: identity_iota::core::SingleStructError<KeyIdStorageErrorKind>) -> Self {
Self {
name: Cow::Borrowed("KeyIdStorageError"),
message: Cow::Owned(format!("{}", error)),
}
}
}

/// Convenience struct to convert Result<JsValue, JsValue> to errors in the Rust library.
pub struct JsValueResult(pub(crate) Result<JsValue>);

Expand All @@ -201,6 +223,12 @@ impl JsValueResult {
.map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err))
}

pub fn to_key_id_storage_error(self) -> KeyIdStorageResult<JsValue> {
self
.stringify_error()
.map_err(|err| KeyIdStorageError::new(KeyIdStorageErrorKind::Unspecified).with_source(err))
}

// Consumes the struct and returns a Result<_, String>, leaving an `Ok` value untouched.
pub(crate) fn stringify_error(self) -> StdResult<JsValue, String> {
self.0.map_err(|js_value| {
Expand All @@ -227,7 +255,6 @@ impl From<Result<JsValue>> for JsValueResult {
}
}

// TODO: Remove or reuse depending on what we do with the account..
impl<T: for<'a> serde::Deserialize<'a>> From<JsValueResult> for KeyStorageResult<T> {
fn from(result: JsValueResult) -> Self {
result.to_key_storage_error().and_then(|js_value| {
Expand All @@ -237,3 +264,13 @@ impl<T: for<'a> serde::Deserialize<'a>> From<JsValueResult> for KeyStorageResult
})
}
}

impl<T: for<'a> serde::Deserialize<'a>> From<JsValueResult> for KeyIdStorageResult<T> {
fn from(result: JsValueResult) -> Self {
result.to_key_id_storage_error().and_then(|js_value| {
js_value
.into_serde()
.map_err(|e| KeyIdStorageError::new(KeyIdStorageErrorKind::SerializationError).with_source(e))
})
}
}
86 changes: 86 additions & 0 deletions bindings/wasm/src/storage/key_id_storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright 2020-2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use super::WasmMethodDigest;
use crate::common::PromiseString;
use crate::error::JsValueResult;
use identity_iota::storage::key_id_storage::KeyIdStorage;
use identity_iota::storage::key_id_storage::KeyIdStorageResult;
use identity_iota::storage::key_id_storage::MethodDigest;
use identity_iota::storage::key_storage::KeyId;
use js_sys::Promise;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(typescript_type = "KeyIdStorage")]
pub type WasmKeyIdStorage;

#[wasm_bindgen(method, js_name = insertKeyId)]
pub fn insert_key_id(this: &WasmKeyIdStorage, method_digest: WasmMethodDigest, key_id: String) -> Promise;

#[wasm_bindgen(method, js_name = getKeyId)]
pub fn get_key_id(this: &WasmKeyIdStorage, method_digest: WasmMethodDigest) -> PromiseString;

#[wasm_bindgen(method, js_name = deleteKeyId)]
pub fn delete_key_id(this: &WasmKeyIdStorage, method_digest: WasmMethodDigest) -> Promise;
}

#[async_trait::async_trait(?Send)]
impl KeyIdStorage for WasmKeyIdStorage {
async fn insert_key_id(&self, method_digest: MethodDigest, key_id: KeyId) -> KeyIdStorageResult<()> {
let promise: Promise = Promise::resolve(&WasmKeyIdStorage::insert_key_id(
self,
WasmMethodDigest(method_digest),
key_id.clone().into(),
));
let result: JsValueResult = JsFuture::from(promise).await.into();
result.into()
}

async fn get_key_id(&self, method_digest: &MethodDigest) -> KeyIdStorageResult<KeyId> {
let promise: Promise = Promise::resolve(&WasmKeyIdStorage::get_key_id(
self,
WasmMethodDigest(method_digest.clone()),
));
let result: JsValueResult = JsFuture::from(promise).await.into();
result.into()
}

async fn delete_key_id(&self, method_digest: &MethodDigest) -> KeyIdStorageResult<()> {
let promise: Promise = Promise::resolve(&WasmKeyIdStorage::delete_key_id(
self,
WasmMethodDigest(method_digest.clone()),
));
let result: JsValueResult = JsFuture::from(promise).await.into();
result.into()
}
}

#[wasm_bindgen(typescript_custom_section)]
const JWK_STORAGE: &'static str = r#"
/**
* Key value Storage for key ids under [`MethodDigest`].
*/
interface KeyIdStorage {
/**
* Insert a key id into the `KeyIdStorage` under the given `MethodDigest`.
*
* If an entry for `key` already exists in the storage an error must be returned
* immediately without altering the state of the storage.
*/
insertKeyId: (methodDigest: MethodDigest, keyId: string) => Promise<void>;
/**
* Obtain the key id associated with the given [`MethodDigest`].
*/
getKeyId: (methodDigest: MethodDigest) => Promise<string>;
/**
* Delete the [`KeyId`] associated with the given [`MethodDigest`] from the [`KeyIdStorage`].
*
* If `key` is not found in storage, an Error must be returned.
*/
deleteKeyId: (methodDigest: MethodDigest) => Promise<void>;
}"#;
42 changes: 42 additions & 0 deletions bindings/wasm/src/storage/method_digest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2020-2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use identity_iota::storage::key_id_storage::MethodDigest;
use js_sys::Uint8Array;
use wasm_bindgen::prelude::*;

use crate::did::WasmVerificationMethod;
use crate::error::Result;
use crate::error::WasmResult;

/// Unique identifier of a [`VerificationMethod`].
///
/// NOTE:
/// This class does not have a JSON representation,
/// use the methods `pack` and `unpack` instead.
#[wasm_bindgen(js_name = MethodDigest, inspectable)]
pub struct WasmMethodDigest(pub(crate) MethodDigest);

#[wasm_bindgen(js_class = MethodDigest)]
impl WasmMethodDigest {
#[wasm_bindgen(constructor)]
pub fn new(verification_method: &WasmVerificationMethod) -> Result<WasmMethodDigest> {
Ok(Self(MethodDigest::new(&verification_method.0).wasm_result()?))
}

/// Packs `MethodDigest` into bytes.
#[wasm_bindgen]
pub fn pack(&self) -> Uint8Array {
let bytes: &[u8] = &self.0.pack();
bytes.into()
}

/// Unpacks bytes into [`MethodDigest`].
#[wasm_bindgen]
pub fn unpack(bytes: &Uint8Array) -> Result<WasmMethodDigest> {
let bytes: Vec<u8> = bytes.to_vec();
Ok(Self(MethodDigest::unpack(bytes).wasm_result()?))
}
}

impl_wasm_clone!(WasmMethodDigest, MethodDigest);
4 changes: 4 additions & 0 deletions bindings/wasm/src/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

mod jwk_gen_output;
mod jwk_storage;
mod key_id_storage;
mod method_digest;

pub use jwk_gen_output::*;
pub use jwk_storage::*;
pub use key_id_storage::*;
pub use method_digest::*;
114 changes: 114 additions & 0 deletions bindings/wasm/tests/key_id_storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
const assert = require("assert");
import { CoreDID, KeyIdStorage, KeyPair, KeyType, MethodDigest, VerificationMethod } from "../node";

describe("Method digest", () => {
it("should have consistent hashing", () => {
let verificationMethodJson = {
id: "did:example:HHoh9NQC9AUsK15Jyyq53VTujxEUizKDXRXd7zbT1B5u#frag_1",
controller: "did:example:HHoh9NQC9AUsK15Jyyq53VTujxEUizKDXRXd7zbT1B5u",
type: "Ed25519VerificationKey2018",
publicKeyMultibase: "zHHoh9NQC9AUsK15Jyyq53VTujxEUizKDXRXd7zbT1B5u",
};

let verificationMethod = VerificationMethod.fromJSON(verificationMethodJson);
let methodDigest = new MethodDigest(verificationMethod);

let packed = methodDigest.pack();
// Packed bytes must be consistent between Rust and Wasm, see Rust tests for `MethodDigest`.
let packedExpected = new Uint8Array([0, 74, 60, 10, 199, 76, 205, 180, 133]);
assert.deepStrictEqual(packed, packedExpected);
});
});

describe("Key Id Storage", () => {
it("should work", async () => {
const KEY_ID = "my-key-id";
let vm: VerificationMethod = createVerificationMethod();
let methodDigest: MethodDigest = new MethodDigest(vm);

let memstore = new KeyIdMemStore();

// Deletion of non saved key id results in error.
assert.rejects(memstore.deleteKeyId(methodDigest));

// Store key id.
await memstore.insertKeyId(methodDigest, KEY_ID);

// Double insertion results in error.
assert.rejects(memstore.insertKeyId(methodDigest, KEY_ID));

// Restore key id from a `MethodDigest` with the same data but not the same reference.
let methodDigestClone = MethodDigest.unpack(methodDigest.pack());
let key_id_restored: string = await memstore.getKeyId(methodDigestClone);

// Check restored key id.
assert.equal(KEY_ID, key_id_restored);

// Delete stored key id.
await memstore.deleteKeyId(methodDigest);

// Double deletion results in error.
assert.rejects(memstore.deleteKeyId(methodDigest));
});
});

function createVerificationMethod(): VerificationMethod {
let id = CoreDID.parse("did:example:abc123");
let keypair = new KeyPair(KeyType.Ed25519);
let method = new VerificationMethod(id, keypair.type(), keypair.public(), "#key-1");
return method;
}

/**
* Converts a `MethodDigest` to a base64 encoded string.
*/
function methodDigestToString(methodDigest: MethodDigest): string {
let arrayBuffer = methodDigest.pack().buffer;
let buffer = Buffer.from(arrayBuffer);
return buffer.toString("base64");
}

/**
* Creates a `MethodDigest` from a base64 encoded string.
*/
function stringToMethodDigest(input: string): MethodDigest {
let buffer = Buffer.from(input, "base64");
let byteArray = Uint8Array.from(buffer);
return MethodDigest.unpack(byteArray);
}

class KeyIdMemStore implements KeyIdStorage {
private _keyIds: Map<string, string>;

constructor() {
this._keyIds = new Map<string, string>();
}

public async insertKeyId(methodDigest: MethodDigest, keyId: string): Promise<void> {
let methodDigestAsString: string = methodDigestToString(methodDigest);
let value = this._keyIds.get(methodDigestAsString);
if (value !== undefined) {
throw new Error("KeyId already exists");
}
this._keyIds.set(methodDigestAsString, keyId);
}

public async getKeyId(methodDigest: MethodDigest): Promise<string> {
let methodDigestAsString: string = methodDigestToString(methodDigest);
let value = this._keyIds.get(methodDigestAsString);
if (value == undefined) {
throw new Error("KeyId not found");
}
return value;
}

public async deleteKeyId(methodDigest: MethodDigest): Promise<void> {
let methodDigestAsString: string = methodDigestToString(methodDigest);
let success = this._keyIds.delete(methodDigestAsString);
if (success) {
return;
} else {
throw new Error("KeyId not found!");
}
}
}
Loading

0 comments on commit 94db021

Please sign in to comment.