Skip to content
Open
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ import { Rclone } from 'rclone';
// Create Rclone instance
Rclone({
password: 'UmyLSdRHfew6aual28-ggx78qHqSfQ',
salt: 'Cj3gLa5PVwc2aot0QpKiOZ3YEzs3Sw'
salt: 'Cj3gLa5PVwc2aot0QpKiOZ3YEzs3Sw',
encoding: 'base32' // Path encoding type, default is base32. base32 and base64 only.
})
.then(rclone => {

Expand Down
13 changes: 7 additions & 6 deletions src/ciphers/PathCipher.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { AES, padding } from 'aes-js';
import { default as decodeBase32 } from 'base32-decode';
import { default as encodeBase32 } from 'base32-encode';

import { Decrypt, Encrypt } from './eme';
import { TextDecoder, TextEncoder } from './text-encoding';
import PathEncoding from './path-encoding';

const { pkcs7 } = padding;

Expand All @@ -16,20 +16,21 @@ const encodeUTF8 = (() => {
return data => encoder.encode(data);
})();

export default function PathCipher({ nameKey, nameTweak } = {}) {
export default function PathCipher({ nameKey, nameTweak } = {}, encodingType = 'base32') {
if (nameKey === undefined || nameTweak === undefined) {
throw new Error('nameKey and nameTweak must be specified');
}
// Name Cipher Fuctions
const nameCipher = new AES(nameKey);
const pathEncoding = PathEncoding(encodingType)

function encryptName(name) {
const ciphertext = encodeUTF8(name);
const paddedCipherText = pkcs7.pad(ciphertext);
const rawCipherText = Encrypt(nameCipher, nameTweak, paddedCipherText);

let encodedCipher = encodeBase32(rawCipherText, 'RFC4648-HEX');
return encodedCipher.replace(/=+$/, '').toLowerCase();
let encodedCipher = pathEncoding.encode(rawCipherText);
return encodedCipher.replace(/=+$/, '');
}

function encrypt(path) {
Expand All @@ -40,7 +41,7 @@ export default function PathCipher({ nameKey, nameTweak } = {}) {
}
function decryptName(name) {
const rawCipherText = new Uint8Array(
decodeBase32(name.toUpperCase(), 'RFC4648-HEX')
pathEncoding.decode(name)
);
const paddedPlaintext = Decrypt(nameCipher, nameTweak, rawCipherText);
return decodeUTF8(pkcs7.strip(paddedPlaintext));
Expand Down
27 changes: 26 additions & 1 deletion src/ciphers/PathCipher.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ test('both nameKey and nameTweak are need', () => {
}).toThrowErrorMatchingSnapshot();
});

test('shoud encrypt/decrypt file name', () => {
test('shoud encrypt/decrypt file name [base32]', () => {
const cases = [
['es785ret6k0hje8fkrqmu9fdus', 'Hallo World'],
['p0e52nreeaj0a5ea7s64m4j72s', '1'],
Expand All @@ -30,3 +30,28 @@ test('shoud encrypt/decrypt file name', () => {
expect(cipher.encrypt(item[1])).toEqual(item[0]);
});
});

test('shoud encrypt/decrypt file name [base64]', () => {
const cases = [
['dw6C7d01ARm5D6b1byXt9w', 'Hallo World'],
['yBxRX25ypgUVyj8MSxJnFw', '1'],
['yBxRX25ypgUVyj8MSxJnFw/qQUDHOGN_jVdLIMQzYrhvA', '1/12'],
[
'yBxRX25ypgUVyj8MSxJnFw/qQUDHOGN_jVdLIMQzYrhvA/1CxFf2Mti1xIPYlGruDh-A',
'1/12/123'
]
];

const cipher = PathCipher({
nameKey: new Uint8Array(32),
nameTweak: new Uint8Array(16)
}, 'base64');

cases.forEach(item => {
expect(cipher.decrypt(item[0])).toEqual(item[1]);
});

cases.forEach(item => {
expect(cipher.encrypt(item[1])).toEqual(item[0]);
});
});
21 changes: 21 additions & 0 deletions src/ciphers/path-encoding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { default as decodeBase32 } from 'base32-decode';
import { default as encodeBase32 } from 'base32-encode';
import { decodeBase64, encodeBase64, toSafeForFileName, toNormal } from '../utils/base64';

const encodings = {
base32: {
encode: (data) => encodeBase32(data, 'RFC4648-HEX').replace(/=+$/, '').toLowerCase(),
decode: (data) => decodeBase32(data.toUpperCase(), 'RFC4648-HEX')
},
base64: {
encode: (data) => toSafeForFileName(encodeBase64(data)),
decode: (data) => decodeBase64(toNormal(data))
}
}

export default (encodingType) => {
const e = encodings[encodingType]

if (!e) throw new Error('Unsupported path encoding types');
return e
}
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export function Rclone(opts) {
return RcloneInternal(opts).then(keys => ({
getKeys: () => keys,
File: FileCipher(keys),
Path: PathCipher(keys)
Path: PathCipher(keys, opts.encoding)
}));
}
13 changes: 9 additions & 4 deletions src/rclone.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,24 @@ test('derive keys from password', done => {

test('use default salt when empty', done => {
// Generated encrypted string with the use of rclone and empty salt
const rcloneEncryptedFileName = 'ecrk8fu3e0pk86td3r634nan08';
const rcloneEncryptedFileNameBase32 = 'ecrk8fu3e0pk86td3r634nan08';
const rcloneEncryptedFileNameBase64 = 'czdEP8NwM0QbrR7MMl1XAg';
const rcloneDecryptedFileName = 'encrypted_file'

Rclone({
password: 'UmyLSdRHfew6aual28-ggx78qHqSfQ',
salt: ''
})
.then(rclone => {
const pathCipher = PathCipher(rclone);
const decryptedString = pathCipher.decrypt(rcloneEncryptedFileName);
let pathCipher = PathCipher(rclone);
let decryptedString = pathCipher.decrypt(rcloneEncryptedFileNameBase32);
expect(decryptedString).toEqual(rcloneDecryptedFileName);

pathCipher = PathCipher(rclone, 'base64');
decryptedString = pathCipher.decrypt(rcloneEncryptedFileNameBase64);
expect(decryptedString).toEqual(rcloneDecryptedFileName);

done();
})
.catch(err => done.fail(err));
});
});
26 changes: 3 additions & 23 deletions src/reveal.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AES from 'aes-js';
import { decodeBase64 } from './utils/base64'
const { ctr } = AES.ModeOfOperation;

// prettier-ignore
Expand All @@ -21,7 +22,7 @@ function reveal(cipherText) {
const blockCipher = createCipher();

cipherText = cipherText.replace(/-/g, '+').replace(/_/g, '/');
const bytes = base64(cipherText);
const bytes = decodeBase64(cipherText);

const iv = bytes.subarray(0, 16);
const buf = bytes.subarray(16);
Expand All @@ -35,25 +36,4 @@ function reveal(cipherText) {
return ctrCipher.decrypt(buf);
}

export { reveal };

function base64(s) {
if (s.length % 4 != 0) {
s += '===='.substr(0, 4 - s.length % 4);
}
return new Uint8Array(
atob2(s)
.split('')
.map(charCodeAt)
);
}

function atob2(data) {
return typeof atob === 'function'
? atob(data)
: Buffer.from(data, 'base64').toString('binary');
}

function charCodeAt(c) {
return c.charCodeAt(0);
}
export { reveal }
39 changes: 39 additions & 0 deletions src/utils/base64.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export function decodeBase64(s) {
if (s.length % 4 != 0) {
s += '===='.substr(0, 4 - s.length % 4);
}
return new Uint8Array(
atob2(s)
.split('')
.map(charCodeAt)
);
}

export function encodeBase64(data) {
return typeof atob === 'function'
? btoa(String.fromCharCode.apply(null, data))
: Buffer.from(data).toString('base64');
}

export function toSafeForFileName(str) {
return str
.replace(/=+$/, '')
.replace(/\//g, '_')
.replace(/\+/g, '-')
}

export function toNormal(str) {
return str
.replace(/_/g, '/')
.replace(/-/g, '+')
}

function atob2(data) {
return typeof atob === 'function'
? atob(data)
: Buffer.from(data, 'base64').toString('binary');
}

function charCodeAt(c) {
return c.charCodeAt(0);
}