Backup configuration
Configure Google Sheet sync
Use this guide to create the Google Sheet tabs, deploy the Apps Script webhook, and connect BPass Settings to your own spreadsheet.
Setup steps
- Create a Google Sheet with two tabs named Regular and Backup.
- Open Extensions, Apps Script, then paste the script below.
- Set BPASS_SECRET to a long random value with at least 16 characters.
- Deploy the Apps Script as a Web app that executes as you and is accessible to anyone with the link.
- Copy the deployed /exec URL into BPass Settings, enter the same webhook secret, then enable sync. BPass uses this secret to encrypt TOTP secrets before writing to the Sheet.
- Use Send test row before backing up real accounts, then run Backup all accounts to replace older plain-text rows.
Security checklist
- Use a long secret and keep it out of public repos.
- Rotate the secret if the Apps Script URL leaks.
- Keep the Sheet private even though synced TOTP secrets are encrypted.
- Use the same Settings secret to restore encrypted backups.
- After rotating the secret, run a fresh full backup.
- Use the Backup tab for full restore and Regular for live adds.
Apps Script
Paste this into Google Apps Script
const BPASS_SECRET = "paste-your-secret-here-min-16-chars";
const HEADERS = ["issuer", "label", "secret", "algorithm", "digits", "period", "createdAt"];
function json(data) {
return ContentService
.createTextOutput(JSON.stringify(data))
.setMimeType(ContentService.MimeType.JSON);
}
function doPost(e) {
try {
const data = e.parameter || {};
if (String(data.token || "") !== BPASS_SECRET) {
return json({ ok: false, error: "Invalid token" });
}
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const tabName = String(data.sheetTab || "Regular").trim();
const sheet = spreadsheet.getSheetByName(tabName) || spreadsheet.insertSheet(tabName);
ensureHeaders(sheet);
if (data.action === "add") {
sheet.appendRow([
data.issuer || "",
data.label || "",
formatStoredSecret(data.secret),
data.algorithm || "SHA1",
Number(data.digits || 6),
Number(data.period || 30),
data.createdAt || new Date().toISOString(),
]);
return json({ ok: true, written: 1 });
}
if (data.action === "backup") {
const accounts = JSON.parse(data.accounts || "[]");
sheet.clearContents();
ensureHeaders(sheet);
if (accounts.length) {
sheet.getRange(2, 1, accounts.length, HEADERS.length).setValues(
accounts.map(function (account) {
return [
account.issuer || "",
account.label || "",
formatStoredSecret(account.secret),
account.algorithm || "SHA1",
Number(account.digits || 6),
Number(account.period || 30),
account.createdAt || new Date().toISOString(),
];
})
);
}
return json({ ok: true, written: accounts.length });
}
if (data.action === "pull") {
return json({ ok: true, accounts: readAccounts(sheet) });
}
return json({ ok: false, error: "Unknown action" });
} catch (error) {
return json({ ok: false, error: String(error && error.message || error) });
}
}
function ensureHeaders(sheet) {
const firstRow = sheet.getRange(1, 1, 1, HEADERS.length).getValues()[0];
if (firstRow.join("") !== HEADERS.join("")) {
sheet.getRange(1, 1, 1, HEADERS.length).setValues([HEADERS]);
}
}
function readAccounts(sheet) {
const values = sheet.getDataRange().getValues();
return values.slice(1).filter(function (row) {
return row[2];
}).map(function (row) {
return {
issuer: String(row[0] || "Unknown"),
label: String(row[1] || ""),
secret: formatStoredSecret(row[2]),
algorithm: String(row[3] || "SHA1"),
digits: Number(row[4] || 6),
period: Number(row[5] || 30),
createdAt: row[6] ? new Date(row[6]).toISOString() : new Date().toISOString(),
};
});
}
function normalizeSecret(secret) {
return String(secret || "").replace(/\s+/g, "").replace(/=+$/, "").toUpperCase();
}
function formatStoredSecret(secret) {
const value = String(secret || "").trim();
if (value.indexOf("bpass:v1:") === 0) return value;
return normalizeSecret(value);
}