Compare commits

..

4 commits

Author SHA1 Message Date
1e99
9daefc9d2d make dialogs more consistant 2024-10-30 13:24:59 +01:00
1e99
5b29bb7285 use gob instead of JSON 2024-10-30 12:18:27 +01:00
1e99
7536e5d9af store passwords on disk using JSON instead of gob 2024-10-30 11:51:20 +01:00
1e99
aaa1715f32 rework password expiration cleaning 2024-10-30 11:47:17 +01:00
8 changed files with 501 additions and 426 deletions

15
main.go
View file

@ -22,7 +22,20 @@ func run() error {
return err return err
} }
defer storage.Close() go func() {
ticker := time.Tick(20 * time.Second)
for {
<-ticker
err := storage.ClearExpired()
if err != nil {
log.Printf("Failed to clear expired passwords: %s", err)
continue
}
log.Printf("Cleared expired passwords.")
}
}()
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle("GET /", routes.ServeFiles(embedFS, "static")) mux.Handle("GET /", routes.ServeFiles(embedFS, "static"))

52
static/api.js Normal file
View file

@ -0,0 +1,52 @@
/** Upload a password to the server
* @param {string} password Encryptes password
* @param {Promise<number>} expiresIn Number of seconds in which the password will expire
*/
async function uploadPassword(password, expiresIn) {
const res = await fetch("/api/password", {
method: "POST",
body: JSON.stringify({
password: password,
"expires-in": expiresIn,
}),
});
if (!res.ok) {
const msg = await res.text();
throw new Error(`Failed to upload password: ${res.status}: ${msg}`);
}
const json = await res.json();
return json.id;
}
/** Check if the server knows about the password
* @param {string} id Password ID
* @returns {Promise<bool>}
*/
async function hasPassword(id) {
const res = await fetch(`/api/password/${id}`, {
method: "HEAD",
});
if (res.status == 404) {
return false;
}
if (res.status == 204) {
return true;
}
throw new Error(`Failed to check if password exists: ${res.status}: ${msg}`);
}
/** Get a password from the server
* @param {string} id Password ID
* @returns {Promise<string>}
*/
async function getPassword(id) {
const res = await fetch(`/api/password/${id}`);
if (!res.ok) {
const msg = await res.text();
throw new Error(`Failed to get password: ${res.status}: ${msg}`);
}
const json = await res.json();
return json.password;
}

108
static/crypto.js Normal file
View file

@ -0,0 +1,108 @@
const ALGORITHM_NAME = "AES-GCM";
const ALGORITHM_LENGTH = 256;
const IV_LENGTH = 16;
const TEXT_ENCODER = new TextEncoder();
const TEXT_DECODER = new TextDecoder();
/** Coverts an ArrayBuffer into a Base64 encoded string
* @param {ArrayBuffer} buffer
* @returns {string}
*/
function bufferToBase64(buffer) {
let binary = "";
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
/** Converts a Base64 encoded string into an ArrayBuffer
* @param {string} base64
* @returns {ArrayBuffer}
*/
function base64ToBuffer(base64) {
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
/** Generates an key and encrypts "password" using it
* @param {Promise<string>} password
*/
async function encryptPassword(password) {
const iv = new Uint8Array(IV_LENGTH);
window.crypto.getRandomValues(iv);
const key = await window.crypto.subtle.generateKey(
{
name: ALGORITHM_NAME,
length: ALGORITHM_LENGTH,
},
true,
["encrypt", "decrypt"],
);
const encryptedPassword = await window.crypto.subtle.encrypt(
{
name: ALGORITHM_NAME,
iv: iv,
},
key,
TEXT_ENCODER.encode(password),
);
const exportedKey = await window.crypto.subtle.exportKey("raw", key);
const encodedPassword = bufferToBase64(encryptedPassword);
const encodedKey = bufferToBase64(exportedKey);
const encodedIv = bufferToBase64(iv.buffer);
return {
password: encodedPassword,
key: encodedKey,
iv: encodedIv,
};
}
/** Decodes to key and iv and decryptes the password
* @param {string} password
* @param {string} key
* @param {string} iv
* @returns {Promise<string>}
*/
async function decryptPassword(password, key, iv) {
const decodedPassword = base64ToBuffer(password);
const decodedKey = base64ToBuffer(key);
const decodedIv = base64ToBuffer(iv);
const cryptoKey = await window.crypto.subtle.importKey(
"raw",
decodedKey,
{
name: ALGORITHM_NAME,
length: ALGORITHM_LENGTH,
},
true,
["encrypt", "decrypt"],
);
const decryptedPassword = await window.crypto.subtle.decrypt(
{
name: ALGORITHM_NAME,
iv: decodedIv,
},
cryptoKey,
decodedPassword,
);
return TEXT_DECODER.decode(decryptedPassword);
}

View file

@ -7,11 +7,11 @@
<!--<link rel="icon" href="favicon.png" />--> <!--<link rel="icon" href="favicon.png" />-->
<title>PassED</title> <title>PassED</title>
<script src="/crypto.js"></script>
<script src="/api.js"></script>
<script src="/index.js" defer></script> <script src="/index.js" defer></script>
<link rel="stylesheet" href="/pico.min.css" /> <link rel="stylesheet" href="/pico.min.css" />
</head> </head>
<body>
<nav class="container-fluid"> <nav class="container-fluid">
<ul> <ul>
<li> <li>
@ -27,14 +27,10 @@
</nav> </nav>
<main class="container"> <main class="container">
<noscript>
<h1>This website requires JavaScript to function.</h1>
</noscript>
<article> <article>
<header>Enter your password</header> <header>Enter Password</header>
<form id="password"> <form id="enter-password">
<label> <label>
Password Password
<textarea name="password"></textarea> <textarea name="password"></textarea>
@ -54,61 +50,51 @@
<button type="submit">Generate URL</button> <button type="submit">Generate URL</button>
</form> </form>
</article> </article>
</main>
<dialog id="url"> <dialog id="url-dialog">
<article> <article>
<header>Password URL</header> <header>Password URL</header>
<fieldset role="group"> <fieldset role="group">
<input id="password-url" readonly autofocus /> <input id="url" readonly autofocus />
<button id="url-copy">Copy</button> <button id="url-copy">Copy</button>
</fieldset> </fieldset>
<button id="share-another">Share Another</button> <footer>
<button id="url-close">Close</button>
</footer>
</article> </article>
</dialog> </dialog>
<dialog id="confirm-view"> <dialog id="view-dialog">
<article> <article>
<header>View Password</header> <header>View Password</header>
<p class="pico-color-red-500"> <textarea readonly id="view-password" input></textarea>
You may only reveal the password once.
</p> <footer>
<button id="confirm-view">OK</button> <button id="view-close">Close</button>
</footer>
</article> </article>
</dialog> </dialog>
<dialog id="view"> <dialog id="confirm-view-dialog">
<article> <article>
<header>Password</header> <header>View Password</header>
<textarea id="password" readonly></textarea> <p>You may only reveal the password once.</p>
<footer>
<button id="view-cancel" class="secondary">
Cancel
</button>
<button id="view-confirm">Confirm</button>
</footer>
</article> </article>
</dialog> </dialog>
<dialog id="loading"> <dialog id="not-found">
<article>
<header>Loading...</header>
</article>
</dialog>
<dialog id="error">
<article>
<header>Error</header>
<label>
Something went horribly wrong..
<textarea id="error-message" readonly></textarea>
</label>
<form method="get" action="/">
<button type="submit">Close</button>
</form>
</article>
</dialog>
<dialog id="password-ne">
<article> <article>
<header>Password does not exist</header> <header>Password does not exist</header>
@ -117,11 +103,26 @@
before or never even existed in the first place. before or never even existed in the first place.
</p> </p>
<form method="get" action="/"> <footer>
<button type="submit">Close</button> <button id="not-found-close" class="secondary">Close</button>
</form> </footer>
</article>
</dialog>
<dialog id="loading-dialog">
<h1>Loading...</h1>
</dialog>
<dialog id="error-dialog">
<article>
<header>Error</header>
<textarea id="error" readonly></textarea>
<footer>
<button id="error-reload">Reload the page</button>
</footer>
</article> </article>
</dialog> </dialog>
</main>
</body> </body>
</html> </html>

View file

@ -1,269 +1,199 @@
/* const loadingDialog = {
Welcome to the magic behind PassED! dialog: document.querySelector("dialog#loading-dialog"),
This file is responsible for encrypting passwords in the browser and to only send encrypted passwords to the server. init() {
this.dialog.addEventListener("cancel", (ev) => {
- https://git.1e99.eu/1e99/passed
- https://discord.gg/NuGxJKtDKS
*/
const ALGORITHM_NAME = "AES-GCM";
const ALGORITHM_LENGTH = 256;
const IV_LENGTH = 16;
const TEXT_ENCODER = new TextEncoder();
const TEXT_DECODER = new TextDecoder();
const ACTIVE_PAGE = "page-active";
/** Coverts an ArrayBuffer into a Base64 encoded string
* @param {ArrayBuffer} buffer
* @returns {string}
*/
function bufferToBase64(buffer) {
let binary = "";
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
/** Converts a Base64 encoded string into an ArrayBuffer
* @param {string} base64
* @returns {ArrayBuffer}
*/
function base64ToBuffer(base64) {
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
/** Generates an key and encrypts "password" using it
* @param {string} password
*/
async function encryptPassword(password) {
const iv = new Uint8Array(IV_LENGTH);
window.crypto.getRandomValues(iv);
const key = await window.crypto.subtle.generateKey(
{
name: ALGORITHM_NAME,
length: ALGORITHM_LENGTH,
},
true,
["encrypt", "decrypt"],
);
const encryptedPassword = await window.crypto.subtle.encrypt(
{
name: ALGORITHM_NAME,
iv: iv,
},
key,
TEXT_ENCODER.encode(password),
);
const exportedKey = await window.crypto.subtle.exportKey("raw", key);
const encodedPassword = bufferToBase64(encryptedPassword);
const encodedKey = bufferToBase64(exportedKey);
const encodedIv = bufferToBase64(iv.buffer);
return {
password: encodedPassword,
key: encodedKey,
iv: encodedIv,
};
}
/** Decodes to key and iv and decryptes the password
* @param {string} password
* @param {string} key
* @param {string} iv
*/
async function decryptPassword(password, key, iv) {
const decodedPassword = base64ToBuffer(password);
const decodedKey = base64ToBuffer(key);
const decodedIv = base64ToBuffer(iv);
const cryptoKey = await window.crypto.subtle.importKey(
"raw",
decodedKey,
{
name: ALGORITHM_NAME,
length: ALGORITHM_LENGTH,
},
true,
["encrypt", "decrypt"],
);
const decryptedPassword = await window.crypto.subtle.decrypt(
{
name: ALGORITHM_NAME,
iv: decodedIv,
},
cryptoKey,
decodedPassword,
);
return TEXT_DECODER.decode(decryptedPassword);
}
function showError(error) {
const errorDialog = document.querySelector("dialog#error");
const errorMessage = document.querySelector("textarea#error-message");
errorMessage.value = error;
errorDialog.showModal();
console.error(error);
}
function init() {
const passwordForm = document.querySelector("form#password");
const loadingDialog = document.querySelector("dialog#loading");
const urlDialog = document.querySelector("dialog#url");
const urlCopy = document.querySelector("button#url-copy");
const passwordUrl = document.querySelector("input#password-url");
const shareAnother = document.querySelector("button#share-another");
passwordForm.addEventListener("submit", async (ev) => {
try {
loadingDialog.showModal();
ev.preventDefault(); ev.preventDefault();
const data = new FormData(ev.target);
const encrypted = await encryptPassword(data.get("password"));
const res = await fetch("/api/password", {
method: "POST",
body: JSON.stringify({
password: encrypted.password,
"expires-in": parseInt(data.get("expires-in")),
}),
}); });
if (!res.ok) { },
const msg = await res.text(); show() {
throw new Error(`Failed to upload password: ${res.status}: ${msg}`); this.dialog.showModal();
} },
close() {
this.dialog.close();
},
};
const json = await res.json(); const errorDialog = {
dialog: document.querySelector("dialog#error-dialog"),
const params = new URLSearchParams(); error: document.querySelector("textarea#error"),
params.set("id", json.id); reload: document.querySelector("button#error-reload"),
params.set("key", encrypted.key); init() {
params.set("iv", encrypted.iv); this.dialog.addEventListener("close", (ev) => {
window.location.reload();
const url = new URL(window.location);
url.search = params.toString();
passwordUrl.value = url.toString();
urlDialog.showModal();
passwordUrl.select();
} catch (error) {
showError(error);
} finally {
loadingDialog.close();
}
}); });
loadingDialog.addEventListener("close", (ev) => ev.preventDefault()); this.reload.addEventListener("click", (ev) => {
shareAnother.addEventListener("click", (ev) => { window.location.href = "/";
passwordForm.reset();
passwordUrl.value = "";
urlDialog.close();
}); });
urlCopy.addEventListener("click", async (ev) => { },
passwordUrl.select(); show(err) {
passwordUrl.setSelectionRange(0, 99999); this.error.value = err;
await navigator.clipboard.writeText(passwordUrl.value); console.error(err);
this.dialog.showModal();
},
};
const urlDialog = {
dialog: document.querySelector("dialog#url-dialog"),
url: document.querySelector("input#url"),
urlCopy: document.querySelector("button#url-copy"),
close: document.querySelector("button#url-close"),
init() {
this.dialog.addEventListener("close", (ev) => {});
this.urlCopy.addEventListener("click", (ev) => {
this.url.select();
this.url.setSelectionRange(0, 99999);
navigator.clipboard.writeText(this.url.value);
}); });
}
async function confirmViewPassword(params) { this.close.addEventListener("click", (ev) => {
const confirmDialog = document.querySelector("dialog#confirm-view"); this.dialog.close();
const confirm = document.querySelector("button#confirm-view"); });
const loadingDialog = document.querySelector("dialog#loading"); },
const passwordNEDialog = document.querySelector("dialog#password-ne"); show(url) {
this.url.value = url;
this.dialog.showModal();
},
};
const notFoundDialog = {
dialog: document.querySelector("dialog#not-found"),
close: document.querySelector("button#not-found-close"),
init() {
this.dialog.addEventListener("close", (ev) => {
window.location.search = "";
});
this.close.addEventListener("click", (ev) => {
window.location.search = "";
});
},
show() {
this.dialog.showModal();
},
};
const viewDialog = {
dialog: document.querySelector("dialog#view-dialog"),
password: document.querySelector("textarea#view-password"),
close: document.querySelector("button#view-close"),
init() {
this.dialog.addEventListener("close", (ev) => {
window.location.search = "";
});
this.close.addEventListener("click", (ev) => {
this.dialog.close();
});
},
show(password) {
this.password.value = password;
this.dialog.showModal();
},
};
const confirmViewDialog = {
dialog: document.querySelector("dialog#confirm-view-dialog"),
cancel: document.querySelector("button#view-cancel"),
confirm: document.querySelector("button#view-confirm"),
resolve: null,
reject: null,
init() {
this.dialog.addEventListener("cancel", (ev) => {
this.dialog.close();
this.resolve(false);
});
this.cancel.addEventListener("click", (ev) => {
this.dialog.close();
this.resolve(false);
});
this.confirm.addEventListener("click", (ev) => {
this.dialog.close();
this.resolve(true);
});
},
show() {
return new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
this.dialog.showModal();
});
},
};
async function viewPassword() {
try { try {
loadingDialog.showModal(); loadingDialog.show();
const res = await fetch(`/api/password/${params.get("id")}`, { const params = new URLSearchParams(window.location.search);
method: "HEAD", const id = params.get("id");
}); const key = params.get("key");
console.log(res.status); const iv = params.get("iv");
if (res.status == 404) {
loadingDialog.close();
passwordNEDialog.showModal();
console.log("Return");
const exists = await hasPassword(id);
if (!exists) {
notFoundDialog.show();
return; return;
} }
if (!res.ok) { const shouldView = await confirmViewDialog.show();
const msg = await res.text(); if (!shouldView) {
throw new Error( // This is needed for the redirect, otherwise the user won't get redirected
`Failed to check if password exists: ${res.status}: ${msg}`, setTimeout(() => (window.location.search = ""), 0);
); return;
} }
const encrypted = await getPassword(id);
const password = await decryptPassword(encrypted, key, iv);
viewDialog.show(password);
} catch (error) { } catch (error) {
showError(error); errorDialog.show(error);
} finally { } finally {
}
loadingDialog.close(); loadingDialog.close();
confirmDialog.showModal(); }
confirm.addEventListener("click", (ev) => {
confirmDialog.close();
viewPassword(params);
});
} }
async function viewPassword(params) { const enterPassword = document.querySelector("form#enter-password");
const viewDialog = document.querySelector("dialog#view");
const password = document.querySelector("textarea#password");
const loadingDialog = document.querySelector("dialog#loading");
enterPassword.addEventListener("submit", async (ev) => {
try { try {
loadingDialog.showModal(); loadingDialog.show();
ev.preventDefault();
const data = new FormData(ev.target);
const res = await fetch(`/api/password/${params.get("id")}`); const password = await encryptPassword(data.get("password"));
if (!res.ok) { const id = await uploadPassword(
const msg = await res.text(); password.password,
throw new Error(`Failed to load password: ${res.status}: ${msg}`); parseInt(data.get("expires-in")),
}
const json = await res.json();
const decrypted = await decryptPassword(
json.password,
params.get("key"),
params.get("iv"),
); );
password.value = decrypted; const params = new URLSearchParams();
viewDialog.showModal(); params.set("id", id);
params.set("key", password.key);
params.set("iv", password.iv);
const url = new URL(window.location);
url.search = params.toString();
urlDialog.show(url.toString());
} catch (error) { } catch (error) {
showError(error); errorDialog.show(error);
} finally { } finally {
loadingDialog.close(); loadingDialog.close();
} }
}
window.addEventListener("load", () => {
const query = window.location.search;
if (query.trim() != "") {
const params = new URLSearchParams(window.location.search);
confirmViewPassword(params);
}
init();
}); });
loadingDialog.init();
errorDialog.init();
urlDialog.init();
notFoundDialog.init();
viewDialog.init();
confirmViewDialog.init();
const query = window.location.search;
if (query.trim() != "") {
viewPassword();
}

View file

@ -8,23 +8,21 @@ import (
"time" "time"
) )
func NewDirStore(clearInterval time.Duration, path string) Store { func NewDirStore(path string) Store {
store := &dir{ store := &dir{
clearInterval: clearInterval,
timeLayout: time.RFC3339Nano,
path: path, path: path,
close: make(chan bool),
} }
go store.clearExpired()
return store return store
} }
type dir struct { type dir struct {
clearInterval time.Duration
timeLayout string
path string path string
close chan bool }
type dirEntry struct {
Password []byte
ExpiresAt time.Time
} }
func (store *dir) Create(password []byte, expiresAt time.Time) (string, error) { func (store *dir) Create(password []byte, expiresAt time.Time) (string, error) {
@ -46,7 +44,7 @@ func (store *dir) Create(password []byte, expiresAt time.Time) (string, error) {
defer file.Close() defer file.Close()
entry := entry{ entry := dirEntry{
Password: password, Password: password,
ExpiresAt: expiresAt, ExpiresAt: expiresAt,
} }
@ -79,7 +77,7 @@ func (store *dir) Get(id string) ([]byte, error) {
defer file.Close() defer file.Close()
var entry entry var entry dirEntry
err = gob.NewDecoder(file).Decode(&entry) err = gob.NewDecoder(file).Decode(&entry)
if err != nil { if err != nil {
return nil, err return nil, err
@ -98,26 +96,12 @@ func (store *dir) Delete(id string) error {
return nil return nil
} }
func (store *dir) Close() error { func (store *dir) ClearExpired() error {
store.close <- true
return nil
}
func (store *dir) clearExpired() error {
ticker := time.NewTicker(store.clearInterval)
defer ticker.Stop()
for {
select {
case <-store.close:
return nil
case <-ticker.C:
// TODO: Error handling?
now := time.Now() now := time.Now()
entries, err := os.ReadDir(store.path) entries, err := os.ReadDir(store.path)
if err != nil { if err != nil {
continue return err
} }
for _, file := range entries { for _, file := range entries {
@ -129,25 +113,28 @@ func (store *dir) clearExpired() error {
0, 0,
) )
if err != nil { if err != nil {
continue return err
} }
defer file.Close() defer file.Close()
var entry entry var entry dirEntry
err = gob.NewDecoder(file).Decode(&entry) err = gob.NewDecoder(file).Decode(&entry)
if err != nil { if err != nil {
continue return err
} }
if now.After(entry.ExpiresAt) { if now.After(entry.ExpiresAt) {
// Close file early as we need to delete it // Close file early as we need to delete it
file.Close() file.Close()
store.Delete(id) err = store.Delete(id)
} if err != nil {
return err
} }
} }
} }
return nil
} }
func (store *dir) getPath(id string) string { func (store *dir) getPath(id string) string {

View file

@ -5,23 +5,23 @@ import (
"time" "time"
) )
func NewRamStore(clearInterval time.Duration) Store { func NewRamStore() Store {
store := &ram{ store := &ram{
clearInterval: clearInterval, passwords: make(map[string]ramEntry),
passwords: make(map[string]entry),
lock: sync.Mutex{}, lock: sync.Mutex{},
close: make(chan bool),
} }
go store.clearExpired()
return store return store
} }
type ram struct { type ram struct {
clearInterval time.Duration passwords map[string]ramEntry
passwords map[string]entry
lock sync.Mutex lock sync.Mutex
close chan bool }
type ramEntry struct {
Password []byte
ExpiresAt time.Time
} }
func (store *ram) Create(password []byte, expiresAt time.Time) (string, error) { func (store *ram) Create(password []byte, expiresAt time.Time) (string, error) {
@ -35,7 +35,7 @@ func (store *ram) Create(password []byte, expiresAt time.Time) (string, error) {
continue continue
} }
store.passwords[id] = entry{ store.passwords[id] = ramEntry{
Password: password, Password: password,
ExpiresAt: expiresAt, ExpiresAt: expiresAt,
} }
@ -63,30 +63,19 @@ func (store *ram) Delete(id string) error {
return nil return nil
} }
func (store *ram) Close() error { func (store *ram) ClearExpired() error {
store.close <- true
return nil
}
func (store *ram) clearExpired() error {
ticker := time.NewTicker(store.clearInterval)
defer ticker.Stop()
for {
select {
case <-store.close:
return nil
case <-ticker.C:
store.lock.Lock() store.lock.Lock()
defer store.lock.Unlock()
time := time.Now() time := time.Now()
for id, password := range store.passwords { for id, password := range store.passwords {
if time.After(password.ExpiresAt) { if time.After(password.ExpiresAt) {
store.Delete(id) err := store.Delete(id)
if err != nil {
return err
}
} }
} }
store.lock.Unlock() return nil
}
}
} }

View file

@ -14,16 +14,11 @@ var (
ErrFull = errors.New("storage is filled") ErrFull = errors.New("storage is filled")
) )
type entry struct {
Password []byte
ExpiresAt time.Time
}
type Store interface { type Store interface {
Create(password []byte, expiresAt time.Time) (string, error) Create(password []byte, expiresAt time.Time) (string, error)
Get(id string) ([]byte, error) Get(id string) ([]byte, error)
Delete(id string) error Delete(id string) error
Close() error ClearExpired() error
} }
func NewStore() (Store, error) { func NewStore() (Store, error) {
@ -32,7 +27,7 @@ func NewStore() (Store, error) {
switch storeType { switch storeType {
case "ram": case "ram":
return NewRamStore(20 * time.Second), nil return NewRamStore(), nil
case "dir": case "dir":
path := os.Getenv("PASSED_STORE_DIR_PATH") path := os.Getenv("PASSED_STORE_DIR_PATH")
if path == "" { if path == "" {
@ -40,11 +35,11 @@ func NewStore() (Store, error) {
path = "passwords" path = "passwords"
} }
return NewDirStore(60*time.Second, path), nil return NewDirStore(path), nil
default: default:
log.Printf("No PASSED_STORE_TYPE provided, defaulting to memory store.") log.Printf("No PASSED_STORE_TYPE provided, defaulting to memory store.")
return NewRamStore(20 * time.Second), nil return NewRamStore(), nil
} }
} }