Skip to content
Closed
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
12 changes: 11 additions & 1 deletion src/browser/HttpSealedFileStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ export class HttpSealedFileStream extends ReadableStream {
* @param {object} [options]
* @param {number} [options.chunkSize] - bytes per Range request (default 1 MB)
* @param {Function} [options.fetch] - fetch impl (defaults to globalThis.fetch)
* @param {Function} [options.onReady] - called after HEAD+tail validated, before body Range fetches
*/
constructor(url, { chunkSize = DEFAULT_CHUNK, fetch: fetchFn } = {}) {
constructor(url, { chunkSize = DEFAULT_CHUNK, fetch: fetchFn, onReady } = {}) {
const _fetch = fetchFn || fetch.bind(globalThis);
const state = {
url,
Expand Down Expand Up @@ -84,6 +85,15 @@ export class HttpSealedFileStream extends ReadableStream {
return;
}

if (onReady) {
try {
onReady();
} catch (e) {
controller.error(e);
return;
}
}

let pos = 0;
while (pos < state.contentSize) {
const chunkEnd = Math.min(pos + CHUNK, state.contentSize);
Expand Down
1 change: 1 addition & 0 deletions src/browser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ npm install @yeez-tech/meta-encryptor
| `filename` | string | ✅ | 下载文件名(必需,无默认值) |
| `onLog` | function | ❌ | 日志回调 `(message: string) => void` |
| `onProgress` | function | ❌ | 进度回调 `(total, processed, readBytes, writeBytes) => void` |
| `onDownloadReady` | function | ❌ | 下载就绪回调:HEAD+tail 校验完成、正文 Range 开始前触发,可用于关闭准备蒙层 |
| `onSuccess` | function | ❌ | 成功回调 `(data: { filename }) => void` |
| `onError` | function | ❌ | 错误回调 `(error: Error) => void` |

Expand Down
8 changes: 5 additions & 3 deletions src/browser/blob_download.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { Unsealer } from './Unsealer.js'
import { HttpSealedFileStream } from './HttpSealedFileStream.js'
import { createProgressTransformer } from '../common/progress.js';

/**
* @param {string} url
* @param {string} privateKeyHex
* @param {string} filename
* @param {{ log?: Function, onProgress?: Function, fetch?: Function }} [opts]
* @param {{ log?: Function, onProgress?: Function, fetch?: Function, size?: number, onDownloadReady?: Function }} [opts]
*/
export async function blobDownloadAndDecrypt(url, privateKeyHex, filename, { log, onProgress, fetch: _fetch } = {}) {
export async function blobDownloadAndDecrypt(url, privateKeyHex, filename, { log, onProgress, fetch: _fetch, size, onDownloadReady } = {}) {
log = log || (() => {})
try {
const chunks = []

const stream = new HttpSealedFileStream(url, { fetch: _fetch })
const stream = new HttpSealedFileStream(url, { fetch: _fetch, onReady: onDownloadReady })
const unsealer = new Unsealer({
privateKeyHex: privateKeyHex.trim(),
progressHandler: (total, processed, readBytes, writeBytes) => {
Expand All @@ -22,6 +23,7 @@ export async function blobDownloadAndDecrypt(url, privateKeyHex, filename, { log

await stream
.pipeThrough(unsealer)
.pipeThrough(createProgressTransformer(size, onProgress))
.pipeTo(new WritableStream({
write(plain) {
chunks.push(new Uint8Array(plain))
Expand Down
11 changes: 9 additions & 2 deletions src/browser/downloadUnsealed.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ async function inspectSealed(url, log) {
* @param {string} options.filename
* @param {Function} [options.onLog]
* @param {Function} [options.onProgress] - (total, processed, readBytes, writeBytes) => {}
* @param {Function} [options.onDownloadReady] - HEAD+tail 完成、正文 Range 开始前触发(可关蒙层)
* @param {Function} [options.onSuccess]
* @param {Function} [options.onError]
* @returns {Promise<void>}
Expand All @@ -95,6 +96,7 @@ export async function downloadUnsealed({
filename,
onLog,
onProgress,
onDownloadReady,
onSuccess,
onError
}) {
Expand Down Expand Up @@ -128,7 +130,12 @@ export async function downloadUnsealed({
if (!mobile) {
try {
log('Trying stream download...')
await streamDownloadAndDecrypt(url, key, filename, { log, onProgress, size: meta.plaintextSize })
await streamDownloadAndDecrypt(url, key, filename, {
log,
onProgress,
size: meta.plaintextSize,
onDownloadReady,
})
if (onSuccess) onSuccess({ filename })
return
} catch (e) {
Expand All @@ -137,7 +144,7 @@ export async function downloadUnsealed({
}

log('Using Blob download...')
await blobDownloadAndDecrypt(url, key, filename, { log, onProgress })
await blobDownloadAndDecrypt(url, key, filename, { log, onProgress, onDownloadReady })
if (onSuccess) onSuccess({ filename })
} catch (error) {
log('Download failed: ' + error.message)
Expand Down
23 changes: 13 additions & 10 deletions src/browser/stream_download.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Unsealer } from './Unsealer.js'
import { HttpSealedFileStream } from './HttpSealedFileStream.js'
import { MetaEncryptorError } from '../common/errors.js';
import { createProgressTransformer } from '../common/progress.js';

async function ensureStreamSaver(log) {
if (typeof window === 'undefined') return null;
Expand All @@ -26,7 +27,14 @@ async function ensureStreamSaver(log) {
}

export async function getBestWritable(filename, { log, size } = {}) {
/*


const ss = await ensureStreamSaver(log);
if (ss) {
log?.('Using StreamSaver...');
return ss.createWriteStream(filename, size !== undefined ? { size } : undefined);
}

if (typeof window !== 'undefined' && window.showSaveFilePicker) {
try {
log?.('Using File System Access API...');
Expand All @@ -37,25 +45,19 @@ export async function getBestWritable(filename, { log, size } = {}) {
log?.(`File System Access API unavailable: ${e.message}`);
}
}
*/

const ss = await ensureStreamSaver(log);
if (ss) {
log?.('Using StreamSaver...');
return ss.createWriteStream(filename, size !== undefined ? { size } : undefined);
}


return null;
}

export async function streamDownloadAndDecrypt(url, privateKeyHex, filename, { log, onProgress, writable, size, fetch: _fetch } = {}) {
export async function streamDownloadAndDecrypt(url, privateKeyHex, filename, { log, onProgress, writable, size, fetch: _fetch, onDownloadReady } = {}) {
log = log || (() => {})
try {
const out = writable || await getBestWritable(filename, { log, size })
if (!out) throw new MetaEncryptorError('ERR_NO_STREAM_WRITABLE');
log('Stream download starting...');

const stream = new HttpSealedFileStream(url, { fetch: _fetch })
const stream = new HttpSealedFileStream(url, { fetch: _fetch, onReady: onDownloadReady })
const unsealer = new Unsealer({
privateKeyHex: privateKeyHex.trim(),
progressHandler: (total, processed, readBytes, writeBytes) => {
Expand All @@ -65,6 +67,7 @@ export async function streamDownloadAndDecrypt(url, privateKeyHex, filename, { l

await stream
.pipeThrough(unsealer)
.pipeThrough(createProgressTransformer(size, onProgress))
.pipeTo(out)

log('Download complete');
Expand Down
20 changes: 20 additions & 0 deletions src/common/progress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Create a TransformStream that reports plaintext download progress.
* Accumulates bytes from each chunk and calls onProgress.
*
* @param {number} plaintextSize - total plaintext size in bytes
* @param {Function} onProgress - progress callback (totalSize, receivedBytes)
* @returns {TransformStream}
*/
export function createProgressTransformer(plaintextSize, onProgress) {
let received = 0
return new TransformStream({
transform(chunk, controller) {
received += chunk.length
if (onProgress) {
onProgress(plaintextSize, received)
}
controller.enqueue(chunk)
}
})
}
9 changes: 6 additions & 3 deletions src/common/ypccrypto.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,17 @@ function generateAESKeyFrom(pkey, skey){
return derived_key;
}

const eth_hash_prefix = Buffer.from("\\x19Ethereum Signed Message:\\n32");
const eth_hash_prefix = Buffer.concat([
Buffer.from([0x19]),
Buffer.from("Ethereum Signed Message:\n32"),
]);

function signMessage(skey, raw) {
let raw_hash = Buffer.from(keccak256(Buffer.from(raw)));
let raw_hash = Buffer.from(keccak256(toUint8Array(raw)));
let msg = new Uint8Array(eth_hash_prefix.length + raw_hash.length)
msg.set(eth_hash_prefix)
msg.set(raw_hash, eth_hash_prefix.length)
msg = Buffer.from(keccak256(Buffer.from(msg)))
msg = Buffer.from(keccak256(toUint8Array(msg)))

const msgBytes = toUint8Array(msg);
const skeyBytes = toUint8Array(skey);
Expand Down
30 changes: 30 additions & 0 deletions test/BrowserCrypto.spec.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BrowserCrypto } from '../src/browser/ypccrypto.browser.js';
import { eth_hash_prefix } from '../src/common/ypccrypto.common.js';
import fs from 'fs/promises';
import path from 'path';

Expand Down Expand Up @@ -256,6 +257,35 @@ describe('BrowserCrypto compatibility with Node YPCCrypto', () => {
});
});

describe('signMessage Ethereum prefix regression', () => {
test('eth_hash_prefix uses correct Ethereum Signed Message format', () => {
expect(eth_hash_prefix.length).toBe(28);
expect(eth_hash_prefix[0]).toBe(0x19);
expect(Buffer.from(eth_hash_prefix.subarray(1)).toString()).toBe(
'Ethereum Signed Message:\n32'
);
});

test('generateSignature matches v4.x compatible vector', () => {
const skey = Buffer.from(
'0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
'hex'
);
const epkey = Buffer.from(
'2acc91b839b115519fa43b7a0bcf20f7e83ef983c00e854343025a5a26a08a3e2b1bbb2c178989126d09d7a7def36fd029eafd60bdfe046b12b226877e018d92',
'hex'
);
const ehash = Buffer.from(
'30890aacb90c4a6440023df1a51a74050756d04811ce0a47eea86dd47dec320a',
'hex'
);
const sig = BrowserCrypto.generateSignature(skey, epkey, ehash);
expect(Buffer.from(sig).toString('hex')).toBe(
'568643ef491fb8e32d061783a6ac94aacdf7967b39ef53490121c39d7997e043630af675554b5538db2b2bab96068351842490a1ee7551d13417fd1052e41cf61c'
);
});
});

// --- Fixtures-based consistency checks ---
function hexToBytes(hex){
if (hex.startsWith('0x')) hex = hex.slice(2);
Expand Down
35 changes: 35 additions & 0 deletions test/HttpSealedFileStream.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,41 @@ describe('HttpSealedFileStream', () => {
expect(chunks[0].length).toBe(64);
});

test('calls onReady after HEAD+tail, before body Range fetches', async () => {
const original = Buffer.alloc(100, 'A');
const { diskBuf } = sealBuffer(original);
let ready = false;
let rangeFetchCount = 0;

const fetch = async (_url, init = {}) => {
if (init.method === 'HEAD') {
return {
ok: true, status: 200,
headers: { get: (n) => n.toLowerCase() === 'content-length' ? String(diskBuf.length) : null }
};
}
if (init.headers?.Range) {
rangeFetchCount++;
}
return createMockFetch(diskBuf)(_url, init);
};

const hsfs = new HttpSealedFileStream('http://x', {
chunkSize: 4096,
fetch,
onReady: () => { ready = true; },
});

const reader = hsfs.getReader();
while (true) {
const { done } = await reader.read();
if (done) break;
}

expect(ready).toBe(true);
expect(rangeFetchCount).toBeGreaterThan(0);
});

test('streams content in multiple chunks with small chunkSize', async () => {
const original = Buffer.alloc(10000, 'Z');
const { diskBuf } = sealBuffer(original);
Expand Down
Loading