mirror of
https://github.com/signalapp/libsignal.git
synced 2024-09-20 03:52:17 +02:00
bridge for username links
This commit is contained in:
parent
e50bec648f
commit
ca262db5ec
@ -5,6 +5,9 @@
|
||||
|
||||
package org.signal.libsignal.usernames;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
import junit.framework.TestCase;
|
||||
import org.signal.libsignal.protocol.util.Hex;
|
||||
|
||||
@ -94,4 +97,44 @@ public class UsernamesTest extends TestCase {
|
||||
}
|
||||
}
|
||||
|
||||
public void testUsernameLinkHappyCase() throws BaseUsernameException {
|
||||
final Username expectedUsername = new Username("hello_signal.42");
|
||||
final Username.UsernameLink link = expectedUsername.generateLink();
|
||||
final Username actualUsername = Username.fromLink(link);
|
||||
assertEquals(expectedUsername.getUsername(), actualUsername.getUsername());
|
||||
}
|
||||
|
||||
public void testCreateLinkFailsForLongUsername() throws BaseUsernameException {
|
||||
final String longUsername = Stream.generate(() -> "a")
|
||||
.limit(128)
|
||||
.collect(Collectors.joining());
|
||||
try {
|
||||
new Username(longUsername).generateLink();
|
||||
fail("Expected to fail creating a link for a long username");
|
||||
} catch (BaseUsernameException ex) {
|
||||
// this is fine
|
||||
}
|
||||
}
|
||||
|
||||
public void testDecryptUsernameFromLinkFailsForInvalidEntropySize() throws BaseUsernameException {
|
||||
final byte[] entropy = new byte[16];
|
||||
final byte[] encryptedUsername = new byte[32];
|
||||
try {
|
||||
Username.fromLink(new Username.UsernameLink(entropy, encryptedUsername));
|
||||
fail("Expected to fail decrypting username link with an invalid entropy size");
|
||||
} catch (BaseUsernameException ex) {
|
||||
// this is fine
|
||||
}
|
||||
}
|
||||
|
||||
public void testDecryptUsernameFromLinkFailsForInvalidEncryptedUsername() throws BaseUsernameException {
|
||||
final byte[] entropy = new byte[32];
|
||||
final byte[] encryptedUsername = new byte[32];
|
||||
try {
|
||||
Username.fromLink(new Username.UsernameLink(entropy, encryptedUsername));
|
||||
fail("Expected to fail decrypting username link with an invalid link data");
|
||||
} catch (BaseUsernameException ex) {
|
||||
// this is fine
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -523,6 +523,9 @@ public final class Native {
|
||||
public static native byte[] UnidentifiedSenderMessageContent_GetSerialized(long obj);
|
||||
public static native long UnidentifiedSenderMessageContent_New(CiphertextMessage message, long sender, int contentHint, byte[] groupId);
|
||||
|
||||
public static native byte[] UsernameLink_Create(String username);
|
||||
public static native String UsernameLink_DecryptUsername(byte[] entropy, byte[] encryptedUsername);
|
||||
|
||||
public static native String Username_CandidatesFrom(String nickname, int minLen, int maxLen);
|
||||
public static native byte[] Username_Hash(String username);
|
||||
public static native byte[] Username_Proof(String username, byte[] randomness);
|
||||
|
@ -4,6 +4,8 @@
|
||||
//
|
||||
package org.signal.libsignal.usernames;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import org.signal.libsignal.internal.Native;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
@ -12,16 +14,50 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public final class Username {
|
||||
private String value;
|
||||
private byte[] hash;
|
||||
private final String username;
|
||||
private final byte[] hash;
|
||||
|
||||
public static class UsernameLink {
|
||||
private final byte[] entropy;
|
||||
private final byte[] encryptedUsername;
|
||||
|
||||
public UsernameLink(final byte[] entropy, final byte[] encryptedUsername) {
|
||||
this.entropy = Objects.requireNonNull(entropy, "entropy");
|
||||
this.encryptedUsername = Objects.requireNonNull(encryptedUsername, "encryptedUsername");
|
||||
}
|
||||
|
||||
public byte[] getEntropy() {
|
||||
return entropy;
|
||||
}
|
||||
|
||||
public byte[] getEncryptedUsername() {
|
||||
return encryptedUsername;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
final UsernameLink that = (UsernameLink) o;
|
||||
return Arrays.equals(entropy, that.entropy) &&
|
||||
Arrays.equals(encryptedUsername, that.encryptedUsername);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Arrays.hashCode(entropy);
|
||||
result = 31 * result + Arrays.hashCode(encryptedUsername);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public Username(String username) throws BaseUsernameException {
|
||||
this.value = username;
|
||||
this.username = Objects.requireNonNull(username, "username");
|
||||
this.hash = hash(username);
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return this.value;
|
||||
return this.username;
|
||||
}
|
||||
|
||||
public byte[] getHash() {
|
||||
@ -37,6 +73,11 @@ public final class Username {
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Username fromLink(final UsernameLink usernameLink) throws BaseUsernameException {
|
||||
final String username = Native.UsernameLink_DecryptUsername(usernameLink.getEntropy(), usernameLink.getEncryptedUsername());
|
||||
return new Username(username);
|
||||
}
|
||||
|
||||
public byte[] generateProof() throws BaseUsernameException {
|
||||
byte[] randomness = new byte[32];
|
||||
SecureRandom r = new SecureRandom();
|
||||
@ -45,7 +86,14 @@ public final class Username {
|
||||
}
|
||||
|
||||
public byte[] generateProofWithRandomness(byte[] randomness) throws BaseUsernameException {
|
||||
return Native.Username_Proof(this.value, randomness);
|
||||
return Native.Username_Proof(this.username, randomness);
|
||||
}
|
||||
|
||||
public UsernameLink generateLink() throws BaseUsernameException {
|
||||
final byte[] bytes = Native.UsernameLink_Create(username);
|
||||
final byte[] entropy = Arrays.copyOfRange(bytes, 0, 32);
|
||||
final byte[] enctyptedUsername = Arrays.copyOfRange(bytes, 32, bytes.length);
|
||||
return new UsernameLink(entropy, enctyptedUsername);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@ -70,6 +118,19 @@ public final class Username {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.value;
|
||||
return this.username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
final Username username1 = (Username) o;
|
||||
return username.equals(username1.username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return username.hashCode();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
package org.signal.libsignal.usernames;
|
||||
|
||||
public class UsernameLinkInputDataTooLong extends BaseUsernameException {
|
||||
public UsernameLinkInputDataTooLong(final String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package org.signal.libsignal.usernames;
|
||||
|
||||
public class UsernameLinkInvalidEntropyDataLength extends BaseUsernameException {
|
||||
public UsernameLinkInvalidEntropyDataLength(final String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package org.signal.libsignal.usernames;
|
||||
|
||||
public class UsernameLinkInvalidLinkData extends BaseUsernameException {
|
||||
public UsernameLinkInvalidLinkData(final String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
2
node/Native.d.ts
vendored
2
node/Native.d.ts
vendored
@ -345,6 +345,8 @@ export function UnidentifiedSenderMessageContent_GetMsgType(m: Wrapper<Unidentif
|
||||
export function UnidentifiedSenderMessageContent_GetSenderCert(m: Wrapper<UnidentifiedSenderMessageContent>): SenderCertificate;
|
||||
export function UnidentifiedSenderMessageContent_New(message: Wrapper<CiphertextMessage>, sender: Wrapper<SenderCertificate>, contentHint: number, groupId: Buffer | null): UnidentifiedSenderMessageContent;
|
||||
export function UnidentifiedSenderMessageContent_Serialize(obj: Wrapper<UnidentifiedSenderMessageContent>): Buffer;
|
||||
export function UsernameLink_Create(username: string): Buffer;
|
||||
export function UsernameLink_DecryptUsername(entropy: Buffer, encryptedUsername: Buffer): string;
|
||||
export function Username_CandidatesFrom(nickname: string, minLen: number, maxLen: number): string;
|
||||
export function Username_Hash(username: string): Buffer;
|
||||
export function Username_Proof(username: string, randomness: Buffer): Buffer;
|
||||
|
@ -28,6 +28,10 @@ export enum ErrorCode {
|
||||
|
||||
InvalidMediaInput,
|
||||
UnsupportedMediaInput,
|
||||
|
||||
InputDataTooLong,
|
||||
InvalidEntropyDataLength,
|
||||
InvalidUsernameLinkEncryptedData,
|
||||
}
|
||||
|
||||
export class LibSignalErrorBase extends Error {
|
||||
@ -135,6 +139,18 @@ export type NicknameTooLongError = LibSignalErrorCommon & {
|
||||
code: ErrorCode.NicknameTooLong;
|
||||
};
|
||||
|
||||
export type InputDataTooLong = LibSignalErrorCommon & {
|
||||
code: ErrorCode.InputDataTooLong;
|
||||
};
|
||||
|
||||
export type InvalidEntropyDataLength = LibSignalErrorCommon & {
|
||||
code: ErrorCode.InvalidEntropyDataLength;
|
||||
};
|
||||
|
||||
export type InvalidUsernameLinkEncryptedData = LibSignalErrorCommon & {
|
||||
code: ErrorCode.InvalidUsernameLinkEncryptedData;
|
||||
};
|
||||
|
||||
export type IoError = LibSignalErrorCommon & {
|
||||
code: ErrorCode.IoError;
|
||||
};
|
||||
@ -162,6 +178,9 @@ export type LibSignalError =
|
||||
| BadNicknameCharacterError
|
||||
| NicknameTooShortError
|
||||
| NicknameTooLongError
|
||||
| InputDataTooLong
|
||||
| InvalidEntropyDataLength
|
||||
| InvalidUsernameLinkEncryptedData
|
||||
| IoError
|
||||
| InvalidMediaInputError
|
||||
| UnsupportedMediaInputError;
|
||||
|
@ -7,6 +7,7 @@ import { assert, expect } from 'chai';
|
||||
import { ErrorCode, LibSignalErrorBase } from '../Errors';
|
||||
import * as usernames from '../usernames';
|
||||
import * as util from './util';
|
||||
import { UsernameLink } from '../usernames';
|
||||
|
||||
util.initLogger();
|
||||
|
||||
@ -84,4 +85,37 @@ describe('usernames', () => {
|
||||
.with.property('code', ErrorCode.BadNicknameCharacter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('link', () => {
|
||||
it('works end to end with valid data', () => {
|
||||
const expectedUsername = 'signal_test.42';
|
||||
const usernameLinkData = usernames.createUsernameLink(expectedUsername);
|
||||
const actualUsername = usernameLinkData.decryptUsername();
|
||||
assert.equal(expectedUsername, actualUsername);
|
||||
});
|
||||
it('will error on too long input data', () => {
|
||||
const longUsername = 'a'.repeat(128);
|
||||
expect(() => usernames.createUsernameLink(longUsername))
|
||||
.throws(LibSignalErrorBase)
|
||||
.with.property('code', ErrorCode.InputDataTooLong);
|
||||
});
|
||||
it('will error on invalid entropy data size', () => {
|
||||
const entropy = Buffer.alloc(16);
|
||||
const encryptedUsername = Buffer.alloc(32);
|
||||
expect(() =>
|
||||
new UsernameLink(entropy, encryptedUsername).decryptUsername()
|
||||
)
|
||||
.throws(LibSignalErrorBase)
|
||||
.with.property('code', ErrorCode.InvalidEntropyDataLength);
|
||||
});
|
||||
it('will error on invalid encrypted username data', () => {
|
||||
const entropy = Buffer.alloc(32);
|
||||
const encryptedUsername = Buffer.alloc(32);
|
||||
expect(() =>
|
||||
new UsernameLink(entropy, encryptedUsername).decryptUsername()
|
||||
)
|
||||
.throws(LibSignalErrorBase)
|
||||
.with.property('code', ErrorCode.InvalidUsernameLinkEncryptedData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -37,6 +37,30 @@ export function generateProofWithRandom(
|
||||
return Native.Username_Proof(username, random);
|
||||
}
|
||||
|
||||
export class UsernameLink {
|
||||
readonly _entropy: Buffer;
|
||||
readonly _encryptedUsername: Buffer;
|
||||
|
||||
constructor(entropy: Buffer, encryptedUsername: Buffer) {
|
||||
this._entropy = entropy;
|
||||
this._encryptedUsername = encryptedUsername;
|
||||
}
|
||||
|
||||
decryptUsername(): string {
|
||||
return Native.UsernameLink_DecryptUsername(
|
||||
this._entropy,
|
||||
this._encryptedUsername
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function createUsernameLink(username: string): UsernameLink {
|
||||
const usernameLinkData = Native.UsernameLink_Create(username);
|
||||
const entropy = usernameLinkData.slice(0, 32);
|
||||
const encryptedUsername = usernameLinkData.slice(32);
|
||||
return new UsernameLink(entropy, encryptedUsername);
|
||||
}
|
||||
|
||||
// Only for testing. Will throw on failure.
|
||||
export function verifyProof(proof: Buffer, hash: Buffer): void {
|
||||
Native.Username_Verify(proof, hash);
|
||||
|
@ -10,7 +10,7 @@ use libsignal_bridge::ffi::*;
|
||||
use libsignal_protocol::*;
|
||||
use signal_crypto::Error as SignalCryptoError;
|
||||
use signal_pin::Error as PinError;
|
||||
use usernames::UsernameError;
|
||||
use usernames::{UsernameError, UsernameLinkError};
|
||||
use zkgroup::{ZkGroupDeserializationFailure, ZkGroupVerificationFailure};
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -64,6 +64,9 @@ pub enum SignalErrorCode {
|
||||
UsernameTooShort = 125,
|
||||
UsernameTooLong = 126,
|
||||
|
||||
UsernameLinkInvalidEntropyDataLength = 127,
|
||||
UsernameLinkInvalid = 128,
|
||||
|
||||
IoError = 130,
|
||||
#[allow(dead_code)]
|
||||
InvalidMediaInput = 131,
|
||||
@ -231,7 +234,8 @@ impl From<&SignalFfiError> for SignalErrorCode {
|
||||
SignalErrorCode::UsernameTooShort
|
||||
}
|
||||
|
||||
SignalFfiError::UsernameError(UsernameError::NicknameTooLong) => {
|
||||
SignalFfiError::UsernameError(UsernameError::NicknameTooLong)
|
||||
| SignalFfiError::UsernameLinkError(UsernameLinkError::InputDataTooLong) => {
|
||||
SignalErrorCode::UsernameTooLong
|
||||
}
|
||||
|
||||
@ -239,6 +243,12 @@ impl From<&SignalFfiError> for SignalErrorCode {
|
||||
SignalErrorCode::VerificationFailure
|
||||
}
|
||||
|
||||
SignalFfiError::UsernameLinkError(UsernameLinkError::InvalidEntropyDataLength) => {
|
||||
SignalErrorCode::UsernameLinkInvalidEntropyDataLength
|
||||
}
|
||||
|
||||
SignalFfiError::UsernameLinkError(_) => SignalErrorCode::UsernameLinkInvalid,
|
||||
|
||||
SignalFfiError::Io(_) => SignalErrorCode::IoError,
|
||||
|
||||
#[cfg(feature = "signal-media")]
|
||||
|
@ -334,6 +334,13 @@ impl<T: ResultTypeInfo> ResultTypeInfo for Result<T, usernames::UsernameError> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ResultTypeInfo> ResultTypeInfo for Result<T, usernames::UsernameLinkError> {
|
||||
type ResultType = T::ResultType;
|
||||
fn convert_into(self) -> SignalFfiResult<Self::ResultType> {
|
||||
T::convert_into(self?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Allocates and returns a new Rust-owned C string.
|
||||
impl ResultTypeInfo for String {
|
||||
type ResultType = *const libc::c_char;
|
||||
|
@ -13,7 +13,7 @@ use device_transfer::Error as DeviceTransferError;
|
||||
use libsignal_protocol::*;
|
||||
use signal_crypto::Error as SignalCryptoError;
|
||||
use signal_pin::Error as PinError;
|
||||
use usernames::UsernameError;
|
||||
use usernames::{UsernameError, UsernameLinkError};
|
||||
use zkgroup::{ZkGroupDeserializationFailure, ZkGroupVerificationFailure};
|
||||
|
||||
use crate::support::describe_panic;
|
||||
@ -32,6 +32,7 @@ pub enum SignalFfiError {
|
||||
ZkGroupVerificationFailure(ZkGroupVerificationFailure),
|
||||
ZkGroupDeserializationFailure(ZkGroupDeserializationFailure),
|
||||
UsernameError(UsernameError),
|
||||
UsernameLinkError(UsernameLinkError),
|
||||
Io(IoError),
|
||||
#[cfg(feature = "signal-media")]
|
||||
MediaSanitizeParse(signal_media::sanitize::ParseErrorReport),
|
||||
@ -60,6 +61,7 @@ impl fmt::Display for SignalFfiError {
|
||||
SignalFfiError::ZkGroupVerificationFailure(e) => write!(f, "{}", e),
|
||||
SignalFfiError::ZkGroupDeserializationFailure(e) => write!(f, "{}", e),
|
||||
SignalFfiError::UsernameError(e) => write!(f, "{}", e),
|
||||
SignalFfiError::UsernameLinkError(e) => write!(f, "{}", e),
|
||||
SignalFfiError::Io(e) => write!(f, "IO error: {}", e),
|
||||
#[cfg(feature = "signal-media")]
|
||||
SignalFfiError::MediaSanitizeParse(e) => {
|
||||
@ -128,6 +130,12 @@ impl From<UsernameError> for SignalFfiError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UsernameLinkError> for SignalFfiError {
|
||||
fn from(e: UsernameLinkError) -> SignalFfiError {
|
||||
SignalFfiError::UsernameLinkError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IoError> for SignalFfiError {
|
||||
fn from(e: IoError) -> SignalFfiError {
|
||||
Self::Io(e)
|
||||
|
@ -832,6 +832,16 @@ impl<T: ResultTypeInfo> ResultTypeInfo for Result<T, usernames::UsernameError> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ResultTypeInfo> ResultTypeInfo for Result<T, usernames::UsernameLinkError> {
|
||||
type ResultType = T::ResultType;
|
||||
fn convert_into(self, env: &JNIEnv) -> SignalJniResult<Self::ResultType> {
|
||||
T::convert_into(self?, env)
|
||||
}
|
||||
fn convert_into_jobject(signal_jni_result: &SignalJniResult<Self::ResultType>) -> JObject {
|
||||
<T as ResultTypeInfo>::convert_into_jobject(signal_jni_result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ResultTypeInfo> ResultTypeInfo for SignalJniResult<T> {
|
||||
type ResultType = T::ResultType;
|
||||
fn convert_into(self, env: &JNIEnv) -> SignalJniResult<Self::ResultType> {
|
||||
|
@ -13,7 +13,7 @@ use device_transfer::Error as DeviceTransferError;
|
||||
use libsignal_protocol::*;
|
||||
use signal_crypto::Error as SignalCryptoError;
|
||||
use signal_pin::Error as PinError;
|
||||
use usernames::UsernameError;
|
||||
use usernames::{UsernameError, UsernameLinkError};
|
||||
use zkgroup::{ZkGroupDeserializationFailure, ZkGroupVerificationFailure};
|
||||
|
||||
use crate::support::describe_panic;
|
||||
@ -32,6 +32,7 @@ pub enum SignalJniError {
|
||||
ZkGroupDeserializationFailure(ZkGroupDeserializationFailure),
|
||||
ZkGroupVerificationFailure(ZkGroupVerificationFailure),
|
||||
UsernameError(UsernameError),
|
||||
UsernameLinkError(UsernameLinkError),
|
||||
Io(IoError),
|
||||
#[cfg(feature = "signal-media")]
|
||||
MediaSanitizeParse(signal_media::sanitize::ParseErrorReport),
|
||||
@ -59,6 +60,7 @@ impl fmt::Display for SignalJniError {
|
||||
SignalJniError::ZkGroupVerificationFailure(e) => write!(f, "{}", e),
|
||||
SignalJniError::ZkGroupDeserializationFailure(e) => write!(f, "{}", e),
|
||||
SignalJniError::UsernameError(e) => write!(f, "{}", e),
|
||||
SignalJniError::UsernameLinkError(e) => write!(f, "{}", e),
|
||||
SignalJniError::Io(e) => write!(f, "{}", e),
|
||||
#[cfg(feature = "signal-media")]
|
||||
SignalJniError::MediaSanitizeParse(e) => write!(f, "{}", e),
|
||||
@ -139,6 +141,12 @@ impl From<UsernameError> for SignalJniError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UsernameLinkError> for SignalJniError {
|
||||
fn from(e: UsernameLinkError) -> Self {
|
||||
SignalJniError::UsernameLinkError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IoError> for SignalJniError {
|
||||
fn from(e: IoError) -> SignalJniError {
|
||||
Self::Io(e)
|
||||
|
@ -39,7 +39,7 @@ pub use io::*;
|
||||
mod storage;
|
||||
pub use storage::*;
|
||||
|
||||
use usernames::UsernameError;
|
||||
use usernames::{UsernameError, UsernameLinkError};
|
||||
|
||||
/// The type of boxed Rust values, as surfaced in JavaScript.
|
||||
pub type ObjectHandle = jlong;
|
||||
@ -403,6 +403,23 @@ fn throw_error(env: &JNIEnv, error: SignalJniError) {
|
||||
)
|
||||
}
|
||||
|
||||
SignalJniError::UsernameLinkError(UsernameLinkError::InputDataTooLong) => {
|
||||
jni_class_name!(org.signal.libsignal.usernames.UsernameLinkInputDataTooLong)
|
||||
}
|
||||
|
||||
SignalJniError::UsernameLinkError(UsernameLinkError::InvalidEntropyDataLength) => {
|
||||
jni_class_name!(
|
||||
org.signal
|
||||
.libsignal
|
||||
.usernames
|
||||
.UsernameLinkInvalidEntropyDataLength
|
||||
)
|
||||
}
|
||||
|
||||
SignalJniError::UsernameLinkError(_) => {
|
||||
jni_class_name!(org.signal.libsignal.usernames.UsernameLinkInvalidLinkData)
|
||||
}
|
||||
|
||||
SignalJniError::Io(_) => {
|
||||
jni_class_name!(java.io.IOException)
|
||||
}
|
||||
|
@ -226,6 +226,32 @@ impl SignalNodeError for usernames::UsernameError {
|
||||
}
|
||||
}
|
||||
|
||||
impl SignalNodeError for usernames::UsernameLinkError {
|
||||
fn throw<'a>(
|
||||
self,
|
||||
cx: &mut impl Context<'a>,
|
||||
module: Handle<'a, JsObject>,
|
||||
operation_name: &str,
|
||||
) -> JsResult<'a, JsValue> {
|
||||
let name = match &self {
|
||||
Self::InputDataTooLong => Some("InputDataTooLong"),
|
||||
Self::InvalidEntropyDataLength => Some("InvalidEntropyDataLength"),
|
||||
Self::UsernameLinkDataTooShort
|
||||
| Self::HmacMismatch
|
||||
| Self::BadCiphertext
|
||||
| Self::InvalidDecryptedDataStructure => Some("InvalidUsernameLinkEncryptedData"),
|
||||
};
|
||||
let message = self.to_string();
|
||||
match new_js_error(cx, module, name, &message, operation_name, None) {
|
||||
Some(error) => cx.throw(error),
|
||||
None => {
|
||||
// Make sure we still throw something.
|
||||
cx.throw_error(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SignalNodeError for sanitize::Error {
|
||||
fn throw<'a>(
|
||||
self,
|
||||
|
@ -9,7 +9,11 @@ use libsignal_bridge_macros::*;
|
||||
use crate::support::*;
|
||||
use crate::*;
|
||||
|
||||
use ::usernames::{NicknameLimits, Username, UsernameError};
|
||||
#[allow(unused_imports)]
|
||||
use ::usernames::{
|
||||
create_for_username, decrypt_username, NicknameLimits, Username, UsernameError,
|
||||
UsernameLinkError,
|
||||
};
|
||||
|
||||
#[bridge_fn]
|
||||
pub fn Username_Hash(username: String) -> Result<[u8; 32], UsernameError> {
|
||||
@ -41,3 +45,17 @@ pub fn Username_CandidatesFrom(
|
||||
let limits = NicknameLimits::new(min_len as usize, max_len as usize);
|
||||
Username::candidates_from(&mut rng, &nickname, limits).map(|names| names.join(","))
|
||||
}
|
||||
|
||||
#[bridge_fn]
|
||||
pub fn UsernameLink_Create(username: String) -> Result<Vec<u8>, UsernameLinkError> {
|
||||
let mut rng = rand::rngs::OsRng;
|
||||
create_for_username(&mut rng, username)
|
||||
}
|
||||
|
||||
#[bridge_fn]
|
||||
pub fn UsernameLink_DecryptUsername(
|
||||
entropy: &[u8],
|
||||
encrypted_username: &[u8],
|
||||
) -> Result<String, UsernameLinkError> {
|
||||
decrypt_username(entropy, encrypted_username)
|
||||
}
|
||||
|
@ -27,10 +27,10 @@ pub enum UsernameError {
|
||||
pub enum UsernameLinkError {
|
||||
/// The combined length of all input data is too long
|
||||
InputDataTooLong,
|
||||
/// Username link data size is too short: must contain IV, ciphertext, and HMAC
|
||||
UsernameLinkDataTooShort,
|
||||
/// Invalid size of the entropy data
|
||||
InvalidEntropyDataLength,
|
||||
/// Username link data size is too short: must contain IV, ciphertext, and HMAC
|
||||
UsernameLinkDataTooShort,
|
||||
/// HMAC on username link doesn't match the one calculated with the given entropy input
|
||||
HmacMismatch,
|
||||
/// Ciphertext in the username link can't be decrypted
|
||||
|
@ -322,6 +322,7 @@ mod test {
|
||||
Username::new(username).map(|name| name.hash()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_usernames() {
|
||||
for username in [
|
||||
|
@ -42,6 +42,8 @@ public enum SignalError: Error {
|
||||
case badNicknameCharacter(String)
|
||||
case nicknameTooShort(String)
|
||||
case nicknameTooLong(String)
|
||||
case usernameLinkInvalidEntropyDataLength(String)
|
||||
case usernameLinkInvalid(String)
|
||||
case ioError(String)
|
||||
case invalidMediaInput(String)
|
||||
case unsupportedMediaInput(String)
|
||||
@ -130,6 +132,10 @@ internal func checkError(_ error: SignalFfiErrorRef?) throws {
|
||||
throw SignalError.nicknameTooShort(errStr)
|
||||
case SignalErrorCodeUsernameTooLong:
|
||||
throw SignalError.nicknameTooLong(errStr)
|
||||
case SignalErrorCodeUsernameLinkInvalidEntropyDataLength:
|
||||
throw SignalError.usernameLinkInvalidEntropyDataLength(errStr)
|
||||
case SignalErrorCodeUsernameLinkInvalid:
|
||||
throw SignalError.usernameLinkInvalid(errStr)
|
||||
case SignalErrorCodeIoError:
|
||||
throw SignalError.ioError(errStr)
|
||||
case SignalErrorCodeInvalidMediaInput:
|
||||
|
@ -15,6 +15,24 @@ public struct Username {
|
||||
self.hash = try generateHash(self.value)
|
||||
}
|
||||
|
||||
public init<
|
||||
LinkBytes: ContiguousBytes,
|
||||
RandBytes: ContiguousBytes
|
||||
>(
|
||||
fromLink bytes: LinkBytes,
|
||||
withRandomness randomness: RandBytes
|
||||
) throws {
|
||||
let username =
|
||||
try randomness.withUnsafeBorrowedBuffer { randBuffer in
|
||||
try bytes.withUnsafeBorrowedBuffer { bytesBuffer in
|
||||
try invokeFnReturningString {
|
||||
signal_username_link_decrypt_username($0, randBuffer, bytesBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
try self.init(username)
|
||||
}
|
||||
|
||||
public func generateProof(withRandomness randomness: Randomness? = nil) -> [UInt8] {
|
||||
failOnError {
|
||||
let randomness = try randomness ?? Randomness.generate()
|
||||
@ -30,6 +48,17 @@ public struct Username {
|
||||
}
|
||||
}
|
||||
|
||||
public func createLink() throws -> ([UInt8], [UInt8]) {
|
||||
let bytes = failOnError {
|
||||
return try self.value.withCString { usernamePtr in
|
||||
try invokeFnReturningArray {
|
||||
signal_username_link_create($0, usernamePtr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return (Array(bytes[..<32]), Array(bytes[32...]))
|
||||
}
|
||||
|
||||
public static func verify(proof: [UInt8], forHash hash: [UInt8]) throws {
|
||||
try checkError(
|
||||
proof.withUnsafeBorrowedBuffer { proofPtr in
|
||||
@ -59,6 +88,8 @@ extension Username: CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
extension Username: Equatable { }
|
||||
|
||||
private func generateHash(_ s: String) throws -> [UInt8] {
|
||||
try s.withCString { strPtr in
|
||||
try invokeFnReturningFixedLengthArray {
|
||||
|
@ -166,6 +166,8 @@ typedef enum {
|
||||
SignalErrorCodeUsernameBadCharacter = 124,
|
||||
SignalErrorCodeUsernameTooShort = 125,
|
||||
SignalErrorCodeUsernameTooLong = 126,
|
||||
SignalErrorCodeUsernameLinkInvalidEntropyDataLength = 127,
|
||||
SignalErrorCodeUsernameLinkInvalid = 128,
|
||||
SignalErrorCodeIoError = 130,
|
||||
SignalErrorCodeInvalidMediaInput = 131,
|
||||
SignalErrorCodeUnsupportedMediaInput = 132,
|
||||
@ -1122,6 +1124,10 @@ SignalFfiError *signal_username_verify(SignalBorrowedBuffer proof, SignalBorrowe
|
||||
|
||||
SignalFfiError *signal_username_candidates_from(const char **out, const char *nickname, uint32_t min_len, uint32_t max_len);
|
||||
|
||||
SignalFfiError *signal_username_link_create(SignalOwnedBuffer *out, const char *username);
|
||||
|
||||
SignalFfiError *signal_username_link_decrypt_username(const char **out, SignalBorrowedBuffer entropy, SignalBorrowedBuffer encrypted_username);
|
||||
|
||||
#if defined(SIGNAL_MEDIA_SUPPORTED)
|
||||
SignalFfiError *signal_signal_media_check_available(void);
|
||||
#endif
|
||||
|
@ -78,4 +78,35 @@ class UsernameTests: TestCaseBase {
|
||||
XCTFail("Unexpected error thrown")
|
||||
}
|
||||
}
|
||||
|
||||
func testUsernameLinkWorksEndToEnd() throws {
|
||||
let original = try Username("SiGNAl.42")
|
||||
let (randomness, linkBytes) = try original.createLink()
|
||||
let recreated = try Username(fromLink: linkBytes, withRandomness: randomness)
|
||||
XCTAssertEqual(original, recreated)
|
||||
}
|
||||
|
||||
func testUsernameLinkInvalidEntropySize() throws {
|
||||
do {
|
||||
let randomness = [UInt8](repeating: 0, count: 16)
|
||||
let linkBytes = [UInt8](repeating: 0, count: 32)
|
||||
_ = try Username(fromLink: linkBytes, withRandomness: randomness)
|
||||
XCTFail("Should have failed")
|
||||
} catch SignalError.usernameLinkInvalidEntropyDataLength {
|
||||
} catch {
|
||||
XCTFail("Unexpected error thrown")
|
||||
}
|
||||
}
|
||||
|
||||
func testUsernameLinkInvalidLinkBytes() throws {
|
||||
do {
|
||||
let randomness = [UInt8](repeating: 0, count: 32)
|
||||
let linkBytes = [UInt8](repeating: 0, count: 32)
|
||||
_ = try Username(fromLink: linkBytes, withRandomness: randomness)
|
||||
XCTFail("Should have failed")
|
||||
} catch SignalError.usernameLinkInvalid {
|
||||
} catch {
|
||||
XCTFail("Unexpected error thrown")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user