Skip to content

Commit fe24e2d

Browse files
committed
Add wasm build
1 parent 75a4195 commit fe24e2d

File tree

4 files changed

+258
-0
lines changed

4 files changed

+258
-0
lines changed

pkg/wasm/index.html

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
6+
<link rel="stylesheet" href="styles.css"/>
7+
</head>
8+
<body>
9+
<h1>lcp-decrypt</h1>
10+
11+
<p class="notes">
12+
lcp-decrypt uses the provided key to decrypt an EPUB file protected
13+
using Readium LCP.
14+
</p>
15+
16+
<form id="file-form">
17+
<div>
18+
<label for="input-file">Encrypted EPUB file</label>
19+
<input id="input-file" name="file" type="file" required />
20+
</div>
21+
<div>
22+
<label for="input-key">User key</label>
23+
<input id="input-key" name="key" type="text" required />
24+
</div>
25+
<div>
26+
<button id="button-submit" type="submit" disabled>Loading...</button>
27+
</div>
28+
</form>
29+
30+
<p class="notes">
31+
The decrypted file will be downloaded to your computer.
32+
</p>
33+
34+
<p class="notes">
35+
The decryption is done locally on your computer, the file is never
36+
uploaded anywhere.
37+
</p>
38+
39+
<script src="wasm_exec.js"></script>
40+
<script src="main.js"></script>
41+
</body>
42+
</html>

pkg/wasm/main.js

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
async function getLCP(go) {
2+
const WASM_URL = "lcp.wasm";
3+
4+
let instance;
5+
6+
if ("instantiateStreaming" in WebAssembly) {
7+
instance = (
8+
await WebAssembly.instantiateStreaming(fetch(WASM_URL), go.importObject)
9+
).instance;
10+
} else {
11+
const res = await fetch(WASM_URL);
12+
if (!res.ok) throw new Error(`unexpected status code: ${res.status}`);
13+
instance = (
14+
await WebAssembly.instantiate(await res.arrayBuffer(), go.importObject)
15+
).instance;
16+
}
17+
18+
go.run(instance);
19+
20+
return instance;
21+
}
22+
23+
/**
24+
* @param {WebAssembly.Instance} lcp
25+
* @param {string} s
26+
* @returns {number} The memory address
27+
*/
28+
function newGoString(lcp, s) {
29+
const data = new TextEncoder().encode(s);
30+
return newGoBytes(lcp, data);
31+
}
32+
33+
/**
34+
* @param {WebAssembly.Instance} lcp
35+
* @param {Uint8Array} data
36+
* @returns {number} The memory address
37+
*/
38+
function newGoBytes(lcp, data) {
39+
const { newBytes } = lcp.exports;
40+
const addr = newBytes(data.length);
41+
// Read *after* calling newBytes
42+
const u8mem = new Uint8Array(lcp.exports.memory.buffer);
43+
44+
for (let i = 0; i < data.length; ++i) u8mem[addr + i] = data[i];
45+
46+
return addr;
47+
}
48+
49+
/**
50+
* @param {WebAssembly.Instance} lcp
51+
* @param {number} ptr
52+
* @returns {Uint8Array | null}
53+
*/
54+
function bytesFromGo(lcp, ptr) {
55+
const { memory, bytesSize } = lcp.exports;
56+
const size = bytesSize(ptr);
57+
if (!size) return null;
58+
59+
return new Uint8Array(memory.buffer).subarray(ptr, ptr + size);
60+
}
61+
62+
/**
63+
* @param {WebAssembly.Instance} lcp
64+
* @param {File} file
65+
* @param {string} key
66+
*/
67+
async function decrypt(lcp, file, key) {
68+
const { decrypt, freeBytes } = lcp.exports;
69+
const fileData = new Uint8Array(await file.arrayBuffer());
70+
console.log(fileData);
71+
const goFile = newGoBytes(lcp, fileData);
72+
const goKey = newGoString(lcp, key);
73+
let goOut;
74+
75+
try {
76+
goOut = decrypt(goFile, goKey);
77+
const decryptedData = bytesFromGo(lcp, goOut);
78+
if (!decryptedData) throw new Error("no decrypted data");
79+
const blob = new Blob([decryptedData], { type: "application/epub+zip" });
80+
const link = document.createElement("a");
81+
link.href = URL.createObjectURL(blob);
82+
link.download = `decrypted.${file.name}`;
83+
document.body.appendChild(link);
84+
link.click();
85+
document.body.removeChild(link);
86+
URL.revokeObjectURL(link.href);
87+
} finally {
88+
freeBytes(goFile);
89+
freeBytes(goKey);
90+
if (goOut) freeBytes(goOut);
91+
}
92+
}
93+
94+
async function main() {
95+
const go = new Go();
96+
const lcp = await getLCP(go);
97+
98+
const submitButton = document.getElementById("button-submit");
99+
100+
const resetSubmitButton = () => {
101+
submitButton.removeAttribute("disabled");
102+
submitButton.innerText = "Decrypt";
103+
};
104+
105+
let busy = false;
106+
resetSubmitButton();
107+
108+
document.getElementById("file-form").addEventListener("submit", (ev) => {
109+
ev.preventDefault();
110+
const formData = new FormData(ev.target);
111+
const file = formData.get("file");
112+
const key = formData.get("key");
113+
114+
if (
115+
busy ||
116+
!file ||
117+
!(file instanceof File) ||
118+
!key ||
119+
typeof key !== "string"
120+
)
121+
return;
122+
123+
busy = true;
124+
submitButton.setAttribute("disabled", "1");
125+
submitButton.innerText = "Decrypting...";
126+
127+
decrypt(lcp, file, key)
128+
.catch((error) => {
129+
console.log(error);
130+
alert("There was an error decrypting the file");
131+
})
132+
.finally(() => {
133+
busy = false;
134+
resetSubmitButton();
135+
});
136+
});
137+
}
138+
139+
main();

pkg/wasm/styles.css

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
html {
2+
margin: 0;
3+
padding: 0;
4+
}
5+
6+
body {
7+
margin: 0;
8+
padding: 0;
9+
font-family: sans-serif;
10+
display: grid;
11+
place-content: center;
12+
}
13+
14+
h1 {
15+
font-size: 1.125rem;
16+
line-height: 1.75rem;
17+
}
18+
19+
#file-form {
20+
display: grid;
21+
grid-auto-flow: row;
22+
gap: 0.5rem;
23+
}
24+
25+
.notes {
26+
max-width: 40rem;
27+
}

pkg/wasm/wasm.go

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
6+
"github.com/abustany/lcp-decrypt/pkg/lcp"
7+
)
8+
9+
var handles = map[*byte][]byte{}
10+
11+
func main() {
12+
}
13+
14+
//export newBytes
15+
func newBytes(size int) *byte {
16+
if size == 0 {
17+
return nil
18+
}
19+
return newHandle(make([]byte, size))
20+
}
21+
22+
func newHandle(b []byte) *byte {
23+
if len(b) == 0 {
24+
return nil
25+
}
26+
handles[&b[0]] = b
27+
return &b[0]
28+
}
29+
30+
//export freeBytes
31+
func freeBytes(ptr *byte) {
32+
delete(handles, ptr)
33+
}
34+
35+
//export bytesSize
36+
func bytesSize(ptr *byte) int {
37+
return len(handles[ptr])
38+
}
39+
40+
//export decrypt
41+
func decrypt(inPtr *byte, userKeyHexPtr *byte) *byte {
42+
var out bytes.Buffer
43+
inputData := handles[inPtr]
44+
45+
if err := lcp.Decrypt(&out, bytes.NewReader(handles[inPtr]), int64(len(inputData)), string(handles[userKeyHexPtr])); err != nil {
46+
panic(err.Error())
47+
}
48+
49+
return newHandle(out.Bytes())
50+
}

0 commit comments

Comments
 (0)