@iotize/tap
Version:
IoTize Device client for Javascript
1,139 lines (1,123 loc) • 49.3 kB
JavaScript
import { TapError, TapResponse, ServiceCallRunner, TapResponseStatusError, defineTapPropertyExtension } from '@iotize/tap';
import '@iotize/tap/service/impl/group';
import { CodeError, isCodeError } from '@iotize/common/error';
import { ResultCode } from '@iotize/tap/client/api';
import { throwError, defer, BehaviorSubject } from 'rxjs';
import '@iotize/tap/service/impl/interface';
import { createDebugger } from '@iotize/common/debug';
import { hexStringToBuffer, bufferToHexString } from '@iotize/common/byte-converter';
import { passwordHasher, pbkdf2, hmacSHA256 } from '@iotize/tap/crypto';
import { AesEcb128Converter } from '@iotize/common/crypto';
import { deepCopy } from '@iotize/common/utility';
import { CryptedFrameConverter, TapClientError, TapRequestHelper, TapStreamWriter, StringConverter } from '@iotize/tap/client/impl';
import { ScramService } from '@iotize/tap/service/impl/scram';
import { map, tap, shareReplay, mergeMap } from 'rxjs/operators';
import { KaitaiStreamWriter } from '@iotize/common/byte-stream';
const prefix = '@iotize/tap/auth';
const debug = createDebugger(prefix);
const INITIAL_SESSION_STATE = {
groupId: 0,
lifeTime: -1,
name: 'anonymous',
startTime: undefined,
profileId: 0,
profileName: 'anonymous',
};
const PASSWORD_LENGTH = 16;
function hashLoginPassword(password) {
return hexStringToBuffer(passwordHasher.hash(password).substring(0, PASSWORD_LENGTH * 2));
}
function XOR(value1, value2) {
if (value1.length !== value2.length) {
throw new Error('Length does not match between the two array. Cannot compute XOR');
}
const result = new Uint8Array(value1.length);
for (let i = 0; i < result.length; i++) {
result[i] = value1[i] ^ value2[i];
}
return result;
}
var __awaiter$4 = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
const TAG$3 = 'BasicAuth';
class BasicAuth {
constructor(tap, options = {
hashPassword: false,
}) {
this.tap = tap;
this.options = options;
}
get hashPassword() {
var _a;
return ((_a = this.options) === null || _a === void 0 ? void 0 : _a.hashPassword) || false;
}
/**
* Tap login
*
*/
login(params) {
return __awaiter$4(this, void 0, void 0, function* () {
const { username, password } = params;
let response;
if (this.hashPassword) {
debug(TAG$3, `Using hash password login`);
response = yield this.tap.service.interface.loginWithHash({
username,
password: hashLoginPassword(password),
});
}
else {
debug(TAG$3, `Using regular login`);
response = yield this.tap.service.interface.login({
username,
password,
});
}
response.successful();
return {};
});
}
logout() {
return __awaiter$4(this, void 0, void 0, function* () {
(yield this.tap.service.interface.logout()).successful();
});
}
/**
* Change password for the groupId
* @param newPassword the new password for the userId
* @param userId the user id for which we want to change the password
*/
changePassword(newPassword, userId) {
return __awaiter$4(this, void 0, void 0, function* () {
if (this.hashPassword) {
const passwordBytes = hashLoginPassword(newPassword);
(yield this.tap.service.group.changePasswordKey(userId, passwordBytes)).successful();
}
else {
(yield this.tap.service.group.changePassword(userId, newPassword)).successful();
}
});
}
}
class TapScramError extends TapError {
constructor(code, message, request) {
super(code, message, undefined);
this.request = request;
}
static scramNotStartedYet(failedRequest) {
return new TapScramError(TapError.Code.ScramNotStartedYet, `SCRAM session has not been initialized yet. You cannot use secure communication.`, failedRequest);
}
static invalidScramKey(failedRequest) {
return new TapScramError(TapError.Code.InvalidScramKey, `SCRAM session key is not valid.`, failedRequest);
}
}
var __awaiter$3 = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
const INITIALIZATION_VECTOR_LENGTH = 16;
const DEFAULT_INITIALIZATION_VECTOR = '00000000000000000000000000000000';
const TAG$2 = 'ScramInterceptor';
function _hex(data) {
return data ? bufferToHexString(data) : '';
}
class ScramInterceptor {
constructor(scramService) {
this.scramService = scramService;
this._ivFrameCounter = 0;
this._ivSupported = true;
this._options = {
encryption: false,
initializationVectorResetPeriod: 100,
keys: {},
frameCounter: 0,
};
this._encryptedFrameConverter = new CryptedFrameConverter({
next: () => {
return this._options.frameCounter++;
},
});
this.initializationVectorGenerator = (length) => {
const data = new Array(length).fill(0).map((_) => {
return Math.floor(Math.random() * 0xff);
});
return Uint8Array.from(data);
};
}
get encryptionAlgo() {
if (!this._encryptionAlgo) {
throw TapClientError.illegalStateError(`Encryption algo has not been set yet`);
}
return this._encryptionAlgo;
}
set ivSupported(v) {
this._ivSupported = v;
}
/**
* Setter for the session key
* @param key if null, it will stop encryption and remove session key. If true it will update session key used for encryption
*/
set sessionKey(key) {
this._options.keys.sessionKey = key;
this._options.frameCounter = 0;
if (this._options.keys.sessionKey) {
this.refreshEncryptionAlgo();
}
else {
this.pauseEncryption();
}
}
get sessionKey() {
return this._options.keys.sessionKey;
}
/**
* Get a copy of encryption options
*/
get options() {
return deepCopy(this._options);
}
setEncryptionKeys(options) {
this._options.keys = options;
this.refreshEncryptionAlgo();
}
getEncryptionKeys() {
return this._options.keys;
}
setFrameCounter(value) {
this._options.frameCounter = value;
}
setInitializationVectorRefreshPeriod(period) {
this._options.initializationVectorResetPeriod = period;
}
intercept(context, next) {
const request = context.request;
debug(TAG$2, `exec ${TapRequestHelper.toString(request)} (encryption:${this._options.encryption}, frameCounter: ${this._options.frameCounter} - skip ${context.skipEncryption}, key: ${_hex(this.sessionKey)} iv decode: ${_hex(this._options.keys.ivDecode)}, iv encode: ${_hex(this._options.keys.ivEncode)})`);
let obs;
if (this._options.encryption && !context.skipEncryption) {
obs = this._sendWithEncryption(context, next).pipe(map((response) => {
const data = response.rawBody();
return {
status: response.status,
data,
};
}));
}
else {
obs = next.handle(context);
}
return obs.pipe(tap((response) => {
debug(TAG$2, `Response to ${TapRequestHelper.toString(request)} => ${response.status} ${bufferToHexString(response.data)}`);
}));
}
_sendWithEncryption(context, next) {
if (!this._encryptedFrameConverter) {
return throwError(TapClientError.illegalStateError(`Encrypted frame converter has not been specified yet`));
}
let callObs;
let newClientIV;
if (this._options.initializationVectorResetPeriod === 1) {
newClientIV = this.initializationVectorGenerator(INITIALIZATION_VECTOR_LENGTH);
this._setEncodeIV(newClientIV);
debug(TAG$2, `Changing initialization vector for every requests. New client IV: ${_hex(newClientIV)}`);
const cryptedFrame = this._buildEncryptedFrame(context);
const call = this.scramService.sendWithIVCall({
request: cryptedFrame,
iv: newClientIV,
});
callObs = this._toCallObservable(Object.assign(Object.assign({}, context), { skipEncryption: true }), next, call).pipe(map((response) => {
// if (response.isSuccessful() && response.rawBody().length < INITIALIZATION_VECTOR_LENGTH){
// // tap firmware version is too old apparentl.
// // CCOM IV resource was already used to generate rand number before firmware version 1.83, thats why there is a success code result
// throw TapError.initializationVectorNotSupported(
// new Error(`Tap response is too short`)
// );
// }
const body = response.body();
debug(TAG$2, `Refreshing decoding initialization vector: ${_hex(body.iv)}`);
this._setDecodeIV(body.iv);
return TapResponse.create(response.status, body.response);
}));
}
else {
if (this._options.initializationVectorResetPeriod > 1 &&
this._ivFrameCounter >= this._options.initializationVectorResetPeriod &&
!isRefreshInitializationVectorRequest(context.request)) {
debug(TAG$2, `_ivInterceptor refreshEncryptionInitializationVector is required ${this._ivFrameCounter}/${this._options.initializationVectorResetPeriod}`);
if (!this._refreshingInitializationVectorObs) {
this._refreshingInitializationVectorObs = defer(() => __awaiter$3(this, void 0, void 0, function* () {
try {
return yield this.refreshInitializationVectors();
}
finally {
this._refreshingInitializationVectorObs = undefined;
}
})).pipe(shareReplay({ bufferSize: 1, refCount: true }));
}
else {
// debug(TAG, `Refreshing initialization vector has already been asked`);
}
callObs = this._refreshingInitializationVectorObs.pipe(mergeMap((_) => {
const encryptedFrame = this._buildEncryptedFrame(context);
return this._toCallObservable(context, next, this.scramService.sendCall(encryptedFrame));
}));
}
else {
const encryptedFrame = this._buildEncryptedFrame(context);
const call = this.scramService.sendCall(encryptedFrame);
callObs = this._toCallObservable(context, next, call);
}
}
return callObs.pipe(map((encryptedWarpperResponse) => {
this._ivFrameCounter++;
// debug(TAG, `Iotize client crypted frame: ${encryptedWarpperResponse}`);
if (!encryptedWarpperResponse.isSuccessful()) {
if (encryptedWarpperResponse.codeRet() ===
ResultCode.SERVICE_UNAVAILABLE) {
throw TapScramError.scramNotStartedYet(context.request);
}
else if (encryptedWarpperResponse.codeRet() === ResultCode.BAD_REQUEST) {
throw TapScramError.invalidScramKey(context.request);
}
encryptedWarpperResponse.successful();
}
const cryptedFrameContent = this.encryptionAlgo.decode(encryptedWarpperResponse.rawBody());
// debug(TAG, `Decrypted frame value: ${_hex(cryptedFrameContent)}`);
const apduFrame = this._encryptedFrameConverter.decode(cryptedFrameContent);
// debug(TAG, `apduFrame frame value: ${_hex(apduFrame)}`);
const tapResponse = context.client.responseDecoder.decode(apduFrame);
const apiResponse = new TapResponse(tapResponse, context.request);
// let response: TapResponse<any> = this.lwm2mResponseConverter.decode(lwm2mResponseFrame.data);
if (context.bodyDecoder) {
apiResponse.setBodyDecoder(context.bodyDecoder);
}
// debug(TAG, `Iotize client decoded response: ${response}`)
return apiResponse;
}));
}
_buildEncryptedFrame(context) {
const iotizeFrame = TapStreamWriter.create(10 + context.request.payload.length).writeTapRequestFrame(context.request).toBytes;
const encryptedFrameModel = this._encryptedFrameConverter.encode(iotizeFrame);
const encryptedFrame = this.encryptionAlgo.encode(encryptedFrameModel);
return encryptedFrame;
}
_setEncodeIV(iv) {
this._options.keys.ivEncode = iv;
this.refreshEncryptionAlgo();
}
_setDecodeIV(iv) {
this._options.keys.ivDecode = iv;
this.refreshEncryptionAlgo();
}
_toCallObservable(context, next, call) {
const tapRequest = ServiceCallRunner.toTapRequest(call);
const bodyDecoderFct = ServiceCallRunner.resolveResponseBodyDecoder(call);
const bodyDecoder = bodyDecoderFct
? {
decode: bodyDecoderFct,
}
: undefined;
return next
.handle(Object.assign(Object.assign({}, context), { request: tapRequest, bodyDecoder }))
.pipe(map((tapResponse) => {
return new TapResponse(tapResponse, context.request, bodyDecoder);
}));
}
/**
* Refresh initialization vectors
*/
refreshInitializationVectors() {
return __awaiter$3(this, void 0, void 0, function* () {
const clientIV = this.initializationVectorGenerator(INITIALIZATION_VECTOR_LENGTH);
try {
debug(TAG$2, `Performing initialization vector refresh`);
const serverIV = (yield this.scramService.setInitializationVector(clientIV)).body();
this._options.keys.ivEncode = clientIV;
this._options.keys.ivDecode = serverIV;
this._ivFrameCounter = 0;
debug(TAG$2, `Initialization vector refreshed: server ${_hex(serverIV)} client=${_hex(clientIV)})`);
this.refreshEncryptionAlgo();
}
catch (err) {
debug(TAG$2, `Cannot refresh initialization vector: ${err.message}`);
if (err instanceof TapResponseStatusError) {
const tapResponseStatus = err.response.status;
if (tapResponseStatus === ResultCode.NOT_IMPLEMENTED ||
tapResponseStatus === ResultCode.NOT_FOUND) {
debug(TAG$2, `Cannot initialize encryption vectors (firmware version does not support it). Error: ${err.message}`);
this._ivSupported = false;
throw TapError.initializationVectorNotSupported(err);
}
}
throw err;
}
});
}
/**
* Resume encryption session
*/
resumeEncryption() {
debug(TAG$2, `resume encryption`);
this._options.encryption = true;
}
/**
* Pause encryption without destorying session (session keys will not be removed)
*/
pauseEncryption() {
debug(TAG$2, `pause encryption`);
this._options.encryption = false;
}
/**
* Initialize a new encrypted sesssion
* Session key will be changed. Initialization vector too (if firmware supports it)
*/
newSession() {
return __awaiter$3(this, void 0, void 0, function* () {
try {
debug(TAG$2, `Creating new encryption session`);
this.clearSession();
const response = yield this.scramService.initialize();
response.successful();
const newSessionKey = response.body();
this.sessionKey = newSessionKey;
debug(TAG$2, `Session key will be ${_hex(newSessionKey)} (length=${newSessionKey.length})`);
if (this._options.initializationVectorResetPeriod > 0) {
try {
yield this.refreshInitializationVectors();
}
catch (err) {
// Ignore initialization vector not supported error
if (err.code !==
TapError.Code.InitializationVectorNotSupported) {
throw err;
}
}
}
return newSessionKey;
}
catch (err) {
throw TapError.cannotStartScram(err);
}
});
}
/**
* Clear encrypted session.
* Destroys frame counter, session keys and initialization vectors
* This will also stop encrypted communication if it was running
*/
clearSession() {
this.pauseEncryption();
this._options.keys.ivDecode = undefined;
this._options.keys.ivEncode = undefined;
this._options.keys.sessionKey = undefined;
this._ivFrameCounter = 0;
}
refreshEncryptionAlgo() {
if (this._options.keys.sessionKey) {
const algo = new AesEcb128Converter({
key: _hex(this._options.keys.sessionKey),
ivDecode: this._options.keys.ivDecode
? _hex(this._options.keys.ivDecode)
: DEFAULT_INITIALIZATION_VECTOR,
ivEncode: this._options.keys.ivEncode
? _hex(this._options.keys.ivEncode)
: DEFAULT_INITIALIZATION_VECTOR,
});
this._encryptionAlgo = algo;
}
}
}
const SCRAM_CALL_FACTORY = new ScramService(undefined);
function isRefreshInitializationVectorRequest(request) {
return (TapRequestHelper.pathToString(request.header.path) ===
SCRAM_CALL_FACTORY.resources.setInitializationVector.path);
}
var __awaiter$2 = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
class TapEncryption {
// private _sessionState: BehaviorSubject<EncryptionKeys>;
constructor(tap) {
this.tap = tap;
this.scramInterceptor = new ScramInterceptor(this.tap.service.scram);
this.tap.client.addInterceptor(this.scramInterceptor);
// this._sessionState = new BehaviorSubject<EncryptionKeys>(this._scramInterceptor.options.keys);
this.tap.events.subscribe((event) => {
switch (event.type) {
// Stop encryption on tap logout
case 'tap-logout':
this.stop();
break;
}
});
}
get isStarted() {
return this.scramInterceptor.options.encryption;
}
setEncryptionKeys(keys) {
this.scramInterceptor.setEncryptionKeys(keys);
}
setEncryptedFrameCounter(frameCounter) {
this.scramInterceptor.setFrameCounter(frameCounter);
}
getEncryptionOptions() {
return this.scramInterceptor.options;
}
refreshEncryptionInitializationVector() {
return this.scramInterceptor.refreshInitializationVectors();
}
setInitializationVectorRefreshPeriod(period) {
return this.scramInterceptor.setInitializationVectorRefreshPeriod(period);
}
// /**
// * Enable/Disable encrytion when communicating with a device
// * @param enable true if requests must be encrypted
// * @param resetSessionKey true if you want to reset the the session key.
// * @param refreshInitializationVector true if you want to change initialization vectors
// *
// * @deprecated use {@link encryption()} instead
// */
// public async enableEncryption(
// enable: boolean,
// resetSessionKey = false,
// refreshInitializationVector = false
// ): Promise<void> {
// return this.encryption(
// enable,
// resetSessionKey,
// refreshInitializationVector
// );
// }
// /**
// * Enable/Disable encrytion when communicating with a device
// * @param enable true if requests must be encrypted
// * @param resetSessionKey true if you want to reset the the session key.
// * @param refreshInitializationVector true if you want to change initialization vectors
// *
// */
// public async encryption(
// enable: boolean,
// resetSessionKey = false,
// refreshInitializationVector = false
// ): Promise<void> {
// await this.scramInterceptor.encryption(
// enable,
// resetSessionKey,
// refreshInitializationVector
// );
// }
/**
* Enable encryption
*/
start() {
return __awaiter$2(this, void 0, void 0, function* () {
const keys = yield this.scramInterceptor.newSession();
this.scramInterceptor.resumeEncryption();
return keys;
});
}
/**
* Disable encryption
*/
stop() {
this.scramInterceptor.clearSession();
}
/**
* Pause encryption
*/
pause() {
this.scramInterceptor.pauseEncryption();
}
/**
* Pause encryption
*/
resume() {
this.scramInterceptor.resumeEncryption();
}
/**
* Setter for the session key
* @param key if null, it will stop encryption and remove session key. If true it will update session key used for encryption
*/
set sessionKey(key) {
this.scramInterceptor.sessionKey = key;
}
/**
* Get current session key for encrypted communicatioin
*/
get sessionKey() {
return this.scramInterceptor.sessionKey;
}
}
const ɵ0$1 = (context) => new TapEncryption(context.tap);
const _TAP_EXTENSION_ENCRYPTION_ = defineTapPropertyExtension('encryption', ɵ0$1);
class TapAuthError extends CodeError {
constructor(code, message, cause) {
super(message, code);
this.cause = cause;
}
static tapRequestError(params, err) {
return new TapAuthError(TapAuthError.Code.TapRequestError, `Login failed. ${err.message}`, err);
}
static tooManyLoginFailedError(params, err) {
return new TapAuthError(TapAuthError.Code.TooManyLoginAttempt, `Login failed due to too many login attempt. You must wait before trying to login again.`, err);
}
static invalidCredentialsError(credentials, err) {
return new TapAuthError(TapAuthError.Code.InvalidCredentials, `Login failed. Given username or password is not valid.`, err);
}
}
(function (TapAuthError) {
let Code;
(function (Code) {
Code["InvalidServerProof"] = "TapAuthErrorInvalidServerProof";
Code["ScramDisabled"] = "TapAuthErrorScramDisabled";
Code["InvalidCredentials"] = "TapAuthErrorInvalidCredentials";
Code["TooManyLoginAttempt"] = "TapAuthErrorTooManyLoginAttempt";
Code["TapRequestError"] = "TapAuthErrorTapRequestError";
})(Code = TapAuthError.Code || (TapAuthError.Code = {}));
class InvalidServerKey extends TapAuthError {
constructor(deviceServerProof, expectedServerProof) {
super(Code.InvalidServerProof, `Login fail invalid server proof ('${bufferToHexString(deviceServerProof)}' != '${bufferToHexString(expectedServerProof)}') `);
this.deviceServerProof = deviceServerProof;
this.expectedServerProof = expectedServerProof;
}
}
TapAuthError.InvalidServerKey = InvalidServerKey;
})(TapAuthError || (TapAuthError = {}));
var __awaiter$1 = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
const TAG$1 = 'Scram';
function DEFAULT_SCRAM_SALT_GENERATOR() {
const data = new Array(ScramAuth.USER_SALT_SIZE).fill(0).map((_) => {
return Math.floor(Math.random() * 0xff);
});
// To be 100% sure that salt will never be [0,0,0,0] which will disable login for the user
if (data[0] === 0) {
data[0] = 1;
}
return Uint8Array.from(data);
}
class ScramAuth {
// protected _sessionState!: BehaviorSubject<SessionState>;
// /**
// * Listen to session state changed
// */
// public get sessionState(): Observable<SessionState> {
// return this._sessionState.asObservable();
// }
// /**
// * Get current session state
// */
// public get sessionStateSnapshot(): SessionState {
// return this._sessionState.value;
// }
constructor(tap) {
this.tap = tap;
this.nonceGenerator = () => {
return Math.floor(Math.random() * 0xffffffff);
};
this.saltGenerator = DEFAULT_SCRAM_SALT_GENERATOR;
// this.sessionData = {};
}
// async changeCurrentUserPassword(newPassword: string): Promise<void> {
// let groupId = (await this.device.service.interface.getCurrentGroupId()).body()!;
// if (!this.sessionData.options || !groupId){
// throw new AuthError(AuthError.Code.NOT_LOGGED_IN, 'Cannot change password, your are not logged in');
// }
// let newPasswordKey: Uint8Array = ScramAuth.createScramPasswordKey(newPassword, this.sessionData.options);
// (await this.device.service.group.changePasswordKey(groupId, newPasswordKey)).successful();
// }
// /**
// * Enable/Disable scram for this tap
// * @param enabled
// */
// async enable(enabled = true): Promise<void> {
// const lockInfo = (await this.device.service.interface.getSecurityOptions()).body();
// if (lockInfo.scramActivated === enabled) {
// return;
// }
// lockInfo.scramActivated = enabled;
// if (enabled) {
// lockInfo.hashPassword = true;
// }
// (await this.device.service.interface.putSecurityOptions(lockInfo)).successful();
// }
changePassword(newPassword, groupId, salt = this.saltGenerator()) {
return __awaiter$1(this, void 0, void 0, function* () {
const options = {
iterationNumber: (yield this.tap.service.scram.getHashIteration()).body(),
salt,
};
const newPasswordKey = ScramAuth.createScramPasswordKey(newPassword, options);
debug(TAG$1, `Changing password key: ${bufferToHexString(newPasswordKey)} salt=${bufferToHexString(salt)}; iteration=${options.iterationNumber} for user id "${groupId}"`);
(yield this.tap.service.group.changePasswordKey(groupId, newPasswordKey)).successful();
});
}
/**
* Perform login
*
* @param params
*
* @throws Error if scram is not activated
*/
login(params) {
return __awaiter$1(this, void 0, void 0, function* () {
const clientNonce = this.generateNonce();
debug(TAG$1, 'clientNonce', clientNonce);
const loginParams = {
username: params.username,
clientNonce,
};
const loginBody = (yield this.tap.service.scram.login(loginParams)).body();
const keys = ScramAuth.computeKeys(params, loginBody, clientNonce);
const sessionData = {};
sessionData.storedKey = keys.storedKey;
sessionData.serverKey = keys.serverKey;
const serverNonce = loginBody.serverNonce;
const deviceServerProof = (yield this.tap.service.scram.loginProof(keys.clientProof)).body();
const expectedServerProof = keys.serverProof;
// TODO maybe we can find something better to test array equals ...
if (bufferToHexString(expectedServerProof) !==
bufferToHexString(deviceServerProof)) {
throw new TapAuthError.InvalidServerKey(expectedServerProof, deviceServerProof);
}
sessionData.clientNonce = clientNonce;
sessionData.key = ScramAuth.computeSessionKey(clientNonce, serverNonce, loginBody.salt, keys.serverKey, keys.storedKey);
return sessionData;
});
}
static createScramPasswordKey(newPassword, options) {
const keys = ScramAuth.computeBaseKeys(newPassword, options);
const saltCopy = new Uint8Array(options.salt);
const buffer = KaitaiStreamWriter.create(ScramAuth.SCRAM_PASSWORD_LENGTH);
buffer
.writeBytes(keys.storedKey, ScramAuth.KEY_SIZE)
.writeBytes(keys.serverKey, ScramAuth.KEY_SIZE)
.writeBytes(saltCopy, ScramAuth.USER_SALT_SIZE);
return buffer.toBytes;
}
static computeBaseKeys(password, options) {
const hashedPassword = hashLoginPassword(password);
// debug(TAG, 'ScramAuth', 'hashedPassword', bufferToHexString(hashedPassword));
const saltedPassword = ScramAuth.saltedPassword(hashedPassword, options.salt, options.iterationNumber);
// debug(TAG, 'ScramAuth', 'saltedPassword', bufferToHexString(saltedPassword));
// let clientKey: Uint8Array = ScramAuth.clientKey(saltedPassword);
// debug(TAG, 'ScramAuth', 'clientKey', bufferToHexString(clientKey));
const storedKey = ScramAuth.storedKey(saltedPassword);
debug(TAG$1, 'storeKey', bufferToHexString(storedKey));
const serverKey = ScramAuth.serverKey(saltedPassword);
debug(TAG$1, 'serverKey', bufferToHexString(serverKey));
return {
hashedPassword,
saltedPassword,
storedKey,
serverKey,
};
}
static computeKeys(credentials, loginBody, clientNonce) {
const keys = ScramAuth.computeBaseKeys(credentials.password, loginBody);
const clientProof = ScramAuth.clientProof(keys.storedKey, clientNonce, loginBody.serverNonce);
// debug(TAG, 'ScramAuth', 'clientProof', bufferToHexString(clientProof));
const serverProof = ScramAuth.serverProof(keys.serverKey, clientNonce, loginBody.serverNonce);
// debug(TAG, 'ScramAuth', 'serverProof', bufferToHexString(serverProof));
return Object.assign(Object.assign({}, keys), { clientProof,
serverProof });
}
logout() {
return __awaiter$1(this, void 0, void 0, function* () {
try {
const response = yield this.tap.service.interface.logout();
response.successful();
this.tap.encryption.stop();
}
catch (err) {
if (isCodeError(TapError.Code.ScramNotStartedYet, err) ||
isCodeError(TapError.Code.InvalidScramKey, err)) {
// ignore error
this.tap.encryption.stop();
return;
}
throw err;
}
});
}
static clientProof(storedKey, clientNonce, serverNonce) {
return ScramAuth.computeProof(storedKey, clientNonce, serverNonce).subarray(0, ScramAuth.KEY_SIZE);
}
static serverProof(serverKey, clientNonce, serverNonce) {
return ScramAuth.computeProof(serverKey, serverNonce, clientNonce).subarray(0, ScramAuth.KEY_SIZE);
}
// getSessionKey(): Uint8Array {
// if (!this.sessionData.key) {
// throw new Error('Session not started');
// }
// return this.sessionData.key;
// }
generateNonce() {
return this.nonceGenerator();
}
/**
* SaltedPwd = PBKDF2 ( HashedPassword, UserSalt, ItCnt )
* @param password
* @param userSalt
* @param iteration
*/
static saltedPassword(hashedPassword, userSalt, iterations) {
return pbkdf2(hashedPassword, userSalt, iterations);
}
/**
* ClientKey = HMAC ( SaltedPwd | « ClientKey »)
* @param saltedPassword
*/
// public static clientKey(saltedPassword: InputDataType): Uint8Array{
// let encodedLabel = ScramAuth.encodeLabel(ScramAuth.CLIENT_KEY_LABEL);
// let encodedLabel2 : Uint8Array = KaitaiStreamWriter.mergeArrays(
// encodedLabel,
// Uint8Array.from([0])
// ).data;
// // debug(TAG, 'ScramAuth', 'clientKey()', 'data => ', bufferToHexString(data), 'len', data.length, `(${saltedPassword.length} + ${encodedLabel.length})`);
// return ScramAuth.HMAC(
// saltedPassword,
// // userSalt
// encodedLabel2
// ).subarray(0, ScramAuth.KEY_SIZE);
// }
/**
* StoredKey = H ( ClientKey )
* @param saltedPassword
*/
static storedKey(saltedPassword) {
return ScramAuth.HASH(saltedPassword, ScramAuth.CLIENT_KEY_LABEL, ScramAuth.CLIENT_KEY_ITERATION_NUMBER);
}
static serverKey(saltedPassword) {
return ScramAuth.HASH(saltedPassword, ScramAuth.SERVER_KEY_LABEL, ScramAuth.SERVER_KEY_ITERATION_NUMBER);
}
static HASH(key, label, iteration) {
return pbkdf2(key, label, iteration, ScramAuth.KEY_SIZE / 4);
}
/**
* ClientSignature = HMAC ( StoredKey | ClientNonce | ServerNonce )
* @param key
* @param nonce1
* @param nonce2
*/
static computeProof(key, nonce1, nonce2) {
const buffer = KaitaiStreamWriter.create(key.length + ScramAuth.CLIENT_NONCE_SIZE + ScramAuth.SERVER_NONCE_SIZE);
buffer
.writeU(nonce1, ScramAuth.CLIENT_NONCE_SIZE)
.writeBytes(key)
.writeU(nonce2, ScramAuth.SERVER_NONCE_SIZE);
return hmacSHA256(buffer.toBytes, key);
}
/**
* ClientProofCheck = StoredKey ^ ClientProof
* @param storedKey
* @param clientProof
*/
static clientProofCheck(storedKey, clientProof) {
return XOR(storedKey, clientProof);
}
/**
* CommunicationKey = H ( ClientNonce | ServerNonce | StoredKey | « CommunicationKey » )
* @param clientNonce
* @param serverNonce
* @param storedKey
*/
static computeSessionKey(clientNonce, serverNonce, userSalt, serverKey, storedKey) {
const encodedLabel = ScramAuth.encodeLabel(ScramAuth.COMMUNICATION_KEY_LABEL);
const buffer = KaitaiStreamWriter.create(ScramAuth.CLIENT_NONCE_SIZE +
ScramAuth.SERVER_NONCE_SIZE +
userSalt.length +
serverKey.length +
storedKey.length);
buffer
.writeU(clientNonce, ScramAuth.CLIENT_NONCE_SIZE)
.writeBytes(serverKey)
.writeBytes(userSalt)
.writeBytes(storedKey)
.writeU(serverNonce, ScramAuth.SERVER_NONCE_SIZE);
// .add(encodedLabel)
return hmacSHA256(buffer.toBytes, serverKey).subarray(0, ScramAuth.KEY_SIZE);
}
/**
*
* @param input
*/
static encodeLabel(input) {
return this.stringConverter.encode(input);
}
}
ScramAuth.CRC_LENGTH = 4;
ScramAuth.CLIENT_NONCE_SIZE = 4;
ScramAuth.SERVER_NONCE_SIZE = 4;
ScramAuth.ITERATION_NUMBER_SIZE = 4;
// public sessionData: ScramAuth.SessionData;
ScramAuth.COMMUNICATION_KEY_LABEL = 'CommunicationKey';
// static CLIENT_PROOF_LABEL: string = "ClientProof";
ScramAuth.CLIENT_KEY_LABEL = 'ClientKey';
ScramAuth.SERVER_KEY_LABEL = 'ServerKey';
ScramAuth.KEY_SIZE = 16;
ScramAuth.CLIENT_KEY_ITERATION_NUMBER = 2;
ScramAuth.SERVER_KEY_ITERATION_NUMBER = 2;
ScramAuth.USER_SALT_SIZE = 4;
ScramAuth.SCRAM_PASSWORD_LENGTH = ScramAuth.KEY_SIZE * 2 + ScramAuth.USER_SALT_SIZE;
ScramAuth.stringConverter = StringConverter.ascii();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
const TAG = 'TapAuth';
class TapAuth {
constructor(tap) {
this.tap = tap;
this._sessionState = new BehaviorSubject(INITIAL_SESSION_STATE);
this._sessionData = new BehaviorSubject({});
}
/**
* Get current session state snapshot
*/
get sessionStateSnapshot() {
return this._sessionState.value;
}
/**
* Listen to session state changed
*/
get sessionState() {
return this._sessionState.asObservable();
}
/**
* Get current session data snapshot
*/
get sessionDataSnapshot() {
return this._sessionData.value;
}
/**
* Listen to session data changed
*/
get sessionData() {
return this._sessionData.asObservable();
}
get service() {
return this.tap.service;
}
setAuthMethod(method) {
this._auth = method;
}
/**
* Clear auth cache.
* Usefull for example if Tap configuration has changed
*/
clearCache() {
this._auth = undefined;
}
// async test() {
// const lockInfo = await this.getLockInfo();
// if (userPassword) {
// if (currentUserId === undefined) {
// currentUserId = (await this.service.interface.getCurrentGroupId()).body();
// }
// const currentUsername = (await this.tap.service.group.getName(
// currentUserId!
// )).body()!;
// await this.login(currentUsername, userPassword, false);
// }
// }
login(params, options) {
return __awaiter(this, void 0, void 0, function* () {
debug(TAG, `Tap login as ${params.username}...`);
const auth = yield this.setupTapAuthEngineIfRequired();
try {
const sessionData = yield auth.login(params);
if (sessionData && sessionData.key) {
this.tap.encryption.sessionKey = sessionData.key;
this.tap.encryption.resume();
}
this._sessionData.next(sessionData);
if (!(options === null || options === void 0 ? void 0 : options.noRefreshSessionState)) {
yield this.refreshSessionState({
username: params.username,
});
}
const event = {
type: 'tap-login',
};
this.tap.notifyEvent(event);
return sessionData;
}
catch (err) {
if (isCodeError(TapError.Code.ResponseStatusError, err)) {
if (err.response.status === ResultCode.NOT_ACCEPTABLE ||
err.response.status === ResultCode.UNAUTHORIZED) {
throw TapAuthError.invalidCredentialsError(params, err);
}
else if (err.response.status === ResultCode.SERVICE_UNAVAILABLE) {
throw TapAuthError.tooManyLoginFailedError(params, err);
}
}
throw TapAuthError.tapRequestError(params, err);
}
});
}
/**
* Tap logout
* Reject if logout failed
*/
logout() {
return __awaiter(this, void 0, void 0, function* () {
debug(TAG, `Tap logout...`);
const auth = yield this.setupTapAuthEngineIfRequired();
yield auth.logout();
// we keep session keys even after logout
this._sessionState.next(INITIAL_SESSION_STATE);
this._sessionData.next({});
const event = {
type: 'tap-logout',
};
this.tap.notifyEvent(event);
});
}
/**
* Read session state from the Tap
* This will perform a few tap requests
*
* TODO we should invalidate encryption key if session state changed
*/
refreshSessionState(defaults) {
return __awaiter(this, void 0, void 0, function* () {
debug(TAG, '[TAP]', `Refreshing session state (current=${this.sessionStateSnapshot.name})...`);
const [groupIdResponse, profileIdResponse] = yield this.service.interface.executeMultipleCalls([
this.service.interface.getCurrentGroupIdCall(),
this.service.interface.getCurrentProfileIdCall(),
]);
const groupId = groupIdResponse.body();
const profileId = profileIdResponse.body();
if (groupId !== this.sessionStateSnapshot.groupId) {
const currentSessionState = Object.assign({}, this.sessionStateSnapshot);
currentSessionState.groupId = groupId;
currentSessionState.profileId = profileId;
const [sessionLifetimeResponse, userNameResponse, profileNameResponse] = yield this.service.interface.executeMultipleCalls([
this.service.group.getSessionLifetimeCall(groupId),
this.service.group.getNameCall(groupId),
this.service.group.getNameCall(profileId),
]);
currentSessionState.name = (defaults === null || defaults === void 0 ? void 0 : defaults.username) || userNameResponse.body();
currentSessionState.profileName = profileNameResponse.body();
currentSessionState.lifeTime = sessionLifetimeResponse.body();
debug(TAG, '[TAP]', `Notifying new session state: ${currentSessionState.name}`);
this._sessionState.next(currentSessionState);
}
else {
debug(TAG, '[TAP]', `Session state did not changed: ${this.sessionStateSnapshot.name} (id=${groupId})`);
}
return this.sessionStateSnapshot;
});
}
// private _isLoggedIn(): boolean {
// return (
// this.sessionStateSnapshot != undefined &&
// this.sessionStateSnapshot.name !==
// INITIAL_SESSION_STATE.name
// );
// }
/**
* Change password for given user. If userIdOrName is not set, it will change password for the currenty
* connected user.
* @param newPassword
* @param userIdOrUndefined user identifier for which password will be change
*/
changePassword(newPassword, userIdOrUndefined) {
return __awaiter(this, void 0, void 0, function* () {
// When tap will be able to change password by username, we will be able to pass directly the username instead of the userId
let userId;
// let username: string;
if (userIdOrUndefined === undefined) {
userId = (yield this.service.interface.getCurrentGroupId()).body();
// username = (await this.service.group.getName(userId)).body();
}
else {
// TODO for now we cannot get the user id from the username
// if (typeof userIdOrName === "string") {
// username = userIdOrName;
// userId = (await this.service.group.getUserIdFromName(username)).body();
// }
// else {
userId = userIdOrUndefined;
// username = (await this.service.group.getName(userId)).body();
// }
}
const auth = yield this.setupTapAuthEngineIfRequired();
yield auth.changePassword(newPassword, userId);
const event = {
type: 'tap-user-password-change',
user: {
id: userId,
// username,
},
};
this.tap.notifyEvent(event);
});
}
setupTapAuthEngineIfRequired() {
return __awaiter(this, void 0, void 0, function* () {
if (!this._auth) {
this._auth = yield this.setupTapAuthEngine();
}
return this._auth;
});
}
setupTapAuthEngine() {
return __awaiter(this, void 0, void 0, function* () {
const securityOptions = (yield this.service.interface.getSecurityOptions()).body();
debug(TAG, `securityOptions ${JSON.stringify(securityOptions)}`);
if (securityOptions.scramActivated) {
debug(TAG, 'Setup with ScramAuth');
return new ScramAuth(this.tap);
}
else {
debug(TAG, 'Setup with BasicAuth');
return new BasicAuth(this.tap, {
hashPassword: securityOptions.hashPassword,
});
}
});
}
}
const ɵ0 = (context) => new TapAuth(context.tap);
const _TAP_EXTENSION_AUTH_ = defineTapPropertyExtension('auth', ɵ0);
// export interface TapEncryptionInterface {
// start();
// stop();
// }
/**
* Generated bundle index. Do not edit.
*/
export { BasicAuth, DEFAULT_SCRAM_SALT_GENERATOR, INITIAL_SESSION_STATE, ScramAuth, TapAuth, TapAuthError, TapScramError, _TAP_EXTENSION_AUTH_, _TAP_EXTENSION_ENCRYPTION_, hashLoginPassword, PASSWORD_LENGTH as ɵa, XOR as ɵb };
//# sourceMappingURL=iotize-tap-auth.js.map