@iotize/tap
Version:
IoTize Device client for Javascript
299 lines • 24.2 kB
JavaScript
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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