Asset Tagging

Asset Tagging adds metadata to an encrypted asset that is cryptographically signed. It is stored in the header that is prepended to the encrypted asset. This Tag can be used to manage the asset in any environment, and allow you to track its usage across any systems or workflows.
The metadata is available for viewing on the encrypted Asset. Any changes made to the metadata after it has been signed, will invalidate the Asset, making it impossible to decrypt.
Creating an Asset
Basic Asset Creation
Assets are created by passing metadata to the encrypt method when you encrypt data. The metadata is stored in an encrypted header that's cryptographically signed and prepended to your encrypted payload.
Basic Syntax:
const encrypted = await ring.encrypt(data, {
asset: {
id: 'unique-identifier',
name: 'Display Name',
type: 'asset-type',
// ... additional metadata
}
})
Minimal Asset Example:
The simplest asset requires just an id and name:
const ring = await client.ring.get('Documents')
const encrypted = await ring.encrypt(documentContent, {
asset: {
id: 'doc-001',
name: 'Q4 Financial Report'
}
})
// Store the encrypted data
await db.documents.insert({
id: 'doc-001',
content: encrypted
})
Recommended Asset Structure:
For better organization and searchability, include type and domain-specific metadata:
const encrypted = await ring.encrypt(patientRecord, {
asset: {
id: 'patient-12345',
name: 'Patient 12345 - Medical Record',
type: 'phi',
patientId: '12345',
department: 'cardiology',
recordType: 'medical-history',
classification: 'highly-sensitive'
}
})
Common Metadata Fields:
While you can include any metadata you need, these fields are commonly used:
| Field | Purpose | Example |
|---|---|---|
id |
Unique identifier for the asset | 'user-123', 'doc-456' |
name |
Display name (shown in Dashboard) | 'John Doe - SSN' |
type |
Asset category/classification | 'phi', 'pii', 'financial' |
classification |
Security classification level | 'public', 'confidential', 'highly-sensitive' |
department |
Owning department | 'hr', 'finance', 'legal' |
createdBy |
User who created the asset | 'user-789' |
createdAt |
Creation timestamp | '2024-01-15T10:00:00Z' |
What Happens When You Create an Asset
- Metadata is hashed - A SHA256 hash of your metadata is created
- Data is encrypted - Your actual data is encrypted using AES-256-GCM
- Header is created - The signed metadata header (including the Asset ID) is created
- Header is prepended - The header is prepended to the encrypted data
- Activity is logged - The creation is recorded in the Activity feed
What You Get Back:
The encrypt method returns a base64-encoded string containing the header and encrypted payload combined:
const encrypted = await ring.encrypt(data, {
asset: { id: 'doc-123', name: 'Contract' }
})
// encrypted is a base64-encoded string:
// "eyJoZWFkZXIiOi...base64...encrypted-data"
// It contains both the signed header and encrypted payload
The Asset ID and all metadata are embedded in the header portion of this encrypted string. When you decrypt later with the SDK, the header is automatically parsed and validated.
Example: Creating Different Asset Types
User PII:
const encrypted = await ring.encrypt(userData, {
asset: {
id: `user-${userId}`,
name: `${userName} - Email Address`,
type: 'pii',
userId,
field: 'email',
classification: 'confidential',
gdpr: {
legalBasis: 'consent',
consentDate: '2024-01-15'
}
}
})
Financial Data:
const encrypted = await ring.encrypt(transactionData, {
asset: {
id: `transaction-${txnId}`,
name: `Transaction ${txnId}`,
type: 'financial',
transactionId: txnId,
amount: 1000.00,
currency: 'USD',
classification: 'confidential',
compliance: {
sox: true,
auditRequired: true
}
}
})
Healthcare PHI:
const encrypted = await ring.encrypt(medicalRecord, {
asset: {
id: `patient-${patientId}`,
name: `Patient ${patientId} - Lab Results`,
type: 'phi',
patientId,
recordType: 'lab-results',
testDate: '2024-01-20',
classification: 'highly-sensitive',
hipaa: {
purpose: 'treatment',
authorizedBy: 'dr-smith-123'
},
retention: {
retentionYears: 7,
destructionDate: '2031-01-20'
}
}
})
Document/File:
const encrypted = await ring.encrypt(fileContent, {
asset: {
id: `file-${fileId}`,
name: 'Contract - Acme Corp.pdf',
type: 'document',
file: {
originalName: 'contract-acme-corp.pdf',
mimeType: 'application/pdf',
size: 1024000,
version: '1.0'
},
ownership: {
owner: 'legal-team',
createdBy: 'user-123',
createdAt: '2024-01-15T10:00:00Z'
},
classification: 'confidential'
}
})
Best Practices:
- Always include
idandname- Required for identification and Dashboard display - Use consistent
typevalues - Makes querying and filtering easier - Include classification - Helps with data governance and access control
- Add timestamps - Include
createdAtor similar for audit trails - Store the
assetId- You'll need it for updates and queries - Keep metadata lightweight - Asset metadata adds to the encrypted data size
- Use descriptive names - The
namefield appears in the Dashboard
Updating Asset Metadata
Update Existing Asset Metadata
You can update the metadata for an already-encrypted asset by providing the assetId property. This is useful when the asset's state or metadata changes over time (e.g., workflow state changes, approval updates, or classification changes).
Important: The encrypted data itself doesn't change - only the metadata associated with it is updated in the Activity feed.
// Initial encryption with workflow state
const encrypted = await ring.encrypt(document, {
asset: {
id: 'doc-123',
name: 'Contract - Acme Corp',
type: 'legal-document',
workflow: {
state: 'draft',
createdBy: 'user-456',
createdAt: '2024-01-15T10:00:00Z'
},
classification: 'confidential'
}
})
// Store encrypted document
await db.documents.insert({
id: 'doc-123',
content: encrypted,
assetId: 'asset-abc-123' // Store the asset ID from the response
})
// Later: Update metadata when document is submitted for review
const updated = await ring.encrypt(document, {
assetId: 'asset-abc-123', // Reference existing asset
asset: {
id: 'doc-123',
name: 'Contract - Acme Corp (Under Review)',
type: 'legal-document',
workflow: {
state: 'under-review',
submittedBy: 'user-456',
submittedAt: '2024-01-16T14:00:00Z',
assignedTo: 'manager-789',
reviewDueDate: '2024-01-20'
},
classification: 'confidential'
}
})
// Later: Update metadata when approved
const approved = await ring.encrypt(document, {
assetId: 'asset-abc-123', // Same asset ID
asset: {
id: 'doc-123',
name: 'Contract - Acme Corp (Approved)',
type: 'legal-document',
workflow: {
state: 'approved',
approvedBy: 'manager-789',
approvedAt: '2024-01-18T09:30:00Z',
finalizedBy: 'legal-director-001'
},
classification: 'confidential',
signedVersion: true
}
})
Common Update Scenarios:
1. Workflow State Changes
// Document moves through approval workflow
async function updateDocumentWorkflow(docId, assetId, newState, metadata) {
const ring = await client.ring.get('Documents')
const encrypted = await ring.encrypt(documentContent, {
assetId, // Existing asset
asset: {
id: docId,
name: `Document ${docId} - ${newState}`,
type: 'document',
workflow: {
state: newState,
updatedAt: new Date().toISOString(),
...metadata
}
}
})
return encrypted
}
// Usage
await updateDocumentWorkflow('doc-123', 'asset-abc-123', 'approved', {
approvedBy: 'manager-789',
approvalDate: '2024-01-18'
})
2. Compliance Status Updates
// Initial PHI encryption
const encrypted = await ring.encrypt(patientRecord, {
asset: {
id: `patient-${patientId}`,
name: `Patient ${patientId} - Medical Record`,
type: 'phi',
hipaa: {
purpose: 'treatment',
authorizedBy: 'dr-smith-123',
consentStatus: 'active'
}
}
})
// Later: Patient revokes consent
const updated = await ring.encrypt(patientRecord, {
assetId: existingAssetId,
asset: {
id: `patient-${patientId}`,
name: `Patient ${patientId} - Medical Record (Consent Revoked)`,
type: 'phi',
hipaa: {
purpose: 'treatment',
authorizedBy: 'dr-smith-123',
consentStatus: 'revoked',
revokedAt: '2024-02-01T10:00:00Z',
revokedBy: patientId
},
accessRestricted: true
}
})
3. Classification Changes
// Initial encryption with lower classification
const encrypted = await ring.encrypt(document, {
asset: {
id: 'report-456',
name: 'Project Report - Alpha',
type: 'document',
classification: 'internal'
}
})
// Later: Upgraded to confidential due to sensitive findings
const reclassified = await ring.encrypt(document, {
assetId: existingAssetId,
asset: {
id: 'report-456',
name: 'Project Report - Alpha (Confidential)',
type: 'document',
classification: 'confidential',
reclassification: {
previousLevel: 'internal',
newLevel: 'confidential',
reclassifiedAt: '2024-01-20T15:00:00Z',
reclassifiedBy: 'security-officer-123',
reason: 'Contains trade secrets'
},
accessControl: {
restrictedTo: ['executives', 'legal']
}
}
})
4. Ownership Transfer
// Transfer document ownership
const transferred = await ring.encrypt(document, {
assetId: existingAssetId,
asset: {
id: 'doc-789',
name: 'Customer Contract - Transferred',
type: 'legal-document',
ownership: {
previousOwner: 'user-123',
currentOwner: 'user-456',
transferredAt: '2024-01-25T12:00:00Z',
transferReason: 'Employee departure',
approvedBy: 'manager-789'
}
}
})
5. Audit Trail Enhancement
// Add audit information after security review
const audited = await ring.encrypt(data, {
assetId: existingAssetId,
asset: {
id: 'data-001',
name: 'Sensitive Data - Audited',
type: 'sensitive-data',
audit: {
lastReviewed: '2024-01-30T10:00:00Z',
reviewedBy: 'security-team',
findings: 'compliant',
nextReviewDate: '2024-04-30',
certifications: ['SOC2', 'ISO27001']
}
}
})
Best Practices for Updates:
- Always include the
nameproperty - Keep Dashboard display names updated - Preserve important context - Include previous states or change history in metadata
- Timestamp all changes - Add
updatedAtor similar timestamps - Track who made changes - Include
updatedByor similar user identifiers - Document the reason - Add context for why metadata was updated
Parent Assets and Hierarchical Relationships
Linking Assets with Parent Relationships
You can establish hierarchical relationships between assets using the parents property. The parents property is an array of Asset IDs that sits at the same level as the asset property. This creates parent-child relationships that help track related data and maintain organizational structure in the Activity feed.
When to Use Parent Assets:
- Document Versions - Link new versions to original documents
- Related Records - Connect child records to parent entities (e.g., transactions to accounts)
- Nested Data Structures - Represent hierarchical data (e.g., folders containing files)
- Workflow Lineage - Track data transformations and processing steps
- Compliance Auditing - Maintain relationships between related sensitive data
- Multi-Parent Relationships - An asset can have multiple parents (e.g., a document that belongs to multiple projects)
Basic Parent-Child Relationship:
// 1. Create parent asset (e.g., customer account)
const parentEncrypted = await ring.encrypt(accountData, {
asset: {
id: 'account-123',
name: 'Customer Account - Acme Corp',
type: 'account',
accountId: 'account-123',
createdAt: '2024-01-15T10:00:00Z'
}
})
// Store the parent asset ID from the response
const parentAssetId = parentEncrypted.assetId // e.g., 'asset-parent-abc-123'
// 2. Create child assets that reference the parent
const childEncrypted = await ring.encrypt(transactionData, {
parents: [parentAssetId], // Array of parent asset IDs
asset: {
id: 'transaction-456',
name: 'Transaction - Purchase',
type: 'transaction',
transactionId: 'transaction-456',
amount: 1000.00,
timestamp: '2024-01-16T14:30:00Z'
}
})
// Another child asset with same parent
const anotherChild = await ring.encrypt(transactionData2, {
parents: [parentAssetId], // Same parent
asset: {
id: 'transaction-789',
name: 'Transaction - Refund',
type: 'transaction',
transactionId: 'transaction-789',
amount: -50.00,
timestamp: '2024-01-17T09:00:00Z'
}
})
// Result: Activity feed shows hierarchy
// └─ Customer Account - Acme Corp
// ├─ Transaction - Purchase
// └─ Transaction - Refund
Adding Parents to Existing Asset:
You can update an existing asset to add parent relationships:
// Existing standalone asset
const originalEncrypted = await ring.encrypt(document, {
asset: {
id: 'doc-999',
name: 'Standalone Document',
type: 'document'
}
})
// Later: Add parent relationship (e.g., discovered this belongs to a project)
const updated = await ring.encrypt(document, {
assetId: 'asset-doc-999', // Existing asset
parents: ['asset-project-alpha-123'], // Add parent link
asset: {
id: 'doc-999',
name: 'Standalone Document (Project Alpha)',
type: 'document',
linkedAt: '2024-01-20T10:00:00Z',
linkedBy: 'user-456',
linkedReason: 'Document categorization update'
}
})
Multiple Parents:
An asset can belong to multiple parents, useful for cross-cutting concerns:
// Create multiple parent assets
const projectAlpha = await ring.encrypt(projectData, {
asset: {
id: 'project-alpha',
name: 'Project Alpha',
type: 'project'
}
})
const legalDept = await ring.encrypt(deptData, {
asset: {
id: 'dept-legal',
name: 'Legal Department',
type: 'department'
}
})
// Document belongs to both a project and a department
const document = await ring.encrypt(documentData, {
parents: [projectAlpha.assetId, legalDept.assetId], // Multiple parents
asset: {
id: 'contract-001',
name: 'Vendor Contract - Acme Corp',
type: 'legal-document',
relatedTo: 'Project Alpha and Legal Department'
}
})
// Result: Document appears under both parents in Activity feed
// └─ Project Alpha
// └─ Vendor Contract - Acme Corp
// └─ Legal Department
// └─ Vendor Contract - Acme Corp
Use Case Examples:
1. Document Version Control
// Original document (v1.0)
const v1 = await ring.encrypt(documentV1, {
asset: {
id: 'contract-acme',
name: 'Contract - Acme Corp v1.0',
type: 'legal-document',
version: '1.0',
createdAt: '2024-01-15T10:00:00Z',
createdBy: 'user-123'
}
})
const v1AssetId = v1.assetId
// Revised document (v2.0) - linked to original
const v2 = await ring.encrypt(documentV2, {
parents: [v1AssetId], // Link to v1.0
asset: {
id: 'contract-acme-v2',
name: 'Contract - Acme Corp v2.0',
type: 'legal-document',
version: '2.0',
previousVersion: '1.0',
revisedAt: '2024-01-20T14:00:00Z',
revisedBy: 'user-456',
changesSummary: 'Updated payment terms and added termination clause'
}
})
// Final version (v3.0) - linked to v2.0
const v3 = await ring.encrypt(documentV3, {
parents: [v2.assetId], // Link to v2.0
asset: {
id: 'contract-acme-v3',
name: 'Contract - Acme Corp v3.0 (Final)',
type: 'legal-document',
version: '3.0',
previousVersion: '2.0',
finalizedAt: '2024-01-25T09:00:00Z',
signedBy: ['acme-corp', 'our-company'],
status: 'executed'
}
})
// Result: Version chain in Activity feed
// └─ Contract v1.0
// └─ Contract v2.0
// └─ Contract v3.0 (Final)
2. Patient Records Hierarchy (HIPAA)
// Patient master record (parent)
const patientRecord = await ring.encrypt(patientData, {
asset: {
id: `patient-${patientId}`,
name: `Patient ${patientId} - Master Record`,
type: 'phi-master',
patientId,
createdAt: '2024-01-15T10:00:00Z'
}
})
const patientAssetId = patientRecord.assetId
// Medical history (child)
const medicalHistory = await ring.encrypt(historyData, {
parents: [patientAssetId],
asset: {
id: `patient-${patientId}-history`,
name: `Patient ${patientId} - Medical History`,
type: 'phi',
recordType: 'medical-history',
hipaa: { purpose: 'treatment' }
}
})
// Lab results (child)
const labResults = await ring.encrypt(labData, {
parents: [patientAssetId],
asset: {
id: `patient-${patientId}-lab-2024-01`,
name: `Patient ${patientId} - Lab Results Jan 2024`,
type: 'phi',
recordType: 'lab-results',
testDate: '2024-01-20',
hipaa: { purpose: 'treatment' }
}
})
// Prescriptions (child)
const prescriptions = await ring.encrypt(rxData, {
parents: [patientAssetId],
asset: {
id: `patient-${patientId}-rx`,
name: `Patient ${patientId} - Prescriptions`,
type: 'phi',
recordType: 'prescriptions',
hipaa: { purpose: 'treatment' }
}
})
// Result: Patient record hierarchy
// └─ Patient 12345 - Master Record
// ├─ Medical History
// ├─ Lab Results Jan 2024
// └─ Prescriptions
3. Folder/File Hierarchy
// Folder (parent)
const folder = await ring.encrypt(folderMetadata, {
asset: {
id: 'folder-legal-2024',
name: 'Legal Documents 2024',
type: 'folder',
path: '/legal/2024',
createdAt: '2024-01-01T00:00:00Z'
}
})
const folderAssetId = folder.assetId
// Files in folder (children)
const file1 = await ring.encrypt(fileContent1, {
parents: [folderAssetId],
asset: {
id: 'file-contract-001',
name: 'Contract - Client A.pdf',
type: 'file',
path: '/legal/2024/Contract - Client A.pdf',
mimeType: 'application/pdf'
}
})
const file2 = await ring.encrypt(fileContent2, {
parents: [folderAssetId],
asset: {
id: 'file-nda-002',
name: 'NDA - Partner B.docx',
type: 'file',
path: '/legal/2024/NDA - Partner B.docx',
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
}
})
// Result: Folder structure
// └─ Legal Documents 2024
// ├─ Contract - Client A.pdf
// └─ NDA - Partner B.docx
4. Data Processing Pipeline
// Source data (parent)
const sourceData = await ring.encrypt(rawData, {
asset: {
id: 'import-2024-01-15',
name: 'Customer Data Import - Jan 15',
type: 'data-import',
source: 'crm-system',
recordCount: 10000,
importedAt: '2024-01-15T08:00:00Z'
}
})
const sourceAssetId = sourceData.assetId
// Validated data (child - step 1)
const validatedData = await ring.encrypt(cleanedData, {
parents: [sourceAssetId],
asset: {
id: 'validated-2024-01-15',
name: 'Validated Customer Data - Jan 15',
type: 'data-processing',
processingStep: 'validation',
recordCount: 9850,
invalidRecords: 150,
processedAt: '2024-01-15T09:00:00Z'
}
})
// Enriched data (child - step 2)
const enrichedData = await ring.encrypt(enrichedDataContent, {
parents: [validatedData.assetId], // Parent is previous step
asset: {
id: 'enriched-2024-01-15',
name: 'Enriched Customer Data - Jan 15',
type: 'data-processing',
processingStep: 'enrichment',
recordCount: 9850,
fieldsAdded: ['segment', 'lifetime_value'],
processedAt: '2024-01-15T10:00:00Z'
}
})
// Result: Processing pipeline lineage
// └─ Customer Data Import - Jan 15
// └─ Validated Customer Data - Jan 15
// └─ Enriched Customer Data - Jan 15
5. Multi-Level Organizational Hierarchy
// Organization (top level)
const org = await ring.encrypt(orgData, {
asset: {
id: 'org-acme',
name: 'Organization - Acme Corp',
type: 'organization',
orgId: 'acme-corp'
}
})
// Department (child of org)
const dept = await ring.encrypt(deptData, {
parents: [org.assetId],
asset: {
id: 'dept-engineering',
name: 'Department - Engineering',
type: 'department',
deptId: 'engineering'
}
})
// Team (child of department)
const team = await ring.encrypt(teamData, {
parents: [dept.assetId],
asset: {
id: 'team-platform',
name: 'Team - Platform Engineering',
type: 'team',
teamId: 'platform-eng'
}
})
// Employee records (children of team)
const employee = await ring.encrypt(employeeData, {
parents: [team.assetId],
asset: {
id: 'employee-123',
name: 'Employee - John Doe',
type: 'employee-record',
employeeId: 'emp-123'
}
})
// Result: Multi-level hierarchy
// └─ Organization - Acme Corp
// └─ Department - Engineering
// └─ Team - Platform Engineering
// └─ Employee - John Doe
Benefits of Parent Assets:
- Audit Trail - Track relationships between related data in Activity feed
- Data Lineage - Understand data provenance and transformations
- Compliance - Demonstrate data relationships for regulatory requirements
- Organization - Maintain logical structure of encrypted data
- Discovery - Find all related assets by querying parent relationships
Querying Parent-Child Relationships:
// Find all children of a parent asset
const children = await assetApi.query({
'parents': parentAssetId // Find assets that have this parent
})
// Or if searching for assets with a specific parent in their parents array
const children = await assetApi.query({
'parents': { $in: [parentAssetId] }
})
// Find assets with multiple specific parents
const assetsWithBothParents = await assetApi.query({
'parents': { $all: [parentAssetId1, parentAssetId2] }
})
// Find entire hierarchy
async function getAssetHierarchy(assetId) {
// Get the asset
const asset = await assetApi.getAsset(assetId)
// Get all children (assets that have this assetId in their parents array)
const children = await assetApi.query({
'parents': assetId
})
// Recursively get children's children
const childHierarchies = await Promise.all(
children.map(child => getAssetHierarchy(child.assetId))
)
return {
asset,
children: childHierarchies
}
}
// Find all descendants (children, grandchildren, etc.)
async function getAllDescendants(parentAssetId, results = []) {
const children = await assetApi.query({
'parents': parentAssetId
})
results.push(...children)
for (const child of children) {
await getAllDescendants(child.assetId, results)
}
return results
}
Common Use Cases
Compliance and Audit Trails
Assets provide a built-in audit trail for compliance requirements (HIPAA, GDPR, SOC2):
HIPAA - Protected Health Information (PHI)
const encrypted = await ring.encrypt(patientRecord, {
asset: {
id: `patient-${patientId}`,
name: `Patient ${patientId} - Medical Record`,
type: 'phi',
patientId,
recordType: 'medical-history',
hipaa: {
purpose: 'treatment',
authorizedBy: 'dr-smith-123',
accessReason: 'routine-checkup',
phi_category: 'medical-records'
},
retention: {
retentionYears: 7,
destructionDate: '2031-01-15'
}
}
})
// Track: Who accessed PHI, when, and for what purpose
GDPR - Personal Data Processing
const encrypted = await ring.encrypt(userData, {
asset: {
id: `user-${userId}`,
name: `User ${userId} - Personal Data`,
type: 'pii',
userId,
gdpr: {
legalBasis: 'consent',
consentId: 'consent-789',
consentDate: '2024-01-15',
dataSubjectRights: ['access', 'rectification', 'erasure'],
processingPurpose: 'customer-service'
},
dataClassification: 'personal-identifiable'
}
})
// Track: Legal basis for processing, consent, and data subject rights
PCI-DSS - Payment Card Data
const encrypted = await ring.encrypt(cardData, {
asset: {
id: `transaction-${txnId}`,
name: `Transaction ${txnId} - Card Data`,
type: 'payment',
transactionId: txnId,
pci: {
scope: 'cardholder-data',
environment: 'production',
processor: 'stripe',
maskedPan: 'xxxx-xxxx-xxxx-1234'
},
compliance: {
standard: 'pci-dss-v4',
requirement: '3.4' // Render PAN unreadable
}
}
})
// Track: Payment card data access and compliance
Data Classification and Governance
Use Assets to classify and track sensitive data across your organization:
Data Classification Levels
// Public data
const publicEncrypted = await ring.encrypt(marketingContent, {
asset: {
id: 'content-123',
name: 'Marketing Material - Q4 Campaign',
type: 'content',
classification: 'public',
department: 'marketing'
}
})
// Internal data
const internalEncrypted = await ring.encrypt(employeeDirectory, {
asset: {
id: 'directory-2024',
name: 'Employee Directory 2024',
type: 'internal',
classification: 'internal',
department: 'hr',
restrictedTo: ['employees']
}
})
// Confidential data
const confidentialEncrypted = await ring.encrypt(financialReport, {
asset: {
id: 'report-q4-2024',
name: 'Q4 Financial Report 2024',
type: 'financial',
classification: 'confidential',
department: 'finance',
restrictedTo: ['finance', 'executives'],
approvalRequired: true
}
})
// Highly sensitive data
const secretEncrypted = await ring.encrypt(tradeSecret, {
asset: {
id: 'patent-application-2024',
name: 'Patent Application - New Algorithm',
type: 'intellectual-property',
classification: 'highly-sensitive',
department: 'r&d',
restrictedTo: ['r&d-leads', 'legal'],
nda: true,
exportControl: 'itar'
}
})
Data Lifecycle Management
const encrypted = await ring.encrypt(document, {
asset: {
id: 'doc-456',
name: 'Contract - Acme Corp Partnership',
type: 'legal-document',
lifecycle: {
createdAt: '2024-01-15',
createdBy: 'user-123',
status: 'active',
retentionPeriod: '7-years',
disposalMethod: 'secure-deletion',
reviewDate: '2025-01-15'
},
ownership: {
owner: 'legal-team',
custodian: 'user-456',
steward: 'legal-director'
}
}
})
Multi-Tenant Data Isolation
Track which tenant owns each encrypted asset:
const encrypted = await ring.encrypt(tenantData, {
asset: {
id: `tenant-${tenantId}-data`,
name: `Tenant ${tenantName} - Customer Data`,
type: 'tenant-data',
tenant: {
id: tenantId,
name: tenantName,
region: 'us-west-2',
tier: 'enterprise'
},
isolation: {
dataResidency: 'us',
dedicatedKeyRing: true,
separateDatabase: true
}
}
})
// Query Activity feed by tenant ID to track cross-tenant access
File and Document Metadata
Track file-specific metadata for document management systems:
const encrypted = await ring.encrypt(fileContent, {
asset: {
id: `file-${fileId}`,
name: fileName, // Original filename for Dashboard display
type: 'document',
file: {
originalName: 'contract-2024.pdf',
mimeType: 'application/pdf',
size: 1024000, // bytes
checksum: 'sha256-hash-of-original',
version: '2.0',
previousVersionId: 'file-123-v1'
},
metadata: {
author: 'Jane Smith',
createdDate: '2024-01-15',
lastModified: '2024-01-20',
tags: ['contract', '2024', 'legal'],
folder: '/legal/contracts/2024'
}
}
})
Workflow and Approval Tracking
Track approval workflows and state changes:
const encrypted = await ring.encrypt(expenseReport, {
asset: {
id: `expense-${reportId}`,
name: `Expense Report - ${employeeName}`,
type: 'expense-report',
workflow: {
state: 'pending-approval',
submittedBy: 'employee-123',
submittedAt: '2024-01-15T10:00:00Z',
assignedTo: 'manager-456',
approvalChain: ['manager-456', 'finance-789'],
currentApprover: 'manager-456',
dueDate: '2024-01-20'
},
business: {
department: 'engineering',
project: 'project-alpha',
costCenter: 'cc-1234',
amount: 500.00,
currency: 'USD'
}
}
})
// Update workflow state with new encryption
const approved = await ring.encrypt(expenseReport, {
asset: {
id: `expense-${reportId}`,
name: `Expense Report - ${employeeName} (Approved)`,
type: 'expense-report',
workflow: {
state: 'approved',
approvedBy: 'manager-456',
approvedAt: '2024-01-16T14:30:00Z',
nextAction: 'payment-processing'
}
}
})
Structuring Asset Metadata
Best Practices
1. Use Consistent Structure
Define standard metadata schemas for each asset type:
// Standard user PII structure
const USER_PII_SCHEMA = {
id: 'string', // Unique identifier
name: 'string', // Display name for Dashboard
type: 'pii', // Asset type
userId: 'string', // User identifier
field: 'string', // Field name (e.g., 'ssn', 'email')
classification: 'string', // Data classification level
department: 'string' // Owning department
}
// Apply consistently
const encrypted = await ring.encrypt(userData, {
asset: {
id: `user-${userId}`,
name: `${userName} - SSN`,
type: 'pii',
userId,
field: 'ssn',
classification: 'highly-sensitive',
department: 'hr'
}
})
2. Include Contextual Information
Add context that helps identify why data was encrypted:
const encrypted = await ring.encrypt(data, {
asset: {
id: `record-${id}`,
name: `Customer Record - ${customerName}`,
type: 'customer-data',
context: {
action: 'customer-onboarding',
triggeredBy: 'user-123',
triggeredAt: new Date().toISOString(),
source: 'web-application',
ipAddress: '192.168.1.1',
userAgent: 'Mozilla/5.0...'
}
}
})
3. Support Querying and Filtering
Include fields that enable filtering in the Activity feed:
const encrypted = await ring.encrypt(data, {
asset: {
id: `transaction-${txnId}`,
name: `Transaction ${txnId}`,
type: 'transaction',
// Queryable fields
dateRange: '2024-01', // Monthly grouping
status: 'completed', // Filter by status
amount: 1000.00, // Numeric filtering
region: 'us-west', // Geographic filtering
customerId: 'cust-123', // Customer-specific queries
tags: ['payment', 'recurring'] // Tag-based filtering
}
})
4. Keep It Lightweight
Asset metadata adds to the encrypted data size. Balance detail with efficiency:
// Too verbose
const encrypted = await ring.encrypt(data, {
asset: {
id: 'record-123',
name: 'Record 123',
type: 'data',
veryLongDescription: 'This is an extremely long description that contains redundant information...',
metadata: {
field1: 'value1',
field2: 'value2',
// ... 50 more fields
}
}
})
// Concise and purposeful
const encrypted = await ring.encrypt(data, {
asset: {
id: 'record-123',
name: 'Customer Record - Acme Corp',
type: 'customer-data',
customerId: 'cust-456',
classification: 'confidential',
department: 'sales'
}
})
Querying Assets
Assets API Endpoints
The Assets API provides endpoints to query, retrieve, and manage asset metadata. All endpoints require authentication via API Key or JWT token.
API Endpoints
Base URL: https://your-tenant.grizzlycbg.io/assets
| Endpoint | Method | Description |
|---|---|---|
/assets/:id |
GET | Retrieve a specific asset by ID |
/assets/search |
POST | Search assets with filters and pagination |
/assets/:id |
DELETE | Delete an asset by ID |
Get Asset by ID
Retrieve a specific asset including its data, parent relationships, and full activity history.
Endpoint: GET /assets/:id
Request:
curl -X GET https://your-tenant.grizzlycbg.io/assets/asset-123 \
-H "Authorization: Bearer apikey-xxx"
Response:
{
"id": "asset-123",
"createdAt": "2024-01-15T10:00:00Z",
"data": {
"name": "Patient 12345 - Medical Record",
"type": "phi",
"patientId": "12345",
"department": "cardiology"
},
"parents": [
{
"assetId": "asset-parent-456",
"createdAt": "2024-01-15T09:00:00Z"
}
],
"activities": [
{
"id": "asset-activity-abc",
"assetId": "asset-123",
"action": "created",
"actionValid": true,
"createdAt": "2024-01-15T10:00:00Z",
"context": {
"accountId": "acc-789",
"keyringId": "ring-101",
"apikeyId": "key-202"
},
"data": {}
}
]
}
Search Assets
Search and filter assets with flexible criteria and cursor-based pagination.
Endpoint: POST /assets/search
Query Parameters:
- cursor (string, optional): Pagination cursor for next page
- limit (integer, optional): Items per page (default: 100)
Request Body (all fields optional):
{
apikeyId?: string // Filter by API Key ID
accountId?: string // Filter by Account ID
keyringId?: string // Filter by KeyRing ID
parents?: string[] // Filter by parent asset IDs
start?: string // Start time (ISO-8601)
end?: string // End time (ISO-8601)
data?: Object // Filter by asset data (supports nested propeties ie. property.child)
}
Response:
{
"data": [
{
"id": "asset-123",
"createdAt": "2024-01-15T10:00:00Z",
"data": {
"name": "Patient Record",
"type": "phi"
}
}
],
"pagination": {
"limit": 100,
"count": 25,
"hasMore": false,
"nextCursor": null
}
}
Query Examples
Filter by Asset Type (PHI)
const response = await fetch('https://your-tenant.grizzlycbg.io/assets/search?limit=50', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
data: {
type: 'phi' // Matching on data.type field
},
start: '2024-01-01T00:00:00Z',
end: '2024-01-31T23:59:59Z'
})
})
const result = await response.json()
console.log(`Found ${result.data.length} PHI assets`)
Filter by Department
const response = await fetch('https://your-tenant.grizzlycbg.io/assets/search', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
data: {
department: 'finance' // Match assets where data.department = 'finance'
},
start: '2024-01-01T00:00:00Z',
end: '2024-12-31T23:59:59Z'
})
})
const result = await response.json()
Filter by Patient ID (HIPAA Audit)
// Find all assets related to a specific patient
const response = await fetch('https://your-tenant.grizzlycbg.io/assets/search', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
data: {
type: 'phi',
patientId: 'patient-456'
}
})
})
const result = await response.json()
// Examine activity history for compliance
result.data.forEach(asset => {
console.log(`Asset: ${asset.data.name}`)
console.log(`Created: ${asset.createdAt}`)
})
Filter by KeyRing
// Find all assets encrypted with a specific KeyRing
const response = await fetch('https://your-tenant.grizzlycbg.io/assets/search', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
keyringId: 'ring-medical-records'
})
})
const result = await response.json()
Filter by Parent Assets
// Find all child assets of a parent
const response = await fetch('https://your-tenant.grizzlycbg.io/assets/search', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
parents: ['asset-parent-123'] // Assets that have this parent
})
})
const result = await response.json()
// Result includes all assets with asset-parent-123 in their parents array
result.data.forEach(child => {
console.log(`Child asset: ${child.id}`)
})
Time-Based Query (Last 24 Hours)
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()
const response = await fetch('https://your-tenant.grizzlycbg.io/assets/search', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
start: twentyFourHoursAgo
})
})
const result = await response.json()
console.log(`${result.data.length} assets created in last 24 hours`)
Complex Query (Nested Data)
// Search for assets with specific nested data structures
const response = await fetch('https://your-tenant.grizzlycbg.io/assets/search', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
data: {
type: 'phi',
hipaa: {
purpose: 'treatment' // Nested matching
}
},
start: '2024-01-01T00:00:00Z',
end: '2024-03-31T23:59:59Z'
})
})
const result = await response.json()
Combined Filters (GDPR Compliance)
// Complex audit query for GDPR data subject access request
const response = await fetch('https://your-tenant.grizzlycbg.io/assets/search', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
accountId: 'acc-customer-portal',
data: {
type: 'pii',
userId: 'user-789',
gdpr: {
legalBasis: 'consent'
}
},
start: '2023-01-01T00:00:00Z',
end: '2024-12-31T23:59:59Z'
})
})
const result = await response.json()
// Export all personal data for GDPR request
const personalData = result.data.map(asset => ({
assetId: asset.id,
createdAt: asset.createdAt,
data: asset.data
}))
Pagination (Cursor-Based)
async function getAllAssets(filters) {
let allAssets = []
let cursor = null
let hasMore = true
while (hasMore) {
const url = cursor
? `https://your-tenant.grizzlycbg.io/assets/search?cursor=${cursor}&limit=100`
: `https://your-tenant.grizzlycbg.io/assets/search?limit=100`
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(filters)
})
const result = await response.json()
allAssets = allAssets.concat(result.data)
hasMore = result.pagination.hasMore
cursor = result.pagination.nextCursor
console.log(`Fetched ${result.pagination.count} assets, total: ${allAssets.length}`)
}
return allAssets
}
// Usage: Get all PHI assets with pagination
const allPhiAssets = await getAllAssets({
data: { type: 'phi' }
})
console.log(`Total PHI assets: ${allPhiAssets.length}`)
Delete Asset
Delete an asset by ID. This operation is idempotent - returns success even if the asset doesn't exist.
Endpoint: DELETE /assets/:id
Request:
curl -X DELETE https://your-tenant.grizzlycbg.io/assets/asset-123 \
-H "Authorization: Bearer apikey-xxx"
Response:
{
"success": true
}
Activity History
When you retrieve a specific asset using GET /assets/:id, the response includes the complete activity history in the activities array. This provides an immutable audit trail of all actions performed on the asset.
// Get asset with full activity history
const response = await fetch('https://your-tenant.grizzlycbg.io/assets/asset-123', {
headers: {
'Authorization': `Bearer ${apiKey}`
}
})
const asset = await response.json()
// Examine activity trail
asset.activities.forEach(activity => {
console.log(`Action: ${activity.action}`)
console.log(`Time: ${activity.createdAt}`)
console.log(`KeyRing: ${activity.context.keyringId}`)
console.log(`API Key: ${activity.context.apikeyId}`)
console.log('---')
})
Benefits: - Immutable Audit Trail - Activity records cannot be modified or deleted - Forensic Investigation - Track all operations on sensitive data - Compliance Reporting - Demonstrate data access patterns for audits - Security Monitoring - Detect unauthorized access attempts
How Does Asset Tagging Work?
Internally, Asset tagging is meant to be cryptographically secure. A threat actor should not be able to alter the header of an encrypted payload without affecting its authenticity. This section will expose the details on how it works under-the-hood.
Asset Tagging at its core involves storing:
- SHA256 Hash of the metadata
- The IV required to decrypt the asset
Storing these two components guarantees referential integrity. Storing the hash ensures that any changes made to the metadata can be detected, and storing the IV ensures that this information is paired with the correct asset; ie the asset cannot be decrypted without this IV.
An Example
Let's walk through an example of how an Asset Tag is created and signed.
Metadata
{
"name": "patient-details.docx",
"policy": {
"hipaa": true,
"allowed_purposes": [
"clinical_trial"
]
}
}
Step 1: Creating an Asset Payload
An Asset Payload is created that contains a SHA256 hash of the json metadata, along with the IV (used in AES-256 GCM encryption) that is required to decrypt the Asset that is being tagged and encrypted.
Asset Payload
{
"hash": "<sha256 hash of the Payload>",
"keyData": "<IV>"
}
Step 2: Creating the Signed Asset
Creating the Signed Asset involves using a Signing Key and Signing IV. A unique Signing IV is created that is different from the one that is included in the keyData property of the Asset Payload. It has the following structure:
{
"asset": "<the original metadata>",
"sig": "<base64 encoded encrypted Asset Payload>",
"iv": "<the Signing IV>",
"ref": "<reference to the Signing Key>"
}
This process allows the metadata to be accessible in its original form, while ensuring the integrity of the original metadata. If the metadata is changed after signing, the hashes will not match, and the Key required to decrypt the asset will not be provided.
Asset Templates
Reusable Asset Configurations
Create templates for common asset types:
class AssetTemplates {
static userPII(userId, userName, field) {
return {
id: `user-${userId}`,
name: `${userName} - ${field.toUpperCase()}`,
type: 'pii',
userId,
field,
classification: 'highly-sensitive',
gdpr: {
legalBasis: 'legitimate-interest',
dataSubjectRights: ['access', 'rectification', 'erasure']
}
}
}
static patientPHI(patientId, recordType) {
return {
id: `patient-${patientId}`,
name: `Patient ${patientId} - ${recordType}`,
type: 'phi',
patientId,
recordType,
classification: 'highly-sensitive',
hipaa: {
phi_category: 'medical-records',
minimum_necessary: true
},
retention: {
retentionYears: 7
}
}
}
static financialData(entityId, dataType) {
return {
id: `financial-${entityId}`,
name: `Financial Data - ${dataType}`,
type: 'financial',
entityId,
dataType,
classification: 'confidential',
compliance: {
sox: true,
auditRequired: true
}
}
}
static document(fileName, owner, classification) {
return {
id: `doc-${Date.now()}`,
name: fileName,
type: 'document',
file: {
originalName: fileName,
mimeType: this.getMimeType(fileName)
},
ownership: {
owner,
uploadedAt: new Date().toISOString()
},
classification
}
}
static getMimeType(fileName) {
const ext = fileName.split('.').pop().toLowerCase()
const mimeTypes = {
'pdf': 'application/pdf',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
}
return mimeTypes[ext] || 'application/octet-stream'
}
}
// Usage
const encrypted = await ring.encrypt(userData, {
asset: AssetTemplates.userPII('user-123', 'John Doe', 'ssn')
})
const encryptedPHI = await ring.encrypt(patientRecord, {
asset: AssetTemplates.patientPHI('patient-456', 'medical-history')
})
Next Steps
- SDK Code Examples - More asset usage examples
- File Encryption Integration - File-specific asset patterns
- Database Encryption Integration - Database field asset patterns
- Security Best Practices - Asset security guidelines