Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wasm bindings for KeyIdStorage #1147

Merged
merged 8 commits into from
Mar 20, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
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
30 changes: 29 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,15 @@ impl From<identity_iota::credential::CompoundPresentationValidationError> for Wa
}
}

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

abdulmth marked this conversation as resolved.
Show resolved Hide resolved
/// Convenience struct to convert Result<JsValue, JsValue> to errors in the Rust library.
pub struct JsValueResult(pub(crate) Result<JsValue>);

Expand All @@ -201,6 +214,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 +246,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 +255,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))
})
}
}
85 changes: 85 additions & 0 deletions bindings/wasm/src/storage/key_id_storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// 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::wasm_bindgen;
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;
olivereanderson marked this conversation as resolved.
Show resolved Hide resolved
}

#[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>;
}"#;
41 changes: 41 additions & 0 deletions bindings/wasm/src/storage/method_digest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// 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;

#[wasm_bindgen(js_name = MethodDigest, inspectable)]
pub struct WasmMethodDigest(pub(crate) MethodDigest);

/// Unique identifier of a [`VerificationMethod`].
///
/// NOTE:
/// This class does not have a JSON representation,
/// use the methods `pack` and `unpack` instead.
#[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::*;
113 changes: 113 additions & 0 deletions bindings/wasm/tests/key_id_storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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();
let packedExpected = new Uint8Array([0, 74, 60, 10, 199, 76, 205, 180, 133]);
assert.deepStrictEqual(packed, packedExpected);
abdulmth marked this conversation as resolved.
Show resolved Hide resolved
});
});

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!");
}
}
}
2 changes: 1 addition & 1 deletion identity_core/src/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ pub use self::object::Value;
pub use self::one_or_many::OneOrMany;
pub use self::one_or_set::OneOrSet;
pub use self::ordered_set::OrderedSet;
pub use self::single_struct_error::*;
pub use self::timestamp::Duration;
pub use self::timestamp::Timestamp;
pub use self::url::Url;
pub use single_struct_error::*;

mod context;
mod fragment;
Expand Down
Loading