Skip to content

Instantly share code, notes, and snippets.

@AbeEstrada
Last active January 5, 2023 00:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AbeEstrada/96851bc70f8391fd920ca10d3ba2733c to your computer and use it in GitHub Desktop.
Save AbeEstrada/96851bc70f8391fd920ca10d3ba2733c to your computer and use it in GitHub Desktop.
Privnote + AWS Lambda
import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3";
const s3Client = new S3Client({ region: "us-east-1" });
const Bucket = "BUCKET_NAME_GOES_HERE";
export const handler = async (event) => {
let statusCode = 200;
let body = ``;
if (event.requestContext.http.method === "POST") {
// POST /
const buff = Buffer.from(event.body ?? "", "base64");
const eventBody = buff.toString("UTF-8");
const params = new URLSearchParams(eventBody);
const uuid = params.get("uuid");
const note = params.get("ciphered");
const bytes = Buffer.byteLength(note);
// Only allow to save notes below 1MB
if (bytes < 1048577) {
const bucketParams = {
Bucket,
ACL: "private",
ServerSideEncryption: "AES256",
// Do not store the last part of the key
Key: uuid?.split("-")?.splice(0, 4)?.join("-"),
Body: `${note}`,
};
try {
// Save note
await s3Client.send(new PutObjectCommand(bucketParams));
body = `<div>
<p>
<label for="note" class="w-100 alert alert-warning fw-light fst-italic py-1">The note will self-destruct after reading it.</label>
<input type="text" class="form-control shadow-sm" id="link" value="https://biubkhqe6zz3hypoitljy2vqti0yyvbn.lambda-url.us-east-1.on.aws/${uuid}" onClick="this.select()" readonly />
</p>
</div>`;
} catch (error) {
console.log(error);
statusCode = 500;
body = `<div class="alert alert-danger">Error: saving</div>`;
}
} else {
statusCode = 500;
body = `<div class="alert alert-danger">Error: too big</div>`;
}
} else {
// GET /:uuid
const uuid = `${event.requestContext.http.path}`.replaceAll("/", "");
if (uuid !== "") {
const bucketParams = { Bucket, Key: uuid?.split("-")?.splice(0, 4)?.join("-") };
try {
// Load note
const data = await s3Client.send(new GetObjectCommand(bucketParams));
const streamToString = await data.Body?.transformToString();
try {
// Delete note
await s3Client.send(new DeleteObjectCommand(bucketParams));
// Show note to the user
body = `<form id="noteForm">
<div class="col-12">
<label for="note" class="w-100 alert alert-warning fw-light fst-italic py-1">This note was destroyed. If you need to keep it, copy it before closing this window.</label>
<textarea class="form-control shadow-sm" id="note" name="note" rows="10" required></textarea>
</div>
</form>
<script type="text/javascript" src="https://unpkg.com/crypto-js/crypto-js.js"></script>
<script type="text/javascript">
const formEl = document.getElementById("noteForm");
document.addEventListener("DOMContentLoaded", () => {
// Decrypt in the browser
const bytes = CryptoJS.AES.decrypt("${streamToString}", "${uuid}");
const originalText = bytes.toString(CryptoJS.enc.Utf8);
formEl.elements.note.value = originalText;
});
</script>`;
} catch (error) {
console.log(error);
statusCode = 500;
body = `<div class="alert alert-danger">Error: deleting</div>`;
}
} catch (error) {
console.log(error);
statusCode = 404;
body = `<div class="alert alert-danger">Error 404: not found</div>`;
}
} else {
// Default GET /
body = `<form id="noteForm" class="row g-3 mx-auto" method="POST">
<div class="col-12">
<label for="note" class="form-label">New note:</label>
<textarea class="form-control shadow-sm" id="note" name="note" rows="10" required></textarea>
</div>
<div class="col-12 mt-4">
<button type="submit" name="submitButton" class="btn btn-primary shadow">Create note</button>
</div>
</form>
<script type="text/javascript" src="https://unpkg.com/crypto-js/crypto-js.js"></script>
<script type="text/javascript">
const formEl = document.getElementById("noteForm");
formEl.addEventListener("submit", (event) => {
formEl.elements.submitButton.disabled = true;
event.preventDefault();
// Generate uuid in the browser
const uuid = crypto.randomUUID();
// Cipher text
const ciphertext = CryptoJS.AES.encrypt(formEl.elements.note.value, uuid).toString();
// Add a hidden input to the form with the ciphered text
const cipheredEl = document.createElement('input');
cipheredEl.type = "hidden";
cipheredEl.name = "ciphered";
cipheredEl.value = ciphertext;
formEl.appendChild(cipheredEl);
// Add a hidden input with the uuid
const uuidEl = document.createElement('input');
uuidEl.type = "hidden";
uuidEl.name = "uuid";
uuidEl.value = uuid;
formEl.appendChild(uuidEl);
// Submit form
formEl.submit();
});
</script>`;
}
}
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Private Note</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous" />
</head>
<body class="bg-body-tertiary">
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid" style="max-width:720px">
<a class="navbar-brand" href="/">🔒 Private Note</a>
</div>
</nav>
<main class="container my-3" style="max-width:720px">
${body}
</main>
</body>
</html>`;
const response = {
statusCode,
headers: { "Content-Type": "text/html" },
body: html,
};
return response;
};
@AbeEstrada
Copy link
Author

This is my Privnote clone in a single AWS Lambda.

Permissions required:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:PutObjectAcl",
        "s3:GetObject",
        "s3:GetObjectAcl",
        "s3:DeleteObject"
      ],
      "Resource": [
        "arn:aws:s3:::BUCKET_NAME_GOES_HERE",
        "arn:aws:s3:::BUCKET_NAME_GOES_HERE/*"
      ]
    }
  ]
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment