UNPKG

@iotize/tap

Version:

IoTize Device client for Javascript

299 lines 24.2 kB
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()); }); }; import './tap-encryption-extension'; // Imports... import '@iotize/tap/service/impl/group'; import '@iotize/tap/service/impl/interface'; import { bufferToHexString } from '@iotize/common/byte-converter'; import { KaitaiStreamWriter } from '@iotize/common/byte-stream'; import { TapError } from '@iotize/tap'; import { StringConverter } from '@iotize/tap/client/impl'; import { hmacSHA256, pbkdf2 } from '@iotize/tap/crypto'; import { isCodeError } from '@iotize/common/error'; import { debug } from './debug'; import { TapAuthError } from './tap-auth-error'; import { hashLoginPassword, XOR } from './utility'; const TAG = 'Scram'; export 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); } export 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(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, `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(this, void 0, void 0, function* () { const clientNonce = this.generateNonce(); debug(TAG, '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, 'storeKey', bufferToHexString(storedKey)); const serverKey = ScramAuth.serverKey(saltedPassword); debug(TAG, '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(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(); //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2NyYW0tYXV0aC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uL2F1dGgvc3JjL2xpYi9zY3JhbS1hdXRoLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7Ozs7OztBQUFBLE9BQU8sNEJBQTRCLENBQUM7QUFDcEMsYUFBYTtBQUNiLE9BQU8sZ0NBQWdDLENBQUM7QUFDeEMsT0FBTyxvQ0FBb0MsQ0FBQztBQUU1QyxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSwrQkFBK0IsQ0FBQztBQUNsRSxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSw0QkFBNEIsQ0FBQztBQUVoRSxPQUFPLEVBQU8sUUFBUSxFQUFFLE1BQU0sYUFBYSxDQUFDO0FBQzVDLE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUMxRCxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBTXhELE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUNuRCxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBRWhDLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUNoRCxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsR0FBRyxFQUFFLE1BQU0sV0FBVyxDQUFDO0FBRW5ELE1BQU0sR0FBRyxHQUFHLE9BQU8sQ0FBQztBQTBCcEIsTUFBTSxVQUFVLDRCQUE0QjtJQUMxQyxNQUFNLElBQUksR0FBRyxJQUFJLEtBQUssQ0FBQyxTQUFTLENBQUMsY0FBYyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxFQUFFO1FBQ2pFLE9BQU8sSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsTUFBTSxFQUFFLEdBQUcsSUFBSSxDQUFDLENBQUM7SUFDMUMsQ0FBQyxDQUFDLENBQUM7SUFDSCwwRkFB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