Key Manager

Key Manager creates, stores and shares a unique 64-character key for each assessment. Each key is associated with a unique record ID and stored securely. So that only the people who should see an assessment's key can retrieve it.

All keys are 64 characters. It is up to the developer to parse the key however they see fit. For example:

  • The first 32 characters could be used as a symmetric encryption key, and the last 32 characters could be used as an HMAC key.
  • Every other character could be used as a symmetric key.
  • A portion of the 64-character key could be used as an initialization vector (IV).
  • A portion of the 64-character key could be hashed and stored with each record to verify the correct key was retrieved before encryping a record.
  • Or whatever the developer chooses. NERCKeyManager does not know how the key will be used.

NERCKeyManager only knows the key, the assessment ID, and the user who created it. The key is never associated with a specific web service or application.

Split-key access diagram

NERCKeyManager is a US-based key management solution in Seattle, WA.

Email us at support@nerckeymanager.com.

Example: using the API

const BASE_URL = 'https://yourapplication.com';

// 1. Register a new user (one time). Username and password are required,
//    email is optional. Responds 201 on success, 409 if the username is taken.
await fetch(BASE_URL + '/register', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ user: 'myUsername', email: 'me@example.com', password: 'myPassword' })
});

// 2. Sign in to get an access token
const login = await fetch(BASE_URL + '/auth', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ user: 'myUsername', password: 'myPassword' })
});
const { accessToken } = await login.json();

const headers = {
    'Content-Type': 'application/json',
    Authorization: 'Bearer ' + accessToken
};

// 3. Create a key for an assessment (you become its owner)
await fetch(BASE_URL + '/key', {
    method: 'POST',
    headers,
    body: JSON.stringify({ assessmentID: 'assessment-123' })
});

// 4. Retrieve the key (owner or shared users only)
const res = await fetch(BASE_URL + '/key/assessment-123', { headers });
const { key } = await res.json();

// 5. Share the key with another user (owner only)
await fetch(BASE_URL + '/key/assessment-123', {
    method: 'PUT',
    headers,
    body: JSON.stringify({
        user: 'myUsername',
        password: 'myPassword',
        assessmentID: 'assessment-123',
        updateAccess: 'otherUsername'
    })
});

// 6. Revoke a user's access (owner only). The user can be a username or an email,
//    so encode it for the URL. No password is needed, the token identifies the owner.
await fetch(BASE_URL + '/key/assessment-123/access/' + encodeURIComponent('otherUsername'), {
    method: 'DELETE',
    headers
});

Endpoint reference

MethodPathAuthRequest bodySuccess response
POST/registerNoneuser, password, email (optional)201 { message }
POST/authNoneuser, password200 { accessToken }, and sets the jwt refresh cookie
GET/refreshjwt cookieNone200 { accessToken }
GET/logoutjwt cookieNone204, and clears the cookie
POST/keyBearer tokenassessmentID201 key record
GET/key/:assessmentIDBearer tokenNone200 key record
PUT/key/:assessmentIDBearer tokenuser, password, assessmentID, updateAccess200 updated key record
DELETE/key/:assessmentID/access/:userBearer tokenNone200 updated key record

Key record

{
    "_id": "66f3...",
    "assessmentID": "assessment-123",
    "key": "64 random characters",
    "userId": "id of the owner",
    "allowedUsers": ["otherUsername"]
}

Tokens

Send the access token on every /key request as Authorization: Bearer <accessToken>. The token from /auth expires after 1 day. The refresh token is kept in an httpOnly jwt cookie, so call /refresh with credentials included (fetch(..., { credentials: 'include' })). A new access token from /refresh expires after 15 seconds. Call /logout to end the session.

Errors and status codes

RequestSituationResponse
Any /key requestAuthorization header missing or not a Bearer token401, no body
Any /key requestAccess token invalid or expired403, no body
POST /registerUsername or password missing400 { message }
POST /registerUsername already taken409, no body
POST /authUsername or password missing400 { message }
POST /authUnknown user401 { message }
POST /authWrong password401, no body
GET /refreshNo jwt cookie401 { message }
GET /refreshRefresh token invalid403, no body
GET /key/:assessmentIDYou are not the owner or a shared user204, no body
PUT /key/:assessmentIDupdateAccess, username or password missing400 { message }
PUT /key/:assessmentIDWrong password204, no body
PUT /key/:assessmentIDAssessment not found200 { message: 'record not found' }
PUT /key/:assessmentIDYou are not the owner200 { message: 'Only owners can share keys.' }
DELETE /key/:assessmentID/access/:userAssessment not found, or the user does not have access404 { message }
DELETE /key/:assessmentID/access/:userYou are not the owner403 { message: 'Only owners can revoke access.' }
POST /keyassessmentID already existsNo response is sent, so the request times out
GET /key/:assessmentIDAssessment does not existNo response is sent, so the request times out

Do not rely on the status code alone. Some failed requests, such as PUT /key with an unknown username, return 200 with a fake key like { "key": "66f3126940345643f01dd" }. Treat a response as real only if it contains assessmentID. Also set a request timeout, because a few errors never get a response.