UNPKG

@iotize/tap

Version:

IoTize Device client for Javascript

1,139 lines (1,123 loc) 49.3 kB
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