Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env sh
npm run lint
npm run format:check
74 changes: 69 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A comprehensive command-line interface for managing W3C Verifiable Credentials,
- ✅ **Key Pair Generation**: Generate cryptographic key pairs with Multikey format
- ✅ **DID Management**: Create and manage did:web identifiers
- ✅ **W3C Verifiable Credentials**: Sign, verify and manage W3C verifiable credentials
- ✅ **OpenAttestation**: Sign, verify and wrap/unwrap OpenAttestation v2/v3 documents
- ✅ **OpenAttestation**: Sign, verify, wrap/unwrap, and encrypt/decrypt OpenAttestation v2/v3 documents
- ✅ **Token Registry**: Mint tokens to blockchain-based token registries
- ✅ **Document Store**: Deploy and manage document store contracts
- ✅ **Title Escrow**: Complete transferable records management (holder/beneficiary transfers)
Expand Down Expand Up @@ -90,6 +90,12 @@ trustvc oa-wrap

# Unwrap an OpenAttestation document
trustvc oa-unwrap

# Encrypt an Open Attestation document for safe sharing
trustvc oa-encrypt

# Decrypt an encrypted Open Attestation document
trustvc oa-decrypt
```

### Wallet Management
Expand Down Expand Up @@ -176,6 +182,8 @@ trustvc title-escrow reject-transfer-owner-holder

- **Document Unwrapping**: Uses `unwrapOA` to unwrap OpenAttestation documents.

- **Document Encryption**: Uses `oa-encrypt` to encrypt OA documents for safe sharing; use `oa-decrypt` with the same key to recover the document.

### Blockchain Operations

- **Token Registry**: Deploy token registry contracts and mint document hashes (tokenIds) to blockchain-based token registries across multiple networks (Ethereum, Polygon, XDC, Stability, Astron).
Expand All @@ -200,7 +208,10 @@ trustvc title-escrow reject-transfer-owner-holder
| | [`verify`](#verify) | Verify OpenAttestation documents |
| | [`oa-wrap`](#oa-wrap) | Wrap OpenAttestation documents |
| | [`oa-unwrap`](#oa-unwrap) | Unwrap OpenAttestation documents |
| **Token Registry** | [`token-registry deploy`](#token-registry-deploy) | Deploy token registry contracts |
| | [`oa-encrypt`](#oa-encrypt) | Encrypt an OA document for safe sharing and storage |
| | [`oa-decrypt`](#oa-decrypt) | Decrypt an OA document encrypted with oa-encrypt |
Comment thread
manishdex25 marked this conversation as resolved.
| **Token Registry** | [`mint`](#mint) | Mint tokens to blockchain registries |

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RishabhS7 please review the readme once

| | [`token-registry deploy`](#token-registry-deploy) | Deploy token registry contracts |
| | [`mint`](#mint) | Mint tokens to blockchain registries |
| | `token-registry mint` | Alternative: `mint` |
| **Document Store** | [`document-store deploy`](#document-store-deploy) | Deploy document store contracts |
Expand Down Expand Up @@ -479,6 +490,57 @@ Unwrapped OpenAttestation document(s) in the specified directory.

</details>

<details>
<summary><h4 id="oa-encrypt">oa-encrypt</h4></summary>

Encrypts an Open Attestation document for safe sharing and storage. You will be prompted for an encryption key — remember it to decrypt later.

**Usage:**

```sh
trustvc oa-encrypt
```

**Interactive Prompts:**

- Path to your Open Attestation document (raw or wrapped OA v2/v3)
- Path for the output encrypted file
- Encryption key (entered securely; not echoed)

**Output:**

Writes an encrypted document file containing `type: "encrypted-document"` and a `ciphertext` field. Only someone with the same key can decrypt it with `oa-decrypt`.

**Supported Input:**

- OpenAttestation v2 (raw or wrapped)
- OpenAttestation v3 (raw or wrapped)

</details>

<details>
<summary><h4 id="oa-decrypt">oa-decrypt</h4></summary>

Decrypts an Open Attestation document that was encrypted using `oa-encrypt`. You will be prompted for the decryption key.

**Usage:**

```sh
trustvc oa-decrypt
```

**Interactive Prompts:**

- Path to the encrypted document file
- Path for the output decrypted document
- Decryption key (entered securely; not echoed)

**Output:**

Writes the decrypted Open Attestation document (raw OA v2/v3 or wrapped OA v2/v3) to the specified path. Fails if the key is wrong or the file is not a valid encrypted OA document.

</details>

<details>
<summary><h4 id="mint">mint</h4></summary>

Expand Down Expand Up @@ -1158,9 +1220,11 @@ npm test
```
src/commands/
├── oa/
│ └── sign.ts # Sign OpenAttestation documents
│ └── wrap.ts # Wrap OpenAttestation documents
│ └── unwrap.ts # Unwrap OpenAttestation documents
│ ├── sign.ts # Sign OpenAttestation documents
│ ├── wrap.ts # Wrap OpenAttestation documents
│ ├── unwrap.ts # Unwrap OpenAttestation documents
│ ├── encrypt.ts # Encrypt OA documents for safe sharing
│ └── decrypt.ts # Decrypt OA documents
├── token-registry/
│ ├── deploy.ts # Deploy token registry contracts
│ └── mint.ts # Mint tokens to registry
Expand Down
35 changes: 31 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"start": "node dist/main.js",
"dev": "npm run build && npm start",
"build": "npm run clean && tsup",
"clean": "rm -rf dist/"
"clean": "rm -rf dist/",
"prepare": "husky"
},
"keywords": [
"trustvc"
Expand All @@ -34,7 +35,7 @@
},
"dependencies": {
"@inquirer/prompts": "^5.3.8",
"@trustvc/trustvc": "^2.9.1",
"@trustvc/trustvc": "^2.10.0",
"@types/yargs": "^17.0.32",
"chalk": "^4.1.2",
"ethers": "^6.15.0",
Expand All @@ -53,6 +54,7 @@
"eslint": "^8.57.0",
"eslint-config-prettier": "^10.1.8",
"eslint-formatter-table": "^7.32.1",
"husky": "^9.0.11",
"prettier": "^3.7.4",
"tsup": "^8.2.4",
"tsx": "^4.19.1",
Expand Down
155 changes: 155 additions & 0 deletions src/commands/oa/decrypt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import fs from 'fs';
import { input, password } from '@inquirer/prompts';
import crypto from 'crypto';
import signale from 'signale';
import { decryptString } from '@trustvc/trustvc';
import {
readDocumentFile,
getCliErrorMessage,
isErrorWithMessage,
ensureInputFileExists,
resolveOutputJsonPath,
validateInputFileExists,
} from '../../utils';

/** Derive a 64-char hex key from passphrase for AES-256 (OPEN-ATTESTATION-TYPE-1). */
const deriveKey = (passphrase: string): string =>
crypto.createHash('sha256').update(passphrase, 'utf8').digest('hex');

export const command = 'oa-decrypt';
export const describe =
'Decrypt a document that was encrypted using oa-encrypt. You will be asked for the decryption key.';

type DecryptInput = {
inputEncryptedPath: string;
outputPath: string;
key: string;
};

// Payload format: OPEN-ATTESTATION-TYPE-1 (cipherText, iv, tag, type)
const ENCRYPTED_DOCUMENT_TYPE = 'OPEN-ATTESTATION-TYPE-1';

/** Message thrown by @trustvc/trustvc when decryption fails (wrong key or corrupted data). */
const DECRYPT_FAILED_LIBRARY_MESSAGE = 'Error decrypting message';

const INVALID_PAYLOAD_MESSAGE =
'Invalid encrypted document: expected cipherText, iv, tag and type "OPEN-ATTESTATION-TYPE-1".';

export const promptForInputs = async (): Promise<DecryptInput | null> => {
const inputEncryptedPath = await input({
message: 'Enter the path to the encrypted document:',
required: true,
validate: (value: string) => {
if (!value || value.trim() === '') return 'Encrypted document path is required';
return validateInputFileExists(value);
},
});

const outputPath = await input({
message: 'Enter the path to save the decrypted document:',
required: true,
validate: (value: string) => {
if (!value || value.trim() === '') return 'Output path is required';
return true;
},
});

const key = await password({
message: 'Enter the decryption key:',
mask: '*',
validate: (value: string) => {
if (!value || value.trim() === '') return 'Decryption key is required';
return true;
},
});

return {
inputEncryptedPath: inputEncryptedPath.trim(),
outputPath: outputPath.trim(),
key: key.trim(),
};
};

type EncryptedPayload = {
cipherText: string;
iv: string;
tag: string;
type: string;
};

const DECRYPT_ERROR_OPTIONS = {
defaultMessage: 'An unexpected error occurred while decrypting the document.',
fileNotFound: 'Unable to read encrypted document. File not found at: {path}',
permissionDenied: 'Permission denied. Cannot write to: {path}',
invalidJson: (msg: string) => `Invalid encrypted file: the file is not valid JSON. ${msg}`,
} as const;

/** Validates raw payload and returns typed fields or throws with a clear message. */
function validateEncryptedPayload(payload: unknown): EncryptedPayload {
if (
payload === null ||
typeof payload !== 'object' ||
!('cipherText' in payload) ||
!('iv' in payload) ||
!('tag' in payload) ||
!('type' in payload)
) {
throw new Error(INVALID_PAYLOAD_MESSAGE);
}
const { cipherText, iv, tag, type } = payload as EncryptedPayload;
if (
typeof cipherText !== 'string' ||
typeof iv !== 'string' ||
typeof tag !== 'string' ||
type !== ENCRYPTED_DOCUMENT_TYPE
) {
throw new Error(INVALID_PAYLOAD_MESSAGE);
}
return { cipherText, iv, tag, type };
}

/** Decrypts payload with derived key; rethrows a user-friendly error on library failure. */
function decryptPayload(payload: EncryptedPayload, key: string): string {
try {
return decryptString({
...payload,
key: deriveKey(key),
});
} catch (err: unknown) {
if (isErrorWithMessage(err) && err.message === DECRYPT_FAILED_LIBRARY_MESSAGE) {
throw new Error(
'Failed to decrypt document. The password/key is likely incorrect or the file is corrupted.',
);
}
throw err;
}
}

/** Loads encrypted file, validates, decrypts, writes plaintext and shows success message. */
async function runDecrypt(answers: DecryptInput): Promise<void> {
const { inputEncryptedPath, outputPath, key } = answers;

ensureInputFileExists(inputEncryptedPath);
const rawPayload = readDocumentFile(inputEncryptedPath);
const payload = validateEncryptedPayload(rawPayload);
const documentString = decryptPayload(payload, key);

const { path: outputFilePath, generated } = resolveOutputJsonPath(outputPath, 'decrypted');
fs.writeFileSync(outputFilePath, documentString, 'utf8');
if (generated) {
signale.success(`No output filename provided. Decrypted document saved to: ${outputFilePath}`);
} else {
signale.success(`Decrypted document saved to: ${outputFilePath}`);
}
}

export const handler = async (): Promise<void> => {
try {
const answers = await promptForInputs();
if (!answers) return;
await runDecrypt(answers);
} catch (err: unknown) {
signale.error(getCliErrorMessage(err, DECRYPT_ERROR_OPTIONS));
process.exitCode = 1;
}
};
Loading