mirror of
https://codeberg.org/keyoxide/keyoxide-web.git
synced 2024-12-22 14:59:29 -07:00
Numerous changes
This commit is contained in:
parent
0beeab7101
commit
16c5997004
9 changed files with 349 additions and 513 deletions
163
content/about.md
Normal file
163
content/about.md
Normal file
|
@ -0,0 +1,163 @@
|
|||
There is a lot to Keyoxide and decentralized identity verification, so let's divide the knowledge in three sections of increasing complexity.
|
||||
|
||||
[[toc]]
|
||||
|
||||
## Basic
|
||||
|
||||
Keyoxide allows you to prove "ownership" of accounts on websites, domain names, IM, etc., regardless of your username.
|
||||
|
||||
That last part is important: you could, for example, be 'alice' on Lobste.rs, but '@alice24' on Twitter. And if your website is 'thatcoder.tld', how are people supposed to know that all that online property is yours?
|
||||
|
||||
Of course, one could opt for full anonymity! In which case, keep these properties as separated as possible.
|
||||
|
||||
But if you'd like these properties to be linked and, by doing so, establish an online identity, you'll need a clever solution.
|
||||
|
||||
Enter Keyoxide.
|
||||
|
||||
When you visit someone's Keyoxide profile and see a green tick next to an account on some website, it was proven beyond doubt that the same person who set up this profile also holds that account.
|
||||
|
||||
## Intermediate
|
||||
|
||||
Keyoxide's purpose is just that: linking online properties together. Now, many services could easily claim they could accomplish the same feat. To ensure it happens in a trustworthy manner, Keyoxide uses an "open source, decentralized and cryptography-based approach to bidirectional linking".
|
||||
|
||||
Let's break down that sentence.
|
||||
|
||||
### Open source
|
||||
|
||||
Open source means: everyone can inspect the code behind Keyoxide. Really! Here, have a look at the code behind this website: [https://codeberg.org/keyoxide/web](https://codeberg.org/keyoxide/web). In fact, here is the code for the page you are currently reading: [link to come](/).
|
||||
|
||||
Not only can you look at it, you are allowed to make changes and even "fork it": take all this code and build your own product with it. Allowed? You are invited to! This keeps the Keyoxide project honest and always moving towards something that is better for everyone.
|
||||
|
||||
"Doesn't open source make it easier for others to steal your idea and your revenue?" Ah, good ol' capitalism. No. More about funding and the flow of money in the [Advanced](#Advanced) section.
|
||||
|
||||
The Keyoxide project is licensed under [AGPL-3.0-or-later](https://codeberg.org/keyoxide/web/src/branch/main/LICENSE).
|
||||
|
||||
### Decentralized
|
||||
|
||||
The topic of decentralization is vast and complex. In short, it refers to the practice of keeping data in separate but connected places, instead of putting all the data in one single place.
|
||||
|
||||
Have you noticed how Google, Facebook and banks are desired targets for hackers? That is because they all use a centralized model. Break in once, get all the data. Of course, breaking in is difficult. But not impossible.
|
||||
|
||||
Keyoxide uses decentralization on two levels: the profile data, and the identity verification process.
|
||||
|
||||
#### Decentralized profile data
|
||||
|
||||
Where does Keyoxide get the data from to generate the profile pages? To make a Facebook profile, you need to give your data to them. Is this the same? Rest assured, Keyoxide does not want your data.
|
||||
|
||||
You put your data in a so-called cryptographic key. For our purposes, let's consider this a "glass vault". Everyone (including Keyoxide) can look inside the glass vault and see your data, but no one except you can change it or delete it. You have full control over your data. You can store it where you want: on a dedicated "key server", on your own server. You can even put it in a little piece of text!
|
||||
|
||||
```
|
||||
-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA512
|
||||
|
||||
Hey there! Here's a signature profile with proofs related to the DOIP project (https://doip.rocks).
|
||||
|
||||
Verify this profile at https://keyoxide.org/sig
|
||||
|
||||
proof=dns:doip.rocks
|
||||
proof=https://fosstodon.org/@keyoxide
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQHEBAEBCgAuFiEENjcgJSPnwTCat56Z7y3FgntEX0sFAl/7L0MQHHRlc3RAZG9p
|
||||
cC5yb2NrcwAKCRDvLcWCe0RfS3iYC/0QQqz2lzSNrkApdIN9OJFfd/sP2qeGr/uH
|
||||
98YHa+ucwBxer6yrAaTYYuBJg1uyzdxQhqF2jWno7FwN4crnj15AN5XGemjpmqat
|
||||
py9wG6vCVjC81q/BWMIMZ7RJ/m8F8Kz556xHiU8KbqLNDqFVcT35/PhJsw71XVCI
|
||||
N3HgrgD7CY/vIsZ3WIH7mne3q9O7X4TJQtFoZZ/l9lKj7qk3LrSFnL6q+JxUr2Im
|
||||
xfYZKaSz6lmLf+vfPc59JuQtV1z0HSNDQkpKEjmLeIlc+ZNAdSQRjkfi+UDK7eKV
|
||||
KGOlkcslroJO6rT3ruqx9L3hHtrM8dKQFgtRSaofB51HCyhNzmipbBHnLnKQrcf6
|
||||
o8nn9OkP7F9NfbBE6xYIUCkgnv1lQbzeXsLLVuEKMW8bvZOmI7jTcthqnwzEIHj/
|
||||
G4p+zPGgO+6Pzuhn47fxH+QZ0KPA8o2vx0DvOkZT6HEqG+EqpIoC/a7wD68n789c
|
||||
K2NLCVb9oIGarPfhIdPV3QbrA5eXRRQ=
|
||||
=QyNy
|
||||
-----END PGP SIGNATURE-----
|
||||
```
|
||||
|
||||
This text above is all the data Keyoxide needs to generate a profile page. Everyone can read it. But no one can modify it. Change a single character in the text above and the signature gets invalidated.
|
||||
|
||||
A true "glass vault".
|
||||
|
||||
#### Decentralized identity verification
|
||||
|
||||
The process itself of verifying identities (more on this in the [bidirectional linking](#bidirectional-linking) section) is also decentralized. You do not need to contact Keyoxide servers for most verifications.
|
||||
|
||||
A Hackernews account can be verified by looking at the content of its "about" section. Most Fediverse accounts can be verified by looking at their "biography" section.
|
||||
|
||||
If you view a profile page on this website, your browser handles the identity verification by directly contacting Hackernews and the Fediverse. No intermediary servers required.
|
||||
|
||||
If you run `keyoxide verify ...` in a terminal, your computer handles the identity verification directly, not some server.
|
||||
|
||||
Once Keyoxide apps for mobile devices are developed (no ETA yet), your mobile device will handle the identity verification directly, not some server.
|
||||
|
||||
The less intermediary servers are required, the more trustworthy the process becomes.
|
||||
|
||||
Note: there are some exceptions to this when it comes to browsers, more on this in the [proxy](#proxy) section.
|
||||
|
||||
### Cryptography
|
||||
|
||||
Proofs are created and managed inside cryptographic keys that act as "secure transport vessels", which are stored on special servers that already contain the cryptographic keys of many people. Keyoxide simply goes looking for your key and reads the content.
|
||||
|
||||
What makes cryptographic keys so useful for us is that they are actually made of two keys: a "private key" and a "public key".
|
||||
|
||||
Everyone has access to the public key. This is the "glass vault" itself: everyone can see it, but no one can modify its content. You can safely share your public key.
|
||||
|
||||
The "glass vault" can only be opened and modified using the private key, which is usually nothing more than a file with seemingly random characters. The private key is yours and yours alone, and should never be shared.
|
||||
|
||||
It goes without saying that losing the private key means losing access to the public key. Likewise, someone who steals your private key can easily modify the contents of your public key. Handling cryptographic keys is no simple task, a process where security always takes precedence over convenience.
|
||||
|
||||
Keyoxide uses the widely-used and well-known [OpenPGP standard](https://www.openpgp.org).
|
||||
|
||||
You'll find guidance over on the [Getting started](/getting-started) page.
|
||||
|
||||
### Bidirectional linking
|
||||
|
||||
How does one prove they have control over two online properties?
|
||||
|
||||
Let's consider this slightly unusual scenario: how could I prove I own a car? We do not want to use any government services (which are centralized) so checking license plates is out of the question. I would also like to remain anonymous, so identity cards are also a no-go.
|
||||
|
||||
Obviously, I can't link the car to my person without revealing my identity. But what if I could link it to my house? That way, "whoever owns this house also owns this car". I establish an identity (I own this house and I own this car) without ever revealing my identity.
|
||||
|
||||
All I need to do is place a note behind a window of my home with the license plate on it. "Behind a window", not "outside": a glass vault! Everyone can read it, but only the person with the house keys can modify it.
|
||||
|
||||
I have now claimed my car in a fully decentralized manner, no need to involve any centralized organization. Is this sufficient? Yes and no. Only I could have placed that note there so that proves beyond doubt my access to the house.
|
||||
|
||||
But what if my neighbor puts my license plate on a note in their home? Their attempt at impersonation is an attempt at claiming ownership over my car.
|
||||
|
||||
This is why we can only trust bidirectional linking: not only does my home need a note behind a window with the car's license plate on it, my car needs a note under the windshield with my address on it. The neighbor can no longer be claim ownership over my car, as the car's note clearly states my home address, not theirs.
|
||||
|
||||
An unusual scenario indeed, but one that simplifies the stakes. Keyoxide allows you to establish an online identity while remaining anonymous: no one needs to know you are, but you can still prove you hold accounts on different websites.
|
||||
|
||||
## Advanced
|
||||
|
||||
By now, you should have all the knowledge to understand what is going on and get started. Here are a few more advanced (and optional) topics.
|
||||
|
||||
### Proxy
|
||||
|
||||
This section involves almost exclusively Keyoxide's web client (the website you are currently viewing). Native clients (like the [Keyoxide CLI](https://codeberg.org/keyoxide/cli)) do not need a proxy under normal circumstances.
|
||||
|
||||
Some services like Gitlab or DNS require complicated verification processes or code that cannot be run in a browser. In such cases, the browser will ask a Keyoxide server to do the verification instead.
|
||||
|
||||
Since this is the internet we are talking about, you should always be skeptical about data that comes from some unknown server. In order to mitigate this, each profile page on this website will invite you to perform the identity verification again but locally, using a native client appropriate to your device if one exists.
|
||||
|
||||
### Keyoxide instances
|
||||
|
||||
The Keyoxide website was built with the idea that other people could put it on their servers as well. We call these "instances". The Keyoxide project's lead developer has put an instance on [https://keyoxide.org](https://keyoxide.org) but that is not the only way to access Keyoxide. Everyone could put it on their servers.
|
||||
|
||||
Yes, another layer of decentralization.
|
||||
|
||||
The idea is simple: you will most likely not know the lead developer, so why should you trust his website [https://keyoxide.org](https://keyoxide.org)? By making the Keyoxide website selfhostable, you could put it yourself on your own server, or ask a friend to put it on theirs.
|
||||
|
||||
(By the way, you can trust me! But that is obviously not the point here)
|
||||
|
||||
Ultimately, any Keyoxide website/instance is potentially compromised and any identity verification should be performed locally to get the most trustworthy results.
|
||||
|
||||
### Funding and the flow of money
|
||||
|
||||
With surveillance capitalism on the rise, it's important to understand where money comes from, especially with a project that involves online identity!
|
||||
|
||||
The project is fully funded by donations. There are no fees to using or hosting Keyoxide. There are no ads. There is no tracking. There are no investors.
|
||||
|
||||
All donations come from the people and the organizations that see the need for a project like Keyoxide to exist and be universally accessible.
|
||||
|
||||
Donations go to the [Key To Identity Foundation](https://keytoidentity.foundation/), founded by the lead developer for the purpose of promoting and sustaining the Keyoxide projects and other future identity-enhancing projects.
|
||||
|
||||
If you'd like to donate as well, please have a look at the [foundation's donate page](https://keytoidentity.foundation/donate). All contributions are much appreciated and help the lead developer to fully commit to the Keyoxide project.
|
|
@ -10,33 +10,9 @@ Well, you can't.
|
|||
|
||||
But you don't need to!
|
||||
|
||||
Keyoxide is not your typical "web application" requiring you to create an account and log in to perform tasks. That sounds strange but this approach has many advantages! Let's first review some concepts of how Keyoxide does its magic.
|
||||
Keyoxide is not your typical "web application" requiring you to create an account and log in to perform tasks. That sounds strange but this approach has many advantages!
|
||||
|
||||
If you just want to know what to do, jump to [Generating an OpenPGP key](#generating-an-openpgp-key).
|
||||
|
||||
## How websites store and handle data
|
||||
|
||||
### Centralized model
|
||||
|
||||
Creating an account on a traditional or "centralized" website like Facebook allows them to store your data on their servers and generate a profile page with that data. The problem with this approach is that you don't own your data: Facebook owns your data. And once it's on their servers, it's hard to get rid of it.
|
||||
|
||||
Fortunately for you, we can do better.
|
||||
|
||||
### Decentralized model
|
||||
|
||||
Keyoxide still needs data to show on your profile page but it doesn't want to store that data on its server. Instead, Keyoxide goes looking for your data where you have decided to store it. This has the distinct advantage that you remain in control of that data! Remove it and it's no longer accessible to Keyoxide.
|
||||
|
||||
Also, no need to create yet another account on yet another website \o/
|
||||
|
||||
## Cryptographic keys
|
||||
|
||||
Proofs are created and managed inside cryptographic keys that act as "secure transport vessels", which are stored on special servers that already contain the cryptographic keys of many people. Keyoxide simply goes looking for your key and read the content.
|
||||
|
||||
What makes cryptographic keys so useful for us is that they are actually made of two keys: a "private key" that only you have on your device and a "public key" that you can give away to everyone. Only you can add and edit proofs but everyone can read them.
|
||||
|
||||
An important note: the guide below follows a few best practices but does make assumptions, for example on how your key is stored online. If you know what you are doing, you are encouraged to go your own route! If you are less familiar with the OpenPGP ecosystem, you might feel safer following this guide.
|
||||
|
||||
Enough theory! Let's go decentralized!
|
||||
If you'd like to know more about the theoretical concepts, have a look at the [About Keyoxide page](/about).
|
||||
|
||||
## Generating an OpenPGP key
|
||||
|
||||
|
|
|
@ -58,6 +58,12 @@ router.get('/', (req, res) => {
|
|||
res.render('index', {highlights: highlights});
|
||||
});
|
||||
|
||||
router.get('/about', (req, res) => {
|
||||
let rawContent = fs.readFileSync(`./content/about.md`, "utf8");
|
||||
const content = md.render(rawContent);
|
||||
res.render(`long-form-content`, { title: `About Keyoxide`, content: content });
|
||||
});
|
||||
|
||||
router.get('/getting-started', (req, res) => {
|
||||
let rawContent = fs.readFileSync(`./content/getting-started.md`, "utf8");
|
||||
const content = md.render(rawContent);
|
||||
|
|
|
@ -167,69 +167,6 @@ async function encryptMessage(opts) {
|
|||
elBtn.setAttribute("disabled", "true");
|
||||
};
|
||||
|
||||
// async function verifyProofs(opts) {
|
||||
// // Init
|
||||
// const elRes = document.body.querySelector("#result");
|
||||
// let keyData, feedback = "", message, encrypted;
|
||||
|
||||
// // Reset feedback
|
||||
// elRes.innerHTML = "";
|
||||
// elRes.classList.remove('green');
|
||||
// elRes.classList.remove('red');
|
||||
|
||||
// try {
|
||||
// // Get key data
|
||||
// keyData = await fetchKeys(opts);
|
||||
// } catch (e) {
|
||||
// console.error(e);
|
||||
// elRes.innerHTML = e;
|
||||
// elRes.classList.remove('green');
|
||||
// elRes.classList.add('red');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// let notations = [], notationsRaw = [];
|
||||
// for (var i = 0; i < keyData.publicKey.users.length; i++) {
|
||||
// notationsRaw = notationsRaw.concat(keyData.publicKey.users[i].selfCertifications[0].notations);
|
||||
// }
|
||||
// notationsRaw.forEach((item, i) => {
|
||||
// if (item[0] == "proof@metacode.biz") {
|
||||
// notations.push(item[1]);
|
||||
// }
|
||||
// });
|
||||
// notations = Array.from(new Set(notations)); // Deduplicate (ES6)
|
||||
|
||||
// // Display feedback
|
||||
// elRes.innerHTML = "Verifying proofs…";
|
||||
|
||||
// let notation, isVerified, verifications = [];
|
||||
// for (var i = 0; i < notations.length; i++) {
|
||||
// notation = notations[i];
|
||||
// verifications.push(await verifyProof(notation, keyData.fingerprint));
|
||||
// }
|
||||
|
||||
// // One-line sorting function (order verifications by type)
|
||||
// verifications = verifications.sort((a,b) => (a.type > b.type) ? 1 : ((b.type > a.type) ? -1 : 0));
|
||||
|
||||
// // Generate feedback
|
||||
// feedback += `<p>`;
|
||||
// for (var i = 0; i < verifications.length; i++) {
|
||||
// if (verifications[i].type == null) { continue; }
|
||||
// feedback += `${verifications[i].type}: `;
|
||||
// feedback += `<a class="proofDisplay" href="${verifications[i].url}" rel="me">${verifications[i].display}</a>`;
|
||||
// if (verifications[i].isVerified) {
|
||||
// feedback += `<a class="proofUrl proofUrl--verified" href="${verifications[i].proofUrl}">verified ✔</a>`;
|
||||
// } else {
|
||||
// feedback += `<a class="proofUrl" href="${verifications[i].proofUrl}">unverified</a>`;
|
||||
// }
|
||||
// feedback += `<br>`;
|
||||
// }
|
||||
// feedback += `</p>`;
|
||||
|
||||
// // Display feedback
|
||||
// elRes.innerHTML = feedback;
|
||||
// }
|
||||
|
||||
async function displayProfile(opts) {
|
||||
/// UTILITY FUNCTIONS
|
||||
// Sort claims by name and filter for errors
|
||||
|
@ -285,7 +222,7 @@ async function displayProfile(opts) {
|
|||
`;
|
||||
}
|
||||
// Generate a HTML string for each userId and associated claims
|
||||
const generateProfileUserIdHTML = (userId, claims, opts) => {
|
||||
const generateProfileUserIdHTML = (userId, claims, dialogIds, opts) => {
|
||||
// Init output
|
||||
let output = '';
|
||||
|
||||
|
@ -325,7 +262,7 @@ async function displayProfile(opts) {
|
|||
|
||||
// Generate output for each claim
|
||||
claims.forEach((claim, i) => {
|
||||
const claimData = claim.serviceproviderData
|
||||
const claimData = claim.serviceproviderData;
|
||||
if (!claimData.serviceprovider.name) {
|
||||
return;
|
||||
}
|
||||
|
@ -341,17 +278,71 @@ async function displayProfile(opts) {
|
|||
<span>${capitalizeLetteredServices(claimData.serviceprovider.name)}</span>
|
||||
<a href="${claimData.profile.uri}">View account</a>
|
||||
<a href="${claimData.proof.uri}">View proof</a>
|
||||
<a href="#">Details</a>
|
||||
<button onClick="document.querySelector('#dialog--${dialogIds[i]}').showModal();">Details</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="claim__verification claim__verification--${claim.isVerified ? "true" : "false"}">${claim.isVerified ? "✔" : "✕"}</div>
|
||||
</div>
|
||||
`;
|
||||
// if (claim.isVerified && claimData.profile.qr) {
|
||||
// feedback += `<a class="proofQR green" href="/util/qr/${encodeURIComponent(claimData.profile.qr)}" target="_blank" title="QR Code">${icon_qr}</a>`;
|
||||
// }
|
||||
})
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
// Generate a HTML string for each userId and associated claims
|
||||
const generateClaimDialogHTML = (claims, dialogIds) => {
|
||||
// Generate dialog for each claim
|
||||
let output = '';
|
||||
|
||||
claims.forEach((claim, i) => {
|
||||
const claimData = claim.serviceproviderData;
|
||||
output += `
|
||||
<dialog id="dialog--${dialogIds[i]}">
|
||||
<div>
|
||||
<p>
|
||||
The claim's service provider is <strong>${claimData.serviceprovider.name}</strong>.
|
||||
</p>
|
||||
<p>
|
||||
The claim points to: <a href="${claimData.profile.uri}">${claimData.profile.uri}</a>.
|
||||
</p>
|
||||
<p>
|
||||
The supposed proof is located at: <a href="${claimData.proof.uri}">${claimData.proof.uri}</a>.
|
||||
</p>
|
||||
`;
|
||||
if (claimData.proof.fetch) {
|
||||
output += `
|
||||
<p class="warning">
|
||||
Due to technical restraints, the machine had to fetch the proof from: <a href="${claimData.proof.fetch}">${claimData.proof.fetch}</a>. This link may not work for you.
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
if (claimData.customRequestHandler) {
|
||||
output += `
|
||||
<p class="warning">
|
||||
This claim's verification process was more complex than most verifications. The link(s) above may offer only limited insight into the verification process.
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
output += `
|
||||
<p>
|
||||
The claim <strong>${claim.isVerified ? 'has been' : 'could not be'} verified</strong> by this proof.
|
||||
</p>
|
||||
`;
|
||||
if (claim.errors.length > 0) {
|
||||
output += `
|
||||
<p class="warning">
|
||||
The verification encountered errors: ${JSON.stringify(claim.errors)}.
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
output += `
|
||||
<form method="dialog">
|
||||
<input type="submit" value="Close" />
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
`;
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
@ -499,43 +490,26 @@ async function displayProfile(opts) {
|
|||
return;
|
||||
}
|
||||
|
||||
let feedbackDialog = "";
|
||||
let dialogIds = null;
|
||||
feedback = "";
|
||||
|
||||
if (opts.mode === 'sig') {
|
||||
const claims = sortClaims(verifications);
|
||||
feedback += generateProfileUserIdHTML(userData, claims, {isPrimary: false});
|
||||
|
||||
// feedback += `<div class="profileDataItem profileDataItem--separator profileDataItem--noLabel">`;
|
||||
// feedback += `<div class="profileDataItem__label"></div>`;
|
||||
// feedback += `<div class="profileDataItem__value">proofs</div>`;
|
||||
// feedback += `</div>`;
|
||||
dialogIds = new Uint32Array(claims.length);
|
||||
window.crypto.getRandomValues(dialogIds);
|
||||
|
||||
// verifications = verifications.filter((a) => (a && a.errors.length == 0 && a.serviceproviderData))
|
||||
// verifications = verifications.sort((a,b) => (a.serviceproviderData.serviceprovider.name > b.serviceproviderData.serviceprovider.name) ? 1 : ((b.serviceproviderData.serviceprovider.name > a.serviceproviderData.serviceprovider.name) ? -1 : 0));
|
||||
|
||||
// verifications.forEach((claim, i) => {
|
||||
// const claimData = claim.serviceproviderData;
|
||||
// if (!claimData.serviceprovider.name) {
|
||||
// return;
|
||||
// }
|
||||
// feedback += `<div class="profileDataItem">`;
|
||||
// feedback += `<div class="profileDataItem__label">${capitalizeLetteredServices(claimData.serviceprovider.name)}</div>`;
|
||||
// feedback += `<div class="profileDataItem__value">`;
|
||||
// feedback += `<a class="proofDisplay" href="${claimData.profile.uri}" rel="me">${claimData.profile.display}</a>`;
|
||||
// if (claim.isVerified) {
|
||||
// feedback += `<a class="proofUrl proofUrl--verified" href="${claimData.proof.uri}">verified ✔</a>`;
|
||||
// } else {
|
||||
// feedback += `<a class="proofUrl" href="${claimData.proof.uri}">unverified</a>`;
|
||||
// }
|
||||
// if (claim.isVerified && claimData.profile.qr) {
|
||||
// feedback += `<a class="proofQR green" href="/util/qr/${encodeURIComponent(claimData.profile.qr)}" target="_blank" title="QR Code">${icon_qr}</a>`;
|
||||
// }
|
||||
// feedback += `</div>`;
|
||||
// feedback += `</div>`;
|
||||
// });
|
||||
feedback += generateProfileUserIdHTML(userData, claims, dialogIds, {isPrimary: false});
|
||||
feedbackDialog += generateClaimDialogHTML(claims, dialogIds);
|
||||
} else {
|
||||
const primaryClaims = getPrimaryClaims(userData, keyData.users, verifications);
|
||||
feedback += generateProfileUserIdHTML(userData, primaryClaims, {isPrimary: true});
|
||||
|
||||
dialogIds = new Uint32Array(primaryClaims.length);
|
||||
window.crypto.getRandomValues(dialogIds);
|
||||
|
||||
feedback += generateProfileUserIdHTML(userData, primaryClaims, dialogIds, {isPrimary: true});
|
||||
feedbackDialog += generateClaimDialogHTML(primaryClaims, dialogIds);
|
||||
|
||||
keyData.users.forEach((user, i) => {
|
||||
if (!user.userId || userData.email && user.userId && user.userId.email === userData.email) {
|
||||
|
@ -548,388 +522,70 @@ async function displayProfile(opts) {
|
|||
isIdenticaltoPrimary: primaryClaims && primaryClaims.toString() === claims.toString()
|
||||
}
|
||||
|
||||
feedback += generateProfileUserIdHTML(user.userId, claims, opts);
|
||||
dialogIds = new Uint32Array(claims.length);
|
||||
window.crypto.getRandomValues(dialogIds);
|
||||
|
||||
feedback += generateProfileUserIdHTML(user.userId, claims, dialogIds, opts);
|
||||
feedbackDialog += generateClaimDialogHTML(claims, dialogIds);
|
||||
})
|
||||
}
|
||||
|
||||
feedback += `
|
||||
<dialog id="dialog--whatisthis">
|
||||
<div>
|
||||
<p>
|
||||
Keyoxide allows anyone to prove that they have accounts on certain websites and, by doing so, establish an online identity. To guarantee the validity of these identity verifications and prevent impersonation, Keyoxide uses secure and well-known encryption paradigms. All claims are verified using bidirectional linking.
|
||||
</p>
|
||||
<p>
|
||||
You are currently viewing someone's Keyoxide profile, including the verification results of their identity claims.
|
||||
</p>
|
||||
<p>
|
||||
More detailed information is available on the <a href="/">What is Keyoxide</a> page.
|
||||
</p>
|
||||
<form method="dialog">
|
||||
<input type="submit" value="Close" />
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog id="dialog--localverification">
|
||||
<div>
|
||||
<p>
|
||||
The profile page you are currently viewing depends at least partially on a server you may not know, operated by someone you may not have reason to trust.
|
||||
</p>
|
||||
<p>
|
||||
You can choose to perform the identity verification again, but this time completely locally, removing the need to trust unknown servers.
|
||||
</p>
|
||||
<p>
|
||||
On linux/mac/windows, run:
|
||||
</p>
|
||||
<pre><code>keyoxide verify ${opts.mode}:${opts.input}</code></pre>
|
||||
<form method="dialog">
|
||||
<input type="submit" value="Close" />
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
<p class="subtle-links">
|
||||
<a href="#">What is this?</a>
|
||||
<a href="#">Perform local verification</a>
|
||||
<button onclick="document.querySelector('#dialog--whatisthis').showModal()">What is this?</button>
|
||||
<button onclick="document.querySelector('#dialog--localverification').showModal()">Perform local verification</button>
|
||||
</p>`;
|
||||
|
||||
// Display feedback
|
||||
document.body.querySelector('#profileProofs').innerHTML = feedback;
|
||||
if (feedbackDialog) {
|
||||
document.body.querySelector('#profileDialogs').innerHTML = feedbackDialog;
|
||||
}
|
||||
|
||||
// async function verifyProof(url, fingerprint) {
|
||||
// // Init
|
||||
// let reVerify, match, output = {url: url, type: null, proofUrl: url, proofUrlFetch: null, isVerified: false, display: null, qr: null};
|
||||
|
||||
// try {
|
||||
// // DNS
|
||||
// if (/^dns:/.test(url)) {
|
||||
// output.type = "domain";
|
||||
// output.display = url.replace(/dns:/, '').replace(/\?type=TXT/, '');
|
||||
// output.proofUrl = `https://dns.shivering-isles.com/dns-query?name=${output.display}&type=TXT`;
|
||||
// output.proofUrlFetch = output.proofUrl;
|
||||
// output.url = `https://${output.display}`;
|
||||
|
||||
// try {
|
||||
// response = await fetch(output.proofUrlFetch, {
|
||||
// headers: {
|
||||
// Accept: 'application/json'
|
||||
// },
|
||||
// credentials: 'omit'
|
||||
// });
|
||||
// if (!response.ok) {
|
||||
// throw new Error('Response failed: ' + response.status);
|
||||
// }
|
||||
// json = await response.json();
|
||||
// reVerify = new RegExp(`openpgp4fpr:${fingerprint}`, 'i');
|
||||
// json.Answer.forEach((item, i) => {
|
||||
// if (reVerify.test(item.data)) {
|
||||
// output.isVerified = true;
|
||||
// }
|
||||
// });
|
||||
// } catch (e) {
|
||||
// } finally {
|
||||
// return output;
|
||||
// }
|
||||
// }
|
||||
// // XMPP
|
||||
// if (/^xmpp:/.test(url)) {
|
||||
// output.type = "xmpp";
|
||||
// match = url.match(/xmpp:([a-zA-Z0-9\.\-\_]*)@([a-zA-Z0-9\.\-\_]*)(?:\?(.*))?/);
|
||||
// output.display = `${match[1]}@${match[2]}`;
|
||||
// output.proofUrl = `https://PLACEHOLDER__XMPP_VCARD_SERVER_DOMAIN/api/vcard/${output.display}/DESC`;
|
||||
// output.qr = url;
|
||||
|
||||
// try {
|
||||
// response = await fetchWithTimeout(output.proofUrl);
|
||||
// if (!response.ok) {
|
||||
// throw new Error('Response failed: ' + response.status);
|
||||
// }
|
||||
// json = await response.json();
|
||||
// reVerify = new RegExp(`[Verifying my OpenPGP key: openpgp4fpr:${fingerprint}]`, 'i');
|
||||
// if (reVerify.test(json)) {
|
||||
// output.isVerified = true;
|
||||
// }
|
||||
// } catch (e) {
|
||||
// } finally {
|
||||
// return output;
|
||||
// }
|
||||
// }
|
||||
// // Twitter
|
||||
// if (/^https:\/\/twitter.com/.test(url)) {
|
||||
// output.type = "twitter";
|
||||
// match = url.match(/https:\/\/twitter\.com\/(.*)\/status\/([0-9]*)(?:\?.*)?/);
|
||||
// output.display = `@${match[1]}`;
|
||||
// output.url = `https://twitter.com/${match[1]}`;
|
||||
// output.proofUrlFetch = `/server/verify/twitter
|
||||
// ?tweetId=${encodeURIComponent(match[2])}
|
||||
// &account=${encodeURIComponent(match[1])}
|
||||
// &fingerprint=${fingerprint}`;
|
||||
// try {
|
||||
// response = await fetch(output.proofUrlFetch);
|
||||
// if (!response.ok) {
|
||||
// throw new Error('Response failed: ' + response.status);
|
||||
// }
|
||||
// json = await response.json();
|
||||
// output.isVerified = json.isVerified;
|
||||
// } catch (e) {
|
||||
// } finally {
|
||||
// return output;
|
||||
// }
|
||||
// }
|
||||
// // HN
|
||||
// if (/^https:\/\/news.ycombinator.com/.test(url)) {
|
||||
// output.type = "hackernews";
|
||||
// match = url.match(/https:\/\/news.ycombinator.com\/user\?id=(.*)/);
|
||||
// output.display = match[1];
|
||||
// output.proofUrl = `https://hacker-news.firebaseio.com/v0/user/${match[1]}.json`;
|
||||
// output.proofUrlFetch = output.proofUrl;
|
||||
// try {
|
||||
// response = await fetch(output.proofUrlFetch, {
|
||||
// headers: {
|
||||
// Accept: 'application/json'
|
||||
// },
|
||||
// credentials: 'omit'
|
||||
// });
|
||||
// if (!response.ok) {
|
||||
// throw new Error('Response failed: ' + response.status);
|
||||
// }
|
||||
// json = await response.json();
|
||||
// reVerify = new RegExp(`openpgp4fpr:${fingerprint}`, 'i');
|
||||
// if (reVerify.test(json.about)) {
|
||||
// output.isVerified = true;
|
||||
// }
|
||||
// } catch (e) {
|
||||
// }
|
||||
|
||||
// if (!output.isVerified) {
|
||||
// output.proofUrlFetch = `/server/verify/proxy
|
||||
// ?url=${encodeURIComponent(output.proofUrl)}
|
||||
// &fingerprint=${fingerprint}
|
||||
// &checkRelation=contains
|
||||
// &checkPath=about
|
||||
// &checkClaimFormat=message`;
|
||||
// try {
|
||||
// response = await fetch(output.proofUrlFetch);
|
||||
// if (!response.ok) {
|
||||
// throw new Error('Response failed: ' + response.status);
|
||||
// }
|
||||
// json = await response.json();
|
||||
// output.isVerified = json.verified;
|
||||
// } catch (e) {
|
||||
// }
|
||||
// }
|
||||
// return output;
|
||||
// }
|
||||
// // dev.to
|
||||
// if (/^https:\/\/dev\.to\//.test(url)) {
|
||||
// output.type = "dev.to";
|
||||
// match = url.match(/https:\/\/dev\.to\/(.*)\/(.*)/);
|
||||
// output.display = match[1];
|
||||
// output.url = `https://dev.to/${match[1]}`;
|
||||
// output.proofUrlFetch = `https://dev.to/api/articles/${match[1]}/${match[2]}`;
|
||||
// try {
|
||||
// response = await fetch(output.proofUrlFetch);
|
||||
// if (!response.ok) {
|
||||
// throw new Error('Response failed: ' + response.status);
|
||||
// }
|
||||
// json = await response.json();
|
||||
// reVerify = new RegExp(`[Verifying my OpenPGP key: openpgp4fpr:${fingerprint}]`, 'i');
|
||||
// if (reVerify.test(json.body_markdown)) {
|
||||
// output.isVerified = true;
|
||||
// }
|
||||
// } catch (e) {
|
||||
// } finally {
|
||||
// return output;
|
||||
// }
|
||||
// }
|
||||
// // Reddit
|
||||
// if (/^https:\/\/(?:www\.)?reddit\.com\/user/.test(url)) {
|
||||
// output.type = "reddit";
|
||||
// match = url.match(/https:\/\/(?:www\.)?reddit\.com\/user\/(.*)\/comments\/(.*)\/([^/]*)/);
|
||||
// output.display = match[1];
|
||||
// output.url = `https://www.reddit.com/user/${match[1]}`;
|
||||
// output.proofUrl = `https://www.reddit.com/user/${match[1]}/comments/${match[2]}.json`;
|
||||
// output.proofUrlFetch = `/server/verify/proxy
|
||||
// ?url=${encodeURIComponent(output.proofUrl)}
|
||||
// &fingerprint=${fingerprint}
|
||||
// &checkRelation=contains
|
||||
// &checkPath=data,children,data,selftext
|
||||
// &checkClaimFormat=message`;
|
||||
// try {
|
||||
// response = await fetch(output.proofUrlFetch);
|
||||
// if (!response.ok) {
|
||||
// throw new Error('Response failed: ' + response.status);
|
||||
// }
|
||||
// json = await response.json();
|
||||
// output.isVerified = json.isVerified;
|
||||
// } catch (e) {
|
||||
// } finally {
|
||||
// return output;
|
||||
// }
|
||||
// }
|
||||
// // Gitea
|
||||
// if (/\/gitea_proof$/.test(url)) {
|
||||
// output.type = "gitea";
|
||||
// match = url.match(/https:\/\/(.*)\/(.*)\/gitea_proof/);
|
||||
// output.display = `${match[2]}@${match[1]}`;
|
||||
// output.url = `https://${match[1]}/${match[2]}`;
|
||||
// output.proofUrl = `https://${match[1]}/api/v1/repos/${match[2]}/gitea_proof`;
|
||||
// output.proofUrlFetch = `/server/verify/proxy
|
||||
// ?url=${encodeURIComponent(output.proofUrl)}
|
||||
// &fingerprint=${fingerprint}
|
||||
// &checkRelation=eq
|
||||
// &checkPath=description
|
||||
// &checkClaimFormat=message`;
|
||||
// output.proofUrl = url; // Actually set the proof URL to something user-friendly
|
||||
// try {
|
||||
// response = await fetch(output.proofUrlFetch);
|
||||
// if (!response.ok) {
|
||||
// throw new Error('Response failed: ' + response.status);
|
||||
// }
|
||||
// json = await response.json();
|
||||
// output.isVerified = json.isVerified;
|
||||
// } catch (e) {
|
||||
// } finally {
|
||||
// return output;
|
||||
// }
|
||||
// }
|
||||
// // Github
|
||||
// if (/^https:\/\/gist.github.com/.test(url)) {
|
||||
// output.type = "github";
|
||||
// match = url.match(/https:\/\/gist.github.com\/(.*)\/(.*)/);
|
||||
// output.display = match[1];
|
||||
// output.url = `https://github.com/${match[1]}`;
|
||||
// output.proofUrlFetch = `https://api.github.com/gists/${match[2]}`;
|
||||
// try {
|
||||
// response = await fetch(output.proofUrlFetch, {
|
||||
// headers: {
|
||||
// Accept: 'application/json'
|
||||
// },
|
||||
// credentials: 'omit'
|
||||
// });
|
||||
// if (!response.ok) {
|
||||
// throw new Error('Response failed: ' + response.status);
|
||||
// }
|
||||
// json = await response.json();
|
||||
// reVerify = new RegExp(`[Verifying my OpenPGP key: openpgp4fpr:${fingerprint}]`, 'i');
|
||||
// if (reVerify.test(json.files["openpgp.md"].content)) {
|
||||
// output.isVerified = true;
|
||||
// }
|
||||
// } catch (e) {
|
||||
// } finally {
|
||||
// return output;
|
||||
// }
|
||||
// }
|
||||
// // GitLab
|
||||
// if (/\/gitlab_proof$/.test(url)) {
|
||||
// output.type = "gitlab";
|
||||
// match = url.match(/https:\/\/(.*)\/(.*)\/gitlab_proof/);
|
||||
// output.display = `${match[2]}@${match[1]}`;
|
||||
// output.url = `https://${match[1]}/${match[2]}`;
|
||||
// output.proofUrlFetch = `https://${match[1]}/api/v4/users?username=${match[2]}`;
|
||||
// try {
|
||||
// const opts = {
|
||||
// headers: {
|
||||
// Accept: 'application/json'
|
||||
// },
|
||||
// credentials: 'omit'
|
||||
// };
|
||||
// // Get user
|
||||
// response = await fetch(output.proofUrlFetch, opts);
|
||||
// if (!response.ok) {
|
||||
// throw new Error('Response failed: ' + response.status);
|
||||
// }
|
||||
// json = await response.json();
|
||||
// const user = json.find(user => user.username === match[2]);
|
||||
// if (!user) {
|
||||
// throw new Error('No user with username ' + match[2]);
|
||||
// }
|
||||
// // Get project
|
||||
// output.proofUrlFetch = `https://${match[1]}/api/v4/users/${user.id}/projects`;
|
||||
// response = await fetch(output.proofUrlFetch, opts);
|
||||
// if (!response.ok) {
|
||||
// throw new Error('Response failed: ' + response.status);
|
||||
// }
|
||||
// json = await response.json();
|
||||
// const project = json.find(proj => proj.path === 'gitlab_proof');
|
||||
// if (!project) {
|
||||
// throw new Error('No project at ' + url);
|
||||
// }
|
||||
// reVerify = new RegExp(`[Verifying my OpenPGP key: openpgp4fpr:${fingerprint}]`, 'i');
|
||||
// if (reVerify.test(project.description)) {
|
||||
// output.isVerified = true;
|
||||
// }
|
||||
// } catch (e) {
|
||||
// } finally {
|
||||
// return output;
|
||||
// }
|
||||
// }
|
||||
// // Lobsters
|
||||
// if (/^https:\/\/lobste.rs/.test(url)) {
|
||||
// output.type = "lobsters";
|
||||
// match = url.match(/https:\/\/lobste.rs\/u\/(.*)/);
|
||||
// output.display = match[1];
|
||||
// output.proofUrl = `https://lobste.rs/u/${match[1]}.json`;
|
||||
// output.proofUrlFetch = `/server/verify/proxy
|
||||
// ?url=${encodeURIComponent(output.proofUrl)}
|
||||
// &fingerprint=${fingerprint}
|
||||
// &checkRelation=contains
|
||||
// &checkPath=about
|
||||
// &checkClaimFormat=message`;
|
||||
// try {
|
||||
// response = await fetch(output.proofUrlFetch);
|
||||
// if (!response.ok) {
|
||||
// throw new Error('Response failed: ' + response.status);
|
||||
// }
|
||||
// json = await response.json();
|
||||
// output.isVerified = json.isVerified;
|
||||
// } catch (e) {
|
||||
// } finally {
|
||||
// return output;
|
||||
// }
|
||||
// }
|
||||
// // Catchall
|
||||
// // Fediverse
|
||||
// try {
|
||||
// response = await fetch(url, {
|
||||
// headers: {
|
||||
// Accept: 'application/json'
|
||||
// },
|
||||
// credentials: 'omit'
|
||||
// });
|
||||
// if (!response.ok) {
|
||||
// throw new Error('Response failed: ' + response.status);
|
||||
// }
|
||||
// json = await response.json();
|
||||
// if ('attachment' in json) {
|
||||
// match = url.match(/https:\/\/(.*)\/@(.*)/);
|
||||
// json.attachment.forEach((item, i) => {
|
||||
// reVerify = new RegExp(fingerprint, 'i');
|
||||
// if (reVerify.test(item.value)) {
|
||||
// output.type = "fediverse";
|
||||
// output.display = `@${json.preferredUsername}@${[match[1]]}`;
|
||||
// output.proofUrlFetch = json.url;
|
||||
// output.isVerified = true;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// if (!output.type && 'summary' in json) {
|
||||
// match = url.match(/https:\/\/(.*)\/users\/(.*)/);
|
||||
// reVerify = new RegExp(`[Verifying my OpenPGP key: openpgp4fpr:${fingerprint}]`, 'i');
|
||||
// if (reVerify.test(json.summary)) {
|
||||
// output.type = "fediverse";
|
||||
// output.display = `@${json.preferredUsername}@${[match[1]]}`;
|
||||
// output.proofUrlFetch = json.url;
|
||||
// output.isVerified = true;
|
||||
// }
|
||||
// }
|
||||
// if (output.type) {
|
||||
// return output;
|
||||
// }
|
||||
// } catch (e) {
|
||||
// console.warn(e);
|
||||
// }
|
||||
// // Discourse
|
||||
// try {
|
||||
// match = url.match(/https:\/\/(.*)\/u\/(.*)/);
|
||||
// output.proofUrl = `${url}.json`;
|
||||
// output.proofUrlFetch = `/server/verify/proxy
|
||||
// ?url=${encodeURIComponent(output.proofUrl)}
|
||||
// &fingerprint=${fingerprint}
|
||||
// &checkRelation=contains
|
||||
// &checkPath=user,bio_raw
|
||||
// &checkClaimFormat=message`;
|
||||
// try {
|
||||
// response = await fetch(output.proofUrlFetch);
|
||||
// if (!response.ok) {
|
||||
// throw new Error('Response failed: ' + response.status);
|
||||
// }
|
||||
// json = await response.json();
|
||||
// if (json.isVerified) {
|
||||
// output.type = "discourse";
|
||||
// output.display = `${match[2]}@${match[1]}`;
|
||||
// output.isVerified = json.isVerified;
|
||||
// return output;
|
||||
// }
|
||||
// } catch (e) {
|
||||
// console.warn(e);
|
||||
// }
|
||||
// } catch (e) {
|
||||
// console.warn(e);
|
||||
// }
|
||||
// } catch (e) {
|
||||
// console.warn(e);
|
||||
// }
|
||||
|
||||
// // Return output without confirmed proof
|
||||
// return output;
|
||||
// }
|
||||
// Register modals
|
||||
document.querySelectorAll('dialog').forEach(function(d) {
|
||||
dialogPolyfill.registerDialog(d);
|
||||
d.addEventListener('click', function(ev) {
|
||||
if (ev && ev.target != d) {
|
||||
return;
|
||||
}
|
||||
d.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchKeys(opts) {
|
||||
// Init
|
||||
|
|
|
@ -386,7 +386,7 @@ h3 {
|
|||
margin: 1.6rem 0;
|
||||
font-size: 1.3em;
|
||||
line-height: 1.6rem;
|
||||
color: var(--grey-600);
|
||||
color: var(--grey-700);
|
||||
font-weight: normal;
|
||||
/* text-align: center; */
|
||||
cursor: default;
|
||||
|
@ -399,12 +399,12 @@ h3 small {
|
|||
border-radius: 4px;
|
||||
}
|
||||
h4 {
|
||||
margin: 3.2rem 0 1.6rem 0;
|
||||
font-size: 1.3em;
|
||||
margin: 1.6rem 0;
|
||||
font-size: 1em;
|
||||
line-height: 1.6rem;
|
||||
color: var(--grey-800);
|
||||
color: var(--purple-700);
|
||||
font-weight: normal;
|
||||
color: var(--grey-600);
|
||||
/* color: var(--purple-700); */
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
}
|
||||
h4 small {
|
||||
|
@ -417,6 +417,11 @@ h4 small {
|
|||
p {
|
||||
margin: 1.6rem 0;
|
||||
}
|
||||
p.warning {
|
||||
padding: 8px;
|
||||
background-color: #fffadc;
|
||||
border: solid 1px #ffeea8;
|
||||
}
|
||||
a {
|
||||
color: var(--blue-700);
|
||||
}
|
||||
|
@ -437,7 +442,7 @@ main h1:first-of-type {
|
|||
/* font-size: 1rem; */
|
||||
/* font-weight: bold; */
|
||||
text-align: left;
|
||||
color: var(--grey-700);
|
||||
/* color: var(--grey-700); */
|
||||
}
|
||||
footer h1 {
|
||||
margin-bottom: 0.8rem;
|
||||
|
@ -451,7 +456,7 @@ footer a {
|
|||
}
|
||||
|
||||
code {
|
||||
padding: 8px;
|
||||
padding: 2px 4px;
|
||||
background-color: var(--purple-100);
|
||||
border: 1px solid var(--purple-500);
|
||||
}
|
||||
|
@ -459,8 +464,9 @@ pre {
|
|||
padding: 8px 12px;
|
||||
background-color: var(--purple-100);
|
||||
border: 1px solid var(--purple-500);
|
||||
overflow-x: scroll;
|
||||
overflow-x: auto;
|
||||
line-height: 1.2rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
pre code {
|
||||
padding: 0;
|
||||
|
@ -491,7 +497,6 @@ form input[type="submit"] {
|
|||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.4rem 0.8rem;
|
||||
/* color: #fff; */
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
background-color: #fff;
|
||||
|
@ -503,3 +508,26 @@ form input[type="submit"]:hover {
|
|||
background-color: var(--blue-700);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
dialog {
|
||||
width: 100% !important;
|
||||
max-width: 800px !important;
|
||||
padding: 0 !important;
|
||||
word-wrap: anywhere;
|
||||
}
|
||||
dialog > div {
|
||||
padding: 1em;
|
||||
}
|
||||
dialog form[method="Dialog"] {
|
||||
margin: 1em 0 0;
|
||||
}
|
||||
dialog form[method="Dialog"] input {
|
||||
width: auto;
|
||||
}
|
||||
dialog p {
|
||||
font-size: 1rem !important;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
dialog p:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
|
|
@ -60,13 +60,13 @@ block content
|
|||
.card
|
||||
h3 Getting started
|
||||
p
|
||||
a(href='/') What is Keyoxide?
|
||||
a(href='/about') About Keyoxide
|
||||
br
|
||||
a(href='/') Getting started
|
||||
a(href='/getting-started') Getting started
|
||||
br
|
||||
a(href='/') Guides
|
||||
a(href='/guides') Guides
|
||||
br
|
||||
a(href='/') FAQ
|
||||
a(href='/faq') FAQ
|
||||
|
||||
.card
|
||||
h3 Utilities
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
header
|
||||
nav
|
||||
.spacer
|
||||
a.text(href='/') About
|
||||
a.text(href='/about') About
|
||||
a.text(href='/getting-started') Getting started
|
||||
a.logo(href='/')
|
||||
img(src='/static/img/logo_circle.png' alt='Keyoxide')
|
||||
|
|
|
@ -3,8 +3,12 @@ extends templates/base.pug
|
|||
block js
|
||||
script(type='application/javascript' src='/static/openpgp.min.js' charset='utf-8')
|
||||
script(type='application/javascript' src='/static/doip.js' charset='utf-8')
|
||||
script(type='application/javascript' src='/static/dialog-polyfill.js' charset='utf-8')
|
||||
script(type='application/javascript' src='/static/scripts.js' charset='utf-8')
|
||||
|
||||
block css
|
||||
link(rel='stylesheet' href='/static/dialog-polyfill.css')
|
||||
|
||||
block content
|
||||
section.profile.narrow
|
||||
noscript
|
||||
|
@ -13,6 +17,8 @@ block content
|
|||
span#profileServer(style='display: none;') #{server}
|
||||
span#profileMode(style='display: none;') #{mode}
|
||||
|
||||
#profileDialogs
|
||||
|
||||
if (mode == 'sig')
|
||||
#profileSigInput.card
|
||||
form#form-generate-signature-profile(method='post')
|
||||
|
|
|
@ -6,6 +6,7 @@ head
|
|||
link(rel='shortcut icon' href='/favicon.svg')
|
||||
title= (title ? title : "Keyoxide")
|
||||
link(rel='stylesheet' href='/static/styles.css')
|
||||
block css
|
||||
|
||||
include ../partials/header.pug
|
||||
|
||||
|
|
Loading…
Reference in a new issue