Storage Interface
Introduction
The high-level account API takes care of publishing updates to an identity and storing secrets securely. It does the latter by using an implementation of the Storage
interface. In this section, we will go into more depth of the interface, and how to implement that interface.
The key idea behind the interface is strongly inspired by the architecture of key management systems (KMS) or secure enclaves: once private keys are entered into the system, they can never be retrieved again. Instead, all operations using the key will have to go through that system. This approach is what allows Storage
implementations to be architected more securely than simply storing and loading private keys from a regular database. Of course, the security is directly dependent on the concrete implementation, which is why we provide one such implementation using Stronghold, and strongly recommend using it. However, there are cases where one cannot use Stronghold
or may want to integrate key management of identities into their own KMS or similar, which is why the Storage
interface is an abstraction over such systems. Any implementation of that interface can then be used by the Account
.
The storage interface has three major categories of functions. A brief overview of those functions:
- DID Operations: Management of identities.
did_create
: Based on a private key, or a generated one, creates a new DID.did_list
: List all DIDs in thisStorage
.did_exists
: Returns whether the given DID exists in thisStorage
.did_purge
: Wipes all data related to the given DID.
- Key Operations: Various functionality to managing cryptographic keys.
key_generate
: Generates a new key for the given DID.key_insert
: Inserts a pre-existing private key for the given DIDlocation
.key_public
: Calculates and returns the public key for the given location to a private key.key_delete
: Removes the key at the given location.key_sign
: Signs the given data with the key at the given location.key_exists
: Returns whether the key at the given location exists.
- Data Operations: Used for keeping state persistent. Storages only need to serialize and store the data.
chain_state_get
: Returns theChainState
data structure for the givenDID
.chain_state_set
: Sets theChainState
data structure for the givenDID
.document_get
: Returns the DID document for the givenDID
.document_set
: Sets the DID document for the givenDID
.
Storage Layout
Identifiers
There are two types of identifiers in the interface, DIDs and key locations. A DID identifies an identity, while a key location identifies a key. An implementation recommendation is to use the DID as a partition key. Everything related to a DID can be stored in a partition identified by that DID. Importantly, the location of a key is only guaranteed to be unique within the DID partition it belongs to. If no partitioning is used, then DID and key location should be combined (e.g. concatenated) to produce a single, globally unique (i.e. across all identities) identifier for a key in storage.
Representations
A KeyLocation
is a compound identifier based on the fragment of a verification method and the hash of a public key. The motivation for this design is that a KeyLocation
can be derived given a DID document and one of its verification methods. Thus, no additional state is necessary.
Canonical string representations of the IotaDID
and KeyLocation
type can be obtained using the string representation of a DID and the canonical
method on KeyLocation
respectively. These representations are intended to be kept stable as much as possible.
Example layout
This illustrates the recommended approach for partitioning the storage layout (where location -> key
is a mapping from location
to key
):
did:iota:Ft3wA8Tv2nF25hij3aegR54Wvqju7t5zqW9xnCB5L3Wu
sign-0:16843234495045965331 -> 0xc6f0dbacd56156ff4c383d549ac61ada87f8aa69454f3bfae99f5fa9e093a5c3
kex-0:7560300328640998700 -> 0xe494e36164e0a760140f3a9ab7dfdad38edac698f93d5239655dbd7499194760
did:iota:DSvXWs7FUch9MQcaUKmrRFZyHYcHwt3t3pbjvKsQBfep
sign-0:16843234495045965331 -> 0xc6f0dbacd56156ff4c383d549ac61ada87f8aa69454f3bfae99f5fa9e093a5c3
kex-0:16546298247591944074 -> 0x8e1d037cd343f84276ab737b638da9095bcb6052f7fd9628d21d20f434f9959a
key:8559754420653090937 -> 0x4ef484a54aa16503878aa1ecaa6d73cb8254aefa3f80a569ed33ca685289d01e
Note how fragments (such as kex-0
) can appear more than once, but the hash of the public key - calculated from the stored private key - makes the location unique in general.
Although unlikely in practice, even the same private key can be used across different DIDs, which produces the same key location (here sign-0:16843234495045965331
) and it's important that these are stored independently, so that deleting one does not accidentally delete the other. Hence why a key's full identifier in storage needs to be based on the DID and the key location.
That said, the following flattened structure also satisfies the requirements:
did:iota:Ft3wA8Tv2nF25hij3aegR54Wvqju7t5zqW9xnCB5L3Wu:sign-0:16843234495045965331 -> 0xc6f0dbacd56156ff4c383d549ac61ada87f8aa69454f3bfae99f5fa9e093a5c3
did:iota:Ft3wA8Tv2nF25hij3aegR54Wvqju7t5zqW9xnCB5L3Wu:kex-0:7560300328640998700 -> 0xe494e36164e0a760140f3a9ab7dfdad38edac698f93d5239655dbd7499194760
did:iota:DSvXWs7FUch9MQcaUKmrRFZyHYcHwt3t3pbjvKsQBfep:sign-0:16843234495045965331 -> 0xc6f0dbacd56156ff4c383d549ac61ada87f8aa69454f3bfae99f5fa9e093a5c3
did:iota:DSvXWs7FUch9MQcaUKmrRFZyHYcHwt3t3pbjvKsQBfep:kex-0:16546298247591944074 -> 0x8e1d037cd343f84276ab737b638da9095bcb6052f7fd9628d21d20f434f9959a
did:iota:DSvXWs7FUch9MQcaUKmrRFZyHYcHwt3t3pbjvKsQBfep:key:8559754420653090937 -> 0x4ef484a54aa16503878aa1ecaa6d73cb8254aefa3f80a569ed33ca685289d01e
The primary advantage of the partitioning is that it simplifies the implementation of the did_purge
operation, which wipes all data belonging to a given DID. With partitioning, this operation can simply wipe the partition whereas a storage with a flattened layout will have to do more work.
Indexing
The interface has two methods called did_list
and did_exists
. These return the list of stored DIDs, and whether a DID exists in storage, respectively. Implementations are thus expected to maintain a list or index of stored DIDs. An identity created with did_create
is added to the index, while an identity deleted through did_purge
is removed from the index.
If the storage implementation can be accessed concurrently, then access to the index needs to be synchronized, since it is unique per storage instance.
Implementation
The IOTA Identity framework ships two implementations of Storage
. The MemStore
is an insecure in-memory implementation intended as an example implementation and for testing. The secure and recommended Storage
is Stronghold
. Stronghold
may be interesting for implementers to look at, as it needs to deal with some challenges the in-memory version does not have.
This section will detail some common challenges and embeds the MemStore
implementations in Rust and TypeScript.
Challenges
The did_create
method takes the fragment of the initial verification method, the name of a network in which the DID will eventually exist, and an optional private key. From these inputs, it either generates a key or uses the passed private key to calculate the public key and from that derive the DID. In case a key needs to be generated, the challenge is to obtain the location for the key to be stored at. Since the key location depends on the public key, but key generation likely needs a location for the key to be stored at, there is a circular dependency that needs to be resolved. This can be resolved in at least two ways.
- Generate the key at a random location, then derive the actual location and move the key there
- If moving a key is not possible, then an additional mapping from key location to some storage-internal location identifier can be maintained. Then it's possible to generate the key at some storage-internal location, calculate the key location and store the mapping.
Since this also needs to happen before the DID can be derived from the public key, similar approaches can be used to work around the not-yet available DID partition key. Storages may choose to have one statically identified partition where keys are generated initially, and then moved from there. Storages whose restrictions do not allow for this, may want to use the flattened storage layout described in example layout and use the mapping approach.
Storage Test Suite
The StorageTestSuite
can be used to test the basic functionality of storage implementations. See its documentation (Rust docs, Wasm docs) for more details.
Examples
This section shows the Rust and TypeScript MemStore
implementations, which are thoroughly commented.
- Node.js
- Rust
// Copyright 2020-2022 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
import { ChainState, DID, Document, Ed25519, KeyLocation, KeyPair, KeyType, Signature, Storage, StorageTestSuite, EncryptionAlgorithm, CekAlgorithm, EncryptedData } from '../../node/identity_wasm.js';
/** An insecure, in-memory `Storage` implementation that serves as an example.
This can be passed to the `AccountBuilder` to create accounts with this as the storage. */
// Refer to the `Storage` interface docs for high-level documentation of the individual methods.
export class MemStore implements Storage {
// We use strings as keys rather than DIDs or KeyLocations because Maps use
// referential equality for object keys, and thus a primitive type needs to be used instead.
// The map from DIDs to chain states.
private _chainStates: Map<string, ChainState>;
// The map from DIDs to DID documents.
private _documents: Map<string, Document>;
// The map from DIDs to vaults.
private _vaults: Map<string, Map<string, KeyPair>>;
/** Creates a new, empty `MemStore` instance. */
constructor() {
this._chainStates = new Map();
this._documents = new Map();
this._vaults = new Map();
}
public async didCreate(network: string, fragment: string, privateKey?: Uint8Array): Promise<[DID, KeyLocation]> {
// Extract a `KeyPair` from the passed private key or generate a new one.
// For `did_create` we can assume the `KeyType` to be `Ed25519` because
// that is the only currently available signature type.
let keyPair;
if (privateKey) {
keyPair = KeyPair.tryFromPrivateKeyBytes(KeyType.Ed25519, privateKey);
} else {
keyPair = new KeyPair(KeyType.Ed25519);
}
// We create the location at which the key pair will be stored.
// Most notably, this uses the public key as an input.
const keyLocation: KeyLocation = new KeyLocation(KeyType.Ed25519, fragment, keyPair.public());
// Next we use the public key to derive the initial DID.
const did: DID = new DID(keyPair.public(), network);
// We use the vaults as the index of DIDs stored in this storage instance.
// If the DID already exists, we need to return an error. We don't want to overwrite an existing DID.
if (this._vaults.has(did.toString())) {
throw new Error("identity already exists");
}
const vault = this._vaults.get(did.toString());
// Get the existing vault and insert the key pair,
// or insert a new vault with the key pair.
if (vault) {
vault.set(keyLocation.canonical(), keyPair);
} else {
const newVault = new Map([[keyLocation.canonical(), keyPair]]);
this._vaults.set(did.toString(), newVault);
}
return [did, keyLocation];
}
public async didPurge(did: DID): Promise<boolean> {
// This method is supposed to be idempotent,
// so we only need to do work if the DID still exists.
// The return value signals whether the DID was actually removed during this operation.
if (this._vaults.has(did.toString())) {
this._chainStates.delete(did.toString());
this._documents.delete(did.toString());
this._vaults.delete(did.toString());
return true;
}
return false;
}
public async didExists(did: DID): Promise<boolean> {
return this._vaults.has(did.toString());
}
public async didList(): Promise<Array<DID>> {
// Get all keys from the vaults and parse them into DIDs.
return Array.from(this._vaults.keys()).map((did) => DID.parse(did));
}
public async keyGenerate(did: DID, keyType: KeyType, fragment: string): Promise<KeyLocation> {
// Generate a new key pair with the given key type.
const keyPair: KeyPair = new KeyPair(keyType);
// Derive the key location from the fragment and public key and set the `KeyType` of the location.
const keyLocation: KeyLocation = new KeyLocation(KeyType.Ed25519, fragment, keyPair.public());
const vault = this._vaults.get(did.toString());
// Get the existing vault and insert the key pair,
// or insert a new vault with the key pair.
if (vault) {
vault.set(keyLocation.canonical(), keyPair);
} else {
const newVault = new Map([[keyLocation.canonical(), keyPair]]);
this._vaults.set(did.toString(), newVault);
}
// Return the location at which the key was generated.
return keyLocation;
}
public async keyInsert(did: DID, keyLocation: KeyLocation, privateKey: Uint8Array): Promise<void> {
// Reconstruct the key pair from the given private key with the location's key type.
const keyPair: KeyPair = KeyPair.tryFromPrivateKeyBytes(keyLocation.keyType(), privateKey);
// Get the vault for the given DID.
const vault = this._vaults.get(did.toString());
// Get the existing vault and insert the key pair,
// or insert a new vault with the key pair.
if (vault) {
vault.set(keyLocation.canonical(), keyPair);
} else {
const newVault = new Map([[keyLocation.canonical(), keyPair]]);
this._vaults.set(did.toString(), newVault);
}
}
public async keyExists(did: DID, keyLocation: KeyLocation): Promise<boolean> {
// Get the vault for the given DID.
const vault = this._vaults.get(did.toString());
// Within the DID vault, check for existence of the given location.
if (vault) {
return vault.has(keyLocation.canonical());
} else {
return false
}
}
public async keyPublic(did: DID, keyLocation: KeyLocation): Promise<Uint8Array> {
// Get the vault for the given DID.
const vault = this._vaults.get(did.toString());
// Return the public key or an error if the vault or key does not exist.
if (vault) {
const keyPair: KeyPair | undefined = vault.get(keyLocation.canonical());
if (keyPair) {
return keyPair.public()
} else {
throw new Error('Key location not found')
}
} else {
throw new Error('DID not found')
}
}
public async keyDelete(did: DID, keyLocation: KeyLocation): Promise<boolean> {
// Get the vault for the given DID.
const vault = this._vaults.get(did.toString());
// This method is supposed to be idempotent, so we delete the key
// if it exists and return whether it was actually deleted during this operation.
if (vault) {
return vault.delete(keyLocation.canonical());
} else {
return false;
}
}
public async keySign(did: DID, keyLocation: KeyLocation, data: Uint8Array): Promise<Signature> {
if (keyLocation.keyType() !== KeyType.Ed25519) {
throw new Error('Unsupported Method')
}
// Get the vault for the given DID.
const vault = this._vaults.get(did.toString());
if (vault) {
const keyPair: KeyPair | undefined = vault.get(keyLocation.canonical());
if (keyPair) {
// Use the `Ed25519` API to sign the given data with the private key.
const signature: Uint8Array = Ed25519.sign(data, keyPair.private());
// Construct a new `Signature` wrapper with the returned signature bytes.
return new Signature(signature)
} else {
throw new Error('Key location not found')
}
} else {
throw new Error('DID not found')
}
}
public async dataEncrypt(did: DID, plaintext: Uint8Array, associatedData: Uint8Array, encryptionAlgorithm: EncryptionAlgorithm, cekAlgorithm: CekAlgorithm, publicKey: Uint8Array): Promise<EncryptedData> {
throw new Error('not yet implemented')
}
public async dataDecrypt(did: DID, data: EncryptedData, encryptionAlgorithm: EncryptionAlgorithm, cekAlgorithm: CekAlgorithm, privateKey: KeyLocation): Promise<Uint8Array> {
throw new Error('not yet implemented')
}
public async chainStateGet(did: DID): Promise<ChainState | undefined> {
// Lookup the chain state of the given DID.
return this._chainStates.get(did.toString());
}
public async chainStateSet(did: DID, chainState: ChainState): Promise<void> {
// Set the chain state of the given DID.
this._chainStates.set(did.toString(), chainState);
}
public async documentGet(did: DID): Promise<Document | undefined> {
// Lookup the DID document of the given DID.
return this._documents.get(did.toString())
}
public async documentSet(did: DID, document: Document): Promise<void> {
// Set the DID document of the given DID.
this._documents.set(did.toString(), document);
}
public async flushChanges(): Promise<void> {
// The MemStore doesn't need to flush changes to disk or any other persistent store,
// which is why this function does nothing.
}
}
export async function storageTestSuite() {
await StorageTestSuite.didCreateGenerateKeyTest(new MemStore());
await StorageTestSuite.didCreatePrivateKeyTest(new MemStore());
await StorageTestSuite.keyGenerateTest(new MemStore());
await StorageTestSuite.keyDeleteTest(new MemStore());
await StorageTestSuite.keyInsertTest(new MemStore());
await StorageTestSuite.didListTest(new MemStore());
await StorageTestSuite.keySignEd25519Test(new MemStore());
await StorageTestSuite.didPurgeTest(new MemStore());
}
// Copyright 2020-2022 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
use core::convert::TryFrom;
use core::fmt::Debug;
use core::fmt::Formatter;
use async_trait::async_trait;
use crypto::ciphers::aes::Aes256Gcm;
use crypto::ciphers::traits::Aead;
use crypto::hashes::sha::Sha256;
use crypto::hashes::Digest;
use hashbrown::HashMap;
use identity_core::crypto::Ed25519;
use identity_core::crypto::KeyPair;
use identity_core::crypto::KeyType;
use identity_core::crypto::PrivateKey;
use identity_core::crypto::PublicKey;
use identity_core::crypto::Sign;
use identity_core::crypto::X25519;
use identity_iota_core::did::IotaDID;
use identity_iota_core::document::IotaDocument;
use identity_iota_core::tangle::NetworkName;
use std::sync::RwLockReadGuard;
use std::sync::RwLockWriteGuard;
use zeroize::Zeroize;
use crate::error::Error;
use crate::error::Result;
use crate::identity::ChainState;
use crate::storage::Storage;
#[cfg(feature = "encryption")]
use crate::types::AgreementInfo;
#[cfg(feature = "encryption")]
use crate::types::CekAlgorithm;
#[cfg(feature = "encryption")]
use crate::types::EncryptedData;
#[cfg(feature = "encryption")]
use crate::types::EncryptionAlgorithm;
use crate::types::KeyLocation;
use crate::types::Signature;
use crate::utils::Shared;
#[cfg(feature = "encryption")]
use crypto::ciphers::aes_kw::Aes256Kw;
// The map from DIDs to chain states.
type ChainStates = HashMap<IotaDID, ChainState>;
// The map from DIDs to DID documents.
type Documents = HashMap<IotaDID, IotaDocument>;
// The map from DIDs to vaults.
type Vaults = HashMap<IotaDID, MemVault>;
// The map from key locations to key pairs, that lives within a DID partition.
type MemVault = HashMap<KeyLocation, KeyPair>;
/// An insecure, in-memory [`Storage`] implementation that serves as an example and is used in tests.
pub struct MemStore {
// Controls whether to print the storages content when debugging.
expand: bool,
// The `Shared<T>` type is simply a light wrapper around `Rwlock<T>`.
chain_states: Shared<ChainStates>,
documents: Shared<Documents>,
vaults: Shared<Vaults>,
}
impl MemStore {
/// Creates a new, empty `MemStore` instance.
pub fn new() -> Self {
Self {
expand: false,
chain_states: Shared::new(HashMap::new()),
documents: Shared::new(HashMap::new()),
vaults: Shared::new(HashMap::new()),
}
}
/// Returns whether to expand the debug representation.
pub fn expand(&self) -> bool {
self.expand
}
/// Sets whether to expand the debug representation.
pub fn set_expand(&mut self, value: bool) {
self.expand = value;
}
}
// Refer to the `Storage` interface docs for high-level documentation of the individual methods.
#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))]
#[cfg_attr(feature = "send-sync-storage", async_trait)]
impl Storage for MemStore {
async fn did_create(
&self,
network: NetworkName,
fragment: &str,
private_key: Option<PrivateKey>,
) -> Result<(IotaDID, KeyLocation)> {
// Extract a `KeyPair` from the passed private key or generate a new one.
// For `did_create` we can assume the `KeyType` to be `Ed25519` because
// that is the only currently available signature type.
let keypair: KeyPair = match private_key {
Some(private_key) => KeyPair::try_from_private_key_bytes(KeyType::Ed25519, private_key.as_ref())?,
None => KeyPair::new(KeyType::Ed25519)?,
};
// We create the location at which the key pair will be stored.
// Most notably, this uses the public key as an input.
let location: KeyLocation = KeyLocation::new(KeyType::Ed25519, fragment.to_owned(), keypair.public().as_ref());
// Next we use the public key to derive the initial DID.
let did: IotaDID = IotaDID::new_with_network(keypair.public().as_ref(), network)
.map_err(|err| crate::Error::DIDCreationError(err.to_string()))?;
// Obtain exclusive access to the vaults.
let mut vaults: RwLockWriteGuard<'_, _> = self.vaults.write()?;
// We use the vaults as the index of DIDs stored in this storage instance.
// If the DID already exists, we need to return an error. We don't want to overwrite an existing DID.
if vaults.contains_key(&did) {
return Err(Error::IdentityAlreadyExists);
}
// Obtain the exiting mem vault or create a new one.
let vault: &mut MemVault = vaults.entry(did.clone()).or_default();
// Insert the key pair at the previously created location.
vault.insert(location.clone(), keypair);
// Return did and location.
Ok((did, location))
}
async fn did_purge(&self, did: &IotaDID) -> Result<bool> {
// This method is supposed to be idempotent,
// so we only need to do work if the DID still exists.
// The return value signals whether the DID was actually removed during this operation.
if self.vaults.write()?.remove(did).is_some() {
let _ = self.documents.write()?.remove(did);
let _ = self.chain_states.write()?.remove(did);
Ok(true)
} else {
Ok(false)
}
}
async fn did_exists(&self, did: &IotaDID) -> Result<bool> {
// Note that any failure to get access to the storage and do the actual existence check
// should result in an error rather than returning `false`.
Ok(self.vaults.read()?.contains_key(did))
}
async fn did_list(&self) -> Result<Vec<IotaDID>> {
Ok(self.vaults.read()?.keys().cloned().collect())
}
async fn key_generate(&self, did: &IotaDID, key_type: KeyType, fragment: &str) -> Result<KeyLocation> {
// Obtain exclusive access to the vaults.
let mut vaults: RwLockWriteGuard<'_, _> = self.vaults.write()?;
// Get or insert the MemVault.
let vault: &mut MemVault = vaults.entry(did.clone()).or_default();
// Generate a new key pair for the given `key_type`.
let keypair: KeyPair = KeyPair::new(key_type)?;
// Derive the key location from the fragment and public key and set the `KeyType` of the location.
let location: KeyLocation = KeyLocation::new(key_type, fragment.to_owned(), keypair.public().as_ref());
vault.insert(location.clone(), keypair);
// Return the location at which the key was generated.
Ok(location)
}
async fn key_insert(&self, did: &IotaDID, location: &KeyLocation, mut private_key: PrivateKey) -> Result<()> {
// Obtain exclusive access to the vaults.
let mut vaults: RwLockWriteGuard<'_, _> = self.vaults.write()?;
// Get or insert the MemVault.
let vault: &mut MemVault = vaults.entry(did.clone()).or_default();
// Reconstruct the key pair from the given private key by inspecting the location for its key type.
// Then insert the key at the given location.
match location.key_type {
KeyType::Ed25519 => {
let keypair: KeyPair = KeyPair::try_from_private_key_bytes(KeyType::Ed25519, private_key.as_ref())
.map_err(|err| Error::InvalidPrivateKey(err.to_string()))?;
private_key.zeroize();
vault.insert(location.to_owned(), keypair);
Ok(())
}
KeyType::X25519 => {
let keypair: KeyPair = KeyPair::try_from_private_key_bytes(KeyType::X25519, private_key.as_ref())
.map_err(|err| Error::InvalidPrivateKey(err.to_string()))?;
private_key.zeroize();
vault.insert(location.to_owned(), keypair);
Ok(())
}
}
}
async fn key_exists(&self, did: &IotaDID, location: &KeyLocation) -> Result<bool> {
// Obtain read access to the vaults.
let vaults: RwLockReadGuard<'_, _> = self.vaults.read()?;
// Within the DID vault, check for existence of the given location.
if let Some(vault) = vaults.get(did) {
return Ok(vault.contains_key(location));
}
Ok(false)
}
async fn key_public(&self, did: &IotaDID, location: &KeyLocation) -> Result<PublicKey> {
// Obtain read access to the vaults.
let vaults: RwLockReadGuard<'_, _> = self.vaults.read()?;
// Lookup the vault for the given DID.
let vault: &MemVault = vaults.get(did).ok_or(Error::KeyVaultNotFound)?;
// Lookup the key pair within the vault.
let keypair: &KeyPair = vault.get(location).ok_or(Error::KeyNotFound)?;
// Return the public key.
Ok(keypair.public().clone())
}
async fn key_delete(&self, did: &IotaDID, location: &KeyLocation) -> Result<bool> {
// Obtain read access to the vaults.
let mut vaults: RwLockWriteGuard<'_, _> = self.vaults.write()?;
// Lookup the vault for the given DID.
let vault: &mut MemVault = vaults.get_mut(did).ok_or(Error::KeyVaultNotFound)?;
// This method is supposed to be idempotent, so we delete the key
// if it exists and return whether it was actually deleted during this operation.
Ok(vault.remove(location).is_some())
}
async fn key_sign(&self, did: &IotaDID, location: &KeyLocation, data: Vec<u8>) -> Result<Signature> {
// Obtain read access to the vaults.
let vaults: RwLockReadGuard<'_, _> = self.vaults.read()?;
// Lookup the vault for the given DID.
let vault: &MemVault = vaults.get(did).ok_or(Error::KeyVaultNotFound)?;
// Lookup the key pair within the vault.
let keypair: &KeyPair = vault.get(location).ok_or(Error::KeyNotFound)?;
match location.key_type {
KeyType::Ed25519 => {
assert_eq!(keypair.type_(), KeyType::Ed25519);
// Use the `Ed25519` API to sign the given data with the private key.
let signature: [u8; 64] = Ed25519::sign(&data, keypair.private())?;
// Construct a new `Signature` wrapper with the returned signature bytes.
let signature: Signature = Signature::new(signature.to_vec());
Ok(signature)
}
KeyType::X25519 => {
// Calling key_sign on key types that cannot be signed with should return an error.
return Err(identity_did::Error::InvalidMethodType.into());
}
}
}
#[cfg(feature = "encryption")]
async fn data_encrypt(
&self,
_did: &IotaDID,
plaintext: Vec<u8>,
associated_data: Vec<u8>,
encryption_algorithm: &EncryptionAlgorithm,
cek_algorithm: &CekAlgorithm,
public_key: PublicKey,
) -> Result<EncryptedData> {
let public_key: [u8; X25519::PUBLIC_KEY_LENGTH] = public_key
.as_ref()
.try_into()
.map_err(|_| Error::InvalidPublicKey(format!("expected public key of length {}", X25519::PUBLIC_KEY_LENGTH)))?;
match cek_algorithm {
CekAlgorithm::ECDH_ES(agreement) => {
// Generate ephemeral key
let keypair: KeyPair = KeyPair::new(KeyType::X25519)?;
// Obtain the shared secret by combining the ephemeral key and the static public key
let shared_secret: [u8; 32] = X25519::key_exchange(keypair.private(), &public_key)?;
let derived_secret: Vec<u8> =
concat_kdf(cek_algorithm.name(), Aes256Gcm::KEY_LENGTH, &shared_secret, agreement)
.map_err(Error::EncryptionFailure)?;
let encrypted_data = try_encrypt(
&derived_secret,
encryption_algorithm,
&plaintext,
associated_data,
Vec::new(),
keypair.public().as_ref().to_vec(),
)?;
Ok(encrypted_data)
}
CekAlgorithm::ECDH_ES_A256KW(agreement) => {
let keypair: KeyPair = KeyPair::new(KeyType::X25519)?;
let shared_secret: [u8; 32] = X25519::key_exchange(keypair.private(), &public_key)?;
let derived_secret: Vec<u8> = concat_kdf(cek_algorithm.name(), Aes256Kw::KEY_LENGTH, &shared_secret, agreement)
.map_err(Error::EncryptionFailure)?;
let cek: Vec<u8> = generate_content_encryption_key(*encryption_algorithm)?;
let mut encrypted_cek: Vec<u8> = vec![0; cek.len() + Aes256Kw::BLOCK];
let aes_kw: Aes256Kw<'_> = Aes256Kw::new(derived_secret.as_ref());
aes_kw
.wrap_key(cek.as_ref(), &mut encrypted_cek)
.map_err(Error::EncryptionFailure)?;
let encrypted_data = try_encrypt(
&cek,
encryption_algorithm,
&plaintext,
associated_data,
encrypted_cek,
keypair.public().as_ref().to_vec(),
)?;
Ok(encrypted_data)
}
}
}
#[cfg(feature = "encryption")]
async fn data_decrypt(
&self,
did: &IotaDID,
data: EncryptedData,
encryption_algorithm: &EncryptionAlgorithm,
cek_algorithm: &CekAlgorithm,
private_key: &KeyLocation,
) -> Result<Vec<u8>> {
// Retrieves the PrivateKey from the vault
let vaults: RwLockReadGuard<'_, _> = self.vaults.read()?;
let vault: &MemVault = vaults.get(did).ok_or(Error::KeyVaultNotFound)?;
let key_pair: &KeyPair = vault.get(private_key).ok_or(Error::KeyNotFound)?;
// Decrypts the data
match key_pair.type_() {
KeyType::Ed25519 => Err(Error::InvalidPrivateKey(
"Ed25519 keys are not supported for decryption".to_owned(),
)),
KeyType::X25519 => {
let public_key: [u8; X25519::PUBLIC_KEY_LENGTH] =
data.ephemeral_public_key.clone().try_into().map_err(|_| {
Error::InvalidPublicKey(format!("expected public key of length {}", X25519::PUBLIC_KEY_LENGTH))
})?;
match cek_algorithm {
CekAlgorithm::ECDH_ES(agreement) => {
let shared_secret: [u8; 32] = X25519::key_exchange(key_pair.private(), &public_key)?;
let derived_secret: Vec<u8> =
concat_kdf(cek_algorithm.name(), Aes256Gcm::KEY_LENGTH, &shared_secret, agreement)
.map_err(Error::DecryptionFailure)?;
try_decrypt(&derived_secret, encryption_algorithm, &data)
}
CekAlgorithm::ECDH_ES_A256KW(agreement) => {
let shared_secret: [u8; 32] = X25519::key_exchange(key_pair.private(), &public_key)?;
let derived_secret: Vec<u8> =
concat_kdf(cek_algorithm.name(), Aes256Kw::KEY_LENGTH, &shared_secret, agreement)
.map_err(Error::DecryptionFailure)?;
let cek_len: usize =
data
.encrypted_cek
.len()
.checked_sub(Aes256Kw::BLOCK)
.ok_or(Error::DecryptionFailure(crypto::Error::BufferSize {
name: "plaintext cek",
needs: Aes256Kw::BLOCK,
has: data.encrypted_cek.len(),
}))?;
let mut cek: Vec<u8> = vec![0; cek_len];
let aes_kw: Aes256Kw<'_> = Aes256Kw::new(derived_secret.as_ref());
aes_kw
.unwrap_key(data.encrypted_cek.as_ref(), &mut cek)
.map_err(Error::DecryptionFailure)?;
try_decrypt(&cek, encryption_algorithm, &data)
}
}
}
}
}
async fn chain_state_get(&self, did: &IotaDID) -> Result<Option<ChainState>> {
// Lookup the chain state of the given DID.
self.chain_states.read().map(|states| states.get(did).cloned())
}
async fn chain_state_set(&self, did: &IotaDID, chain_state: &ChainState) -> Result<()> {
// Set the chain state of the given DID.
self.chain_states.write()?.insert(did.clone(), chain_state.clone());
Ok(())
}
async fn document_get(&self, did: &IotaDID) -> Result<Option<IotaDocument>> {
// Lookup the DID document of the given DID.
self.documents.read().map(|documents| documents.get(did).cloned())
}
async fn document_set(&self, did: &IotaDID, document: &IotaDocument) -> Result<()> {
// Set the DID document of the given DID.
self.documents.write()?.insert(did.clone(), document.clone());
Ok(())
}
async fn flush_changes(&self) -> Result<()> {
// The MemStore doesn't need to flush changes to disk or any other persistent store,
// which is why this function does nothing.
Ok(())
}
}
fn try_encrypt(
key: &[u8],
algorithm: &EncryptionAlgorithm,
data: &[u8],
associated_data: Vec<u8>,
encrypted_cek: Vec<u8>,
ephemeral_public_key: Vec<u8>,
) -> Result<EncryptedData> {
match algorithm {
EncryptionAlgorithm::AES256GCM => {
let nonce: &[u8] = &Aes256Gcm::random_nonce().map_err(Error::EncryptionFailure)?;
let padding: usize = Aes256Gcm::padsize(data).map(|size| size.get()).unwrap_or_default();
let mut ciphertext: Vec<u8> = vec![0; data.len() + padding];
let mut tag: Vec<u8> = [0; Aes256Gcm::TAG_LENGTH].to_vec();
Aes256Gcm::try_encrypt(key, nonce, associated_data.as_ref(), data, &mut ciphertext, &mut tag)
.map_err(Error::EncryptionFailure)?;
Ok(EncryptedData::new(
nonce.to_vec(),
associated_data,
tag,
ciphertext,
encrypted_cek,
ephemeral_public_key,
))
}
}
}
fn try_decrypt(key: &[u8], algorithm: &EncryptionAlgorithm, data: &EncryptedData) -> Result<Vec<u8>> {
match algorithm {
EncryptionAlgorithm::AES256GCM => {
let mut plaintext = vec![0; data.ciphertext.len()];
let len: usize = Aes256Gcm::try_decrypt(
key,
&data.nonce,
&data.associated_data,
&mut plaintext,
&data.ciphertext,
&data.tag,
)
.map_err(Error::DecryptionFailure)?;
plaintext.truncate(len);
Ok(plaintext)
}
}
}
/// The Concat KDF (using SHA-256) as defined in Section 5.8.1 of NIST.800-56A
fn concat_kdf(
alg: &'static str,
len: usize,
shared_secret: &[u8],
agreement: &AgreementInfo,
) -> crypto::error::Result<Vec<u8>> {
let mut digest: Sha256 = Sha256::new();
let mut output: Vec<u8> = Vec::new();
let target: usize = (len + (Sha256::output_size() - 1)) / Sha256::output_size();
let rounds: u32 = u32::try_from(target).map_err(|_| crypto::error::Error::InvalidArgumentError {
alg,
expected: "iterations can't exceed 2^32 - 1",
})?;
for count in 0..rounds {
// Iteration Count
digest.update(&(count as u32 + 1).to_be_bytes());
// Derived Secret
digest.update(shared_secret);
// AlgorithmId
digest.update(&(alg.len() as u32).to_be_bytes());
digest.update(alg.as_bytes());
// PartyUInfo
digest.update(&(agreement.apu.len() as u32).to_be_bytes());
digest.update(&agreement.apu);
// PartyVInfo
digest.update(&(agreement.apv.len() as u32).to_be_bytes());
digest.update(&agreement.apv);
// SuppPubInfo
digest.update(&agreement.pub_info);
// SuppPrivInfo
digest.update(&agreement.priv_info);
output.extend_from_slice(&digest.finalize_reset());
}
output.truncate(len);
Ok(output)
}
impl Debug for MemStore {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
if self.expand {
f.debug_struct("MemStore")
.field("chain_states", &self.chain_states)
.field("states", &self.documents)
.field("vaults", &self.vaults)
.finish()
} else {
f.write_str("MemStore")
}
}
}
impl Default for MemStore {
fn default() -> Self {
Self::new()
}
}
/// Generate a random content encryption key of suitable length for `encryption_algorithm`.
fn generate_content_encryption_key(encryption_algorithm: EncryptionAlgorithm) -> Result<Vec<u8>> {
let mut bytes: Vec<u8> = vec![0; encryption_algorithm.key_length()];
crypto::utils::rand::fill(bytes.as_mut()).map_err(Error::EncryptionFailure)?;
Ok(bytes)
}
#[cfg(test)]
mod tests {
use crate::storage::Storage;
use crate::storage::StorageTestSuite;
use super::MemStore;
fn test_memstore() -> impl Storage {
MemStore::new()
}
#[tokio::test]
async fn test_memstore_did_create_with_private_key() {
StorageTestSuite::did_create_private_key_test(test_memstore())
.await
.unwrap()
}
#[tokio::test]
async fn test_memstore_did_create_generate_key() {
StorageTestSuite::did_create_generate_key_test(test_memstore())
.await
.unwrap()
}
#[tokio::test]
async fn test_memstore_key_generate() {
StorageTestSuite::key_generate_test(test_memstore()).await.unwrap()
}
#[tokio::test]
async fn test_memstore_key_delete() {
StorageTestSuite::key_delete_test(test_memstore()).await.unwrap()
}
#[tokio::test]
async fn test_memstore_did_list() {
StorageTestSuite::did_list_test(test_memstore()).await.unwrap()
}
#[tokio::test]
async fn test_memstore_key_insert() {
StorageTestSuite::key_insert_test(test_memstore()).await.unwrap()
}
#[tokio::test]
async fn test_memstore_key_sign_ed25519() {
StorageTestSuite::key_sign_ed25519_test(test_memstore()).await.unwrap()
}
#[tokio::test]
async fn test_memstore_key_value_store() {
StorageTestSuite::key_value_store_test(test_memstore()).await.unwrap()
}
#[tokio::test]
async fn test_memstore_did_purge() {
StorageTestSuite::did_purge_test(test_memstore()).await.unwrap()
}
#[tokio::test]
async fn test_memstore_encryption() {
StorageTestSuite::encryption_test(test_memstore(), test_memstore())
.await
.unwrap()
}
}