Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
## 1.3.0

### Changed

- `IronNode.User.generateDeviceKeys()` now also returns the standard IronCore device fields (`accountId`, `segmentId`, `devicePrivateKey`, `signingPrivateKey`) plus the new device's `deviceId`, `name`, `created`, `lastUpdated`, and `publicSigningKey`. The existing `accountID`, `segmentID`, `deviceKeys`, and `signingKeys` fields are deprecated.
- `IronNode.initialize()` accepts a `DeviceContext` object, such as the result of `generateDeviceKeys()` or a device JSON produced by another IronCore SDK. It also still accepts the pre-1.3.0 `generateDeviceKeys()` result (`accountID`, `segmentID`, `deviceKeys`, `signingKeys`), typed as the deprecated `LegacyDeviceContext`.
- `SDK.user.listDevices()` is typed to include each device's `publicSigningKey`, which the API already returns.
- Added `SDK.user.deleteDeviceByPublicSigningKey()` and `IronNode.User.deleteDeviceByPublicSigningKey()` (JWT authorized) to delete a device given its public signing key.
- Deleting the SDK's own device now ends the session through every delete path: `SDK.user.deleteDevice(id)` with the current device's ID, `SDK.user.deleteDeviceByPublicSigningKey()`, and `IronNode.User.deleteDeviceByPublicSigningKey()`. Previously only `SDK.user.deleteDevice()` with no ID did. `SDK.user.deleteDevice(id)` makes an extra device list request to find out whether `id` is the current device.
- `SDK.user.deleteDevice()` rejects a device ID that is not a positive number. Previously `0` and `null` deleted the current device without ending the session.
- Fixed the type of `name` on devices returned by `SDK.user.listDevices()` to `string | null`. Devices created without a `deviceName` have a `null` name.

## 1.2.0

### Changed
Expand Down
2 changes: 1 addition & 1 deletion examples/.device_keys
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"accountID":"demos@ironcorelabs.com","segmentID":1682,"deviceKeys":{"publicKey":{"x":"flWAeCL79abDGzUKthGWjw/CyDlNL203nNzOLxpi9a8=","y":"aO2LVVaf5w8m7LYN/FNz//daLBQv41ub46IgOUKlXag="},"privateKey":"Gw0rAdswdoiTTPO0v3wAWUDS5JOsjrHINLx2ma9Wf2Y="},"signingKeys":{"publicKey":"YZJbEAkk5MFCbBhyTueeUluruvsmjENdNBW8nEm7lJE=","privateKey":"yRvXk23hSua7YjHUdFoFaHR4qWSOBuWSHyPDMVksWPxhklsQCSTkwUJsGHJO555SW6u6+yaMQ100FbycSbuUkQ=="}}
{"accountId":"demos@ironcorelabs.com","segmentId":1682,"devicePrivateKey":"Gw0rAdswdoiTTPO0v3wAWUDS5JOsjrHINLx2ma9Wf2Y=","signingPrivateKey":"yRvXk23hSua7YjHUdFoFaHR4qWSOBuWSHyPDMVksWPxhklsQCSTkwUJsGHJO555SW6u6+yaMQ100FbycSbuUkQ=="}
2 changes: 1 addition & 1 deletion examples/dek-reuse/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"typescript": "^4.9.5"
},
"dependencies": {
"@ironcorelabs/ironnode": "^1.1.2-pre",
"@ironcorelabs/ironnode": "^1.3.0",
"@types/node": "^20.14.8"
},
"scripts": {
Expand Down
8 changes: 3 additions & 5 deletions examples/dek-reuse/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {SDK, DeviceDetails, initialize} from "@ironcorelabs/ironnode"
import {SDK, DeviceContext, initialize} from "@ironcorelabs/ironnode"
import * as crypto from "crypto"
import * as fs from "fs"

Expand Down Expand Up @@ -70,11 +70,9 @@ async function checkDek(dek: Buffer, fileNum: number) {
(async () => {
const configPath = "../.device_keys"
fs.accessSync(configPath, fs.constants.R_OK)
const deviceKeys: DeviceDetails = JSON.parse(fs.readFileSync(configPath, "utf8"))
const deviceContext: DeviceContext = JSON.parse(fs.readFileSync(configPath, "utf8"))

const ironnode = await initialize(deviceKeys.accountID, deviceKeys.segmentID,
deviceKeys.deviceKeys.privateKey,
deviceKeys.signingKeys.privateKey)
const ironnode = await initialize(deviceContext)

await writeInitialData(ironnode)

Expand Down
2 changes: 1 addition & 1 deletion integration/sdkOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ function askForOperation(IronNode: SDK): Promise<void> {

export function initializeSDKWithLocalDevice() {
const Config = require(path.join(__dirname, "./.device.json"));
return initialize(Config.accountID, Config.segmentID, Config.deviceKeys.privateKey, Config.signingKeys.privateKey)
return initialize(Config)
.then((IronNode) => {
log(IronNode.userContext);
return askForOperation(IronNode);
Expand Down
50 changes: 42 additions & 8 deletions ironnode.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,16 @@ export interface UserUpdateResult {
export interface UserPublicKeyGetResponse {
[userID: string]: PublicKey<string> | null;
}
export interface UserDevice {
id: number;
name: string | null;
created: RFC3339Date;
updated: RFC3339Date;
publicSigningKey: Base64String;
isCurrentDevice: boolean;
}
export interface UserDeviceListResponse {
result: Array<{
id: number;
name: string;
created: string;
updated: string;
isCurrentDevice: boolean;
}>;
result: UserDevice[];
}

export interface Document {
Expand Down Expand Up @@ -168,6 +170,7 @@ export interface User {
getPublicKey(users: string | string[]): Promise<UserPublicKeyGetResponse>;
listDevices(): Promise<UserDeviceListResponse>;
deleteDevice(id?: number): Promise<{id: number}>;
deleteDeviceByPublicSigningKey(publicSigningKey: Base64String): Promise<{id: number}>;
rotateMasterKey(password: string): Promise<{needsRotation: boolean}>;
changePassword(currentPassword: string, newPassword: string): Promise<void>;
disableSelf(): Promise<UserUpdateResult>;
Expand All @@ -191,15 +194,45 @@ export class SDKError extends Error {

export const ErrorCodes: {[key: string]: number};

/** Device keys in the JSON format shared by all IronCore SDKs. */
export interface DeviceContext {
accountId: string;
segmentId: number;
devicePrivateKey: Base64String;
/** Ed25519 expanded private key: the 32 byte seed followed by the 32 byte public key. */
signingPrivateKey: Base64String;
}

/** @deprecated The device format `generateDeviceKeys()` returned before 1.3.0. Use `DeviceContext`. */
export interface LegacyDeviceContext {
accountID: string;
segmentID: number;
deviceKeys: {privateKey: Base64String};
signingKeys: {privateKey: Base64String};
}

export function initialize(deviceContext: DeviceContext): Promise<SDK>;
/** @deprecated Pass a `DeviceContext`. */
export function initialize(legacyDeviceContext: LegacyDeviceContext): Promise<SDK>;
export function initialize(accountID: string, segmentID: number, privateDeviceKey: Base64String, privateSigningKey: Base64String): Promise<SDK>;

export interface DeviceDetails {
export interface DeviceDetails extends DeviceContext {
deviceId: number;
name: string | null;
created: RFC3339Date;
lastUpdated: RFC3339Date;
/** Identifies this device to `deleteDeviceByPublicSigningKey`, matching `UserDevice.publicSigningKey`. */
publicSigningKey: Base64String;
/** @deprecated Use `accountId`. */
accountID: string;
/** @deprecated Use `segmentId`. */
segmentID: number;
/** @deprecated Use `devicePrivateKey`. */
deviceKeys: {
publicKey: PublicKey<Base64String>;
privateKey: Base64String;
};
/** @deprecated Use `signingPrivateKey` and `publicSigningKey`. */
signingKeys: {
publicKey: Base64String;
privateKey: Base64String;
Expand All @@ -211,6 +244,7 @@ export namespace User {
export function create(jwt: string, password: string, options?: UserCreateOptions): Promise<ApiUserResponse>;
export function generateDeviceKeys(jwt: string, password: string, options?: DeviceCreateOptions): Promise<DeviceDetails>;
export function updateStatus(jwt: string, status: (typeof UserStatus)[keyof typeof UserStatus]): Promise<UserUpdateResult>;
export function deleteDeviceByPublicSigningKey(jwt: string, publicSigningKey: Base64String): Promise<{id: number}>;
}

export const UserStatus: {
Expand Down
41 changes: 40 additions & 1 deletion src/api/UserApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ const userDeviceList = (sign: MessageSignature, accountID: string) => ({
* Delete a users device keys
*/
const userDeviceDelete = (sign: MessageSignature, accountID: string, deviceID?: number) => ({
url: `users/${encodeURIComponent(accountID)}/devices/${deviceID || "current"}`,
url: `users/${encodeURIComponent(accountID)}/devices/${deviceID ?? "current"}`,
options: {
method: "DELETE",
headers: {
Expand All @@ -210,6 +210,28 @@ const userDeviceDelete = (sign: MessageSignature, accountID: string, deviceID?:
errorCode: ErrorCodes.USER_DEVICE_DELETE_REQUEST_FAILURE,
});

const userDeviceDeleteBySigningKey = (sign: MessageSignature, accountID: string, publicSigningKey: Base64String) => ({
url: `users/${encodeURIComponent(accountID)}/devices/${encodeURIComponent(publicSigningKey)}`,
options: {
method: "DELETE",
headers: {
Authorization: ApiRequest.getAuthHeader(sign),
},
},
errorCode: ErrorCodes.USER_DEVICE_DELETE_REQUEST_FAILURE,
});

const userDeviceDeleteBySigningKeyWithJwt = (jwt: string, publicSigningKey: Base64String) => ({
url: `users/devices/${encodeURIComponent(publicSigningKey)}`,
options: {
method: "DELETE",
headers: {
Authorization: `jwt ${jwt}`,
},
},
errorCode: ErrorCodes.USER_DEVICE_DELETE_REQUEST_FAILURE,
});

/**
* Update the encrypted private key for the currently logged in user
*/
Expand Down Expand Up @@ -361,6 +383,23 @@ export default {
return ApiRequest.fetchJSON<{id: number}>(url, errorCode, options);
},

/**
* Make request to delete one of the current user's devices given its public signing key.
*/
callUserDeviceDeleteBySigningKeyApi(publicSigningKey: Base64String) {
const {accountID} = ApiState.accountAndSegmentIDs();
const {url, options, errorCode} = userDeviceDeleteBySigningKey(getSignatureHeader(), accountID, publicSigningKey);
return ApiRequest.fetchJSON<{id: number}>(url, errorCode, options);
},

/**
* Make request to delete a device of the user in the JWT given its public signing key.
*/
callUserDeviceDeleteBySigningKeyWithJwtApi(jwt: string, publicSigningKey: Base64String) {
const {url, options, errorCode} = userDeviceDeleteBySigningKeyWithJwt(jwt, publicSigningKey);
return ApiRequest.fetchJSON<{id: number}>(url, errorCode, options);
},

/**
* Update the status of the current SDK user
*/
Expand Down
41 changes: 0 additions & 41 deletions src/api/tests/UserApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,45 +350,4 @@ describe("UserApi", () => {
);
});
});

describe("callUserDeviceDeleteApi", () => {
test("calls delete API and returns response", (done) => {
const deviceDeleteResult = {id: 35352};
(ApiRequest.fetchJSON as jest.Mock).mockReturnValue(Future.of(deviceDeleteResult));

UserApi.callUserDeviceDeleteApi(35352).engage(
(error) => done.fail(error),
(deletedDevice: any) => {
expect(deletedDevice).toEqual(deviceDeleteResult);
expect(ApiRequest.fetchJSON).toHaveBeenCalledWith(
`users/${TestUtils.testAccountID}/devices/35352`,
jasmine.any(Number),
jasmine.any(Object)
);
const request = (ApiRequest.fetchJSON as jest.Mock).mock.calls[0][2];
expect(request.headers.Authorization).toMatch(/IronCore\s{1}\d{1}[.][a-zA-Z0-9=\/+]+[.][a-zA-Z0-9=\/+]+/);
expect(request.method).toEqual("DELETE");
done();
}
);
});

test("deletes the current device if no ID provided", (done) => {
const deviceDeleteResult = {id: 35352};
(ApiRequest.fetchJSON as jest.Mock).mockReturnValue(Future.of(deviceDeleteResult));

UserApi.callUserDeviceDeleteApi().engage(
(error) => done.fail(error),
(deletedDevice: any) => {
expect(deletedDevice).toEqual(deviceDeleteResult);
expect(ApiRequest.fetchJSON).toHaveBeenCalledWith(
`users/${TestUtils.testAccountID}/devices/current`,
jasmine.any(Number),
jasmine.any(Object)
);
done();
}
);
});
});
});
60 changes: 49 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,43 @@
import {DeviceCreateOptions, UserCreateOptions} from "../ironnode";
import {DeviceContext, DeviceCreateOptions, LegacyDeviceContext, SDK, UserCreateOptions} from "../ironnode";
import {Base64String} from "./commonTypes";
import {UserStatus} from "./Constants";
import * as Utils from "./lib/Utils";
import * as Initialization from "./sdk/Initialization";
import * as UserOperations from "./operations/UserOperations";

/**
* Initialize the Node SDK by providing account information necessary to run operations from a server side context. Returns a Promise which
* will resolve with the IronWeb Node SDK object.
* Initialize the Node SDK by providing account information necessary to run operations from a server side context.
* @param {DeviceContext} deviceContext Device keys, such as the result of `User.generateDeviceKeys()`
*/
export function initialize(deviceContext: DeviceContext): Promise<SDK>;
/** @deprecated Pass a `DeviceContext`. */
//tslint:disable-next-line:unified-signatures
export function initialize(legacyDeviceContext: LegacyDeviceContext): Promise<SDK>;
/**
* @param {string} accountID ID of the account to perform actions as
* @param {number} segmentID Internal segment ID the account is part of
* @param {Base64String} privateDeviceKey Base64 encoded device key bytes associated to the account in use
* @param {Base64String} privateSigningKey Base64 encoded signing key bytes associated to the account in use
*/
export function initialize(accountID: string, segmentID: number, privateDeviceKey: Base64String, privateSigningKey: Base64String) {
export function initialize(accountID: string, segmentID: number, privateDeviceKey: Base64String, privateSigningKey: Base64String): Promise<SDK>;
export function initialize(
accountIDOrContext: string | DeviceContext | LegacyDeviceContext,
segmentID?: number,
privateDeviceKey?: Base64String,
privateSigningKey?: Base64String
): Promise<SDK> {
if (typeof accountIDOrContext === "object" && accountIDOrContext !== null) {
if ("accountId" in accountIDOrContext) {
const {accountId, segmentId, devicePrivateKey, signingPrivateKey} = accountIDOrContext;
return initializeFromKeys(accountId, segmentId, devicePrivateKey, signingPrivateKey);
}
const {accountID, segmentID: legacySegmentID, deviceKeys, signingKeys} = accountIDOrContext;
return initializeFromKeys(accountID, legacySegmentID, deviceKeys?.privateKey, signingKeys?.privateKey);
}
return initializeFromKeys(accountIDOrContext, segmentID, privateDeviceKey, privateSigningKey);
}

function initializeFromKeys(accountID: string, segmentID?: number, privateDeviceKey?: Base64String, privateSigningKey?: Base64String) {
Utils.validateID(accountID);
if (typeof segmentID !== "number") {
throw new Error(`Expected a numerical segment ID but instead got '${segmentID}`);
Expand Down Expand Up @@ -45,13 +69,12 @@ export const User = {
},
/**
* Generate a new pair of device keys for the user specified in the provided signed JWT. Takes the users password in order to decrypt their master
* keys and generates a new device key pair as well as a transform key between their master key pair and their device key pair. Returns both the
* device key pair and a signing key pair as base64 encoded strings as well as the users ID and the segment ID for the user. All of these fields can
* then be stored somewhere secure and passed in to run the `initialize` function in order for the current user to interact with the SDK. Also allows
* the call to provide a readable "name" for this device.
* @param {string} jwt Signed JWT for the user to sync
* @param {string} password Password to use to escrow the users private key
* @param {DeviceCreateOptions} options Device create options.
* keys and generates a new device key pair as well as a transform key between their master key pair and their device key pair. Resolves with a
* `DeviceDetails`, a `DeviceContext` plus the new device's ID, name, timestamps, and public signing key. Store it somewhere secure and pass it to
* `initialize`. Also allows the call to provide a readable "name" for this device.
* @param {string} jwt Signed JWT for the user to sync
* @param {string} password Password to use to escrow the users private key
* @param {DeviceCreateOptions} deviceOptions Device create options.
*/
generateDeviceKeys(jwt: string, password: string, deviceOptions: DeviceCreateOptions = {deviceName: ""}) {
return Initialization.generateDevice(jwt, password, deviceOptions).toPromise();
Expand All @@ -72,6 +95,21 @@ export const User = {
}
return UserOperations.updateUserStatus(jwt, status).toPromise();
},
/**
* Delete a device of the user in the provided signed JWT. Does not require an initialized SDK. Deleting the initialized SDK's own device ends
* its session.
* @param {string} jwt Signed JWT for the user who owns the device
* @param {Base64String} publicSigningKey Public signing key of the device to delete, as returned by `SDK.user.listDevices()`
*/
deleteDeviceByPublicSigningKey(jwt: string, publicSigningKey: Base64String) {
if (typeof jwt !== "string" || !jwt.length) {
throw new Error("Expected a non-empty JWT string.");
}
if (typeof publicSigningKey !== "string" || !publicSigningKey.length) {
throw new Error(`Invalid public signing key provided. Expected a non-empty base64 string but got ${publicSigningKey}`);
}
return UserOperations.deleteUserDeviceByPublicSigningKeyWithJwt(jwt, publicSigningKey).toPromise();
},
};

/**
Expand Down
Loading