mirror of
https://github.com/blyssprivacy/sdk.git
synced 2026-01-09 15:18:01 -05:00
Private Valentines demo (#9)
JS Client changes: Gracefully return null if key is not found in bucket (instead of throwing) Support single and multi-key lookups in a unified privateRead function
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,3 +6,5 @@ node_modules
|
||||
|
||||
.env/
|
||||
*.pyc
|
||||
*build/
|
||||
.DS_Store
|
||||
|
||||
BIN
examples/valentines/fonts/IBMPlexMono-Regular.woff2
Normal file
BIN
examples/valentines/fonts/IBMPlexMono-Regular.woff2
Normal file
Binary file not shown.
36
examples/valentines/package.json
Normal file
36
examples/valentines/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "blyss-private-valentines",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"keywords": [],
|
||||
"main": "src/index.tsx",
|
||||
"dependencies": {
|
||||
"react": "18.0.0",
|
||||
"react-dom": "18.0.0",
|
||||
"react-scripts": "5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "18.0.25",
|
||||
"@types/react-dom": "18.0.9",
|
||||
"typescript": "4.4.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "PORT=3010 BROWSER=none react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
19
examples/valentines/public/index.html
Normal file
19
examples/valentines/public/index.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||
/>
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<title>Private Valentine Retrieval</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
5
examples/valentines/sandbox.config.json
Normal file
5
examples/valentines/sandbox.config.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"infiniteLoopProtection": false,
|
||||
"hardReloadOnChange": false,
|
||||
"view": "browser"
|
||||
}
|
||||
432
examples/valentines/src/App.tsx
Normal file
432
examples/valentines/src/App.tsx
Normal file
@@ -0,0 +1,432 @@
|
||||
import './styles.css';
|
||||
import { Log, LogMessage } from './util';
|
||||
import { Bucket, Client } from '@blyss/sdk';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
// This function gets called only on the first query
|
||||
async function setup(apiKey: string): Promise<Bucket> {
|
||||
const client = new Client(apiKey);
|
||||
|
||||
// Create the bucket, if it doesn't exist.
|
||||
// By default, only you can read and write from the buckets you create.
|
||||
// To make a bucket others can read, prefix the name with "global."
|
||||
const bucketName = 'global.private-valentines-13643';
|
||||
if (!(await client.exists(bucketName))) {
|
||||
console.log('creating bucket');
|
||||
await client.create(bucketName);
|
||||
}
|
||||
|
||||
// Connect to your bucket
|
||||
const bucket = await client.connect(bucketName);
|
||||
|
||||
return bucket;
|
||||
}
|
||||
|
||||
async function deriveMessageKey(recipient: string): Promise<CryptoKey> {
|
||||
// 0.1. Create base key material from recipient handle
|
||||
const baseKey = await window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(recipient),
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveKey']
|
||||
);
|
||||
|
||||
const pkbdf2Params = {
|
||||
name: 'PBKDF2',
|
||||
salt: new TextEncoder().encode('valentine'),
|
||||
iterations: 100000,
|
||||
hash: 'SHA-256'
|
||||
};
|
||||
|
||||
const aesGenKeyParams = {
|
||||
name: 'AES-GCM',
|
||||
length: 256
|
||||
};
|
||||
|
||||
const key = await window.crypto.subtle.deriveKey(
|
||||
pkbdf2Params,
|
||||
baseKey,
|
||||
aesGenKeyParams,
|
||||
false,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
async function computeServerKey(mailbox: string): Promise<string> {
|
||||
// 2.2. Hash recipient's handle to get destination key on server
|
||||
const hash = await window.crypto.subtle.digest(
|
||||
'SHA-256',
|
||||
new TextEncoder().encode(mailbox)
|
||||
);
|
||||
// 2.3. Convert hash to base64 string
|
||||
const targetKey = window.btoa(String.fromCharCode(...new Uint8Array(hash)));
|
||||
return targetKey;
|
||||
}
|
||||
|
||||
function SendValentineCard({
|
||||
loading,
|
||||
handler
|
||||
}: {
|
||||
loading: boolean;
|
||||
handler: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||
}) {
|
||||
return (
|
||||
<form className="actioncard" onSubmit={handler}>
|
||||
<h2>😘 Send</h2>
|
||||
<div className="actioncard-field">
|
||||
<h3>To:</h3>
|
||||
<input
|
||||
type="text"
|
||||
id="to"
|
||||
// placeholder="mailbox destination"
|
||||
title="up to 500 Unicode chars, enforced by truncation."
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="actioncard-field">
|
||||
<h3>Valentine message:</h3>
|
||||
<textarea
|
||||
id="msg"
|
||||
// placeholder="Valentine message"
|
||||
title="UTF8 up to 1KiB, enforced by truncation. Message will be client-side encrypted, using a key derived from the recipient's name."
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="actioncard-buttons">
|
||||
<button disabled={loading}>
|
||||
{loading ? ' sending... ' : 'send valentine'}
|
||||
</button>
|
||||
<div>{loading ? <div className="loader"></div> : null}</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function PrivateReceiveValentineCard({
|
||||
loading,
|
||||
fetchedMessage,
|
||||
handler
|
||||
}: {
|
||||
loading: boolean;
|
||||
fetchedMessage: string;
|
||||
handler: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||
}) {
|
||||
return (
|
||||
<form className="actioncard" onSubmit={handler}>
|
||||
<h2>💌 Private Retrieve</h2>
|
||||
<div className="actioncard-field">
|
||||
<h3>Mailbox to check:</h3>
|
||||
<input
|
||||
type="text"
|
||||
id="to"
|
||||
placeholder="(recipient's name)"
|
||||
title="up to 500 Unicode chars, enforced by truncation."
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="actioncard-field">
|
||||
<h3>Received message:</h3>
|
||||
<textarea
|
||||
className="fetchedMessage"
|
||||
id="msg"
|
||||
value={fetchedMessage}
|
||||
title="display for the fetched message."
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<div className="actioncard-buttons">
|
||||
<button disabled={loading}>
|
||||
{loading ? 'fetching...' : 'fetch valentine'}
|
||||
</button>
|
||||
<div>{loading ? <div className="loader"></div> : null}</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function Explainer() {
|
||||
return (
|
||||
<div className="explainer">
|
||||
<p>
|
||||
Send and receive encrypted valentines, while keeping your amour’s
|
||||
identity fully private. Your browser will fetch Valentines via the{' '}
|
||||
<a href="https://blyss.dev">Blyss</a> protocol, which secures{' '}
|
||||
<i>metadata</i> - like “which Valentines are you looking at?" More info{' '}
|
||||
<a href="#faq">below</a>.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Faq() {
|
||||
return (
|
||||
<div className="FAQ" id="faq">
|
||||
<h2>FAQ</h2>
|
||||
<h4>
|
||||
Is this really homomorphic encryption? I thought that was impossible /
|
||||
really slow.
|
||||
</h4>
|
||||
<p>
|
||||
Yup, this is real-deal fully homomorphic encryption, running as realtime
|
||||
multiplayer in your browser. Five years ago, this demo probably did seem
|
||||
impossible, but a lot of recent work has made FHE fast enough for some
|
||||
specific applications, like the private information retrieval we show
|
||||
here. Want to try using fast FHE in your own apps? Here's our{' '}
|
||||
<a href="https://github.com/blyssprivacy/sdk">open-source SDK!</a>
|
||||
</p>
|
||||
|
||||
<h4>How are my message contents secured?</h4>
|
||||
<p>
|
||||
To <b>send</b> a message addressed to mailbox <strong>M</strong>, the
|
||||
browser client first derives a key{' '}
|
||||
<code>
|
||||
<strong>K</strong> = PKBDF2(
|
||||
<strong>M</strong>)
|
||||
</code>
|
||||
, using a fixed salt. <strong>K</strong> is used by the client to AES
|
||||
encrypt the message; <strong>K</strong> never leaves your device. Of
|
||||
course, the server can't know <strong>M</strong>, so the client writes
|
||||
the encrypted message to server location{' '}
|
||||
<code>
|
||||
<strong>L</strong> = SHA256(<strong>M</strong>)
|
||||
</code>
|
||||
.<br></br>
|
||||
<br></br>
|
||||
To <b>retrieve</b> a message sent to mailbox <strong>M</strong>, we do
|
||||
the same steps in reverse: the client first computes{' '}
|
||||
<code>
|
||||
<strong>L</strong> = SHA256(<strong>M</strong>)
|
||||
</code>
|
||||
, then performs a metadata-private read for <strong>L</strong>; the
|
||||
result is the encrypted message data of mailbox <strong>M</strong>,
|
||||
which the client finally decrypts with key{' '}
|
||||
<code>
|
||||
<strong>K</strong> = PKBDF2(<strong>M</strong>)
|
||||
</code>
|
||||
.
|
||||
</p>
|
||||
|
||||
<h4>How is my message metadata secured?</h4>
|
||||
<p>
|
||||
Fully homomorphic encryption (FHE) is what makes this special. It lets
|
||||
the server retrieve any data the client requests, while the server
|
||||
remains completely oblivious to the client's selection. Here are some
|
||||
more detailed explainers on FHE, in increasing levels of technicality: a{' '}
|
||||
<a href="https://blintzbase.com/posts/pir-and-fhe-from-scratch/">
|
||||
blog post
|
||||
</a>{' '}
|
||||
we wrote, our{' '}
|
||||
<a href="https://github.com/blyssprivacy/sdk">source code</a>, and a{' '}
|
||||
<a href="https://eprint.iacr.org/2022/368">paper we published</a>.
|
||||
</p>
|
||||
<h4>Is this like end-to-end encryption?</h4>
|
||||
<p>
|
||||
No, this is a different kind of privacy. In this toy demo, your message
|
||||
contents are encrypted, but under a weak key that is merely derived from
|
||||
the recipient's name - not something we'd ever call E2E. But the
|
||||
metadata of message retrievals is actually protected, so the server
|
||||
cannot know whom is messaging whom. Caveat: regardless of encryption
|
||||
strategy, patterns in client activity can always hint at client
|
||||
relationships, unless communicating parties take care to decorrelate
|
||||
their actions.
|
||||
</p>
|
||||
<h4>Could this be used as a metadata-private messenger?</h4>
|
||||
<p>
|
||||
With a couple more steps (starting with E2EE), maybe! If you're
|
||||
interested in this sort of thing, we should{' '}
|
||||
<a href="mailto:founders@blyss.dev">definitely talk</a>.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// UI
|
||||
function App() {
|
||||
const [bucketHandle, setBucketHandle] = useState<Bucket | undefined>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [posting, setPosting] = useState(false);
|
||||
const [apiKey, setApiKey] = useState(
|
||||
'CSdK3rKXvb4zb43AgycQn6KAmJDILMXU8IWUHrn7'
|
||||
);
|
||||
const [numMessages, setNumMessages] = useState(
|
||||
Math.floor(Math.random() * 100) + 950
|
||||
);
|
||||
const [fetchedMessage, setfetchedMessage] = useState('');
|
||||
|
||||
const [trace, setTrace] = useState<Log[]>([]);
|
||||
const logMessage = (t: Log) => setTrace([t, ...trace]);
|
||||
|
||||
async function animatePost(to: string, message: string): Promise<void> {
|
||||
setPosting(true);
|
||||
|
||||
// enforce size limits
|
||||
if (to.length > 500) {
|
||||
to = to.slice(0, 500);
|
||||
}
|
||||
if (message.length > 1000) {
|
||||
message = message.slice(0, 500);
|
||||
}
|
||||
|
||||
// 0. Derive an encryption key from the recipient's handle
|
||||
const key = await deriveMessageKey(to);
|
||||
|
||||
// 1. Get a handle to the bucket
|
||||
let bucket = bucketHandle;
|
||||
if (!bucket) {
|
||||
console.log('setup!');
|
||||
bucket = await setup(apiKey);
|
||||
setBucketHandle(bucket);
|
||||
}
|
||||
|
||||
// 2.1. Encrypt the message
|
||||
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||
const encryptedMessage = await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv
|
||||
},
|
||||
key,
|
||||
new TextEncoder().encode(message)
|
||||
);
|
||||
|
||||
// 2.2. Prepend iv to encrypted message
|
||||
const encryptedMessageWithIv = new Uint8Array([
|
||||
...iv,
|
||||
...new Uint8Array(encryptedMessage)
|
||||
]);
|
||||
|
||||
// 2.3. Write encrypted message to KV server
|
||||
const serverKey = await computeServerKey(to);
|
||||
const start = performance.now();
|
||||
const _ = await bucket.write({
|
||||
[serverKey]: encryptedMessageWithIv
|
||||
});
|
||||
const tookMs = performance.now() - start;
|
||||
const isRetrieval = false;
|
||||
|
||||
// 3. Log the result to the UI
|
||||
logMessage({
|
||||
to,
|
||||
isRetrieval,
|
||||
tookMs
|
||||
});
|
||||
|
||||
setPosting(false);
|
||||
}
|
||||
|
||||
async function animateFetch(to: string): Promise<void> {
|
||||
setLoading(true);
|
||||
|
||||
// enforce size limits
|
||||
if (to.length > 500) {
|
||||
to = to.slice(0, 500);
|
||||
}
|
||||
|
||||
// 1. Get a handle to the bucket
|
||||
let bucket = bucketHandle;
|
||||
if (!bucket) {
|
||||
console.log('setup!');
|
||||
bucket = await setup(apiKey);
|
||||
setBucketHandle(bucket);
|
||||
}
|
||||
|
||||
// 2. Retrieve the specified mailbox
|
||||
const serverKey = await computeServerKey(to);
|
||||
const start = performance.now();
|
||||
const fetchedResult = await bucket.privateRead(serverKey);
|
||||
const tookMs = performance.now() - start;
|
||||
const isRetrieval = true;
|
||||
|
||||
// 3. Log the result to the UI
|
||||
logMessage({
|
||||
to,
|
||||
isRetrieval,
|
||||
tookMs
|
||||
});
|
||||
|
||||
if (fetchedResult === null) {
|
||||
// 4.1 If the mailbox is empty, we're done
|
||||
console.log('no messages yet :(');
|
||||
setfetchedMessage('no messages yet :(');
|
||||
} else {
|
||||
// 4.2 Decrypt the message
|
||||
console.log('decrypting message...');
|
||||
const key = await deriveMessageKey(to);
|
||||
const decryptedMessage = await window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: fetchedResult.slice(0, 12)
|
||||
},
|
||||
key,
|
||||
fetchedResult.slice(12)
|
||||
);
|
||||
const decodedMessage = new TextDecoder().decode(decryptedMessage);
|
||||
setfetchedMessage(decodedMessage);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
const handleSubmit = (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
const to = (event.target as any).to.value;
|
||||
const msg = (event.target as any).msg.value;
|
||||
console.log('Sending message ' + msg + ' to mailbox ' + to);
|
||||
return animatePost(to, msg);
|
||||
};
|
||||
|
||||
const handleFetch = (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
const to = (event.target as any).to.value;
|
||||
console.log('Checking mailbox: ' + to + '...');
|
||||
return animateFetch(to);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<div className="App-main">
|
||||
<div className="title">
|
||||
<h2 style={{ margin: 0 }}>Private Valentine Retrieval</h2>
|
||||
<h3 style={{ margin: 0, color: '#F68E9D' }}>
|
||||
(using homomorphic encryption!)
|
||||
</h3>
|
||||
</div>
|
||||
<Explainer></Explainer>
|
||||
<div className="deck">
|
||||
<PrivateReceiveValentineCard
|
||||
loading={loading}
|
||||
fetchedMessage={fetchedMessage}
|
||||
handler={handleFetch}
|
||||
></PrivateReceiveValentineCard>
|
||||
<SendValentineCard
|
||||
loading={posting}
|
||||
handler={handleSubmit}
|
||||
></SendValentineCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="footer">
|
||||
<Faq></Faq>
|
||||
<div className="trace">
|
||||
<div>
|
||||
<h2>Trace</h2>
|
||||
</div>
|
||||
<div>
|
||||
{trace.length > 0
|
||||
? trace.map((t, i) => (
|
||||
<div key={i}>
|
||||
<LogMessage {...t} />
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
13
examples/valentines/src/index.tsx
Normal file
13
examples/valentines/src/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
import App from "./App";
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
const root = createRoot(rootElement!);
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
218
examples/valentines/src/styles.css
Normal file
218
examples/valentines/src/styles.css
Normal file
@@ -0,0 +1,218 @@
|
||||
@font-face {
|
||||
font-family: 'PlexMonoRegular';
|
||||
src: url('../fonts/IBMPlexMono-Regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
:root {
|
||||
--blyss-pink: #F68E9D;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
margin: auto;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #282c34;
|
||||
color: white;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--blyss-pink);
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
monospace;
|
||||
background-color: #222;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
|
||||
.App {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* align-items: center; */
|
||||
max-width: 900px;
|
||||
margin: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: "PlexMonoRegular";
|
||||
text-align: center;
|
||||
font-size: 30px;
|
||||
margin-top: 5%;
|
||||
}
|
||||
|
||||
.deck {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
gap: 30px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.actioncard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 20px;
|
||||
border: var(--blyss-pink) dashed 2px;
|
||||
border-radius: 20px;
|
||||
width: 300px;
|
||||
height: 400px;
|
||||
background-color: #222;
|
||||
font-family: "PlexMonoRegular";
|
||||
font-size: 16px;
|
||||
|
||||
}
|
||||
|
||||
.actioncard-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.actioncard-field h3 {
|
||||
color: white;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.actioncard input, .actioncard textarea {
|
||||
background: #FFF;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
/* without this repeat, input won't inherit from actioncard? */
|
||||
font-family: "PlexMonoRegular";
|
||||
font-size: 16px;
|
||||
resize: none; /* hides the resize handle in lower-right corner */
|
||||
}
|
||||
|
||||
.actioncard textarea {
|
||||
flex-grow: 2;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.actioncard-field .fetchedMessage {
|
||||
color: white;
|
||||
background-color: #282c34;
|
||||
}
|
||||
|
||||
|
||||
::placeholder {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.actioncard h2 {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
|
||||
.actioncard-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
column-gap: 20px;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
|
||||
button {
|
||||
background: var(--blyss-pink);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
font-family: "PlexMonoRegular";
|
||||
font-size: 16px;
|
||||
color: black;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-main {
|
||||
/* padding: 60px;
|
||||
padding-top: 120px; */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.explainer {
|
||||
font-size: 24px;
|
||||
line-height: 1.5;
|
||||
margin-left: 10%;
|
||||
margin-right: 10%;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 50px;
|
||||
gap: 50px;
|
||||
}
|
||||
|
||||
.FAQ {
|
||||
max-width: 500px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.FAQ h4{
|
||||
margin-bottom: 0px
|
||||
}
|
||||
.FAQ p{
|
||||
margin-top: 10px
|
||||
}
|
||||
|
||||
.trace {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.logline {
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
||||
@keyframes heartBeat {
|
||||
0% { transform: scale(0.95) }
|
||||
5% { transform: scale(1.1) }
|
||||
39% { transform: scale(0.85) }
|
||||
45% { transform: scale(1) }
|
||||
60% { transform: scale(0.95) }
|
||||
100% { transform: scale(0.9) }
|
||||
}
|
||||
|
||||
.loader {
|
||||
position: relative;
|
||||
width: 20px;
|
||||
height: 30px;
|
||||
animation: heartBeat 1.2s infinite cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||
}
|
||||
|
||||
.loader:before,
|
||||
.loader:after {
|
||||
content: "";
|
||||
background: #ff3d00 ;
|
||||
width: 20px;
|
||||
height: 30px;
|
||||
border-radius: 50px 50px 0 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
transform: rotate(45deg);
|
||||
transform-origin: 50% 68%;
|
||||
box-shadow: 5px 4px 5px #0004 inset;
|
||||
}
|
||||
.loader:after {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
28
examples/valentines/src/util.tsx
Normal file
28
examples/valentines/src/util.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
export interface Log {
|
||||
to: string;
|
||||
isRetrieval: boolean;
|
||||
tookMs: number;
|
||||
}
|
||||
|
||||
export function LogMessage({ to, isRetrieval, tookMs }: Log) {
|
||||
const tookMsg = (
|
||||
<span style={{ color: '#666', paddingLeft: 5 }}>
|
||||
({Math.round((tookMs / 1000) * 10) / 10} s)
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div className="logline">
|
||||
{!isRetrieval ? (
|
||||
<>
|
||||
Sent a valentine to {to}. {tookMsg}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Privately checked mailbox for {to}. {tookMsg}.
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
26
examples/valentines/tsconfig.json
Normal file
26
examples/valentines/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
@@ -87,8 +87,25 @@ export class Bucket {
|
||||
|
||||
private async getEndResult(key: string, queryResult: Uint8Array) {
|
||||
const decryptedResult = this.lib.decodeResponse(queryResult);
|
||||
const decompressedResult = decompress(decryptedResult);
|
||||
const extractedResult = this.lib.extractResult(key, decompressedResult);
|
||||
|
||||
let decompressedResult = null;
|
||||
try {
|
||||
decompressedResult = decompress(decryptedResult);
|
||||
} catch (e) {
|
||||
console.log('decompress error', e);
|
||||
}
|
||||
if (decompressedResult === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let extractedResult = null;
|
||||
try {
|
||||
extractedResult = this.lib.extractResult(key, decompressedResult);
|
||||
} catch {}
|
||||
if (extractedResult === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = deserialize(extractedResult);
|
||||
return result;
|
||||
}
|
||||
@@ -259,8 +276,14 @@ export class Bucket {
|
||||
*
|
||||
* @param {string} key - The key to _privately_ retrieve the value of.
|
||||
*/
|
||||
async privateRead(key: string): Promise<any> {
|
||||
return (await this.performPrivateRead(key)).data;
|
||||
async privateRead(key: string | string[]): Promise<any> {
|
||||
if (Array.isArray(key)) {
|
||||
return (await this.performPrivateReads(key)).map(r => r.data);
|
||||
} else {
|
||||
console.log('key', key);
|
||||
let result = await this.performPrivateRead(key);
|
||||
return result ? result.data : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,19 +312,25 @@ export class Bucket {
|
||||
*
|
||||
* @param keys - The keys to _privately_ intersect the value of.
|
||||
*/
|
||||
async privateIntersect(keys: string[]): Promise<any> {
|
||||
async privateIntersect(
|
||||
keys: string[],
|
||||
retrieveValues: boolean = true
|
||||
): Promise<any> {
|
||||
if (keys.length < BLOOM_CUTOFF) {
|
||||
return (await this.performPrivateReads(keys)).map(x => x.data);
|
||||
}
|
||||
|
||||
const bloomFilter = await this.api.bloom(this.name);
|
||||
const matches = [];
|
||||
const matches: string[] = [];
|
||||
for (const key of keys) {
|
||||
if (await bloomLookup(bloomFilter, key)) {
|
||||
matches.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (!retrieveValues) {
|
||||
return matches;
|
||||
}
|
||||
return (await this.performPrivateReads(matches)).map(x => x.data);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,15 @@ import { Bucket } from './bucket';
|
||||
export interface BucketParameters {
|
||||
/** The maximum item size this bucket supports */
|
||||
maxItemSize: MaxItemSizeIdentifier;
|
||||
keyStoragePolicy: kspIdentifier;
|
||||
}
|
||||
|
||||
type MaxItemSizeIdentifier = '100B' | '1KB' | '10KB';
|
||||
type kspIdentifier = 'none' | 'bloom' | 'full';
|
||||
|
||||
const DEFAULT_BUCKET_PARAMETERS: BucketParameters = {
|
||||
maxItemSize: '1KB'
|
||||
maxItemSize: '1KB',
|
||||
keyStoragePolicy: 'bloom'
|
||||
};
|
||||
|
||||
const BLYSS_BUCKET_URL = 'https://beta.api.blyss.dev';
|
||||
|
||||
@@ -53,4 +53,4 @@
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.11.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user