@xmtp/content-type-remote-attachment
Version:
An XMTP content type to support sending file attachments that are stored off network
3 lines (2 loc) • 4.35 kB
JavaScript
import{ContentTypeId as e}from"@xmtp/content-type-primitives";import*as t from"@noble/secp256k1";import{ciphertext as n,content as a}from"@xmtp/proto";const r=new e({authorityId:"xmtp.org",typeId:"attachment",versionMajor:1,versionMinor:0});class o{get contentType(){return r}encode(e){return{type:r,parameters:{filename:e.filename,mimeType:e.mimeType},content:e.data}}decode(e){return{filename:e.parameters.filename,mimeType:e.parameters.mimeType,data:e.content}}fallback(e){return`Can’t display "${e.filename}". This app doesn’t support attachments.`}shouldPush(){return!0}}class c{aes256GcmHkdfSha256;constructor(e){if(!e.aes256GcmHkdfSha256)throw new Error("invalid ciphertext");if(e.aes256GcmHkdfSha256.payload.length<16)throw new Error(`invalid ciphertext ciphertext length: ${e.aes256GcmHkdfSha256.payload.length}`);if(32!==e.aes256GcmHkdfSha256.hkdfSalt.length)throw new Error(`invalid ciphertext salt length: ${e.aes256GcmHkdfSha256.hkdfSalt.length}`);if(12!==e.aes256GcmHkdfSha256.gcmNonce.length)throw new Error(`invalid ciphertext nonce length: ${e.aes256GcmHkdfSha256.gcmNonce.length}`);this.aes256GcmHkdfSha256=e.aes256GcmHkdfSha256}toBytes(){return n.Ciphertext.encode(this).finish()}static fromBytes(e){return new c(n.Ciphertext.decode(e))}}const s=window.crypto,i=(new Uint8Array).buffer;function d(e,t){return{name:"AES-GCM",iv:e}}async function h(e,t){const n=await s.subtle.importKey("raw",e,"HKDF",!1,["deriveKey"]);return s.subtle.deriveKey({name:"HKDF",hash:"SHA-256",salt:t,info:i},n,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}(new Uint8Array).buffer;const m=new e({authorityId:"xmtp.org",typeId:"remoteStaticAttachment",versionMajor:1,versionMinor:0});class l{static async load(n,r){const o=await fetch(n.url);if(!o.ok)throw new Error(`unable to fetch remote attachment at ${n.url}: ${o.status} ${o.statusText}`);const i=new Uint8Array(await o.arrayBuffer());if(0===i.length)throw new Error(`no payload for remote attachment at ${n.url}`);const m=new Uint8Array(await s.subtle.digest("SHA-256",i));if(t.etc.bytesToHex(m)!==n.contentDigest)throw new Error("content digest does not match");const l=new c({aes256GcmHkdfSha256:{hkdfSalt:n.salt,gcmNonce:n.nonce,payload:i}}),p=await async function(e,t){if(!e.aes256GcmHkdfSha256)throw new Error("invalid payload ciphertext");const n=await h(t,e.aes256GcmHkdfSha256.hkdfSalt),a=await s.subtle.decrypt(d(e.aes256GcmHkdfSha256.gcmNonce),n,e.aes256GcmHkdfSha256.payload);return new Uint8Array(a)}(l,n.secret),f=a.EncodedContent.decode(p);if(!f.type)throw new Error("no content type");const y=r.codecFor(new e(f.type));if(!y)throw new Error(`no codec found for ${f.type.typeId}`);return y.decode(f,r)}static async encodeEncrypted(e,n){const r=s.getRandomValues(new Uint8Array(32)),o=a.EncodedContent.encode(n.encode(e,{codecFor(){}})).finish(),i=await async function(e,t){const n=s.getRandomValues(new Uint8Array(32)),a=s.getRandomValues(new Uint8Array(12)),r=await h(t,n),o=await s.subtle.encrypt(d(a),r,e);return new c({aes256GcmHkdfSha256:{payload:new Uint8Array(o),hkdfSalt:n,gcmNonce:a}})}(o,r),m=i.aes256GcmHkdfSha256?.hkdfSalt,l=i.aes256GcmHkdfSha256?.gcmNonce,p=i.aes256GcmHkdfSha256?.payload;if(!m||!l||!p)throw new Error("missing encryption key");const f=new Uint8Array(await s.subtle.digest("SHA-256",p));return{digest:t.etc.bytesToHex(f),secret:r,salt:m,nonce:l,payload:p}}get contentType(){return m}encode(e){if(!e.url.startsWith("https"))throw new Error("scheme must be https");return{type:m,parameters:{contentDigest:e.contentDigest,salt:t.etc.bytesToHex(e.salt),nonce:t.etc.bytesToHex(e.nonce),secret:t.etc.bytesToHex(e.secret),scheme:e.scheme,contentLength:String(e.contentLength),filename:e.filename},content:(new TextEncoder).encode(e.url)}}decode(e){return{url:(new TextDecoder).decode(e.content),contentDigest:e.parameters.contentDigest,salt:t.etc.hexToBytes(e.parameters.salt),nonce:t.etc.hexToBytes(e.parameters.nonce),secret:t.etc.hexToBytes(e.parameters.secret),scheme:e.parameters.scheme,contentLength:parseInt(e.parameters.contentLength,10),filename:e.parameters.filename}}fallback(e){return`Can’t display "${e.filename}". This app doesn’t support attachments.`}shouldPush(){return!0}}export{o as AttachmentCodec,r as ContentTypeAttachment,m as ContentTypeRemoteAttachment,l as RemoteAttachmentCodec};
//# sourceMappingURL=index.js.map