Skip to content

Commit

Permalink
feat: Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
franky47 committed Sep 26, 2020
1 parent 5bacb4c commit b781e12
Show file tree
Hide file tree
Showing 9 changed files with 1,503 additions and 1,143 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
node_modules/
dist/
coverage/
.env
yarn-error.log

# Docker containers with persistance
.volumes/
78 changes: 51 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,68 @@
# `typescript-library-starter`
# `@47ng/simple-e2ee`

[![NPM](https://img.shields.io/npm/v/typescript-library-starter?color=red)](https://www.npmjs.com/package/typescript-library-starter)
[![MIT License](https://img.shields.io/github/license/47ng/typescript-library-starter.svg?color=blue)](https://github.com/47ng/typescript-library-starter/blob/master/LICENSE)
[![Travis CI Build](https://img.shields.io/travis/com/47ng/typescript-library-starter.svg)](https://travis-ci.com/47ng/typescript-library-starter)
[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=47ng/typescript-library-starter)](https://dependabot.com)
[![Average issue resolution time](https://isitmaintained.com/badge/resolution/47ng/typescript-library-starter.svg)](https://isitmaintained.com/project/47ng/typescript-library-starter)
[![Number of open issues](https://isitmaintained.com/badge/open/47ng/typescript-library-starter.svg)](https://isitmaintained.com/project/47ng/typescript-library-starter)
[![NPM](https://img.shields.io/npm/v/@47ng/simple-e2ee?color=red)](https://www.npmjs.com/package/@47ng/simple-e2ee)
[![MIT License](https://img.shields.io/github/license/47ng/simple-e2ee.svg?color=blue)](https://github.com/47ng/simple-e2ee/blob/next/LICENSE)
[![Continuous Integration](https://github.com/47ng/simple-e2ee/workflows/Continuous%20Integration/badge.svg?branch=next)](https://github.com/47ng/simple-e2ee/actions)
[![Coverage Status](https://coveralls.io/repos/github/47ng/simple-e2ee/badge.svg?branch=next)](https://coveralls.io/github/47ng/simple-e2ee?branch=next)
[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=47ng/simple-e2ee)](https://dependabot.com)

Template repository for TypeScript libraries.
Simple end-to-end encryption for web apps. Inspired from [Excalidraw](https://excalidraw.com) and [Firefox Send](https://github.com/mozilla/send) (RIP).

## Installation

✂️---
_Cut here_
## How it works

1. [Use this repository as a template](https://github.com/47ng/typescript-library-starter/generate) to create your own.
2. Replace all mentions of `typescript-library-starter` with the name
of your package.
3. Setup Travis CI by adding an NPM deploy token and a Slack channel token:
You encrypt your data in the browser, send the encrypted data
to a server, and use the hash part of the URL (which never
hits the server) to share the key with someone else.

```zsh
# Copy your NPM deploy token to clipboard, then:
$ travis encrypt $(pbpaste) --add deploy.api_key --com
On the other side, the recipient obtains the key from the URL,
the payload from the server and decrypts the data.

# Copy your Slack channel token to clipboard, then:
$ travis encrypt $(pbpaste) --add notifications.slack.rooms --com
```
This library does not handle any of the server upload/download
side, it's up to you. It only deals with encryption, decryption
and moving keys in and out of URLs.

--- ✂️
## Installation

```shell
$ yarn add typescript-library-starter
$ yarn add @47ng/simple-e2ee
# or
$ npm i typescript-library-starter
$ npm i @47ng/simple-e2ee
```

## Usage

## Configuration
```ts
import {
encrypt,
decrypt,
applyKeyToURL,
getKeyFromURL
} from '@47ng/simple-e2ee'

// Before sending data to the server, encrypt it:
const { payload, key } = encrypt(
"whatever you want, as long as it's JSON-serialisable"
)

// Upload `payload` to the server
// Stick the key onto the current URL:
const shareURL = applyKeyToURL(key)

// Optionally, apply the key to another URL
// (example, with an ID returned from the server):
const shareURL = applyKeyToURL(key, `https://example.com/share/${id}`)

// On the other side, get the key from the current URL,
// and the payload from the server:
const key = getKeyFromURL()
const message = decrypt(payload, key)

// Optionally, obtain the key from any URL:
const key = getKeyFromURL(
`https://example.com/share/foo#KatLceVEOM2znzX_FGPKu6Zz1adWkhlq9b2R9WRjUsM=`
)
```

## License

[MIT](https://github.com/47ng/typescript-library-starter/blob/master/LICENSE) - Made with ❤️ by [François Best](https://francoisbest.com).
[MIT](https://github.com/47ng/simple-e2ee/blob/master/LICENSE) - Made with ❤️ by [François Best](https://francoisbest.com).
7 changes: 0 additions & 7 deletions nodemon.json

This file was deleted.

85 changes: 67 additions & 18 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "typescript-library-starter",
"version": "0.0.1",
"description": "Template repository for TypeScript libraries",
"name": "@47ng/simple-e2ee",
"version": "0.0.0-semantically-released",
"description": "Simple E2EE for web apps",
"main": "dist/index.js",
"license": "MIT",
"author": {
Expand All @@ -11,42 +11,91 @@
},
"repository": {
"type": "git",
"url": "https://github.com/47ng/typescript-library-starter"
"url": "https://github.com/47ng/simple-e2ee"
},
"keywords": [
"boilerplate",
"template"
"e2ee",
"tweetnacl"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"test": "jest --verbose",
"test:watch": "jest --verbose --watch",
"dev": "nodemon -e ts,.env -w .env -w . -x 'run-s build:ts test'",
"test": "jest --coverage",
"test:watch": "jest --watch",
"build:clean": "rm -rf ./dist",
"build:ts": "tsc",
"build": "run-s build:clean build:ts",
"ci": "run-s test build"
"ci": "run-s build test"
},
"dependencies": {
"@47ng/codec": "^1.0.0",
"tweetnacl": "^1.0.3"
},
"devDependencies": {
"@types/jest": "^26.0.14",
"@types/node": "^14.11.1",
"husky": "^4.3.0",
"@commitlint/config-conventional": "^9.1.1",
"@types/jest": "^26.0.4",
"@types/node": "^14.0.23",
"commitlint": "^9.1.0",
"husky": "^4.2.5",
"jest": "^25.5.4",
"nodemon": "^2.0.4",
"node-webcrypto-ossl": "^2.1.2",
"npm-run-all": "^4.1.5",
"ts-jest": "^25.5.1",
"ts-node": "^9.0.0",
"typescript": "^3.9.7"
"ts-node": "^8.10.2",
"typescript": "^3.9.6"
},
"jest": {
"verbose": true,
"preset": "ts-jest/presets/js-with-ts",
"testEnvironment": "node"
"testEnvironment": "./tests/browser.env.js"
},
"prettier": {
"arrowParens": "avoid",
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"useTabs": false
},
"husky": {
"hooks": {
"pre-push": "yarn ci"
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
],
"rules": {
"type-enum": [
2,
"always",
[
"build",
"chore",
"ci",
"clean",
"doc",
"feat",
"fix",
"perf",
"ref",
"revert",
"style",
"test"
]
],
"subject-case": [
0,
"always",
"sentence-case"
],
"body-leading-blank": [
2,
"always",
true
]
}
}
}
103 changes: 98 additions & 5 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,100 @@
import lib from './index'
import {
encrypt,
decrypt,
PAYLOAD_REGEX_V1,
applyKeyToURL,
getKeyFromURL
} from './index'

describe('typescript-library-starter', () => {
test('testing works', () => {
expect(lib).toBe('Hello, World !')
})
test('encrypt generates a key and a payload', () => {
expect(encrypt('').payload).toMatch(PAYLOAD_REGEX_V1)
expect(encrypt({}).payload).toMatch(PAYLOAD_REGEX_V1)
expect(encrypt([]).payload).toMatch(PAYLOAD_REGEX_V1)
expect(encrypt(null).payload).toMatch(PAYLOAD_REGEX_V1)
expect(encrypt(42).payload).toMatch(PAYLOAD_REGEX_V1)
expect(encrypt({ hello: 'world' }).payload).toMatch(PAYLOAD_REGEX_V1)
})

test('Encrypt => Decrypt', () => {
function testEncryptionDecryption<T>(input: T) {
const { payload, key } = encrypt(input)
expect(decrypt(payload, key)).toEqual(input)
}

testEncryptionDecryption('')
testEncryptionDecryption({})
testEncryptionDecryption([])
testEncryptionDecryption(null)
testEncryptionDecryption(42)
testEncryptionDecryption({ hello: 'world' })
})

test('Failure - Invalid payload', () => {
const run = () => decrypt('not a payload', 'not a key')
expect(run).toThrowError('Invalid payload format')
})

test('Failure - Invalid key', () => {
const run = () =>
decrypt(
'1.G3Yvm4G3X97h2-ky8ORVIlVMBbHNVFd_.cNFG6eisWeBkC-Rer6I4J7kZ',
'not a key'
)
expect(run).toThrowError('bad key size')
})

test('Failure - Wrong key', () => {
const run = () =>
decrypt(
'1.G3Yvm4G3X97h2-ky8ORVIlVMBbHNVFd_.cNFG6eisWeBkC-Rer6I4J7kZ',
'KatLceVEOM2znzX_FGPKu6Zz1adWkhlq9b2R9WRjUsM='
)
expect(run).toThrowError('Could not decrypt payload (wrong key)')
})

// --

test('Inject key in current URL', () => {
const url = applyKeyToURL('KatLceVEOM2znzX_FGPKu6Zz1adWkhlq9b2R9WRjUsM=')
expect(url).toEqual(
'http://localhost/#KatLceVEOM2znzX_FGPKu6Zz1adWkhlq9b2R9WRjUsM='
)
})

test('Inject key in URL', () => {
const url = applyKeyToURL(
'KatLceVEOM2znzX_FGPKu6Zz1adWkhlq9b2R9WRjUsM=',
'https://example.com'
)
expect(url).toEqual(
'https://example.com/#KatLceVEOM2znzX_FGPKu6Zz1adWkhlq9b2R9WRjUsM='
)
})

test('Inject key in URL that already has a hash', () => {
const run = () =>
applyKeyToURL(
'KatLceVEOM2znzX_FGPKu6Zz1adWkhlq9b2R9WRjUsM=',
'https://example.com#already-hashed'
)
expect(run).toThrow()
})

test('Get key from URL', () => {
const key = getKeyFromURL(
'https://example.com#KatLceVEOM2znzX_FGPKu6Zz1adWkhlq9b2R9WRjUsM='
)
expect(key).toEqual('KatLceVEOM2znzX_FGPKu6Zz1adWkhlq9b2R9WRjUsM=')
})

test('Get key from URL without a hash', () => {
const run = () => getKeyFromURL()
expect(run).toThrow('Could not retrieve key: no hash in URL')
})

test('Get key from current URL', () => {
window.location.hash = 'KatLceVEOM2znzX_FGPKu6Zz1adWkhlq9b2R9WRjUsM='
const key = getKeyFromURL()
expect(key).toEqual('KatLceVEOM2znzX_FGPKu6Zz1adWkhlq9b2R9WRjUsM=')
window.location.hash = ''
})
53 changes: 52 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,52 @@
export default 'Hello, World !'
import nacl from 'tweetnacl'
import b64 from '@47ng/codec/dist/b64'
import utf8 from '@47ng/codec/dist/utf8'
import { URL } from 'url'

export function encrypt<T>(message: T) {
const key = nacl.randomBytes(nacl.secretbox.keyLength)
const nonce = nacl.randomBytes(nacl.secretbox.nonceLength)
const payload = utf8.encode(JSON.stringify(message))
const ciphertext = nacl.secretbox(payload, nonce, key)
return {
payload: ['1', b64.encode(nonce), b64.encode(ciphertext)].join('.'),
key: b64.encode(key)
}
}

export const PAYLOAD_REGEX_V1 = /^1\.([a-zA-Z0-9-_]{32})\.([a-zA-Z0-9-_]{24,})={0,2}$/

export function decrypt<T>(payload: string, key: string): T {
const match = payload.match(PAYLOAD_REGEX_V1)
if (!match) {
throw new Error('Invalid payload format')
}
const nonce = b64.decode(match[1])
const ciphertext = b64.decode(match[2])
const message = nacl.secretbox.open(ciphertext, nonce, b64.decode(key))
if (!message) {
throw new Error('Could not decrypt payload (wrong key)')
}
const json = utf8.decode(message)
return JSON.parse(json)
}

export function applyKeyToURL(key: string, baseURL?: string) {
const url = new URL(baseURL ?? window.location.toString())
if (!!url.hash) {
throw new Error(
'URL already has a hash part, this will conflict with the E2EE key'
)
}
url.hash = key
return url.toString()
}

export function getKeyFromURL(baseURL?: string) {
const url = baseURL ? new URL(baseURL) : window.location
const hash = url.hash.replace(/^#/, '')
if (hash === '') {
throw new Error('Could not retrieve key: no hash in URL')
}
return hash
}
Loading

0 comments on commit b781e12

Please sign in to comment.