Prep crypto work
This commit is contained in:
parent
373e048245
commit
bfdc8383b1
136
common/crypto.ts
136
common/crypto.ts
@ -10,3 +10,139 @@ export function simpleHash(s: string): number {
|
|||||||
}
|
}
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function base32Encode(data: Uint8Array): string {
|
||||||
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||||
|
let result = "";
|
||||||
|
let bits = 0;
|
||||||
|
let value = 0;
|
||||||
|
for (const byte of data) {
|
||||||
|
value = (value << 8) | byte;
|
||||||
|
bits += 8;
|
||||||
|
while (bits >= 5) {
|
||||||
|
result += alphabet[(value >>> (bits - 5)) & 31];
|
||||||
|
bits -= 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bits > 0) {
|
||||||
|
result += alphabet[(value << (5 - bits)) & 31];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function base32Decode(data: string): Uint8Array {
|
||||||
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||||
|
const result = new Uint8Array(Math.floor(data.length * 5 / 8));
|
||||||
|
let bits = 0;
|
||||||
|
let value = 0;
|
||||||
|
let index = 0;
|
||||||
|
for (const char of data) {
|
||||||
|
value = (value << 5) | alphabet.indexOf(char);
|
||||||
|
bits += 5;
|
||||||
|
if (bits >= 8) {
|
||||||
|
result[index++] = (value >>> (bits - 8)) & 255;
|
||||||
|
bits -= 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deriveKeyFromPassword(
|
||||||
|
salt: Uint8Array,
|
||||||
|
password: string,
|
||||||
|
): Promise<CryptoKey> {
|
||||||
|
const baseKey = new TextEncoder().encode(password);
|
||||||
|
const importedKey = await window.crypto.subtle.importKey(
|
||||||
|
"raw",
|
||||||
|
baseKey,
|
||||||
|
{ name: "PBKDF2" },
|
||||||
|
false,
|
||||||
|
["deriveKey"],
|
||||||
|
);
|
||||||
|
return crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: "PBKDF2",
|
||||||
|
salt,
|
||||||
|
iterations: 10000,
|
||||||
|
hash: "SHA-256",
|
||||||
|
},
|
||||||
|
importedKey,
|
||||||
|
{
|
||||||
|
name: "AES-GCM",
|
||||||
|
length: 256,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
["encrypt", "decrypt"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts using AES-GCM and prepends the IV to the ciphertext
|
||||||
|
* @param key
|
||||||
|
* @param message
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function encryptAES(
|
||||||
|
key: CryptoKey,
|
||||||
|
message: Uint8Array,
|
||||||
|
): Promise<Uint8Array> {
|
||||||
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
const ciphertext = await window.crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: "AES-GCM",
|
||||||
|
iv: iv,
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
message,
|
||||||
|
);
|
||||||
|
return appendBuffer(iv, new Uint8Array(ciphertext));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts using AES-GCM and expects the IV to be prepended to the ciphertext
|
||||||
|
* @param key
|
||||||
|
* @param data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function decryptAES(
|
||||||
|
key: CryptoKey,
|
||||||
|
data: Uint8Array,
|
||||||
|
): Promise<Uint8Array> {
|
||||||
|
const iv = data.slice(0, 12);
|
||||||
|
const ciphertext = data.slice(12);
|
||||||
|
const decrypted = await window.crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: "AES-GCM",
|
||||||
|
iv: iv,
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
ciphertext,
|
||||||
|
);
|
||||||
|
return new Uint8Array(decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateSalt(): Uint8Array {
|
||||||
|
return crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportKey(key: CryptoKey): Promise<Uint8Array> {
|
||||||
|
const arrayBuffer = await window.crypto.subtle.exportKey("raw", key);
|
||||||
|
return new Uint8Array(arrayBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importKey(key: Uint8Array): Promise<CryptoKey> {
|
||||||
|
return window.crypto.subtle.importKey(
|
||||||
|
"raw",
|
||||||
|
key,
|
||||||
|
{ name: "AES-GCM" },
|
||||||
|
true,
|
||||||
|
["encrypt", "decrypt"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendBuffer(buffer1: Uint8Array, buffer2: Uint8Array): Uint8Array {
|
||||||
|
const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
|
||||||
|
tmp.set(new Uint8Array(buffer1), 0);
|
||||||
|
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { assertEquals } from "../test_deps.ts";
|
import { assertEquals } from "../test_deps.ts";
|
||||||
import { traverseAndRewriteJSON } from "./json.ts";
|
import { decodeBSON, encodeBSON, traverseAndRewriteJSON } from "./json.ts";
|
||||||
|
|
||||||
Deno.test("traverseAndRewrite should recursively traverse and rewrite object properties", () => {
|
Deno.test("traverseAndRewrite", () => {
|
||||||
const bufArray = new Uint8Array([1, 2, 3]);
|
const bufArray = new Uint8Array([1, 2, 3]);
|
||||||
const obj = {
|
const obj = {
|
||||||
foo: "bar",
|
foo: "bar",
|
||||||
@ -35,3 +35,29 @@ Deno.test("traverseAndRewrite should recursively traverse and rewrite object pro
|
|||||||
special: bufArray,
|
special: bufArray,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test("BSON encoding", () => {
|
||||||
|
// Test some primitives
|
||||||
|
assertEquals(decodeBSON(encodeBSON("test")), "test");
|
||||||
|
assertEquals(decodeBSON(encodeBSON([1, 2, 3])), [1, 2, 3]);
|
||||||
|
assertEquals(decodeBSON(encodeBSON(true)), true);
|
||||||
|
assertEquals(decodeBSON(encodeBSON(false)), false);
|
||||||
|
assertEquals(decodeBSON(encodeBSON(null)), null);
|
||||||
|
assertEquals(decodeBSON(encodeBSON(0)), 0);
|
||||||
|
|
||||||
|
assertEquals(decodeBSON(encodeBSON(undefined)), undefined);
|
||||||
|
|
||||||
|
const blob = new Uint8Array([1, 2, 3]);
|
||||||
|
assertEquals(decodeBSON(encodeBSON(blob)), blob);
|
||||||
|
|
||||||
|
// Then move to more advanced wrapped content
|
||||||
|
const obj = {
|
||||||
|
foo: "bar",
|
||||||
|
list: ["hello", { sup: "world" }],
|
||||||
|
nested: {
|
||||||
|
baz: "qux",
|
||||||
|
},
|
||||||
|
bin: blob,
|
||||||
|
};
|
||||||
|
assertEquals(decodeBSON(encodeBSON(obj)), obj);
|
||||||
|
});
|
||||||
|
@ -1,3 +1,50 @@
|
|||||||
|
import { BSON } from "https://esm.sh/bson@6.2.0";
|
||||||
|
|
||||||
|
// BSON doesn't support top-level primitives, so we need to wrap them in an object
|
||||||
|
const topLevelValueKey = "$_tl";
|
||||||
|
|
||||||
|
// BSON doesn't support undefined, so we need to encode it as a "magic" string
|
||||||
|
const undefinedPlaceHolder = "$_undefined_$";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BSON encoder, but also supporting "edge cases" like encoding strings, numbers, etc.
|
||||||
|
* @param obj
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function encodeBSON(obj: any): Uint8Array {
|
||||||
|
if (
|
||||||
|
obj === undefined || obj === null ||
|
||||||
|
!(typeof obj === "object" && obj.constructor === Object)
|
||||||
|
) {
|
||||||
|
obj = { [topLevelValueKey]: obj };
|
||||||
|
}
|
||||||
|
obj = traverseAndRewriteJSON(obj, (val) => {
|
||||||
|
if (val === undefined) {
|
||||||
|
return undefinedPlaceHolder;
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
return BSON.serialize(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeBSON(data: Uint8Array): any {
|
||||||
|
let result = BSON.deserialize(data);
|
||||||
|
// For whatever reason the BSON library doesn't unwrap binary blobs automatically
|
||||||
|
result = traverseAndRewriteJSON(result, (val) => {
|
||||||
|
if (typeof val?.value === "function") {
|
||||||
|
return val.value();
|
||||||
|
} else if (val === undefinedPlaceHolder) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
if (Object.hasOwn(result, topLevelValueKey)) {
|
||||||
|
return result[topLevelValueKey];
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Traverses and rewrites an object recursively.
|
* Traverses and rewrites an object recursively.
|
||||||
*
|
*
|
||||||
@ -13,8 +60,8 @@ export function traverseAndRewriteJSON(
|
|||||||
obj = rewrite(obj);
|
obj = rewrite(obj);
|
||||||
// Recurse down if this is an array or a "plain object"
|
// Recurse down if this is an array or a "plain object"
|
||||||
if (
|
if (
|
||||||
obj && Array.isArray(obj) ||
|
obj && (Array.isArray(obj) ||
|
||||||
(typeof obj === "object" && obj.constructor === Object)
|
(typeof obj === "object" && obj.constructor === Object))
|
||||||
) {
|
) {
|
||||||
const keys = Object.keys(obj);
|
const keys = Object.keys(obj);
|
||||||
for (let i = 0; i < keys.length; i++) {
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
@ -1,4 +1,14 @@
|
|||||||
import { FileMeta } from "../../plug-api/types.ts";
|
import { FileMeta } from "../../plug-api/types.ts";
|
||||||
|
import {
|
||||||
|
base32Decode,
|
||||||
|
base32Encode,
|
||||||
|
decryptAES,
|
||||||
|
deriveKeyFromPassword,
|
||||||
|
encryptAES,
|
||||||
|
exportKey,
|
||||||
|
generateSalt,
|
||||||
|
importKey,
|
||||||
|
} from "../crypto.ts";
|
||||||
import { SpacePrimitives } from "./space_primitives.ts";
|
import { SpacePrimitives } from "./space_primitives.ts";
|
||||||
|
|
||||||
export const encryptedFileExt = ".crypt";
|
export const encryptedFileExt = ".crypt";
|
||||||
@ -51,7 +61,7 @@ export class EncryptedSpacePrimitives implements SpacePrimitives {
|
|||||||
if (this.spaceSalt) {
|
if (this.spaceSalt) {
|
||||||
throw new Error("Space already initialized");
|
throw new Error("Space already initialized");
|
||||||
}
|
}
|
||||||
this.spaceSalt = this.generateSalt();
|
this.spaceSalt = generateSalt();
|
||||||
await this.wrapped.writeFile(saltFile, this.spaceSalt);
|
await this.wrapped.writeFile(saltFile, this.spaceSalt);
|
||||||
await this.createKey(password);
|
await this.createKey(password);
|
||||||
}
|
}
|
||||||
@ -65,15 +75,18 @@ export class EncryptedSpacePrimitives implements SpacePrimitives {
|
|||||||
throw new Error("Space not initialized");
|
throw new Error("Space not initialized");
|
||||||
}
|
}
|
||||||
// First derive an encryption key solely used for encrypting the key file from the user's password
|
// First derive an encryption key solely used for encrypting the key file from the user's password
|
||||||
const keyEncryptionKey = await this.deriveKeyFromPassword(password);
|
const keyEncryptionKey = await deriveKeyFromPassword(
|
||||||
|
this.spaceSalt!,
|
||||||
|
password,
|
||||||
|
);
|
||||||
const encryptedKeyFileName = await this.encryptPath(
|
const encryptedKeyFileName = await this.encryptPath(
|
||||||
keyEncryptionKey,
|
keyEncryptionKey,
|
||||||
keyPath,
|
keyPath,
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.masterKey = await this.importKey(
|
this.masterKey = await importKey(
|
||||||
await this.decryptAES(
|
await decryptAES(
|
||||||
keyEncryptionKey,
|
keyEncryptionKey,
|
||||||
(await this.wrapped.readFile(
|
(await this.wrapped.readFile(
|
||||||
encryptedKeyFileName,
|
encryptedKeyFileName,
|
||||||
@ -102,7 +115,10 @@ export class EncryptedSpacePrimitives implements SpacePrimitives {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async createKey(password: string): Promise<void> {
|
private async createKey(password: string): Promise<void> {
|
||||||
const keyEncryptionKey = await this.deriveKeyFromPassword(password);
|
const keyEncryptionKey = await deriveKeyFromPassword(
|
||||||
|
this.spaceSalt!,
|
||||||
|
password,
|
||||||
|
);
|
||||||
this.encryptedKeyFileName = await this.encryptPath(
|
this.encryptedKeyFileName = await this.encryptPath(
|
||||||
keyEncryptionKey,
|
keyEncryptionKey,
|
||||||
keyPath,
|
keyPath,
|
||||||
@ -111,9 +127,9 @@ export class EncryptedSpacePrimitives implements SpacePrimitives {
|
|||||||
// And write it
|
// And write it
|
||||||
await this.wrapped.writeFile(
|
await this.wrapped.writeFile(
|
||||||
this.encryptedKeyFileName,
|
this.encryptedKeyFileName,
|
||||||
await this.encryptAES(
|
await encryptAES(
|
||||||
keyEncryptionKey,
|
keyEncryptionKey,
|
||||||
await this.exportKey(this.masterKey),
|
await exportKey(this.masterKey),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -123,7 +139,7 @@ export class EncryptedSpacePrimitives implements SpacePrimitives {
|
|||||||
throw new Error("No key loaded");
|
throw new Error("No key loaded");
|
||||||
}
|
}
|
||||||
const oldPasswordKeyFileName = await this.encryptPath(
|
const oldPasswordKeyFileName = await this.encryptPath(
|
||||||
await this.deriveKeyFromPassword(oldPassword),
|
await deriveKeyFromPassword(this.spaceSalt!, oldPassword),
|
||||||
keyPath,
|
keyPath,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -139,7 +155,10 @@ export class EncryptedSpacePrimitives implements SpacePrimitives {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// First derive an encryption key solely used for encrypting the key file from the user's password
|
// First derive an encryption key solely used for encrypting the key file from the user's password
|
||||||
const keyEncryptionKey = await this.deriveKeyFromPassword(newPasword);
|
const keyEncryptionKey = await deriveKeyFromPassword(
|
||||||
|
this.spaceSalt!,
|
||||||
|
newPasword,
|
||||||
|
);
|
||||||
|
|
||||||
this.encryptedKeyFileName = await this.encryptPath(
|
this.encryptedKeyFileName = await this.encryptPath(
|
||||||
keyEncryptionKey,
|
keyEncryptionKey,
|
||||||
@ -148,9 +167,9 @@ export class EncryptedSpacePrimitives implements SpacePrimitives {
|
|||||||
// And write it
|
// And write it
|
||||||
await this.wrapped.writeFile(
|
await this.wrapped.writeFile(
|
||||||
this.encryptedKeyFileName,
|
this.encryptedKeyFileName,
|
||||||
await this.encryptAES(
|
await encryptAES(
|
||||||
keyEncryptionKey,
|
keyEncryptionKey,
|
||||||
await this.exportKey(this.masterKey),
|
await exportKey(this.masterKey),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -162,98 +181,6 @@ export class EncryptedSpacePrimitives implements SpacePrimitives {
|
|||||||
return name.startsWith("_plug/");
|
return name.startsWith("_plug/");
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateSalt(): Uint8Array {
|
|
||||||
return crypto.getRandomValues(new Uint8Array(16));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async exportKey(key: CryptoKey): Promise<Uint8Array> {
|
|
||||||
const arrayBuffer = await window.crypto.subtle.exportKey("raw", key);
|
|
||||||
return new Uint8Array(arrayBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private importKey(key: Uint8Array): Promise<CryptoKey> {
|
|
||||||
return window.crypto.subtle.importKey(
|
|
||||||
"raw",
|
|
||||||
key,
|
|
||||||
{ name: "AES-GCM" },
|
|
||||||
true,
|
|
||||||
["encrypt", "decrypt"],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async deriveKeyFromPassword(
|
|
||||||
password: string,
|
|
||||||
): Promise<CryptoKey> {
|
|
||||||
const baseKey = new TextEncoder().encode(password);
|
|
||||||
const importedKey = await window.crypto.subtle.importKey(
|
|
||||||
"raw",
|
|
||||||
baseKey,
|
|
||||||
{ name: "PBKDF2" },
|
|
||||||
false,
|
|
||||||
["deriveKey"],
|
|
||||||
);
|
|
||||||
return crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: "PBKDF2",
|
|
||||||
salt: this.spaceSalt!,
|
|
||||||
iterations: 10000,
|
|
||||||
hash: "SHA-256",
|
|
||||||
},
|
|
||||||
importedKey,
|
|
||||||
{
|
|
||||||
name: "AES-GCM",
|
|
||||||
length: 256,
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
["encrypt", "decrypt"],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encrypts using AES-GCM and prepends the IV to the ciphertext
|
|
||||||
* @param key
|
|
||||||
* @param message
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
private async encryptAES(
|
|
||||||
key: CryptoKey,
|
|
||||||
message: Uint8Array,
|
|
||||||
): Promise<Uint8Array> {
|
|
||||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
||||||
const ciphertext = await window.crypto.subtle.encrypt(
|
|
||||||
{
|
|
||||||
name: "AES-GCM",
|
|
||||||
iv: iv,
|
|
||||||
},
|
|
||||||
key,
|
|
||||||
message,
|
|
||||||
);
|
|
||||||
return appendBuffer(iv, new Uint8Array(ciphertext));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrypts using AES-GCM and expects the IV to be prepended to the ciphertext
|
|
||||||
* @param key
|
|
||||||
* @param data
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
async decryptAES(
|
|
||||||
key: CryptoKey,
|
|
||||||
data: Uint8Array,
|
|
||||||
): Promise<Uint8Array> {
|
|
||||||
const iv = data.slice(0, 12);
|
|
||||||
const ciphertext = data.slice(12);
|
|
||||||
const decrypted = await window.crypto.subtle.decrypt(
|
|
||||||
{
|
|
||||||
name: "AES-GCM",
|
|
||||||
iv: iv,
|
|
||||||
},
|
|
||||||
key,
|
|
||||||
ciphertext,
|
|
||||||
);
|
|
||||||
return new Uint8Array(decrypted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Left pads a string with zeros to a length of 32, encrypts it using AES-GCM and returns the base32 encoded ciphertext
|
* Left pads a string with zeros to a length of 32, encrypts it using AES-GCM and returns the base32 encoded ciphertext
|
||||||
* @param key
|
* @param key
|
||||||
@ -359,7 +286,7 @@ export class EncryptedSpacePrimitives implements SpacePrimitives {
|
|||||||
await this.encryptPath(this.masterKey!, name),
|
await this.encryptPath(this.masterKey!, name),
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
data: await this.decryptAES(this.masterKey!, data),
|
data: await decryptAES(this.masterKey!, data),
|
||||||
meta: {
|
meta: {
|
||||||
...meta,
|
...meta,
|
||||||
name,
|
name,
|
||||||
@ -378,7 +305,7 @@ export class EncryptedSpacePrimitives implements SpacePrimitives {
|
|||||||
}
|
}
|
||||||
const newMeta = await this.wrapped.writeFile(
|
const newMeta = await this.wrapped.writeFile(
|
||||||
await this.encryptPath(this.masterKey!, name),
|
await this.encryptPath(this.masterKey!, name),
|
||||||
await this.encryptAES(this.masterKey!, data),
|
await encryptAES(this.masterKey!, data),
|
||||||
selfUpdate,
|
selfUpdate,
|
||||||
meta,
|
meta,
|
||||||
);
|
);
|
||||||
@ -410,46 +337,3 @@ function removePadding(str: string, paddingChar: string): string {
|
|||||||
}
|
}
|
||||||
return str.substring(0, endIndex + 1);
|
return str.substring(0, endIndex + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function base32Encode(data: Uint8Array): string {
|
|
||||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
||||||
let result = "";
|
|
||||||
let bits = 0;
|
|
||||||
let value = 0;
|
|
||||||
for (const byte of data) {
|
|
||||||
value = (value << 8) | byte;
|
|
||||||
bits += 8;
|
|
||||||
while (bits >= 5) {
|
|
||||||
result += alphabet[(value >>> (bits - 5)) & 31];
|
|
||||||
bits -= 5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (bits > 0) {
|
|
||||||
result += alphabet[(value << (5 - bits)) & 31];
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function base32Decode(data: string): Uint8Array {
|
|
||||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
||||||
const result = new Uint8Array(Math.floor(data.length * 5 / 8));
|
|
||||||
let bits = 0;
|
|
||||||
let value = 0;
|
|
||||||
let index = 0;
|
|
||||||
for (const char of data) {
|
|
||||||
value = (value << 5) | alphabet.indexOf(char);
|
|
||||||
bits += 5;
|
|
||||||
if (bits >= 8) {
|
|
||||||
result[index++] = (value >>> (bits - 8)) & 255;
|
|
||||||
bits -= 8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function appendBuffer(buffer1: Uint8Array, buffer2: Uint8Array): Uint8Array {
|
|
||||||
const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
|
|
||||||
tmp.set(new Uint8Array(buffer1), 0);
|
|
||||||
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
|
|
||||||
return tmp;
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user