-
Notifications
You must be signed in to change notification settings - Fork 30
Securely Handle Personal Access Tokens #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
micchickenburger
wants to merge
15
commits into
oppia:develop
Choose a base branch
from
micchickenburger:develop
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
66704c8
create initial project dashboard page with graphql client and initial…
micchickenburger 72f5ba3
remove graphql library and use from cdn instead
micchickenburger 02745d8
move PAT to variable and do not commit it
micchickenburger 3e014c1
Merge remote-tracking branch 'upstream/develop' into develop
micchickenburger 9e4d453
create an https server for development to test web crypto functions
micchickenburger 441a638
add indexeddb abstraction with a crypto keystore and an app datastore
micchickenburger c12cfb7
move graphql functions into separate module
micchickenburger 5cc18d2
add a module for securely handling and storing github personal access…
micchickenburger c288641
moved startup functions into their own file
micchickenburger 6ba03d6
add basic styles. add prompt for retrieving user personal access toke…
micchickenburger 027e0f0
add windows considerations for https
micchickenburger 56eb357
Update README.md
micchickenburger ac1a7e8
minor style fix
micchickenburger 394b44b
documentation and code style conformity changes
micchickenburger cd0af2e
Merge branch 'develop' of https://github.com/micchickenburger/oppia.g…
micchickenburger File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
development.pem |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,9 +4,28 @@ | |
<meta charset="utf-8" /> | ||
<title>Project Dashboard</title> | ||
<script src="https://cdn.jsdelivr.net/gh/f/[email protected]/graphql.min.js" integrity="sha256-Zo7tOMz6K/oPU5ch0Mtpp0LRQCUVX0HvyBd3gj3rc7g=" crossorigin="anonymous"></script> | ||
<link rel="stylesheet" href="../static/css/stylesheet.css" /> | ||
<link rel="stylesheet" href="styles.css" /> | ||
</head> | ||
<body> | ||
<h1>Project Dashboard</h1> | ||
<script src="scripts.js"></script> | ||
<section id="authenticate"> | ||
<h1>Authenticate with Github</h1> | ||
<p>In order to use the Oppia Project Dashboard, you need to provide a personal access token.</p> | ||
<form autocomplete="off"> | ||
<input type="password" placeholder="Personal Access Token" pattern="[a-f0-9]{40}" minlength="40" maxlength="40" autofocus required autocomplete="off" /> | ||
<button type="submit">Login</button> | ||
</form> | ||
<ol> | ||
<li>Navigate to <a href="https://github.com/settings/tokens" target="_blank">https://github.com/settings/tokens</a> and login if necessary.</li> | ||
<li>Click "Generate new token"</li> | ||
<li>You can use "Oppia Project Dashboard" as the note.</li> | ||
<li>Select only the "public_repo" checkbox. Then click Generate Token.</li> | ||
<li>Paste the token in the field above and click Login.</li> | ||
</ol> | ||
</section> | ||
<main> | ||
<h1>Project Dashboard</h1> | ||
</main> | ||
<script src="scripts/startup.js" type="module"></script> | ||
</body> | ||
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,97 @@ | ||||||
/** | ||||||
* An abstraction layer for IndexedDB. | ||||||
* | ||||||
* @module | ||||||
*/ | ||||||
|
||||||
const name = 'OppiaProjectDashboard'; | ||||||
const version = 1; // long long; incrementing fires onupgradeneeded event | ||||||
let db = null; | ||||||
|
||||||
/** | ||||||
* Open IndexedDB. | ||||||
* | ||||||
* @returns {boolean} | ||||||
*/ | ||||||
const open = async () => { | ||||||
let handle = new Promise((resolve, reject) => { | ||||||
let req = indexedDB.open(name, version); | ||||||
req.onsuccess = event => resolve(event.target.result); | ||||||
req.onupgradeneeded = (event) => { | ||||||
let db = event.target.result; | ||||||
|
||||||
// Create keystore | ||||||
if (!db.objectStoreNames.contains('keystore')) { | ||||||
let keystore = db.createObjectStore('keystore', { autoIncrement: true, keyPath: 'name' }); | ||||||
keystore.createIndex('name', 'name', { unique: true }); | ||||||
} | ||||||
|
||||||
// Create datastore | ||||||
if (!db.objectStoreNames.contains('datastore')) { | ||||||
let datastore = db.createObjectStore('datastore', { autoIncrement: true }); | ||||||
// TODO: Create indexes | ||||||
} | ||||||
|
||||||
resolve(db); | ||||||
}; | ||||||
req.onerror = reject; | ||||||
req.onblocked = reject; | ||||||
}); | ||||||
|
||||||
db = await handle; | ||||||
return true; | ||||||
}; | ||||||
|
||||||
/** | ||||||
* Close IndexedDB. | ||||||
* | ||||||
* @returns {boolean} | ||||||
*/ | ||||||
const close = async () => { | ||||||
if (db) { | ||||||
await db.close(); | ||||||
db = null; | ||||||
} | ||||||
return true | ||||||
}; | ||||||
|
||||||
/** | ||||||
* Write a key into the keystore. | ||||||
* | ||||||
* @param {string} name - Name of the key to store (must be unique). | ||||||
* @param {CryptoKey} key | ||||||
* @returns {boolean} | ||||||
*/ | ||||||
const setKey = async (name, key) => { | ||||||
if (!db) await open(); | ||||||
|
||||||
let transaction = db.transaction(['keystore'], 'readwrite'); | ||||||
let objectStore = transaction.objectStore('keystore'); | ||||||
await objectStore.add({ name, key }); | ||||||
return true; | ||||||
}; | ||||||
|
||||||
/** | ||||||
* Get a key from the keystore. | ||||||
* | ||||||
* @param {string} name | ||||||
* @returns {CryptoKey} | ||||||
*/ | ||||||
const getKey = async (name) => { | ||||||
if (!db) await open(); | ||||||
|
||||||
let key = new Promise((resolve, reject) => { | ||||||
let transaction = db.transaction(['keystore'], 'readonly'); | ||||||
let objectStore = transaction.objectStore('keystore'); | ||||||
let op = objectStore.get(name); | ||||||
op.onsuccess = (event) => resolve(event.target.result); | ||||||
op.onerror = reject; | ||||||
}); | ||||||
|
||||||
await key; | ||||||
return key; | ||||||
}; | ||||||
|
||||||
// TODO(55): Write getters and setters for datastore | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
export { open, close, setKey, getKey }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
/** | ||
* Methods for securely processing and storing GitHub | ||
* Personal Access Tokens (PATs). | ||
* | ||
* @method | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this be |
||
*/ | ||
import { getKey, setKey } from './db.js'; | ||
|
||
/** | ||
* Get Personal Access Token from Local Storage. | ||
* | ||
* @returns {string} | ||
*/ | ||
const getPat = async () => { | ||
let ciphertext = localStorage.getItem('PAT'); | ||
if (!ciphertext) throw new Error('No PAT currently stored in local storage'); | ||
|
||
let initVector = localStorage.getItem('initVector'); | ||
if (!initVector) throw new Error('No IV currently stored in local storage'); | ||
|
||
// Ciphertext and IV were stored as strings in local storage | ||
// Split by comma to convert them back into Uint8Arrays | ||
ciphertext = Uint8Array.from(ciphertext.split(',')); | ||
initVector = Uint8Array.from(initVector.split(',')); | ||
|
||
// Get the symmetric key and decrypt the ciphertext | ||
let { key } = await getKey('pat_key'); | ||
let pat = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: initVector }, key, ciphertext); | ||
|
||
// Convert plaintext data into a string | ||
return (new TextDecoder()).decode(pat); | ||
}; | ||
|
||
/** | ||
* Set Personal Access Token to Local Storage. | ||
* | ||
* @param {string} pat - The Personal Access Token entered by user. | ||
*/ | ||
const setPat = async (pat) => { | ||
// Get encryption key | ||
let key = await getKey('pat_key'); | ||
|
||
if (!key) { | ||
// Create symmetric key for encrypting PAT for local storage | ||
key = await crypto.subtle.generateKey({ | ||
name: 'AES-GCM', | ||
length: 256, | ||
}, | ||
false, // do not allow export | ||
['encrypt', 'decrypt']); | ||
|
||
// Save key | ||
await setKey('pat_key', key); | ||
} else ({ key } = key); | ||
|
||
// Encode PAT into Uint8Array | ||
const encoder = new TextEncoder(); | ||
let plaintext = encoder.encode(pat); | ||
|
||
// Generate initialization vector | ||
let initVector = crypto.getRandomValues(new Uint8Array(12)); | ||
|
||
// Encrypt plaintext | ||
let ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: initVector }, key, plaintext); | ||
|
||
// Save ciphertext and IV to local storage as strings | ||
localStorage.setItem('PAT', new Uint8Array(ciphertext).toString()); // ciphertext is of type buffer | ||
localStorage.setItem('initVector', initVector.toString()); | ||
}; | ||
|
||
export { getPat, setPat }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { open } from './db.js'; | ||
import { getPat, setPat } from './pat.js'; | ||
import queryData from './graphql.js'; | ||
|
||
// Open Database | ||
open(); | ||
|
||
// If the Personal Access Token is not in local storage, prompt user for one | ||
if (!localStorage.getItem('PAT')) { | ||
// Show the authenticate form and bind validation functions | ||
const prompt = document.querySelector('#authenticate'); | ||
const main = document.querySelector('main'); | ||
prompt.classList.add('show'); | ||
main.classList.add('blur'); | ||
|
||
const patInput = document.querySelector('#authenticate input'); | ||
|
||
patInput.addEventListener('input', () => { | ||
patInput.setCustomValidity(''); // reset message | ||
patInput.checkValidity(); | ||
}); | ||
|
||
patInput.addEventListener('invalid', () => { | ||
patInput.setCustomValidity('A GitHub Personal Access token is a 40 character hexadecimal string'); | ||
}); | ||
|
||
// Handle form submission | ||
document.querySelector('#authenticate form').addEventListener('submit', async (event) => { | ||
event.preventDefault(); | ||
event.stopPropagation(); | ||
|
||
await setPat(patInput.value); | ||
|
||
// Send graphql query | ||
queryData(getPat()); | ||
|
||
// Hide prompt | ||
prompt.classList.remove('show'); | ||
main.classList.remove('blur'); | ||
}); | ||
} else { | ||
// Otherwise send graphql query | ||
queryData(getPat()); | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest mentioning somewhere in this section that only Python 3 is supported when accessing the project dashboard (just to be explicit since above we have both Python 2 & 3 instructions).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think since Python 2 has been deprecated for over a year it might be okay to remove python2 instructions. Thoughts?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're planning to keep the Python 2 instructions since the Oppia web backend still uses it.