完成大部分
This commit is contained in:
1 parent
e97707ac59
commit
13b03e016e
21 files changed
+1628
-275
No files matched your search
@@ -0,0 +1,99 @@
|
||||
const ZW_VERSION = 1
|
||||
const KEY_SEED = 'JGJS2026::ZW::ARCHIVE::V1::DO_NOT_TAMPER'
|
||||
const MAGIC_BYTES = new Uint8Array([0x4a, 0x47, 0x4a, 0x53, 0x5a, 0x57]) // JGJSZW
|
||||
|
||||
export const ZW_FILE_EXTENSION = '.zw'
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
const decoder = new TextDecoder()
|
||||
let cachedKeyPromise: Promise<CryptoKey> | null = null
|
||||
|
||||
const toArrayBuffer = (bytes: Uint8Array): ArrayBuffer =>
|
||||
bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer
|
||||
|
||||
const getArchiveKey = async (): Promise<CryptoKey> => {
|
||||
if (!cachedKeyPromise) {
|
||||
cachedKeyPromise = (async () => {
|
||||
const hash = await crypto.subtle.digest('SHA-256', encoder.encode(KEY_SEED))
|
||||
return crypto.subtle.importKey('raw', hash, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt'])
|
||||
})()
|
||||
}
|
||||
return cachedKeyPromise
|
||||
}
|
||||
|
||||
export const encodeZwArchive = async (payload: unknown): Promise<Uint8Array> => {
|
||||
const key = await getArchiveKey()
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12))
|
||||
const ivBuffer = toArrayBuffer(iv)
|
||||
const plainText = encoder.encode(JSON.stringify(payload))
|
||||
const cipherBuffer = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv: ivBuffer },
|
||||
key,
|
||||
toArrayBuffer(plainText)
|
||||
)
|
||||
const cipher = new Uint8Array(cipherBuffer)
|
||||
const result = new Uint8Array(MAGIC_BYTES.length + 1 + 1 + iv.length + cipher.length)
|
||||
let offset = 0
|
||||
result.set(MAGIC_BYTES, offset)
|
||||
offset += MAGIC_BYTES.length
|
||||
result[offset] = ZW_VERSION
|
||||
offset += 1
|
||||
result[offset] = iv.length
|
||||
offset += 1
|
||||
result.set(iv, offset)
|
||||
offset += iv.length
|
||||
result.set(cipher, offset)
|
||||
return result
|
||||
}
|
||||
|
||||
export const decodeZwArchive = async <T>(raw: ArrayBuffer | Uint8Array): Promise<T> => {
|
||||
const bytes = raw instanceof Uint8Array ? raw : new Uint8Array(raw)
|
||||
const minLength = MAGIC_BYTES.length + 1 + 1 + 1 + 16
|
||||
if (bytes.length < minLength) {
|
||||
throw new Error('INVALID_ZW_PAYLOAD')
|
||||
}
|
||||
|
||||
let offset = 0
|
||||
for (let i = 0; i < MAGIC_BYTES.length; i += 1) {
|
||||
if (bytes[offset + i] !== MAGIC_BYTES[i]) {
|
||||
throw new Error('INVALID_ZW_HEADER')
|
||||
}
|
||||
}
|
||||
offset += MAGIC_BYTES.length
|
||||
|
||||
const version = bytes[offset]
|
||||
offset += 1
|
||||
if (version !== ZW_VERSION) {
|
||||
throw new Error('INVALID_ZW_VERSION')
|
||||
}
|
||||
|
||||
const ivLength = bytes[offset]
|
||||
offset += 1
|
||||
if (ivLength <= 0 || bytes.length <= offset + ivLength) {
|
||||
throw new Error('INVALID_ZW_PAYLOAD')
|
||||
}
|
||||
|
||||
const iv = bytes.slice(offset, offset + ivLength)
|
||||
offset += ivLength
|
||||
const cipher = bytes.slice(offset)
|
||||
if (cipher.length < 16) {
|
||||
throw new Error('INVALID_ZW_PAYLOAD')
|
||||
}
|
||||
|
||||
const key = await getArchiveKey()
|
||||
const ivBuffer = toArrayBuffer(iv)
|
||||
const cipherBuffer = toArrayBuffer(cipher)
|
||||
|
||||
let plainBuffer: ArrayBuffer
|
||||
try {
|
||||
plainBuffer = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: ivBuffer }, key, cipherBuffer)
|
||||
} catch {
|
||||
throw new Error('INVALID_ZW_TAMPERED')
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(decoder.decode(plainBuffer)) as T
|
||||
} catch {
|
||||
throw new Error('INVALID_ZW_CONTENT')
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user