virgil-sdk
Version:
Virgil Security Services SDK
2,047 lines • 69.7 kB
JavaScript
'use strict';
var base64 = require('base-64');
var fetchPonyfill = require('fetch-ponyfill');
var fs = require('fs');
var path = require('path');
var crypto = require('crypto');
var mkdirp = require('mkdirp');
var rimraf = require('rimraf');
/**
* Decodes the base64 encoded string into a `string`.
* @hidden
* @param {string} input
* @returns {string}
*/
function base64Decode(input) {
return base64.decode(input);
}
/**
* Encodes the `input` string into a base64 `string`.
* @hidden
* @param {string} input
* @returns {string}
*/
function base64Encode(input) {
return base64.encode(input);
}
/**
* Converts regular base64 encoded string to URL-safe base64 encoded string.
* @hidden
* @param {string} input - Regular base64 encoded string.
* @returns {string} - URL-safe base64 encoded string.
*/
function base64UrlFromBase64(input) {
input = input.split('=')[0];
input = input.replace(/\+/g, '-').replace(/\//g, '_');
return input;
}
/**
* Converts URL-safe base64 encoded string to regular base64 encoded string.
* @hidden
* @param {string} input - URL-safe base64 encoded string.
* @returns {string} - Regular base64 encoded string.
*/
function base64UrlToBase64(input) {
input = input.replace(/-/g, '+').replace(/_/g, '/');
switch (input.length % 4) {
case 0: break; // no padding needed
case 2:
input = input + '==';
break;
case 3:
input = input + '=';
break;
default:
throw new Error('Invalid base64 string');
}
return input;
}
/**
* * Encodes the `input` string into a string using URL-safe base64 encoding.
*
* @hidden
*
* @param {string} input - The input.
* @returns {string}
*/
function base64UrlEncode(input) {
let output = base64Encode(input);
return base64UrlFromBase64(output);
}
/**
* Decodes the URL-safe base64-encoded `input` string into a `string`.
*
* @hidden
*
* @param {string} input
* @returns {string}
*/
function base64UrlDecode(input) {
const str = base64UrlToBase64(input);
return base64Decode(str);
}
/**
* Converts javascript date object or timestamp in milliseconds
* to Unix timestamp.
*
* @hidden
*
* @param {Date | number} date - The date or timestamp to convert.
* @returns {number}
*/
function getUnixTimestamp(date) {
let time;
if (typeof date === 'number') {
time = date;
}
else {
time = date.getTime();
}
return Math.floor(time / 1000);
}
/**
* Adds the given number of seconds to the given date.
*
* @hidden
*
* @param {Date | number} date - The date to add seconds to.
* If `date` is a `number` it is treated as a timestamp in milliseconds.
* @param {number} seconds - The number of seconds to add.
* @returns {Date} - The new date.
*/
function addSeconds(date, seconds) {
if (typeof date === 'number') {
return new Date(date + seconds * 1000);
}
return new Date(date.getTime() + seconds * 1000);
}
/**
* JWT Subject.
* @hidden
*/
const SubjectPrefix = "identity-";
/**
* JWT Issuer.
* @hidden
*/
const IssuerPrefix = "virgil-";
/**
* Content type of the token. Used to convey structural information
* about the JWT.
* @hidden
*/
const VirgilContentType = "virgil-jwt;v=1";
/**
* Media type of the JWT.
* @hidden
*/
const JwtContentType = "JWT";
/**
* Class representing the JWT providing access to the
* Virgil Security APIs.
* Implements {@link IAccessToken} interface.
*/
class Jwt {
constructor(header, body, signature) {
if (typeof header === 'string') {
const stringRepresentation = header;
const parts = stringRepresentation.split('.');
if (parts.length !== 3)
throw new Error('Wrong JWT format');
try {
this.header = JSON.parse(base64UrlDecode(parts[0]));
this.body = JSON.parse(base64UrlDecode(parts[1]));
this.signature = base64UrlToBase64(parts[2]);
}
catch (e) {
throw new Error('Wrong JWT format');
}
this.unsignedData = parts[0] + '.' + parts[1];
this.stringRepresentation = stringRepresentation;
}
else if (typeof header === 'object' && typeof body === 'object') {
this.header = header;
this.body = body;
this.signature = signature;
this.unsignedData = this.headerBase64() + '.' + this.bodyBase64();
this.stringRepresentation = this.signature == null
? this.unsignedData
: this.unsignedData + '.' + this.signatureBase64();
}
else {
throw new TypeError('Invalid arguments for function Jwt. ' +
'Expected a string representation of a token, or header and body as objects');
}
}
/**
* Parses the string representation of the JWT into
* an object representation.
*
* @param {string} jwtStr - The JWT string. Must have the following format:
*
* `base64UrlEncode(Header) + "." + base64UrlEncode(Body) + "." + base64UrlEncode(Signature)`
*
* See the {@link https://jwt.io/introduction/ | Introduction to JWT} for more details.
*
* @returns {Jwt}
*/
static fromString(jwtStr) {
return new Jwt(jwtStr);
}
/**
* Returns the string representation of this JWT.
* @returns {string}
*/
toString() {
return this.stringRepresentation;
}
/**
* Retrieves the identity that is the subject of this JWT.
* @returns {string}
*/
identity() {
if (this.body.sub.indexOf(SubjectPrefix) !== 0) {
throw new Error('wrong sub format');
}
return this.body.sub.substr(SubjectPrefix.length);
}
/**
* Retrieves the application ID that is the issuer of this JWT.
* @returns {string}
*/
appId() {
if (this.body.iss.indexOf(IssuerPrefix) !== 0) {
throw new Error('wrong iss format');
}
return this.body.iss.substr(IssuerPrefix.length);
}
/**
* Returns a boolean indicating whether this JWT is (or will be)
* expired at the given date or not.
*
* @param {Date} at - The date to check. Defaults to `new Date()`.
* @returns {boolean} - `true` if token is expired, otherwise `false`.
*/
isExpired(at = new Date) {
const now = getUnixTimestamp(at);
return this.body.exp < now;
}
headerBase64() {
return base64UrlEncode(JSON.stringify(this.header));
}
bodyBase64() {
return base64UrlEncode(JSON.stringify(this.body));
}
signatureBase64() {
return base64UrlFromBase64(this.signature);
}
}
/**
* Test if `condition` is truthy. If it is not, an `Error` is thrown with a
* `message` property equal to `message` parameter.
* @hidden
* @param {boolean} condition
* @param {string} message
*/
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
const DEFAULT_TOKEN_TTL = 20 * 60 * 1000; // 20 minutes
/**
* Class responsible for JWT generation.
*/
class JwtGenerator {
constructor(options) {
validateOptions$1(options);
this.appId = options.appId;
this.apiKey = options.apiKey;
this.apiKeyId = options.apiKeyId;
this.accessTokenSigner = options.accessTokenSigner;
this.millisecondsToLive = options.millisecondsToLive !== undefined
? Number(options.millisecondsToLive)
: DEFAULT_TOKEN_TTL;
}
/**
* Generates a token with the given identity as the subject and optional
* additional data.
* @param {string} identity - Identity to be associated with JWT (i.e.
* the Subject).
* @param {IExtraData} ada - Additional data to be encoded in the JWT.
* @returns {Jwt}
*/
generateToken(identity, ada) {
if (!identity) {
throw new TypeError('Illegal arguments for function `generateToken`. Argument `identity` is required.');
}
const iat = getUnixTimestamp(new Date());
const exp = getUnixTimestamp(new Date().getTime() + this.millisecondsToLive);
const body = {
iss: IssuerPrefix + this.appId,
sub: SubjectPrefix + identity,
iat,
exp,
ada
};
const header = {
alg: this.accessTokenSigner.getAlgorithm(),
kid: this.apiKeyId,
typ: JwtContentType,
cty: VirgilContentType
};
const unsignedJwt = new Jwt(header, body);
const signature = this.accessTokenSigner.generateTokenSignature({ value: unsignedJwt.unsignedData, encoding: 'utf8' }, this.apiKey);
return new Jwt(header, body, signature.toString('base64'));
}
}
function validateOptions$1(opts) {
const invalidOptionMessage = (name) => `Invalid JwtGenerator options. \`${name}\` is required`;
assert(opts != null, 'JwtGenerator options must be provided');
assert(opts.apiKey != null, invalidOptionMessage('apiKey'));
assert(opts.apiKeyId != null, invalidOptionMessage('apiKeyId'));
assert(opts.appId != null, invalidOptionMessage('appId'));
assert(opts.accessTokenSigner != null, invalidOptionMessage('accessTokenSigner'));
}
/**
* Class responsible for verification of JWTs.
*/
class JwtVerifier {
constructor(options) {
validateOptions(options);
this.accessTokenSigner = options.accessTokenSigner;
this.apiPublicKey = options.apiPublicKey;
this.apiKeyId = options.apiKeyId;
}
/**
* Verifies the validity of the given JWT.
* @param {Jwt} token - The JWT to verify.
* @returns {boolean}
*/
verifyToken(token) {
if (token == null) {
throw new Error('Token is empty');
}
if (!this.allFieldsAreCorrect(token)) {
return false;
}
return this.accessTokenSigner.verifyTokenSignature({ value: token.unsignedData, encoding: 'utf8' }, { value: token.signature, encoding: 'base64' }, this.apiPublicKey);
}
allFieldsAreCorrect(token) {
return token.header.kid == this.apiKeyId
&& token.header.alg == this.accessTokenSigner.getAlgorithm()
&& token.header.cty == VirgilContentType
&& token.header.typ == JwtContentType;
}
}
function validateOptions(opts) {
const invalidOptionMessage = (name) => `Invalid JwtVerifier options. \`${name}\` is required`;
assert(opts != null, 'JwtVerifier options must be provided');
assert(opts.apiPublicKey != null, invalidOptionMessage('apiPublicKey'));
assert(opts.apiKeyId != null, invalidOptionMessage('apiKeyId'));
assert(opts.accessTokenSigner != null, invalidOptionMessage('accessTokenSigner'));
}
const TOKEN_EXPIRATION_MARGIN = 5;
/**
* Implementation of {@link IAccessTokenProvider} that caches the JWT
* in memory while it's fresh (i.e. not expired) and uses the user-provided
* callback function to get the JWT when requested by the clients.
*/
class CachingJwtProvider {
/**
* Creates a new instance of `CachingJwtProvider`.
* @param {GetJwtCallback} renewJwtFn - The function that will be called
* whenever the fresh JWT is needed. If the `renewJwtFn` returns the JWT
* as a string, it will be converted to {@link Jwt} instance automatically.
* @param {Jwt|string} [initialToken] - Optional initial JWT.
*/
constructor(renewJwtFn, initialToken) {
if (typeof renewJwtFn !== 'function') {
throw new TypeError('`renewJwtFn` must be a function');
}
if (initialToken) {
let jwt;
if (typeof initialToken === 'string') {
jwt = Jwt.fromString(initialToken);
}
else if (initialToken instanceof Jwt) {
jwt = initialToken;
}
else {
throw new Error(`Expected "initialToken" to be a string or an instance of Jwt, got ${typeof initialToken}`);
}
this.cachedJwt = jwt;
}
this.getJwt = (context) => {
if (this.cachedJwt && !this.cachedJwt.isExpired(addSeconds(new Date, TOKEN_EXPIRATION_MARGIN))) {
return Promise.resolve(this.cachedJwt);
}
if (this.jwtPromise) {
return this.jwtPromise;
}
this.jwtPromise = Promise.resolve(renewJwtFn(context))
.then(token => {
const jwt = typeof token === 'string' ? Jwt.fromString(token) : token;
this.cachedJwt = jwt;
this.jwtPromise = undefined;
return jwt;
}).catch(err => {
this.jwtPromise = undefined;
throw err;
});
return this.jwtPromise;
};
}
/**
* Returns a `Promise` resolved with the cached token if it's fresh, or the
* token obtained by the call to the `renewJwtCallback` otherwise. The token
* obtained from the `renewJwtCallback` is then cached. If the `renewJwtCallback`
* returns the JWT as a string, it is converted to {@link Jwt} instance before returning.
* @param {ITokenContext} context
* @returns {Promise<IAccessToken>}
*/
getToken(context) {
return this.getJwt(context);
}
}
/**
* Implementation of {@link IAccessToken} that calls the user-provided
* callback function to get the JWT when requested by the clients.
*/
class CallbackJwtProvider {
/**
* Creates a new instance of `CallbackJwtProvider`.
*
* @param {GetJwtCallback} getJwtFn - The function that will be called
* whenever the JWT is needed. If the `getJwtFn` returns the JWT as a
* string, it will be converted to {@link Jwt} instance automatically.
*/
constructor(getJwtFn) {
if (typeof getJwtFn !== 'function') {
throw new TypeError('`getJwtFn` must be a function');
}
this.getJwt = (context) => Promise.resolve(getJwtFn(context))
.then(token => typeof token === 'string' ? Jwt.fromString(token) : token);
}
/**
* Returns a `Promise` resolved with the {@link Jwt} instance obtained
* by the call to the {@link CallbackJwtProvider.getJwt}. If the
* `getJwtFn` returns the JWT as a string, it is converted to
* {@link Jwt} instance before returning.
*
* @param {ITokenContext} context
* @returns {Promise<IAccessToken>}
*/
getToken(context) {
return this.getJwt(context);
}
}
/**
* Implementation of {@link IAccessTokenProvider} that returns a
* user-provided constant access token whenever it is requested by the clients.
*/
class ConstAccessTokenProvider {
/**
* Creates a new instance of `ConstAccessTokenProvider`
* @param {IAccessToken} accessToken - The access token to be returned
* whenever it is requested.
*/
constructor(accessToken) {
this.accessToken = accessToken;
if (accessToken == null) {
throw new TypeError('`accessToken` is required');
}
}
;
/**
* Returns a `Promise` fulfilled with the
* {@link ConstAccessTokenProvider.accessToken} provided to the constructor
* of this instance.
*
* @param {ITokenContext} context
* @returns {Promise<IAccessToken>}
*/
getToken(context) {
return Promise.resolve(this.accessToken);
}
}
/**
* Implementation of {@link IAccessTokenProvider} that generates a
* new JWT whenever it is requested by the clients.
*
* This class is meant to be used on the server side only.
*/
class GeneratorJwtProvider {
/**
* Creates a new instance of `GeneratorJwtProvider` with the given
* {@link JwtGenerator}, additional data and default identity.
*
* @param {JwtGenerator} jwtGenerator - Object to delegate the JWT generation to.
* @param {IExtraData} additionalData - Additional data to include with the JWT.
* @param {string} defaultIdentity - Identity of the user to include in the token
* when none is provided explicitly by the client.
*/
constructor(jwtGenerator, additionalData, defaultIdentity) {
this.jwtGenerator = jwtGenerator;
this.additionalData = additionalData;
this.defaultIdentity = defaultIdentity;
if (jwtGenerator == null) {
throw new TypeError('`jwtGenerator` is required');
}
}
/**
* Returns a `Promise` fulfilled with the JWT obtained from the call
* to {@link GeneratorJwtProvider.jwtGenerator} {@link JwtGenerator.generateToken}
* method, passing it the {@link GeneratorJwtProvider.additionalData} and
* {@link GeneratorJwtProvider.defaultIdentity}
*
* @param {ITokenContext} context
* @returns {Promise<IAccessToken>}
*/
getToken(context) {
return Promise.resolve().then(() => {
const jwt = this.jwtGenerator.generateToken(context.identity || this.defaultIdentity || '', this.additionalData);
return jwt;
});
}
}
/**
* Intermediate representation of the Virgil Card with `contentSnapshot`
* and `snapshot`s of the signatures in UTF-8.
*/
class RawSignedModel {
/**
* Initializes a new instance of `RawSignedModel`.
* @param {string} contentSnapshot - The content snapshot in UTF-8.
* @param {IRawSignature[]} signatures - The signatures. If signatures
* themselves have snapshots, those must also be in UTF-8.
*/
constructor(contentSnapshot, signatures) {
this.contentSnapshot = contentSnapshot;
this.signatures = signatures;
}
/**
* Converts the `str` in base64 encoding into a `RawSignedModel` object.
*
* @param {string} str - Base64 string representation of the card as
* returned by {@RawSignedModel.toString} method.
*
* @returns {RawSignedModel}
*/
static fromString(str) {
const jsonStr = base64Decode(str);
let obj;
try {
obj = JSON.parse(jsonStr);
}
catch (error) {
throw new Error('The string to be parsed is in invalid format');
}
return RawSignedModel.fromJson(obj);
}
/**
* Converts the `json` serializable object into a `RawSignedModel` object.
* @param {IRawSignedModelJson} json - JSON-serializable object returned by
* {@link RawSignedModel.toJson} method.
* @returns {RawSignedModel}
*/
static fromJson(json) {
const contentSnapshotUtf8 = base64Decode(json.content_snapshot);
const signaturesWithUtf8Snapshots = (json.signatures || []).map(({ signer, signature, snapshot }) => {
if (snapshot) {
return {
signer,
signature,
snapshot: base64Decode(snapshot)
};
}
return { signer, signature };
});
return new RawSignedModel(contentSnapshotUtf8, signaturesWithUtf8Snapshots);
}
/**
* This is to make it work with `JSON.stringify`, calls
* {@link RawSignedModel.toJson} under the hood.
* @returns {IRawSignedModelJson}
*/
toJSON() {
return this.toJson();
}
/**
* Returns a JSON-serializable representation of this model in the
* format it is stored in the Virgil Cards Service. (i.e. with
* `contentSnapshot` and `snapshot`s of the signatures as base64 encoded
* strings.
* @returns {IRawSignedModelJson}
*/
toJson() {
return {
content_snapshot: base64Encode(this.contentSnapshot),
signatures: this.signatures.map(({ signer, signature, snapshot }) => {
if (snapshot) {
return {
signer,
signature,
snapshot: base64Encode(snapshot)
};
}
return { signer, signature };
})
};
}
/**
* Serializes this model to string in base64 encoding.
* @returns {string}
*/
toString() {
return base64Encode(JSON.stringify(this));
}
/**
* Same as {@link RawSignedModel.toJson}. Please use that instead.
* @returns {IRawSignedModelJson}
*/
exportAsJson() {
return this.toJson();
}
/**
* Same as {@link RawSignedModel.toString}. Please use that instead.
* @returns {string}
*/
exportAsString() {
return this.toString();
}
}
const { fetch, Request, Response, Headers } = fetchPonyfill();
const OS_LIST = [
/* Windows Phone */
{
name: 'Windows Phone',
test: [/windows phone/i],
},
/* Windows */
{
test: [/windows/i],
name: 'Windows'
},
/* macOS */
{
test: [/macintosh/i],
name: 'macOS'
},
/* iOS */
{
test: [/(ipod|iphone|ipad)/i],
name: 'iOS'
},
/* Android */
{
test: [/android/i],
name: 'Android',
},
/* Linux */
{
test: [/linux/i],
name: 'Linux',
},
/* Chrome OS */
{
test: [/CrOS/],
name: 'Chrome OS'
},
/* Playstation 4 */
{
test: [/PlayStation 4/],
name: 'PlayStation 4',
},
];
// Order of browsers matters! Edge, Opera and Chromium have Chrome in User Agent.
const BROWSER_LIST = [
{
test: [/googlebot/i],
name: 'Googlebot'
},
{
test: [/opera/i, /opr\/|opios/i],
name: 'Opera',
},
{
test: [/msie|trident/i],
name: 'Internet Explorer',
},
{
test: [/\sedg/i],
name: 'Microsoft Edge'
},
{
test: [/firefox|iceweasel|fxios/i],
name: 'Firefox',
},
{
test: [/chromium/i],
name: 'Chromium'
},
{
test: [/chrome|crios|crmo/i],
name: 'Chrome',
},
{
test: [/android/i],
name: 'Android Browser'
},
{
test: [/playstation 4/i],
name: 'PlayStation 4',
},
{
test: [/safari|applewebkit/i],
name: 'Safari',
}
];
/**
* Class responsible for tracking which Virgil SDK is being used to make requests, its version,
* browser and platform.
*/
class VirgilAgent {
/**
* Initializes a new instance of `VirgilAgent`.
* @param {string} product - name of product eg (sdk, brainkey, bpp, keyknox, ratchet, e3kit, purekit)
* argument of request methods.
* @param {string} version - version of the product.
* @param {string} [userAgent] - string with device user agent. Optional
*/
constructor(product, version, userAgent) {
/**
* Detect Cordova / PhoneGap / Ionic frameworks on a mobile device.
* @return {boolean} true if detects Ionic.
*/
this.isIonic = () => typeof window !== "undefined" &&
!!("cordova" in window || "phonegap" in window || "PhoneGap" in window) &&
/android|ios|iphone|ipod|ipad|iemobile/i.test(this.userAgent);
this.userAgent = userAgent || this.getUserAgent();
this.value = `${product};js;${this.getHeaderValue()};${version}`;
}
/**
* Returns navigator.userAgent string or '' if it's not defined.
* @return {string} user agent string
*/
getUserAgent() {
if (typeof navigator !== "undefined" && typeof navigator.userAgent === "string") {
return navigator.userAgent;
}
else {
return "";
}
}
;
/**
* Detects device OS
* @returns {string} returns OS if detected or 'other'.
*/
getOsName() {
const os = OS_LIST.find((os) => os.test.some(condition => condition.test(this.userAgent)));
return os ? os.name : 'other';
}
/**
* Detects device browser
* @returns {string} returns browser if detected of 'other'.
*/
getBrowser() {
const browser = BROWSER_LIST.find((browser) => browser.test.some(condition => condition.test(this.userAgent)));
return browser ? browser.name : 'other';
}
/**
* Detect React Native.
* @return {boolean} true if detects ReactNative .
*/
isReactNative() {
return typeof navigator === "object" && navigator.product === "ReactNative";
}
/**
* Return information for `virgil-agent` header.
* @return {string} string in format: PRODUCT;FAMILY;PLATFORM;VERSION
*/
getHeaderValue() {
try {
if (this.isReactNative())
return "ReactNative";
if (this.isIonic())
return `Ionic/${this.getOsName()}`;
if (!undefined && typeof global !== 'undefined') {
const majorVersion = process.version.replace(/\.\d+\.\d+$/, '').replace('v', '');
return `Node${majorVersion}/${process.platform}`;
}
return `${this.getBrowser()}/${this.getOsName()}`;
}
catch (e) {
return `Unknown`;
}
}
}
/**
* Class responsible for making HTTP requests.
* @hidden
*/
class Connection {
/**
* Initializes a new instance of `Connection`.
* @param {string} prefix - `prefix` will be prepended to the `endpoint`
* argument of request methods.
* @param {VirgilAgentValue} [virgilAgentValue] - optional instance of VirgilAgent for products that wraps
* Virgil SDK
*/
constructor(prefix, info) {
this.prefix = prefix;
if (!info)
info = { product: 'sdk', version: "6.2.0" };
this.virgilAgentValue = new VirgilAgent(info.product, info.version).value;
}
/**
* Issues a GET request against the `endpoint`.
* @param {string} endpoint - Endpoint URL relative to the `prefix`.
* @param {string} accessToken - Token to authenticate the request.
* @returns {Promise<Response>}
*/
get(endpoint, accessToken) {
const headers = this.createHeaders(accessToken);
return this.send(endpoint, 'GET', { headers });
}
/**
* Issues a POST request against the `endpoint` sending the `data` as JSON.
* @param {string} endpoint - Endpoint URL relative to the `prefix`.
* @param {string} accessToken - Token to authenticate the request.
* @param {object} data - Response body.
* @returns {Promise<Response>}
*/
post(endpoint, accessToken, data = {}) {
const headers = this.createHeaders(accessToken);
headers.set('Content-Type', 'application/json');
return this.send(endpoint, 'POST', {
headers: headers,
body: JSON.stringify(data)
});
}
send(endpoint, method, params) {
return fetch(this.prefix + endpoint, { method, ...params });
}
createHeaders(accessToken) {
const headers = new Headers();
headers.set('Authorization', `Virgil ${accessToken}`);
headers.set('Virgil-Agent', this.virgilAgentValue);
return headers;
}
}
/**
* Custom error class for errors specific to Virgil SDK.
*/
class VirgilError extends Error {
constructor(m, name = 'VirgilError', DerivedClass = VirgilError) {
super(m);
Object.setPrototypeOf(this, DerivedClass.prototype);
this.name = name;
}
}
var ErrorCode;
(function (ErrorCode) {
ErrorCode[ErrorCode["AccessTokenExpired"] = 20304] = "AccessTokenExpired";
ErrorCode[ErrorCode["Unknown"] = 0] = "Unknown";
})(ErrorCode || (ErrorCode = {}));
/**
* Error thrown by {@link CardManager} when request to the Virgil Cards Service
* fails.
*/
class VirgilHttpError extends VirgilError {
constructor(message, status, errorCode) {
super(message, 'VirgilHttpError', VirgilHttpError);
this.httpStatus = status;
this.errorCode = errorCode;
}
}
/**
* Generates error object from response object with HTTP status >= 400
*
* @hidden
*
* @param {Response} response
* @returns {Promise<VirgilHttpError>}
*/
async function generateErrorFromResponse(response) {
if (response.status >= 400 && response.status < 500) {
const reason = await response.json();
return new VirgilHttpError(reason.message, response.status, reason.code);
}
else {
return new VirgilHttpError(response.statusText, response.status, 0);
}
}
const PublishEndpoint = '/card/v5';
const SearchEndpoint = '/card/v5/actions/search';
const GetCardEndpoint = (cardId) => `/card/v5/${cardId}`;
const RevokeCardEndpoint = (cardId) => `/card/v5/actions/revoke/${cardId}`;
/**
* Class responsible for sending requests to the Virgil Cards Service.
*
* @hidden
*/
class CardClient {
/**
* Initializes new instance of `CardClient`.
* @param {IConnection | string} connection - Object implementing the
* {@link IConnection} interface.
*/
constructor(connection, productInfo) {
if (typeof connection === 'string') {
this.connection = new Connection(connection, productInfo);
}
else if (connection) {
this.connection = connection;
}
else {
this.connection = new Connection('https://api.virgilsecurity.com', productInfo);
}
}
/**
* Issues a request to search cards by the `identity`.
* @param {string[]} identities - Array of identities to search for.
* @param {string} accessToken - A token to authenticate the request.
* @returns {Promise<RawSignedModel[]>}
*/
async searchCards(identities, accessToken) {
const response = await this.connection.post(SearchEndpoint, accessToken, { identities });
if (!response.ok)
throw await generateErrorFromResponse(response);
const cardsJson = await response.json();
if (cardsJson === null)
return [];
return cardsJson.map(RawSignedModel.fromJson);
}
/**
* Issues a request to get the card by id.
* @param {string} cardId - Id of the card to fetch.
* @param {string} accessToken - A token to authenticate the request.
* @returns {Promise<ICardResult>}
*/
async getCard(cardId, accessToken) {
if (!cardId)
throw new TypeError('`cardId` should not be empty');
if (!accessToken)
throw new TypeError('`accessToken` should not be empty');
const response = await this.connection.get(GetCardEndpoint(cardId), accessToken);
if (!response.ok) {
throw await generateErrorFromResponse(response);
}
const isOutdated = response.headers.get('X-Virgil-Is-Superseeded') === 'true';
const cardJson = await response.json();
const cardRaw = RawSignedModel.fromJson(cardJson);
return { cardRaw, isOutdated };
}
/**
* Issues a request to publish the card.
* @param {RawSignedModel} model - Card to publish.
* @param {string} accessToken - A token to authenticate the request.
* @returns {Promise<RawSignedModel>}
*/
async publishCard(model, accessToken) {
if (!model)
throw new TypeError('`model` should not be empty');
if (!accessToken)
throw new TypeError('`accessToken` should not be empty');
const response = await this.connection.post(PublishEndpoint, accessToken, model);
if (!response.ok) {
throw await generateErrorFromResponse(response);
}
const cardJson = await response.json();
return RawSignedModel.fromJson(cardJson);
}
async revokeCard(cardId, accessToken) {
if (!cardId)
throw new TypeError('`cardId` should not be empty');
if (!accessToken)
throw new TypeError('`accessToken` should not be empty');
const response = await this.connection.post(RevokeCardEndpoint(cardId), accessToken);
if (!response.ok) {
throw await generateErrorFromResponse(response);
}
}
}
/**
* @hidden
*/
const SelfSigner = "self";
/**
* @hidden
*/
const VirgilSigner = "virgil";
/**
* @hidden
*/
const CardVersion = '5.0';
/**
* @hidden
*/
const CardIdByteLength = 32;
/**
* Class responsible for generating signatures of the cards.
*/
class ModelSigner {
/**
* Initializes a new instance of `ModelSigner`.
* @param {ICardCrypto} crypto - Object implementing the
* {@link ICardCrypto} interface.
*/
constructor(crypto) {
this.crypto = crypto;
}
/**
* Generates a new signature based on `rawParams`.
* @param {IRawSignParams} rawParams
*/
sign(rawParams) {
const { model, signerPrivateKey, signer, extraSnapshot } = this.prepareParams(rawParams);
const signedSnapshot = extraSnapshot != null
? model.contentSnapshot + extraSnapshot
: model.contentSnapshot;
const signature = this.crypto.generateSignature({ value: signedSnapshot, encoding: 'utf8' }, signerPrivateKey);
model.signatures.push({
signer,
signature: signature.toString('base64'),
snapshot: extraSnapshot
});
}
prepareParams({ model, signerPrivateKey, extraFields, signer }) {
signer = signer || SelfSigner;
let extraSnapshot;
if (extraFields != null) {
extraSnapshot = JSON.stringify(extraFields);
}
const final = { model, signerPrivateKey, signer, extraSnapshot };
this.validate(final);
return final;
}
validate({ model, signerPrivateKey, signer }) {
if (model == null) {
throw new Error("Model is empty");
}
if (signerPrivateKey == null) {
throw new Error("`signerPrivateKey` property is mandatory");
}
if (model.signatures != null && model.signatures.some(s => s.signer == signer)) {
throw new Error("The model already has this signature.");
}
}
}
/**
* Converts an {@link ICard} to a {@link RawSignedModel}.
*
* @hidden
*
* @param {ICard} card - The {@link ICard} to convert.
* @returns {RawSignedModel}
*/
function cardToRawSignedModel(card) {
return new RawSignedModel(card.contentSnapshot, card.signatures.slice());
}
/**
* Generates a {@link RawSignedModel} from the given `params`.
*
* @hidden
*
* @param {ICardCrypto} crypto - Object implementing the {@link ICardCrypto}
* interface.
* @param {INewCardParams} params - New card parameters.
* @returns {RawSignedModel}
*/
function generateRawSigned(crypto, params) {
const { identity, publicKey, previousCardId } = params;
const now = getUnixTimestamp(new Date);
const details = {
identity: identity,
previous_card_id: previousCardId,
created_at: now,
version: CardVersion,
public_key: crypto.exportPublicKey(publicKey).toString('base64'),
};
return new RawSignedModel(JSON.stringify(details), []);
}
/**
* Converts the {@link RawSignedModel} into the {@link ICard}.
*
* @hidden
*
* @param {ICardCrypto} crypto - Object implementing the {@link ICardCrypto}
* interface.
* @param {RawSignedModel} model - The model to convert.
* @param {boolean} isOutdated - Boolean indicating whether there is a newer
* Virgil Card replacing the one that `model` represents.
*
* @returns {ICard}
*/
function parseRawSignedModel(crypto, model, isOutdated = false) {
const content = JSON.parse(model.contentSnapshot);
const signatures = model.signatures.map(rawSignToCardSign);
return {
id: generateCardId(crypto, model.contentSnapshot),
publicKey: crypto.importPublicKey({ value: content.public_key, encoding: 'base64' }),
contentSnapshot: model.contentSnapshot,
identity: content.identity,
version: content.version,
createdAt: new Date(content.created_at * 1000),
previousCardId: content.previous_card_id,
signatures,
isOutdated
};
}
/**
* Given the array of `cards`, returns another array with outdated cards
* filtered out and the `previousCard` properties of the cards that replace
* the outdated ones being populated with appropriate outdated cards.
* i.e. turns this (A is for Actual, O is for Outdated):
* ```
* A -> O -> A -> A -> O
* ```
* into this
* ```
* A -> A -> A
* | |
* O O
* ```
*
* @hidden
*
* @param {ICard[]} cards - The cards array to transform.
* @returns {ICard[]} - Transformed array.
*/
function linkedCardList(cards) {
const unsorted = Object.create(null);
for (const card of cards) {
unsorted[card.id] = card;
}
for (const card of cards) {
if (card.previousCardId == null)
continue;
if (unsorted[card.previousCardId] == null)
continue;
unsorted[card.previousCardId].isOutdated = true;
card.previousCard = unsorted[card.previousCardId];
delete unsorted[card.previousCardId];
}
return Object.keys(unsorted).map(key => unsorted[key]);
}
/**
* Calculates ID for the VirgilCard from the `snapshot` of its contents.
*
* @hidden
*
* @param {ICardCrypto} crypto - Object implementing the {@link ICardCrypto}
* interface.
* @param {string} snapshot - The VirgilCard's contents snapshot.
* @returns {string} - VirgilCard's ID encoded in HEX.
*/
function generateCardId(crypto, snapshot) {
const fingerprint = crypto
.generateSha512({ value: snapshot, encoding: 'utf8' })
.slice(0, CardIdByteLength);
return fingerprint.toString('hex');
}
function rawSignToCardSign({ snapshot, signature, signer }) {
return {
signer,
signature,
snapshot,
extraFields: tryParseExtraFields(snapshot)
};
}
function tryParseExtraFields(snapshot) {
if (snapshot) {
try {
return JSON.parse(snapshot);
}
catch (ignored) { }
}
return {};
}
/**
* Error thrown by {@link CardManager} instances when the card received from
* the network (or imported from string\json) fails verification.
*/
class VirgilCardVerificationError extends VirgilError {
constructor(m) {
super(m, 'CardVerificationError', VirgilCardVerificationError);
}
}
const CARDS_SERVICE__NAME = 'cards';
/**
* @hidden
*/
const throwingAccessTokenProvider = {
getToken: () => {
throw new Error('Please set `CardManager.accessTokenProvider` to be able to make requests.');
}
};
const getCardServiceTokenContext = (context) => ({
...context,
service: CARDS_SERVICE__NAME,
});
/**
* Class responsible for creating, publishing and retrieving Virgil Cards.
*/
class CardManager {
constructor(params) {
this.crypto = params.cardCrypto;
this.client = new CardClient(params.apiUrl, params.productInfo);
this.modelSigner = new ModelSigner(params.cardCrypto);
this.signCallback = params.signCallback;
this.retryOnUnauthorized = params.retryOnUnauthorized;
this.cardVerifier = params.cardVerifier;
this.accessTokenProvider = params.accessTokenProvider || throwingAccessTokenProvider;
}
/**
* Generates a {@link RawSignedModel} that represents a card from
* `cardParams`.
* Use this method if you don't need to publish the card right away, for
* example if you need to first send it to your backend server to apply
* additional signature.
*
* @param {INewCardParams} cardParams - New card parameters.
* @returns {RawSignedModel}
*/
generateRawCard(cardParams) {
const model = generateRawSigned(this.crypto, cardParams);
this.modelSigner.sign({
model,
signerPrivateKey: cardParams.privateKey,
signer: SelfSigner,
extraFields: cardParams.extraFields
});
return model;
}
/**
* Generates a card from `cardParams` and publishes it in the Virgil Cards
* Service.
* @param {INewCardParams} cardParams - New card parameters.
* @returns {Promise<ICard>}
*/
async publishCard(cardParams) {
validateCardParams(cardParams);
const tokenContext = {
service: CARDS_SERVICE__NAME,
identity: cardParams.identity,
operation: 'publish'
};
const token = await this.accessTokenProvider.getToken(tokenContext);
const rawSignedModel = this.generateRawCard(Object.assign({}, cardParams, { identity: token.identity() }));
return await this.publishRawSignedModel(rawSignedModel, tokenContext, token);
}
/**
* Publishes a previously generated card in the form of
* {@link RawSignedModel} object.
*
* @param {RawSignedModel} rawCard - The card to publish.
* @returns {Promise<ICard>}
*/
async publishRawCard(rawCard) {
assert(rawCard != null && rawCard.contentSnapshot != null, '`rawCard` should not be empty');
const cardDetails = JSON.parse(rawCard.contentSnapshot);
const tokenContext = getCardServiceTokenContext({ identity: cardDetails.identity, operation: 'publish' });
const token = await this.accessTokenProvider.getToken(tokenContext);
return this.publishRawSignedModel(rawCard, tokenContext, token);
}
/**
* Fetches the card by `cardId` from the Virgil Card Service.
* @param {string} cardId - Id of the card to fetch.
* @returns {Promise<ICard>}
*/
async getCard(cardId) {
const tokenContext = getCardServiceTokenContext({ operation: 'get' });
const accessToken = await this.accessTokenProvider.getToken(tokenContext);
const cardWithStatus = await this.tryDo(tokenContext, accessToken, async (token) => await this.client.getCard(cardId, token.toString()));
const card = parseRawSignedModel(this.crypto, cardWithStatus.cardRaw, cardWithStatus.isOutdated);
if (card.id !== cardId) {
throw new VirgilCardVerificationError('Received invalid card');
}
this.validateCards([card]);
return card;
}
/**
* Fetches collection of cards with the given `identity` from the Virgil
* Cards Service.
* @param {string|string[]} identities - Identity or an array of identities of the cards to fetch.
* @returns {Promise<ICard[]>}
*/
async searchCards(identities) {
if (!identities)
throw new TypeError('Argument `identities` is required');
const identitiesArr = Array.isArray(identities) ? identities : [identities];
if (identitiesArr.length === 0)
throw new TypeError('Identities array must not be empty');
const tokenContext = getCardServiceTokenContext({ operation: 'search' });
const accessToken = await this.accessTokenProvider.getToken(tokenContext);
const rawCards = await this.tryDo(tokenContext, accessToken, async (token) => await this.client.searchCards(identitiesArr, token.toString()));
const cards = rawCards.map(raw => parseRawSignedModel(this.crypto, raw, false));
const identitiesSet = new Set(identitiesArr);
if (cards.some(c => !identitiesSet.has(c.identity))) {
throw new VirgilCardVerificationError('Received invalid cards');
}
this.validateCards(cards);
return linkedCardList(cards);
}
/**
* Marks the Virgil Card specified by `cardId` as revoked. Revoked cards will have `isOutdated`
* property set to `true` when retrieved via {@link CardManager.getCard} method.
* Also revoked cards will be absent in the {@link CardManager.searchCards} result.
* @param {string} cardId - Id of the card to revoke.
* @returns {Promise}
*/
async revokeCard(cardId) {
if (!cardId)
throw new TypeError('Argument `cardId` is required');
const tokenContext = getCardServiceTokenContext({ operation: 'revoke' });
const accessToken = await this.accessTokenProvider.getToken(tokenContext);
await this.tryDo(tokenContext, accessToken, async (token) => await this.client.revokeCard(cardId, token.toString()));
}
/**
* Converts the card in the form of {@link RawSignedModel} object to the
* {@link ICard} object.
*
* @see {@link CardManager.exportCard}
*
* @param {RawSignedModel} rawCard - The card to convert.
* @returns {ICard}
*/
importCard(rawCard) {
const card = parseRawSignedModel(this.crypto, rawCard);
this.validateCards([card]);
return card;
}
/**
* Converts the card in the base64 string form to the {@link ICard} object.
*
* @see {@link CardManager.exportCardAsString}
*
* @param {string} str - The string in base64.
* @returns {ICard}
*/
importCardFromString(str) {
assert(Boolean(str), '`str` should not be empty');
return this.importCard(RawSignedModel.fromString(str));
}
/**
* Converts the card in the JSON-serializable object form to the
* {@link ICard} object.
*
* @see {@link CardManager.exportCardAsJson}
*
* @param {IRawSignedModelJson} json
* @returns {ICard}
*/
importCardFromJson(json) {
assert(Boolean(json), '`json` should not be empty');
return this.importCard(RawSignedModel.fromJson(json));
}
/**
* Converts the card in the form of {@link ICard} object to the
* {@link RawSignedModel} object.
*
* @see {@link CardManager.importCard}
*
* @param {ICard} card
* @returns {RawSignedModel}
*/
exportCard(card) {
return cardToRawSignedModel(card);
}
/**
* Converts the card in the form of {@link ICard} object to the string
* in base64 encoding.
*
* @see {@link CardManager.importCardFromString}
*
* @param {ICard} card
* @returns {string}
*/
exportCardAsString(card) {
return this.exportCard(card).toString();
}
/**
* Converts the card in the form of {@link ICard} object to the
* JSON-serializable object form.
*
* @see {@link CardManager.importCardFromJson}
*
* @param {ICard} card
* @returns {IRawSignedModelJson}
*/
exportCardAsJson(card) {
return this.exportCard(card).toJson();
}
/**
* @hidden
*/
async publishRawSignedModel(rawCard, context, accessToken) {
if (this.signCallback != null) {
rawCard = await this.signCallback(rawCard);
}
const publishedModel = await this.tryDo(context, accessToken, async (token) => await this.client.publishCard(rawCard, token.toString()));
if (rawCard.contentSnapshot !== publishedModel.contentSnapshot) {
throw new VirgilCardVerificationError('Received invalid card');
}
const card = parseRawSignedModel(this.crypto, publishedModel);
this.validateCards([card]);
return card;
}
/**
* @hidden
*/
async tryDo(context, token, func) {
try {
return await func(token);
}
catch (e) {
if (e instanceof VirgilHttpError &&
e.httpStatus === 401 &&
e.errorCode === ErrorCode.AccessTokenExpired &&
this.retryOnUnauthorized) {
token = await this.accessTokenProvider.getToken(getCardServiceTokenContext({
identity: context.identity,
operation: context.operation,
forceReload: true
}));
return await func(token);
}
throw e;
}
}
/**
* Delegates to the {@link CardManager.cardVerifier} to verify the validity
* of the `cards`.
*
* @throws {@link VirgilCardVerificationError} if any of the cards is not
* valid.
*
* @param {ICard[]} cards
*/
validateCards(cards) {
if (this.cardVerifier == null)
return;
for (const card of cards) {
if (!this.cardVerifier.verifyCard(card)) {
throw new VirgilCardVerificationError('Validation errors have been detected');
}
}
}
}
/**
* @hidden
*/
function validateCardParams(params, validateIdentity = false) {
assert(params != null, 'Card parameters must be provided');
assert(params.privateKey != null, 'Card\'s private key is required');
assert(params.publicKey != null, 'Card\'s public key is required');
if (validateIdentity) {
assert(typeof params.identity === 'string' && params.identity !== '', 'Card\'s identity is required');
}
}
const DEFAULTS$2 = {
verifySelfSignature: true,
verifyVirgilSignature: true,
whitelists: []
};
const VIRGIL_CARDS_PUBKEY_BASE64 = 'MCowBQYDK2VwAyEAljOYGANYiVq1WbvVvoYIKtvZi2ji9bAhxyu6iV/LF8M=';
/**
* Class responsible for validating cards by verifying their digital
* signatures.
*/
class VirgilCardVerifier {
/**
* Initializes a new instance of `VirgilCardVerifier`.
* @param {ICardCrypto} crypto - Object implementing the
* {@link ICardCrypto} interface.
* @param {IVirgilCardVerifierParams} options - Initialization options.
*/
constructor(crypto, options) {
this.crypto = crypto;
const params = { ...DEFAULTS$2, ...(options || {}) };
this.verifySelfSignature = params.verifySelfSignature;
this.verifyVirgilSignature = params.verifyVirgilSignature;
this.whitelists = params.whitelists;
this.virgilCardsPublicKey = crypto.importPublicKey({ value: VIRGIL_CARDS_PUBKEY_BASE64, encoding: 'base64' });
}
/**
* Verifies the signatures of the `card`.
* @param {ICard} card
* @returns {boolean} `true` if the signatures to be verified are present
* and valid, otherwise `false`.
*/
verifyCard(card) {
if (this.selfValidationFailed(card)) {
return false;
}
if (this.virgilValidationFailed(card)) {
return false;
}
if (!this.whitelists || this.whitelists.length === 0) {
return true;
}
const signers = card.signatures.map(s => s.signer);
for (const whitelist of this.whitelists) {
if (whitelist == null || whitelist.length === 0) {
return false;
}
const intersectedCreds = whitelist.filter(x => signers.indexOf(x.signer) !== -1);
if (intersectedCreds.length === 0) {
return false;
}
const isValidForSome = intersectedCreds.some(cred => this.validateSignerSignature(card, this.getPublicKey(cred.publicKeyBase64), cred.signer));
if (!isValidForSome) {
return false;
}
}
return true;
}
selfValidationFailed(card) {
return this.verifySelfSignature
&& !this.validateSignerSignature(card, card.publicKey, SelfSigner);
}
virgilValidationFailed(card) {
return this.verifyVirgilSignature
&& !this.validateSignerSignature(card, this.virgilCardsPublicKey, VirgilSigner);
}
getPublicKey(signerPublicKeyBase64) {
return this.crypto.importPublicKey({ value: signerPublicKeyBase64, encoding: 'base64' });
}
validateSignerSignature(card, signerPublicKey, signer) {
const signature = card.signatures.find(s => s.signer === signer);
if (signature == null)
return false;
const extendedSnapshot = signature.snapshot == null
? card.contentSnapshot
: card.contentSnapshot + signature.snapshot;
return this.crypto.verifySignature({ value: extendedSnapshot, encoding: 'utf8' }, { value: signature.signature, encoding: 'base64' }, signerPublicKey);
}
}
/**
* Error thrown by {@link IStorageAdapter.store} method when saving a value
* with a key that already exists in store.
*/
class StorageEntryAlreadyExistsError extends VirgilError {
constructor(key) {
super(`Storage entry ${key ? 'with key ' + name : 'with the given key'}already exists`, 'StorageEntryAlreadyExistsError', StorageEntryAlreadyExistsError);
}
}
const NO_SUCH_FILE = 'ENOENT';
const FILE_EXISTS = 'EEXIST';
/**
* Implementation of {@link IStorageAdapter} that uses file system for
* persistence. For use in Node.js.
*/
class FileSystemStorageAdapter {
/**
* Initializes a new instance of `FileSystemStorageAdapter`.
* @param {IStorageAdapterConfig} config - Configuration options.
* Currently only `dir` is supported and must be a file system path
* to the folder where the data will be stored.
*/
constructor(config) {
this.config = config;
mkdirp.sync(path.resolve(this.config.dir));
}
/**
* @inheritDoc
*/
store(key, data) {
return new Promise((resolve, reject) => {
const file = this.resolveFilePath(key);
fs.writeFile(file, data, { flag: 'wx' }, err => {
if (err && err.code === FILE_EXISTS) {
return reject(new StorageEntryAlreadyExistsError());
}
resolve();
});
});
}
/**
* @inheritDoc
*/
load(key) {
return Promise.resolve().then(() => {
const filename = this.resolveFilePath(key);
return readFileAsync(filename);
});
}
/**
* @inheritDoc
*/
exists(key) {
return new Promise((resolve, reject) => {
const file = this.resolveFilePath(key);
fs.access(file, err => {
if (err) {
if (err.code === NO_SUCH_FILE) {
return resolve(false);
}
return reject(err);
}
resolve(true);
});
});
}
/**
* @inheritDoc
*/
remove(key) {
return new Promise((resolve, reject) => {
const file = this.resolveFilePath(key);
fs.unlink(file, err => {
if (err) {
if (err.code === NO_SUCH_FILE) {
return resolve(false);
}
return reject(err);
}
resolve(true);
});
});
}
/**
* @inheritDoc
*/
update(key, data) {
return new Promise((resolve, reject) => {
const file = this.resolveFilePath(key);
fs.writeFile(file, data, { flag: 'w' }, err => {
if (err) {
return reject(err);
}
resolve();
});
});
}
/**
* @inheritDoc
*/
clear() {
return new Promise((resolve, reject) => {
rimraf(this.config.dir, err => {
if (err) {
return reject(err);
}
resolve();
});
});
}
/**
* @inheritDoc
*/
list() {
return new Promise((resolve, reject) => {
fs.readdir(this.config.dir, (err, files) => {
if (err) {
return reject(err);
}
Promise.all(files.map(filename => readFileAsync(path.resolve(this.config.dir, filename)))).then(contents => {
const entries = contents.filter(content => content !== null);
resolve(entries);
}).catch(reject);
});
});
}
resolveFilePath(key) {
return path.resolve(this.config.dir, this.hash(key));
}
hash(data) {
return crypto
.createHash('sha256')
.update(data)
.digest('hex');
}
}
function readFileAsync(filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, (err, data) => {
if (err) {
if (err.code === NO_SUCH_FILE) {
return resolve(null);
}
return reject(err);
}
resolve(data.toString('utf8'));
});
});
}
/**
* Error thrown from {@link PrivateKeyStorage.save} method when saving
* a private key with a name that already exists in store.
*/
class PrivateKeyExistsError extends VirgilError {
constructor(message = 'Private key with same name already exists') {
super(message, 'PrivateKeyExistsError', PrivateKeyExistsError);
}
}
const DEFAULTS$1 = {
dir: '.virgil_keys',
name: 'VirgilKeys'
};
/**
* Class representing a storage container for private key data.
* Use this class if you need to load the keys stored with
* version 4.x of this library. For new code, use the
* {@link PrivateKeyStorage} instead.
*
* @deprecated since version 5.0
*/
class KeyStorage {
constructor(config = {}) {
console.log('Warning! `KeyStorage` is deprecated. Use `PrivateKeyStorage` instead.');
this.adapter = resolveAdapter$1(config);
}
/**
* Checks whether a private key data with the given name exist in persistent storage.
* @param {string} name - Name to check.
* @returns {Promise<boolean>} - True if key data exist, otherwise false.
*/
exists(name) {
validateName$1(name);
return this.adapter.exists(name);
}
/**
* Loads the private key data by the given name.
* @param {string} name - Name of key data to load.
* @returns {Promise<string | null>} - Private key data as a string,
* or null if there is no data for the given name.
*/
load(name) {
validateName$1(name);
return this.adapter.load(name);
}
/**
* Removes the private key data stored under the given name from persistent storage.
* @param {string} name - Name of the key data to remove.
* @returns {Promise<boolean>} - True if the key has been removed, otherwise false.
*/
remove(name) {
validateName$1(name);
return this.adapter.remove(name);
}
/**
* Persists the private key data under the given name.
* @param {string} name - Name of the key data.
* @param {string} data - The key data.
* @returns {Promise<void>}
*/
save(name, data) {
validateName$1(name);
return this.adapter.store(name, data)
.catch(error => {
if (error && error.code === 'EEXIST') {
return Promise.reject(new PrivateKeyExistsError());
}
return Promise.reject(error);
});
}
}
function resolveAdapter$1(config) {
if (typeof config === 'string') {
return new FileSystemStorageAdapter({ dir: config, name: config });
}
const { adapter, ...rest } = config;
if (adapter != null) {
return adapter;
}
return new FileSystemStorageAdapter({ ...DEFAULTS$1, ...rest });
}
function validateName$1(name) {
if (!name)
throw new TypeError('Argument `name` is required.');
}
/**
* Error thrown when the value loaded from persistent storage cannot be
* parsed as a {@link IKeyEntry} object.
*/
class InvalidKeyEntryError extends VirgilError {
constructor(message = 'Loaded key entry was in invalid format.') {
super(message, 'InvalidKeyEntryError', InvalidKeyEntryError);
}
}
/**
* Error thrown from {@link KeyEntryStorage.save} method when saving a
* a key entry with the name that already exists in store.
*/
class KeyEntryAlreadyExistsError extends VirgilError {
constructor(name) {
super(`Key entry ${name ? 'named ' + name : 'with same name'}already exists`, 'KeyEntryAlreadyExistsError', KeyEntryAlreadyExistsError);
}
}
/**
* Error thrown from {@link KeyEntryStorage.update} method when updating
* a key entry that doesn't exist in store.
*/
class KeyEntryDoesNotExistError extends VirgilError {
constructor(name) {
super(`Key entry ${name ? 'named ' + name : 'with the given name'} does not exist.`, 'KeyEntryDoesNotExistError', KeyEntryDoesNotExistError);
}
}
const DEFAULTS = {
dir: '.virgil_key_entries',
name: 'VirgilKeyEntries'
};
const CREATION_DATE_KEY = 'creationDate';
const MODIFICATION_DATE_KEY = 'modificationDate';
/**
* Class responsible for persisting private key bytes with optional
* user-defined metadata.
*/
class KeyEntryStorage {
/**
* Initializes a new instance of `KeyEntryStorage`.
*
* @param {IKeyEntryStorageConfig} config - Instance configuration.
*/
constructor(config = {}) {
this.adapter = resolveAdapter(config);
}
/**
* @inheritDoc
*/
exists(name) {
validateName(name);
return this.adapter.exists(name);
}
/**
* @inheritDoc
*/
load(name) {
validateName(name);
return this.adapter.load(name).then(data => {
if (data == null) {
return null;
}
return deserializeKeyEntry(data);
});
}
/**
* @inheritDoc
*/
remove(name) {
validateName(name);
return this.adapter.remove(name);
}
/**
* @inheritDoc
*/
save({ name, value, meta }) {
validateNameProperty(name);
validateValueProperty(value);
const keyEntry = {
name: name,
value: value,
meta: meta,
creationDate: new Date(),
modificationDate: new Date()
};
return this.adapter.store(name, serializeKeyEntry(keyEntry))
.then(() => keyEntry)
.catch(error => {
if (error && error.name === 'StorageEntryAlreadyExistsError') {
throw new KeyEntryAlreadyExistsError(name);
}
throw error;
});
}
/**
* @inheritDoc
*/
list() {
return this.adapter.list()
.then(entries => entries.map(entry => deserializeKeyEntry(entry)));
}
/**
* @inheritDoc
*/
update({ name, value, meta }) {
validateNameProperty(name);
if (!(value || meta)) {
throw new TypeError('Invalid argument. Either `value` or `meta` property is required.');
}
return this.adapter.load(name)
.then(data => {
if (data === null) {
throw new KeyEntryDoesNotExistError(name);
}
const entry = deserializeKeyEntry(data);
const updatedEntry = Object.assign(entry, {
value: value || entry.value,
meta: meta || entry.meta,
modificationDate: new Date()
});
return this.adapter.update(name, serializeKeyEntry(updatedEntry))
.then(() => updatedEntry);
});
}
/**
* @inheritDoc
*/
clear() {
return this.adapter.clear();
}
}
function serializeKeyEntry(keyEntry) {
return JSON.stringify(keyEntry);
}
function deserializeKeyEntry(data) {
try {
return JSON.parse(data, (key, value) => {
if (key === CREATION_DATE_KEY || key === MODIFICATION_DATE_KEY) {
return new Date(value);
}
return value;
});
}
catch (error) {
throw new InvalidKeyEntryError();
}
}
function resolveAdapter(config) {
if (typeof config === 'string') {
return new FileSystemStorageAdapter({ dir: config, name: config });
}
const { adapter, ...rest } = config;
if (adapter != null) {
return adapter;
}
return new FileSystemStorageAdapter({ ...DEFAULTS, ...rest });
}
const requiredArg = (name) => (value) => {
if (!value)
throw new TypeError(`Argument '${name}' is required.`);
};
const requiredProp = (name) => (value) => {
if (!value)
throw new TypeError(`Invalid argument. Property ${name} is required`);
};
const validateName = requiredArg('name');
const validateNameProperty = requiredProp('name');
const validateValueProperty = requiredProp('value');
/**
* Class responsible for storage of private keys.
*/
class PrivateKeyStorage {
/**
* Initializes a new instance of `PrivateKeyStorage`.
* @param {IPrivateKeyExporter} privateKeyExporter - Object responsible for
* exporting private key bytes from `IPrivateKey` objects and importing
* private key bytes into `IPrivateKey` objects.
* @param {IKeyEntryStorage} keyEntryStorage - Object responsible for
* persistence of private keys data.
*/
constructor(privateKeyExporter, keyEntryStorage = new KeyEntryStorage()) {
this.privateKeyExporter = privateKeyExporter;
this.keyEntryStorage = keyEntryStorage;
}
/**
* Persists the given `privateKey` and `meta` under the given `name`.
* If an entry with the same name already exists rejects the returned
* Promise with {@link PrivateKeyExistsError} error.
*
* @param {string} name - Name of the private key.
* @param {IPrivateKey} privateKey - The private key object.
* @param {Object<string, string>} [meta] - Optional metadata to store with the key.
*
* @returns {Promise<void>}
*/
async store(name, privateKey, meta) {
const privateKeyData = this.privateKeyExporter.exportPrivateKey(privateKey);
try {
await this.keyEntryStorage.save({ name, value: privateKeyData.toString('base64'), meta });
}
catch (error) {
if (error && error.name === 'KeyEntryAlreadyExistsError') {
throw new PrivateKeyExistsError(`Private key with the name ${name} already exists.`);
}
throw error;
}
}
/**
* Retrieves the private key with the given `name` from persistent storage.
* If private with the given name does not exist, resolves the returned
* Promise with `null`.
*
* @param {string} name - Name of the private key to load.
* @returns {Promise<IPrivateKeyEntry|null>}
*/
async load(name) {
const keyEntry = await this.keyEntryStorage.load(name);
if (keyEntry === null) {
return null;
}
const privateKey = this.privateKeyExporter.importPrivateKey(keyEntry.value);
return {
privateKey,
meta: keyEntry.meta
};
}
/**
* Removes the private key entry with the given `name` from persistent
* storage.
*
* @param {string} name - Name of the private key to remove.
* @returns {Promise<void>}
*/
async delete(name) {
await this.keyEntryStorage.remove(name);
}
}
exports.CachingJwtProvider = CachingJwtProvider;
exports.CallbackJwtProvider = CallbackJwtProvider;
exports.CardManager = CardManager;
exports.ConstAccessTokenProvider = ConstAccessTokenProvider;
exports.DefaultStorageAdapter = FileSystemStorageAdapter;
exports.GeneratorJwtProvider = GeneratorJwtProvider;
exports.InvalidKeyEntryError = InvalidKeyEntryError;
exports.Jwt = Jwt;
exports.JwtGenerator = JwtGenerator;
exports.JwtVerifier = JwtVerifier;
exports.KeyEntryAlreadyExistsError = KeyEntryAlreadyExistsError;
exports.KeyEntryDoesNotExistError = KeyEntryDoesNotExistError;
exports.KeyEntryStorage = KeyEntryStorage;
exports.KeyStorage = KeyStorage;
exports.ModelSigner = ModelSigner;
exports.PrivateKeyStorage = PrivateKeyStorage;
exports.RawSignedModel = RawSignedModel;
exports.StorageEntryAlreadyExistsError = StorageEntryAlreadyExistsError;
exports.VirgilAgent = VirgilAgent;
exports.VirgilCardVerifier = VirgilCardVerifier;