Make synthesizeToFile create file on a client side.
In previous setup, synthesizeToFile method relied on synthesizer service to create world readable output file. This is potential source of vulnerabilities. This change moves output file creation to the client side, and synthesizer service receives already opened file descriptor. This change may break applications that are creating files in now unaccessible locations, like /sdcard/. Bug: 8027957 Change-Id: I97351be5d2f2f8ef9aa43d0ab08c4b825ca4c22b
This commit is contained in:
committed by
Android (Google) Code Review
parent
7f7535fd25
commit
5acb33af35
@@ -20,10 +20,12 @@ import android.os.FileUtils;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.RandomAccessFile;
|
import java.io.RandomAccessFile;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Speech synthesis request that writes the audio to a WAV file.
|
* Speech synthesis request that writes the audio to a WAV file.
|
||||||
@@ -39,16 +41,19 @@ class FileSynthesisCallback extends AbstractSynthesisCallback {
|
|||||||
private static final short WAV_FORMAT_PCM = 0x0001;
|
private static final short WAV_FORMAT_PCM = 0x0001;
|
||||||
|
|
||||||
private final Object mStateLock = new Object();
|
private final Object mStateLock = new Object();
|
||||||
private final File mFileName;
|
|
||||||
private int mSampleRateInHz;
|
private int mSampleRateInHz;
|
||||||
private int mAudioFormat;
|
private int mAudioFormat;
|
||||||
private int mChannelCount;
|
private int mChannelCount;
|
||||||
private RandomAccessFile mFile;
|
|
||||||
|
private FileChannel mFileChannel;
|
||||||
|
|
||||||
|
private boolean mStarted = false;
|
||||||
private boolean mStopped = false;
|
private boolean mStopped = false;
|
||||||
private boolean mDone = false;
|
private boolean mDone = false;
|
||||||
|
|
||||||
FileSynthesisCallback(File fileName) {
|
FileSynthesisCallback(FileChannel fileChannel) {
|
||||||
mFileName = fileName;
|
mFileChannel = fileChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -63,54 +68,23 @@ class FileSynthesisCallback extends AbstractSynthesisCallback {
|
|||||||
* Must be called while holding the monitor on {@link #mStateLock}.
|
* Must be called while holding the monitor on {@link #mStateLock}.
|
||||||
*/
|
*/
|
||||||
private void cleanUp() {
|
private void cleanUp() {
|
||||||
closeFileAndWidenPermissions();
|
closeFile();
|
||||||
if (mFile != null) {
|
|
||||||
mFileName.delete();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Must be called while holding the monitor on {@link #mStateLock}.
|
* Must be called while holding the monitor on {@link #mStateLock}.
|
||||||
*/
|
*/
|
||||||
private void closeFileAndWidenPermissions() {
|
private void closeFile() {
|
||||||
try {
|
try {
|
||||||
if (mFile != null) {
|
if (mFileChannel != null) {
|
||||||
mFile.close();
|
mFileChannel.close();
|
||||||
mFile = null;
|
mFileChannel = null;
|
||||||
}
|
}
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
Log.e(TAG, "Failed to close " + mFileName + ": " + ex);
|
Log.e(TAG, "Failed to close output file descriptor", ex);
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Make the written file readable and writeable by everyone.
|
|
||||||
// This allows the app that requested synthesis to read the file.
|
|
||||||
//
|
|
||||||
// Note that the directory this file was written must have already
|
|
||||||
// been world writeable in order it to have been
|
|
||||||
// written to in the first place.
|
|
||||||
FileUtils.setPermissions(mFileName.getAbsolutePath(), 0666, -1, -1); //-rw-rw-rw
|
|
||||||
} catch (SecurityException se) {
|
|
||||||
Log.e(TAG, "Security exception setting rw permissions on : " + mFileName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether a given file exists, and deletes it if it does.
|
|
||||||
*/
|
|
||||||
private boolean maybeCleanupExistingFile(File file) {
|
|
||||||
if (file.exists()) {
|
|
||||||
Log.v(TAG, "File " + file + " exists, deleting.");
|
|
||||||
if (!file.delete()) {
|
|
||||||
Log.e(TAG, "Failed to delete " + file);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getMaxBufferSize() {
|
public int getMaxBufferSize() {
|
||||||
return MAX_AUDIO_BUFFER_SIZE;
|
return MAX_AUDIO_BUFFER_SIZE;
|
||||||
@@ -132,25 +106,20 @@ class FileSynthesisCallback extends AbstractSynthesisCallback {
|
|||||||
if (DBG) Log.d(TAG, "Request has been aborted.");
|
if (DBG) Log.d(TAG, "Request has been aborted.");
|
||||||
return TextToSpeech.ERROR;
|
return TextToSpeech.ERROR;
|
||||||
}
|
}
|
||||||
if (mFile != null) {
|
if (mStarted) {
|
||||||
cleanUp();
|
cleanUp();
|
||||||
throw new IllegalArgumentException("FileSynthesisRequest.start() called twice");
|
throw new IllegalArgumentException("FileSynthesisRequest.start() called twice");
|
||||||
}
|
}
|
||||||
|
mStarted = true;
|
||||||
if (!maybeCleanupExistingFile(mFileName)) {
|
|
||||||
return TextToSpeech.ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
mSampleRateInHz = sampleRateInHz;
|
mSampleRateInHz = sampleRateInHz;
|
||||||
mAudioFormat = audioFormat;
|
mAudioFormat = audioFormat;
|
||||||
mChannelCount = channelCount;
|
mChannelCount = channelCount;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mFile = new RandomAccessFile(mFileName, "rw");
|
mFileChannel.write(ByteBuffer.allocate(WAV_HEADER_LENGTH));
|
||||||
// Reserve space for WAV header
|
|
||||||
mFile.write(new byte[WAV_HEADER_LENGTH]);
|
|
||||||
return TextToSpeech.SUCCESS;
|
return TextToSpeech.SUCCESS;
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
Log.e(TAG, "Failed to open " + mFileName + ": " + ex);
|
Log.e(TAG, "Failed to write wav header to output file descriptor" + ex);
|
||||||
cleanUp();
|
cleanUp();
|
||||||
return TextToSpeech.ERROR;
|
return TextToSpeech.ERROR;
|
||||||
}
|
}
|
||||||
@@ -168,15 +137,15 @@ class FileSynthesisCallback extends AbstractSynthesisCallback {
|
|||||||
if (DBG) Log.d(TAG, "Request has been aborted.");
|
if (DBG) Log.d(TAG, "Request has been aborted.");
|
||||||
return TextToSpeech.ERROR;
|
return TextToSpeech.ERROR;
|
||||||
}
|
}
|
||||||
if (mFile == null) {
|
if (mFileChannel == null) {
|
||||||
Log.e(TAG, "File not open");
|
Log.e(TAG, "File not open");
|
||||||
return TextToSpeech.ERROR;
|
return TextToSpeech.ERROR;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
mFile.write(buffer, offset, length);
|
mFileChannel.write(ByteBuffer.wrap(buffer, offset, length));
|
||||||
return TextToSpeech.SUCCESS;
|
return TextToSpeech.SUCCESS;
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
Log.e(TAG, "Failed to write to " + mFileName + ": " + ex);
|
Log.e(TAG, "Failed to write to output file descriptor", ex);
|
||||||
cleanUp();
|
cleanUp();
|
||||||
return TextToSpeech.ERROR;
|
return TextToSpeech.ERROR;
|
||||||
}
|
}
|
||||||
@@ -197,21 +166,21 @@ class FileSynthesisCallback extends AbstractSynthesisCallback {
|
|||||||
if (DBG) Log.d(TAG, "Request has been aborted.");
|
if (DBG) Log.d(TAG, "Request has been aborted.");
|
||||||
return TextToSpeech.ERROR;
|
return TextToSpeech.ERROR;
|
||||||
}
|
}
|
||||||
if (mFile == null) {
|
if (mFileChannel == null) {
|
||||||
Log.e(TAG, "File not open");
|
Log.e(TAG, "File not open");
|
||||||
return TextToSpeech.ERROR;
|
return TextToSpeech.ERROR;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// Write WAV header at start of file
|
// Write WAV header at start of file
|
||||||
mFile.seek(0);
|
mFileChannel.position(0);
|
||||||
int dataLength = (int) (mFile.length() - WAV_HEADER_LENGTH);
|
int dataLength = (int) (mFileChannel.size() - WAV_HEADER_LENGTH);
|
||||||
mFile.write(
|
mFileChannel.write(
|
||||||
makeWavHeader(mSampleRateInHz, mAudioFormat, mChannelCount, dataLength));
|
makeWavHeader(mSampleRateInHz, mAudioFormat, mChannelCount, dataLength));
|
||||||
closeFileAndWidenPermissions();
|
closeFile();
|
||||||
mDone = true;
|
mDone = true;
|
||||||
return TextToSpeech.SUCCESS;
|
return TextToSpeech.SUCCESS;
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
Log.e(TAG, "Failed to write to " + mFileName + ": " + ex);
|
Log.e(TAG, "Failed to write to output file descriptor", ex);
|
||||||
cleanUp();
|
cleanUp();
|
||||||
return TextToSpeech.ERROR;
|
return TextToSpeech.ERROR;
|
||||||
}
|
}
|
||||||
@@ -226,7 +195,7 @@ class FileSynthesisCallback extends AbstractSynthesisCallback {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] makeWavHeader(int sampleRateInHz, int audioFormat, int channelCount,
|
private ByteBuffer makeWavHeader(int sampleRateInHz, int audioFormat, int channelCount,
|
||||||
int dataLength) {
|
int dataLength) {
|
||||||
// TODO: is AudioFormat.ENCODING_DEFAULT always the same as ENCODING_PCM_16BIT?
|
// TODO: is AudioFormat.ENCODING_DEFAULT always the same as ENCODING_PCM_16BIT?
|
||||||
int sampleSizeInBytes = (audioFormat == AudioFormat.ENCODING_PCM_8BIT ? 1 : 2);
|
int sampleSizeInBytes = (audioFormat == AudioFormat.ENCODING_PCM_8BIT ? 1 : 2);
|
||||||
@@ -251,8 +220,9 @@ class FileSynthesisCallback extends AbstractSynthesisCallback {
|
|||||||
header.putShort(bitsPerSample);
|
header.putShort(bitsPerSample);
|
||||||
header.put(new byte[]{ 'd', 'a', 't', 'a' });
|
header.put(new byte[]{ 'd', 'a', 't', 'a' });
|
||||||
header.putInt(dataLength);
|
header.putInt(dataLength);
|
||||||
|
header.flip();
|
||||||
|
|
||||||
return headerBuf;
|
return header;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package android.speech.tts;
|
|||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.speech.tts.ITextToSpeechCallback;
|
import android.speech.tts.ITextToSpeechCallback;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,11 +45,12 @@ interface ITextToSpeechService {
|
|||||||
* @param callingInstance a binder representing the identity of the calling
|
* @param callingInstance a binder representing the identity of the calling
|
||||||
* TextToSpeech object.
|
* TextToSpeech object.
|
||||||
* @param text The text to synthesize.
|
* @param text The text to synthesize.
|
||||||
* @param filename The file to write the synthesized audio to.
|
* @param fileDescriptor The file descriptor to write the synthesized audio to. Has to be
|
||||||
|
writable.
|
||||||
* @param param Request parameters.
|
* @param param Request parameters.
|
||||||
*/
|
*/
|
||||||
int synthesizeToFile(in IBinder callingInstance, in String text,
|
int synthesizeToFileDescriptor(in IBinder callingInstance, in String text,
|
||||||
in String filename, in Bundle params);
|
in ParcelFileDescriptor fileDescriptor, in Bundle params);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plays an existing audio resource.
|
* Plays an existing audio resource.
|
||||||
|
|||||||
@@ -27,11 +27,15 @@ import android.net.Uri;
|
|||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@@ -1225,8 +1229,29 @@ public class TextToSpeech {
|
|||||||
return runAction(new Action<Integer>() {
|
return runAction(new Action<Integer>() {
|
||||||
@Override
|
@Override
|
||||||
public Integer run(ITextToSpeechService service) throws RemoteException {
|
public Integer run(ITextToSpeechService service) throws RemoteException {
|
||||||
return service.synthesizeToFile(getCallerIdentity(), text, filename,
|
ParcelFileDescriptor fileDescriptor;
|
||||||
getParams(params));
|
int returnValue;
|
||||||
|
try {
|
||||||
|
File file = new File(filename);
|
||||||
|
if(file.exists() && !file.canWrite()) {
|
||||||
|
Log.e(TAG, "Can't write to " + filename);
|
||||||
|
return ERROR;
|
||||||
|
}
|
||||||
|
fileDescriptor = ParcelFileDescriptor.open(file,
|
||||||
|
ParcelFileDescriptor.MODE_WRITE_ONLY |
|
||||||
|
ParcelFileDescriptor.MODE_CREATE |
|
||||||
|
ParcelFileDescriptor.MODE_TRUNCATE);
|
||||||
|
returnValue = service.synthesizeToFileDescriptor(getCallerIdentity(), text,
|
||||||
|
fileDescriptor, getParams(params));
|
||||||
|
fileDescriptor.close();
|
||||||
|
return returnValue;
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
Log.e(TAG, "Opening file " + filename + " failed", e);
|
||||||
|
return ERROR;
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Closing file " + filename + " failed", e);
|
||||||
|
return ERROR;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, ERROR, "synthesizeToFile");
|
}, ERROR, "synthesizeToFile");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import android.os.IBinder;
|
|||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
import android.os.MessageQueue;
|
import android.os.MessageQueue;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.os.RemoteCallbackList;
|
import android.os.RemoteCallbackList;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
@@ -33,7 +34,8 @@ import android.speech.tts.TextToSpeech.Engine;
|
|||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.FileDescriptor;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -654,19 +656,19 @@ public abstract class TextToSpeechService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SynthesisToFileSpeechItem extends SynthesisSpeechItem {
|
private class SynthesisToFileSpeechDescriptorItem extends SynthesisSpeechItem {
|
||||||
private final File mFile;
|
private final FileDescriptor mFileDescriptor;
|
||||||
|
|
||||||
public SynthesisToFileSpeechItem(Object callerIdentity, int callerUid, int callerPid,
|
public SynthesisToFileSpeechDescriptorItem(Object callerIdentity, int callerUid,
|
||||||
Bundle params, String text,
|
int callerPid, Bundle params, String text, FileDescriptor fileDescriptor) {
|
||||||
File file) {
|
|
||||||
super(callerIdentity, callerUid, callerPid, params, text);
|
super(callerIdentity, callerUid, callerPid, params, text);
|
||||||
mFile = file;
|
mFileDescriptor = fileDescriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected AbstractSynthesisCallback createSynthesisCallback() {
|
protected AbstractSynthesisCallback createSynthesisCallback() {
|
||||||
return new FileSynthesisCallback(mFile);
|
FileOutputStream fileOutputStream = new FileOutputStream(mFileDescriptor);
|
||||||
|
return new FileSynthesisCallback(fileOutputStream.getChannel());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -797,15 +799,15 @@ public abstract class TextToSpeechService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int synthesizeToFile(IBinder caller, String text, String filename,
|
public int synthesizeToFileDescriptor(IBinder caller, String text, ParcelFileDescriptor
|
||||||
Bundle params) {
|
fileDescriptor, Bundle params) {
|
||||||
if (!checkNonNull(caller, text, filename, params)) {
|
if (!checkNonNull(caller, text, fileDescriptor, params)) {
|
||||||
return TextToSpeech.ERROR;
|
return TextToSpeech.ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
File file = new File(filename);
|
SpeechItem item = new SynthesisToFileSpeechDescriptorItem(caller, Binder.getCallingUid(),
|
||||||
SpeechItem item = new SynthesisToFileSpeechItem(caller, Binder.getCallingUid(),
|
Binder.getCallingPid(), params, text,
|
||||||
Binder.getCallingPid(), params, text, file);
|
fileDescriptor.getFileDescriptor());
|
||||||
return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item);
|
return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user