@open-secure-viewer/irm
v9.1.0
Published
IRM key exchange module for Open Secure Viewer — RSA-4096 + AES-GCM WebCrypto
Downloads
2,045
Maintainers
Readme
@open-secure-viewer/irm
Information Rights Management (IRM) key exchange for the Open Secure Viewer SDK. Implements RSA-4096 + AES-256-GCM key exchange using the browser's native WebCrypto API — zero persistence, zero backend code in the SDK.
Install
npm i @open-secure-viewer/irm @open-secure-viewer/coreHow It Works
- Browser generates a one-time RSA-4096 key pair in memory (private key never leaves the browser)
- Public key is sent to your backend
- Backend encrypts the document AES key with the RSA public key and returns the encrypted blob
- SDK decrypts the AES key in-browser and uses it to decrypt the document bytes
- Key pair is discarded after use — nothing is stored in localStorage/sessionStorage/IndexedDB
React Hook
import { useIrmLoader } from '@open-secure-viewer/react';
import { SecureViewer, useOsvViewer } from '@open-secure-viewer/react';
function ProtectedDoc() {
const { viewer } = useOsvViewer();
const { loadIrmDocument, isExchanging, irmError } = useIrmLoader(viewer);
useEffect(() => {
loadIrmDocument({
contentId: 'doc-123',
getViewUrl: async (publicKey) => {
const res = await fetch(`/api/view?pk=${publicKey}`);
const { url } = await res.json();
return url;
},
});
}, []);
}IntraLinks Proprietary PDF Filter (customHandlerId: 342553)
Some IntraLinks IRM PDFs embed a proprietary /Filter inside the PDF
/Encrypt dictionary. PDFTron handles them via customHandlerId: 342553;
the open-source PDFium build the SDK uses does not ship that handler, so
loading fails with Unsupported security scheme even when the password is
correct.
@open-secure-viewer/irm exports IntraLinksPdfHandler — a
PdfSecurityHandler plugin that the core SDK invokes automatically after
PDFium's initial load fails. It tries two strategies:
- AES-GCM whole-payload decrypt — for backends that wrap the document
bytes in AES-GCM (key =
SHA-256(password), IV =nonce). Mirrors the XLSX renderer's transport-layer fallback. - PDF
/Encryptdictionary patching — rewrites the proprietary/Filterto/Standardso PDFium can decrypt the bytes with the supplied password. Works when the underlying cipher matches ISO 32000 AES.
Register once at app startup
import { OsvViewer } from '@open-secure-viewer/core';
import { IntraLinksPdfHandler } from '@open-secure-viewer/irm';
OsvViewer.registerPdfSecurityHandler(new IntraLinksPdfHandler());Use from DocumentViewer.tsx
After registering the handler, pass the password + nonce returned from the
RSA key exchange via fetchOpts — the SDK fetches and decrypts the PDF
automatically.
import React, { useState, useEffect } from 'react';
import { SecureViewer } from '@open-secure-viewer/react';
import { registerOfficeRenderers } from '@open-secure-viewer/office';
import { OsvViewer } from '@open-secure-viewer/core';
import { IntraLinksPdfHandler } from '@open-secure-viewer/irm';
import { IlLoading } from 'il-framework-component/src/components/IlLoading';
import { IlContent } from '../ContentViewerComponent/contentViewerTypes';
import { consumeContentKey } from '../../../services/secureViewerService';
registerOfficeRenderers();
OsvViewer.registerPdfSecurityHandler(new IntraLinksPdfHandler());
interface IDocumentViewerComponentProps {
viewerContent: IlContent;
isContentLoading: boolean;
}
interface IIrmFetchOpts {
password: string;
nonce?: string;
credentials: RequestCredentials;
}
const DocumentViewer: React.FC<IDocumentViewerComponentProps> = ({
viewerContent,
isContentLoading,
}) => {
const [error, setError] = useState<string>();
const [irmFetchOpts, setIrmFetchOpts] = useState<IIrmFetchOpts | null>(null);
const { id, downloadUrl, isIRMProtected } = viewerContent;
useEffect(() => {
setError(undefined);
setIrmFetchOpts(null);
if (!isIRMProtected || !downloadUrl || !id) return;
(async () => {
const contentKey = await consumeContentKey(id, downloadUrl);
setIrmFetchOpts({
password: contentKey.password,
nonce: contentKey.nonce,
credentials: 'include',
});
})().catch((err: Error) => setError(err.message));
}, [id, downloadUrl, isIRMProtected]);
if (error) return <p role="alert">{error}</p>;
const isLoading = isContentLoading || (isIRMProtected && irmFetchOpts === null);
const fetchOpts = isIRMProtected && irmFetchOpts
? irmFetchOpts
: { credentials: 'include' as RequestCredentials };
return isLoading ? (
<IlLoading />
) : (
<SecureViewer
source={downloadUrl}
wasmUrl={`${process.env.PUBLIC_URL}/pdfium/pdfium.wasm`}
security={{
disablePrint: true,
disableCopy: true,
disableContextMenu: true,
watermark: {
diagonal: {
text: `CONFIDENTIAL — ${new Date().toLocaleDateString()}`,
opacity: 0.12,
fontSize: 40,
},
},
}}
style={{ width: '100%', height: 'calc(100vh - 48px)' }}
fetchOpts={fetchOpts}
onError={(err) => setError(err.message)}
/>
);
};
export default DocumentViewer;Customising the handler
If the IntraLinks /Filter name in your captured PDFs differs from the
defaults (/IntraLinks, /ILSecurityHandler, /ILDRM, /ILRM), or if
customHandlerId: 342553 uses non-standard /V / /R / /O / /U
values, override via constructor options:
OsvViewer.registerPdfSecurityHandler(
new IntraLinksPdfHandler({
filterNames: ['IntraLinksCustom', 'MyCorp.IL'],
debug: true,
}),
);Inspect the /Encrypt dictionary in a captured IRM PDF (e.g. with
qpdf --qdf input.pdf - or any hex editor) to discover the exact filter
name. The il-viewer-app source is also authoritative — search for the
PDFTron customHandlerId registration to confirm the filter token and the
cipher parameters used.
Links
License
MIT
