Skip to content

Commit

Permalink
more encoding and partial test
Browse files Browse the repository at this point in the history
  • Loading branch information
fyears committed Mar 21, 2024
1 parent 7805191 commit 483dcae
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 53 deletions.
81 changes: 46 additions & 35 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { scrypt } from "scrypt-js";
import { secretbox, randomBytes } from "tweetnacl";
import { pad, unpad } from "pkcs7-padding";
import { EMECipher, AESCipherBlock } from "@fyears/eme";
import { base32, base32hex } from "rfc4648";
import { base32hex, base64url } from "rfc4648";
import {
encode as base32768Encode,
decode as base32768Decode,
} from "base32768";

const newNonce = () => randomBytes(secretbox.nonceLength);

Expand All @@ -23,9 +27,6 @@ const defaultSalt = new Uint8Array([
export const msgErrorBadDecryptUTF8 = "bad decryption - utf-8 invalid";
export const msgErrorBadDecryptControlChar =
"bad decryption - contains control chars";
export const msgErrorNotAMultipleOfBlocksize = "not a multiple of blocksize";
export const msgErrorTooShortAfterDecode = "too short after base32 decode";
export const msgErrorTooLongAfterDecode = "too long after base32 decode";
export const msgErrorEncryptedFileTooShort =
"file is too short to be encrypted";
export const msgErrorEncryptedFileBadHeader = "file has truncated block header";
Expand All @@ -41,27 +42,22 @@ export const msgErrorBadSeek = "Seek beyond end of file";
export const msgErrorSuffixMissingDot =
"suffix config setting should include a '.'";

type FileNameEncodingType = "base32" | "base64" | "base32768";

// Cipher defines an encoding and decoding cipher for the crypt backend
export class Cipher {
dataKey: Uint8Array; // [32]byte // Key for secretbox
nameKey: Uint8Array; // [32]byte // 16,24 or 32 bytes
nameTweak: Uint8Array; // [nameCipherBlockSize]byte // used to tweak the name crypto
// const block gocipher.Block
// const mode NameEncryptionMode
// const fileNameEnc fileNameEncoding
// const buffers sync.Pool // encrypt/decrypt buffers
// const cryptoRand io.Reader // read crypto random numbers from here
nameTweak: Uint8Array;
fileNameEnc: FileNameEncodingType;
dirNameEncrypt: boolean;
// passBadBlocks: boolean; // if set passed bad blocks as zeroed blocks
// encryptedSuffix: string;

constructor() {
constructor(fileNameEnc: FileNameEncodingType = "base32") {
this.dataKey = new Uint8Array(32);
this.nameKey = new Uint8Array(32);
this.nameTweak = new Uint8Array(nameCipherBlockSize);
this.dirNameEncrypt = true;
// this.passBadBlocks = false;
// this.encryptedSuffix = "";
this.fileNameEnc = fileNameEnc;
}

toString() {
Expand All @@ -70,9 +66,42 @@ dataKey=${this.dataKey}
nameKey=${this.nameKey}
nameTweak=${this.nameTweak}
dirNameEncrypt=${this.dirNameEncrypt}
fileNameEnc=${this.fileNameEnc}
`;
}

encodeToString(ciphertext: Uint8Array) {
if (this.fileNameEnc === "base32") {
return base32hex.stringify(ciphertext, { pad: false }).toLowerCase();
} else if (this.fileNameEnc === "base64") {
return base64url.stringify(ciphertext, { pad: false });
} else if (this.fileNameEnc === "base32768") {
return base32768Encode(ciphertext);
} else {
throw Error(`unknown fileNameEnc=${this.fileNameEnc}`);
}
}

decodeString(ciphertext: string) {
if (this.fileNameEnc === "base32") {
if (ciphertext.endsWith("=")) {
// should not have ending = in our seting
throw new Error(msgErrorBadBase32Encoding);
}
return base32hex.parse(ciphertext.toUpperCase(), {
loose: true,
});
} else if (this.fileNameEnc === "base64") {
return base64url.parse(ciphertext, {
loose: true,
});
} else if (this.fileNameEnc === "base32768") {
return base32768Decode(ciphertext);
} else {
throw Error(`unknown fileNameEnc=${this.fileNameEnc}`);
}
}

async key(password: string, salt: string) {
const keySize =
this.dataKey.length + this.nameKey.length + this.nameTweak.length;
Expand Down Expand Up @@ -128,7 +157,7 @@ dirNameEncrypt=${this.dirNameEncrypt}
const bc = new AESCipherBlock(this.nameKey);
const eme = new EMECipher(bc);
const ciphertext = await eme.encrypt(this.nameTweak, paddedPlaintext);
return base32hex.stringify(ciphertext, { pad: false }).toLowerCase();
return this.encodeToString(ciphertext);
}

async encryptFileName(input: string) {
Expand All @@ -149,25 +178,7 @@ dirNameEncrypt=${this.dirNameEncrypt}
if (ciphertext === "") {
return "";
}
if (ciphertext.endsWith("=")) {
// should not have ending = in our seting
throw new Error(msgErrorBadBase32Encoding);
}
const rawCiphertext = base32hex.parse(ciphertext.toUpperCase(), {
loose: true,
});

if (rawCiphertext.byteLength % nameCipherBlockSize !== 0) {
throw new Error(msgErrorNotAMultipleOfBlocksize);
}

if (rawCiphertext.byteLength === 0) {
// not possible if decodeFilename() working correctly
throw new Error(msgErrorTooShortAfterDecode);
}
if (rawCiphertext.byteLength > 2048) {
throw new Error(msgErrorTooLongAfterDecode);
}
const rawCiphertext = this.decodeString(ciphertext);

const bc = new AESCipherBlock(this.nameKey);
const eme = new EMECipher(bc);
Expand Down
133 changes: 115 additions & 18 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@ import {
msgErrorEncryptedFileTooShort,
msgErrorEncryptedFileBadHeader,
msgErrorBadBase32Encoding,
msgErrorTooLongAfterDecode,
msgErrorNotAMultipleOfBlocksize,
} from "../src";
import { base32hex } from "rfc4648";
import { base32hex, base64 } from "rfc4648";
import { promises as fs } from "fs";
import * as path from "path";

describe("Filename Encryption", () => {
describe("Filename Encryption: base32", () => {
it("TestEncryptSegmentBase32", async () => {
const cases = [
["", ""],
Expand Down Expand Up @@ -61,25 +59,22 @@ describe("Filename Encryption", () => {
const cases = [
["64=", msgErrorBadBase32Encoding],
["!", ""],
[new TextDecoder().decode(longName), msgErrorTooLongAfterDecode],
[
base32hex.stringify(new TextEncoder().encode("a")),
msgErrorNotAMultipleOfBlocksize,
],
[
base32hex.stringify(new TextEncoder().encode("123456789abcdef")),
msgErrorNotAMultipleOfBlocksize,
],
[new TextDecoder().decode(longName), ""],
[base32hex.stringify(new TextEncoder().encode("a")), ""],
[base32hex.stringify(new TextEncoder().encode("123456789abcdef")), ""],
[base32hex.stringify(new TextEncoder().encode("123456789abcdef0")), ""],
];

for (const [input, errMsg] of cases) {
// console.log(input)
// await decryptSegment(input)
if (errMsg === "") {
rejects(async () => await c.decryptSegment(input));
await rejects(async () => await c.decryptSegment(input));
} else {
rejects(async () => await c.decryptSegment(input), new Error(errMsg));
await rejects(async () => await c.decryptSegment(input), {
name: "Error",
message: errMsg,
});
}
}
});
Expand Down Expand Up @@ -115,13 +110,115 @@ describe("Filename Encryption", () => {
deepStrictEqual(expected, await c.decryptFileName(input));

// Add a character should raise ErrorNotAMultipleOfBlocksize
rejects(async () => {
await rejects(async () => {
await c.decryptFileName(`1${input}`);
}, new Error(msgErrorNotAMultipleOfBlocksize));
});
}
});
});

describe("Filename Encryption: base64", () => {
describe("Filename Encryption: base64", () => {
it("TestEncryptSegmentBase64", async () => {
const cases = [
["", ""],
["1", "yBxRX25ypgUVyj8MSxJnFw"],
["12", "qQUDHOGN_jVdLIMQzYrhvA"],
["123", "1CxFf2Mti1xIPYlGruDh-A"],
["1234", "RL-xOTmsxsG7kuTy2XJUxw"],
["12345", "3FP_GHoeBJdq0yLgaED8IQ"],
["123456", "Xc4T1Gqrs3OVYnrE6dpEWQ"],
["1234567", "uZeEzssOnDWHEOzLqjwpog"],
["12345678", "8noiTP5WkkbEuijsPhOpxQ"],
["123456789", "GeNxgLA0wiaGAKU3U7qL4Q"],
["1234567890", "x1DUhdmqoVWYVBLD3dha-A"],
["12345678901", "iEyP_3BZR6vvv_2WM6NbZw"],
["123456789012", "4OPGvS4SZdjvS568APUaFw"],
["1234567890123", "Y8c5Wr8OhYYUo7fPwdojdg"],
["12345678901234", "tjQPabXW112wuVF8Vh46TA"],
["123456789012345", "c5Vh1kTd8WtIajmFEtz2dA"],
["1234567890123456", "tKa5gfvTzW4d-2bMtqYgdf5Rz-k2ZqViW6HfjbIZ6cE"],
];
const c = new Cipher("base64");
await c.key("", "");
for (const [input, expected] of cases) {
const actual = await c.encryptSegment(input);
deepStrictEqual(actual, expected);

const recovered = await c.decryptSegment(expected);
deepStrictEqual(recovered, input);
}
});

it("TestDecryptSegmentBase64", async () => {
// We've tested the forwards above, now concentrate on the errors
const longName = new Uint8Array(3328);
for (let i = 0; i < longName.length; ++i) {
longName[i] = parseInt("a");
}
const c = new Cipher("base64");
const cases = [
["6H=", ""],
["!", ""],
[new TextDecoder().decode(longName), ""],
[base64.stringify(new TextEncoder().encode("a")), ""],
[base64.stringify(new TextEncoder().encode("123456789abcdef")), ""],
[base64.stringify(new TextEncoder().encode("123456789abcdef0")), ""],
];

for (const [input, errMsg] of cases) {
// console.log(input)
// await decryptSegment(input)
if (errMsg === "") {
await rejects(async () => await c.decryptSegment(input));
} else {
await rejects(async () => await c.decryptSegment(input), {
name: "Error",
message: errMsg,
});
}
}
});

it("TestStandardEncryptFileNameBase64", async () => {
const cases = [
["1", "yBxRX25ypgUVyj8MSxJnFw"],
["1/12", "yBxRX25ypgUVyj8MSxJnFw/qQUDHOGN_jVdLIMQzYrhvA"],
[
"1/12/123",
"yBxRX25ypgUVyj8MSxJnFw/qQUDHOGN_jVdLIMQzYrhvA/1CxFf2Mti1xIPYlGruDh-A",
],
// ["1-v2001-02-03-040506-123", "yBxRX25ypgUVyj8MSxJnFw-v2001-02-03-040506-123"],
// ["1/12-v2001-02-03-040506-123", "yBxRX25ypgUVyj8MSxJnFw/qQUDHOGN_jVdLIMQzYrhvA-v2001-02-03-040506-123"]
];
const c = new Cipher("base64");
for (const [input, expected] of cases) {
deepStrictEqual(expected, await c.encryptFileName(input));
}
});

it("TestStandardDecryptFileNameBase64", async () => {
const cases = [
["yBxRX25ypgUVyj8MSxJnFw", "1"],
["yBxRX25ypgUVyj8MSxJnFw/qQUDHOGN_jVdLIMQzYrhvA", "1/12"],
[
"yBxRX25ypgUVyj8MSxJnFw/qQUDHOGN_jVdLIMQzYrhvA/1CxFf2Mti1xIPYlGruDh-A",
"1/12/123",
],
];
const c = new Cipher("base64");
for (const [input, expected] of cases) {
deepStrictEqual(expected, await c.decryptFileName(input));

// Add a character should raise ErrorNotAMultipleOfBlocksize
await rejects(async () => {
await c.decryptFileName(`1${input}`);
});
}
});
});
});

describe("Nonce Computation", () => {
it("TestNonceIncrement", async () => {
const cases: Array<Array<Array<number>>> = [
Expand Down Expand Up @@ -771,7 +868,7 @@ describe("Really RClone Files", () => {
const recovered = await cipher.decryptFileName(actual);
deepStrictEqual(recovered, testFileName);

rejects(async () => await cipher.decryptFileName(`xx${actual}`));
await rejects(async () => await cipher.decryptFileName(`xx${actual}`));
});

it("MonaLisaImageContent", async () => {
Expand Down

0 comments on commit 483dcae

Please sign in to comment.